Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* **sqzer:** `Sqzer::lossy_options` names the options set on the builder under which an encoder gives samples up, for a caller that wants to refuse a lossless target before encoding
* **cli:** each entry of `trials` in a `--json` record carries `size`, the bytes that trial encoded to. `-v` prints it after the score, and the progress line shows it per trial
* **wasm:** each `Trial` in `trials`, and what `onTrial` receives, carries `size`, the bytes that trial encoded to, so a page can plot quality against score and size as the search runs ([#55](https://github.com/sqzer-dev/sqzer/issues/55))
* **wasm:** `options()` lists the keys `optimize` and `encode` take, each with its default as the package spells it and its help, as `codecs()` lists the backend options: `[{ key: "target", default: "70", help: "..." }, ...]`. A key whose default depends on the image or the format says `auto`; one that does nothing when left out has none. The defaults are read from the library's builder, so a page can show every control at the package's default and drop its own copy ([#57](https://github.com/sqzer-dev/sqzer/issues/57))

## 0.3.0

Expand Down
10 changes: 10 additions & 0 deletions crates/sqzer-wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ onTrial called per trial of the search with { n, max, quality, score, size

> **Note**: `maxPixels` defaults to 24 million, not the 268 million of the command line. The target search needs about 160 bytes per pixel, and a WebAssembly module addresses 4 GiB.

`options()` lists these keys with their defaults, as `codecs()` lists the backend options, so a page can show every control at the package's default without a copy of this table.

```js
options();
// [{ key: "format", default: "auto", help: "output format: jpeg, png, webp, avif or jxl. ..." },
// { key: "target", default: "70", help: "SSIMULACRA2 score to reach by searching encoder quality, up to 100. ..." },
// { key: "width", help: "target width in pixels. ..." }, // no default: left out, nothing is resized
// ...]
```

## Results and errors

`optimize` and `encode` return the record `sqzer --json` prints, in camel case, with the encoded file added. Keys that do not apply are left out.
Expand Down
20 changes: 18 additions & 2 deletions crates/sqzer-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! The API is the library's builder spelled the way JavaScript spells it.
//!
//! ```js
//! import init, { optimize, decode, decodeAny, fromPixels, codecs } from "sqzer";
//! import init, { optimize, decode, decodeAny, fromPixels, codecs, options } from "sqzer";
//! await init();
//!
//! // everything in one call: probe, decode, prepare, resize, encode
Expand Down Expand Up @@ -41,7 +41,7 @@ use sqzer::{Progress, Ready, Sqzer};
use wasm_bindgen::prelude::*;

pub use error::SqzerError;
use options::{DecodeAnyOptions, DecodeOptions, MAX_PIXELS, Options};
use options::{DecodeAnyOptions, DecodeOptions, MAX_PIXELS, OptionInfo, Options};
use output::{Codec, Output, Source, TrialProgress, tidy};
use tsify::{Ts, Tsify};

Expand Down Expand Up @@ -272,6 +272,22 @@ pub fn codecs() -> Result<Vec<Ts<Codec>>, SqzerError> {
.collect()
}

/// The keys `optimize` and `SqzerImage.encode` take, each with its default
/// as the package spells it and what it does, as `codecs()` lists the
/// backend options. A key whose default depends on the image or the format
/// says `auto`; one that has no default has none. The defaults are the
/// library's own, read from the builder an encode without options uses.
///
/// # Errors
/// None in practice: the listing always serialises.
#[wasm_bindgen]
pub fn options() -> Result<Vec<Ts<OptionInfo>>, SqzerError> {
Options::listing()
.iter()
.map(|option| Ok(option.into_ts()?))
.collect()
}

/// A decoded image, from `decode`, `decodeAny` or `fromPixels`. Call
/// `free()` when done with it: its pixels live in the module's memory,
/// which the garbage collector does not see.
Expand Down
236 changes: 209 additions & 27 deletions crates/sqzer-wasm/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
use std::collections::BTreeMap;

use js_sys::{Function, Object};
use serde::Deserialize;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use sqzer::Sqzer;
use sqzer::core::params::Target;
use sqzer::core::resize::{Resize, Size};
Expand Down Expand Up @@ -287,32 +287,125 @@ impl From<Filter> for resize::Filter {
}
}

/// The keys of [`Options`], [`DecodeOptions`] and [`DecodeAnyOptions`] as
/// JavaScript spells them. `serde-wasm-bindgen` reads the fields it knows
/// and never sees the rest, so a misspelt key is refused against these.
/// One key of [`Options`], as `options()` lists it: the shape of
/// [`crate::output::CodecOption`].
#[derive(Debug, Clone, Serialize, Tsify)]
pub struct OptionInfo {
/// The key, as `optimize` and `encode` take it.
pub key: &'static str,
/// The value used when the key is not set, as the package spells it.
/// `auto` where it depends on the image or the format. Left out for a
/// key that has none: leaving `width` out resizes nothing, and
/// `quality` left out means the `target` is searched for.
#[serde(skip_serializing_if = "Option::is_none")]
#[tsify(optional)]
pub default: Option<String>,
/// What it does and which values it takes.
pub help: &'static str,
}

/// A key of [`Options`] and what `options()` says about it.
struct Key {
name: &'static str,
help: &'static str,
}

/// The keys of [`Options`] as JavaScript spells them, with their help.
/// `serde-wasm-bindgen` reads the fields it knows and never sees the rest,
/// so a misspelt key is refused against these, and `options()` lists them.
/// Add a field's key here with the field.
const OPTIONS: &[&str] = &[
"format",
"target",
"quality",
"lossless",
"preset",
"effort",
"subsampling",
"keepIcc",
"keepMetadata",
"fast",
"width",
"height",
"fit",
"position",
"background",
"scale",
"enlarge",
"filter",
"maxPixels",
"codecOpts",
"onTrial",
const OPTIONS: &[Key] = &[
Key {
name: "format",
help: "output format: jpeg, png, webp, avif or jxl. Left out, it is chosen per image: \
avif for a photograph, lossless webp for a graphic",
},
Key {
name: "target",
help: "SSIMULACRA2 score to reach by searching encoder quality, up to 100. 100 is \
identical, 50 shows artefacts on close inspection",
},
Key {
name: "quality",
help: "encode at this quality, 0 to 100, with no search. Excludes target and lossless",
},
Key {
name: "lossless",
help: "lossless output. Excludes target and quality",
},
Key {
name: "preset",
help: "named settings: web, thumbnail, archive or lossless. The other keys override it",
},
Key {
name: "effort",
help: "0 to 10. Higher is slower and smaller",
},
Key {
name: "subsampling",
help: "chroma subsampling, for codecs that have it: auto, 444, 422 or 420",
},
Key {
name: "keepIcc",
help: "keep the ICC profile instead of converting to sRGB",
},
Key {
name: "keepMetadata",
help: "keep EXIF and XMP instead of stripping them",
},
Key {
name: "fast",
help: "one encode at the calibrated seed quality for the target, no search",
},
Key {
name: "width",
help: "target width in pixels. With the default fit, scales down to fit inside width x \
height; one side may be given alone",
},
Key {
name: "height",
help: "target height in pixels",
},
Key {
name: "fit",
help: "how the image meets the width x height box, as CSS object-fit: inside, cover, \
contain, fill or outside",
},
Key {
name: "position",
help: "where cover crops and contain places the image: center, top, bottom, left, \
right, top-left, top-right, bottom-left or bottom-right",
},
Key {
name: "background",
help: "padding of contain: #rrggbb, #rrggbbaa, white, black or transparent. auto is \
transparent for a format with alpha, white for one without",
},
Key {
name: "scale",
help: "a factor instead of a box: 0.5 halves both sides. Above 1 needs enlarge",
},
Key {
name: "enlarge",
help: "allow scaling up. Nothing scales up without it",
},
Key {
name: "filter",
help: "resampling filter: lanczos3, mitchell, catmull-rom, bilinear, box or nearest",
},
Key {
name: "maxPixels",
help: "refuse an image above this many pixels, decoded or resized",
},
Key {
name: "codecOpts",
help: "backend options, { \"jpeg:progressive\": \"false\" }. codecs() lists every key \
with its default",
},
Key {
name: "onTrial",
help: "called once per trial of the target search with { n, max, quality, score, size }",
},
];
const DECODE_OPTIONS: &[&str] = &["maxPixels"];
const DECODE_ANY_OPTIONS: &[&str] = &["maxPixels", "width", "height"];
Expand Down Expand Up @@ -347,7 +440,8 @@ impl Options {
/// [`Error::InvalidParams`] for a key this package does not have or a
/// value of the wrong type.
pub fn parse(options: Option<Ts<Self>>) -> Result<Self> {
let options: Self = parse(options.map(Into::into), OPTIONS)?;
let keys: Vec<&str> = OPTIONS.iter().map(|k| k.name).collect();
let options: Self = parse(options.map(Into::into), &keys)?;
let on_trial = &options.on_trial;
if !(on_trial.is_undefined() || on_trial.is_null() || on_trial.is_function()) {
return Err(invalid("`onTrial` is not a function"));
Expand Down Expand Up @@ -521,6 +615,94 @@ impl Options {
pub fn on_trial(&self) -> Option<&Function> {
self.on_trial.dyn_ref()
}

/// Every key with its default and its help, for `options()`. The
/// defaults are read from the builder the package starts from, so
/// they cannot drift from what an encode without options does.
#[must_use]
pub fn listing() -> Vec<OptionInfo> {
let sqzer = crate::sqzer(None);
let params = sqzer.params();
let resize = sqzer.resize_bounds();
let default = |key: &str| -> Option<String> {
Some(match key {
// Decided per image or per format.
"format" | "background" => "auto".to_string(),
"target" => match params.target {
Target::Ssimulacra2(t) => crate::output::tidy(t).to_string(),
Target::Quality(_) | Target::Lossless => return None,
},
"lossless" | "fast" => false.to_string(),
"effort" => params.effort.to_string(),
"subsampling" => subsampling_name(params.subsampling).to_string(),
"keepIcc" => params.keep_icc.to_string(),
"keepMetadata" => params.keep_metadata.to_string(),
"fit" => fit_name(resize.fit).to_string(),
"position" => position_name(resize.position).to_string(),
"enlarge" => resize.enlarge.to_string(),
"filter" => filter_name(resize.filter).to_string(),
"maxPixels" => MAX_PIXELS.to_string(),
// Nothing happens when these are left out.
_ => return None,
})
};
OPTIONS
.iter()
.map(|k| OptionInfo {
key: k.name,
default: default(k.name),
help: k.help,
})
.collect()
}
}

/// The spelling of [`Subsampling`] for a library value.
fn subsampling_name(s: params::Subsampling) -> &'static str {
match s {
params::Subsampling::Auto => "auto",
params::Subsampling::S444 => "444",
params::Subsampling::S422 => "422",
params::Subsampling::S420 => "420",
}
}

/// The spelling of [`Fit`] for a library value.
fn fit_name(f: resize::Fit) -> &'static str {
match f {
resize::Fit::Inside => "inside",
resize::Fit::Cover => "cover",
resize::Fit::Contain => "contain",
resize::Fit::Fill => "fill",
resize::Fit::Outside => "outside",
}
}

/// The spelling of [`Position`] for a library value.
fn position_name(p: resize::Position) -> &'static str {
match p {
resize::Position::Center => "center",
resize::Position::Top => "top",
resize::Position::Bottom => "bottom",
resize::Position::Left => "left",
resize::Position::Right => "right",
resize::Position::TopLeft => "top-left",
resize::Position::TopRight => "top-right",
resize::Position::BottomLeft => "bottom-left",
resize::Position::BottomRight => "bottom-right",
}
}

/// The spelling of [`Filter`] for a library value.
fn filter_name(f: resize::Filter) -> &'static str {
match f {
resize::Filter::Lanczos3 => "lanczos3",
resize::Filter::Mitchell => "mitchell",
resize::Filter::CatmullRom => "catmull-rom",
resize::Filter::Bilinear => "bilinear",
resize::Filter::Box => "box",
resize::Filter::Nearest => "nearest",
}
}

impl DecodeOptions {
Expand Down
Loading
Loading