Add viewer route
This commit is contained in:
parent
3c55ab88e4
commit
984d99e0e2
24 changed files with 5448 additions and 2924 deletions
5770
package-lock.json
generated
5770
package-lock.json
generated
File diff suppressed because it is too large
Load diff
70
package.json
70
package.json
|
@ -1,37 +1,37 @@
|
||||||
{
|
{
|
||||||
"name": "embroidery-viewer",
|
"name": "embroidery-viewer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check . && eslint ."
|
"lint": "prettier --check . && eslint ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-svelte": "^3.9.1",
|
"eslint-plugin-svelte": "^3.9.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^6.2.6"
|
"vite": "^6.2.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
"sveltekit-i18n": "^2.4.2"
|
"sveltekit-i18n": "^2.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
src/lib/assets/upload.svg
Normal file
18
src/lib/assets/upload.svg
Normal 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 |
143
src/lib/components/CardList.svelte
Normal file
143
src/lib/components/CardList.svelte
Normal 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>
|
85
src/lib/components/Dropzone.svelte
Normal file
85
src/lib/components/Dropzone.svelte
Normal 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>
|
63
src/lib/components/FileList.svelte
Normal file
63
src/lib/components/FileList.svelte
Normal 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>
|
141
src/lib/file-renderer/index.js
Normal file
141
src/lib/file-renderer/index.js
Normal 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 '';
|
||||||
|
}
|
851
src/lib/file-renderer/jdataview.js
Normal file
851
src/lib/file-renderer/jdataview.js
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
331
src/lib/file-renderer/pattern.js
Normal file
331
src/lib/file-renderer/pattern.js
Normal 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 };
|
89
src/lib/format-readers/dst.js
Normal file
89
src/lib/format-readers/dst.js
Normal 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();
|
||||||
|
}
|
56
src/lib/format-readers/exp.js
Normal file
56
src/lib/format-readers/exp.js
Normal 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();
|
||||||
|
}
|
19
src/lib/format-readers/index.js
Normal file
19
src/lib/format-readers/index.js
Normal 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 };
|
212
src/lib/format-readers/jef.js
Normal file
212
src/lib/format-readers/jef.js
Normal 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;
|
18
src/lib/format-readers/pec.js
Normal file
18
src/lib/format-readers/pec.js
Normal 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;
|
||||||
|
}
|
192
src/lib/format-readers/pes.js
Normal file
192
src/lib/format-readers/pes.js
Normal 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;
|
31
src/lib/format-readers/types.js
Normal file
31
src/lib/format-readers/types.js
Normal 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
|
||||||
|
*/
|
14
src/lib/translations/en-US/viewer.json
Normal file
14
src/lib/translations/en-US/viewer.json
Normal 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."
|
||||||
|
}
|
|
@ -60,6 +60,12 @@ const config = {
|
||||||
loader: async () =>
|
loader: async () =>
|
||||||
(await import('./en-US/terms-of-service.json')).default,
|
(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,
|
locale: SUPPORTED_LOCALES.PT_BR,
|
||||||
key: 'header',
|
key: 'header',
|
||||||
|
@ -101,6 +107,12 @@ const config = {
|
||||||
loader: async () =>
|
loader: async () =>
|
||||||
(await import('./pt-BR/terms-of-service.json')).default,
|
(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,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
14
src/lib/translations/pt-BR/viewer.json
Normal file
14
src/lib/translations/pt-BR/viewer.json
Normal 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."
|
||||||
|
}
|
47
src/lib/utils/filterFiles.js
Normal file
47
src/lib/utils/filterFiles.js
Normal 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
25
src/lib/utils/rgbToHex.js
Normal 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 (0–255).
|
||||||
|
* @returns {string} The hex color string (e.g., "#ffcc00").
|
||||||
|
*/
|
||||||
|
const rgbToHex = (color) => {
|
||||||
|
return (
|
||||||
|
'#' +
|
||||||
|
componentToHex(color.r) +
|
||||||
|
componentToHex(color.g) +
|
||||||
|
componentToHex(color.b)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { rgbToHex };
|
27
src/lib/utils/shadeColor.js
Normal file
27
src/lib/utils/shadeColor.js
Normal 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 };
|
|
@ -1,7 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { t, locale } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
|
|
||||||
console.log($locale);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section aria-labelledby="about-heading">
|
<section aria-labelledby="about-heading">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue