Add viewer route

This commit is contained in:
Leonardo Murça 2025-06-04 15:31:53 -03:00
parent 3c55ab88e4
commit 984d99e0e2
24 changed files with 5448 additions and 2924 deletions

5770
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,37 +1,37 @@
{
"name": "embroidery-viewer",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.9.1",
"globals": "^16.0.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.6"
},
"dependencies": {
"accept-language-parser": "^1.5.0",
"sveltekit-i18n": "^2.4.2"
}
"name": "embroidery-viewer",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.9.1",
"globals": "^16.0.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.6"
},
"dependencies": {
"accept-language-parser": "^1.5.0",
"sveltekit-i18n": "^2.4.2"
}
}

18
src/lib/assets/upload.svg Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="68.965652mm"
height="68.948975mm"
viewBox="0 0 68.965652 68.948975"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-78.581248,-114.14203)"><path
d="m 101.71922,133.815 7.89657,-7.93103 v 33.06897 c 0,4.59771 6.89657,4.59771 6.89657,0 v -33.06897 l 7.89657,7.93103 c 1.34889,1.35999 3.54767,1.35999 4.89656,0 1.36001,-1.34889 1.36001,-3.5477 0,-4.89659 l -13.79313,-13.79308 c -0.32789,-0.31411 -0.71459,-0.56036 -1.1379,-0.72463 -0.83953,-0.34489 -1.78117,-0.34489 -2.6207,0 -0.42331,0.16427 -0.81001,0.41052 -1.1379,0.72463 l -13.793128,13.79308 c -3.265094,3.26437 1.631464,8.16096 4.896558,4.89659 z m 42.3794,14.7931 c -1.90447,0 -3.44833,1.5439 -3.44828,3.44837 v 20.68969 c -2e-5,1.90442 -1.54386,3.44824 -3.44828,3.44824 H 88.926098 c -1.904415,0 -3.448254,-1.54382 -3.448278,-3.44824 v -20.68969 c 0,-4.59771 -6.89657,-4.59771 -6.89657,0 v 20.68969 c -10e-7,5.7133 4.631545,10.34485 10.344848,10.34485 h 48.275962 c 5.7133,0 10.34485,-4.63155 10.34485,-10.34485 v -20.68969 c 5e-5,-1.90447 -1.54382,-3.44837 -3.44829,-3.44837 z"
id="path1"
style="opacity:1;mix-blend-mode:normal;fill:#06345f;fill-opacity:1;stroke-width:3.448;stroke-dasharray:none" /></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,143 @@
<script>
import { t } from '$lib/translations';
import renderFileToCanvas from '$lib/file-renderer';
export let files = [];
let canvasRefs = [];
let colorRefs = [];
let stitchesRefs = [];
let sizeRefs = [];
let errorMessageRef;
let localizedStrings = {
stitches: $t('viewer.stitches'),
dimensions: $t('viewer.dimensions'),
};
const downloadCanvasAsImage = (canvas, filename) => {
const image = canvas
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream');
const link = document.createElement('a');
link.download = `${filename.split('.').slice(0, -1).join('.')}.png`;
link.href = image;
link.click();
};
const onKeydown = (evt) => {
if (evt.key === 'Enter') {
document.getElementById('download-button').click();
}
};
</script>
{#if files.length !== 0}
<div id="container" style="width: 100%; heigth: 100vh;">
{#each Array.from(files) as file, i}
<div class="canvas-container">
<canvas bind:this={canvasRefs[i]} class="canvas"></canvas>
<p><strong>{file.name}</strong></p>
<div class="stitches-container" bind:this={stitchesRefs[i]}></div>
<div class="size-container" bind:this={sizeRefs[i]}></div>
<div class="colors-container" bind:this={colorRefs[i]}></div>
<div
id="download-button"
role="button"
tabindex="0"
on:keydown={onKeydown}
on:click={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
>
{$t('viewer.download')}
</div>
</div>
{canvasRefs[i] &&
renderFileToCanvas(
file,
canvasRefs[i],
errorMessageRef,
colorRefs[i],
stitchesRefs[i],
sizeRefs[i],
localizedStrings,
)}
{/each}
<!-- svelte-ignore a11y-missing-content -->
<h1 bind:this={errorMessageRef}></h1>
</div>
{/if}
<style>
#container {
display: flex;
width: 100%;
justify-content: space-evenly;
flex-wrap: wrap;
margin-top: 50px;
}
.canvas-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 550px;
max-height: 1000px;
margin-bottom: 15px;
padding: 10px;
/* border: 2px solid black;*/
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
border-radius: 16px;
}
.canvas {
height: 70%;
width: 100%;
object-fit: contain;
}
.colors-container {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 5px;
row-gap: 5px;
padding-bottom: 15px;
}
.stitches-container {
padding: 10px 0;
}
div[role='button'] {
background-color: #05345f;
font-weight: bold;
color: white;
padding: 10px;
border-radius: 10px;
padding: 10px;
width: 50%;
text-align: center;
}
div[role='button']:hover {
cursor: pointer;
background-color: black;
color: white;
}
@media only screen and (max-device-width: 812px) {
.canvas-container {
width: 100%;
height: auto;
}
#container {
width: 100%;
}
div[role='button'] {
width: 100%;
padding: 15px;
}
}
</style>

View file

@ -0,0 +1,85 @@
<script>
import { t } from '$lib/translations';
import upload from '$lib/assets/upload.svg';
export let files;
export let supportedFormats;
export let onKeydown;
export let onClick;
export let onDrop;
export let onChange;
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
id="dropzone"
tabindex={0}
role="region"
on:keydown={onKeydown}
on:dragover|preventDefault|stopPropagation
on:drop|preventDefault|stopPropagation={onDrop}
>
<img src={upload} width="40" height="40" alt="Upload icon" />
<label id="file-label" for="file-input">{@html $t('viewer.dropzone')}</label>
<input
id="file-input"
type="file"
name="files[]"
accept={supportedFormats.join(',')}
multiple
on:change={onChange}
bind:this={files}
/>
<button on:click|preventDefault={onClick}>{$t('viewer.browse')}</button>
</div>
<style>
#dropzone {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
border: 1px solid #d3dce6;
border-radius: 12px;
width: 100%;
padding: 15px;
z-index: 10;
}
#file-label {
z-index: -1;
margin-top: 10px;
}
#file-input {
display: none;
}
button {
margin-top: 20px;
padding: 12px 24px;
background-color: #06345f;
color: white;
border: none;
border-radius: 10px;
font-size: 1rem;
cursor: pointer;
width: 60%;
font-weight: bold;
}
button:hover {
color: #fff;
background-color: #000;
}
@media only screen and (max-device-width: 812px) {
#dropzone {
width: 100%;
}
button {
width: 100%;
}
}
</style>

View file

@ -0,0 +1,63 @@
<script>
export let title;
export let files = [];
export let isError = false;
</script>
{#if files.length !== 0}
<div id="selected-files-container">
<h2>{title}:</h2>
<div id="files-list">
{#each Array.from(files) as file}
<div id={isError ? 'selected-file-card-error' : 'selected-file-card'}>
<span>{file.name}</span>
<span>{Math.round(file.size / 1000)} KB</span>
</div>
{/each}
</div>
</div>
{/if}
<style>
#selected-files-container {
text-align: center;
}
#files-list {
display: flex;
flex-direction: column;
align-items: center;
}
#selected-file-card {
display: flex;
justify-content: space-between;
color: #06345f;
font-weight: bolder;
width: 500px;
padding-left: 15px;
margin-top: 10px;
}
#selected-file-card-error {
display: flex;
justify-content: space-between;
color: #06345f;
font-weight: bolder;
width: 500px;
padding-left: 15px;
margin-top: 10px;
color: red;
}
@media only screen and (max-device-width: 812px) {
#selected-files-container,
#selected-file-card-error {
width: 100%;
}
#selected-file-card {
width: 100%;
}
}
</style>

View file

