Compare commits

..

No commits in common. "main" and "refactor_pes" have entirely different histories.

178 changed files with 2204 additions and 8054 deletions

View file

@ -1,17 +0,0 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml,*.js}]
indent_style = space
indent_size = 2

View file

@ -1,32 +0,0 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
env: {
browser: true,
es2022: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:svelte/recommended',
'prettier',
],
overrides: [
{
files: ['*.svelte'],
processor: 'svelte3/svelte3',
},
],
plugins: ['svelte'],
settings: {
// Let ESLint understand Svelte
'svelte3/ignore-styles': () => true,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
// Customize your rules here
},
};

View file

@ -1,36 +0,0 @@
name: Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: docker
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.SSH_KEY }}" > ./deploy.key
chmod 600 ./deploy.key
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Create env file
run: |
touch .env
echo EMAIL_ACCESS_KEY=${{ secrets.EMAIL_ACCESS_KEY }} >> .env
echo EMAIL_BASE_URL=${{ secrets.EMAIL_BASE_URL }} >> .env
- name: Verify .env file creation
run: cat .env
- name: Install PM2
run: npm i -g pm2
- name: Deploy
run: env $(cat .env | grep -v \"#\" | xargs) pm2 deploy ecosystem.config.cjs production

27
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Deploy
on:
push:
branches: ["main"]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
env:
SSH_KEY: ${{secrets.SSH_KEY}}
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16
uses: actions/setup-node@v3
with:
node-version: 16
cache: "npm"
- run: npm install
- run: npm run build
- run: mkdir ~/.ssh
- run: echo "$SSH_KEY" >> ~/.ssh/id_rsa_embroideryviewer
- run: chmod 400 ~/.ssh/id_rsa_embroideryviewer
- run: echo -e "Host embroideryviewer\n\tUser embroideryviewer\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_rsa_embroideryviewer\n\tStrictHostKeyChecking No" >> ~/.ssh/config
- run: rsync -avz --progress dist/ embroideryviewer:web/prod

34
.gitignore vendored
View file

@ -1,11 +1,25 @@
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
deploy.key
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/.vscode

View file

@ -1,22 +0,0 @@
# Ignore node_modules
node_modules/
# Build output
.build/
.svelte-kit/
dist/
# Ignore lock files
package-lock.json
pnpm-lock.yaml
yarn.lock
# Ignore environment files
.env
.env.*.local
# VSCode settings
.vscode/
# Ignore output from lint or test tools
coverage/

View file

@ -1,10 +0,0 @@
{
"singleQuote": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80,
"semi": true,
"bracketSpacing": true,
"arrowParens": "always"
}

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Leonardo Murça
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,73 +1,10 @@
# 🧵 Embroidery Viewer
# Embroidery Viewer
![Logo](/logo.webp)
![Deploy workflow status](https://git.leomurca.xyz/leomurca/embroidery-viewer/actions/workflows/deploy.yml/badge.svg)
**The simplest way to preview embroidery files — instantly, in your browser.**
👉 **Try it now:** https://embroideryviewer.xyz
A free online tool to view embroidery files.
Available at https://embroideryviewer.xyz.
![Demo](/demo.gif)
<a href="https://buymeacoffee.com/embroideryviewerxyz">
<img src="docs/yellow-button.png" width="200" alt="Alt Text">
</a>
Current supported formats: **.pes, .dst, .pec, .jef and .exp**.
---
## ✨ Why Embroidery Viewer?
Working with embroidery files shouldnt require heavy, expensive software.
Embroidery Viewer was built to solve a simple problem:
> _“I just want to quickly see my design.”_
No installs. No friction. Just drag, drop, and view.
---
## 🚀 Features
- ⚡ **Instant preview** — open files in seconds
- 🔒 **Private by design** — everything runs in your browser
- 🧵 **Multiple formats supported**
- 🖥️ **Works on any device** (desktop, tablet, mobile)
- 📂 **Batch-friendly** — view multiple files in sequence
---
## 📁 Supported Formats
- `.pes`
- `.dst`
- `.pec`
- `.jef`
- `.exp`
---
## 🧠 How it works
All processing happens **client-side** using modern web technologies.
Your files are never uploaded to any server.
---
## 💡 Inspiration
Inspired by:
https://github.com/redteam316/html5-embroidery.git
---
## ❤️ Support the project
If this tool helped you, consider supporting its development:
- Share it with others
- Give feedback
- Or contribute to the project
---
Inspired by https://github.com/redteam316/html5-embroidery.git.

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

After

Width:  |  Height:  |  Size: 13 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,38 +0,0 @@
module.exports = {
apps: [
{
name: 'embroidery-viewer-prod',
script: './build/index.js',
time: true,
instances: 1,
autorestart: true,
max_restarts: 50,
watch: false,
max_memory_restart: '1G',
env: {
PORT: 7281,
PUBLIC_APP_ENV: 'production',
NODE_ENV: 'production',
},
},
],
deploy: {
production: {
user: 'deployer',
host: '45.76.5.44',
key: 'deploy.key',
ref: 'origin/main',
repo: 'git@git.leomurca.xyz:leomurca/embroidery-viewer.git',
path: '/home/deployer/embroidery-viewer',
'pre-deploy':
'rm -rf node_modules build .svelte-kit && npm ci && PUBLIC_APP_ENV=production npm run build',
'post-deploy':
'pm2 startOrReload ecosystem.config.cjs --only embroidery-viewer-prod --env production && pm2 save',
env: {
PORT: 7281,
PUBLIC_APP_ENV: 'production',
NODE_ENV: 'production',
},
},
},
};

View file

@ -1,26 +0,0 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default [
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
}
},
{
files: ['**/*.svelte', '**/*.svelte.js'],
languageOptions: { parserOptions: { svelteConfig } }
}
];

42
index.html Normal file
View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="Leonardo Murça" />
<meta name="description" content="Free online embroidery viewer.">
<meta name="keywords"
content="Free Emrbroidery Viewer, embroidery design, sewing machine, preview .pes files, preview embroider designs, brother machine.">
<script async defer data-website-id="e9089f5e-32ea-45f1-a000-b16af1dba58a"
src="https://umami.leomurca.xyz/umami.js"></script>
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<title>Embroidery Viewer</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View file

@ -1,19 +1,33 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
"compilerOptions": {
"moduleResolution": "Node",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

BIN
logo.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

3655
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,38 +1,17 @@
{
"name": "embroidery-viewer",
"private": true,
"version": "3.0.3",
"version": "1.2.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
"postbuild": "npx svelte-sitemap --domain https://embroideryviewer.xyz -o dist"
},
"devDependencies": {
"@eslint/compat": "^2.0.5",
"@eslint/js": "^10.0.1",
"@sveltejs/kit": "^2.57.1",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@types/node": "^25.6.0",
"eslint": "^10.2.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.17.0",
"globals": "^17.5.0",
"prettier": "^3.8.3",
"prettier-plugin-svelte": "^3.5.1",
"svelte": "^5.55.4",
"svelte-check": "^4.4.6",
"typescript": "^6.0.2",
"vite": "^8.0.8"
},
"dependencies": {
"@sveltejs/adapter-node": "^5.5.4",
"accept-language-parser": "^1.5.0",
"sveltekit-i18n": "^2.4.2"
"@sveltejs/vite-plugin-svelte": "^2.4.1",
"svelte": "^3.59.1",
"vite": "^4.3.9"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/apple-icon-57x57.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/apple-icon-60x60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/apple-icon-72x72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
public/apple-icon-76x76.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

2
public/browserconfig.xml Normal file
View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/favicon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

41
public/manifest.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

BIN
public/ms-icon-144x144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
public/ms-icon-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
public/ms-icon-310x310.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/ms-icon-70x70.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

22
src/App.svelte Normal file
View file

@ -0,0 +1,22 @@
<script>
import Header from "./lib/Header.svelte";
import FileViewer from "./lib/FileViewer.svelte";
import Footer from "./lib/Footer.svelte";
</script>
<Header />
<main>
<FileViewer />
</main>
<Footer />
<style>
main {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 15px;
}
</style>

50
src/app.css Normal file
View file

@ -0,0 +1,50 @@
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
margin: 0;
width: 100%;
min-height: 100vh;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
background-color: #F2F6F5;
z-index: 10;
}
input[type="submit"] {
width: 100%;
font-size: 20px;
margin-top: 20px;
background-color: #05345f;
font-weight: 700;
color: white;
padding: 10px;
-webkit-appearance: none;
border-radius: 0;
}
input[type="submit"]:hover {
cursor: pointer;
background-color: black;
color: white;
}

13
src/app.d.ts vendored
View file

@ -1,13 +0,0 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View file

@ -1,47 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<script
data-name="BMC-Widget"
data-cfasync="false"
src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js"
data-id="embroideryviewerxyz"
data-description="Support me on Buy me a coffee!"
data-message="Enjoying Embroidery Viewer?
Buy me a coffee and help keep it running ☕"
data-color="#FFDD03"
data-position="Right"
data-x_margin="18"
data-y_margin="18"
></script>
<!-- Basic -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="Embroidery Viewer" />
<meta name="theme-color" content="#ffffff" />
<!-- Favicon -->
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="canonical" href="https://embroideryviewer.xyz/" />
<!-- Mobile / PWA friendliness -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link
rel="preload"
href="/fonts/merienda.regular.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/thumbnail.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View file

@ -0,0 +1,74 @@
import { jDataView } from "./jdataview";
import { supportedFormats } from "../format-readers";
import { Pattern } from "./pattern";
function renderFile(filename, evt, canvas, colorView, stitchesView, sizeView) {
const fileExtension = filename.toLowerCase().split(".").pop();
const view = jDataView(evt.target.result, 0, evt.size);
const pattern = new Pattern();
supportedFormats[fileExtension].read(view, pattern);
pattern.moveToPositive();
pattern.drawShapeTo(canvas);
pattern.drawColorsTo(colorView);
pattern.drawStitchesCountTo(stitchesView);
pattern.drawSizeValuesTo(stitchesView);
}
function renderAbortMessage(errorMessageRef) {
errorMessageRef.innerHTML = "Render aborted!";
}
function renderErrorMessage(errorName, errorMessageRef) {
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 occured. This can be due to:</p>";
message +=
"<ul><li>Accessing certain files deemed unsafe for Web applications.</li>";
message += "<li>Performing too many read calls on file resources.</li>";
message +=
"<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 wrong happened!";
break;
}
errorMessageRef.innerHTML = message;
}
export default function renderFileToCanvas(
fileObject,
canvas,
errorMessageRef,
colorView,
stitchesView,
sizeView
) {
const reader = new FileReader();
reader.onloadend = (evt) =>
renderFile(fileObject.name, evt, canvas, colorView, stitchesView, sizeView);
reader.abort = (/** @type {any} */ _) => renderAbortMessage(errorMessageRef);
reader.onerror = (evt) =>
renderErrorMessage(evt.target.error.name, errorMessageRef);
if (fileObject) {
reader.readAsArrayBuffer(fileObject);
}
return "";
}

