├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dev
├── App.svelte
├── main.js
└── public
│ └── index.html
├── package.json
├── rollup.config.js
├── rollup.dev.config.js
├── src
├── ImgEncoder.svelte
├── ImgEncoder.ts
├── pan-zoom.ts
└── preprocessor
│ └── index.js
├── tsconfig.json
└── types
└── detect-pointer-events.d.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | yarn.lock
4 | yarn-error.log
5 | package-lock.json
6 | /index.mjs
7 | /index.js
8 | test/public/bundle.js
9 | !test/src/index.js
10 | /.gtm/
11 | /.rpt2_cache/
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # svelte-subdivide changelog
2 |
3 | ## 1.2.4
4 |
5 | * Now captures `mouseup` events outside of element.
6 |
7 | ## 1.2.3
8 |
9 | * Now uses more reliable code to detect if running in a browser.
10 |
11 | ## 1.2.2
12 |
13 | * Removed Typescript from component source code.
14 |
15 | ## 1.2.1
16 |
17 | * Fixed build issue inside Svelte projects.
18 |
19 | ## 1.2.0
20 |
21 | * Added support for mouse events. Should make Safari work.
22 |
23 | ## 1.1.0
24 |
25 | * Added `classes` property, helpful for theming.
26 |
27 | ## 1.0.0
28 |
29 | * Basic functionality, allows resizing and moving the selected image.
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Sebastian Ferreyra Pons
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # svelte-image-encoder ([demo](https://svelte.dev/repl/cb1ec0dcc5dfaa1e0de3844f3e7348d6))
2 |
3 | > This repos is now considered deprecated and is superseded by
4 | > [svelte-image-input](https://github.com/saabi/svelte-image-input), which
5 | > supports more features such as image loading, dropping and pasting.
6 |
7 | A component for creating `data:` URLs from images in real time. You can also move and resize the image before encoding.
8 |
9 | The data URL enables sending and receiving the image inside JSON AJAX requests and perhaps storing images
10 | in database string columns, where an image URL would go, simplifying code logic.
11 |
12 | The original intended use is for use in a profile picture editor, allowing the user to resize and crop
13 | images, finally storing them in a small `data:` URL but it may be useful when you need basic image
14 | resizing/cropping capabilities.
15 |
16 | ## Installation
17 |
18 | ```bash
19 | npm i svelte-image-encoder [-D]
20 | ```
21 |
22 | ## Usage
23 |
24 | ```html
25 |
30 |
31 |
32 |
{url}
33 | ```
34 |
35 | ## Parameters
36 |
37 | You can specify the following parameters:
38 |
39 | * `src` — the original image URL. Any valid image URL will work, except if `crossOrigin` is set to false.
40 | * `url` — the generated `data:` URL. Read only.
41 | * `realTime` — if `true` the data URL is generated in real time while dragging or resizing the image, defaults to `false` and is generally not needed to be `true`.
42 | * `quality` — JPEG quality factor. Defaults to `0.5`.
43 | * `width` — The encoded image's width. Defaults to `256`.
44 | * `height` — The encoded image's height. Defaults to `256`.
45 | * `crossOrigin` — enables loading cross origin URLs. Defaults to `false`.
46 | * `classes` — Allows for theming by specifying global classes. Defaults to ``.
47 |
48 | ## Configuring webpack
49 |
50 | If you're using webpack with [svelte-loader](https://github.com/sveltejs/svelte-loader), make sure that you add `"svelte"` to [`resolve.mainFields`](https://webpack.js.org/configuration/resolve/#resolve-mainfields) in your webpack config. This ensures that webpack imports the uncompiled component (`src/index.svelte`) rather than the compiled version (`index.mjs`) — this is more efficient.
51 |
52 | If you're using Rollup with [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte), this will happen automatically.
53 |
54 |
55 | ## TODO
56 |
57 | * Add a boolean property to choose between displaying the compressed or uncompressed result. Needs som reworking of the internals.
58 |
59 | ## License
60 |
61 | [ISC](LICENSE)
62 |
--------------------------------------------------------------------------------
/dev/App.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 | Quality:
18 |
19 |
20 |
21 |
22 | Result ({url && url.length} bytes):
23 | { url }
24 |
25 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/dev/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte';
2 |
3 | const app = new App({
4 | target: document.body
5 | });
--------------------------------------------------------------------------------
/dev/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Svelte app
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-image-encoder",
3 | "version": "1.2.5",
4 | "description": "A Svelte component for editing and compressing profile pictures before upload to a server",
5 | "svelte": "src/ImgEncoder.svelte",
6 | "module": "index.mjs",
7 | "main": "index.js",
8 | "scripts": {
9 | "build": "rollup -c",
10 | "build:dev": "rollup -c rollup.dev.config.js -w",
11 | "prepublishOnly": "npm run build"
12 | },
13 | "dependencies": {
14 | "detect-pointer-events": "^1.0.1"
15 | },
16 | "devDependencies": {
17 | "rollup": "^1.14.6",
18 | "rollup-plugin-commonjs": "^10.0.0",
19 | "rollup-plugin-node-resolve": "^5.0.1",
20 | "rollup-plugin-svelte": "^5.1.0",
21 | "serve-handler": "^6.0.1",
22 | "svelte": "^3.5.1",
23 | "typescript": "^3.5.1",
24 | "rollup-plugin-typescript2": "^0.21.1"
25 | },
26 | "repository": "https://github.com/saabi/svelte-image-encoder",
27 | "author": "Sebastian Ferreyra Pons",
28 | "license": "ISC",
29 | "keywords": [
30 | "svelte",
31 | "profile picture",
32 | "image encoder"
33 | ],
34 | "files": [
35 | "src",
36 | "types",
37 | "index.mjs",
38 | "index.js",
39 | "tsconfig.json"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import resolve from 'rollup-plugin-node-resolve';
3 | import commonjs from 'rollup-plugin-commonjs';
4 | import typescript from 'rollup-plugin-typescript2';
5 | import pkg from './package.json';
6 |
7 | export default [
8 | {
9 | input: 'src/ImgEncoder.svelte',
10 | output: [
11 | { file: pkg.module, 'format': 'es' },
12 | { file: pkg.main, 'format': 'umd', name: 'ImgEncoder' }
13 | ],
14 | plugins: [
15 | typescript(),
16 | svelte(),
17 | commonjs(),
18 | resolve()
19 | ]
20 | }
21 | ];
--------------------------------------------------------------------------------
/rollup.dev.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import resolve from 'rollup-plugin-node-resolve';
3 | import commonjs from 'rollup-plugin-commonjs';
4 | import typescript from 'rollup-plugin-typescript2';
5 | import pkg from './package.json';
6 |
7 | export default [
8 | {
9 | input: 'dev/main.js',
10 | output: {
11 | sourcemap: true,
12 | format: 'iife',
13 | name: 'app',
14 | file: 'dev/public/bundle.js',
15 | },
16 | plugins: [
17 | typescript(),
18 | svelte({dev: true}),
19 | commonjs(),
20 | resolve()
21 | ]
22 | }
23 | ];
--------------------------------------------------------------------------------
/src/ImgEncoder.svelte:
--------------------------------------------------------------------------------
1 |
90 |
91 |
92 |
93 |
99 |
100 |
--------------------------------------------------------------------------------
/src/ImgEncoder.ts:
--------------------------------------------------------------------------------
1 | import { Transform, panHandler } from './pan-zoom';
2 | import { onMount } from 'svelte';
3 |
4 | export let src = '';
5 | export let url = '';
6 | export let quality = 0.5;
7 | export let width = 256;
8 | export let height = 256;
9 | export let realTime = false;
10 | export let crossOrigin = false;
11 | export let classes = '';
12 | //export let showResult = true;
13 | //TODO: add support for optionally showing compressed result instead of original
14 |
15 | panHandler; //mentioned so that the Typescript compiler emits the import.
16 |
17 | let canvas: HTMLCanvasElement;
18 | let img: HTMLImageElement | undefined;
19 | let ctx: CanvasRenderingContext2D | null;
20 |
21 | let offsetX = 0;
22 | let offsetY = 0;
23 | let scale = 1;
24 | let minScale = 1;
25 | let dragging = false;
26 |
27 | // not a POJO because getters/setters are instrumentable by Svelte
28 | // and `transform` is updated by imported functions
29 | let transform: Transform = {
30 | getMinScale() { // read only, TODO: maxScale
31 | return minScale;
32 | },
33 | getScale() {
34 | return scale;
35 | },
36 | setScale(s) {
37 | scale = s;
38 | },
39 | getOffsetX() {
40 | return offsetX;
41 | },
42 | getOffsetY() {
43 | return offsetY;
44 | },
45 | setOffsetX(ox) {
46 | offsetX = ox;
47 | },
48 | setOffsetY(oy) {
49 | offsetY = oy;
50 | },
51 | setDragging(d) {
52 | if (!realTime && d === false ) url = canvas.toDataURL('image/jpeg', quality);
53 | dragging = d;
54 | },
55 | getDragging() {
56 | return dragging;
57 | }
58 | }
59 |
60 | function redraw() {
61 | if (!img || !ctx)
62 | return;
63 | if (offsetX < 0)
64 | offsetX = 0;
65 | if (offsetY < 0)
66 | offsetY = 0;
67 | let limit = img.width*scale - width;
68 | if (offsetX > limit)
69 | offsetX = limit;
70 | limit = img.height*scale - height;
71 | if (offsetY > limit)
72 | offsetY = limit;
73 |
74 | ctx.resetTransform();
75 | ctx.clearRect(0, 0, width, height);
76 | ctx.translate(-offsetX, -offsetY);
77 | ctx.scale(scale, scale);
78 | ctx.drawImage(img, 0, 0);
79 |
80 | if (realTime || !dragging) url = canvas.toDataURL('image/jpeg', quality);
81 | }
82 |
83 | $: img && (img.crossOrigin = crossOrigin ? 'anonymous' : null);
84 | $: img && (img.src = src);
85 | $: quality, width, height, offsetX, offsetY, scale, redraw();
86 |
87 | onMount( ()=> {
88 | ctx = canvas.getContext('2d');
89 | img = new Image();
90 | img.onload = function() {
91 | offsetX = 0;
92 | offsetY = 0;
93 | scale = minScale = Math.max(width/img!.width, height/img!.height);
94 | };
95 | });
96 |
--------------------------------------------------------------------------------
/src/pan-zoom.ts:
--------------------------------------------------------------------------------
1 | import detectPointerEvents from 'detect-pointer-events';
2 |
3 | export interface Transform {
4 | getMinScale(): number;
5 | getScale(): number;
6 | setScale(s: number): void;
7 | getOffsetX(): number;
8 | getOffsetY(): number;
9 | setOffsetX(o: number): void;
10 | setOffsetY(o: number): void;
11 | setDragging(d: boolean): void;
12 | getDragging(): boolean;
13 | }
14 |
15 | // Firefox resets some properties in stored/cached
16 | // Event objects when new events are fired so
17 | // we have to store a clone.
18 | // TODO: Should we store the original object when using Chrome?
19 | function iterationCopy(src: any) {
20 | let target: any = {};
21 | for (let prop in src) {
22 | target[prop] = src[prop];
23 | }
24 | return target;
25 | }
26 |
27 | function updateScale(transform:Transform, s: number, x: number, y: number) {
28 | const minScale = transform.getMinScale();
29 | const scale = transform.getScale();
30 | if (s < minScale) s = minScale;
31 | let offsetX = transform.getOffsetX();
32 | let offsetY = transform.getOffsetY();
33 |
34 | offsetX = s * (offsetX + x)/scale - x;
35 | offsetY = s * (offsetY + y)/scale - y;
36 |
37 | transform.setOffsetX(offsetX);
38 | transform.setOffsetY(offsetY);
39 | transform.setScale(s);
40 | }
41 |
42 | function simpleDragZoom(e: PointerEvent | MouseEvent, scaleOrigin: { x: number; y: number; s: number; } | null, transform: Transform) {
43 | if (e.shiftKey) { //scale
44 | if (!scaleOrigin)
45 | scaleOrigin = { x: e.offsetX, y: e.offsetY, s: transform.getScale() };
46 | updateScale(transform, scaleOrigin.s + (scaleOrigin.y - e.offsetY) / 50, scaleOrigin.x, scaleOrigin.y);
47 | }
48 | else { //drag
49 | scaleOrigin = null;
50 | let offsetX = transform.getOffsetX();
51 | let offsetY = transform.getOffsetY();
52 | offsetX -= e.movementX;
53 | offsetY -= e.movementY;
54 | transform.setOffsetX(offsetX);
55 | transform.setOffsetY(offsetY);
56 | }
57 | return scaleOrigin;
58 | }
59 |
60 | function withPointers(node: HTMLElement, transform: Transform) {
61 | function rescaleWithWheel(e: MouseWheelEvent) {
62 | e.preventDefault();
63 | e.cancelBubble = true;
64 | const delta = Math.sign(e.deltaY);
65 | updateScale(transform, transform.getScale() - delta/10, e.offsetX, e.offsetY);
66 | }
67 |
68 | // pointer event cache
69 | const pointers: PointerEvent[] = [];
70 | function storeEvent(ev: PointerEvent) {
71 | for (var i = 0; i < pointers.length; i++) {
72 | if (pointers[i].pointerId === ev.pointerId) {
73 | const ev2 = iterationCopy(ev);
74 | pointers[i] = ev2;
75 | break;
76 | }
77 | }
78 | if(i === pointers.length)
79 | pointers.push(ev);
80 | }
81 | function removeEvent(ev: PointerEvent) {
82 | for (var i = 0; i < pointers.length; i++) {
83 | if (pointers[i].pointerId === ev.pointerId) {
84 | pointers.splice(i, 1);
85 | break;
86 | }
87 | }
88 | }
89 |
90 |
91 | let scaleOrigin: {x: number, y: number, s: number} | null = null;
92 | function startDrag(e: PointerEvent) {
93 | node.setPointerCapture(e.pointerId);
94 | if (!transform.getDragging()) {
95 | node.addEventListener(detectPointerEvents.prefix('pointermove'), drag, true);
96 | transform.setDragging(true);
97 | }
98 |
99 | e.preventDefault();
100 | e.cancelBubble = true;
101 | storeEvent(e);
102 | }
103 |
104 | function drag(e: PointerEvent) {
105 | if (pointers.length === 1) {
106 | scaleOrigin = simpleDragZoom(e, scaleOrigin, transform);
107 | }
108 | else if (pointers.length === 2) { //scale
109 | const x0 = pointers[0].offsetX;
110 | const y0 = pointers[0].offsetY;
111 | const x1 = pointers[1].offsetX;
112 | const y1 = pointers[1].offsetY;
113 | const x2 = e.offsetX;
114 | const y2 = e.offsetY;
115 | const dx = x0-x1;
116 | const dy = y0-y1;
117 | const l1 = Math.sqrt(dx*dx + dy*dy);
118 | let dx1, dy1;
119 | if (e.pointerId === pointers[0].pointerId) {
120 | dx1 = x2 - x1;
121 | dy1 = y2 - y1;
122 | }
123 | else {
124 | dx1 = x2 - x0;
125 | dy1 = y2 - y0;
126 | }
127 | var l2 = Math.sqrt(dx1*dx1+dy1*dy1);
128 | updateScale(transform, transform.getScale() * l2/l1, x2, y2);
129 | }
130 |
131 | e.preventDefault();
132 | e.cancelBubble = true;
133 | storeEvent(e);
134 | }
135 | function stopDrag(e: PointerEvent) {
136 | e.preventDefault();
137 | e.cancelBubble = true;
138 |
139 | removeEvent(e);
140 |
141 | node.releasePointerCapture(e.pointerId);
142 | if (pointers.length === 0) {
143 | transform.setDragging(false);
144 | node.removeEventListener(detectPointerEvents.prefix('pointermove'), drag, true);
145 | scaleOrigin = null;
146 | }
147 | }
148 |
149 | node.addEventListener(detectPointerEvents.prefix('pointerdown'), startDrag, true);
150 | node.addEventListener(detectPointerEvents.prefix('pointerup'), stopDrag, true);
151 | node.addEventListener('wheel', rescaleWithWheel, true);
152 |
153 | return () => {
154 | node.removeEventListener(detectPointerEvents.prefix('pointerdown'), startDrag, true);
155 | node.removeEventListener(detectPointerEvents.prefix('pointerup'), stopDrag, true);
156 | node.removeEventListener('wheel', rescaleWithWheel, true);
157 | }
158 | }
159 |
160 | function withMouse(node: HTMLElement, transform: Transform) {
161 | function rescaleWithWheel(e: MouseWheelEvent) {
162 | e.preventDefault();
163 | e.cancelBubble = true;
164 | const delta = Math.sign(e.deltaY);
165 | updateScale(transform, transform.getScale() - delta/10, e.offsetX, e.offsetY);
166 | }
167 |
168 | let scaleOrigin: {x: number, y: number, s: number} | null = null;
169 | function startDrag(e: MouseEvent) {
170 | if (typeof (node).setCapture === 'function')
171 | (node).setCapture();
172 | if (!transform.getDragging()) {
173 | node.addEventListener('mousemove', drag, true);
174 | window.addEventListener('mouseup', stopDrag, true);
175 | transform.setDragging(true);
176 | }
177 |
178 | e.preventDefault();
179 | e.cancelBubble = true;
180 | }
181 |
182 | function drag(e: MouseEvent) {
183 | scaleOrigin = simpleDragZoom(e, scaleOrigin, transform);
184 |
185 | e.preventDefault();
186 | e.cancelBubble = true;
187 | }
188 | function stopDrag(e: MouseEvent) {
189 | e.preventDefault();
190 | e.cancelBubble = true;
191 |
192 | if (typeof (node).releaseCapture === 'function')
193 | (node).releaseCapture();
194 | transform.setDragging(false);
195 | node.removeEventListener('mousemove', drag, true);
196 | window.removeEventListener('mouseup', stopDrag, true);
197 | scaleOrigin = null;
198 | }
199 |
200 | node.addEventListener('mousedown', startDrag, true);
201 | node.addEventListener('mouseup', stopDrag, true);
202 | node.addEventListener('wheel', rescaleWithWheel, true);
203 |
204 | return () => {
205 | node.removeEventListener('mousedown', startDrag, true);
206 | node.removeEventListener('mouseup', stopDrag, true);
207 | node.removeEventListener('wheel', rescaleWithWheel, true);
208 | }
209 | }
210 |
211 | const runningInBrowser = typeof window !== 'undefined';
212 | const usePointerEvents = runningInBrowser && !!detectPointerEvents.maxTouchPoints;
213 | export const panHandler = usePointerEvents ? withPointers : withMouse;
214 |
--------------------------------------------------------------------------------
/src/preprocessor/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path');
3 | const ts = require('typescript')
4 | const tsConfig = require('../../tsconfig.json')
5 |
6 | module.exports = {
7 | preprocess: {
8 | script: ({ content, attributes, filename }) => {
9 | let transpiled
10 | if (attributes.src) {
11 | const filePath = path.resolve(path.dirname(filename), attributes.src)
12 | console.log(filename);
13 | transpiled = ts.transpileModule(fs.readFileSync(filePath).toString(), tsConfig)
14 | }
15 | else if (attributes.type === 'typescript') {
16 | transpiled = ts.transpileModule(content, tsConfig)
17 | }
18 | else {
19 | return {code: content};
20 | }
21 | return {
22 | code: transpiled.outputText,
23 | map: transpiled.sourceMapText
24 | };
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": { "*": ["types/*"] },
5 | "target": "ES2015",
6 | "lib": ["dom", "es2015"],
7 | "module": "ES2015",
8 | "esModuleInterop": true,
9 | "pretty": true,
10 | "strict": true,
11 | "noImplicitAny": true,
12 | "noImplicitReturns": true,
13 | "noImplicitThis": true,
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "alwaysStrict": true,
17 | "diagnostics": true,
18 | "listFiles": true,
19 | "listEmittedFiles": true,
20 | "strictNullChecks": true,
21 | "strictFunctionTypes": true,
22 | "removeComments": true,
23 | "preserveConstEnums": true,
24 | "experimentalDecorators": true,
25 | "emitDecoratorMetadata": true,
26 | "noFallthroughCasesInSwitch": true,
27 | "sourceMap": true,
28 | "allowSyntheticDefaultImports": true
29 | },
30 | "include": ["src/**/*.ts"]
31 | }
--------------------------------------------------------------------------------
/types/detect-pointer-events.d.ts:
--------------------------------------------------------------------------------
1 | declare module DetectPointerEvents {
2 | export const hasApi: boolean;
3 | export const requiresPrefix: boolean;
4 | export const hasTouch: boolean;
5 | export const maxTouchPoints: number;
6 | // Below we lie to the compiler since `prefix(name)` returns the
7 | // browser equivalent of (the event, method or property) `name`.
8 | // Declaring the real behavior makes the compiler emit an error
9 | // when used elsewhere in `node.addEventListener(name, handler)`.
10 | export function prefix(value: 'pointerdown'): 'pointerdown';
11 | export function prefix(value: 'pointerup'): 'pointerup';
12 | export function prefix(value: 'pointercancel'): 'pointercancel';
13 | export function prefix(value: 'pointermove'): 'pointermove';
14 | export function prefix(value: 'pointerover'): 'pointerover';
15 | export function prefix(value: 'pointerout'): 'pointerout';
16 | export function prefix(value: 'pointerenter'): 'pointerenter';
17 | export function prefix(value: 'pointerleave'): 'pointerleave';
18 | export function prefix(value: 'gotpointercapture'): 'gotpointercapture';
19 | export function prefix(value: 'lostpointercapture'): 'lostpointercapture';
20 | export function prefix(value: 'maxTouchPoints'): 'maxTouchPoints';
21 | export function update(): void;
22 | }
23 |
24 | export default DetectPointerEvents;
25 |
--------------------------------------------------------------------------------