@ -0,0 +1,141 @@
import { supportedFormats } from '$lib/format-readers';
import { jDataView } from './jdataview';
import { Pattern } from './pattern';
/**
* Render the embroidery pattern file to the provided canvas and update views.
* @param {string} filename - The name of the file.
* @param {ProgressEvent<FileReader>} evt - The file load event.
* @param {HTMLCanvasElement} canvas - Canvas to render the pattern.
* @param {HTMLElement} colorView - Element to display colors.
* @param {HTMLElement} stitchesView - Element to display stitch count.
* @param {HTMLElement} sizeView - Element to display size.
* @param {{stitches: string, dimensions: string}} localizedStrings - Localized labels.
*/
function renderFile(
filename,
evt,
canvas,
colorView,
stitchesView,
sizeView,
localizedStrings,
) {
const fileExtension = filename.toLowerCase().split('.').pop();
const arrayBuffer = evt.target?.result;
if (!(fileExtension && arrayBuffer)) {
throw new Error('Invalid file extension or file data');
}
const view = new jDataView(arrayBuffer, 0, evt.total || 0);
const pattern = new Pattern();
const formatReader = supportedFormats[fileExtension];
if (!formatReader || typeof formatReader.read !== 'function') {
throw new Error(`Unsupported file format: ${fileExtension}`);
}
// @ts-ignore
formatReader.read(view, pattern);
pattern.moveToPositive();
pattern.drawShapeTo(canvas);
pattern.drawColorsTo(colorView);
pattern.drawStitchesCountTo(stitchesView, localizedStrings.stitches);
pattern.drawSizeValuesTo(sizeView, localizedStrings.dimensions);
}
/**
* Display a generic abort message.
* @param {HTMLElement} errorMessageRef - Element to display the message.
*/
function renderAbortMessage(errorMessageRef) {
errorMessageRef.textContent = 'Render aborted!';
}
/**
* Display a detailed error message based on error type.
* @param {string} errorName - The name of the error.
* @param {HTMLElement} errorMessageRef - Element to display the message.
*/
function renderErrorMessage(errorName, errorMessageRef) {
/** @type {string} */
let message;
switch (errorName) {
case 'NotFoundError':
message =
'The file could not be found at the time the read was processed.';
break;
case 'SecurityError':
message =
'<p>A file security error occurred. This can be due to:</p>' +
'<ul>' +
'<li>Accessing certain files deemed unsafe for Web applications.</li>' +
'<li>Performing too many read calls on file resources.</li>' +
'<li>The file has changed on disk since the user selected it.</li>' +
'</ul>';
break;
case 'NotReadableError':
message =
'The file cannot be read. This can occur if the file is open in another application.';
break;
case 'EncodingError':
message = 'The length of the data URL for the file is too long.';
break;
default:
message = 'Something went wrong!';
break;
}
errorMessageRef.innerHTML = message;
}
/**
* Read a file and render its pattern to canvas with error handling.
* @param {File} fileObject - The file to read.
* @param {HTMLCanvasElement} canvas - The canvas to render on.
* @param {HTMLElement} errorMessageRef - Element to show error messages.
* @param {HTMLElement} colorView - Element to display colors.
* @param {HTMLElement} stitchesView - Element to display stitch count.
* @param {HTMLElement} sizeView - Element to display size.
* @param {{stitches: string, dimensions: string}} localizedStrings - Localized strings.
* @returns {string} Empty string after starting file read.
*/
export default function renderFileToCanvas(
fileObject,
canvas,
errorMessageRef,
colorView,
stitchesView,
sizeView,
localizedStrings,
) {
const reader = new FileReader();
reader.onloadend = (evt) =>
renderFile(
fileObject.name,
evt,
canvas,
colorView,
stitchesView,
sizeView,
localizedStrings,
);
reader.onabort = () => renderAbortMessage(errorMessageRef);
reader.onerror = (evt) =>
renderErrorMessage(
// @ts-ignore
evt.target.error?.name || 'UnknownError',
errorMessageRef,
);
if (fileObject) {
reader.readAsArrayBuffer(fileObject);
}
return '';
}

View file

