forked from external-repos/squoosh
Compare commits
42 Commits
libsquoosh
...
maudnals-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87eae143ef | ||
|
|
8aeea1dc04 | ||
|
|
8a860f285c | ||
|
|
dd2b2c361f | ||
|
|
e673c9c772 | ||
|
|
c484750779 | ||
|
|
66ec763667 | ||
|
|
029eda7b37 | ||
|
|
e6810059ef | ||
|
|
4eba015009 | ||
|
|
dd49a1f23d | ||
|
|
5ecb99992c | ||
|
|
d9c6ebe0a2 | ||
|
|
fba67a7a18 | ||
|
|
129b925098 | ||
|
|
e23bc4d2e5 | ||
|
|
f523a07f01 | ||
|
|
e3f840c6da | ||
|
|
cad09160b6 | ||
|
|
9663c23489 | ||
|
|
58d467fbba | ||
|
|
b2465ebe48 | ||
|
|
8ea8d05706 | ||
|
|
95a1b35c91 | ||
|
|
914cdea41d | ||
|
|
6bfce29af6 | ||
|
|
64cad1cc23 | ||
|
|
87f25d909b | ||
|
|
bdfdaf53af | ||
|
|
1ab9e8b3ab | ||
|
|
2718077d3d | ||
|
|
c4bc369c6b | ||
|
|
68ce8f420d | ||
|
|
27f103fee5 | ||
|
|
c21d3714a8 | ||
|
|
3ea0d88c4f | ||
|
|
202d0bc088 | ||
|
|
bdb5b16372 | ||
|
|
30140c2b7a | ||
|
|
10996cdbca | ||
|
|
e5806507d4 | ||
|
|
1c5b44f9a1 |
1
.husky/pre-commit
Executable file
1
.husky/pre-commit
Executable file
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
@@ -4,6 +4,7 @@ import { program } from 'commander/esm.mjs';
|
||||
import JSON5 from 'json5';
|
||||
import path from 'path';
|
||||
import { promises as fsp } from 'fs';
|
||||
import { cpus } from 'os';
|
||||
import ora from 'ora';
|
||||
import kleur from 'kleur';
|
||||
|
||||
@@ -75,7 +76,9 @@ async function getInputFiles(paths) {
|
||||
|
||||
for (const inputPath of paths) {
|
||||
const files = (await fsp.lstat(inputPath)).isDirectory()
|
||||
? (await fsp.readdir(inputPath, {withFileTypes: true})).filter(dirent => dirent.isFile()).map(dirent => path.join(inputPath, dirent.name))
|
||||
? (await fsp.readdir(inputPath, { withFileTypes: true }))
|
||||
.filter((dirent) => dirent.isFile())
|
||||
.map((dirent) => path.join(inputPath, dirent.name))
|
||||
: [inputPath];
|
||||
for (const file of files) {
|
||||
try {
|
||||
@@ -101,7 +104,7 @@ async function getInputFiles(paths) {
|
||||
async function processFiles(files) {
|
||||
files = await getInputFiles(files);
|
||||
|
||||
const imagePool = new ImagePool();
|
||||
const imagePool = new ImagePool(cpus().length);
|
||||
|
||||
const results = new Map();
|
||||
const progress = progressTracker(results);
|
||||
@@ -116,7 +119,8 @@ async function processFiles(files) {
|
||||
let decoded = 0;
|
||||
let decodedFiles = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const image = imagePool.ingestImage(file);
|
||||
const buffer = await fsp.readFile(file);
|
||||
const image = imagePool.ingestImage(buffer);
|
||||
await image.decoded;
|
||||
results.set(image, {
|
||||
file,
|
||||
@@ -178,7 +182,7 @@ async function processFiles(files) {
|
||||
const outputPath = path.join(
|
||||
program.opts().outputDir,
|
||||
path.basename(originalFile, path.extname(originalFile)) +
|
||||
program.opts().suffix
|
||||
program.opts().suffix,
|
||||
);
|
||||
for (const output of Object.values(image.encodedWith)) {
|
||||
const outputFile = `${outputPath}.${(await output).extension}`;
|
||||
|
||||
@@ -14,28 +14,40 @@ import { promises as fs } from 'fs';
|
||||
|
||||
import { lookup as lookupMime } from 'mime-types';
|
||||
|
||||
const prefix = 'data-url:';
|
||||
const prefix = /^data-url(-text)?:/;
|
||||
|
||||
export default function dataURLPlugin() {
|
||||
return {
|
||||
name: 'data-url-plugin',
|
||||
async resolveId(id, importer) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
const match = prefix.exec(id);
|
||||
if (!match) return;
|
||||
return (
|
||||
prefix + (await this.resolve(id.slice(prefix.length), importer)).id
|
||||
match[0] + (await this.resolve(id.slice(match[0].length), importer)).id
|
||||
);
|
||||
},
|
||||
async load(id) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
const realId = id.slice(prefix.length);
|
||||
const match = prefix.exec(id);
|
||||
if (!match) return;
|
||||
|
||||
const isText = !!match[1];
|
||||
const realId = id.slice(match[0].length);
|
||||
this.addWatchFile(realId);
|
||||
|
||||
const source = await fs.readFile(realId);
|
||||
const type = lookupMime(realId) || 'text/plain';
|
||||
|
||||
return `export default 'data:${type};base64,${source.toString(
|
||||
'base64',
|
||||
)}';`;
|
||||
if (isText) {
|
||||
const encodedBody = encodeURIComponent(source.toString('utf8'));
|
||||
|
||||
return `export default ${JSON.stringify(
|
||||
`data:${type};charset=utf-8,${encodedBody}`,
|
||||
)};`;
|
||||
}
|
||||
|
||||
return `export default ${JSON.stringify(
|
||||
`data:${type};base64,${source.toString('base64')}`,
|
||||
)};`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,7 +77,9 @@ export default function entryDataPlugin() {
|
||||
}
|
||||
|
||||
return JSON.stringify(
|
||||
getDependencies(chunks, chunk).map((filename) => fileNameToURL(filename)),
|
||||
getDependencies(chunks, chunk).map((filename) =>
|
||||
fileNameToURL(filename),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -28,10 +28,17 @@ if (!self.<%- amdFunctionName %>) {
|
||||
<% } else { %>
|
||||
new Promise(resolve => {
|
||||
if ("document" in self) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "preload";
|
||||
link.as = "script";
|
||||
link.href = uri;
|
||||
link.onload = () => {
|
||||
const script = document.createElement("script");
|
||||
script.src = uri;
|
||||
script.onload = resolve;
|
||||
document.head.appendChild(script);
|
||||
};
|
||||
document.head.appendChild(link);
|
||||
} else {
|
||||
self.nextDefineUri = uri;
|
||||
importScripts(uri);
|
||||
|
||||
@@ -16,21 +16,25 @@ You can start using the libSquoosh by adding these lines to the top of your JS p
|
||||
|
||||
```js
|
||||
import { ImagePool } from '@squoosh/lib';
|
||||
const imagePool = new ImagePool();
|
||||
import { cpus } from 'os';
|
||||
const imagePool = new ImagePool(cpus().length);
|
||||
```
|
||||
|
||||
This will create an image pool with an underlying processing pipeline that you can use to ingest and encode images. The ImagePool constructor takes one argument that defines how many parallel operations it is allowed to run at any given time. By default, this number is set to the amount of CPU cores available in the system it is running on.
|
||||
This will create an image pool with an underlying processing pipeline that you can use to ingest and encode images. The ImagePool constructor takes one argument that defines how many parallel operations it is allowed to run at any given time.
|
||||
|
||||
:warning: Important! Make sure to only create 1 `ImagePool` when performing parallel image processing. If you create multiple pools, the `ImagePool` can run out of memory and crash. By reusing a single `ImagePool`, you can ensure that the backing worker queue and processing pipeline releases memory prior to processing the next image.
|
||||
|
||||
## Ingesting images
|
||||
|
||||
You can ingest a new image like so:
|
||||
|
||||
```js
|
||||
const imagePath = 'path/to/image.png';
|
||||
const image = imagePool.ingestImage(imagePath);
|
||||
import fs from 'fs/promises';
|
||||
const file = await fs.readFile('./path/to/image.png');
|
||||
const image = imagePool.ingestImage(file);
|
||||
```
|
||||
|
||||
The `ingestImage` function can take anything the node [`readFile`][readfile] function can take, including a buffer and `FileHandle`.
|
||||
The `ingestImage` function can accept any [`ArrayBuffer`][arraybuffer] whether that is from `readFile()` or `fetch()`.
|
||||
|
||||
The returned `image` object is a representation of the original image, that you can now preprocess, encode, and extract information about.
|
||||
|
||||
@@ -47,7 +51,7 @@ const preprocessOptions = {
|
||||
enabled: true,
|
||||
width: 100,
|
||||
height: 50,
|
||||
}
|
||||
},
|
||||
/*
|
||||
//When either width or height is specified, the image resized to specified size keeping aspect ratio.
|
||||
resize: {
|
||||
@@ -55,7 +59,7 @@ const preprocessOptions = {
|
||||
width: 100,
|
||||
}
|
||||
*/
|
||||
}
|
||||
};
|
||||
await image.preprocess(preprocessOptions);
|
||||
|
||||
const encodeOptions = {
|
||||
@@ -63,9 +67,8 @@ const encodeOptions = {
|
||||
jxl: {
|
||||
quality: 90,
|
||||
},
|
||||
}
|
||||
};
|
||||
await image.encode(encodeOptions);
|
||||
|
||||
```
|
||||
|
||||
The default values for each option can be found in the [`codecs.ts`][codecs.ts] file under `defaultEncoderOptions`. Every unspecified value will use the default value specified there. _Better documentation is needed here._
|
||||
@@ -168,4 +171,4 @@ const encodeOptions: {
|
||||
[squoosh]: https://squoosh.app
|
||||
[codecs.ts]: https://github.com/GoogleChromeLabs/squoosh/blob/dev/libsquoosh/src/codecs.ts
|
||||
[butteraugli]: https://github.com/google/butteraugli
|
||||
[readfile]: https://nodejs.org/api/fs.html#fs_fspromises_readfile_path_options
|
||||
[arraybuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
|
||||
|
||||
@@ -29,14 +29,23 @@ interface ResizeWithAspectParams {
|
||||
target_height: number;
|
||||
}
|
||||
|
||||
interface ResizeInstantiateOptions {
|
||||
export interface ResizeOptions {
|
||||
width: number;
|
||||
height: number;
|
||||
method: string;
|
||||
method: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3';
|
||||
premultiply: boolean;
|
||||
linearRGB: boolean;
|
||||
}
|
||||
|
||||
export interface QuantOptions {
|
||||
numColors: number;
|
||||
dither: number;
|
||||
}
|
||||
|
||||
export interface RotateOptions {
|
||||
numRotations: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// Needed for being able to use ImageData as type in codec types
|
||||
type ImageData = import('./image_data.js').default;
|
||||
@@ -52,6 +61,7 @@ import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js';
|
||||
import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm';
|
||||
import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js';
|
||||
import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm';
|
||||
import type { EncodeOptions as MozJPEGEncodeOptions } from '../../codecs/mozjpeg/enc/mozjpeg_enc';
|
||||
|
||||
// WebP
|
||||
import type { WebPModule as WebPEncodeModule } from '../../codecs/webp/enc/webp_enc';
|
||||
@@ -59,6 +69,7 @@ import webpEnc from '../../codecs/webp/enc/webp_node_enc.js';
|
||||
import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm';
|
||||
import webpDec from '../../codecs/webp/dec/webp_node_dec.js';
|
||||
import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm';
|
||||
import type { EncodeOptions as WebPEncodeOptions } from '../../codecs/webp/enc/webp_enc.js';
|
||||
|
||||
// AVIF
|
||||
import type { AVIFModule as AVIFEncodeModule } from '../../codecs/avif/enc/avif_enc';
|
||||
@@ -69,6 +80,7 @@ import avifEncMtWorker from 'chunk-url:../../codecs/avif/enc/avif_node_enc_mt.wo
|
||||
import avifEncMtWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc_mt.wasm';
|
||||
import avifDec from '../../codecs/avif/dec/avif_node_dec.js';
|
||||
import avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm';
|
||||
import type { EncodeOptions as AvifEncodeOptions } from '../../codecs/avif/enc/avif_enc.js';
|
||||
|
||||
// JXL
|
||||
import type { JXLModule as JXLEncodeModule } from '../../codecs/jxl/enc/jxl_enc';
|
||||
@@ -76,6 +88,7 @@ import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js';
|
||||
import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm';
|
||||
import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js';
|
||||
import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm';
|
||||
import type { EncodeOptions as JxlEncodeOptions } from '../../codecs/jxl/enc/jxl_enc.js';
|
||||
|
||||
// WP2
|
||||
import type { WP2Module as WP2EncodeModule } from '../../codecs/wp2/enc/wp2_enc';
|
||||
@@ -83,6 +96,7 @@ import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js';
|
||||
import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm';
|
||||
import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js';
|
||||
import wp2DecWasm from 'asset-url:../../codecs/wp2/dec/wp2_node_dec.wasm';
|
||||
import type { EncodeOptions as WP2EncodeOptions } from '../../codecs/wp2/enc/wp2_enc.js';
|
||||
|
||||
// PNG
|
||||
import * as pngEncDec from '../../codecs/png/pkg/squoosh_png.js';
|
||||
@@ -95,6 +109,9 @@ const pngEncDecPromise = pngEncDec.default(
|
||||
import * as oxipng from '../../codecs/oxipng/pkg/squoosh_oxipng.js';
|
||||
import oxipngWasm from 'asset-url:../../codecs/oxipng/pkg/squoosh_oxipng_bg.wasm';
|
||||
const oxipngPromise = oxipng.default(fsp.readFile(pathify(oxipngWasm)));
|
||||
interface OxiPngEncodeOptions {
|
||||
level: number;
|
||||
}
|
||||
|
||||
// Resize
|
||||
import * as resize from '../../codecs/resize/pkg/squoosh_resize.js';
|
||||
@@ -171,13 +188,7 @@ export const preprocessors = {
|
||||
buffer: Uint8Array,
|
||||
input_width: number,
|
||||
input_height: number,
|
||||
{
|
||||
width,
|
||||
height,
|
||||
method,
|
||||
premultiply,
|
||||
linearRGB,
|
||||
}: ResizeInstantiateOptions,
|
||||
{ width, height, method, premultiply, linearRGB }: ResizeOptions,
|
||||
) => {
|
||||
({ width, height } = resizeWithAspect({
|
||||
input_width,
|
||||
@@ -218,7 +229,7 @@ export const preprocessors = {
|
||||
buffer: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
{ numColors, dither }: { numColors: number; dither: number },
|
||||
{ numColors, dither }: QuantOptions,
|
||||
) =>
|
||||
new ImageData(
|
||||
imageQuant.quantize(buffer, width, height, numColors, dither),
|
||||
@@ -239,7 +250,7 @@ export const preprocessors = {
|
||||
buffer: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
{ numRotations }: { numRotations: number },
|
||||
{ numRotations }: RotateOptions,
|
||||
) => {
|
||||
const degrees = (numRotations * 90) % 360;
|
||||
const sameDimensions = degrees == 0 || degrees == 180;
|
||||
@@ -401,13 +412,13 @@ export const codecs = {
|
||||
jxlEncWasm,
|
||||
),
|
||||
defaultEncoderOptions: {
|
||||
speed: 4,
|
||||
effort: 1,
|
||||
quality: 75,
|
||||
progressive: false,
|
||||
epf: -1,
|
||||
nearLossless: 0,
|
||||
lossyPalette: false,
|
||||
decodingSpeedTier: 0,
|
||||
photonNoiseIso: 0,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'quality',
|
||||
@@ -480,3 +491,12 @@ export const codecs = {
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export {
|
||||
MozJPEGEncodeOptions,
|
||||
WebPEncodeOptions,
|
||||
AvifEncodeOptions,
|
||||
JxlEncodeOptions,
|
||||
WP2EncodeOptions,
|
||||
OxiPngEncodeOptions,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { isMainThread } from 'worker_threads';
|
||||
import { cpus } from 'os';
|
||||
import { promises as fsp } from 'fs';
|
||||
|
||||
import { codecs as encoders, preprocessors } from './codecs.js';
|
||||
import {
|
||||
AvifEncodeOptions,
|
||||
codecs as encoders,
|
||||
JxlEncodeOptions,
|
||||
MozJPEGEncodeOptions,
|
||||
OxiPngEncodeOptions,
|
||||
preprocessors,
|
||||
QuantOptions,
|
||||
ResizeOptions,
|
||||
RotateOptions,
|
||||
WebPEncodeOptions,
|
||||
WP2EncodeOptions,
|
||||
} from './codecs.js';
|
||||
import WorkerPool from './worker_pool.js';
|
||||
import { autoOptimize } from './auto-optimizer.js';
|
||||
import type ImageData from './image_data';
|
||||
@@ -10,30 +20,34 @@ import type ImageData from './image_data';
|
||||
export { ImagePool, encoders, preprocessors };
|
||||
type EncoderKey = keyof typeof encoders;
|
||||
type PreprocessorKey = keyof typeof preprocessors;
|
||||
type FileLike = Buffer | ArrayBuffer | string | ArrayBufferView;
|
||||
|
||||
type PreprocessOptions = {
|
||||
resize?: ResizeOptions;
|
||||
quant?: QuantOptions;
|
||||
rotate?: RotateOptions;
|
||||
};
|
||||
type EncodeResult = {
|
||||
optionsUsed: object;
|
||||
binary: Uint8Array;
|
||||
extension: string;
|
||||
size: number;
|
||||
};
|
||||
type EncoderOptions = {
|
||||
mozjpeg?: Partial<MozJPEGEncodeOptions>;
|
||||
webp?: Partial<WebPEncodeOptions>;
|
||||
avif?: Partial<AvifEncodeOptions>;
|
||||
jxl?: Partial<JxlEncodeOptions>;
|
||||
wp2?: Partial<WP2EncodeOptions>;
|
||||
oxipng?: Partial<OxiPngEncodeOptions>;
|
||||
};
|
||||
|
||||
async function decodeFile({
|
||||
file,
|
||||
}: {
|
||||
file: FileLike;
|
||||
file: ArrayBuffer | ArrayLike<number>;
|
||||
}): Promise<{ bitmap: ImageData; size: number }> {
|
||||
let buffer;
|
||||
if (ArrayBuffer.isView(file)) {
|
||||
buffer = Buffer.from(file.buffer);
|
||||
file = 'Binary blob';
|
||||
} else if (file instanceof ArrayBuffer) {
|
||||
buffer = Buffer.from(file);
|
||||
file = 'Binary blob';
|
||||
} else if ((file as unknown) instanceof Buffer) {
|
||||
// TODO: Check why we need type assertions here.
|
||||
buffer = (file as unknown) as Buffer;
|
||||
file = 'Binary blob';
|
||||
} else if (typeof file === 'string') {
|
||||
buffer = await fsp.readFile(file);
|
||||
} else {
|
||||
throw Error('Unexpected input type');
|
||||
}
|
||||
const firstChunk = buffer.slice(0, 16);
|
||||
const array = new Uint8Array(file);
|
||||
const firstChunk = array.slice(0, 16);
|
||||
const firstChunkString = Array.from(firstChunk)
|
||||
.map((v) => String.fromCodePoint(v))
|
||||
.join('');
|
||||
@@ -41,14 +55,14 @@ async function decodeFile({
|
||||
detectors.some((detector) => detector.exec(firstChunkString)),
|
||||
)?.[0] as EncoderKey | undefined;
|
||||
if (!key) {
|
||||
throw Error(`${file} has an unsupported format`);
|
||||
throw Error(`File has an unsupported format`);
|
||||
}
|
||||
const encoder = encoders[key];
|
||||
const mod = await encoder.dec();
|
||||
const rgba = mod.decode(new Uint8Array(buffer));
|
||||
const rgba = mod.decode(array);
|
||||
return {
|
||||
bitmap: rgba,
|
||||
size: buffer.length,
|
||||
size: array.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,7 +97,7 @@ async function encodeImage({
|
||||
encConfig: any;
|
||||
optimizerButteraugliTarget: number;
|
||||
maxOptimizerRounds: number;
|
||||
}) {
|
||||
}): Promise<EncodeResult> {
|
||||
let binary: Uint8Array;
|
||||
let optionsUsed = encConfig;
|
||||
const encoder = await encoders[encName].enc();
|
||||
@@ -169,12 +183,15 @@ function handleJob(params: JobMessage) {
|
||||
* Represents an ingested image.
|
||||
*/
|
||||
class Image {
|
||||
public file: FileLike;
|
||||
public file: ArrayBuffer | ArrayLike<number>;
|
||||
public workerPool: WorkerPool<JobMessage, any>;
|
||||
public decoded: Promise<{ bitmap: ImageData }>;
|
||||
public encodedWith: { [key: string]: any };
|
||||
public encodedWith: { [key in EncoderKey]?: EncodeResult };
|
||||
|
||||
constructor(workerPool: WorkerPool<JobMessage, any>, file: FileLike) {
|
||||
constructor(
|
||||
workerPool: WorkerPool<JobMessage, any>,
|
||||
file: ArrayBuffer | ArrayLike<number>,
|
||||
) {
|
||||
this.file = file;
|
||||
this.workerPool = workerPool;
|
||||
this.decoded = workerPool.dispatchJob({ operation: 'decode', file });
|
||||
@@ -183,10 +200,10 @@ class Image {
|
||||
|
||||
/**
|
||||
* Define one or several preprocessors to use on the image.
|
||||
* @param {object} preprocessOptions - An object with preprocessors to use, and their settings.
|
||||
* @param {PreprocessOptions} preprocessOptions - An object with preprocessors to use, and their settings.
|
||||
* @returns {Promise<undefined>} - A promise that resolves when all preprocessors have completed their work.
|
||||
*/
|
||||
async preprocess(preprocessOptions = {}) {
|
||||
async preprocess(preprocessOptions: PreprocessOptions = {}) {
|
||||
for (const [name, options] of Object.entries(preprocessOptions)) {
|
||||
if (!Object.keys(preprocessors).includes(name)) {
|
||||
throw Error(`Invalid preprocessor "${name}"`);
|
||||
@@ -210,17 +227,16 @@ class Image {
|
||||
/**
|
||||
* Define one or several encoders to use on the image.
|
||||
* @param {object} encodeOptions - An object with encoders to use, and their settings.
|
||||
* @returns {Promise<void>} - A promise that resolves when the image has been encoded with all the specified encoders.
|
||||
* @returns {Promise<{ [key in keyof T]: EncodeResult }>} - A promise that resolves when the image has been encoded with all the specified encoders.
|
||||
*/
|
||||
async encode(
|
||||
async encode<T extends EncoderOptions>(
|
||||
encodeOptions: {
|
||||
optimizerButteraugliTarget?: number;
|
||||
maxOptimizerRounds?: number;
|
||||
} & {
|
||||
[key in EncoderKey]?: any; // any is okay for now
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
} & T,
|
||||
): Promise<{ [key in keyof T]: EncodeResult }> {
|
||||
const { bitmap } = await this.decoded;
|
||||
const setEncodedWithPromises = [];
|
||||
for (const [name, options] of Object.entries(encodeOptions)) {
|
||||
if (!Object.keys(encoders).includes(name)) {
|
||||
continue;
|
||||
@@ -231,7 +247,9 @@ class Image {
|
||||
typeof options === 'string'
|
||||
? options
|
||||
: Object.assign({}, encRef.defaultEncoderOptions, options);
|
||||
this.encodedWith[encName] = this.workerPool.dispatchJob({
|
||||
setEncodedWithPromises.push(
|
||||
this.workerPool
|
||||
.dispatchJob({
|
||||
operation: 'encode',
|
||||
bitmap,
|
||||
encName,
|
||||
@@ -240,9 +258,15 @@ class Image {
|
||||
encodeOptions.optimizerButteraugliTarget ?? 1.4,
|
||||
),
|
||||
maxOptimizerRounds: Number(encodeOptions.maxOptimizerRounds ?? 6),
|
||||
});
|
||||
})
|
||||
.then((encodeResult) => {
|
||||
this.encodedWith[encName] = encodeResult;
|
||||
}),
|
||||
);
|
||||
}
|
||||
await Promise.all(Object.values(this.encodedWith));
|
||||
|
||||
await Promise.all(setEncodedWithPromises);
|
||||
return this.encodedWith as { [key in keyof T]: EncodeResult };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,19 +278,19 @@ class ImagePool {
|
||||
|
||||
/**
|
||||
* Create a new pool.
|
||||
* @param {number} [threads] - Number of concurrent image processes to run in the pool. Defaults to the number of CPU cores in the system.
|
||||
* @param {number} [threads] - Number of concurrent image processes to run in the pool.
|
||||
*/
|
||||
constructor(threads: number) {
|
||||
this.workerPool = new WorkerPool(threads || cpus().length, __filename);
|
||||
this.workerPool = new WorkerPool(threads, __filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest an image into the image pool.
|
||||
* @param {FileLike} image - The image or path to the image that should be ingested and decoded.
|
||||
* @param {ArrayBuffer | ArrayLike<number>} file - The image that should be ingested and decoded.
|
||||
* @returns {Image} - A custom class reference to the decoded image.
|
||||
*/
|
||||
ingestImage(image: FileLike): Image {
|
||||
return new Image(this.workerPool, image);
|
||||
ingestImage(file: ArrayBuffer | ArrayLike<number>): Image {
|
||||
return new Image(this.workerPool, file);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
5
missing-types.d.ts
vendored
5
missing-types.d.ts
vendored
@@ -44,6 +44,11 @@ declare module 'data-url:*' {
|
||||
export default url;
|
||||
}
|
||||
|
||||
declare module 'data-url-text:*' {
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
declare module 'service-worker:*' {
|
||||
const url: string;
|
||||
export default url;
|
||||
|
||||
765
package-lock.json
generated
765
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -8,7 +8,8 @@
|
||||
"debug": "node --inspect-brk node_modules/.bin/rollup -c",
|
||||
"dev": "DEV_PORT=\"${DEV_PORT:=5000}\" run-p watch serve",
|
||||
"watch": "rollup -cw",
|
||||
"serve": "serve --listen=$DEV_PORT --config ../../../serve.json .tmp/build/static"
|
||||
"serve": "serve --listen=$DEV_PORT --config ../../../serve.json .tmp/build/static",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
@@ -16,15 +17,15 @@
|
||||
"@rollup/plugin-replace": "^2.3.4",
|
||||
"@surma/rollup-plugin-off-main-thread": "^2.2.2",
|
||||
"@types/dedent": "^0.7.0",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/node": "^14.14.7",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/node": "^16.11.1",
|
||||
"@web/rollup-plugin-import-meta-assets": "^1.0.6",
|
||||
"comlink": "^4.3.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"dedent": "^0.7.0",
|
||||
"del": "^5.1.0",
|
||||
"file-drop-element": "^1.0.1",
|
||||
"husky": "^4.3.0",
|
||||
"husky": "^7.0.2",
|
||||
"idb-keyval": "^3.2.0",
|
||||
"image-size": "^0.9.3",
|
||||
"linkstate": "^2.0.0",
|
||||
@@ -40,24 +41,20 @@
|
||||
"postcss-url": "^8.0.0",
|
||||
"preact": "^10.5.5",
|
||||
"preact-render-to-string": "^5.1.11",
|
||||
"prettier": "^2.1.2",
|
||||
"prettier": "^2.4.1",
|
||||
"rollup": "^2.38.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"serve": "^11.3.2",
|
||||
"typescript": "^4.1.3",
|
||||
"typescript": "^4.4.4",
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,css,json,md,ts,tsx}": "prettier --write",
|
||||
"*.{c,h,cpp,hpp}": "clang-format -i",
|
||||
"*.rs": "rustfmt"
|
||||
},
|
||||
"dependencies": {
|
||||
"strict-csp": "^1.0.4",
|
||||
"wasm-feature-detect": "^1.2.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,10 @@ main();
|
||||
ga('set', 'transport', 'beacon');
|
||||
ga('set', 'dimension1', displayMode);
|
||||
ga('send', 'pageview', '/index.html', { title: 'Squoosh' });
|
||||
// Load the GA script
|
||||
// Load the GA script without keeping the browser spinner going.
|
||||
addEventListener('load', () => {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://www.google-analytics.com/analytics.js';
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,9 +40,8 @@ type PartialButNotUndefined<T> = {
|
||||
[P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
const supportedEncoderMapP: Promise<PartialButNotUndefined<
|
||||
typeof encoderMap
|
||||
>> = (async () => {
|
||||
const supportedEncoderMapP: Promise<PartialButNotUndefined<typeof encoderMap>> =
|
||||
(async () => {
|
||||
const supportedEncoderMap: PartialButNotUndefined<typeof encoderMap> = {
|
||||
...encoderMap,
|
||||
};
|
||||
@@ -57,7 +56,7 @@ const supportedEncoderMapP: Promise<PartialButNotUndefined<
|
||||
);
|
||||
|
||||
return supportedEncoderMap;
|
||||
})();
|
||||
})();
|
||||
|
||||
export default class Options extends Component<Props, State> {
|
||||
state: State = {
|
||||
|
||||
@@ -116,7 +116,7 @@ async function decodeImage(
|
||||
// Otherwise fall through and try built-in decoding for a laugh.
|
||||
return await builtinDecode(signal, blob, mimeType);
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw err;
|
||||
if (err instanceof Error && err.name === 'AbortError') throw err;
|
||||
console.log(err);
|
||||
throw Error("Couldn't decode image");
|
||||
}
|
||||
@@ -481,7 +481,7 @@ export default class Compress extends Component<Props, State> {
|
||||
open('https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli');
|
||||
}
|
||||
} catch (e) {
|
||||
this.props.showSnack(e);
|
||||
this.props.showSnack(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -640,7 +640,7 @@ export default class Compress extends Component<Props, State> {
|
||||
return { sides };
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') return;
|
||||
if (err instanceof Error && err.name === 'AbortError') return;
|
||||
this.props.showSnack(`Source decoding error: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
@@ -698,7 +698,7 @@ export default class Compress extends Component<Props, State> {
|
||||
return newState;
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') return;
|
||||
if (err instanceof Error && err.name === 'AbortError') return;
|
||||
this.setState({ loading: false });
|
||||
this.props.showSnack(`Preprocessing error: ${err}`);
|
||||
throw err;
|
||||
@@ -822,7 +822,7 @@ export default class Compress extends Component<Props, State> {
|
||||
|
||||
this.activeSideJobs[sideIndex] = undefined;
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') return;
|
||||
if (err instanceof Error && err.name === 'AbortError') return;
|
||||
this.setState((currentState) => {
|
||||
const sides = cleanMerge(currentState.sides, sideIndex, {
|
||||
loading: false,
|
||||
|
||||
@@ -14,9 +14,11 @@ import { EncodeOptions } from '../shared/meta';
|
||||
import { threads } from 'wasm-feature-detect';
|
||||
|
||||
async function initMT() {
|
||||
const { default: init, initThreadPool, optimise } = await import(
|
||||
'codecs/oxipng/pkg-parallel/squoosh_oxipng'
|
||||
);
|
||||
const {
|
||||
default: init,
|
||||
initThreadPool,
|
||||
optimise,
|
||||
} = await import('codecs/oxipng/pkg-parallel/squoosh_oxipng');
|
||||
await init();
|
||||
await initThreadPool(navigator.hardwareConcurrency);
|
||||
return optimise;
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as styles from './styles.css';
|
||||
import 'add-css:./styles.css';
|
||||
|
||||
// So it doesn't cause an error when running in node
|
||||
const HTMLEl = ((__PRERENDER__
|
||||
const HTMLEl = (__PRERENDER__
|
||||
? Object
|
||||
: HTMLElement) as unknown) as typeof HTMLElement;
|
||||
: HTMLElement) as unknown as typeof HTMLElement;
|
||||
|
||||
/**
|
||||
* A simple spinner. This custom element has no JS API. Just put it in the document, and it'll
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as style from './styles.css';
|
||||
import 'add-css:./styles.css';
|
||||
|
||||
// So it doesn't cause an error when running in node
|
||||
const HTMLEl = ((__PRERENDER__
|
||||
const HTMLEl = (__PRERENDER__
|
||||
? Object
|
||||
: HTMLElement) as unknown) as typeof HTMLElement;
|
||||
: HTMLElement) as unknown as typeof HTMLElement;
|
||||
|
||||
export interface SnackOptions {
|
||||
timeout?: number;
|
||||
|
||||
21
src/shared/missing-types.d.ts
vendored
21
src/shared/missing-types.d.ts
vendored
@@ -13,24 +13,3 @@
|
||||
/// <reference path="../../missing-types.d.ts" />
|
||||
|
||||
declare const __PRERENDER__: boolean;
|
||||
|
||||
type ResizeObserverCallback = (
|
||||
entries: ResizeObserverEntry[],
|
||||
observer: ResizeObserver,
|
||||
) => void;
|
||||
|
||||
interface ResizeObserverEntry {
|
||||
readonly target: Element;
|
||||
readonly contentRect: DOMRectReadOnly;
|
||||
}
|
||||
|
||||
interface ResizeObserver {
|
||||
observe(target: Element): void;
|
||||
unobserve(target: Element): void;
|
||||
disconnect(): void;
|
||||
}
|
||||
|
||||
declare var ResizeObserver: {
|
||||
prototype: ResizeObserver;
|
||||
new (callback: ResizeObserverCallback): ResizeObserver;
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -14,7 +14,7 @@ import smallSectionAsset from 'url:./imgs/info-content/small.svg';
|
||||
import simpleSectionAsset from 'url:./imgs/info-content/simple.svg';
|
||||
import secureSectionAsset from 'url:./imgs/info-content/secure.svg';
|
||||
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
|
||||
import logoWithText from 'url:./imgs/logo-with-text.svg';
|
||||
import logoWithText from 'data-url-text:./imgs/logo-with-text.svg';
|
||||
import * as style from './style.css';
|
||||
import type SnackBarElement from 'shared/custom-els/snack-bar';
|
||||
import 'shared/custom-els/snack-bar';
|
||||
|
||||
@@ -30,11 +30,6 @@ interface WindowEventMap {
|
||||
beforeinstallprompt: BeforeInstallPromptEvent;
|
||||
}
|
||||
|
||||
interface ClipboardItem {
|
||||
types: string[];
|
||||
getType(type: string): Promise<Blob>;
|
||||
}
|
||||
|
||||
interface Clipboard {
|
||||
read(): Promise<ClipboardItem[]>;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ const Index: FunctionalComponent<Props> = () => (
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link rel="shortcut icon" href={favicon} />
|
||||
<link rel="apple-touch-icon" href={ogImage} />
|
||||
<meta name="theme-color" content="#ff3385" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="canonical" href={siteOrigin} />
|
||||
|
||||
@@ -16,8 +16,35 @@ import { join as joinPath } from 'path';
|
||||
import render from 'preact-render-to-string';
|
||||
import { VNode } from 'preact';
|
||||
|
||||
import { StrictCsp } from 'strict-csp';
|
||||
|
||||
export function renderPage(vnode: VNode) {
|
||||
return '<!DOCTYPE html>' + render(vnode);
|
||||
const htmlString = '<!DOCTYPE html>' + render(vnode);
|
||||
const htmlStringWithCsp = addCspAsMetaTag(htmlString);
|
||||
return htmlStringWithCsp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to the html string a strict Content-Security-Policy (CSP), as a meta tag.
|
||||
* Details at https://web.dev/strict-csp/
|
||||
*/
|
||||
function addCspAsMetaTag(htmlString: string) {
|
||||
const s = new StrictCsp(htmlString);
|
||||
// Refactor sourced scripts so that we can set a strict CSP
|
||||
s.refactorSourcedScriptsForHashBasedCsp();
|
||||
// Hash inline scripts from this html file, if there are any
|
||||
const scriptHashes = s.hashAllInlineScripts();
|
||||
// Generate a strict CSP as a string
|
||||
// enableUnsafeEval is set to true, to accomodate for uses of eval by emscripten. This makes the CSP a bit less secure
|
||||
const strictCsp = StrictCsp.getStrictCsp(scriptHashes, {
|
||||
enableBrowserFallbacks: true,
|
||||
enableTrustedTypes: false,
|
||||
enableUnsafeEval: true,
|
||||
});
|
||||
// Set this CSP via a meta tag
|
||||
s.addMetaTag(strictCsp);
|
||||
const htmlStringWithCsp = s.serializeDom();
|
||||
return htmlStringWithCsp;
|
||||
}
|
||||
|
||||
interface OutputMap {
|
||||
|
||||
@@ -82,12 +82,8 @@ initialJs = subtractSets(
|
||||
export const initial = ['/', ...initialJs];
|
||||
|
||||
export const theRest = (async () => {
|
||||
const [
|
||||
supportsThreads,
|
||||
supportsSimd,
|
||||
supportsWebP,
|
||||
supportsAvif,
|
||||
] = await Promise.all([
|
||||
const [supportsThreads, supportsSimd, supportsWebP, supportsAvif] =
|
||||
await Promise.all([
|
||||
threads(),
|
||||
simd(),
|
||||
...[webpDataUrl, avifDataUrl].map(async (dataUrl) => {
|
||||
|
||||
Reference in New Issue
Block a user