View file

@ -1,7 +1,5 @@
/* 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
//
@ -11,22 +9,22 @@ import { browser } from '$app/environment';
var compatibility = {
// NodeJS Buffer in v0.5.5 and newer
NodeBuffer: 'Buffer' in globalThis && 'readInt16LE' in Buffer.prototype,
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,
"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,
"CanvasPixelArray" in globalThis &&
"ImageData" in globalThis &&
"document" in globalThis,
};
var createPixelData = function (byteLength, buffer) {
var data = createPixelData.context2d.createImageData(
(byteLength + 3) / 4,
1,
1
).data;
data.byteLength = byteLength;
if (buffer !== undefined) {
@ -36,8 +34,7 @@ var createPixelData = function (byteLength, buffer) {
}
return data;
};
createPixelData.context2d =
browser ?? document.createElement('canvas').getContext('2d');
createPixelData.context2d = document.createElement("canvas").getContext("2d");
var dataTypes = {
Int8: 1,
@ -51,14 +48,14 @@ var dataTypes = {
};
var nodeNaming = {
Int8: 'Int8',
Int16: 'Int16',
Int32: 'Int32',
Uint8: 'UInt8',
Uint16: 'UInt16',
Uint32: 'UInt32',
Float32: 'Float',
Float64: 'Double',
Int8: "Int8",
Int16: "Int16",
Int32: "Int32",
Uint8: "UInt8",
Uint16: "UInt16",
Uint32: "UInt32",
Float32: "Float",
Float64: "Double",
};
function arrayFrom(arrayLike, forceCopy) {
@ -101,13 +98,13 @@ export function jDataView(buffer, byteOffset, byteLength, littleEndian) {
!this._isPixelData &&
!(buffer instanceof Array)
) {
throw new TypeError('jDataView buffer has an incompatible type');
throw new TypeError("jDataView buffer has an incompatible type");
}
// Default Values
this._littleEndian = !!littleEndian;
var bufferLength = 'byteLength' in buffer ? buffer.byteLength : buffer.length;
var bufferLength = "byteLength" in buffer ? buffer.byteLength : buffer.length;
this.byteOffset = byteOffset = defined(byteOffset, 0);
this.byteLength = byteLength = defined(byteLength, bufferLength - byteOffset);
@ -122,15 +119,15 @@ export function jDataView(buffer, byteOffset, byteLength, littleEndian) {
this._engineAction = this._isDataView
? this._dataViewAction
: this._isNodeBuffer
? this._nodeBufferAction
: this._isArrayBuffer
? this._arrayBufferAction
: this._arrayAction;
? this._nodeBufferAction
: this._isArrayBuffer
? this._arrayBufferAction
: this._arrayAction;
}
function getCharCodes(string) {
if (compatibility.NodeBuffer) {
return new Buffer(string, 'binary');
return new Buffer(string, "binary");
}
var Type = compatibility.ArrayBuffer ? Uint8Array : Array,
@ -145,7 +142,7 @@ function getCharCodes(string) {
// 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':
case "number":
if (compatibility.NodeBuffer) {
buffer = new Buffer(buffer);
buffer.fill(0);
@ -161,12 +158,12 @@ jDataView.wrapBuffer = function (buffer) {
}
return buffer;
case 'string':
case "string":
buffer = getCharCodes(buffer);
/* falls through */
default:
if (
'length' in buffer &&
"length" in buffer &&
!(
(compatibility.NodeBuffer && buffer instanceof Buffer) ||
(compatibility.ArrayBuffer && buffer instanceof ArrayBuffer) ||
@ -233,7 +230,7 @@ function Int64(lo, hi) {
jDataView.Int64 = Int64;
Int64.prototype =
'create' in Object ? Object.create(Uint64.prototype) : new Uint64();
"create" in Object ? Object.create(Uint64.prototype) : new Uint64();
Int64.prototype.valueOf = function () {
if (this.hi < pow2(31)) {
@ -264,20 +261,20 @@ jDataView.prototype = {
_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 byteOffset !== "number") {
throw new TypeError("Offset is not a number.");
}
if (typeof byteLength !== 'number') {
throw new TypeError('Size 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.');
throw new RangeError("Length is negative.");
}
if (
byteOffset < 0 ||
byteOffset + byteLength > defined(maxLength, this.byteLength)
) {
throw new RangeError('Offsets are out of bounds.');
throw new RangeError("Offsets are out of bounds.");
}
},
@ -287,7 +284,7 @@ jDataView.prototype = {
isReadAction,
defined(byteOffset, this._offset),
defined(littleEndian, this._littleEndian),
value,
value
);
},
@ -296,13 +293,13 @@ jDataView.prototype = {
isReadAction,
byteOffset,
littleEndian,
value,
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);
? this._view["get" + type](byteOffset, littleEndian)
: this._view["set" + type](byteOffset, value, littleEndian);
},
_nodeBufferAction: function (
@ -310,17 +307,17 @@ jDataView.prototype = {
isReadAction,
byteOffset,
littleEndian,
value,
value
) {
// Move the internal offset forward
this._offset = byteOffset + dataTypes[type];
var nodeName =
nodeNaming[type] +
(type === 'Int8' || type === 'Uint8' ? '' : littleEndian ? 'LE' : 'BE');
(type === "Int8" || type === "Uint8" ? "" : littleEndian ? "LE" : "BE");
byteOffset += this.byteOffset;
return isReadAction
? this.buffer['read' + nodeName](byteOffset)
: this.buffer['write' + nodeName](value, byteOffset);
? this.buffer["read" + nodeName](byteOffset)
: this.buffer["write" + nodeName](value, byteOffset);
},
_arrayBufferAction: function (
@ -328,10 +325,10 @@ jDataView.prototype = {
isReadAction,
byteOffset,
littleEndian,
value,
value
) {
var size = dataTypes[type],
TypedArray = globalThis[type + 'Array'],
TypedArray = globalThis[type + "Array"],
typedArray;
littleEndian = defined(littleEndian, this._littleEndian);
@ -348,7 +345,7 @@ jDataView.prototype = {
var bytes = new Uint8Array(
isReadAction
? this.getBytes(size, byteOffset, littleEndian, true)
: size,
: size
);
typedArray = new TypedArray(bytes.buffer, 0, 1);
@ -363,8 +360,8 @@ jDataView.prototype = {
_arrayAction: function (type, isReadAction, byteOffset, littleEndian, value) {
return isReadAction
? this['_get' + type](byteOffset, littleEndian)
: this['_set' + type](byteOffset, value, littleEndian);
? this["_get" + type](byteOffset, littleEndian)
: this["_set" + type](byteOffset, value, littleEndian);
},
// Helpers
@ -385,7 +382,7 @@ jDataView.prototype = {
: (this.buffer.slice || Array.prototype.slice).call(
this.buffer,
byteOffset,
byteOffset + length,
byteOffset + length
);
return littleEndian || length <= 1 ? result : arrayFrom(result).reverse();
@ -396,7 +393,7 @@ jDataView.prototype = {
var result = this._getBytes(
length,
byteOffset,
defined(littleEndian, true),
defined(littleEndian, true)
);
return toArray ? arrayFrom(result) : result;
},
@ -448,18 +445,18 @@ jDataView.prototype = {
this._offset = byteOffset + byteLength;
return this.buffer.toString(
encoding || 'binary',
encoding || "binary",
this.byteOffset + byteOffset,
this.byteOffset + this._offset,
this.byteOffset + this._offset
);
}
var bytes = this._getBytes(byteLength, byteOffset, true),
string = '';
string = "";
byteLength = bytes.length;
for (var i = 0; i < byteLength; i++) {
string += String.fromCharCode(bytes[i]);
}
if (encoding === 'utf8') {
if (encoding === "utf8") {
string = decodeURIComponent(escape(string));
}
return string;
@ -474,11 +471,11 @@ jDataView.prototype = {
this.buffer.write(
subString,
this.byteOffset + byteOffset,
encoding || 'binary',
encoding || "binary"
);
return;
}
if (encoding === 'utf8') {
if (encoding === "utf8") {
subString = unescape(encodeURIComponent(subString));
}
this._setBytes(byteOffset, getCharCodes(subString), true);
@ -519,13 +516,13 @@ jDataView.prototype = {
this.getBytes(end - start, start, true, true),
undefined,
undefined,
this._littleEndian,
this._littleEndian
)
: new jDataView(
this.buffer,
this.byteOffset + start,
end - start,
this._littleEndian,
this._littleEndian
);
},
@ -682,7 +679,7 @@ jDataView.prototype = {
value,
mantSize,
expSize,
littleEndian,
littleEndian
) {
var signBit = value < 0 ? 1 : 0,
exponent,
@ -754,7 +751,7 @@ jDataView.prototype = {
this.setUint32(
byteOffset + parts[partName],
value[partName],
littleEndian,
littleEndian
);
}
@ -773,7 +770,7 @@ jDataView.prototype = {
this._setBytes(
byteOffset,
[value & 0xff, (value >>> 8) & 0xff, (value >>> 16) & 0xff, value >>> 24],
littleEndian,
littleEndian
);
},
@ -781,7 +778,7 @@ jDataView.prototype = {
this._setBytes(
byteOffset,
[value & 0xff, (value >>> 8) & 0xff],
littleEndian,
littleEndian
);
},
@ -811,10 +808,10 @@ var proto = jDataView.prototype;
for (var type in dataTypes) {
(function (type) {
proto['get' + type] = function (byteOffset, littleEndian) {
proto["get" + type] = function (byteOffset, littleEndian) {
return this._action(type, true, byteOffset, littleEndian);
};
proto['set' + type] = function (byteOffset, value, littleEndian) {
proto["set" + type] = function (byteOffset, value, littleEndian) {
this._action(type, false, byteOffset, littleEndian, value);
};
})(type);
@ -826,19 +823,19 @@ proto._setInt8 = proto._setUint8;
proto.setSigned = proto.setUnsigned;
for (var method in proto) {
if (method.slice(0, 3) === 'set') {
if (method.slice(0, 3) === "set") {
(function (type) {
proto['write' + type] = function () {
proto["write" + type] = function () {
Array.prototype.unshift.call(arguments, undefined);
this['set' + type].apply(this, arguments);
this["set" + type].apply(this, arguments);
};
})(method.slice(3));
}
}
if (typeof module !== 'undefined' && typeof module.exports === 'object') {
if (typeof module !== "undefined" && typeof module.exports === "object") {
module.exports = jDataView;
} else if (typeof define === 'function' && define.amd) {
} else if (typeof define === "function" && define.amd) {
define([], function () {
return jDataView;
});

View file

@ -0,0 +1,225 @@
import { rgbToHex } from "../utils/rgbToHex";
import { shadeColor } from "../utils/shadeColor";
function Stitch(x, y, flags, color) {
this.flags = flags;
this.x = x;
this.y = y;
this.color = color;
}
function Color(r, g, b, description) {
this.r = r;
this.g = g;
this.b = b;
this.description = description;
}
const stitchTypes = {
normal: 0,
jump: 1,
trim: 2,
stop: 4,
end: 8,
};
function Pattern() {
this.colors = [];
this.stitches = [];
this.hoop = {};
this.lastX = 0;
this.lastY = 0;
this.top = 0;
this.bottom = 0;
this.left = 0;
this.right = 0;
this.currentColorIndex = 0;
}
Pattern.prototype.addColorRgb = function (r, g, b, description) {
this.colors[this.colors.length] = new Color(r, g, b, description);
};
Pattern.prototype.addColor = function (color) {
this.colors[this.colors.length] = color;
};
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[this.stitches.length] = new Stitch(
x,
y,
flags,
this.currentColorIndex
);
};
Pattern.prototype.addStitchRel = function (dx, dy, flags, isAutoColorIndex) {
if (this.stitches.length !== 0) {
let nx = this.lastX + dx,
ny = this.lastY + dy;
this.lastX = nx;
this.lastY = ny;
this.addStitchAbs(nx, ny, flags, isAutoColorIndex);
} else {
this.addStitchAbs(dx, dy, flags, isAutoColorIndex);
}
};
Pattern.prototype.calculateBoundingBox = function () {
let i = 0,
stitchCount = this.stitches.length,
pt;
if (stitchCount === 0) {
this.bottom = 1;
this.right = 1;
return;
}
this.left = 99999;
this.top = 99999;
this.right = -99999;
this.bottom = -99999;
for (i = 0; i < stitchCount; i += 1) {
pt = this.stitches[i];
if (!(pt.flags & stitchTypes.trim)) {
this.left = this.left < pt.x ? this.left : pt.x;
this.top = this.top < pt.y ? this.top : pt.y;
this.right = this.right > pt.x ? this.right : pt.x;
this.bottom = this.bottom > pt.y ? this.bottom : pt.y;
}
}
};
Pattern.prototype.moveToPositive = function () {
let i = 0,
stitchCount = this.stitches.length;
for (i = 0; i < stitchCount; i += 1) {
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;
};
Pattern.prototype.invertPatternVertical = function () {
let i = 0,
temp = -this.top,
stitchCount = this.stitches.length;
for (i = 0; i < stitchCount; i += 1) {
this.stitches[i].y = -this.stitches[i].y;
}
this.top = -this.bottom;
this.bottom = temp;
};
Pattern.prototype.addColorRandom = function () {
this.colors[this.colors.length] = new Color(
Math.round(Math.random() * 256),
Math.round(Math.random() * 256),
Math.round(Math.random() * 256),
"random"
);
};
Pattern.prototype.fixColorCount = function () {
let maxColorIndex = 0,
stitchCount = this.stitches.length,
i;
for (i = 0; i < stitchCount; i += 1) {
maxColorIndex = Math.max(maxColorIndex, this.stitches[i].color);
}
while (this.colors.length <= maxColorIndex) {
this.addColorRandom();
}
this.colors.splice(maxColorIndex + 1, this.colors.length - maxColorIndex - 1);
};
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");
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;
}
}
};
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;'></div>`;
});
};
Pattern.prototype.drawStitchesCountTo = function (stitchesContainer) {
stitchesContainer.innerHTML += `<div><strong>Stitches:</strong> ${this.stitches.length} </div>`;
};
Pattern.prototype.drawSizeValuesTo = function (sizeContainer) {
sizeContainer.innerHTML += `<div><strong>Size (x, y):</strong> ${Math.round(
this.right / 10
)}mm x ${Math.round(this.bottom / 10)}mm </div>`;
};
export { Pattern, Color, stitchTypes };

107
src/format-readers/dst.js Normal file
View file

@ -0,0 +1,107 @@
// @ts-nocheck
import { stitchTypes } from "../file-renderer/pattern";
function decodeExp(b2) {
let returnCode = 0;
if (b2 === 0xf3) {
return stitchTypes.end;
}
if ((b2 & 0xc3) === 0xc3) {
return stitchTypes.trim | stitchTypes.stop;
}
if (b2 & 0x80) {
returnCode |= stitchTypes.trim;
}
if (b2 & 0x40) {
returnCode |= stitchTypes.stop;
}
return returnCode;
}
export function dstRead(file, pattern) {
let flags,
x,
y,
prevJump = false,
thisJump = false,
b = [],
byteCount = file.byteLength;
file.seek(512);
while (file.tell() < byteCount - 3) {
b[0] = file.getUint8();
b[1] = file.getUint8();
b[2] = file.getUint8();
x = 0;
y = 0;
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[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] & 0x01) {
x += 3;
}
if (b[1] & 0x02) {
x -= 3;
}
if (b[1] & 0x04) {
x += 27;
}
if (b[1] & 0x08) {
x -= 27;
}
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] & 0x04) {
x += 81;
}
if (b[2] & 0x08) {
x -= 81;
}
if (b[2] & 0x20) {
y += 81;
}
if (b[2] & 0x10) {
y -= 81;
}
flags = decodeExp(b[2]);
thisJump = flags & stitchTypes.jump;
if (prevJump) {
flags |= stitchTypes.jump;
}
pattern.addStitchRel(x, y, flags, true);
prevJump = thisJump;
}
pattern.addStitchRel(0, 0, stitchTypes.end, true);
pattern.invertPatternVertical();
}

50
src/format-readers/exp.js Normal file
View file

@ -0,0 +1,50 @@
import { stitchTypes } from "../file-renderer/pattern";
function expDecode(input) {
return input > 128 ? -(~input & 255) - 1 : input;
}
export function expRead(file, pattern) {
let b0 = 0,
b1 = 0,
dx = 0,
dy = 0,
flags = 0,
i = 0,
byteCount = file.byteLength;
while (i < byteCount) {
flags = stitchTypes.normal;
b0 = file.getInt8(i);
i += 1;
b1 = file.getInt8(i);
i += 1;
if (b0 === -128) {
if (b1 & 1) {
b0 = file.getInt8(i);
i += 1;
b1 = file.getInt8(i);
i += 1;
flags = stitchTypes.stop;
} else if (b1 === 2 || b1 === 4) {
b0 = file.getInt8(i);
i += 1;
b1 = file.getInt8(i);
i += 1;
flags = stitchTypes.trim;
} else if (b1 === -128) {
b0 = file.getInt8(i);
i += 1;
b1 = file.getInt8(i);
i += 1;
b0 = 0;
b1 = 0;
flags = stitchTypes.trim;
}
}
dx = expDecode(b0);
dy = expDecode(b1);
pattern.addStitchRel(dx, dy, flags, true);
}
pattern.addStitchRel(0, 0, stitchTypes.end);
pattern.invertPatternVertical();
}

View file

@ -0,0 +1,15 @@
import { dstRead } from "./dst";
import { expRead } from "./exp";
import { jefRead } from "./jef";
import { pecRead } from "./pec";
import { pesRead } from "./pes";
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 };

135
src/format-readers/jef.js Normal file
View file

@ -0,0 +1,135 @@
import { Color, stitchTypes } from "../file-renderer/pattern";
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"),
];
function jefDecode(inputByte) {
return inputByte >= 0x80 ? -(~inputByte & 0xff) - 1 : inputByte;
}
export function jefRead(file, pattern) {
file.seek(24);
const numberOfColors = file.getInt32(file.tell(), true);
const numberOfStitches = file.getInt32(file.tell(), true);
file.seek(file.tell() + 84);
for (let i = 0; i < numberOfColors; i += 1) {
pattern.addColor(colors[file.getUint32(file.tell(), true) % 78]);
}
for (let i = 0; i < 6 - numberOfColors; i += 1) {
file.getUint32();
}
let flags,
b0,
b1,
dx,
dy,
stitchCount = 0;
while (stitchCount < numberOfStitches + 100) {
flags = stitchTypes.normal;
b0 = file.getUint8();
b1 = file.getUint8();
if (b0 === 0x80) {
if (b1 & 0x01) {
b0 = file.getUint8();
b1 = file.getUint8();
flags = stitchTypes.stop;
} else if (b1 === 0x02 || b1 === 0x04) {
b0 = file.getUint8();
b1 = file.getUint8();
flags = stitchTypes.trim;
} else if (b1 === 0x10) {
pattern.addStitchRel(0, 0, stitchTypes.end, true);
break;
}
}
dx = jefDecode(b0);
dy = jefDecode(b1);
pattern.addStitchRel(dx, dy, flags, true);
stitchCount += 1;
}
pattern.invertPatternVertical();
}
export const jefColors = colors;

View file

@ -1,10 +1,5 @@
import { pecColors, pecReadStitches } from './pes';
import { pecColors, pecReadStitches } from "./pes";
/**
*
* @param {EmbroideryFileView} file
* @param {EmbroideryPattern} pattern
*/
export function pecRead(file, pattern) {
let colorChanges, i;
file.seek(0x38);

153
src/format-readers/pes.js Normal file
View file

@ -0,0 +1,153 @@
import { Color, stitchTypes } from "../file-renderer/pattern";
const namedColors = [
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"),
];
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);
stitchNumber++;
}
}
function isEndStitch(xOffset, yOffset) {
return xOffset === 0xff && yOffset === 0x00;
}
function isStopStitch(xOffset, yOffset) {
return xOffset === 0xfe && yOffset === 0xb0;
}
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;
}
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];
}
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(namedColors[file.getInt8()]);
}
file.seek(pecStart + 532);
readPecStitches(file, pattern);
pattern.addStitchRel(0, 0, stitchTypes.end);
}
export const pecReadStitches = readPecStitches;
export const pecColors = namedColors;