@ -0,0 +1,851 @@
/* eslint-disable no-unused-vars */
/* eslint-disable no-undef */
// @ts-nocheck
import { browser } from '$app/environment';
// jDataView by Vjeux <vjeuxx@gmail.com> - Jan 2010
// Continued by RReverser <me@rreverser.com> - Feb 2013
//
// A unique way to work with a binary file in the browser
// http://github.com/jDataView/jDataView
// http://jDataView.github.io/
var compatibility = {
// NodeJS Buffer in v0.5.5 and newer
NodeBuffer: 'Buffer' in globalThis && 'readInt16LE' in Buffer.prototype,
DataView:
'DataView' in globalThis &&
('getFloat64' in DataView.prototype || // Chrome
'getFloat64' in new DataView(new ArrayBuffer(1))), // Node
ArrayBuffer: 'ArrayBuffer' in globalThis,
PixelData:
'CanvasPixelArray' in globalThis &&
'ImageData' in globalThis &&
'document' in globalThis,
};
var createPixelData = function (byteLength, buffer) {
var data = createPixelData.context2d.createImageData(
(byteLength + 3) / 4,
1,
).data;
data.byteLength = byteLength;
if (buffer !== undefined) {
for (var i = 0; i < byteLength; i++) {
data[i] = buffer[i];
}
}
return data;
};
createPixelData.context2d =
browser ?? document.createElement('canvas').getContext('2d');
var dataTypes = {
Int8: 1,
Int16: 2,
Int32: 4,
Uint8: 1,
Uint16: 2,
Uint32: 4,
Float32: 4,
Float64: 8,
};
var nodeNaming = {
Int8: 'Int8',
Int16: 'Int16',
Int32: 'Int32',
Uint8: 'UInt8',
Uint16: 'UInt16',
Uint32: 'UInt32',
Float32: 'Float',
Float64: 'Double',
};
function arrayFrom(arrayLike, forceCopy) {
return !forceCopy && arrayLike instanceof Array
? arrayLike
: Array.prototype.slice.call(arrayLike);
}
function defined(value, defaultValue) {
return value !== undefined ? value : defaultValue;
}
export function jDataView(buffer, byteOffset, byteLength, littleEndian) {
/* jshint validthis:true */
if (buffer instanceof jDataView) {
var result = buffer.slice(byteOffset, byteOffset + byteLength);
result._littleEndian = defined(littleEndian, result._littleEndian);
return result;
}
if (!(this instanceof jDataView)) {
return new jDataView(buffer, byteOffset, byteLength, littleEndian);
}
this.buffer = buffer = jDataView.wrapBuffer(buffer);
// Check parameters and existing functionnalities
this._isArrayBuffer =
compatibility.ArrayBuffer && buffer instanceof ArrayBuffer;
this._isPixelData =
compatibility.PixelData && buffer instanceof CanvasPixelArray;
this._isDataView = compatibility.DataView && this._isArrayBuffer;
this._isNodeBuffer = compatibility.NodeBuffer && buffer instanceof Buffer;
// Handle Type Errors
if (
!this._isNodeBuffer &&
!this._isArrayBuffer &&
!this._isPixelData &&
!(buffer instanceof Array)
) {
throw new TypeError('jDataView buffer has an incompatible type');
}
// Default Values
this._littleEndian = !!littleEndian;
var bufferLength = 'byteLength' in buffer ? buffer.byteLength : buffer.length;
this.byteOffset = byteOffset = defined(byteOffset, 0);
this.byteLength = byteLength = defined(byteLength, bufferLength - byteOffset);
if (!this._isDataView) {
this._checkBounds(byteOffset, byteLength, bufferLength);
} else {
this._view = new DataView(buffer, byteOffset, byteLength);
}
// Create uniform methods (action wrappers) for the following data types
this._engineAction = this._isDataView
? this._dataViewAction
: this._isNodeBuffer
? this._nodeBufferAction
: this._isArrayBuffer
? this._arrayBufferAction
: this._arrayAction;
}
function getCharCodes(string) {
if (compatibility.NodeBuffer) {
return new Buffer(string, 'binary');
}
var Type = compatibility.ArrayBuffer ? Uint8Array : Array,
codes = new Type(string.length);
for (var i = 0, length = string.length; i < length; i++) {
codes[i] = string.charCodeAt(i) & 0xff;
}
return codes;
}
// mostly internal function for wrapping any supported input (String or Array-like) to best suitable buffer format
jDataView.wrapBuffer = function (buffer) {
switch (typeof buffer) {
case 'number':
if (compatibility.NodeBuffer) {
buffer = new Buffer(buffer);
buffer.fill(0);
} else if (compatibility.ArrayBuffer) {
buffer = new Uint8Array(buffer).buffer;
} else if (compatibility.PixelData) {
buffer = createPixelData(buffer);
} else {
buffer = new Array(buffer);
for (var i = 0; i < buffer.length; i++) {
buffer[i] = 0;
}
}
return buffer;
case 'string':
buffer = getCharCodes(buffer);
/* falls through */
default:
if (
'length' in buffer &&
!(
(compatibility.NodeBuffer && buffer instanceof Buffer) ||
(compatibility.ArrayBuffer && buffer instanceof ArrayBuffer) ||
(compatibility.PixelData && buffer instanceof CanvasPixelArray)
)
) {
if (compatibility.NodeBuffer) {
buffer = new Buffer(buffer);
} else if (compatibility.ArrayBuffer) {
if (!(buffer instanceof ArrayBuffer)) {
buffer = new Uint8Array(buffer).buffer;
// bug in Node.js <= 0.8:
if (!(buffer instanceof ArrayBuffer)) {
buffer = new Uint8Array(arrayFrom(buffer, true)).buffer;
}
}
} else if (compatibility.PixelData) {
buffer = createPixelData(buffer.length, buffer);
} else {
buffer = arrayFrom(buffer);
}
}
return buffer;
}
};
function pow2(n) {
return n >= 0 && n < 31 ? 1 << n : pow2[n] || (pow2[n] = Math.pow(2, n));
}
// left for backward compatibility
jDataView.createBuffer = function () {
return jDataView.wrapBuffer(arguments);
};
function Uint64(lo, hi) {
this.lo = lo;
this.hi = hi;
}
jDataView.Uint64 = Uint64;
Uint64.prototype = {
valueOf: function () {
return this.lo + pow2(32) * this.hi;
},
toString: function () {
return Number.prototype.toString.apply(this.valueOf(), arguments);
},
};
Uint64.fromNumber = function (number) {
var hi = Math.floor(number / pow2(32)),
lo = number - hi * pow2(32);
return new Uint64(lo, hi);
};
function Int64(lo, hi) {
Uint64.apply(this, arguments);
}
jDataView.Int64 = Int64;
Int64.prototype =
'create' in Object ? Object.create(Uint64.prototype) : new Uint64();
Int64.prototype.valueOf = function () {
if (this.hi < pow2(31)) {
return Uint64.prototype.valueOf.apply(this, arguments);
}
return -(pow2(32) - this.lo + pow2(32) * (pow2(32) - 1 - this.hi));
};
Int64.fromNumber = function (number) {
var lo, hi;
if (number >= 0) {
var unsigned = Uint64.fromNumber(number);
lo = unsigned.lo;
hi = unsigned.hi;
} else {
hi = Math.floor(number / pow2(32));
lo = number - hi * pow2(32);
hi += pow2(32);
}
return new Int64(lo, hi);
};
jDataView.prototype = {
_offset: 0,
_bitOffset: 0,
compatibility: compatibility,
_checkBounds: function (byteOffset, byteLength, maxLength) {
// Do additional checks to simulate DataView
if (typeof byteOffset !== 'number') {
throw new TypeError('Offset is not a number.');
}
if (typeof byteLength !== 'number') {
throw new TypeError('Size is not a number.');
}
if (byteLength < 0) {
throw new RangeError('Length is negative.');
}
if (
byteOffset < 0 ||
byteOffset + byteLength > defined(maxLength, this.byteLength)
) {
throw new RangeError('Offsets are out of bounds.');
}
},
_action: function (type, isReadAction, byteOffset, littleEndian, value) {
return this._engineAction(
type,
isReadAction,
defined(byteOffset, this._offset),
defined(littleEndian, this._littleEndian),
value,
);
},
_dataViewAction: function (
type,
isReadAction,
byteOffset,
littleEndian,
value,
) {
// Move the internal offset forward
this._offset = byteOffset + dataTypes[type];
return isReadAction
? this._view['get' + type](byteOffset, littleEndian)
: this._view['set' + type](byteOffset, value, littleEndian);
},
_nodeBufferAction: function (
type,
isReadAction,
byteOffset,
littleEndian,
value,
) {
// Move the internal offset forward
this._offset = byteOffset + dataTypes[type];
var nodeName =
nodeNaming[type] +
(type === 'Int8' || type === 'Uint8' ? '' : littleEndian ? 'LE' : 'BE');
byteOffset += this.byteOffset;
return isReadAction
? this.buffer['read' + nodeName](byteOffset)
: this.buffer['write' + nodeName](value, byteOffset);
},
_arrayBufferAction: function (
type,
isReadAction,
byteOffset,
littleEndian,
value,
) {
var size = dataTypes[type],
TypedArray = globalThis[type + 'Array'],
typedArray;
littleEndian = defined(littleEndian, this._littleEndian);
// ArrayBuffer: we use a typed array of size 1 from original buffer if alignment is good and from slice when it's not
if (
size === 1 ||
((this.byteOffset + byteOffset) % size === 0 && littleEndian)
) {
typedArray = new TypedArray(this.buffer, this.byteOffset + byteOffset, 1);
this._offset = byteOffset + size;
return isReadAction ? typedArray[0] : (typedArray[0] = value);
} else {
var bytes = new Uint8Array(
isReadAction
? this.getBytes(size, byteOffset, littleEndian, true)
: size,
);
typedArray = new TypedArray(bytes.buffer, 0, 1);
if (isReadAction) {
return typedArray[0];
} else {
typedArray[0] = value;
this._setBytes(byteOffset, bytes, littleEndian);
}
}
},
_arrayAction: function (type, isReadAction, byteOffset, littleEndian, value) {
return isReadAction
? this['_get' + type](byteOffset, littleEndian)
: this['_set' + type](byteOffset, value, littleEndian);
},
// Helpers
_getBytes: function (length, byteOffset, littleEndian) {
littleEndian = defined(littleEndian, this._littleEndian);
byteOffset = defined(byteOffset, this._offset);
length = defined(length, this.byteLength - byteOffset);
this._checkBounds(byteOffset, length);
byteOffset += this.byteOffset;
this._offset = byteOffset - this.byteOffset + length;
var result = this._isArrayBuffer
? new Uint8Array(this.buffer, byteOffset, length)
: (this.buffer.slice || Array.prototype.slice).call(
this.buffer,
byteOffset,
byteOffset + length,
);
return littleEndian || length <= 1 ? result : arrayFrom(result).reverse();
},
// wrapper for external calls (do not return inner buffer directly to prevent it's modifying)
getBytes: function (length, byteOffset, littleEndian, toArray) {
var result = this._getBytes(
length,
byteOffset,
defined(littleEndian, true),
);
return toArray ? arrayFrom(result) : result;
},
_setBytes: function (byteOffset, bytes, littleEndian) {
var length = bytes.length;
// needed for Opera
if (length === 0) {
return;
}
littleEndian = defined(littleEndian, this._littleEndian);
byteOffset = defined(byteOffset, this._offset);
this._checkBounds(byteOffset, length);
if (!littleEndian && length > 1) {
bytes = arrayFrom(bytes, true).reverse();
}
byteOffset += this.byteOffset;
if (this._isArrayBuffer) {
new Uint8Array(this.buffer, byteOffset, length).set(bytes);
} else {
if (this._isNodeBuffer) {
new Buffer(bytes).copy(this.buffer, byteOffset);
} else {
for (var i = 0; i < length; i++) {
this.buffer[byteOffset + i] = bytes[i];
}
}
}
this._offset = byteOffset - this.byteOffset + length;
},
setBytes: function (byteOffset, bytes, littleEndian) {
this._setBytes(byteOffset, bytes, defined(littleEndian, true));
},
getString: function (byteLength, byteOffset, encoding) {
if (this._isNodeBuffer) {
byteOffset = defined(byteOffset, this._offset);
byteLength = defined(byteLength, this.byteLength - byteOffset);
this._checkBounds(byteOffset, byteLength);
this._offset = byteOffset + byteLength;
return this.buffer.toString(
encoding || 'binary',
this.byteOffset + byteOffset,
this.byteOffset + this._offset,
);
}
var bytes = this._getBytes(byteLength, byteOffset, true),
string = '';
byteLength = bytes.length;
for (var i = 0; i < byteLength; i++) {
string += String.fromCharCode(bytes[i]);
}
if (encoding === 'utf8') {
string = decodeURIComponent(escape(string));
}
return string;
},
setString: function (byteOffset, subString, encoding) {
if (this._isNodeBuffer) {
byteOffset = defined(byteOffset, this._offset);
this._checkBounds(byteOffset, subString.length);
this._offset =
byteOffset +
this.buffer.write(
subString,
this.byteOffset + byteOffset,
encoding || 'binary',
);
return;
}
if (encoding === 'utf8') {
subString = unescape(encodeURIComponent(subString));
}
this._setBytes(byteOffset, getCharCodes(subString), true);
},
getChar: function (byteOffset) {
return this.getString(1, byteOffset);
},
setChar: function (byteOffset, character) {
this.setString(byteOffset, character);
},
tell: function () {
return this._offset;
},
seek: function (byteOffset) {
this._checkBounds(byteOffset, 0);
/* jshint boss: true */
return (this._offset = byteOffset);
},
skip: function (byteLength) {
return this.seek(this._offset + byteLength);
},
slice: function (start, end, forceCopy) {
function normalizeOffset(offset, byteLength) {
return offset < 0 ? offset + byteLength : offset;
}
start = normalizeOffset(start, this.byteLength);
end = normalizeOffset(defined(end, this.byteLength), this.byteLength);
return forceCopy
? new jDataView(
this.getBytes(end - start, start, true, true),
undefined,
undefined,
this._littleEndian,
)
: new jDataView(
this.buffer,
this.byteOffset + start,
end - start,
this._littleEndian,
);
},
alignBy: function (byteCount) {
this._bitOffset = 0;
if (defined(byteCount, 1) !== 1) {
return this.skip(byteCount - (this._offset % byteCount || byteCount));
} else {
return this._offset;
}
},
// Compatibility functions
_getFloat64: function (byteOffset, littleEndian) {
var b = this._getBytes(8, byteOffset, littleEndian),
sign = 1 - 2 * (b[7] >> 7),
exponent = ((((b[7] << 1) & 0xff) << 3) | (b[6] >> 4)) - ((1 << 10) - 1),
// Binary operators such as | and << operate on 32 bit values, using + and Math.pow(2) instead
mantissa =
(b[6] & 0x0f) * pow2(48) +
b[5] * pow2(40) +
b[4] * pow2(32) +
b[3] * pow2(24) +
b[2] * pow2(16) +
b[1] * pow2(8) +
b[0];
if (exponent === 1024) {
if (mantissa !== 0) {
return NaN;
} else {
return sign * Infinity;
}
}
if (exponent === -1023) {
// Denormalized
return sign * mantissa * pow2(-1022 - 52);
}
return sign * (1 + mantissa * pow2(-52)) * pow2(exponent);
},
_getFloat32: function (byteOffset, littleEndian) {
var b = this._getBytes(4, byteOffset, littleEndian),
sign = 1 - 2 * (b[3] >> 7),
exponent = (((b[3] << 1) & 0xff) | (b[2] >> 7)) - 127,
mantissa = ((b[2] & 0x7f) << 16) | (b[1] << 8) | b[0];
if (exponent === 128) {
if (mantissa !== 0) {
return NaN;
} else {
return sign * Infinity;
}
}
if (exponent === -127) {
// Denormalized
return sign * mantissa * pow2(-126 - 23);
}
return sign * (1 + mantissa * pow2(-23)) * pow2(exponent);
},
_get64: function (Type, byteOffset, littleEndian) {
littleEndian = defined(littleEndian, this._littleEndian);
byteOffset = defined(byteOffset, this._offset);
var parts = littleEndian ? [0, 4] : [4, 0];
for (var i = 0; i < 2; i++) {
parts[i] = this.getUint32(byteOffset + parts[i], littleEndian);
}
this._offset = byteOffset + 8;
return new Type(parts[0], parts[1]);
},
getInt64: function (byteOffset, littleEndian) {
return this._get64(Int64, byteOffset, littleEndian);
},
getUint64: function (byteOffset, littleEndian) {
return this._get64(Uint64, byteOffset, littleEndian);
},
_getInt32: function (byteOffset, littleEndian) {
var b = this._getBytes(4, byteOffset, littleEndian);
return (b[3] << 24) | (b[2] << 16) | (b[1] << 8) | b[0];
},
_getUint32: function (byteOffset, littleEndian) {
return this._getInt32(byteOffset, littleEndian) >>> 0;
},
_getInt16: function (byteOffset, littleEndian) {
return (this._getUint16(byteOffset, littleEndian) << 16) >> 16;
},
_getUint16: function (byteOffset, littleEndian) {
var b = this._getBytes(2, byteOffset, littleEndian);
return (b[1] << 8) | b[0];
},
_getInt8: function (byteOffset) {
return (this._getUint8(byteOffset) << 24) >> 24;
},
_getUint8: function (byteOffset) {
return this._getBytes(1, byteOffset)[0];
},
_getBitRangeData: function (bitLength, byteOffset) {
var startBit = (defined(byteOffset, this._offset) << 3) + this._bitOffset,
endBit = startBit + bitLength,
start = startBit >>> 3,
end = (endBit + 7) >>> 3,
b = this._getBytes(end - start, start, true),
wideValue = 0;
/* jshint boss: true */
if ((this._bitOffset = endBit & 7)) {
this._bitOffset -= 8;
}
for (var i = 0, length = b.length; i < length; i++) {
wideValue = (wideValue << 8) | b[i];
}
return {
start: start,
bytes: b,
wideValue: wideValue,
};
},
getSigned: function (bitLength, byteOffset) {
var shift = 32 - bitLength;
return (this.getUnsigned(bitLength, byteOffset) << shift) >> shift;
},
getUnsigned: function (bitLength, byteOffset) {
var value =
this._getBitRangeData(bitLength, byteOffset).wideValue >>>
-this._bitOffset;
return bitLength < 32 ? value & ~(-1 << bitLength) : value;
},
_setBinaryFloat: function (
byteOffset,
value,
mantSize,
expSize,
littleEndian,
) {
var signBit = value < 0 ? 1 : 0,
exponent,
mantissa,
eMax = ~(-1 << (expSize - 1)),
eMin = 1 - eMax;
if (value < 0) {
value = -value;
}
if (value === 0) {
exponent = 0;
mantissa = 0;
} else if (isNaN(value)) {
exponent = 2 * eMax + 1;
mantissa = 1;
} else if (value === Infinity) {
exponent = 2 * eMax + 1;
mantissa = 0;
} else {
exponent = Math.floor(Math.log(value) / Math.LN2);
if (exponent >= eMin && exponent <= eMax) {
mantissa = Math.floor((value * pow2(-exponent) - 1) * pow2(mantSize));
exponent += eMax;
} else {
mantissa = Math.floor(value / pow2(eMin - mantSize));
exponent = 0;
}
}
var b = [];
while (mantSize >= 8) {
b.push(mantissa % 256);
mantissa = Math.floor(mantissa / 256);
mantSize -= 8;
}
exponent = (exponent << mantSize) | mantissa;
expSize += mantSize;
while (expSize >= 8) {
b.push(exponent & 0xff);
exponent >>>= 8;
expSize -= 8;
}
b.push((signBit << expSize) | exponent);
this._setBytes(byteOffset, b, littleEndian);
},
_setFloat32: function (byteOffset, value, littleEndian) {
this._setBinaryFloat(byteOffset, value, 23, 8, littleEndian);
},
_setFloat64: function (byteOffset, value, littleEndian) {
this._setBinaryFloat(byteOffset, value, 52, 11, littleEndian);
},
_set64: function (Type, byteOffset, value, littleEndian) {
if (!(value instanceof Type)) {
value = Type.fromNumber(value);
}
littleEndian = defined(littleEndian, this._littleEndian);
byteOffset = defined(byteOffset, this._offset);
var parts = littleEndian ? { lo: 0, hi: 4 } : { lo: 4, hi: 0 };
for (var partName in parts) {
this.setUint32(
byteOffset + parts[partName],
value[partName],
littleEndian,
);
}
this._offset = byteOffset + 8;
},
setInt64: function (byteOffset, value, littleEndian) {
this._set64(Int64, byteOffset, value, littleEndian);
},
setUint64: function (byteOffset, value, littleEndian) {
this._set64(Uint64, byteOffset, value, littleEndian);
},
_setUint32: function (byteOffset, value, littleEndian) {
this._setBytes(
byteOffset,
[value & 0xff, (value >>> 8) & 0xff, (value >>> 16) & 0xff, value >>> 24],
littleEndian,
);
},
_setUint16: function (byteOffset, value, littleEndian) {
this._setBytes(
byteOffset,
[value & 0xff, (value >>> 8) & 0xff],
littleEndian,
);
},
_setUint8: function (byteOffset, value) {
this._setBytes(byteOffset, [value & 0xff]);
},
setUnsigned: function (byteOffset, value, bitLength) {
var data = this._getBitRangeData(bitLength, byteOffset),
wideValue = data.wideValue,
b = data.bytes;
wideValue &= ~(~(-1 << bitLength) << -this._bitOffset); // clearing bit range before binary "or"
wideValue |=
(bitLength < 32 ? value & ~(-1 << bitLength) : value) << -this._bitOffset; // setting bits
for (var i = b.length - 1; i >= 0; i--) {
b[i] = wideValue & 0xff;
wideValue >>>= 8;
}
this._setBytes(data.start, b, true);
},
};
var proto = jDataView.prototype;
for (var type in dataTypes) {
(function (type) {
proto['get' + type] = function (byteOffset, littleEndian) {
return this._action(type, true, byteOffset, littleEndian);
};
proto['set' + type] = function (byteOffset, value, littleEndian) {
this._action(type, false, byteOffset, littleEndian, value);
};
})(type);
}
proto._setInt32 = proto._setUint32;
proto._setInt16 = proto._setUint16;
proto._setInt8 = proto._setUint8;
proto.setSigned = proto.setUnsigned;
for (var method in proto) {
if (method.slice(0, 3) === 'set') {
(function (type) {
proto['write' + type] = function () {
Array.prototype.unshift.call(arguments, undefined);
this['set' + type].apply(this, arguments);
};
})(method.slice(3));
}
}
if (typeof module !== 'undefined' && typeof module.exports === 'object') {
module.exports = jDataView;
} else if (typeof define === 'function' && define.amd) {
define([], function () {
return jDataView;
});
} else {
var oldGlobalThis = globalThis.jDataView;
(globalThis.jDataView = jDataView).noConflict = function () {
globalThis.jDataView = oldGlobalThis;
return this;
};
}

