mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-18 19:49:04 +00:00
Options UI (#39)
* Initial work to add Options config
* Use a single encoder instance and retry up to 10 times on failure.
* Switch both sides to allow encoding from the source image, add options configuration for each.
* Styling for options (and a few tweaks for the app)
* Dep updates.
* Remove commented out code.
* Fix Encoder typing
* Fix lint issues
* Apparently I didnt have tslint autofix enabled on the chromebook
* Attempt to fix layout/panning issues
* Fix missing custom element import!
* Fix variable naming, remove dynamic encoder names, remove retry, allow encoders to return ImageData.
* Refactor state management to use an Array of objects and immutable updates instead of relying on explicit update notifications.
* Add Identity encoder, which is a passthrough encoder that handles the "original" view.
* Drop comlink-loader into the project and add ".worker" to the jpeg encoder filename so it runs in a worker (🦄)
* lint fixes.
* cleanup
* smaller PR feedback fixes
* rename "jpeg" codec to "MozJpeg"
* Formatting fixes for Options
* Colocate codecs and their options UIs in src/codecs, and standardize the namings
* Handle canvas errors
* Throw if quality is undefined, add default quality
* add note about temp styles
* add note about temp styles [2]
* Renaming updateOption
* Clarify option input bindings
* Move updateCanvas() to util and rename to drawBitmapToCanvas
* use generics to pass through encoder options
* Remove unused dependencies
* fix options type
* const
* Use `Array.prototype.some()` for image loading check
* Display encoding errors in the UI.
* I fought typescript and I think I won
* This doesn't need to be optional
* Quality isn't optional
* Simplifying comlink casting
* Splitting counters into loading and displaying
* Still loading if the loading counter isn't equal.
This commit is contained in:
committed by
Jake Archibald
parent
65847c0ed7
commit
3035a68b90
15
src/codecs/encoders.ts
Normal file
15
src/codecs/encoders.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as mozJPEG from './mozjpeg/encoder';
|
||||
import { EncoderState as MozJPEGEncodeData, EncodeOptions as MozJPEGEncodeOptions } from './mozjpeg/encoder';
|
||||
import * as identity from './identity/encoder';
|
||||
import { EncoderState as IdentityEncodeData, EncodeOptions as IdentityEncodeOptions } from './identity/encoder';
|
||||
|
||||
export type EncoderState = IdentityEncodeData | MozJPEGEncodeData;
|
||||
export type EncoderOptions = IdentityEncodeOptions | MozJPEGEncodeOptions;
|
||||
export type EncoderType = keyof typeof encoderMap;
|
||||
|
||||
export const encoderMap = {
|
||||
[identity.type]: identity,
|
||||
[mozJPEG.type]: mozJPEG
|
||||
};
|
||||
|
||||
export const encoders = Array.from(Object.values(encoderMap));
|
||||
6
src/codecs/identity/encoder.ts
Normal file
6
src/codecs/identity/encoder.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'identity';
|
||||
export const label = 'Original image';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
79
src/codecs/mozjpeg/EncoderWorker.ts
Normal file
79
src/codecs/mozjpeg/EncoderWorker.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import mozjpeg_enc from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
|
||||
// Using require() so TypeScript doesn’t complain about this not being a module.
|
||||
import { EncodeOptions } from './encoder';
|
||||
const wasmBinaryUrl = require('../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm');
|
||||
|
||||
// API exposed by wasm module. Details in the codec’s README.
|
||||
interface ModuleAPI {
|
||||
version(): number;
|
||||
create_buffer(width: number, height: number): number;
|
||||
destroy_buffer(pointer: number): void;
|
||||
encode(buffer: number, width: number, height: number, quality: number): void;
|
||||
free_result(): void;
|
||||
get_result_pointer(): number;
|
||||
get_result_size(): number;
|
||||
}
|
||||
|
||||
export default class MozJpegEncoder {
|
||||
private emscriptenModule: Promise<EmscriptenWasm.Module>;
|
||||
private api: Promise<ModuleAPI>;
|
||||
|
||||
constructor() {
|
||||
this.emscriptenModule = new Promise((resolve) => {
|
||||
const m = mozjpeg_enc({
|
||||
// Just to be safe, don’t automatically invoke any wasm functions
|
||||
noInitialRun: false,
|
||||
locateFile(url: string): string {
|
||||
// Redirect the request for the wasm binary to whatever webpack gave us.
|
||||
if (url.endsWith('.wasm')) {
|
||||
return wasmBinaryUrl;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
onRuntimeInitialized() {
|
||||
// An Emscripten is a then-able that, for some reason, `then()`s itself,
|
||||
// causing an infite loop when you wrap it in a real promise. Deleten the `then`
|
||||
// prop solves this for now.
|
||||
// See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129
|
||||
// TODO(surma@): File a bug with Emscripten on this.
|
||||
delete (m as any).then;
|
||||
resolve(m);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
this.api = (async () => {
|
||||
// Not sure why, but TypeScript complains that I am using
|
||||
// `emscriptenModule` before it’s getting assigned, which is clearly not
|
||||
// true :shrug: Using `any`
|
||||
const m = await (this as any).emscriptenModule;
|
||||
return {
|
||||
version: m.cwrap('version', 'number', []),
|
||||
create_buffer: m.cwrap('create_buffer', 'number', ['number', 'number']),
|
||||
destroy_buffer: m.cwrap('destroy_buffer', '', ['number']),
|
||||
encode: m.cwrap('encode', '', ['number', 'number', 'number', 'number']),
|
||||
free_result: m.cwrap('free_result', '', []),
|
||||
get_result_pointer: m.cwrap('get_result_pointer', 'number', []),
|
||||
get_result_size: m.cwrap('get_result_size', 'number', [])
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
||||
async encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
|
||||
const m = await this.emscriptenModule;
|
||||
const api = await this.api;
|
||||
|
||||
const p = api.create_buffer(data.width, data.height);
|
||||
m.HEAP8.set(data.data, p);
|
||||
api.encode(p, data.width, data.height, options.quality);
|
||||
const resultPointer = api.get_result_pointer();
|
||||
const resultSize = api.get_result_size();
|
||||
const resultView = new Uint8Array(m.HEAP8.buffer, resultPointer, resultSize);
|
||||
const result = new Uint8Array(resultView);
|
||||
api.free_result();
|
||||
api.destroy_buffer(p);
|
||||
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return result.buffer as ArrayBuffer;
|
||||
}
|
||||
}
|
||||
16
src/codecs/mozjpeg/encoder.ts
Normal file
16
src/codecs/mozjpeg/encoder.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import EncoderWorker from './EncoderWorker';
|
||||
|
||||
export interface EncodeOptions { quality: number; }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'mozjpeg';
|
||||
export const label = 'MozJPEG';
|
||||
export const mimeType = 'image/jpeg';
|
||||
export const extension = 'jpg';
|
||||
export const defaultOptions: EncodeOptions = { quality: 7 };
|
||||
|
||||
export async function encode(data: ImageData, options: EncodeOptions) {
|
||||
// We need to await this because it's been comlinked.
|
||||
const encoder = await new EncoderWorker();
|
||||
return encoder.encode(data, options);
|
||||
}
|
||||
35
src/codecs/mozjpeg/options.tsx
Normal file
35
src/codecs/mozjpeg/options.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { EncodeOptions } from './encoder';
|
||||
import { bind } from '../../lib/util';
|
||||
|
||||
type Props = {
|
||||
options: EncodeOptions,
|
||||
onChange(newOptions: EncodeOptions): void
|
||||
};
|
||||
|
||||
export default class MozJpegCodecOptions extends Component<Props, {}> {
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const el = event.currentTarget as HTMLInputElement;
|
||||
this.props.onChange({ quality: Number(el.value) });
|
||||
}
|
||||
|
||||
render({ options }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
Quality:
|
||||
<input
|
||||
name="quality"
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
value={'' + options.quality}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user