View file

@ -1,72 +1,47 @@
<script>
import { t } from '$lib/translations';
import renderFileToCanvas from '$lib/file-renderer';
import renderFileToCanvas from "../file-renderer";
/**
* @type {ArrayLike<any>}
*/
export let files = [];
/**
* @type {HTMLElement}
*/
let errorMessageRef;
let canvasRefs = [];
let colorRefs = [];
let stitchesRefs = [];
let sizeRefs = [];
let localizedStrings = {
stitches: $t('viewer.stitches'),
dimensions: $t('viewer.dimensions'),
};
let errorMessageRef;
/**
* Downloads a given HTMLCanvasElement as a PNG image.
*
* @param {HTMLCanvasElement} canvas - The canvas element to export as an image.
* @param {string} filename - The desired name of the downloaded file (extension will be replaced with `.png`).
*/
const downloadCanvasAsImage = (canvas, filename) => {
const image = canvas
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream');
.toDataURL("image/png")
.replace("image/png", "image/octet-stream");
const link = document.createElement('a');
link.download = `${filename.split('.').slice(0, -1).join('.')}.png`;
const link = document.createElement("a");
link.download = `${filename.split(".").slice(0, -1).join(".")}.png`;
link.href = image;
link.click();
};
/**
* Cliks the button to render the files when user press enter.
*
* @param {KeyboardEvent} evt - The event that triggered the language switch.
*/
const onKeydown = (evt) => {
if (evt.key === 'Enter') {
const button = document.getElementById('download-button');
if (button) button.click();
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 (i)}
{#each Array.from(files) as file, i}
<div class="canvas-container">
<canvas bind:this={canvasRefs[i]} class="canvas"></canvas>
<canvas bind:this={canvasRefs[i]} class="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 class="stitches-container" bind:this={stitchesRefs[i]} />
<div class="size-container" bind:this={sizeRefs[i]} />
<div class="colors-container" bind:this={colorRefs[i]} />
<div
id="download-button"
role="button"
tabindex="0"
onkeydown={onKeydown}
onclick={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
on:keydown={onKeydown}
on:click={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
>
{$t('viewer.download')}
Download
</div>
</div>
{canvasRefs[i] &&
@ -76,12 +51,11 @@
errorMessageRef,
colorRefs[i],
stitchesRefs[i],
sizeRefs[i],
localizedStrings,
sizeRefs[i]
)}
{/each}
<!-- svelte-ignore a11y-missing-content -->
<h1 bind:this={errorMessageRef}></h1>
<h1 bind:this={errorMessageRef} />
</div>
{/if}
@ -103,9 +77,7 @@
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;
border: 2px solid black;
}
.canvas {
@ -127,18 +99,15 @@
padding: 10px 0;
}
div[role='button'] {
div[role="button"] {
background-color: #05345f;
font-weight: bold;
font-weight: 500;
color: white;
padding: 10px;
border-radius: 10px;
padding: 10px;
width: 50%;
text-align: center;
border-radius: 0;
}
div[role='button']:hover {
div[role="button"]:hover {
cursor: pointer;
background-color: black;
color: white;
@ -153,10 +122,5 @@
#container {
width: 100%;
}
div[role='button'] {
width: 100%;
padding: 15px;
}
}
</style>

63
src/lib/Dropzone.svelte Normal file
View file

@ -0,0 +1,63 @@
<script>
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}
on:keydown={onKeydown}
on:click={onClick}
on:dragover|preventDefault|stopPropagation
on:drop|preventDefault|stopPropagation={onDrop}
>
<label id="file-label" for="file-input"
>Drag and drop files here or click to upload.</label
>
<input
id="file-input"
type="file"
name="files[]"
accept={supportedFormats.join(",")}
multiple
on:change={onChange}
bind:files
/>
</div>
<style>
#dropzone {
display: flex;
height: 100px;
width: 100%;
border: 5px dotted black;
padding: 15px;
z-index: 10;
}
#file-label {
z-index: -1;
font-weight: 600;
}
#file-input {
display: none;
}
#dropzone:hover {
cursor: pointer;
border: 5px dotted #05345f;
color: #05345f;
}
@media only screen and (max-device-width: 812px) {
#dropzone {
width: 100%;
}
}
</style>