View file

@ -0,0 +1,331 @@
import { rgbToHex } from '$lib/utils/rgbToHex';
import { shadeColor } from '$lib/utils/shadeColor';
/**
* Represents a single stitch in the pattern.
* @param {number} x - The absolute x position of the stitch.
* @param {number} y - The absolute y position of the stitch.
* @param {number} flags - Stitch flags (e.g. jump, trim).
* @param {number} color - Index of the stitch color.
* @constructor
*/
function Stitch(x, y, flags, color) {
this.x = x;
this.y = y;
this.flags = flags;
this.color = color;
}
/**
* Represents a color with RGB components and an optional description.
* @param {number} r - Red component (0-255).
* @param {number} g - Green component (0-255).
* @param {number} b - Blue component (0-255).
* @param {string} description - Color description.
* @constructor
*/
function Color(r, g, b, description) {
this.r = r;
this.g = g;
this.b = b;
this.description = description;
}
/**
* Stitch type bit flags.
* @readonly
* @enum {number}
*/
const stitchTypes = {
normal: 0,
jump: 1,
trim: 2,
stop: 4,
end: 8,
};
/**
* Represents an embroidery pattern containing stitches and colors.
* @constructor
*/
function Pattern() {
/** @type {Color[]} */
this.colors = [];
/** @type {Stitch[]} */
this.stitches = [];
/** Hoop info (not typed, depends on implementation) */
this.hoop = {};
/** Last stitch position for relative moves */
this.lastX = 0;
this.lastY = 0;
/** Bounding box */
this.top = 0;
this.bottom = 0;
this.left = 0;
this.right = 0;
/** Current color index used for new stitches */
this.currentColorIndex = 0;
}
/**
* Adds a color by RGB values.
* @param {number} r
* @param {number} g
* @param {number} b
* @param {string} description
*/
Pattern.prototype.addColorRgb = function (r, g, b, description) {
this.colors.push(new Color(r, g, b, description));
};
/**
* Adds an existing Color instance.
* @param {Color} color
*/
Pattern.prototype.addColor = function (color) {
this.colors.push(color);
};
/**
* Adds a stitch at absolute coordinates.
* @param {number} x - Absolute X coordinate.
* @param {number} y - Absolute Y coordinate.
* @param {number} flags - Stitch flags.
* @param {boolean} isAutoColorIndex - Whether to automatically increment color on stop.
*/
Pattern.prototype.addStitchAbs = function (x, y, flags, isAutoColorIndex) {
if ((flags & stitchTypes.end) === stitchTypes.end) {
this.calculateBoundingBox();
this.fixColorCount();
}
if (
(flags & stitchTypes.stop) === stitchTypes.stop &&
this.stitches.length === 0
) {
return;
}
if ((flags & stitchTypes.stop) === stitchTypes.stop && isAutoColorIndex) {
this.currentColorIndex += 1;
}
this.stitches.push(new Stitch(x, y, flags, this.currentColorIndex));
};
/**
* Adds a stitch relative to the last stitch.
* @param {number} dx - Delta X from last stitch.
* @param {number} dy - Delta Y from last stitch.
* @param {number} flags - Stitch flags.
* @param {boolean} [isAutoColorIndex=false] - Whether to automatically increment color on stop. Optional.
*/
Pattern.prototype.addStitchRel = function (dx, dy, flags, isAutoColorIndex) {
if (typeof isAutoColorIndex === 'undefined') {
isAutoColorIndex = false;
}
if (this.stitches.length !== 0) {
const nx = this.lastX + dx;
const ny = this.lastY + dy;
this.lastX = nx;
this.lastY = ny;
this.addStitchAbs(nx, ny, flags, isAutoColorIndex);
} else {
this.addStitchAbs(dx, dy, flags, isAutoColorIndex);
}
};
/**
* Calculates the bounding box of all stitches, excluding trims.
*/
Pattern.prototype.calculateBoundingBox = function () {
const stitchCount = this.stitches.length;
if (stitchCount === 0) {
this.bottom = 1;
this.right = 1;
return;
}
this.left = Infinity;
this.top = Infinity;
this.right = -Infinity;
this.bottom = -Infinity;
for (let i = 0; i < stitchCount; i++) {
const pt = this.stitches[i];
if (!(pt.flags & stitchTypes.trim)) {
if (pt.x < this.left) this.left = pt.x;
if (pt.y < this.top) this.top = pt.y;
if (pt.x > this.right) this.right = pt.x;
if (pt.y > this.bottom) this.bottom = pt.y;
}
}
};
/**
* Moves all stitches so the pattern is positioned at positive coordinates.
*/
Pattern.prototype.moveToPositive = function () {
const stitchCount = this.stitches.length;
for (let i = 0; i < stitchCount; i++) {
this.stitches[i].x -= this.left;
this.stitches[i].y -= this.top;
}
this.right -= this.left;
this.left = 0;
this.bottom -= this.top;
this.top = 0;
};
/**
* Flips the pattern vertically.
*/
Pattern.prototype.invertPatternVertical = function () {
const stitchCount = this.stitches.length;
const tempTop = -this.top;
for (let i = 0; i < stitchCount; i++) {
this.stitches[i].y = -this.stitches[i].y;
}
this.top = -this.bottom;
this.bottom = tempTop;
};
/**
* Adds a random color to the pattern.
*/
Pattern.prototype.addColorRandom = function () {
this.colors.push(
new Color(
Math.round(Math.random() * 256),
Math.round(Math.random() * 256),
Math.round(Math.random() * 256),
'random',
),
);
};
/**
* Fixes the color count so colors match used indices in stitches.
*/
Pattern.prototype.fixColorCount = function () {
let maxColorIndex = 0;
const stitchCount = this.stitches.length;
for (let i = 0; i < stitchCount; i++) {
if (this.stitches[i].color > maxColorIndex) {
maxColorIndex = this.stitches[i].color;
}
}
while (this.colors.length <= maxColorIndex) {
this.addColorRandom();
}
// Remove extra colors beyond max used index
this.colors.splice(maxColorIndex + 1);
};
/**
* Draws the stitch pattern to a canvas element.
* @param {HTMLCanvasElement} canvas
*/
Pattern.prototype.drawShapeTo = function (canvas) {
canvas.width = this.right;
canvas.height = this.bottom;
let gradient, tx, ty;
let lastStitch = this.stitches[0];
let gWidth = 100;
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
if (!ctx) {
// If context is null, just return or handle accordingly
return;
}
ctx.lineWidth = 3;
ctx.lineJoin = 'round';
let color = this.colors[this.stitches[0].color];
for (let i = 0; i < this.stitches.length; i++) {
const currentStitch = this.stitches[i];
if (i > 0) lastStitch = this.stitches[i - 1];
tx = currentStitch.x - lastStitch.x;
ty = currentStitch.y - lastStitch.y;
gWidth = Math.sqrt(tx * tx + ty * ty);
gradient = ctx.createRadialGradient(
currentStitch.x - tx,
currentStitch.y - ty,
0,
currentStitch.x - tx,
currentStitch.y - ty,
gWidth * 1.4,
);
gradient.addColorStop(0, shadeColor(rgbToHex(color), -60));
gradient.addColorStop(0.05, rgbToHex(color));
gradient.addColorStop(0.5, shadeColor(rgbToHex(color), 60));
gradient.addColorStop(0.9, rgbToHex(color));
gradient.addColorStop(1.0, shadeColor(rgbToHex(color), -60));
ctx.strokeStyle = gradient;
if (
currentStitch.flags === stitchTypes.jump ||
currentStitch.flags === stitchTypes.trim ||
currentStitch.flags === stitchTypes.stop
) {
color = this.colors[currentStitch.color];
ctx.beginPath();
ctx.strokeStyle =
'rgba(' + color.r + ',' + color.g + ',' + color.b + ',0)';
ctx.moveTo(currentStitch.x, currentStitch.y);
ctx.stroke();
}
ctx.beginPath();
ctx.moveTo(lastStitch.x, lastStitch.y);
ctx.lineTo(currentStitch.x, currentStitch.y);
ctx.stroke();
lastStitch = currentStitch;
}
}
};
/**
* Draws color swatches into a container element.
* @param {HTMLElement} colorContainer
*/
Pattern.prototype.drawColorsTo = function (colorContainer) {
this.colors.forEach((color) => {
colorContainer.innerHTML += `<div style='background-color: rgb(${color.r}, ${color.g}, ${color.b}); height: 25px; width: 25px; border: 1px solid #000000; border-radius: 16px;'></div>`;
});
};
/**
* Displays the stitch count in a container element.
* @param {HTMLElement} stitchesContainer
* @param {string} stitchesString - Label for stitches count.
*/
Pattern.prototype.drawStitchesCountTo = function (
stitchesContainer,
stitchesString,
) {
stitchesContainer.innerHTML += `<div><strong>${stitchesString}:</strong> ${this.stitches.length}</div>`;
};
/**
* Displays pattern dimensions in a container element.
* @param {HTMLElement} sizeContainer
* @param {string} dimensionsString - Label for dimensions.
*/
Pattern.prototype.drawSizeValuesTo = function (
sizeContainer,
dimensionsString,
) {
sizeContainer.innerHTML += `<div><strong>${dimensionsString}:</strong> ${Math.round(this.right / 10)}mm x ${Math.round(this.bottom / 10)}mm</div>`;
};
export { Pattern, Color, stitchTypes };

