diff --git a/src/codecs/encoders.ts b/src/codecs/encoders.ts index ef053535..97530615 100644 --- a/src/codecs/encoders.ts +++ b/src/codecs/encoders.ts @@ -2,6 +2,7 @@ import * as identity from './identity/encoder-meta'; import * as optiPNG from './optipng/encoder-meta'; import * as mozJPEG from './mozjpeg/encoder-meta'; import * as webP from './webp/encoder-meta'; +import * as jxl from './jxl/encoder-meta'; import * as browserPNG from './browser-png/encoder-meta'; import * as browserJPEG from './browser-jpeg/encoder-meta'; import * as browserWebP from './browser-webp/encoder-meta'; @@ -20,6 +21,7 @@ export type EncoderState = optiPNG.EncoderState | mozJPEG.EncoderState | webP.EncoderState | + jxl.EncoderState | browserPNG.EncoderState | browserJPEG.EncoderState | browserWebP.EncoderState | @@ -34,6 +36,7 @@ export type EncoderOptions = optiPNG.EncodeOptions | mozJPEG.EncodeOptions | webP.EncodeOptions | + jxl.EncodeOptions | browserPNG.EncodeOptions | browserJPEG.EncodeOptions | browserWebP.EncodeOptions | @@ -50,6 +53,7 @@ export const encoderMap = { [optiPNG.type]: optiPNG, [mozJPEG.type]: mozJPEG, [webP.type]: webP, + [jxl.type]: jxl, [browserPNG.type]: browserPNG, [browserJPEG.type]: browserJPEG, [browserWebP.type]: browserWebP, diff --git a/src/codecs/jxl/encoder-meta.ts b/src/codecs/jxl/encoder-meta.ts index 330a2825..10eb5e32 100644 --- a/src/codecs/jxl/encoder-meta.ts +++ b/src/codecs/jxl/encoder-meta.ts @@ -1,72 +1,12 @@ -export enum WebPImageHint { - WEBP_HINT_DEFAULT, // default preset. - WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot - WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting - WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc). +export interface EncodeOptions { } -export interface EncodeOptions { - quality: number; - target_size: number; - target_PSNR: number; - method: number; - sns_strength: number; - filter_strength: number; - filter_sharpness: number; - filter_type: number; - partitions: number; - segments: number; - pass: number; - show_compressed: number; - preprocessing: number; - autofilter: number; - partition_limit: number; - alpha_compression: number; - alpha_filtering: number; - alpha_quality: number; - lossless: number; - exact: number; - image_hint: number; - emulate_jpeg_size: number; - thread_level: number; - low_memory: number; - near_lossless: number; - use_delta_palette: number; - use_sharp_yuv: number; -} export interface EncoderState { type: typeof type; options: EncodeOptions; } -export const type = 'webp'; -export const label = 'WebP'; -export const mimeType = 'image/webp'; -export const extension = 'webp'; +export const type = 'jxl'; +export const label = 'JPEG XL'; +export const mimeType = 'image/jpegxl'; +export const extension = 'jxl'; // These come from struct WebPConfig in encode.h. export const defaultOptions: EncodeOptions = { - quality: 75, - target_size: 0, - target_PSNR: 0, - method: 4, - sns_strength: 50, - filter_strength: 60, - filter_sharpness: 0, - filter_type: 1, - partitions: 0, - segments: 4, - pass: 1, - show_compressed: 0, - preprocessing: 0, - autofilter: 0, - partition_limit: 0, - alpha_compression: 1, - alpha_filtering: 1, - alpha_quality: 100, - lossless: 0, - exact: 0, - image_hint: 0, - emulate_jpeg_size: 0, - thread_level: 0, - low_memory: 0, - near_lossless: 100, - use_delta_palette: 0, - use_sharp_yuv: 0, }; diff --git a/src/codecs/jxl/encoder.ts b/src/codecs/jxl/encoder.ts index 5fafb577..3dea97ce 100644 --- a/src/codecs/jxl/encoder.ts +++ b/src/codecs/jxl/encoder.ts @@ -1,15 +1,15 @@ -import webp_enc, { WebPModule } from '../../../codecs/webp_enc/webp_enc'; -import wasmUrl from '../../../codecs/webp_enc/webp_enc.wasm'; +import jxl_enc, { JXLModule } from '../../../codecs/jxl_enc/jxl_enc'; +import wasmUrl from '../../../codecs/jxl_enc/jxl_enc.wasm'; import { EncodeOptions } from './encoder-meta'; import { initEmscriptenModule } from '../util'; -let emscriptenModule: Promise; +let emscriptenModule: Promise; export async function encode(data: ImageData, options: EncodeOptions): Promise { - if (!emscriptenModule) emscriptenModule = initEmscriptenModule(webp_enc, wasmUrl); + if (!emscriptenModule) emscriptenModule = initEmscriptenModule(jxl_enc, wasmUrl); const module = await emscriptenModule; - const resultView = module.encode(data.data, data.width, data.height, options); + const resultView = module.encode(data.data, data.width, data.height); const result = new Uint8Array(resultView); module.free_result(); diff --git a/src/codecs/jxl/options.tsx b/src/codecs/jxl/options.tsx index 6d363ad3..0fa499ff 100644 --- a/src/codecs/jxl/options.tsx +++ b/src/codecs/jxl/options.tsx @@ -1,13 +1,13 @@ import { h, Component } from 'preact'; import { bind } from '../../lib/initial-util'; -import { inputFieldCheckedAsNumber, inputFieldValueAsNumber, preventDefault } from '../../lib/util'; -import { EncodeOptions, WebPImageHint } from './encoder-meta'; +import { /*inputFieldCheckedAsNumber, inputFieldValueAsNumber,*/ preventDefault } from '../../lib/util'; +import { EncodeOptions } from './encoder-meta'; import * as style from '../../components/Options/style.scss'; -import Checkbox from '../../components/checkbox'; -import Expander from '../../components/expander'; -import Select from '../../components/select'; -import Range from '../../components/range'; -import linkState from 'linkstate'; +// import Checkbox from '../../components/checkbox'; +// import Expander from '../../components/expander'; +// import Select from '../../components/select'; +// import Range from '../../components/range'; +// import linkState from 'linkstate'; interface Props { options: EncodeOptions; @@ -15,331 +15,28 @@ interface Props { } interface State { - showAdvanced: boolean; } -// From kLosslessPresets in config_enc.c -// The format is [method, quality]. -const losslessPresets:[number, number][] = [ - [0, 0], [1, 20], [2, 25], [3, 30], [3, 50], - [4, 50], [4, 75], [4, 90], [5, 90], [6, 100], -]; -const losslessPresetDefault = 6; - -function determineLosslessQuality(quality: number, method: number): number { - const index = losslessPresets.findIndex( - ([presetMethod, presetQuality]) => presetMethod === method && presetQuality === quality, - ); - if (index !== -1) return index; - // Quality doesn't match one of the presets. - // This can happen when toggling 'lossless'. - return losslessPresetDefault; -} - -export default class WebPEncoderOptions extends Component { +export default class JXLEncoderOptions extends Component { state: State = { - showAdvanced: false, }; @bind onChange(event: Event) { - const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement; - const lossless = inputFieldCheckedAsNumber(form.lossless); - const { options } = this.props; - const losslessPresetValue = inputFieldValueAsNumber( - form.lossless_preset, determineLosslessQuality(options.quality, options.method), - ); + // const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement; + // const { options } = this.props; - const newOptions: EncodeOptions = { - // Copy over options the form doesn't care about, eg emulate_jpeg_size - ...options, - // And now stuff from the form: - lossless, - // Special-cased inputs: - // In lossless mode, the quality is derived from the preset. - quality: lossless ? - losslessPresets[losslessPresetValue][1] : - inputFieldValueAsNumber(form.quality, options.quality), - // In lossless mode, the method is derived from the preset. - method: lossless ? - losslessPresets[losslessPresetValue][0] : - inputFieldValueAsNumber(form.method_input, options.method), - image_hint: inputFieldCheckedAsNumber(form.image_hint, options.image_hint) ? - WebPImageHint.WEBP_HINT_GRAPH : - WebPImageHint.WEBP_HINT_DEFAULT, - // .checked - exact: inputFieldCheckedAsNumber(form.exact, options.exact), - alpha_compression: inputFieldCheckedAsNumber( - form.alpha_compression, options.alpha_compression, - ), - autofilter: inputFieldCheckedAsNumber(form.autofilter, options.autofilter), - filter_type: inputFieldCheckedAsNumber(form.filter_type, options.filter_type), - use_sharp_yuv: inputFieldCheckedAsNumber(form.use_sharp_yuv, options.use_sharp_yuv), - // .value - near_lossless: 100 - inputFieldValueAsNumber(form.near_lossless, 100 - options.near_lossless), - alpha_quality: inputFieldValueAsNumber(form.alpha_quality, options.alpha_quality), - alpha_filtering: inputFieldValueAsNumber(form.alpha_filtering, options.alpha_filtering), - sns_strength: inputFieldValueAsNumber(form.sns_strength, options.sns_strength), - filter_strength: inputFieldValueAsNumber(form.filter_strength, options.filter_strength), - filter_sharpness: - 7 - inputFieldValueAsNumber(form.filter_sharpness, 7 - options.filter_sharpness), - pass: inputFieldValueAsNumber(form.pass, options.pass), - preprocessing: inputFieldValueAsNumber(form.preprocessing, options.preprocessing), - segments: inputFieldValueAsNumber(form.segments, options.segments), - partitions: inputFieldValueAsNumber(form.partitions, options.partitions), - }; - this.props.onChange(newOptions); - } - - private _losslessSpecificOptions(options: EncodeOptions) { - return ( -
-
- - Effort: - -
-
- - Slight loss: - -
- -
- ); - } - - private _lossySpecificOptions(options: EncodeOptions) { - const { showAdvanced } = this.state; - - return ( -
-
- - Effort: - -
-
- - Quality: - -
- - - {showAdvanced ? -
- -
- - Alpha quality: - -
-
- - Alpha filter quality: - -
- - - {options.autofilter ? null : -
- - Filter strength: - -
- } -
- -
- - Filter sharpness: - -
- -
- - Passes: - -
-
- - Spacial noise shaping: - -
- -
- - Segments: - -
-
- - Partitions: - -
-
- : null - } -
-
- ); + // const newOptions: EncodeOptions = { + // // Copy over options the form doesn't care about, eg emulate_jpeg_size + // ...options, + // }; + // this.props.onChange(newOptions); } render({ options }: Props) { - // I'm rendering both lossy and lossless forms, as it becomes much easier when - // gathering the data. return (
- - {options.lossless - ? this._losslessSpecificOptions(options) - : this._lossySpecificOptions(options) - } - + Lol
); } diff --git a/src/codecs/processor-worker/index.ts b/src/codecs/processor-worker/index.ts index 417eb987..1e35eb75 100644 --- a/src/codecs/processor-worker/index.ts +++ b/src/codecs/processor-worker/index.ts @@ -77,6 +77,15 @@ async function webpDecode(data: ArrayBuffer): Promise { return decode(data); } +async function jxlEncode( + data: ImageData, options: import('../jxl/encoder-meta').EncodeOptions, +): Promise { + const { encode } = await import( + /* webpackChunkName: "process-jxl-enc" */ + '../jxl/encoder'); + return encode(data, options); +} + async function jxlDecode(data: ArrayBuffer): Promise { const { decode } = await import( /* webpackChunkName: "process-jxl-dec" */ @@ -92,6 +101,7 @@ const exports = { optiPngEncode, webpEncode, webpDecode, + jxlEncode, jxlDecode, }; export type ProcessorWorkerApi = typeof exports; diff --git a/src/codecs/processor.ts b/src/codecs/processor.ts index 15d7330b..1730919e 100644 --- a/src/codecs/processor.ts +++ b/src/codecs/processor.ts @@ -4,6 +4,7 @@ import { canvasEncode, blobToArrayBuffer } from '../lib/util'; import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta'; import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta'; import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta'; +import { EncodeOptions as JXLEncoderOptions } from './jxl/encoder-meta'; import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta'; import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta'; import { BrowserResizeOptions, VectorResizeOptions } from './resize/processor-meta'; @@ -163,6 +164,11 @@ export default class Processor { return this._workerApi!.webpDecode(data); } + @Processor._processingJob({ needsWorker: true }) + jxlEncode(data: ImageData, opts: JXLEncoderOptions): Promise { + return this._workerApi!.jxlEncode(data, opts); + } + @Processor._processingJob({ needsWorker: true }) async jxlDecode(blob: Blob): Promise { const data = await blobToArrayBuffer(blob); diff --git a/src/components/Options/index.tsx b/src/components/Options/index.tsx index 2fdc1c6a..a3afeb5e 100644 --- a/src/components/Options/index.tsx +++ b/src/components/Options/index.tsx @@ -7,6 +7,7 @@ import OptiPNGEncoderOptions from '../../codecs/optipng/options'; import MozJpegEncoderOptions from '../../codecs/mozjpeg/options'; import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options'; import WebPEncoderOptions from '../../codecs/webp/options'; +import JXLEncoderOptions from '../../codecs/jxl/options'; import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options'; import QuantizerOptionsComponent from '../../codecs/imagequant/options'; @@ -16,6 +17,7 @@ import * as identity from '../../codecs/identity/encoder-meta'; import * as optiPNG from '../../codecs/optipng/encoder-meta'; import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta'; import * as webP from '../../codecs/webp/encoder-meta'; +import * as jxl from '../../codecs/jxl/encoder-meta'; import * as browserPNG from '../../codecs/browser-png/encoder-meta'; import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta'; import * as browserWebP from '../../codecs/browser-webp/encoder-meta'; @@ -47,6 +49,7 @@ const encoderOptionsComponentMap: { [optiPNG.type]: OptiPNGEncoderOptions, [mozJPEG.type]: MozJpegEncoderOptions, [webP.type]: WebPEncoderOptions, + [jxl.type]: JXLEncoderOptions, [browserPNG.type]: undefined, [browserJPEG.type]: BrowserJPEGEncoderOptions, [browserWebP.type]: BrowserWebPEncoderOptions, diff --git a/src/components/compress/index.tsx b/src/components/compress/index.tsx index 4c19a7bd..4b17d511 100644 --- a/src/components/compress/index.tsx +++ b/src/components/compress/index.tsx @@ -10,6 +10,7 @@ import * as identity from '../../codecs/identity/encoder-meta'; import * as optiPNG from '../../codecs/optipng/encoder-meta'; import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta'; import * as webP from '../../codecs/webp/encoder-meta'; +import * as jxl from '../../codecs/jxl/encoder-meta'; import * as browserPNG from '../../codecs/browser-png/encoder-meta'; import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta'; import * as browserWebP from '../../codecs/browser-webp/encoder-meta'; @@ -141,6 +142,7 @@ async function compressImage( case optiPNG.type: return processor.optiPngEncode(image, encodeData.options); case mozJPEG.type: return processor.mozjpegEncode(image, encodeData.options); case webP.type: return processor.webpEncode(image, encodeData.options); + case jxl.type: return processor.jxlEncode(image, encodeData.options); case browserPNG.type: return processor.browserPngEncode(image); case browserJPEG.type: return processor.browserJpegEncode(image, encodeData.options); case browserWebP.type: return processor.browserWebpEncode(image, encodeData.options);