44
src/lib/FileList.svelte Normal file
View file

@ -0,0 +1,44 @@
<script>
export let title;
export let files = [];
export let isError = false;
</script>
{#if files.length !== 0}
<div id="selected-files-container">
<h2>{title}:</h2>
{#each Array.from(files) as file}
<div id={isError ? "selected-file-card-error" : "selected-file-card"}>
<p>{file.name} ({file.size / 1000} kb)</p>
</div>
{/each}
</div>
{/if}
<style>
#selected-file-card {
border: 1px solid #000;
width: 500px;
padding-left: 15px;
margin-top: 10px;
}
#selected-file-card-error {
border: 1px solid red;
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>

86
src/lib/FileViewer.svelte Normal file
View file

@ -0,0 +1,86 @@
<script>
import CardList from "./CardList.svelte";
import Dropzone from "./Dropzone.svelte";
import FileList from "./FileList.svelte";
import { filterFiles } from "../utils/filterFiles";
import { supportedFormats } from "../format-readers";
let acceptedFiles;
let rejectedFiles;
let areAcceptedFilesRendered = false;
const fileRequirements = {
supportedFormats: Object.values(supportedFormats).map((f) => f.ext),
maxSize: 700000,
};
const onSubmit = () => {
areAcceptedFilesRendered = true;
};
const onDrop = (evt) => {
onChange(evt);
};
const onChange = (evt) => {
acceptedFiles = null;
areAcceptedFilesRendered = false;
const changedFiles = evt.dataTransfer
? evt.dataTransfer.files
: evt.target.files;
const results = filterFiles(changedFiles, fileRequirements);
acceptedFiles = results.accepted;
rejectedFiles = results.rejected;
};
const onClick = () => {
document.getElementById("file-input").click();
};
const onKeydown = (evt) => {
if (evt.key === "Enter") {
document.getElementById("file-input").click();
}
};
</script>
<form
id="form"
enctype="multipart/form-data"
on:submit|preventDefault|stopPropagation={onSubmit}
>
<h2>Upload files</h2>
<p>
Max file size is <strong>{fileRequirements.maxSize / 1000}kb</strong>.
Accepted formats:
<strong>{fileRequirements.supportedFormats.join(", ")}</strong>.
</p>
<Dropzone
files={acceptedFiles}
supportedFormats={fileRequirements.supportedFormats}
{onKeydown}
{onClick}
{onDrop}
{onChange}
/>
<input type="submit" value="Render files" />
</form>
{#if areAcceptedFilesRendered}
<CardList files={acceptedFiles} />
{:else}
<FileList title="Rejected Files" files={rejectedFiles} isError />
<FileList title="Selected Files" files={acceptedFiles} />
{/if}
<style>
@media only screen and (max-device-width: 812px) {
#form {
width: 100%;
}
}
</style>

24
src/lib/Footer.svelte Normal file
View file

@ -0,0 +1,24 @@
<script>
import { appVersion } from "../utils/env";
</script>
<footer>
<p>
Copyright © {new Date().getFullYear()}
<a href="https://leomurca.xyz" target="_blank" rel="noreferrer"
>Leonardo Murça</a
>.
</p>
<p>
version: {appVersion()}
</p>
</footer>
<style>
footer {
bottom: 0;
}
p {
text-align: center;
}
</style>

42
src/lib/Header.svelte Normal file
View file

@ -0,0 +1,42 @@
<script>
import MediaQuery from "../lib/MediaQuery.svelte";
import logo from "../assets/embroidery-viewer-logo.webp";
import logoMobile from "../assets/embroidery-viewer-logo-mobile.webp";
const configsFor = (matches) => {
return matches
? { src: logoMobile, width: 350, height: 96 }
: { src: logo, width: 460, height: 200 };
};
</script>
<header>
<a href="/">
<MediaQuery query="(min-width: 481px) and (max-width: 812px)" let:matches>
{@const configs = configsFor(matches)}
<img
class="logo"
alt="Embroidery viewer logo."
src={configs.src}
width={configs.width}
height={configs.height}
/>
</MediaQuery>
</a>
</header>
<style>
header {
margin-top: 100px;
}
.logo {
background-image: logo;
}
@media only screen and (max-device-width: 812px) {
.logo {
width: 100%;
padding: 20px;
}
}
</style>

View file

@ -1,15 +1,9 @@
<script>
import { onMount } from 'svelte';
import { onMount } from "svelte";
export let query;
/**
* @type {MediaQueryList}
*/
let mql;
/**
* @type {((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null}
*/
let mqlListener;
let wasMounted = false;
let matches = false;
@ -28,9 +22,6 @@
}
}
/**
* @param {string} query
*/
function addNewListener(query) {
mql = window.matchMedia(query);
mqlListener = (v) => (matches = v.matches);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,18 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,9 +0,0 @@
<script>
import { isDevelopment } from '$lib/utils/env';
</script>
<svelte:head>
{#if !isDevelopment()}
<script async src="https://hk.leomurca.xyz/hk.js"></script>
{/if}
</svelte:head>

View file

@ -1,69 +0,0 @@
<script>
import { t } from '$lib/translations';
const trackEvent = () => {
// @ts-ignore
window.hk?.event?.('install_now');
};
</script>
<div class="bar">
<div class="content">
<p>
{$t('announcement.message')}
<a
href="https://play.google.com/store/apps/details?id=xyz.embroideryviewer.android"
target="_blank"
rel="noopener noreferrer"
class="cta"
onclick={trackEvent}
>
{$t('announcement.cta-text')}
</a>
</p>
</div>
</div>
<style>
.bar {
width: 100%;
background: #06345f;
color: white;
font-size: 0.95rem;
z-index: 3;
}
.content {
max-width: 1200px;
margin: 0 auto;
padding: 0.6rem 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
p {
margin: 0;
line-height: 1.4;
}
.cta {
margin-left: 0.5rem;
font-weight: 600;
text-decoration: underline;
color: #ffffff;
white-space: nowrap;
}
.cta:hover {
opacity: 0.85;
}
@media (max-width: 640px) {
.content {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View file

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

View file

@ -1,66 +0,0 @@
<script>
export let title;
/**
* @type {ArrayLike<any>}
*/
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, i (i)}
<div id={isError ? 'selected-file-card-error' : 'selected-file-card'}>
<span>{file.name}</span>
<span>{Math.round(file.size / 1000)} KB</span>
</div>
{/each}
</div>
</div>
{/if}
<style>
#selected-files-container {
text-align: center;
}
#files-list {
display: flex;
flex-direction: column;
align-items: center;
}
#selected-file-card {
display: flex;
justify-content: space-between;
color: #06345f;
font-weight: bolder;
width: 500px;
padding-left: 15px;
margin-top: 10px;
}
#selected-file-card-error {
display: flex;
justify-content: space-between;
color: #06345f;
font-weight: bolder;
width: 500px;
padding-left: 15px;
margin-top: 10px;
color: red;
}
@media only screen and (max-device-width: 812px) {
#selected-files-container,
#selected-file-card-error {
width: 100%;
}
#selected-file-card {
width: 100%;
}
}
</style>