View file

@ -0,0 +1,89 @@
import { stitchTypes } from '$lib/file-renderer/pattern';
/**
* Decodes stitch flags from the 3rd byte of a DST stitch command.
*
* @param {number} b2 The third byte of the stitch command.
* @returns {number} Bitmask representing stitch types.
*/
function decodeExp(b2) {
if (b2 === 0xf3) {
return stitchTypes.end;
}
if ((b2 & 0xc3) === 0xc3) {
return stitchTypes.trim | stitchTypes.stop;
}
let returnCode = 0;
if (b2 & 0x80) {
returnCode |= stitchTypes.trim;
}
if (b2 & 0x40) {
returnCode |= stitchTypes.stop;
}
return returnCode;
}
/**
* Reads a DST embroidery file and populates the pattern object.
*
*
* @param {EmbroideryFileView} file
* @param {EmbroideryPattern} pattern
*/
export function dstRead(file, pattern) {
let prevJump = false;
const byteCount = file.byteLength;
file.seek(512); // Skip DST header
while (file.tell() < byteCount - 3) {
/** @type {number[]} */
const b = [file.getUint8(), file.getUint8(), file.getUint8()];
let x = 0;
let y = 0;
// Decode X movements
if (b[0] & 0x01) x += 1;
if (b[0] & 0x02) x -= 1;
if (b[0] & 0x04) x += 9;
if (b[0] & 0x08) x -= 9;
if (b[1] & 0x01) x += 3;
if (b[1] & 0x02) x -= 3;
if (b[1] & 0x04) x += 27;
if (b[1] & 0x08) x -= 27;
if (b[2] & 0x04) x += 81;
if (b[2] & 0x08) x -= 81;
// Decode Y movements
if (b[0] & 0x80) y += 1;
if (b[0] & 0x40) y -= 1;
if (b[0] & 0x20) y += 9;
if (b[0] & 0x10) y -= 9;
if (b[1] & 0x80) y += 3;
if (b[1] & 0x40) y -= 3;
if (b[1] & 0x20) y += 27;
if (b[1] & 0x10) y -= 27;
if (b[2] & 0x20) y += 81;
if (b[2] & 0x10) y -= 81;
let flags = decodeExp(b[2]);
const thisJump = (flags & stitchTypes.jump) !== 0;
if (prevJump) {
flags |= stitchTypes.jump;
}
pattern.addStitchRel(x, y, flags, true);
prevJump = thisJump;
}
pattern.addStitchRel(0, 0, stitchTypes.end, true);
pattern.invertPatternVertical();
}

