Commit Graph
1420 Commits
Author SHA1 Message Date
Dylan M. Taylor e7c3dda7f3 craft-video: AV1 and HEVC take the colour description; AV1 motion search from EffectCraft (#829)
Three-way merge of the changes EffectCraft made to its av1enc and hevcenc
crates after craft-video took them:

- hevc: EncoderConfig carries colour_primaries, transfer_characteristics and
  matrix_coeffs (H.273 code points, BT.709 by default) and the VUI writes them.
- av1: EncoderConfig carries color_primaries, transfer_characteristics and
  matrix_coefficients and color_config writes them.
- av1: motion search keeps a visited set and stops a SAD once it cannot win,
  SATD has fixed-size kernels, and a decision snapshot saves only the block's
  BlockDecoded flags. Output is unchanged.

ffprobe tests check the colour description of both streams.
2026-11-11 16:40:32 -05:00
Dylan M. Taylor a9243c8c40 Add craft-lottie: the Lottie wire layer from EffectCraft (#879)
The encodings every Lottie layer, mask and shape reuses, on serde_json
values: the document header, animatable properties and their keyframes
(legacy end values, hold keys, spatial tangents), ease tangents to and
from speed and influence, bezier paths, gradient stop lists, the tolerant
number and colour readers, and embedded images as data URIs.

Taken from effectcraft crates/lottie at e18f48fb (anim.rs, shapes.rs,
import.rs, export.rs, base64.rs) with EffectCraft's project types replaced
by plain values. The layer, shape, effect and text mapping stays in
EffectCraft, and dotLottie is ZIP, which is not this crate's format.
2026-11-11 16:24:27 -05:00
Dylan M. Taylor 58b02d4e6a Add craft-captions: SRT, WebVTT, SCC, MCC, EBU STL and TTML behind one cue model (#876)
Carved from FilmCraft's filmcraft-captions (crates/captions at 4bec22bd): the
file readers and writers, CEA-608/708 coding, format detection and the cue
document. Rendering and the mapping onto FilmCraft's caption tracks stay there.

- time: the crate has its own exact Tick (1/254016000000 s) and rational
  FrameRate, the subset of filmcraft-time the formats use.
- TTML is read through craft-xml instead of roxmltree: no entity expansion,
  nesting bounded by the reader (128 levels, as before).
- parse refuses input over 64 MiB and keeps at most 1,000,000 cues.
- MCC and EBU STL name the caller's program (WriteOptions::program).
- plain_text (inline tag stripping) moves with the SCC/MCC writers that need it.
2026-11-11 16:23:34 -05:00
Dylan M. Taylor 81307f8274 Add craft-vectordrawable: Android Vector Drawable writer (#874)
The format half of FigCraft's crates/vectordrawable (at 24cf7032): the
model of paths, groups, clips and gradients, the XML writer, and the
mapping of a gradient on an affine frame to what a drawable can say.
The walk over FigCraft's document, its compatibility modes and warnings
stay in FigCraft.

Part of #778.
2026-11-11 16:23:23 -05:00
Dylan M. Taylor feb941cc19 craft-raw: Hasselblad 3FR and FFF (feature hasselblad) (#867)
* craft-raw: the file-format layer of camera raw files, with DNG and CRW

A new crate for #587 and #778: bytes in, the sensor data out as a neutral
RawImage (samples, CFA, levels, active area, crop, orientation, colour tags,
opcode lists, profile tables), and back for DNG. One Cargo feature per format:
dng (read and write), jxl (JPEG XL DNG), crw (through craft-crw).

The base is LightCraft's crates/raw at 5ff567a4: dng, dngwrite, tiffraw,
unpack, ljpeg, jxl, semantic, preview, the parsing half of opcodes, profile
and gaintable, the model and probe. Demosaicing and everything after it stay
with the develop pipeline.

Ported from PhotoCraft's and FilmCraft's DNG readers, each with a test: the
default crop's per-tag defaults, an unusable crop falling back to the active
area, non-finite white levels, the white default of a linearized image, the
AsShotWhiteXY range, 8-bit-only lossy JPEG, lossy tiles that must cover their
part of the image, and the CinemaDNG frame tags.

* craft-raw: Hasselblad 3FR and FFF (feature hasselblad)

RawFormat::CfaTiff decodes: the DNG-layout reader plus what these files add.
Uncompressed data or Hasselblad's lossless JPEG (selection value 8), RGGB when
no CFAPattern is stated, an FFF's colour matrix from the 3FR header it embeds,
and the 8-bit RGB preview handed on as a baseline TIFF.

Moved from LightCraft's vendor/hasselblad.rs at 5ff567a4 with its tests.
Ported from PhotoCraft's reader: four BlackLevel values without a
BlackLevelRepeatDim are one per CFA site in a file that is not a DNG.
2026-11-11 16:23:13 -05:00
Dylan M. Taylor 7f0f352d21 pdf inspection: count pending text clips and drop page content after reading (#566)
* pdf inspection: count pending text clips and drop page content after reading

- hayro-interpret: state_bytes counts the pending text-clip outlines a
  saved state copies, so MAX_SAVED_STATE_BYTES bounds them too.
- plate-test structure: read each page through a ContentReader into a
  buffer dropped after the page, instead of page.operations(), which keeps
  every page's decoded content for as long as the document.

* address review: cap page content read at the 256 MiB decode limit

* fix the saved-state test: start the clip outline with a MoveTo
2026-11-11 16:12:06 -05:00
Dylan M. Taylor 32589246be prepress: index Tidy::is_kept by x and y, validate a collinear run once (#851)
Findings 9ef8d922 and ec807eda (vectorcraft pathops boolean.rs analogues).
2026-11-11 15:48:26 -05:00
Dylan M. Taylor 5ebe40409f fonts-build: publish licences as .txt, refuse line breaks in watched paths, bound manifest and font reads (#837)
Ports vectorcraft 16d7fed6 (licence served as text), lightcraft 0e3b3ed5 (line break in rerun-if-changed) and 9cb2c721 / cadcraft #2732 27b4d723 (bounded regular-file reads).
2026-11-11 15:47:59 -05:00
Dylan M. Taylor 7b59dff881 svg-paths: charge sibling and ancestor selector walks to the selector budget (#835) 2026-11-11 15:47:33 -05:00
Dylan M. Taylor 6a73cd9c6b dxf: escape and cut a code page name before logging it (#855) 2026-11-11 15:47:06 -05:00
Dylan M. Taylor 9eee29b69d hayro-syntax: refuse a legacy encryption key length above 128 bits instead of panicking (#817)
Port of pdfcraft #1882 (344ba9f8). Revisions 2-4 take the file key from a 16-byte MD5 hash; /V 2 /R 3 /Length 256 sliced past it and panicked on open.
2026-11-11 15:46:28 -05:00
Dylan M. Taylor cc6106c56a craft-raw: the file-format layer of camera raw files, with DNG and CRW (#862)
A new crate for #587 and #778: bytes in, the sensor data out as a neutral
RawImage (samples, CFA, levels, active area, crop, orientation, colour tags,
opcode lists, profile tables), and back for DNG. One Cargo feature per format:
dng (read and write), jxl (JPEG XL DNG), crw (through craft-crw).

The base is LightCraft's crates/raw at 5ff567a4: dng, dngwrite, tiffraw,
unpack, ljpeg, jxl, semantic, preview, the parsing half of opcodes, profile
and gaintable, the model and probe. Demosaicing and everything after it stay
with the develop pipeline.

Ported from PhotoCraft's and FilmCraft's DNG readers, each with a test: the
default crop's per-tag defaults, an unusable crop falling back to the active
area, non-finite white levels, the white default of a linearized image, the
AsShotWhiteXY range, 8-bit-only lossy JPEG, lossy tiles that must cover their
part of the image, and the CinemaDNG frame tags.
2026-11-11 15:27:15 -05:00
Dylan M. Taylor de1daefe8c craft-media: AVI and OpenDML demuxer from FilmCraft (feature avi) (#856)
Moves filmcraft-avi (storytold/filmcraft@4bec22b) into craft_media::avi.
ByteSource is the crate's trait, Error::Io holds the io::Error, and
Stream::codec_params() maps the FourCC and the WAVEFORMATEX tag to a CodecId.
Adds Container::Avi and tests for the stream, header-size, nesting and
OpenDML index limits.
2026-11-11 15:26:20 -05:00
01cbc50269 metafile: dual EMF+ files draw their EMF+ records (stacked on #843) (#845)
* metafile: hold a placed pen width to what covers the frame

Picture::place scaled a pen width by the fit with no upper bound, so a
pen of 1e30 units made a renderer flatten round caps of that size. A
placed pen is now at most twice the diagonal of the frame and the path
together, which already covers the whole frame.

Ported from storytold/wordcraft#5427.

* metafile: one byte budget for items, EMF+ objects and clips; count clip regions when placing

- Canvas::shade_shape returns when the canvas is full or the clip is
  empty, instead of sampling a tile that is then dropped.
- EMF+ objects, the pieces of a continued object and the current and
  saved EMF+ clips count against the canvas's 256 MiB budget, in both
  directions: an object is not stored past what the canvas holds, and
  the canvas stops at what the objects hold.
- A nested metafile held by an EMF+ image object is charged for what its
  items own (segments, pixels, text, clip rectangles; a shared tile or
  clip path once) rather than 128 bytes an item.
- Picture::place charges each clip region it expands to the 500,000
  element budget, for every kind of item, and expands a region once for
  the consecutive items that share it.

Ported from storytold/deckcraft#1777, with its review comments addressed
(the canvas checks include the held bytes; text and bitmap items are
checked too; regions are matched by content, not by address).

* metafile: a negative crop side pads the picture

crop_window took each side as 0..1, so a negative side (a negative
a:srcRect side in an Office file, or a picture fitted whole into a frame
of another shape) was dropped and the picture was stretched over its
frame. A side is now taken down to -100: the window starts before the
source or ends past it, and Picture::fit places the source inside the
frame at its own proportions.

Ported from storytold/wordcraft#2027 (the -100 bound and the invariant
test) and storytold/wordcraft#5206 (the padding test).

* metafile: decode 8-bit text in the font's code page

8-bit text (WMF TEXTOUT and EXTTEXTOUT, EMF EXTTEXTOUTA and SMALLTEXTOUT)
was decoded in Windows-1250, 1251 or 1252 only, and a double-byte
character became a blank.

- Greek, Turkish, Vietnamese, Hebrew, Arabic, Baltic and Thai character
  sets decode in their code pages (encoding_rs, already a workspace
  dependency).
- Shift_JIS, UHC, GBK and Big5 pairs decode to their character, with the
  lead and trail ranges of each set; the character keeps the advances of
  both bytes. A lead byte without a trail byte is one undefined byte.
- A WMF face name is read in the font's code page.
- The Symbol font selected with DEFAULT_CHARSET keeps its bytes as symbol
  codes (U+F000 + code), as SYMBOL_CHARSET fonts do.

Ported from storytold/wordcraft#5187 (the code pages, the double-byte
ranges, the face name and the Symbol font) and storytold/deckcraft#792
(the Hebrew and Arabic tests).

* metafile: dual EMF+ files draw their EMF+ records, as GDI+ does

For a dual EMF+ file the reader played the plain EMF records and ignored
the EMF+ ones. Office draws metafiles with GDI+, which does the
opposite, and a file's GDI fallback can be incomplete (the EMF+ part
draws a nested metafile the fallback lacks).

EMF+ records now always play. In a dual file the plain EMF records are
skipped, except between an EmfPlusGetDC record and the next EMF+ record.
EMF+ only files and plain EMF files are read as before.

Ported from storytold/deckcraft#1464.

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: David Lin <woei66@gmail.com>
Co-authored-by: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com>
2026-11-11 15:26:10 -05:00
f19f131247 craft-audio: a damaged FLAC keeps its timing; AAC uses BitReader::peek (#846)
* craft-audio: AAC Huffman lookahead uses BitReader::peek

`BitReader::peek` reads from one 8-byte load since the bit reader was made
faster, which is what the AAC decoder's private `peek32` did. The private copy
goes; the lookahead is the same zero-padded 32 bits.

Ported from storytold/filmcraft#4232.

* craft-audio: a damaged FLAC keeps its length and timing

symphonia's FLAC demuxer drops a frame whose checksum fails and resyncs on the
next one, so the decoder never saw an error: the file decoded shorter and all
later audio came out one block early, with `damaged` at 0.

The jump in the packet timestamps is the audio that was lost. It is returned
as silence, in blocks of at most 65 536 frames, and counted once in
`AudioInfo::damaged`. It never passes the length the file declares; with no
declared length a jump of more than 2^20 frames is not believed. A seek into
the lost frames lands on silence at the right position.

Ported from storytold/soundcraft#556, onto the streaming decoder.

---------

Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Gert Pellin <gert@pellin.be>
2026-11-11 15:25:53 -05:00
Dylan M. Taylor ba3b3a892b craft-audio: document that decode and decode_with keep every sample, and what to pass for untrusted files (#826) 2026-11-11 15:25:43 -05:00
Dylan M. Taylor 0226f1d03e craft-video: H.264 decoder from FilmCraft, with DeckCraft's hardening (h264-decode) (#834)
* craft-video: H.264 decoder from FilmCraft (`h264-decode` feature)

FilmCraft's crates/h264 at 4c0d2ad7 becomes craft_video::h264::decode. The encoder moves to
h264/encode and is still exported from craft_video::h264. Feature h264 now turns on h264-decode
and h264-encode, so either half can be built without the other.

Without the threads feature a reference row that was never published read through expect();
it now reads as a gray row. craft_video::budget (the picture memory budget from DeckCraft's
bitstream crate) is added for the next commit. craft-video is built at opt-level 3 in the dev
profile, as FilmCraft built its codecs.

* craft-video: H.264 decoder hardening from DeckCraft's copy

- The picture store (DPB, current picture, pictures in flight or queued for output) is checked
  against a memory budget before a picture is allocated; Decoder::set_memory_budget.
- configure_avcc checks every entry before handling any: SPS NAL units in the first list, PPS in
  the second. A picture slice in the record starts no picture.
- The inverse transforms clamp coefficients to the spec's r range instead of overflowing.
- Temporal direct motion vectors are scaled in i32 and clipped to the spec range.
- slice_qp_delta is added with a checked add.
- A picture job sets up its picture state inside the panic guard.
2026-11-11 15:25:33 -05:00
d1f13fbea2 craft-heif: crop to the declared size, read_metadata, stereo test data (ports of open LightCraft PRs) (#841)
* craft-heif: stills decode at the size the container declares

A coded HEVC picture is a whole number of coding blocks and can be larger
than the image the container's ispe declares (Sony codes 1920x1080 as
1920x1088 without a conformance window). decode returned the coded size,
so probe and decode disagreed and the padding rows showed as a dark band.

decode_item now crops to ispe before the alpha merge and the transforms,
measures the clap parity on the declared size, and compares the alpha
item's declared size with the picture's instead of the coded planes.
A declared size larger than the coded picture is Malformed.

Ported from storytold/lightcraft#3977.

* craft-heif: read_metadata reads the primary image's EXIF and XMP without decoding

Cataloguing a photo needs its EXIF and XMP, not its pixels. read_metadata
opens the container under the same panic guard and box census as probe
and returns what decode would attach.

Ported from storytold/lightcraft#2087.

* craft-heif testdata: Spec::stereo builds a spatial photo (grpl/ster entity group)

A second image of the primary's size and a ster entity group naming both,
so callers can test stereo detection on a file this crate decodes.

Ported from storytold/lightcraft#1684.

* craft-heif: put_alpha uses is_multiple_of

---------

Co-authored-by: Mason <mason276752@gmail.com>
Co-authored-by: jkalkstein <76634771+jkalkstein@users.noreply.github.com>
Co-authored-by: Mitch Lindgren <mitch@lindg.ren>
2026-11-11 15:22:02 -05:00
Dylan M. Taylor 562327b58c craft-video: Vp9Encoder::encode_frame refuses planes shorter than the configured size (port of effectcraft #2995) (#832) 2026-11-11 15:00:31 -05:00
Dylan M. Taylor 4e92a95e41 craft-video: the DNx decoder refuses repeated or decreasing scan-line offsets (port of filmcraft #4101) (#833) 2026-11-11 15:00:17 -05:00
Dylan M. Taylor b16a2584fe craft-media tests: scratch files live in a directory only the test created (#839) 2026-11-11 15:00:02 -05:00
Dylan M. TaylorandLloyd Nicholson b72cb4d68c craft-mesh OBJ: map options by argument count, material libraries read once (from open EffectCraft PRs) (#854)
* craft-mesh OBJ: map_* options are passed over by their argument counts

A map line with options named its image by its final token, so
'map_Kd -o 0.5 0.5 my checker.png' named 'checker.png'. Options are now
skipped by the arguments they take (-o, -s and -t take one to three
numbers), so a name with spaces after options is one name. After an option
the reader does not know, the name is still the final token.

Ported from storytold/effectcraft#1324.

* craft-mesh OBJ: a material library named again is read once

Exporters repeat the mtllib line for every object. Each repeat asked the
resolver again, read the library again, added its materials and images a
second time, and counted its bytes against max_data_bytes once per line, so
a legitimate file with many objects could reach the limit.

Ported from storytold/effectcraft#2965 (its OBJ part: 'a library already
loaded is skipped').

---------

Co-authored-by: Lloyd Nicholson <nichlloyd@gmail.com>
2026-11-11 14:58:24 -05:00
8f1f35aa19 craft-xml: whole-tree attribute limit, node_count, parse_streaming_partial, strict pull reader (from open app PRs) (#850)
* craft-xml: a limit on the attributes a tree holds over all its elements

max_attrs bounds one start tag and max_nodes counts elements and text, so
attributes were the one thing nothing counted across the tree: 1,025
elements of 1,024 attributes each are 1,025 nodes and over a million
(String, String) pairs, ten times the input in headers alone.

Limits::max_attrs_total (default 4,000,000) bounds the attributes kept at
once; elements handed to a streaming callback take theirs out of the
count. Error::TooManyAttrsTotal.

Ported from storytold/wordcraft#4320, which added the same budget to
WordCraft's DOCX reader.

* craft-xml tests: an internal DTD subset is refused wherever it hides, in linear time

The cases of storytold/filmcraft#4139 (a marker inside a comment or a
processing instruction, a '>' inside the system literal, 100,000 markers
that never close) against Doctype::RejectInternalSubset. The reader already
handles them; the tests keep it so.

* craft-xml: El::node_count, for one node budget over the parts of a package

max_nodes bounds one parse. A package of many parts, each inside the limit,
still costs a tree per part the reader keeps. node_count is what max_nodes
counted for an element, so a reader charges each tree against one budget
and reads the next part with what is left as its max_nodes: a part over
the budget stops before its tree is built.

Ported from storytold/wordcraft#5665, which adds a package-wide element
budget to WordCraft's DOCX reader (parse_counted with a max).

* craft-xml: parse_streaming_partial returns what was read when a streaming read fails

A worksheet whose rows cannot all be read (one row over the node limit, a
callback error, markup the tokenizer cannot read) lost everything else in
the part with it. parse_streaming_partial returns the error together with
the root as far as it was read: the children complete before the failure
and the elements open at that point. parse_streaming is unchanged.

Ported from storytold/gridcraft#3317 (xml::parse_streaming_partial).

* craft-xml: a strict pull reader refuses a second root and text outside the root

Options::events builds no tree, so the checks the tree reader makes for a
strict read (a second root element, character data outside the root) were
not made at all: a strict pull over '<a/><b/>' or '<a/>text' ended without
an error. A reader that walks events would take a damaged file's first
element for the whole document. The reader now makes both checks itself,
so the pull reader and the tree agree.

Ported from storytold/wordcraft#1862, which made WordCraft's ODT event
reader report malformed XML instead of stopping silently.

---------

Co-authored-by: Oliwer Urbaniak <oliwerx12@gmail.com>
Co-authored-by: Bra1nZ <95569966+Bra1nZ@users.noreply.github.com>
Co-authored-by: Neil Cawse <neilcawse@geotab.com>
Co-authored-by: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com>
2026-11-11 14:58:15 -05:00
ebb40fa0c9 craft-dxf: R12 binary, more code pages, borrowing sections, exponent reals (ports of open CadCraft PRs) (#847)
* craft-dxf: the ASCII reader unescapes only text that holds a backslash

Only a string with a backslash can hold a \U+ or \M+ escape; every other
string moves into the tag as it is instead of being copied. The EOF check
reads the value before it moves.

Ported from storytold/cadcraft#1833.

* craft-dxf: write_ascii_in_page writes the code page without the UTF-8 fallback

write_ascii_in falls back to pure ASCII when the code page bytes happen to
be valid UTF-8, for readers that guess UTF-8. A reader that decodes a
pre-2007 DXF strictly by its $DWGCODEPAGE needs the code page bytes.

Ported from storytold/cadcraft#2851.

* craft-dxf: behaviour-preserving cleanups

Exact group-code mappings in one place, documented errors on the fallible
public parsers, and simpler option, result and formatting idioms. No change
to parsing, writing, numeric conversion, encoding or the public API.

Ported from storytold/cadcraft#2571.

* craft-dxf: read R12 and earlier binary DXF (1-byte group codes)

Binary DXF written before R13 stores each group code in one byte, with 255
escaping a 2-byte code. The reader only knew the R13+ layout, where codes
are two bytes, and also treated 255 as an escape there. The layout is now
told from the first tag; 255 is an escape only in the 1-byte layout and an
ordinary code in R13+.

Ported from storytold/cadcraft#1499.

* craft-dxf: section_slices, record_slices and Tag::str_ref borrow instead of copying

sections() and records() copy every tag out of the parsed list, so a reader
that keeps the list holds a large file's tags two or three times. The new
functions return slices of the list. sections() and records() now build
their copies from them, so there is one implementation of each split.

Ported from storytold/cadcraft#1708. storytold/cadcraft#1588 solves the same
problem by changing Section itself; this keeps the existing API.

* craft-dxf: reals beyond 1e16 or below 1e-10 are written in exponent form; string_groups

format_real wrote f64::MAX as 309 digits. Values outside 1e-10..1e16 are now
written as AutoCAD writes them (1.0E+20) and read back exactly.
string_groups splits text into the 250-character groups long strings are
stored in (MAX_STRING_CHARS).

Ported from storytold/cadcraft#1790.

* craft-dxf: DOS 720/737/775/855/857/860/861/863/864/865/869 and ISO 8859-15 code pages; cheaper unescape and hex

$DWGCODEPAGE values DOS720, 737, 775, 855, 857, 860, 861, 863, 864, 865, 869
and ISO8859_15 had no decoder and fell back to Windows-1252. ISO 8859-15
decodes through encoding_rs, the DOS pages through tables checked against
the Unicode Consortium mapping files (dos_tables.rs). DOS 864's byte 0x25 is
U+066A, so a writer escapes '%' there. U+FFFD is never written as a table
byte. A string without an escape moves into its tag without a copy, cstr
finds its terminator with one scan, and binary chunks are hex-encoded from a
table.

Ported from storytold/cadcraft#1271. Its group-code-255 change is carried by
the R12 binary port: the 1-byte layout is detected only when the escaped
code is one that needs the escape, so an R13+ file starting with code 255
still reads.

* craft-dxf: Korean Johab (ANSI_1361) text and \M+4 escapes decode

Johab had no decoder: files declaring ANSI_1361 were read as Windows-1252
and \M+4 escapes stayed literal. Hangul syllables are composed from the
three 5-bit jamo indices (KS X 1001 annex 3); symbols and Hanja map onto
KS X 1001 rows decoded as EUC-KR. Read only: a writer escapes instead.

Ported from storytold/cadcraft#1911 (its DOS code pages are covered by the
tables from storytold/cadcraft#1271).

---------

Co-authored-by: mo <131036780+mbolling@users.noreply.github.com>
Co-authored-by: ffishman <20211677+ffishman@users.noreply.github.com>
Co-authored-by: Cédric Legrand <cedric@legrand-tech.fr>
Co-authored-by: valentinevg812-eng <valentinevg812@gmail.com>
Co-authored-by: Mads Holten Rasmussen <mads.h.rasmussen@gmail.com>
Co-authored-by: LastSkywalker <maksim@sky-tehnol.uk>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Francesco BV <francesco.esposito@bvitalia.net>
2026-11-11 14:58:05 -05:00
9adca4a8e5 craft-tiff: shared child IFDs, multi-string ASCII, parse_with_status (ports of open LightCraft PRs) (#842)
* craft-tiff: keep shared child IFDs and every string of a multi-string ASCII value

Two pointers to the same child IFD (two SubIFDs entries, or an Exif IFD
shared by two directories) were treated as a loop and the second was
dropped. Loops are now detected on the current parent path only; a shared
IFD is parsed once and copied for later pointers, and each copy is charged
its IFDs, entries and value bytes so max_ifds, max_entries_total and the
value budget still hold.

ASCII values keep every NUL-separated string (Value::as_strings); as_str
and as_bytes return the first.

Ported from storytold/lightcraft#3002.

* craft-tiff: parse_with_status reports whether the whole IFD structure was read

The reader is best effort: truncated tables, over-budget values, dangling
pointers and unfollowed children are skipped silently. A caller that must
tell a clean absence from something it could not inspect (a privacy scan
for GPS data) gets the same tree plus a completeness flag.

Also adds the PHOTOSHOP_IMAGE_RESOURCES and GPS destination tag constants.

Ported from storytold/lightcraft#3766. On top of the shared-child change,
a second pointer to an IFD already read counts as read, and a next pointer
that loops back to the chain is incomplete.

---------

Co-authored-by: Andrew Frahn <andrew@emmertex.com>
Co-authored-by: ronibalthazar <61232335+ronibalthazar@users.noreply.github.com>
2026-11-11 14:57:55 -05:00
Dylan M. Taylor 12e24c1029 svg-paths: CSS colour parser and a small SVG writer (#857)
color: parses the CSS colours SVG paint uses (hex, rgb()/hsl(), the 148 named
colours) into an opaque 8-bit sRGB Color, and writes the canonical #rrggbb form.
Shape::stroke is still returned as written; parse_color reads it.

write: SvgWriter (XML declaration, root element with the SVG namespace, elements
and attributes escaped through craft-xml, caller-controlled line breaks), path
data in shortest round-trip form or with fixed decimals, and fixed-decimal
numbers that are never NaN or infinite.
2026-11-11 14:57:46 -05:00
Dylan M. Taylorandvangel76 e7814b1727 craft-media: Opus timing treats an out-of-range Ogg granule as missing (#859)
A granule outside 0..=MAX_OPUS_GRANULE (negative other than -1, or absurdly
large) no longer moves the packet positions or the total: the page's packets
continue from the previous position. Ported from storytold/filmcraft#4049.

Co-authored-by: vangel76 <vangel1976@googlemail.com>
2026-11-11 14:57:36 -05:00
Dylan M. Taylor 251ef87c89 ci: use StepSecurity's maintained actions (#853)
Each third-party action that StepSecurity publishes a secure drop-in
replacement for now uses that replacement, pinned to a commit with its
version as a comment. Versions follow what we used: the newest
StepSecurity release in the same major, or the closest major they
publish. dtolnay/rust-toolchain refs that named a toolchain (stable,
nightly, 1.76) become the replacement's `toolchain` input.
2026-11-11 14:57:04 -05:00
Dylan M. Taylor 725ce2bdf2 craft-cms: read mAB/mBA elements through one checked slice (#823) 2026-11-11 14:41:43 -05:00
Dylan M. Taylor b0a2510aac sonar: clear the two new vulnerabilities and stop counting codec tables as duplication (#848) 2026-11-11 14:33:10 -05:00
75895c0321 metafile: pen width cap, shared byte budget, negative crops, code pages (#843)
* metafile: hold a placed pen width to what covers the frame

Picture::place scaled a pen width by the fit with no upper bound, so a
pen of 1e30 units made a renderer flatten round caps of that size. A
placed pen is now at most twice the diagonal of the frame and the path
together, which already covers the whole frame.

Ported from storytold/wordcraft#5427.

* metafile: one byte budget for items, EMF+ objects and clips; count clip regions when placing

- Canvas::shade_shape returns when the canvas is full or the clip is
  empty, instead of sampling a tile that is then dropped.
- EMF+ objects, the pieces of a continued object and the current and
  saved EMF+ clips count against the canvas's 256 MiB budget, in both
  directions: an object is not stored past what the canvas holds, and
  the canvas stops at what the objects hold.
- A nested metafile held by an EMF+ image object is charged for what its
  items own (segments, pixels, text, clip rectangles; a shared tile or
  clip path once) rather than 128 bytes an item.
- Picture::place charges each clip region it expands to the 500,000
  element budget, for every kind of item, and expands a region once for
  the consecutive items that share it.

Ported from storytold/deckcraft#1777, with its review comments addressed
(the canvas checks include the held bytes; text and bitmap items are
checked too; regions are matched by content, not by address).

* metafile: a negative crop side pads the picture

crop_window took each side as 0..1, so a negative side (a negative
a:srcRect side in an Office file, or a picture fitted whole into a frame
of another shape) was dropped and the picture was stretched over its
frame. A side is now taken down to -100: the window starts before the
source or ends past it, and Picture::fit places the source inside the
frame at its own proportions.

Ported from storytold/wordcraft#2027 (the -100 bound and the invariant
test) and storytold/wordcraft#5206 (the padding test).

* metafile: decode 8-bit text in the font's code page

8-bit text (WMF TEXTOUT and EXTTEXTOUT, EMF EXTTEXTOUTA and SMALLTEXTOUT)
was decoded in Windows-1250, 1251 or 1252 only, and a double-byte
character became a blank.

- Greek, Turkish, Vietnamese, Hebrew, Arabic, Baltic and Thai character
  sets decode in their code pages (encoding_rs, already a workspace
  dependency).
- Shift_JIS, UHC, GBK and Big5 pairs decode to their character, with the
  lead and trail ranges of each set; the character keeps the advances of
  both bytes. A lead byte without a trail byte is one undefined byte.
- A WMF face name is read in the font's code page.
- The Symbol font selected with DEFAULT_CHARSET keeps its bytes as symbol
  codes (U+F000 + code), as SYMBOL_CHARSET fonts do.

Ported from storytold/wordcraft#5187 (the code pages, the double-byte
ranges, the face name and the Symbol font) and storytold/deckcraft#792
(the Hebrew and Arabic tests).

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: David Lin <woei66@gmail.com>
2026-11-11 14:25:35 -05:00
ce31f42e76 craft-media: QuickTime logs box, Matroska Colour in the muxer, laced remux test (#840)
* craft-media: read and write the QuickTime logs box

A video sample entry's `logs` child holds the CoreMedia log transfer-function
identifier (`com.apple.rec2020.apple-log` for Apple Log). It is kept as
`VideoParams::log_transfer_function`, whatever curve it names.

The reader takes non-empty UTF-8 without NUL of at most 255 bytes; anything
else, or a second `logs` box, reads as unknown. The writer refuses an
identifier the reader would not accept.

Ported from storytold/filmcraft#1764.

* craft-media: the Matroska muxer writes a video track's Colour element

`TrackSpec::color` takes the reader's `Color` and the muxer writes the fields
that are set: matrix, range, transfer, primaries, chroma layout, light levels
and mastering metadata (RFC 9559). VP9's own colour syntax carries only the
matrix and range, so without the element a Rec. 2020, P3, PQ or HLG WebM reads
as BT.709.

Ported from storytold/effectcraft#2926, which wrote the element by hand in
EffectCraft's WebM export.

* craft-media: the laced remux test laces ffmpeg's Vorbis fixture again

ffmpeg's Vorbis files declare a short DefaultDuration and store longer
durations per block, so with the track's own default nothing laced and the
test failed ("vorbis.mkv Xiph: nothing was laced", ffmpeg 9.0.2). Audio tracks
now take their median packet duration as the default, and frames are written
with their explicit durations, so a short last packet ends its lace.

Ported from storytold/filmcraft#3622 (test-only part).

---------

Co-authored-by: 20Coder <51251284+BitL8-ByteShort@users.noreply.github.com>
Co-authored-by: JonRC <jonarc06@gmail.com>
Co-authored-by: Michael Butler <michaelbutler75@gmail.com>
2026-11-11 14:25:21 -05:00
Dylan M. Taylor c814fdfe7c craft-audio: bound MP4 audio stts entries and frames x channels per packet (port of soundcraft #1332) (#825) 2026-11-11 14:24:58 -05:00
Dylan M. Taylor 0e60ef3b0a craft-audio: extension and media-type tables, linear WAV chunk scan, SoundCraft's later tests (#824)
DECODE_EXTENSIONS lists the file extensions of the formats decode reads.
EncodeFormat gains ALL, extension() and mime_type(). SoundCraft kept these
tables in its own FileFormat type; they are format knowledge, so they live
here.

A WAV with many empty data chunks parsed in quadratic time: whether an empty
data chunk is followed by chunks alone was decided for every one of them, by
walking the rest of the file. Only the first data chunk is used, so it is
decided once.

Tests SoundCraft added to audio-io after craft-audio was cut from it: Matroska
element sizes beyond the file, an AIFF channel count with the sign bit set,
probe and decode agreeing on the length of AAC in MP4, and one small file per
decodable extension (tests/fixtures).
2026-11-11 14:24:44 -05:00
Dylan M. Taylor 1175abec2e craft-video: HEVC decoder from FilmCraft, with DeckCraft's hardening (hevc-decode) (#828)
* craft-video: HEVC decoder from FilmCraft (`hevc-decode` feature)

FilmCraft's crates/hevc at 4c0d2ad7 becomes craft_video::hevc::decode. The encoder moves to
hevc/encode and is still exported from craft_video::hevc. Feature hevc now turns on hevc-decode
and hevc-encode, so either half can be built without the other.

craft_video::budget (the picture memory budget from DeckCraft's bitstream crate) is added for the
next commit. craft-video is built at opt-level 3 in the dev profile, as FilmCraft built its
codecs.

* craft-video: HEVC decoder hardening from DeckCraft's copy

- The picture store (DPB, current picture, pictures in flight) is checked against a memory
  budget before a picture is allocated; Decoder::set_memory_budget.
- configure_hvcc checks every array before handling any NAL: only VPS, SPS, PPS and SEI arrays,
  each NAL header agreeing with its array type. A picture slice in the record starts no picture.
- init_qp_minus26, slice_qp_delta and delta_chroma_log2_weight_denom are range checked before
  the additions that overflowed on Exp-Golomb values near the i32 limits.
- A picture job sets up its picture state inside the panic guard.
2026-11-11 14:24:30 -05:00
Dylan M. Taylor 4c6fe7c864 craft-cms: confirm ICC identity by bytes, not the 64-bit hash alone (#822) 2026-11-11 13:53:39 -05:00
Dylan M. Taylor 4e309f8458 craft-heif: cap hvcC parameter sets and tiles, count alpha-grid tiles (port of lightcraft #3645) (#812)
* craft-heif: cap hvcC parameter sets and tiles, count alpha-grid tiles in the visit budget

* craft-heif: charge every tile reference of a thumbnail candidate to the visit budget
2026-11-11 13:53:29 -05:00
Dylan M. Taylor 6ac2a6a34a craft-video: VP9 decoder from FilmCraft, with DeckCraft's memory budget (vp9-decode) (#821)
* craft-video: VP9 decoder from FilmCraft (`vp9-decode` feature)

FilmCraft's crates/vp9 at 4c0d2ad7 becomes craft_video::vp9::decode. The encoder moves to
vp9/encode and is still exported from craft_video::vp9. Feature vp9 now turns on vp9-decode and
vp9-encode, so either half can be built without the other.

The decoder's tests find ffmpeg and the fixture directory through craft-media-testkit; a fixture
that cannot be generated fails under CRAFT_MEDIA_REQUIRE_ORACLES=1. craft-video is built at
opt-level 3 in the dev profile, as FilmCraft built its codecs.

* craft-video: VP9 decoder checks its picture store against a memory budget

DeckCraft's copy of the decoder refuses a frame size whose pictures (eight reference slots, the
new frame and those in flight) would not fit a byte budget. The budget type comes with it as
craft_video::budget; FilmCraft's 2^26 luma sample cap stays.
2026-11-11 13:47:53 -05:00
Dylan M. Taylor 9dfc35965c Add craft-metafile: WMF and EMF reader and writer (#830)
* craft-metafile: DeckCraft's WMF, EMF and EMF+ reader with WordCraft's changes merged

The base is DeckCraft's crates/metafile at cf0d7fbb. WordCraft's changes since
the copy was taken (WordCraft 05679497, the ancestor) are merged three ways from
WordCraft 639b8133: the cropped, depth-once DIB decoder, the bitmap side cap,
the header-only size(), crop_window, the free-slot object table, capacity-based
budget charging and the exact reservation in subpaths. WordCraft's own text
records lose to DeckCraft's richer text model. All 120 DeckCraft tests and all
69 WordCraft tests are kept (132 distinct).

* craft-metafile: line styles, clip paths and the records behind them, from VectorCraft's reader

The model gains what VectorCraft's reader kept and the other two dropped:
LineStyle (cap, join, miter limit, dashes) on paths, and clip paths
(SELECTCLIPPATH) shared between the items they clip. The reader now plays
EXTCREATEPEN style entries and the pen's cap and join bits, SETMITERLIMIT,
SETARCDIRECTION, SCALEVIEWPORTEXTEX, SCALEWINDOWEXTEX, SELECTCLIPPATH and
ABORTPATH; STROKEANDFILLPATH closes open figures and CLOSEFIGURE moves the
current position; the header's micrometre device size is used when present;
BITMAPCOREHEADER, BI_JPEG and BI_PNG bitmaps decode; records of unknown types
are counted in Picture::skipped_records. Each has a test.

* craft-metafile: EMF and WMF writers behind the write feature, bitmap transforms, a decode budget

The writers are VectorCraft's, taking a list of drawing operations (fills,
strokes with pens, images, clips) instead of its document; the document walk
stays in the application. Bitmap items gain a transform, so an image drawn
under a turning world transform keeps its turn (the writer places images that
way). JPEG and PNG bitmaps share one per-file pixel budget. The tests cover the
writers record by record, round-trip both formats through the reader, and play
VectorCraft's hand-built files into the picture model.

* craft-metafile: a test per limit, README, crate docs

Adds tests for the limits no test held to their value (string lengths, clip
region rectangles, placed elements, tile side, palette size, nesting, the EMF+
object table, save stack, point lists, gradient stops, region depth, continued
objects, bitmap masking work) and a truncation and mutation sweep over the new
records. Records that change what is drawn but are not played now count as
skipped instead of ignored. The README has the coverage tables, the limits,
what stays in the apps, the origin of each part and a migration table per app.

* craft-metafile: a region node cap test, the box of a turned bitmap

The region node cap gets a test of its own (a balanced tree of intersections),
and a turned bitmap's rectangle is the box around its four corners, so a caller
that draws rectangles still has an area to draw into.
2026-11-11 13:39:28 -05:00
Dylan M. Taylor b59aeeb10a craft-cms: per-profile budgets for decoded curve samples and CLUT values (#815) 2026-11-11 13:31:08 -05:00
Dylan M. Taylor 2408292033 craft-heif: cap zero-width iloc extents, apply the census on the thumbnail path (#813) 2026-11-11 13:30:58 -05:00
Dylan M. Taylor cf14284af1 craft-tiff: cap chunk descriptors at the file size and saturate the grid product (#827) 2026-11-11 13:30:49 -05:00
Dylan M. Taylor 8f3e83aaee Add craft-mesh: STL, OBJ/MTL, PLY and glTF/GLB behind one neutral mesh model (#819)
* Add craft-mesh: neutral mesh model with STL, OBJ/MTL and PLY

* craft-mesh: glTF 2.0 and GLB

* craft-mesh: README, crate list entry, a test for the UV set bound
2026-11-11 13:04:03 -05:00
Dylan M. Taylor 06af3f2d46 Add craft-cfb: Compound File Binary reader and writer (#818)
The base is FilmCraft's filmcraft-cfb (reader, writer, tests) at
filmcraft 6d5269ed. Ported on top of it, each with a test:

From GridCraft's and DeckCraft's readers: only sector shifts 9 and 12;
a file that ends inside its last sector still reads; a chain loop
shorter than the stream is an error instead of repeated data; a damaged
mini stream no longer fails the open; links to missing directory
entries are ignored; a stream size cap (Limits, default 1 GiB, and a
per-call cap); a lenient directory scan (directory_names,
has_entry_named) for files too damaged to open.

From WordCraft's guard: a FAT sector listed twice is refused; spare FAT
sectors and zero-padded DIFAT sectors are accepted.

New: Limits::max_dir_entries, walk() and path() to list entries,
has_stream, Writer::stream_at and write_streams, and only storages the
root reaches are walked.
2026-11-11 12:49:55 -05:00
Dylan M. Taylor 8152fc0be0 Add craft-xml: bounded XML tree, streaming and writer (#816)
Almost every Craft app carries its own small XML helper: a bounded
element tree on a pull parser and an escaping string writer. This is
the shared one, so each app can drop its copy.

- An owned tree (El, Node) read from bytes with limits on input size,
  depth, node count and attributes per element. A limit exceeded is an
  error. No DTD is processed and no entity is expanded beyond the five
  predefined ones and numeric character references. UTF-8 and UTF-16.
- Names as written, by local name, or rewritten to the caller's
  canonical prefixes by namespace URI, with hooks for a prefix that
  depends on the parent and for attribute values that hold prefixes.
- Lenient and strict reading; streaming of repeated elements to a
  callback; first_start; a pull reader with byte ranges.
- Accessors, typed attribute getters, mutation and a builder. Where
  the apps disagreed (whole or local names, own or deep text, exact or
  lossy numbers) there are two spellings.
- esc, esc_attr, compact and pretty serialising, and a Writer.
  A name that is not an XML name is never written.

Built on quick-xml 0.41's tokenizer (MIT), the version the apps use.
The apps' own tests for their helpers are ported in tests/apps.rs.
No app is switched here.
2026-11-11 12:48:51 -05:00
Dylan M. Taylor d4e3017c34 Add craft-mime: Internet message and MIME reader and writer (#814)
MailCraft and WordCraft each parse MIME: MailCraft for mail (crates/mime,
crates/charset), WordCraft for MHT web archives (formats/src/mht.rs). This
adds one shared crate for the format, taken from MailCraft's code where it
is format code, with WordCraft's multipart/related lookups and its fix for
comments in Content-Type.

Reader: parse() gives a tree of parts (header fields, content type and
parameters, decoded body, charset-decoded text). It never fails or panics,
is linear in the input, and is bounded by Limits (input size, header count
and sizes, depth, part count, decoded size); Message::truncated reports a
limit that left something out.

Writer: Entity::text / file / multipart and write_message, with folded
headers, RFC 2047 and RFC 2231 encoding, quoted-printable and base64 with
line limits, and boundaries checked against the content. Every header
value loses CR, LF and NUL, and a name that is no field name is not
written.

Also: address lists, mbox splitting and writing on raw message bytes, and
Date parsing and formatting on a plain struct (chrono is a private
dependency; no date type is in the public API).

No app is switched here. The README has the migration tables and says what
stays in the apps.
2026-11-11 12:48:08 -05:00
Dylan M. Taylor 27f2204603 craft-media: DeckCraft's two Ogg fixes (#810)
Packet byte ranges are checked against the source before a buffer is
sized for them (deckcraft 51d19baa), and packets after the last known
granule count towards the Opus duration (deckcraft 9d36b593). Each test
failed before its fix.
2026-11-11 12:48:03 -05:00
Dylan M. Taylor ac5f998274 ci: run Harden-Runner on every job (#811)
Adds step-security/harden-runner (audit, pinned to v2.22.4 like
craft-common-actions) as the first step of each job that did not have it,
so every workflow in the org's public repos reports its egress to
StepSecurity. Audit, not block: block-mode allow lists come from each
job's observed traffic.
2026-11-11 12:10:08 -05:00
Dylan M. Taylor 56d088bf04 craft-audio: flac::Encoder, the FLAC packet encoder, is public (#809)
* craft-audio: FilmCraft's FLAC encoder and MP1/MP2 decoder speed-ups

FLAC: per-order predictor kernels, merged Rice partition sums, the LPC
window kept per block length, and batches consumed by offset
(filmcraft 5eaff794, d9dad4a5). The output is unchanged: 64 golden cases
hash the same before and after.

MP1/MP2: the faster synthesis filterbank (filmcraft 51c755e5), with its
bit-exact test.

* craft-audio: FilmCraft's AAC and Opus decoder speed-ups and refactors

AAC: table-driven Huffman decoding, per-stage FFT twiddles and fused
stages, reused per-channel work buffers, and the encoder's memoised
per-band work (filmcraft f9627280, d587ceed, 6121635c, dc124ab7,
7a532eca).

Opus: radix-2/3/4 FFT kernels in the CELT IMDCT, the PVQ table built
once, one de-emphasis loop for both channels, the SILK resampler
without per-sample divisions and with eight partial sums, concealment
and FEC capped at 120 ms (filmcraft bf7ac420, 12e32026, c4dc64d1,
eb352ec8, 87b8e22d, 014b5e56, 78d77b72).

The per-frame mid-only flag this crate fixed in the SILK decoder is
kept in the refactored decode.

* craft-media: Ogg CRC-32 by slice-by-16

FilmCraft's 6763f9bf: sixteen independent lookups per 16 input bytes, so
opening a file is several times faster. Checked against a bit-at-a-time
reference on every short length and alignment, and across every split.

* craft-audio: SoundCraft's CAF cookie-size and FLAC vendor-length checks

A CAF magic cookie size above the signed range is refused instead of
passed to symphonia (soundcraft 3bd3965), and a Vorbis comment block
whose vendor string claims more than the block holds is refused
(soundcraft ddfe991). Each with its regression test.

* craft-audio: AC-3 reads bits through the shared BitReader

FilmCraft's 1c9c2e82: the AC-3 decoder's private bit-at-a-time reader is
replaced by craft-media's BitReader, with its two tests: the decoded
output of 2,000 synthetic and 12 real syncframes (and their damaged
variants) hashes the same as before, and the reader agrees with the old
one on random reads, skips and overruns. The three AC-3 features now
turn on craft-media's bitstream module.

* craft-audio: the quantiser rounds without a libm call

SoundCraft's d293336, the audio-io part: round-half-away by integer
arithmetic in the bounce's inner loop, with its test against f64::round
over the quantiser's range.

* craft-audio: flac::Encoder, the FLAC packet encoder, is public

FilmCraft muxes FLAC into its own outputs from integer samples, so it
needs the encoder below FileEncoder: planar samples in, frames out, and
the STREAMINFO to patch in at the end. The module is behind flac-encode,
its errors convert to AudioError::Encode, and pending_samples says how
long the final frame will be.
2026-11-11 11:48:37 -05:00
Dylan M. Taylor 44b4a23137 craft-audio, craft-media: the changes FilmCraft and SoundCraft made to their copies since extraction (#807)
* craft-audio: FilmCraft's FLAC encoder and MP1/MP2 decoder speed-ups

FLAC: per-order predictor kernels, merged Rice partition sums, the LPC
window kept per block length, and batches consumed by offset
(filmcraft 5eaff794, d9dad4a5). The output is unchanged: 64 golden cases
hash the same before and after.

MP1/MP2: the faster synthesis filterbank (filmcraft 51c755e5), with its
bit-exact test.

* craft-audio: FilmCraft's AAC and Opus decoder speed-ups and refactors

AAC: table-driven Huffman decoding, per-stage FFT twiddles and fused
stages, reused per-channel work buffers, and the encoder's memoised
per-band work (filmcraft f9627280, d587ceed, 6121635c, dc124ab7,
7a532eca).

Opus: radix-2/3/4 FFT kernels in the CELT IMDCT, the PVQ table built
once, one de-emphasis loop for both channels, the SILK resampler
without per-sample divisions and with eight partial sums, concealment
and FEC capped at 120 ms (filmcraft bf7ac420, 12e32026, c4dc64d1,
eb352ec8, 87b8e22d, 014b5e56, 78d77b72).

The per-frame mid-only flag this crate fixed in the SILK decoder is
kept in the refactored decode.

* craft-media: Ogg CRC-32 by slice-by-16

FilmCraft's 6763f9bf: sixteen independent lookups per 16 input bytes, so
opening a file is several times faster. Checked against a bit-at-a-time
reference on every short length and alignment, and across every split.

* craft-audio: SoundCraft's CAF cookie-size and FLAC vendor-length checks

A CAF magic cookie size above the signed range is refused instead of
passed to symphonia (soundcraft 3bd3965), and a Vorbis comment block
whose vendor string claims more than the block holds is refused
(soundcraft ddfe991). Each with its regression test.

* craft-audio: AC-3 reads bits through the shared BitReader

FilmCraft's 1c9c2e82: the AC-3 decoder's private bit-at-a-time reader is
replaced by craft-media's BitReader, with its two tests: the decoded
output of 2,000 synthetic and 12 real syncframes (and their damaged
variants) hashes the same as before, and the reader agrees with the old
one on random reads, skips and overruns. The three AC-3 features now
turn on craft-media's bitstream module.

* craft-audio: the quantiser rounds without a libm call

SoundCraft's d293336, the audio-io part: round-half-away by integer
arithmetic in the bounce's inner loop, with its test against f64::round
over the quantiser's range.
2026-11-11 11:32:33 -05:00