View file

@ -1,185 +0,0 @@
<script>
import { resolve } from '$app/paths';
import { t } from '$lib/translations';
import { appVersion } from '$lib/utils/env';
import MailIcon from '$lib/components/icons/MailIcon.svelte';
import ArrowTopIcon from './icons/ArrowTopIcon.svelte';
import logo from '$lib/assets/logo-white.webp';
</script>
<footer>
<div id="content-container">
<section class="footer-block">
<img
src={logo}
style="height: 80px; width: auto; margin-lft: -5px;"
alt="Logotipo da Embroidery Viewer."
/>
<p>{$t('footer.slogan')}</p>
</section>
<section class="footer-block" aria-labelledby="contact-title">
<h1 id="contact-title">{$t('footer.contact-title')}</h1>
<p>{$t('footer.contact-description')}</p>
<address class="contact-container">
<div class="contact-item">
<MailIcon size={30} />
<a href="mailto:leo@leomurca.xyz}" aria-label="leo@leomurca.xyz"
>leo@leomurca.xyz</a
>
</div>
</address>
</section>
<section class="footer-block">
<h1>{$t('footer.resources')}</h1>
<nav class="social-container" aria-label="Social media">
<a href={resolve('/about')}>{$t('footer.about')}</a>
<a href={resolve('/privacy-policy')}>{$t('footer.privacy.policy')}</a>
<a href={resolve('/terms-of-service')}
>{$t('footer.terms.of.service')}</a
>
</nav>
<button
class="back-to-top-button"
aria-label={$t('footer.back-to-top.aria-label')}
onclick={() => scrollTo({ top: 0, behavior: 'smooth' })}
>
<ArrowTopIcon size={30} /> {$t('footer.back-to-top.label')}</button
>
</section>
</div>
<section class="credits-container">
Copyright {new Date().getFullYear()}
<a href="https://leomurca.xyz" target="_blank" rel="noreferrer"
>Leonardo Murça</a
>
|
{$t('footer.version')}:
<span style="font-family: var(--font-bold);">{appVersion()}</span>
</section>
</footer>
<style>
footer {
background-color: var(--color-primary);
width: 100%;
}
#content-container {
width: 85%;
display: flex;
justify-content: space-between;
margin: 0 auto;
padding: 60px 0 60px 30px;
scroll-margin-top: 100px;
color: white;
gap: 20px;
}
.footer-block {
width: 100%;
}
h1 {
font-weight: 700;
color: white;
font-size: 1.2rem;
}
.contact-container {
display: flex;
flex-direction: column;
gap: 8px;
letter-spacing: 0.1rem;
font-style: normal;
}
.contact-container a {
color: white;
}
.contact-item {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.contact-item:hover {
font-weight: 700;
}
.social-container {
display: flex;
flex-direction: column;
}
.social-container a {
color: white;
}
.social-container a:hover {
font-weight: 700;
}
.back-to-top-button {
text-align: center;
background-color: transparent;
border-radius: 20px;
border: none;
padding: 13px;
text-decoration: none;
color: white;
font-size: 1rem;
width: 40%;
margin-top: 30px;
border: 1px solid white;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.back-to-top-button:hover {
background-color: #ffffff;
color: var(--color-primary);
}
.credits-container {
background-color: var(--color-secondary);
color: white;
margin: 0 auto;
padding: 20px 30px;
padding-left: 9%;
width: 100%;
}
.credits-container a {
color: white;
border-bottom: 1px solid white;
}
.credits-container a:hover {
background-color: white;
color: var(--color-secondary);
}
/* Responsive */
@media (max-width: 768px) {
#content-container {
width: 100%;
margin: 0;
padding: 60px 30px;
flex-direction: column;
}
.back-to-top-button {
width: 100%;
justify-content: center;
gap: 5px;
}
}
</style>

View file

@ -1,89 +0,0 @@
<script>
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { locale, t } from '$lib/translations';
import { normalizeLocaleUnderscore } from '$lib/utils/normalizeLocaleUnderscore';
import { isDevelopment } from '$lib/utils/env';
/**
* =========================
* Props
* =========================
*/
/** @type {string} Page title (translation key) */
export let title;
/** @type {string} Page description (translation key) */
export let description;
/** @type {string} SEO keywords (translation key or raw string) */
export let keywords;
/** @type {string} Canonical URL (absolute) */
export let url;
/** @type {string} Open Graph type (e.g., 'website', 'article') */
export let ogType = 'website';
/** @type {string} Optional override for Open Graph description */
export let ogDescription = description;
/** @type {string} Twitter card type */
export let twitterCard = 'summary_large_image';
/** @type {boolean} Whether the page should be indexed */
export let shouldBeIndexed = !isDevelopment();
/**
* =========================
* Derived / Computed values
* =========================
*/
let image = `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/logo-icon.webp`;
// Translations (avoid repeating $t everywhere)
$: translatedTitle = title ? $t(title) : '';
$: translatedDescription = description ? $t(description) : '';
$: translatedKeywords = keywords ? $t(keywords) : '';
// Fallbacks
$: finalOgDescription = ogDescription
? $t(ogDescription)
: translatedDescription;
// Locale formatting (e.g., en-US -> en_US)
$: ogLocale = normalizeLocaleUnderscore($locale);
// Robots directive
$: robotsContent = shouldBeIndexed ? 'index, follow' : 'noindex, nofollow';
</script>
<svelte:head>
<!-- Primary SEO -->
<title>{translatedTitle}</title>
<meta name="description" content={translatedDescription} />
<meta name="keywords" content={translatedKeywords} />
<!-- Robots -->
<meta name="robots" content={robotsContent} />
<meta name="googlebot" content={robotsContent} />
<!-- Open Graph (Facebook, LinkedIn, etc.) -->
<meta property="og:type" content={ogType} />
<meta property="og:title" content={translatedTitle} />
<meta property="og:description" content={finalOgDescription} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<meta property="og:locale" content={ogLocale} />
<meta property="og:site_name" content="Embroidery Viewer" />
<!-- Twitter -->
<meta name="twitter:card" content={twitterCard} />
<meta name="twitter:title" content={translatedTitle} />
<meta name="twitter:description" content={finalOgDescription} />
<meta name="twitter:image" content={image} />
<!-- Optional: improves link previews in some platforms -->
<meta property="og:image:alt" content={translatedTitle} />
</svelte:head>

View file

@ -1,236 +0,0 @@
<script>
import { t } from '$lib/translations';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import logo from '$lib/assets/logo.webp';
import LanguageSwitch from './LanguageSwitch.svelte';
import MediaQuery from './MediaQuery.svelte';
let isOpen = $state(false);
let route = $derived(page.url.pathname);
$effect(() => {
route; // track dependency
isOpen = false;
});
</script>
<header>
<div class="logo">
<MediaQuery query="(max-width: 768px)" let:matches>
<a href={resolve('/')}>
<img
src={logo}
alt="Embroidery viewer logo"
width="150"
height={matches ? 70 : 100}
/>
</a>
</MediaQuery>
</div>
<nav class:active={route !== '/'} id="menuToggle">
<input type="checkbox" bind:checked={isOpen} />
<span></span>
<span></span>
<span></span>
<ul id="menu">
<li><a href={resolve('/about')}>{$t('header.aboutNav')}</a></li>
<li><a href={resolve('/viewer')}>{$t('header.viewerNav')}</a></li>
<li><a href={resolve('/support-us')}>{$t('header.supportUsNav')}</a></li>
<li style="font-size: 22px; padding: 10px 0;"><LanguageSwitch /></li>
</ul>
</nav>
</header>
<style>
header {
position: absolute;
top: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 45px 30px 10px 100px;
width: 100%;
}
.logo {
z-index: -1;
}
.logo img {
height: auto;
max-height: 60px;
}
.logo a {
border-bottom: none;
}
.logo a:hover {
background: transparent;
}
#menuToggle {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-end;
}
/* Click area */
#menuToggle input {
display: block;
width: 40px;
height: 32px;
position: absolute;
top: -7px;
left: -5px;
cursor: pointer;
opacity: 0;
z-index: 2;
}
/* Bars */
#menuToggle span {
display: block;
width: 30px;
height: 3px;
margin-bottom: 7px;
background: #ffffff;
border-radius: 2px;
transition: all 0.35s ease;
transform-origin: 4px 0px;
}
/* Adjust origins for rotation */
#menuToggle span:nth-child(2) {
transform-origin: 0% 0%;
width: 20px;
}
#menuToggle span:nth-child(4) {
transform-origin: 0% 100%;
width: 20px;
}
/* === ANIMATION === */
/* Top bar → rotates */
#menuToggle input:checked ~ span:nth-child(2) {
transform: rotate(45deg) translate(-1px, -1px);
width: 30px;
}
/* Middle bar → fades out */
#menuToggle input:checked ~ span:nth-child(3) {
opacity: 0;
transform: scale(0.2);
}
/* Bottom bar → rotates opposite */
#menuToggle input:checked ~ span:nth-child(4) {
transform: rotate(-45deg) translate(1px, -1px);
width: 30px;
}
#menu {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: absolute;
top: -80px;
width: 400px;
margin: -100px 0 0 0;
padding: 50px;
padding-top: 125px;
right: -100px;
background: var(--color-primary);
list-style-type: none;
-webkit-font-smoothing: antialiased;
transform-origin: 0% 0%;
transform: translate(100%, 0);
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1);
z-index: -1;
margin: 30px;
border-radius: 70% 30% 30% 70% / 60% 40% 0 100%;
box-shadow:
0 2.8px 2.2px rgba(0, 0, 0, 0.02),
0 6.7px 5.3px rgba(0, 0, 0, 0.028),
0 12.5px 10px rgba(0, 0, 0, 0.035),
0 22.3px 17.9px rgba(0, 0, 0, 0.042),
0 41.8px 33.4px rgba(0, 0, 0, 0.05),
0 100px 80px rgba(0, 0, 0, 0.07);
}
#menu li a {
padding: 10px 0;
font-size: 22px;
color: white;
display: block;
}
#menu li a:hover {
color: #adadad;
transition: all 0.5s ease;
}
#menuToggle input:checked ~ ul {
transform: scale(1, 1);
opacity: 1;
}
.active input:checked ~ span:nth-child(2),
.active input:checked ~ span:nth-child(3),
.active input:checked ~ span:nth-child(4) {
background-color: white !important;
}
.active span {
background-color: var(--color-primary) !important;
}
@media (max-width: 1458px) {
#menuToggle span {
background-color: var(--color-primary);
}
/* Top bar → rotates */
#menuToggle input:checked ~ span:nth-child(2) {
background-color: white;
}
/* Middle bar → fades out */
#menuToggle input:checked ~ span:nth-child(3) {
background-color: white;
}
/* Bottom bar → rotates opposite */
#menuToggle input:checked ~ span:nth-child(4) {
background-color: white;
}
}
@media (max-width: 1159px) {
header {
padding-top: 70px;
}
}
@media (max-width: 768px) {
header {
padding: 110px 20px 10px 20px;
}
#menu {
width: 100vw;
top: -110px;
margin: 0px 0 0 0;
right: -20px;
border-radius: 0;
border-radius: 0% 0% 80% 80%;
transform: translate(0%, -100%);
}
}
</style>