View file

@ -0,0 +1,56 @@
import { stitchTypes } from '$lib/file-renderer/pattern';
/**
* Decodes a single byte with EXP format rules.
* Values above 128 are negative numbers encoded with bitwise operations.
*
* @param {number} input - A signed 8-bit integer (-128 to 127).
* @returns {number} - Decoded signed integer.
*/
function expDecode(input) {
return input > 128 ? -(~input & 0xff) - 1 : input;
}
/**
* Reads an EXP format file and adds stitches to the given pattern.
*
* @param {EmbroideryFileView} file - A DataView representing the binary EXP file content.
* @param {EmbroideryPattern} pattern - The pattern object with addStitchRel and invertPatternVertical methods.
* @returns {void}
*/
export function expRead(file, pattern) {
let index = 0;
const byteCount = file.byteLength;
while (index < byteCount) {
let flags = stitchTypes.normal;
let b0 = file.getInt8(index++);
let b1 = file.getInt8(index++);
if (b0 === -128) {
if (b1 & 1) {
b0 = file.getInt8(index++);
b1 = file.getInt8(index++);
flags = stitchTypes.stop;
} else if (b1 === 2 || b1 === 4) {
b0 = file.getInt8(index++);
b1 = file.getInt8(index++);
flags = stitchTypes.trim;
} else if (b1 === -128) {
b0 = file.getInt8(index++);
b1 = file.getInt8(index++);
b0 = 0;
b1 = 0;
flags = stitchTypes.trim;
}
}
const dx = expDecode(b0);
const dy = expDecode(b1);
pattern.addStitchRel(dx, dy, flags, true);
}
pattern.addStitchRel(0, 0, stitchTypes.end);
pattern.invertPatternVertical();
}

View file

@ -0,0 +1,19 @@
import { dstRead } from './dst';
import { expRead } from './exp';
import { jefRead } from './jef';
import { pecRead } from './pec';
import { pesRead } from './pes';
/**
* Supported embroidery file formats.
* @type {SupportedFormats}
*/
const supportedFormats = {
pes: { ext: '.pes', read: pesRead },
dst: { ext: '.dst', read: dstRead },
pec: { ext: '.pec', read: pecRead },
jef: { ext: '.jef', read: jefRead },
exp: { ext: '.exp', read: expRead },
};
export { supportedFormats };

View file

@ -0,0 +1,212 @@
import { Color, stitchTypes } from '$lib/file-renderer/pattern';
/** @type {Color[]} */
const colors = [
new Color(0, 0, 0, 'Black'),
new Color(0, 0, 0, 'Black'),
new Color(255, 255, 255, 'White'),
new Color(255, 255, 23, 'Yellow'),
new Color(250, 160, 96, 'Orange'),
new Color(92, 118, 73, 'Olive Green'),
new Color(64, 192, 48, 'Green'),
new Color(101, 194, 200, 'Sky'),
new Color(172, 128, 190, 'Purple'),
new Color(245, 188, 203, 'Pink'),
new Color(255, 0, 0, 'Red'),
new Color(192, 128, 0, 'Brown'),
new Color(0, 0, 240, 'Blue'),
new Color(228, 195, 93, 'Gold'),
new Color(165, 42, 42, 'Dark Brown'),
new Color(213, 176, 212, 'Pale Violet'),
new Color(252, 242, 148, 'Pale Yellow'),
new Color(240, 208, 192, 'Pale Pink'),
new Color(255, 192, 0, 'Peach'),
new Color(201, 164, 128, 'Beige'),
new Color(155, 61, 75, 'Wine Red'),
new Color(160, 184, 204, 'Pale Sky'),
new Color(127, 194, 28, 'Yellow Green'),
new Color(185, 185, 185, 'Silver Grey'),
new Color(160, 160, 160, 'Grey'),
new Color(152, 214, 189, 'Pale Aqua'),
new Color(184, 240, 240, 'Baby Blue'),
new Color(54, 139, 160, 'Powder Blue'),
new Color(79, 131, 171, 'Bright Blue'),
new Color(56, 106, 145, 'Slate Blue'),
new Color(0, 32, 107, 'Nave Blue'),
new Color(229, 197, 202, 'Salmon Pink'),
new Color(249, 103, 107, 'Coral'),
new Color(227, 49, 31, 'Burnt Orange'),
new Color(226, 161, 136, 'Cinnamon'),
new Color(181, 148, 116, 'Umber'),
new Color(228, 207, 153, 'Blonde'),
new Color(225, 203, 0, 'Sunflower'),
new Color(225, 173, 212, 'Orchid Pink'),
new Color(195, 0, 126, 'Peony Purple'),
new Color(128, 0, 75, 'Burgundy'),
new Color(160, 96, 176, 'Royal Purple'),
new Color(192, 64, 32, 'Cardinal Red'),
new Color(202, 224, 192, 'Opal Green'),
new Color(137, 152, 86, 'Moss Green'),
new Color(0, 170, 0, 'Meadow Green'),
new Color(33, 138, 33, 'Dark Green'),
new Color(93, 174, 148, 'Aquamarine'),
new Color(76, 191, 143, 'Emerald Green'),
new Color(0, 119, 114, 'Peacock Green'),
new Color(112, 112, 112, 'Dark Grey'),
new Color(242, 255, 255, 'Ivory White'),
new Color(177, 88, 24, 'Hazel'),
new Color(203, 138, 7, 'Toast'),
new Color(247, 146, 123, 'Salmon'),
new Color(152, 105, 45, 'Cocoa Brown'),
new Color(162, 113, 72, 'Sienna'),
new Color(123, 85, 74, 'Sepia'),
new Color(79, 57, 70, 'Dark Sepia'),
new Color(82, 58, 151, 'Violet Blue'),
new Color(0, 0, 160, 'Blue Ink'),
new Color(0, 150, 222, 'Solar Blue'),
new Color(178, 221, 83, 'Green Dust'),
new Color(250, 143, 187, 'Crimson'),
new Color(222, 100, 158, 'Floral Pink'),
new Color(181, 80, 102, 'Wine'),
new Color(94, 87, 71, 'Olive Drab'),
new Color(76, 136, 31, 'Meadow'),
new Color(228, 220, 121, 'Mustard'),
new Color(203, 138, 26, 'Yellow Ochre'),
new Color(198, 170, 66, 'Old Gold'),
new Color(236, 176, 44, 'Honeydew'),
new Color(248, 128, 64, 'Tangerine'),
new Color(255, 229, 5, 'Canary Yellow'),
new Color(250, 122, 122, 'Vermillion'),
new Color(107, 224, 0, 'Bright Green'),
new Color(56, 108, 174, 'Ocean Blue'),
new Color(227, 196, 180, 'Beige Grey'),
new Color(227, 172, 129, 'Bamboo'),
];
/**
* Decode a single byte for JEF stitch data (signed int8-like with special encoding).
* @param {number} byte
* @returns {number}
*/
const jefDecode = (byte) => (byte >= 0x80 ? -(~byte & 0xff) - 1 : byte);
/**
* Check if a byte represents a special stitch (0x80).
* @param {number} byte
* @returns {boolean}
*/
const isSpecialStitch = (byte) => byte === 0x80;
/**
* Check if a byte represents a stop or trim command.
* @param {number} byte
* @returns {boolean}
*/
const isStopOrTrim = (byte) =>
(byte & 0x01) !== 0 || byte === 0x02 || byte === 0x04;
/**
* Check if a byte indicates end of pattern.
* @param {number} byte
* @returns {boolean}
*/
const isEndOfPattern = (byte) => byte === 0x10;
/**
* Check if a byte indicates a stop command.
* @param {number} byte
* @returns {boolean}
*/
const isStop = (byte) => (byte & 0x01) !== 0;
/**
* Read two stitch data bytes from the file.
* @param {EmbroideryFileView} file
* @returns {{ byte1: number, byte2: number }}
*/
const readStitchData = (file) => ({
byte1: file.getUint8(),
byte2: file.getUint8(),
});
/**
* Add colors from file data to the pattern.
* @param {EmbroideryFileView} file
* @param {EmbroideryPattern} pattern
* @param {number} colorCount
*/
const addColorsToPattern = (file, pattern, colorCount) => {
for (let i = 0; i < colorCount; i++) {
const colorIndex = file.getUint32(file.tell(), true) % colors.length;
pattern.addColor(colors[colorIndex]);
}
};
/**
* Determine the stitch type and potentially read additional bytes.
* @param {EmbroideryFileView} file
* @param {number} byte1
* @param {number} byte2
* @returns {{ type: number, byte1: number, byte2: number, end?: boolean }}
*/
const determineStitchType = (file, byte1, byte2) => {
if (isSpecialStitch(byte1)) {
if (isStopOrTrim(byte2)) {
return {
type: isStop(byte2) ? stitchTypes.stop : stitchTypes.trim,
byte1: file.getUint8(),
byte2: file.getUint8(),
};
} else if (isEndOfPattern(byte2)) {
return { type: stitchTypes.end, byte1: 0, byte2: 0, end: true };
}
}
return { type: stitchTypes.normal, byte1, byte2 };
};
/**
* Process stitches in the file and add them to the pattern.
* @param {EmbroideryFileView} file
* @param {EmbroideryPattern} pattern
* @param {number} stitchCount
*/
const processStitches = (file, pattern, stitchCount) => {
let stitchesProcessed = 0;
while (stitchesProcessed < stitchCount + 100) {
const { byte1, byte2 } = readStitchData(file);
const {
type,
byte1: decodedByte1,
byte2: decodedByte2,
end,
} = determineStitchType(file, byte1, byte2);
pattern.addStitchRel(
jefDecode(decodedByte1),
jefDecode(decodedByte2),
type,
true,
);
if (end) break;
stitchesProcessed++;
}
};
/**
* Reads a JEF file and adds stitches and colors to the pattern.
* @param {EmbroideryFileView} file
* @param {EmbroideryPattern} pattern
*/
export function jefRead(file, pattern) {
file.seek(24);
const colorCount = file.getInt32(file.tell(), true);
const stitchCount = file.getInt32(file.tell(), true);
file.seek(file.tell() + 84);
addColorsToPattern(file, pattern, colorCount);
file.seek(file.tell() + (6 - colorCount) * 4);
processStitches(file, pattern, stitchCount);
pattern.invertPatternVertical();
}
export const jefColors = colors;

