Skip to content

Commit

Permalink
start on image optimize
Browse files Browse the repository at this point in the history
  • Loading branch information
kjk committed Jun 30, 2024
1 parent 5060f46 commit 4c883e5
Show file tree
Hide file tree
Showing 26 changed files with 7,438 additions and 7 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"marked": "^12.0.2",
"protobufjs": "^7.3.2",
"qr-scanner": "^1.4.2",
"svgo": "^3.3.2",
"yaml": "^2.4.5"
}
}
8 changes: 4 additions & 4 deletions frontend/src/Main.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"/gisteditor/",
"GitHub Gist editor and a notepad for notes and code snippets stored locally in the browser",
)}
{@render row(
<!-- {@render row(
"notepad2",
"/notepad2/",
`like a Windows <a
Expand All @@ -104,7 +104,7 @@
href="https://github.com/zufuliu/notepad2">notepad2</a
>
text editor but in the browser`,
)}
)} -->
{@render row("Go Playground", "/goplayground/", "a better Go Playground")}
<!-- {@render row(
"calc",
Expand Down Expand Up @@ -133,9 +133,9 @@
<tbody>
{@render row("reader", "/reader/", "Comic Book (.cbz, .cbr) reader")}
{@render row(
"image resize and optimize",
"image optimize",
"/image-resize-optimize/",
"optimize and resize multiple images at once",
"optimize multiple images at once",
)}
{@render row("qrscanner", "/qrscanner/", "qrscanner")}
</tbody>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/image-resize-optimize/ImageOptim.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<script>
import ShowSupportsFileSystem from "../ShowSupportsFileSystem.svelte";
import { getFileExt, supportsFileSystem } from "../fileutil";
import { supportsFileSystem } from "../fileutil";
import { sniffFileType } from "../sniffFileType";
import { fmtSize, len } from "../util";
import TopNav from "../TopNav.svelte";
Expand Down Expand Up @@ -117,7 +117,7 @@
</script>

<TopNav>
<span class="text-purple-800">Resize and optimize multiple image files</span>
<span class="text-purple-800">Optimize multiple image files</span>
</TopNav>

<div class="mx-4 mt-2">
Expand All @@ -126,7 +126,7 @@
<div>Optimize and resize multiple image files on your computer</div>
<div>
<button class="underline" onclick={handleClick}
>Select folder with images</button
>Open folder with images</button
> from your computer.
</div>
{#if len(files) > 0}
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/image-resize-optimize/engines/AvifImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Reference:
* https://github.com/packurl/wasm_avif
*/
import { Mimes } from "../mimes";
import { avif } from "./AvifWasmModule";
import { ImageBase } from "./ImageBase";
export class AvifImage extends ImageBase {
/**
* Encode avif image with canvas context
* @param context
* @param width
* @param height
* @param quality
* @param speed
* @returns
*/
static async encode(context, width, height, quality = 50, speed = 8) {
const imageData = context.getImageData(0, 0, width, height).data;
const bytes = new Uint8Array(imageData);
const result = await avif(bytes, width, height, quality, speed);
return new Blob([result], { type: Mimes.avif });
}
async compress() {
const { width, height } = this.getOutputDimension();
try {
const { context } = await this.createCanvas(width, height);
const blob = await AvifImage.encode(
context,
width,
height,
this.option.avif.quality,
this.option.avif.speed,
);
return {
width,
height,
blob,
src: URL.createObjectURL(blob),
};
} catch (error) {
return this.failResult();
}
}
}
47 changes: 47 additions & 0 deletions frontend/src/image-resize-optimize/engines/AvifWasmModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import avifWasmBinaryFile from "./avif.wasm?url";
/**
* Encodes the supplied ImageData rgba array.
* @param {Uint8Array} bytes
* @param {number} width
* @param {number} height
* @param {number} quality (1 to 100)
* @param {number} speed (1 to 10)
* @return {Promise<Uint8Array>}
*/
export const avif = async (bytes, width, height, quality = 50, speed = 6) => {
const imports = {
wbg: {
__wbg_log_12edb8942696c207: (p, n) => {
new TextDecoder().decode(
new Uint8Array(wasm.memory.buffer).subarray(p, p + n),
);
},
},
};
const {
instance: { exports: wasm },
} = await WebAssembly.instantiateStreaming(
await fetch(avifWasmBinaryFile, { cache: "force-cache" }),
imports,
);
const malloc = wasm.__wbindgen_malloc;
const free = wasm.__wbindgen_free;
const pointer = wasm.__wbindgen_add_to_stack_pointer;
const n1 = bytes.length;
const p1 = malloc(n1, 1);
const r = pointer(-16);
try {
new Uint8Array(wasm.memory.buffer).set(bytes, p1);
wasm.avif_from_imagedata(r, p1, n1, width, height, quality, speed);
const arr = new Int32Array(wasm.memory.buffer);
const p2 = arr[r / 4];
const n2 = arr[r / 4 + 1];
const res = new Uint8Array(wasm.memory.buffer)
.subarray(p2, p2 + n2)
.slice();
free(p2, n2);
return res;
} finally {
pointer(16);
}
};
19 changes: 19 additions & 0 deletions frontend/src/image-resize-optimize/engines/CanvasImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ImageBase } from "./ImageBase";
/**
* JPEG/JPG/WEBP is compatible
*/
export class CanvasImage extends ImageBase {
async compress() {
const dimension = this.getOutputDimension();
const blob = await this.createBlob(
dimension.width,
dimension.height,
this.option.jpeg.quality,
);
return {
...dimension,
blob,
src: URL.createObjectURL(blob),
};
}
}
84 changes: 84 additions & 0 deletions frontend/src/image-resize-optimize/engines/GifImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Reference:
* https://github.com/renzhezhilu/gifsicle-wasm-browser
* https://www.lcdf.org/gifsicle/man.html
*/
import { gifsicle } from "./GifWasmModule";
import { ImageBase } from "./ImageBase";
export class GifImage extends ImageBase {
async compress() {
try {
const { width, height } = this.getOutputDimension();
const commands = [
`--optimize=3`,
`--resize=${width}x${height}`,
`--colors=${this.option.gif.colors}`,
];
if (this.option.gif.dithering) {
commands.push(`--dither=floyd-steinberg`);
}
commands.push(`--output=/out/${this.info.name}`);
commands.push(this.info.name);
const buffer = await this.info.blob.arrayBuffer();
const result = await gifsicle({
data: [
{
file: buffer,
name: this.info.name,
},
],
command: [commands.join(" ")],
});
if (!Array.isArray(result) || result.length !== 1) {
return this.failResult();
}
const blob = new Blob([result[0].file], {
type: this.info.blob.type,
});
return {
width,
height,
blob,
src: URL.createObjectURL(blob),
};
} catch (error) {
return this.failResult();
}
}
async preview() {
const { width, height } = this.getPreviewDimension();
const commands = [
`--resize=${width}x${height}`,
`--colors=${this.option.gif.colors}`,
`--output=/out/${this.info.name}`,
this.info.name,
];
const buffer = await this.info.blob.arrayBuffer();
const result = await gifsicle({
data: [
{
file: buffer,
name: this.info.name,
},
],
command: [commands.join(" ")],
});
if (!Array.isArray(result) || result.length !== 1) {
return {
width: this.info.width,
height: this.info.height,
blob: this.info.blob,
src: URL.createObjectURL(this.info.blob),
};
}
const blob = new Blob([result[0].file], {
type: this.info.blob.type,
});
return {
width,
height,
blob,
src: URL.createObjectURL(blob),
};
}
}
Loading

0 comments on commit 4c883e5

Please sign in to comment.