View file

@ -1,145 +0,0 @@
<script>
import { locale, SUPPORTED_LOCALES } from '$lib/translations';
const isEnglish = $derived($locale === SUPPORTED_LOCALES.EN_US);
/**
* Switches the current locale to the opposite language (EN_US <-> PT_BR).
* Prevents the default link behavior (e.g., page jump).
*/
const onSwitchToOppositeLang = () => {
$locale =
$locale === SUPPORTED_LOCALES.EN_US
? SUPPORTED_LOCALES.PT_BR
: SUPPORTED_LOCALES.EN_US;
};
</script>
<center>
<div class="switch">
<input
id="language-toggle"
class="check-toggle check-toggle-round-flat"
type="checkbox"
onclick={onSwitchToOppositeLang}
checked={isEnglish}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onSwitchToOppositeLang();
}
}}
/>
<label for="language-toggle"></label>
<span class="off">EN</span>
<span class="on">PT</span>
</div>
</center>
<style>
.switch {
position: relative;
display: inline-block;
margin: 0;
font-weight: 700;
z-index: 1;
}
.switch > span {
position: absolute;
top: 7px;
pointer-events: none;
font-weight: bold;
font-size: 12px;
text-transform: uppercase;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
width: 50%;
text-align: center;
}
input.check-toggle-round-flat:checked ~ .off {
color: white !important;
}
input.check-toggle-round-flat:checked ~ .on {
color: var(--color-primary) !important;
}
.switch > span.on {
left: 0;
padding-left: 2px;
color: white;
}
.switch > span.off {
right: 0;
padding-right: 4px;
color: var(--color-primary) !important;
}
.check-toggle {
position: absolute;
margin-left: -9999px;
visibility: hidden;
}
.check-toggle + label {
display: block;
position: relative;
cursor: pointer;
outline: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
input.check-toggle-round-flat + label {
padding: 2px;
width: 97px;
height: 35px;
background-color: white;
-webkit-border-radius: 60px;
-moz-border-radius: 60px;
-ms-border-radius: 60px;
-o-border-radius: 60px;
border-radius: 60px;
}
input.check-toggle-round-flat + label:before,
input.check-toggle-round-flat + label:after {
display: block;
position: absolute;
content: '';
}
input.check-toggle-round-flat + label:before {
top: 2px;
left: 2px;
bottom: 2px;
right: 2px;
background-color: white;
-moz-border-radius: 60px;
-ms-border-radius: 60px;
-o-border-radius: 60px;
border-radius: 60px;
}
input.check-toggle-round-flat + label:after {
top: 4px;
left: 4px;
bottom: 4px;
width: 48px;
background-color: var(--color-primary);
-webkit-border-radius: 52px;
-moz-border-radius: 52px;
-ms-border-radius: 52px;
-o-border-radius: 52px;
border-radius: 52px;
-webkit-transition: margin 0.2s;
-moz-transition: margin 0.2s;
-o-transition: margin 0.2s;
transition: margin 0.2s;
}
input.check-toggle-round-flat:checked + label:after {
margin-left: 42px;
}
</style>

View file

@ -1,14 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
height={size}
viewBox="0 -960 960 960"
width={size}
fill={color}
>
<path d="M480-528 296-344l-56-56 240-240 240 240-56 56-184-184Z" />
</svg>

View file

@ -1,21 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill={color}
aria-hidden="true"
id="Bolt--Streamline-Heroicons"
height={size}
width={size}
>
<path
fill-rule="evenodd"
d="M9.743333333333332 1.0633333333333332a0.5 0.5 0 0 1 0.23933333333333331 0.568L8.654666666666666 6.5h4.845333333333333a0.5 0.5 0 0 1 0.36533333333333334 0.8413333333333333l-7 7.5a0.5 0.5 0 0 1 -0.848 -0.4733333333333333l1.3279999999999998 -4.867999999999999H2.5a0.5 0.5 0 0 1 -0.36533333333333334 -0.8413333333333333l7 -7.5a0.5 0.5 0 0 1 0.6086666666666667 -0.09533333333333333Z"
clip-rule="evenodd"
stroke-width="0.6667"
></path>
</svg>

File diff suppressed because one or more lines are too long

View file

@ -1,21 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
width={size}
height={size}
viewBox="0 0 115.03736 82.932701"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
><defs id="defs1" /><g id="layer1" transform="translate(46.342105,-326.35286)"
><path
style={`fill:${color}`}
d="m -33.360673,409.11068 c -0.52052,-0.66147 -13.05506,-29.41883 -12.98111,-29.7819 0.0676,-0.33192 10.25008,-3.75875 11.16875,-3.75875 0.39694,0 -0.23972,-1.36265 6.66385,14.2629 3.11223,7.04422 5.6586,13.00337 5.6586,13.24254 0,0.28356 -1.49602,1.30029 -4.29948,2.92205 -2.36471,1.36794 -4.67009,2.70349 -5.12306,2.96789 -0.65389,0.38166 -0.87798,0.4116 -1.08755,0.14527 z m 51.11115,-7.18941 c -1.0255,-0.19444 -6.26425,-1.60693 -11.6416602,-3.13887 -11.60606,-3.30637 -11.88394,-3.36862 -15.03947,-3.36862 -3.0879598,0 -5.4300198,0.58865 -9.3851798,2.35884 -1.65323,0.73993 -3.20801,1.34533 -3.45507,1.34533 -0.33715,0 -1.59126,-2.59045 -5.02761,-10.38489 -2.51812,-5.7117 -4.63162,-10.51906 -4.69666,-10.68304 -0.065,-0.16398 0.75205,-0.77969 1.81577,-1.36826 1.06372,-0.58856 3.06512,-1.98586 4.44757,-3.1051 1.38245,-1.11925 3.21341,-2.51262 4.06879,-3.09639 5.178,-3.5338 12.3545098,-5.07308 18.9419698,-4.06285 3.59287,0.55099 6.45084,1.42963 10.85591,3.33749 7.3390302,3.17857 11.7920602,4.46814 22.2961402,6.45677 6.83656,1.29429 8.39146,1.84388 9.38771,3.31816 1.351,1.99922 0.46493,4.08861 -2.34074,5.5196 -2.74024,1.39761 -3.54371,1.37687 -13.73269,-0.35444 -8.2986,-1.4101 -9.07435,-1.4986 -12.17084,-1.38851 -2.5834002,0.0919 -3.7126702,0.25703 -5.1593702,0.75463 -1.63768,0.56329 -1.86179,0.72144 -1.93595,1.36615 -0.12182,1.05906 0.46688,1.15204 2.81088,0.44394 3.5053302,-1.05892 6.4809702,-0.89831 16.9844402,0.91671 8.37829,1.44778 8.95895,1.5149 10.60612,1.22601 4.34498,-0.76205 7.62602,-3.6216 7.64432,-6.66234 0.003,-0.56374 -0.31433,-1.57393 -0.76375,-2.42828 -0.88082,-1.67447 -0.97521,-1.4883 1.59586,-3.14763 1.45305,-0.93779 3.67139,-2.74223 8.64098,-7.02876 5.07066,-4.37371 7.8612,-5.88252 9.87621,-5.33994 0.5539,0.14915 1.07867,0.38701 1.16616,0.52858 0.0875,0.14156 0.75876,0.25739 1.49171,0.25739 2.23308,0 3.66323,1.33775 3.66323,3.42656 0,1.22305 -0.39272,1.86203 -5.78067,9.40573 -5.63224,7.88573 -7.41725,9.89443 -10.15569,11.42836 -5.52566,3.09519 -23.79832,12.65577 -25.06445,13.11418 -1.88226,0.68147 -7.21439,0.87102 -9.94397,0.35349 z m 8.93119,-37.4527 c -10.54204,-7.34003 -15.45466,-12.20175 -18.3280302,-18.13812 -2.47555,-5.11448 -2.73882,-10.04227 -0.74529,-13.94992 0.92633,-1.81576 3.3031302,-4.09338 5.2439402,-5.02511 3.14838,-1.51146 7.45008,-1.29668 10.56261,0.52739 1.77607,1.04084 4.13023,3.35279 5.47731,5.37909 0.66799,1.00478 1.26128,1.82688 1.31843,1.82688 0.0571,0 0.69136,-0.8632 1.40937,-1.91822 2.27222,-3.33879 4.61009,-5.23911 7.75182,-6.30101 2.33309,-0.78858 5.77205,-0.63428 7.95915,0.35711 1.75477,0.79542 4.18097,2.97573 5.14853,4.62674 4.08542,6.97124 0.45301,16.98315 -9.18256,25.30961 -4.39191,3.79521 -12.25534,9.42128 -13.13598,9.39843 -0.27047,-0.007 -1.83615,-0.94881 -3.4793,-2.09287 z"
id="path2"
/></g
></svg
>

View file

@ -1,17 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
fill={color}
width={size}
height={size}
viewBox="0 0 256 256"
id="Flat"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M213.65723,66.34326l-40-40A8.00076,8.00076,0,0,0,168,24H88A16.01833,16.01833,0,0,0,72,40V56H56A16.01833,16.01833,0,0,0,40,72V216a16.01833,16.01833,0,0,0,16,16H168a16.01833,16.01833,0,0,0,16-16V200h16a16.01833,16.01833,0,0,0,16-16V72A8.00035,8.00035,0,0,0,213.65723,66.34326ZM136,192H88a8,8,0,0,1,0-16h48a8,8,0,0,1,0,16Zm0-32H88a8,8,0,0,1,0-16h48a8,8,0,0,1,0,16Zm64,24H184V104a8.00035,8.00035,0,0,0-2.34277-5.65674l-40-40A8.00076,8.00076,0,0,0,136,56H88V40h76.68652L200,75.314Z"
/>
</svg>

View file

@ -1,18 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
stroke-width="1.5"
>
<rect x="3" y="3" width="7" height="7" rx="1.5" />
<rect x="14" y="3" width="7" height="7" rx="1.5" />
<rect x="3" y="14" width="7" height="7" rx="1.5" />
<rect x="14" y="14" width="7" height="7" rx="1.5" />
</svg>

View file

@ -1,19 +0,0 @@
<script>
export let size = 20;
export let strokeWidth = 2;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width={strokeWidth}
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="5" width="18" height="14" rx="2" ry="2" />
<polyline points="3,7 12,13 21,7" />
</svg>

View file

@ -1,17 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
fill={color}
width={size}
height={size}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
id="lock-check"
class="icon glyph"
><path
d="M18,8H17V7A5,5,0,0,0,7,7V8H6a2,2,0,0,0-2,2V20a2,2,0,0,0,2,2H18a2,2,0,0,0,2-2V10A2,2,0,0,0,18,8ZM9,7a3,3,0,0,1,6,0V8H9Zm6.71,6.71-4,4a1,1,0,0,1-1.42,0l-2-2a1,1,0,0,1,1.42-1.42L11,15.59l3.29-3.3a1,1,0,0,1,1.42,1.42Z"
></path></svg
>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,93 +0,0 @@
<script>
import { t } from '$lib/translations';
</script>
<section id="faq">
<h1>{$t('faq.title')}</h1>
<p class="intro">
{$t('faq.intro')}
</p>
<div class="faq-list">
<details>
<summary>{$t('faq.items.openPesOnline.summary')}</summary>
<p>{$t('faq.items.openPesOnline.description')}</p>
</details>
<details>
<summary>{$t('faq.items.supportedFormats.summary')}</summary>
<p>{$t('faq.items.supportedFormats.description')}</p>
</details>
<details>
<summary>{$t('faq.items.needSoftware.summary')}</summary>
<p>{$t('faq.items.needSoftware.description')}</p>
</details>
<details>
<summary>{$t('faq.items.isSafe.summary')}</summary>
<p>{$t('faq.items.isSafe.description')}</p>
</details>
<details>
<summary>{$t('faq.items.multipleFiles.summary')}</summary>
<p>{$t('faq.items.multipleFiles.description')}</p>
</details>
<details>
<summary>{$t('faq.items.mobileSupport.summary')}</summary>
<p>{$t('faq.items.mobileSupport.description')}</p>
</details>
</div>
</section>
<style>
#faq {
padding: 100px 20px;
max-width: 800px;
margin: 0 auto;
}
#faq h1 {
font-weight: 700;
font-size: 2.5rem;
text-align: center;
line-height: 1.5;
}
#faq .intro {
text-align: center;
margin: 10px 0 40px;
}
.faq-list details {
border-bottom: 1px solid #eee;
padding: 20px 0;
}
.faq-list summary {
cursor: pointer;
font-weight: 600;
font-size: 1.2rem;
list-style: none;
position: relative;
color: var(--color-primary);
}
.faq-list summary::after {
content: '+';
position: absolute;
right: 0;
font-size: 1.7rem;
transition: transform 0.3s ease;
}
.faq-list details[open] summary::after {
transform: rotate(45deg);
}
.faq-list p {
margin-top: 10px;
line-height: 1.6;
}
</style>