View file

@ -0,0 +1,18 @@
import { pecColors, pecReadStitches } from './pes';
/**
*
* @param {EmbroideryFileView} file
* @param {EmbroideryPattern} pattern
*/
export function pecRead(file, pattern) {
let colorChanges, i;
file.seek(0x38);
colorChanges = file.getUint8();
for (i = 0; i <= colorChanges; i++) {
pattern.addColor(pecColors[file.getUint8() % 65]);
}
file.seek(0x21c);
pecReadStitches(file, pattern);
return true;
}

View file

@ -0,0 +1,192 @@
import { Color, stitchTypes } from '$lib/file-renderer/pattern';
/**
* Array of predefined embroidery colors used in PEC files.
* @type {Color[]}
*/
export const pecColors = [
new Color(0, 0, 0, 'Unknown'),
new Color(14, 31, 124, 'Prussian Blue'),
new Color(10, 85, 163, 'Blue'),
new Color(0, 135, 119, 'Teal Green'),
new Color(75, 107, 175, 'Cornflower Blue'),
new Color(237, 23, 31, 'Red'),
new Color(209, 92, 0, 'Reddish Brown'),
new Color(145, 54, 151, 'Magenta'),
new Color(228, 154, 203, 'Light Lilac'),
new Color(145, 95, 172, 'Lilac'),
new Color(158, 214, 125, 'Mint Green'),
new Color(232, 169, 0, 'Deep Gold'),
new Color(254, 186, 53, 'Orange'),
new Color(255, 255, 0, 'Yellow'),
new Color(112, 188, 31, 'Lime Green'),
new Color(186, 152, 0, 'Brass'),
new Color(168, 168, 168, 'Silver'),
new Color(125, 111, 0, 'Russet Brown'),
new Color(255, 255, 179, 'Cream Brown'),
new Color(79, 85, 86, 'Pewter'),
new Color(0, 0, 0, 'Black'),
new Color(11, 61, 145, 'Ultramarine'),
new Color(119, 1, 118, 'Royal Purple'),
new Color(41, 49, 51, 'Dark Gray'),
new Color(42, 19, 1, 'Dark Brown'),
new Color(246, 74, 138, 'Deep Rose'),
new Color(178, 118, 36, 'Light Brown'),
new Color(252, 187, 197, 'Salmon Pink'),
new Color(254, 55, 15, 'Vermillion'),
new Color(240, 240, 240, 'White'),
new Color(106, 28, 138, 'Violet'),
new Color(168, 221, 196, 'Seacrest'),
new Color(37, 132, 187, 'Sky Blue'),
new Color(254, 179, 67, 'Pumpkin'),
new Color(255, 243, 107, 'Cream Yellow'),
new Color(208, 166, 96, 'Khaki'),
new Color(209, 84, 0, 'Clay Brown'),
new Color(102, 186, 73, 'Leaf Green'),
new Color(19, 74, 70, 'Peacock Blue'),
new Color(135, 135, 135, 'Gray'),
new Color(216, 204, 198, 'Warm Gray'),
new Color(67, 86, 7, 'Dark Olive'),
new Color(253, 217, 222, 'Flesh Pink'),
new Color(249, 147, 188, 'Pink'),
new Color(0, 56, 34, 'Deep Green'),
new Color(178, 175, 212, 'Lavender'),
new Color(104, 106, 176, 'Wisteria Violet'),
new Color(239, 227, 185, 'Beige'),
new Color(247, 56, 102, 'Carmine'),
new Color(181, 75, 100, 'Amber Red'),
new Color(19, 43, 26, 'Olive Green'),
new Color(199, 1, 86, 'Dark Fuschia'),
new Color(254, 158, 50, 'Tangerine'),
new Color(168, 222, 235, 'Light Blue'),
new Color(0, 103, 62, 'Emerald Green'),
new Color(78, 41, 144, 'Purple'),
new Color(47, 126, 32, 'Moss Green'),
new Color(255, 204, 204, 'Flesh Pink'),
new Color(255, 217, 17, 'Harvest Gold'),
new Color(9, 91, 166, 'Electric Blue'),
new Color(240, 249, 112, 'Lemon Yellow'),
new Color(227, 243, 91, 'Fresh Green'),
new Color(255, 153, 0, 'Orange'),
new Color(255, 240, 141, 'Cream Yellow'),
new Color(255, 200, 200, 'Applique'),
];
/**
* Reads stitch data from a PEC embroidery file and adds it to the pattern.
* @param {EmbroideryFileView} file
* @param {EmbroideryPattern} pattern - The pattern to populate.
*/
function readPecStitches(file, pattern) {
let stitchNumber = 0;
const byteCount = file.byteLength;
while (file.tell() < byteCount) {
let [xOffset, yOffset] = [file.getUint8(), file.getUint8()];
let stitchType = stitchTypes.normal;
if (isEndStitch(xOffset, yOffset)) {
pattern.addStitchRel(0, 0, stitchTypes.end, true);
break;
}
if (isStopStitch(xOffset, yOffset)) {
file.getInt8(); // Skip extra byte
pattern.addStitchRel(0, 0, stitchTypes.stop, true);
stitchNumber++;
continue;
}
stitchType = determineStitchType(xOffset, yOffset);
[xOffset, yOffset] = decodeCoordinates(xOffset, yOffset, file);
pattern.addStitchRel(xOffset, yOffset, stitchType, true);
// eslint-disable-next-line no-unused-vars
stitchNumber++;
}
}
/**
* Determines whether the stitch is an "end" stitch.
* @param {number} xOffset
* @param {number} yOffset
* @returns {boolean}
*/
function isEndStitch(xOffset, yOffset) {
return xOffset === 0xff && yOffset === 0x00;
}
/**
* Determines whether the stitch is a "stop" stitch.
* @param {number} xOffset
* @param {number} yOffset
* @returns {boolean}
*/
function isStopStitch(xOffset, yOffset) {
return xOffset === 0xfe && yOffset === 0xb0;
}
/**
* Infers the stitch type from byte flags.
* @param {number} xOffset
* @param {number} yOffset
* @returns {number}
*/
function determineStitchType(xOffset, yOffset) {
if (xOffset & 0x80) {
if (xOffset & 0x20) return stitchTypes.trim;
if (xOffset & 0x10) return stitchTypes.jump;
}
if (yOffset & 0x80) {
if (yOffset & 0x20) return stitchTypes.trim;
if (yOffset & 0x10) return stitchTypes.jump;
}
return stitchTypes.normal;
}
/**
* Decodes 12-bit signed coordinates from PEC format.
* @param {number} xOffset
* @param {number} yOffset
* @param {DataView & { tell: () => number, seek: (pos: number) => void, getUint8: () => number, getInt8: () => number }} file
* @returns {[number, number]} - Decoded [x, y] coordinates.
*/
function decodeCoordinates(xOffset, yOffset, file) {
if (xOffset & 0x80) {
xOffset = ((xOffset & 0x0f) << 8) + yOffset;
if (xOffset & 0x800) xOffset -= 0x1000;
yOffset = file.getUint8();
} else if (xOffset >= 0x40) {
xOffset -= 0x80;
}
if (yOffset & 0x80) {
yOffset = ((yOffset & 0x0f) << 8) + file.getUint8();
if (yOffset & 0x800) yOffset -= 0x1000;
} else if (yOffset > 0x3f) {
yOffset -= 0x80;
}
return [xOffset, yOffset];
}
/**
* Parses a PES file and adds stitch and color data to the pattern.
* @param {DataView & { tell: () => number, seek: (pos: number) => void, getUint8: () => number, getInt8: () => number }} file
* @param {EmbroideryPattern} pattern - The pattern to populate.
*/
export function pesRead(file, pattern) {
const pecStart = file.getInt32(8, true);
file.seek(pecStart + 48);
const numColors = file.getInt8() + 1;
for (let i = 0; i < numColors; i++) {
pattern.addColor(pecColors[file.getInt8()]);
}
file.seek(pecStart + 532);
readPecStitches(file, pattern);
pattern.addStitchRel(0, 0, stitchTypes.end);
}
export const pecReadStitches = readPecStitches;