View file

@ -1,224 +0,0 @@
<script>
import { resolve } from '$app/paths';
import { t } from '$lib/translations';
import BoltIcon from '$lib/components/icons/BoltIcon.svelte';
import PadlockIcon from '$lib/components/icons/PadlockIcon.svelte';
import FourSquaresIcon from '$lib/components/icons/FourSquaresIcon.svelte';
import FilesIcon from '$lib/components/icons/FilesIcon.svelte';
</script>
<section id="features">
<header>
<h1>{$t('features.title')}</h1>
<p class="subtitle">{$t('features.subtitle')}</p>
</header>
<div class="cards-container">
<div class="blob">
<p class="adjective">{$t('features.cards.fast.adjective')}</p>
<h2>{$t('features.cards.fast.title')}</h2>
<BoltIcon color="#06345f" size={100} />
<p class="description">{$t('features.cards.fast.description')}</p>
</div>
<div class="blob">
<p class="adjective">{$t('features.cards.private.adjective')}</p>
<h2>{$t('features.cards.private.title')}</h2>
<PadlockIcon color="#06345f" size={100} />
<p class="description">{$t('features.cards.private.description')}</p>
</div>
<div class="blob">
<p class="adjective">{$t('features.cards.optimized.adjective')}</p>
<h2>{$t('features.cards.optimized.title')}</h2>
<FourSquaresIcon color="#06345f" size={100} />
<p class="description">{$t('features.cards.optimized.description')}</p>
</div>
<div class="blob">
<p class="adjective">{$t('features.cards.compatibility.adjective')}</p>
<h2>{$t('features.cards.compatibility.title')}</h2>
<FilesIcon color="#06345f" size={100} />
<p class="description">
{$t('features.cards.compatibility.description')}
</p>
</div>
</div>
<a class="organic-btn-secondary" href={resolve('/viewer')}
>{$t('features.cta')}</a
>
</section>
<style>
#features {
display: flex;
align-items: center;
flex-direction: column;
padding: 100px 0;
background-color: var(--color-primary);
position: relative;
overflow: hidden;
}
#features::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg width='600' height='600' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 150 C 60 20, 140 180, 190 50' stroke='white' stroke-width='1.5' stroke-dasharray='4 6' opacity='0.2'/%3E%3Ccircle cx='10' cy='150' r='3' fill='white' opacity='0.25'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 600px;
background-position: right -100px top -80px;
pointer-events: none;
}
#features::after {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg width='600' height='600' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 40 C 80 180, 120 0, 180 140' stroke='white' stroke-width='1.2' stroke-dasharray='3 8' opacity='0.15'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 500px;
background-position: left -120px bottom -80px;
pointer-events: none;
}
header {
text-align: center;
line-height: 1.5;
width: 80%;
}
h1 {
color: white;
font-size: 2.7rem;
}
.subtitle {
color: white;
font-size: 1.2rem;
text-align: center;
}
.cards-container {
display: grid;
gap: 20px;
grid-template-columns: repeat(4, 1fr);
margin: 0 auto;
width: 100%;
padding: 100px 40px;
}
.blob {
width: 100%;
max-width: 380px;
height: 400px;
aspect-ratio: 1;
border-radius: 25% 50% 75% 100%;
text-align: center;
padding: 30px 60px;
background: linear-gradient(
30deg in oklch shorter hue,
oklch(0.98 0.01 260) 10%,
oklch(0.92 0.02 260)
);
box-shadow: 1rem 1rem 50px #0001;
}
.blob h2 {
font-size: 1.5rem;
margin-top: 10px;
}
.description {
font-size: 1rem;
margin-top: 20px;
}
.adjective {
color: var(--color-primary);
font-size: 0.8rem !important;
}
.organic-btn-secondary {
font-size: 1.3rem;
padding: 30px 90px;
}
@media (max-width: 1639px) {
.blob {
width: 100%;
max-width: 340px;
height: 380px;
}
}
@media (max-width: 1480px) {
.cards-container {
grid-template-columns: repeat(3, 1fr);
}
.blob {
width: 100%;
max-width: 400px;
height: 400px;
}
}
@media (max-width: 1297px) {
.cards-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 1210px) {
h1 {
font-size: 2.3rem;
}
.subtitle {
font-size: 1rem;
}
}
@media (max-width: 1033px) {
h1 {
font-size: 2rem;
}
}
@media (max-width: 880px) {
#features {
padding: 20px;
}
header {
width: 100%;
}
h1 {
font-size: 2rem;
}
.cards-container {
padding: 0;
grid-template-columns: 1fr;
}
.blob {
width: 100%;
max-width: 100%;
padding-bottom: 50px;
height: fit-content;
aspect-ratio: 0;
}
}
@media (max-width: 540px) {
.organic-btn-secondary {
width: 100%;
text-align: center;
font-size: 1rem;
padding: 20px 50px;
margin-top: 30px;
}
}
</style>

View file

@ -1,97 +0,0 @@
<script>
import { resolve } from '$app/paths';
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { t } from '$lib/translations';
import { isMobile } from '$lib/utils/isMobile';
const backgroundImage = isMobile()
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/hero-mobile.webp`
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/hero.webp`;
</script>
<section
id="hero"
style={`background: url(${backgroundImage}) center/cover no-repeat`}
>
<div class="overlay">
<h1>{$t('hero.title')}</h1>
<p>{$t('hero.description')}</p>
<a class="organic-btn" href={resolve('/viewer')}>{$t('hero.cta')}</a>
</div>
</section>
<style>
#hero {
position: relative;
z-index: 0;
min-height: 100vh;
display: flex;
}
.overlay {
display: flex;
flex-direction: column;
max-width: 800px;
z-index: 1;
padding-left: 100px;
padding-top: 130px;
}
h1 {
font-size: clamp(3.5rem, 4vw, 3.5rem);
font-weight: 700;
margin-bottom: 1rem;
line-height: 1.2;
}
p {
font-size: 1.5rem;
margin-bottom: 2rem;
}
@media (max-width: 1350px) {
h1 {
font-size: clamp(3.3rem, 4vw, 3.3rem);
}
}
@media (max-width: 1280px) {
h1 {
font-size: clamp(3rem, 4vw, 3rem);
}
p {
font-size: 1.1rem;
}
}
@media (max-width: 1180px) {
h1 {
font-size: clamp(3rem, 4vw, 3rem);
}
}
@media (max-width: 768px) {
h1 {
font-size: clamp(2rem, 4vw, 2rem);
}
p {
margin-top: 0;
}
#hero {
background-size: contain !important;
background-position: bottom !important;
}
.overlay {
width: 100%;
padding-top: 100px;
padding-left: 0;
height: 100vh;
padding-left: 20px;
padding-right: 20px;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show more