View file

@ -0,0 +1,31 @@
/**
* A custom DataView with embroidery reader-specific helper methods.
* @typedef {DataView & {
* tell: () => number;
* seek: (pos: number) => void;
* getUint8: () => number;
* getInt8: () => number;
* getInt32: (pos: number, littleEndian: boolean) => number;
* }} EmbroideryFileView
*/
/**
* A Pattern extended with optional embroidery reader methods.
* @typedef {import('$lib/file-renderer/pattern').Pattern & {
* addColor?: (color: import('$lib/file-renderer/pattern').Color) => void;
* addStitchRel: (dx: number, dy: number, stitchType: string, autoAdvance?: boolean) => void;
* invertPatternVertical?: () => void;
* }} EmbroideryPattern
*/
/**
* Represents a reader for a specific embroidery file format.
* @typedef {Object} FormatReader
* @property {string} ext - File extension (e.g., '.pes', '.dst').
* @property {(view: EmbroideryFileView, pattern: EmbroideryPattern) => void} read - Function to parse the embroidery format and populate the pattern.
*/
/**
* A map of supported embroidery file formats keyed by format name (e.g., "pes", "dst").
* @typedef {Object.<string, FormatReader>} SupportedFormats
*/

View file

@ -0,0 +1,14 @@
{
"title": "Upload files",
"fileSize": "Max file size is <strong>{{fileSize}}MB</strong>.",
"supportedFormats": "Accepted formats: <strong>{{supportedFormats}}</strong>.",
"render": "Render files",
"dropzone": "<strong>Choose files</strong><br /><span>or drag and drop them here</span>",
"browse": "Browse",
"selected": "Selected files",
"rejected": "Rejected files",
"stitches": "Stitches",
"dimensions": "Dimensions (x, y)",
"download": "Download image",
"warning.copyright": "Do not upload copyrighted material you do not own or have rights to."
}

View file

@ -60,6 +60,12 @@ const config = {
loader: async () =>
(await import('./en-US/terms-of-service.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'viewer',
routes: ['/viewer'],
loader: async () => (await import('./en-US/viewer.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'header',
@ -101,6 +107,12 @@ const config = {
loader: async () =>
(await import('./pt-BR/terms-of-service.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'viewer',
routes: ['/viewer'],
loader: async () => (await import('./pt-BR/viewer.json')).default,
},
],
};

View file

@ -0,0 +1,14 @@
{
"title": "Carregar arquivos",
"languageSwitch": "🇺🇸",
"fileSize": "O tamanho máximo de cada arquivo é <strong>{{fileSize}}MB</strong>.",
"supportedFormats": "Formatos aceitos: <strong>{{supportedFormats}}</strong>.",
"render": "Renderizar arquivos",
"dropzone": "<strong>Selecione arquivos</strong><br /><span>ou arraste e solte-os aqui</span>",
"browse": "Selecionar arquivos",
"selected": "Arquivos selecionados",
"rejected": "Arquivos recusados",
"stitches": "Pontos",
"download": "Baixar imagem",
"warning.copyright": "Não carregue material protegido por direitos autorais que você não possui ou sobre os quais não tenha direitos."
}

View file

@ -0,0 +1,47 @@
/**
* Returns the lowercase file extension (including dot) of a filename.
* @param {string} name - The name of the file.
* @returns {string} The file extension, e.g., ".png"
*/
const formattedFilenameExt = (name) => {
const parts = typeof name === 'string' ? name.split('.') : [];
const ext = parts.length > 1 ? parts.pop() : '';
return ext ? `.${ext.toLowerCase()}` : '';
};
/**
* Checks whether a file meets the size and format requirements.
* @param {{ maxSize: number, supportedFormats: string[] }} requirements
* @param {File} file
* @returns {boolean}
*/
const areRequirementsFulfilled = (requirements, file) => {
return (
file.size <= requirements.maxSize &&
requirements.supportedFormats.includes(formattedFilenameExt(file.name))
);
};
/**
* Filters a list of files into accepted and rejected based on requirements.
* @param {FileList | File[]} files - The list of files to filter.
* @param {{ maxSize: number, supportedFormats: string[] }} requirements
* @returns {{ accepted: File[], rejected: File[] }}
*/
export function filterFiles(files, requirements) {
/** @type {File[]} */
const accepted = [];
/** @type {File[]} */
const rejected = [];
for (const file of Array.from(files)) {
if (file && areRequirementsFulfilled(requirements, file)) {
accepted.push(file);
} else {
rejected.push(file);
}
}
return { accepted, rejected };
}

25
src/lib/utils/rgbToHex.js Normal file
View file

@ -0,0 +1,25 @@
/**
* Converts a single color component to a 2-digit hexadecimal string.
* @param {number} c - A number between 0 and 255.
* @returns {string} The 2-digit hex representation.
*/
const componentToHex = (c) => {
const hex = c.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
/**
* Converts an RGB object to a hexadecimal color string.
* @param {{ r: number, g: number, b: number }} color - An object with r, g, and b properties (0255).
* @returns {string} The hex color string (e.g., "#ffcc00").
*/
const rgbToHex = (color) => {
return (
'#' +
componentToHex(color.r) +
componentToHex(color.g) +
componentToHex(color.b)
);
};
export { rgbToHex };

View file

@ -0,0 +1,27 @@
/**
* Shades a hex color by a given percentage.
* Positive values lighten the color, negative values darken it.
*
* @param {string} color - A 7-character hex color string (e.g. "#ffcc00").
* @param {number} percent - A percentage from -100 to 100 to adjust brightness.
* @returns {string} - The adjusted hex color string.
*/
function shadeColor(color, percent) {
const num = parseInt(color.slice(1), 16);
const amt = Math.round(2.55 * percent);
let r = (num >> 16) + amt;
let g = ((num >> 8) & 0xff) + amt;
let b = (num & 0xff) + amt;
// Clamp each component between 0 and 255
r = Math.min(255, Math.max(0, r));
g = Math.min(255, Math.max(0, g));
b = Math.min(255, Math.max(0, b));
const shaded = (1 << 24) + (r << 16) + (g << 8) + b;
return `#${shaded.toString(16).slice(1)}`;
}
export { shadeColor };

View file

@ -1,7 +1,5 @@
<script>
import { t, locale } from '$lib/translations';
console.log($locale);
import { t } from '$lib/translations';
</script>
<section aria-labelledby="about-heading">

View file

@ -1 +1,139 @@
<h1>Viewer</h1>
<script>
import { t } from '$lib/translations';
import CardList from '$lib/components/CardList.svelte';
import Dropzone from '$lib/components/Dropzone.svelte';
import FileList from '$lib/components/FileList.svelte';
import { filterFiles } from '$lib/utils/filterFiles';
import { supportedFormats } from '$lib/format-readers';
/** @type {File[] | []} */
let acceptedFiles = [];
/** @type {File[] | []} */
let rejectedFiles = [];
let areAcceptedFilesRendered = false;
const fileRequirements = {
supportedFormats: Object.values(supportedFormats).map((f) => f.ext),
maxSize: 1000000,
};
function onSubmit() {
areAcceptedFilesRendered = true;
}
/**
* @param {DragEvent} evt
*/
function onDrop(evt) {
onChange(evt);
}
/**
* @param {Event | DragEvent} evt
*/
function onChange(evt) {
acceptedFiles = null;
rejectedFiles = null;
areAcceptedFilesRendered = false;
const changedFiles =
'dataTransfer' in evt && evt.dataTransfer
? evt.dataTransfer.files
: evt.target?.files;
if (!changedFiles) return;
const results = filterFiles(changedFiles, fileRequirements);
acceptedFiles = results.accepted;
rejectedFiles = results.rejected;
}
function onClick() {
const el = document.getElementById('file-input');
if (el) el.click();
}
/**
* @param {KeyboardEvent} evt
*/
function onKeydown(evt) {
if (evt.key === 'Enter') {
const el = document.getElementById('file-input');
if (el) el.click();
}
}
</script>
<form
id="form"
enctype="multipart/form-data"
on:submit|preventDefault|stopPropagation={onSubmit}
>
<div class="title-container">
<h2>{$t('viewer.title')}</h2>
</div>
<p>
{@html $t('viewer.fileSize', {
fileSize: fileRequirements.maxSize / 1_000_000,
})}
{@html $t('viewer.supportedFormats', {
supportedFormats: fileRequirements.supportedFormats.join(', '),
})}
</p>
<Dropzone
files={acceptedFiles}
supportedFormats={fileRequirements.supportedFormats}
{onKeydown}
{onClick}
{onDrop}
{onChange}
/>
<input id="submit" type="submit" value={$t('viewer.render')} />
<p class="disclaimer">
<em>{$t('viewer.warning.copyright')}</em>
</p>
</form>
{#if areAcceptedFilesRendered}
<CardList files={acceptedFiles} />
{:else}
<FileList title={$t('viewer.selected')} files={acceptedFiles} />
<FileList title={$t('viewer.rejected')} files={rejectedFiles} isError />
{/if}
<style>
form {
width: fit-content;
margin: 0 auto;
}
.title-container {
display: flex;
justify-content: space-between;
align-items: center;
}
#submit {
border: none;
border-radius: 10px;
padding: 15px;
}
.disclaimer {
font-size: 13px;
text-align: center;
}
@media only screen and (max-device-width: 768px) {
#form {
width: 100%;
}
}
</style>