Commit Graph
240 Commits
Author SHA1 Message Date
Dylan M. Taylor 9435232e51 craft-tga: Targa reader and run-length writer (#892)
FilmCraft's clean-room Targa reader and PhotoCraft's run-length writer in one
crate. The reader returns RGBA8 and names the layout the file stored; the
writer's packets end with their scan line.
2026-11-11 18:39:14 -05:00
Dylan M. Taylor b2f247d3bf craft-dpx: DPX and Cineon reader and writer (#887)
One implementation of DPX (SMPTE ST 268) and Kodak Cineon for the apps that
each carried their own: PhotoCraft (reader, writer, Cineon, header metadata),
FilmCraft (Y'CbCr descriptors, ABGR, packed 10/12-bit) and EffectCraft
(Cineon in both byte orders, pixel aspect ratio, 12-bit method B).

The crate returns code values and header fields; colour conversion and
orientation stay in the apps.
2026-11-11 18:38:46 -05:00
Dylan M. Taylorandsubha-2025 0ff43c72b3 Add craft-raster-codecs: DDS, DICOM, FITS, IFF/ILBM, PCX, PNM and XPM from PhotoCraft (#880)
* Add craft-raster-codecs: DDS, DICOM, FITS, IFF/ILBM, PCX, PNM and XPM from PhotoCraft

One module per format, moved from photocraft-codecs at 63b641b5 with its tests.
Decoders return one flat Image and take Limits; encoders take an image already in a
layout the format stores. Layout planning, colour and the document mapping stay in the app.

* craft-raster-codecs: PCX reads four 8-bit planes as RGBA and version 4 headers; pcx::dimensions

Four 8-bit planes are red, green, blue and alpha: they decode to RGBA instead of being
refused. Version 4 (PC Paintbrush for Windows) headers are accepted. pcx::dimensions gives the
pixel size from the header without decoding.

Ported from the PCX reader of storytold/designcraft#2011.

---------

Co-authored-by: subha-2025 <218455239+subha-2025@users.noreply.github.com>
2026-11-11 18:37:51 -05:00
Dylan M. Taylor 26b36b4f9e Add craft-fig: the .fig container, read and written (#905)
* Add craft-kiwi: the kiwi binary schema format, decoded and encoded

The generic kiwi decoder and encoder from FigCraft's .fig import and export
(crates/format/src/fig/kiwi.rs at 3b9bf8c9), moved as it is: binary schema
decode and encode, the compact text form of a schema, message decode into a
tree keyed by field name, and message encode checked against the schema.

The value budget that follows a .fig file's compressed size stays with the
.fig reader; here decode_message_within takes the budget and
DEFAULT_MAX_VALUES is the floor FigCraft used.

* Add craft-fig: the .fig container, read and written

The app-independent half of FigCraft's .fig import and export
(crates/format/src/fig at 3b9bf8c9): the ZIP archive with its image
budget, the chunked canvas (deflate, zlib and zstd chunks), the value
budget that follows the compressed canvas, images carried in the
message's blobs, and the vector network and geometry blob encodings.
The message stays a craft-kiwi tree read by field name; mapping it onto
a document stays in the app.

* craft-kiwi: no scan per field in schema decode; the text form keeps the limits

Schema::decode compared each message field id with every earlier one: 20
definitions of 10 000 fields, within every cap, took a billion
comparisons. A set per definition now.

Schema::parse applies the definition and field caps of the binary form,
looks type names up in an index instead of scanning the lines per field,
accepts a struct or message without fields, and refuses two fields of one
name and a message field id that is 0 (it ends a message) or repeated.
2026-11-11 18:08:28 -05:00
Dylan M. Taylor a1e9eb8a31 Add craft-assuan: the Assuan client and gpg-agent operations from MailCraft (#902)
Moved from MailCraft's crates/assuan at a44a45013d1c. The code is unchanged apart
from the crate name and doc wording that named the app. The S-expression reader,
socket-file reader, limits and all 60 tests come with it.
2026-11-11 18:08:00 -05:00
Dylan M. TaylorandDaniele Mori 97bb39ca15 Add craft-xls: Excel 97-2003 (.xls, BIFF8) workbook reader, from GridCraft (#878)
* Add craft-xls: Excel 97-2003 (.xls, BIFF8) workbook reader

Moved from GridCraft's gridcraft-xlsx crate (crates/xlsx/src/xls.rs and
xls/ at 5cf52c5f), where it read into GridCraft's own workbook model.
Here it reads into a model of the file's own terms: cells, formulas as
A1 text, one CellFormat per XF, ColInfo and Row records in file units,
defined names, print areas as formula text.

The container is read with craft-cfb. RString cells and the fixture
writer in craft_xls::testing come from WordCraft's mail-merge reader
(crates/formats/src/sheet/xls.rs).

* craft-xls: report the sheet the workbook was saved on

Window1's itabCur, counted over every BoundSheet8 record, as
Workbook::active_sheet (an index into the worksheets that were read).
Whether to open on a hidden sheet is the caller's choice.

Ported from storytold/gridcraft#3551.

---------

Co-authored-by: Daniele Mori <3956663+Morzilla@users.noreply.github.com>
2026-11-11 18:07:19 -05:00
Dylan M. Taylor 991ea7dbd6 Add craft-csv: delimited text and SYLK, from GridCraft (#889)
Moved from GridCraft at 5cf52c5f: the encoding detection, delimiter
sniffing, record parser and field quoting of crates/xlsx/src/csv.rs,
and the SYLK reader of crates/engine/src/io/sylk.rs.

Fields stay text and the SYLK reader returns its own Sheet; typed
values, dates and the workbook stay in GridCraft.
2026-11-11 18:07:00 -05:00
Dylan M. Taylor afcc37cf20 Add craft-penpot: the Penpot export container, read and written (#916)
The archive half of FigCraft's Penpot import and export
(crates/format/src/penpot.rs and penpot/embedded_fonts.rs at 3b9bf8c9):
ZIP validation, the manifest, bounded JSON members with the shared byte
and value budgets, pages, page shapes, media objects, font signatures,
and the archive writer. Members are handed out as the JSON the file
holds; mapping shapes stays in the app.

Duplicate member names are now refused: the ZIP reader keeps one member
per name, so the per-name check FigCraft has never fires. The directory's
own member count is compared instead.
2026-11-11 18:06:22 -05:00
Dylan M. Taylor b249d84e92 Add craft-sketch: the .sketch container, read (#912)
* Add craft-sketch: the .sketch container, read

The archive half of FigCraft's .sketch import
(crates/format/src/sketch.rs at 3b9bf8c9): detection, the version cut
(Sketch 48 and newer), document.json with its fallback, the pages in
document order, images, and the budgets on member size, compression
ratio, totals and page count. The document and the pages are handed out
as the JSON the file holds; mapping the layers stays in the app.

* craft-sketch: credit line in the README
2026-11-11 17:46:57 -05:00
Dylan M. Taylor f806e149bb Add craft-kiwi: the kiwi binary schema format, decoded and encoded (#903)
* Add craft-kiwi: the kiwi binary schema format, decoded and encoded

The generic kiwi decoder and encoder from FigCraft's .fig import and export
(crates/format/src/fig/kiwi.rs at 3b9bf8c9), moved as it is: binary schema
decode and encode, the compact text form of a schema, message decode into a
tree keyed by field name, and message encode checked against the schema.

The value budget that follows a .fig file's compressed size stays with the
.fig reader; here decode_message_within takes the budget and
DEFAULT_MAX_VALUES is the floor FigCraft used.

* craft-kiwi: no scan per field in schema decode; the text form keeps the limits

Schema::decode compared each message field id with every earlier one: 20
definitions of 10 000 fields, within every cap, took a billion
comparisons. A set per definition now.

Schema::parse applies the definition and field caps of the binary form,
looks type names up in an index instead of scanning the lines per field,
accepts a struct or message without fields, and refuses two fields of one
name and a message field id that is 0 (it ends a message) or repeated.
2026-11-11 17:46:14 -05:00
Dylan M. Taylor 0984f412a6 craft-affinity: bounded reader for native Affinity documents, from VectorCraft and PhotoCraft (#914)
* craft-affinity: bounded reader for native Affinity documents, moved from VectorCraft

The reader of .af, .afdesign, .afphoto and .afpub files (archive, object stream and document
model) as it is in storytold/vectorcraft at 6e873b80 (crates/affinity), under a neutral name.
Its corpus tests read the pinned public documents scripts/fetch-affinity-corpus.sh fetches.

* craft-affinity: fold in what only PhotoCraft's copy of the reader had

From storytold/photocraft at 63b641b5 (crates/affinity), onto the VectorCraft copy:

- Special shapes: clouds, hearts, cogs, callouts, arrows, double stars, tears, crescents,
  diamonds and segments come in as their own outlines, not their bounding ellipse.
- Null slots of object lists are charged to the document's work before a list is scanned,
  on every visit of a shared object (children, attachments, effects, compound operands,
  page lists, paint lists, gradient colours, text blocks and runs, table cells, tile lists).
- A colour or gradient position record is decoded only as far as its components are read.
- 16-bit and 8-bit planes are interleaved through tables, not an f64 for each pixel.
- Limits::max_pixel_bytes: the caller sets what the pixel layers of one import may hold.
- Pixels::cmyka8_to_rgba8 for a caller without colour management.

The crate README and docs describe the model on its own terms.

* craft-affinity: a callout with a right-to-left box no longer panics; an oversized shape imports as its box

Ported from storytold/photocraft#10473 by DJHoltkamp. A callout rectangle clamped its tail
with clamp(x0, x1), which panics when the box is stored right to left. A box with finite
corners whose size overflows gave infinite or NaN outline points; it now imports as its box
with a warning. Also drops the inflated doc.dat once it is parsed.
2026-11-11 17:42:11 -05:00
Dylan M. TaylorandEthan Dale 2144a8c48c craft-mesh: FBX reader (6.x and 7.x, binary and ASCII) (#906)
* craft-mesh: FBX 7.x reader from EffectCraft (`fbx`)

EffectCraft's FBX reader (crates/model/src/fbx.rs: binary with 32- and 64-bit
record offsets and zlib arrays, ASCII, geometry layer elements, the Model
transform chain, Lambert/Phong materials, textures) onto the crate's Scene.
The reader was written by vangel76 (EffectCraft #736); its memory budget and
shared instanced meshes by DWAA1660 (EffectCraft #1234).

It includes the fixes of the open EffectCraft PR #2955: a binary child record
must end within its parent, a failed array inflate reports only its status,
textures that end at one image share it and its bytes are charged to the
budget, and material slots and connections are found through maps.

What differs from the app's copy, because the model here is neutral:
polygons stay whole and layer elements stay indexed pools (no vertex
unification, no computed normals or tangents); images are bytes, not decoded
pixels; the axis system is not applied (Document::axes_to_y_up gives it);
the memory budget is Limits::max_data_bytes; a file without meshes is a
scene, not an error. base64_decode moves from the gltf module to the crate
root so both formats share it (gltf re-exports it).

* craft-mesh: read FBX 6.x

FBX 6.1 files differ from 7.x in how the scene is laid out, not in the
record syntax: a Model holds its own Vertices and layer elements, properties
are Properties60/Property (no label field), Connect lines name objects by
Class::Name instead of by id, and GlobalSettings sits among the objects.
The reader now takes all four, and NormalIndex beside NormalsIndex, so 6.x
geometry, transforms, the hierarchy and materials are read. 6.x textures
(connected to Models) are not.

Ported from storytold/deckcraft#1380 (crates/mesh/src/fbx), whose reader
took 6.1 to 7.7; its mesh building, axis turn and rasteriser stay in
DeckCraft.

---------

Co-authored-by: Ethan Dale <ethandale@Ethans-MacBook-Pro.local>
2026-11-11 17:35:28 -05:00
Dylan M. TaylorandNetterSilver 2f8f9ad8c9 craft-timeline: exact media time, reports, ALE and CMX 3600 EDL (from FilmCraft) (#911)
* craft-timeline: exact media time, reports, media paths, ALE and CMX 3600 EDL

New crate for editorial timeline interchange (#782), moved from FilmCraft at
4bec22bd:

- time: filmcraft-time whole (Tick, FrameRate, TimeRange, SMPTE timecode,
  feet+frames, typed-timecode parsing) with its tests.
- report: Report / Level / ReportEntry.
- path: file URL and path conversion, relative references joined to a folder.
- rate: timebase+ntsc and floating rates to exact rates, MAX_RATE.
- ale: AleDoc, parse, write, standard heading and columns.
- edl: the parser (events, lines, timecode fields, M2, AUD, comment extensions),
  the frame-rate guess, and the line layout of the writer, on frame counts.
- Format, detect_text, decode_text.

Each format module gives the document as the format writes it. Mapping onto an
application's timeline stays in the application.

* craft-timeline: strip a UTF-8 byte order mark when parsing ALE files

An ALE saved with a BOM was refused as "not an ALE file": the mark made the
first line something other than "Heading".

Ported from storytold/filmcraft#4531.

---------

Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
2026-11-11 17:32:53 -05:00
Dylan M. Taylor 5375f49ffc Add craft-plotter (HP-GL, DMPL, GP-GL); craft-gcode: bare output and Grbl replies (#863)
* craft-gcode: bare output for line-by-line senders, and Grbl replies

Writer::bare writes the same moves as Writer::new with every line a
command: no comments, coordinates at up to three decimals without
trailing zeros, M5 at the start, and the return to the origin at the
end. A sender that waits for an ok after each line can stream it as is.
Writer::set_feed changes the feed rate between contours.

The new grbl module has the real-time command bytes (status query, feed
hold, cycle start, soft reset) and the parsers of the controller's
answers: the state of a status report (read up to MAX_REPLY_LEN bytes)
and the ok / error / ALARM acknowledgement of a line. No I/O.

* Add craft-plotter: HP-GL, DMPL and GP-GL from a job in millimetres

One crate for the command languages of vinyl cutters and pen plotters,
one module per language: hpgl, dmpl, gpgl (plain) and gpgl::silhouette
(the Silhouette dialect in its three generations, with registration
marks, the device queries and the parsers of their replies).

A Job is ordered operations: polylines in millimetres plus tool
settings. Each encode gives a Program: the commands, their framing, and
where a sender waits for the registration marks. The crate does no I/O
and builds for wasm.

A job is refused past 64 operations, 2,000,000 points, 10 passes or a
2 mm overcut, or when a value is not finite. Replies are read up to 256
bytes and report at most 8 tool holders. Every limit has a test, and
tests/golden.rs fixes the exact commands of each language.

The README records where the protocol facts come from.

* craft-plotter: optional serde for Mat, as its short name

A caller that keeps a mat in a settings file or a machine table needs to
read and write it. The serde feature serializes Mat as its short name
("12x24") and refuses an unknown name. Off by default; the crate still
has thiserror as its only required dependency.
2026-11-11 17:29:14 -05:00
Dylan M. TaylorandSimon Koch-Sultan 9ab7768771 Add craft-rtf: RTF tokenizer, code pages, and a reader and writer for a small model (#890)
* Add craft-rtf: RTF tokenizer, text decoding, and a reader and writer for a small model

The tokenizing rules, code pages, multibyte decoding, dates and escaping come
from WordCraft's RTF reader and writer. The model (paragraphs with outline
level and indent, character formatting, tables) covers what DesignCraft's text
import, DeckCraft's outline import and export and PdfCraft's export use.

An unpaired UTF-16 surrogate in \u reads as U+FFFD and a high surrogate pairs
only with the very next \u, as storytold/wordcraft#6105 describes for Word.

* craft-rtf: a symbol font's bytes are its own codes

A font with \fcharset2 (Symbol, Wingdings) has no letters: a byte from 0x80
reads as U+F000 + the byte instead of a letter of the document's code page.
The symbol page applies to body text only, a symbol default font's page does
not apply to text in a font chosen with \f, \plain makes the default font's
page current again, and a code page an entry names with \cpg is kept.

Ported from storytold/wordcraft#6014.

* craft-rtf: alignment, indents, spacing, superscript and subscript; NUL is not text

The model and both directions gain what DesignCraft's RTF export writes:
\ql \qc \qr \qj, \ri, \fi, \sb, \sa, \super, \sub, \nosupersub.
A \u0 or \'00 no longer puts a NUL into the text.

---------

Co-authored-by: Simon Koch-Sultan <126534658+scks1@users.noreply.github.com>
2026-11-11 17:26:57 -05:00
Dylan M. TaylorandTed Mao 305bc3c971 Add craft-gainmap: ISO 21496-1, hdrgm and Apple gain maps, from LightCraft (#904)
* Add craft-gainmap: ISO 21496-1 and hdrgm gain map metadata, compute and apply, from LightCraft

Moved from lightcraft-codecs' gainmap.rs at 8ef35987 with its tests: the metadata reader and
writer and the arithmetic. The JPEG container (MPF index, two encoded images) stays in the app.

* craft-gainmap: Apple legacy gain-map reconstruction

Apple's legacy single-channel gain maps interpolate a linear gain ratio, not the log2 gain of
ISO 21496-1: apple::apply rebuilds the HDR rendition from a decoded base and the map, with the
headroom taken from the maker-note pair or the HDRGainMapHeadroom tag, checked inputs and
pixel-centre sampling.

Ported from storytold/lightcraft#3760 (which carries #1978).

---------

Co-authored-by: Ted Mao <tedmao@gmail.com>
2026-11-11 17:07:37 -05:00
Dylan M. TaylorandMichael 5b94fc7764 Add craft-epub: EPUB packages, written (EPUB 3) and read (EPUB 2 and 3) (#893)
* craft-epub: read EPUB 2 and 3 packages (container, package document, bounded entry reads)

The container layer of WordCraft's EPUB import (storytold/wordcraft#3119): container.xml,
the package document's metadata, manifest and spine, href resolution, and entry reads inside
per-entry and total byte budgets. Moved onto craft-xml, read strictly, and extended to EPUB 2
packages (NCX path, cover meta, dc-metadata).

* craft-epub: write EPUB 3 packages (reflowable and fixed layout)

The packaging of DesignCraft's EPUB export (crates/epub/src/package.rs at 513f77b8): the stored
mimetype, container, package document, nested navigation, fixed-layout metadata and the
derived identifier, behind a neutral Book of metadata, resources, spine and navigation. Adds
the dc:subject and dc:description metadata, the W3CDTF check and the stricter language tag
check of WordCraft's EPUB export (storytold/wordcraft#2815).

---------

Co-authored-by: Michael <monashmichaelmu@gmail.com>
2026-11-11 17:00:41 -05:00
Dylan M. Taylor ee78a9bd46 craft-psd: Photoshop PSD/PSB reader and writer, moved from PhotoCraft (#875)
PhotoCraft's `photocraft-psd` crate (crates/psd at storytold/photocraft
63b641b55e54bf8f5c83efd977fdb16a2c255431) as `craft-psd`: the lossless
PSD/PSB model, reader, writer and builder, descriptors, the Photoshop
resource files that share its primitives (.abr, .asl, .aco, .act, .ase,
.grd, .pat, .atn, .csh, .shc) and the Photoshop blocks of a layered TIFF.
The crate had no dependency on any other PhotoCraft crate.

Code is unchanged apart from the crate name, craft-libs' rustfmt width,
comments that named other PhotoCraft crates, issue references (now
`photocraft#N`), and the corpus tests' directory (`CRAFT_PSD_CORPUS` or
`corpus/` at the workspace root). Tests, proptests, fuzz targets and
fixtures move with it.

Part of #778, #786.
2026-11-11 17:00:33 -05:00
Dylan M. Taylorandarnav-hiwarkar 119ac0771f Add craft-aep: After Effects project container and typed view from EffectCraft (#888)
* Add craft-aep: After Effects project container and typed view from EffectCraft

The .aep (RIFX 'Egg!') and .aepx container as a lossless chunk tree that
writes back byte for byte, and the typed, read-only view over it:
compositions, layers, properties, keyframes, effects and their settings
data, masks, shapes, text documents, markers, cameras, track mattes,
footage, project settings and the saving version.

Moved from effectcraft crates/aep/src/container at f6b4da0a with its unit
tests, the container integration tests, the aepdump and stubicc tools and
docs/aep-format.md. The match-name tables (effect_map, style_map), the
importer and the exporter are written against EffectCraft's model and
stay there. ExtractorChannels::sources takes the caller's name for a
copied channel instead of an EffectCraft constant.

* craft-aep: read the Photoshop layer a footage item shows

A footage item made from one layer of a .psd stores the layer in its
`opti` (tag `8BPS`): the layer record's index (big-endian i32 at 14, -1
for the merged image), the document's two sides (little-endian u32 at 32
and 36), the layer's bounds (little-endian i32 at 78, 82, 86, 90) and its
name (UTF-8 from 344). `Footage::psd_layer` reads them as `PsdLayer`.

The format part of storytold/effectcraft#3449; the importer's use of it
stays in EffectCraft.

---------

Co-authored-by: arnav-hiwarkar <asharnav2008@gmail.com>
2026-11-11 16:42:18 -05:00
Dylan M. Taylor d3bc790aba craft-media: MXF demuxer and writer from FilmCraft (feature mxf) (#877)
Moves filmcraft-mxf (storytold/filmcraft@4bec22b) into craft_media::mxf.
ByteSource is the crate's trait, Codec::codec_id() and
EssenceTrack::codec_params() map tracks to CodecId, and the writer names
craft-media as company and product unless the caller sets them.

Fixes two panics: EssenceTrack::sync_before on a track without picture
samples, and Timecode::format for a drop-frame start near i64::MAX.
Adds Container::Mxf, an ffprobe/ffmpeg oracle test for the reader and the
writer, and hostile-input tests.
2026-11-11 16:41:32 -05:00
Dylan M. Taylor e21d7e85f8 Add craft-idml: the IDML package layer and InDesign Tagged Text from DesignCraft (#894)
The format-only part of DesignCraft's crates/idml (at 513f77b8): the bounded, lenient package
reader, designmap includes, the package writer, the <Properties> accessors and the attribute
value codecs. DesignCraft's own XML tree is replaced by craft-xml, which adds node and attribute
caps per part. crates/textimport/src/tagged.rs moves as craft_idml::tagged behind a neutral
model of paragraphs and runs.
2026-11-11 16:41:22 -05:00
Dylan M. Taylor c684e6a759 Add craft-midi: Standard MIDI File reader and writer from SoundCraft (#884)
read_smf (formats 0 and 1, format 2 best effort) and write_smf (format 1,
960 PPQ) with the model they read into: Note, CtrlEvent, CtrlKind and
Sequence, in ticks at 960 per quarter note. serde on the model types is
behind the `serde` feature.

Moved from soundcraft crates/midi at 17d3f007: smf.rs, sequence.rs, the
error type, the SMF tests and the Sequence tests. The editing operations
(quantize, transpose, ...) and the display names stay in SoundCraft. Adds
a test for the MAX_TRACKS limit, which had none.
2026-11-11 16:41:11 -05:00
Dylan M. Taylor f46dc936a5 Add craft-opc: the OPC package layer of DOCX, XLSX and PPTX files (#883)
One crate for what WordCraft's docx::package, GridCraft's xlsx::package and
DeckCraft's pptx::opc each carried: bounded ZIP readers (every part up front,
or on demand), part names, relationships, content types and a package writer.

The declared entry count is checked from the end records before the zip crate
indexes the entries. Limits have no defaults: each format passes its own.
2026-11-11 16:41:01 -05:00
Dylan M. Taylor ccd0046430 Add craft-drawingml: units, colours, preset shapes, custom geometry and text warps (#882)
* Add craft-drawingml: units, colours, preset shapes, custom geometry and text warps

The parts of DrawingML that need no document model, moved from DeckCraft
(60f91f78): crates/color, crates/geom (preset, warp, Xfrm and the EMU
conversions) and crates/pptx/src/custgeom.rs with CustomPath from
crates/model.

The code is unchanged except that custgeom reads craft-xml trees and writes
to a craft-xml Writer instead of DeckCraft's own XML helper, and matches
elements by local name. The 59 tests of the moved code come with it; six
more cover the custom geometry limits.

* drawingml: evaluate a custom geometry's guide lists once per coordinate space

read_cust_geom rebuilt Guides and evaluated avLst and gdLst again for every
path: up to 1,000 paths times up to 10,000 guides, each name copied and
hashed.

Guides are now cached per path coordinate space (w, h). At most 16 spaces
and 4 MiB of guide names are kept per geometry; paths in further spaces are
left out. The first space is always kept, and a geometry without guides is
not capped.

Ported from storytold/deckcraft#1789. A list counts as having guides only
when it has gd children, as that PR's review asked.
2026-11-11 16:40:51 -05:00
Dylan M. Taylor a266035672 craft-svg: craft-svg-paths grows into the SVG crate, with sniffing, SVGZ and pre-parse guards (#881)
crates/svg-paths becomes crates/svg (package craft-svg); the geometry
reader is craft_svg::paths (was the crate root), color and write keep
their names. craft-svg-paths stays as a crate that re-exports them.

New, merged from the copies the apps keep:
- sniff: is_svg, is_svgz
- svgz: bounded gunzip, UTF-8/UTF-16/ISO 8859-1 decoding, compression
- guard: element nesting and entity expansion measured on the text
  (VectorCraft's import/depth.rs text pass and entities.rs)

paths: a smooth segment with no control point to reflect starts at the
current point (S after a quadratic or a close, T after a cubic or a
close), and a relative root width or height (100%, 2em) counts as
absent instead of failing the import.

Part of #778 and #789.
2026-11-11 16:40:42 -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 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 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
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
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 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
el-j 9f10d434d9 craft-video: clean-room video codecs, one feature per codec (#702–#708)
The eight-part craft-video series landed as one commit, because each part's branch contains the parts before it.

- #702 dnx: DNxHD/DNxHR decoder and encoder
- #703 vp8: VP8 decoder
- #704 prores: ProRes decoder and encoder
- #705 h264: H.264 encoder
- #706 hevc: HEVC encoder
- #707 vp9: VP9 encoder
- #708 av1: AV1 encoder
2026-11-11 08:25:23 -05:00
Aleksander HelgakerandClaude Opus 5.5 ca99f91a18 craft-audio: AAC encoder (6/10) (#444)
* craft-audio: AAC decoder

FilmCraft's `filmcraft-aac` decoder as `craft_audio::aac` behind
`aac-decode`, kept over symphonia's: the same quality (about 135 dB
SNR against ffmpeg), and it decodes 5.1, which symphonia refuses.
HE-AAC decodes as its AAC-LC core, upsampled 2x to the stream's rate.

- `TrackDecoder` decodes AAC, including ADTS-framed packets without
  extradata (MPEG-TS, raw `.aac`).
- The native read path takes AAC in MP4 and Matroska and raw ADTS
  streams (frame sync over the file, with resync after damage).
- AAC errors convert into `AudioError`.

The AAC module's shared parts (Huffman, MDCT, TNS, ICS) arrive whole
as ported; their encoder half is unused until the encoder PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 1 (#443)

Raw ADTS and TrackDecoder:
- Channel configuration 0 (a PCE in the stream, as ffmpeg's -aac_pce and
  AudioToolbox's 7.x write) decodes; it was refused.
- Frames with several raw data blocks are refused; only their first block
  was decoded, silently losing the rest.
- A frame that changes the sample rate, profile or channel configuration is
  concealed; it played at the wrong speed.
- A source that fails during the frame scan is AudioError::Io; it looked
  like the end of the file.
- A stream after an ID3 tag longer than the file head is found.
- conceal counts all the ADTS frames of a packet, and a packet of more than
  1024 frames is refused before it is decoded into memory.

AAC decoder:
- Indexed channel configurations place elements by arrival order, not tag,
  as ffmpeg does; tagged differently, channels were swapped.
- A PCE in an access unit that fails no longer changes the layout, which
  left every later packet failing.
- Non-finite samples are zeroed before the output and the overlap.

MP4: probe and decode agree: the samples' durations end the track when no
single edit does, and an edit cannot outlast the media.

The routing oracle fails on a fixture ffmpeg cannot write when oracles are
required. The README's Limits names HE-AAC v2 as mono, multi-block ADTS,
iTunSMPB, Matroska's tick precision and AAC in CAF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 2 (#443)

Corrects round 1:
- A multi-block ADTS frame after the stream has started is concealed at its
  whole length; round 1's filter skipped it, shifting the audio after it.
- Sample durations only trim an MP4's last packet: wrong ones (some count a
  HE-AAC packet's core frames) were cutting audio. Durations of 0 with an edit
  use the edit.

Raw ADTS packets now keep their headers, so TrackDecoder alone handles format
changes, multi-block frames and concealment, for apps as for files, and nested
ADTS payloads are not split again. Headers come from the frame scan's reads,
not one read per frame. The stream's format is the one most of its first frames
share, so one damaged header does not silence the file. The ID3 walk and a
packet's ADTS split are bounded.

AAC:
- HE-AAC is delayed by the SBR filterbanks' 481 core samples; it played 19.5 ms
  early against ffmpeg in every container.
- A failed access unit leaves the decoder's state as it was, so the next packet
  decodes as if it had not been there.
- reset restarts the noise generator, so a seek into PNS repeats.
- A signalled rate that is not twice the core's plays the core; it reported the
  signalled rate with the core's samples.

The decoder oracle's tolerance tightens from 1e-3 to 1e-5. Tests cover HE-AAC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 3 (#443)

Corrects round 2:
- ADTS headers are compared with the stream's format only when the first header
  gave it. With extradata the format comes from there, so ADTS in MP4 whose
  headers say something else decodes again; every packet had been concealed.
- A stream's only program config element is found in its first frames even when
  that frame's header is damaged.
- The test of wrong sample durations patched the second run's count, not the
  delta, so "durations of one" changed nothing. It now patches the delta, and
  checks a file was changed and that seeks land on the same audio.

MP4 seeks place a packet by its index when every packet decodes to the same
length, as decoding does; they used the durations round 2 had stopped trusting
for the length, and returned the wrong audio. A last delta of 0 is overridden too.

Docs: what TrackInfo says for raw ADTS, that opening one reads the whole file, and
which codecs symphonia reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 4 (#443)

- TrackDecoder takes an ADTS stream's format from the one most of the lookahead
  shares, as the file path did, so one damaged first header does not fail every
  packet of an MPEG-TS stream. open_adts now just asks it.
- MP4 samples of several ADTS frames keep the sample table's timing; a packet
  count of one frame each truncated them silently.
- Sample durations past what the packets decode to no longer over-report the
  length.
- split_adts_upto is not behind aac-decode, as split_adts is not: an aac-encode
  build in the next PR did not compile.

Removes an unreachable ID_END check and open_adts's unused core rate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 5 (#443)

No change in decoding. An aac-encode build in the next PR needs the
split_adts_upto re-export behind aac-decode, and ID_END only in tests.

Docs say the lookahead's headers give an ADTS stream's configuration, that AAC in
CAF still goes through symphonia, and that ProgramConfig is not #[non_exhaustive].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 6 (#443)

Two tests from round 1 failed on ffmpeg before 9, including CI's 8.0. Neither was
a decoding fault:
- ffmpeg 9 ends an MP4 with no edit list at its sample durations and earlier
  versions present the whole last packet, so the length is checked against the
  file's known duration, not ffmpeg's decode.
- The order ffmpeg gives channels with -aac_pce differs by version, so each decoded
  channel must match a different one of ffmpeg's, whatever the order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 7 (#443)

Reading:
- an element of another type or a tag a program config does not list is
  refused, not written to another channel; only an LFE coded as an SCE
  is still allowed, as ffmpeg allows it
- an explicit sampling rate of 0 is refused
- a packet's later ADTS frames are searched for SBR
- a multi-frame ADTS packet that fails leaves the decoder as it was, and
  one whose frames change the channel count is an error
- the whole ADTS frames before a damaged end of a packet decode
- a failed read of a raw stream's first frames is an I/O error
- an MP4 sample of several ADTS frames past the lookahead keeps its length
- an Ogg file with leading bytes keeps its track order and mask comments

Writing:
- ProgramConfig::write, AudioSpecificConfig::to_bytes and
  AdtsHeader::write refuse what their fields cannot hold

Tests:
- the routing test checks one alignment for the ffmpeg version under test
- a default decode past an Ogg stream symphonia has no track for

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 8 (#443)

From wanikhawar's review:
- AudioSpecificConfig::parse reads backward-compatible SBR and PS signalling
  (a 0x2b7 sync extension after the config), which craft-media's Matroska
  demuxer writes for legacy HE-AAC IDs. Without lookahead those streams decoded
  as their core.
- An MP4 media edit after one empty edit is applied: its priming skip and its
  end. The empty edit's delay is not added as silence.
- A packet lost whole is concealed as long as the last one decoded, so a stream
  of multi-frame ADTS packets keeps its timing.
- ADTS-framed MP4 samples keep their sample table's timing, instead of every
  sample being read at open to look for several frames in one.
- AdtsHeader::write refuses an object type, sampling frequency index or channel
  configuration its fields cannot hold; it masked them into other values.
- AudioSpecificConfig::lc no longer panics for a sampling frequency index with no
  rate, and the decoder refuses such a config.
- AudioSpecificConfig::to_bytes refuses channel configuration 0 without a program
  config element, which would not parse back.
- Upsample2x starts each channel's history afresh when the channel count
  changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 9 (#443)

From CodeRabbit's review of round 8:
- AudioSpecificConfig::parse consumes extensionFlag3 after the config when
  extensionFlag is set, so the 0x2b7 sync extension after it is found.
- AudioSpecificConfig::to_bytes refuses a sampling rate of 0, which would not
  parse back.
- ADTS-framed HE-AAC in MP4 whose sample durations count core frames is not cut
  to the durations' half length: durations short of one frame per sample no
  longer end an ADTS-framed track, and its length is unknown until decoded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 10 (#443)

ADTS-framed AAC in MP4: durations short of the frames in the samples read
ahead no longer end the track. With HE-AAC samples of two ADTS frames each,
timed as two core frames, the durations matched one output frame per sample,
so the undercount check passed and the track stopped at half its length,
which probe reported as known. The frames each read-ahead sample holds are
now counted, and durations short of them leave the length unknown until
decoded, as durations short of one frame per sample already did. Correctly
timed AAC-LC samples of several frames keep their end trim.

Reported by FredApps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 11 (#443)

Corrects round 10. A short ADTS-framed MP4 track, whose last sample is in
the read-ahead, lost that sample's trim when it cut more than one frame:
the trim counted as durations falling short, so the length was left to
decoding and its padding was presented. The track's last sample is now left
out of the read-ahead comparison.

Reported by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: document the ADTS-in-MP4 seek and edit limits (#443)

AAC in MP4 stored as ADTS frames, with sample durations that count fewer
frames than the samples hold, decodes in full from the start, but seeks
place samples by those durations and land later than asked, and an edit
list measured in them can end the track early. The README's Limits section
now says so, and the seek example points to it.

Reported by FredApps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: say why the ADTS-in-MP4 seek limit is kept (#443)

The Limits entry from 79f5839 said what goes wrong but not why it is left.
It now says that such a file is outside the MP4 specification: an MP4 AAC
sample is one raw access unit with the configuration in the sample
description (ISO/IEC 14496-14, 14496-3), so ADTS headers do not belong in
it, and a sample's duration is the time it decodes to (ISO/IEC 14496-12).
It says why files within the specification are not affected, what still
works, and why seeks and edits in such files are not corrected: finding
where each sample's audio starts means counting the ADTS frames in every
sample before it, one read per sample, which would slow every ADTS-framed
MP4 at open to work around files outside the specification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: AAC encoder

FilmCraft's AAC-LC encoder behind `aac-encode`: psychoacoustic model,
window switching, M/S, TNS, bit reservoir, CBR and VBR, 1-8 channels,
optional ADTS framing. `rayon` encodes channels in parallel.
`aac::aac_order` maps WAV order to the encoder's input order. Settings
the encoder cannot use are `aac::Error::InvalidSettings`, which
converts to `AudioError::Encode`. `encode` now turns on `aac-encode`.

Tests that need AAC streams written by our own encoder come with it:
the encoder oracle and robustness tests, packet concealment, and
`TrackDecoder` on ADTS without extradata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 1 (#444)

From CodeRabbit's first review of the AAC encoder:

- CBR bitrate limits use the exact bits per frame (bitrate * 1024
  against channels * sample_rate) instead of the truncated quotient, so
  288001 b/s at 48 kHz mono is rejected.
- Rates below 48 bits per channel per frame are rejected. That is the
  largest silent frame per channel measured across 1-8 channels; below
  it the encoder overshot the requested rate (96 kHz mono at 1000 b/s
  came out near 4500 b/s).
- Fill elements are capped so the byte-aligned access unit stays within
  6144 bits per channel. write_fill can write 8 bits over its request,
  which gave 769-byte mono units at the maximum rate and when the
  reservoir was flushed on the last frame.
- encode() buffers one frame of input at a time. Appending the whole
  slice and draining 1024 samples per frame copied the rest of the
  input once per frame, quadratic in the slice length. Output is
  byte-identical to feeding the input in chunks.
- README: 8 channels are taken in channel configuration 7's element
  order; a WAV 7.1 buffer has no equivalent there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 2 (#444)

From CodeRabbit's review of round 1:

- The CBR minimum adds the program config's bits when ADTS is on. The
  7-channel layout repeats it in every ADTS frame, and at full
  bandwidth a silent frame took 344 bits against the 336 bits that
  31,500 b/s gives at 96 kHz. Bandwidth alone stays within 48 bits
  per channel.
- write_fill never writes more than it is asked for: each element's
  size comes from the bits left, so the total ends 0-6 bits short.
  The old version could pass the request by 8 bits and could also
  stop early (asked for 14, it wrote 7).
- The encoder fills to the unit's target end, capped at
  max_frame_bits, instead of keeping round 1's 8-bit margin. Frames at
  the maximum rate were 767 bytes when there was little to code; they
  are now exactly 768 bytes per channel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 3 (#444)

Encoder::new rejects a NaN VBR quality, the known issue ported from
FilmCraft and listed in the PR description.

NaN passes f32::clamp, so the rate-distortion multiplier became NaN.
The frame-size loop compares it with `<` and never ran, and no band
was skipped, so every frame was coded at full precision: mono units
reached 2465 bytes against the 768-byte limit, and 8-channel units of
up to 17024 bytes did not fit the ADTS length field, so they went out
without headers. Infinities and other out-of-range values are clamped
and stay within the limit; the test checks both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 4 (#444)

From CodeRabbit's review of round 3:

- The CBR fill target is rounded down to a whole byte. Round 2 filled
  to an unaligned target, so after byte alignment a unit could end up
  to 7 bits past the bits it had: in a sweep of silent CBR encodes,
  1188 of 13200 frames overspent, all of them frames with fill, hidden
  by the level clamp at 0. The unit now ends exactly at the rounded
  target. The 0-7 bits rounded off stay in the reservoir and are left
  unspent after the last frame; the comment says so instead of
  claiming an exact total.
- The NaN-quality test checks that infinities and out-of-range values
  encode exactly as the nearest end of the 0.5-6.0 range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 07:21:26 -05:00
139a39e94c cms: write the profile ID into encoded profiles (#749)
encode() stamps the header profile ID: the MD5 of the profile with the
flags, rendering-intent and ID fields zeroed (ICC.1:2022 7.2.18).
profile_id(bytes) computes it for any profile. Adds md-5 (RustCrypto,
MIT OR Apache-2.0, no default features); digest was already in the lock.

Closes #748.

Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 07:17:22 -05:00
Dylan M. Taylor 73702b326a Add craft-silhouette-studio: Silhouette Studio document reader (#805)
Bounded, fail-closed reader for .studio and .studio3 documents: outlines, line colours and page size in millimetres. Protected designs are refused.
2026-11-11 06:40:49 -05:00
Dylan M. Taylor 3dcaa07a81 Add craft-svg-paths: SVG geometry reader (#802)
Reads an SVG's shapes, clips and page size into kurbo paths in millimetres, with a bounded CSS cascade and fail-closed handling of unsupported content. The caller maps style to its own shape kinds.
2026-11-11 06:23:12 -05:00
Dylan M. Taylor 34685c4cdc Add craft-gcode: Grbl-style G-code writer for laser cutters and plotters (#801)
Polylines in millimetres to G0/G1 moves with laser power, feed and passes. Comments are kept free of Grbl real-time bytes.
2026-11-11 06:22:20 -05:00
FredApps 60ede99985 print: xps_driver says whether a Windows printer's driver renders XPS itself (#804)
* print: xps_driver says whether a Windows printer's driver renders XPS itself

craft_print::xps_driver(printer) reads PRINTER_DRIVER_XPS through winprint's new
PrinterDevice::is_xps_driver (storytold/winprint.rs#21): Some(true) for v4 drivers and v3
drivers with an XPS rendering filter, Some(false) for GDI drivers, None when it can't be read and
on other systems. On a GDI driver, reached through Windows' XPS-to-GDI conversion, a page with
transparency prints with halos, as black boxes or not at all (storytold/pdfcraft#6155, #5838,
#5940), so PdfCraft will flatten it for those drivers only.

The Windows end-to-end test checks the CI queue's "Generic / Text Only" driver reads as GDI.

* Test: the GDI-driver check in a helper (keeps the end-to-end test under the complexity limit)

* winprint bc06a7c48b768eb27a828c7986f6d2868b245ee0: storytold/winprint.rs main with #21 (PrinterDevice::is_xps_driver)
2026-11-11 07:48:44 +01:00
.CooCoo 5a93050c9f update: integration of the open fixes to craft-update (#763)
* update: an unsigned Windows copy gets Download instead of Update and Restart

The update helper installs nothing for an unsigned copy, so the prompt's
Update and Restart button on a local or unsigned build downloaded the
release and then stopped with "this copy isn't signed". is_self_updating
now answers false for an unsigned copy on Windows, and the prompt offers
Download. CRAFT_UPDATE_DEV still offers the update.

* update: check that a Windows copy can install before downloading

windows_precheck ran after the download, so a copy that could not install
downloaded the whole release first. The checks that do not depend on the
download now run before it; a new MSI's signature is checked after it.
The cache folder is created first, so a first update passes the check.

* update: a failure recorded before the running program is not reported

take_outcome reported any recorded failure at the next start, so a copy
put in place after an update had failed still opened with "The update
could not be installed". A failure record older than the running program
is now removed without being reported. A failure recorded after the
program was put in place, such as a failed helper's, is still reported.

* update: a Cargo build with a separate build directory is a development build

With build.build-dir set, Cargo keeps .rustc_info.json and the profile's
.fingerprint, deps and build folders in the build directory, and the
program's own folder has only its lock file. Such a build counted as an
installed copy and was offered Update and Restart. A profile folder with
Cargo's .cargo-lock under a target root with CACHEDIR.TAG is now a
development build, and gets the download link.

* update: a cache folder that can't be created is reported as that

same_cache_as_helper ignored the error from creating the folder, so a
failure read as a cache in the wrong place. It now names the folder and
the error.

* craft-update: a slow macOS signature check is not reported as a bad signature

`signed_by` gave `codesign` 120 seconds and treated every failure as a bad
signature, so a Mac that could not finish in time was told the app "isn't
signed by its developer".

The check now has 10 minutes and checks one bundle at a time. A job waiting
for its turn reports `JobPhase::Waiting` and can be canceled. A timeout, a
`codesign` that cannot start or is stopped by a signal, and any exit other
than 1 or 3 report that the signature couldn't be checked. Exit 3 refuses the
app as not signed by its developer. Exit 1 refuses it with `codesign`'s own
reason, because `codesign` also exits 1 for a bundle it cannot read. The app
is not installed in any of these cases.

On macOS, an install or uninstall now stops when `pgrep` gives no answer,
where a timeout used to read as "not running". `pgrep` has 30 seconds. An
install looks again after the signature check, which can wait behind other
apps' checks, and does not replace an app opened during the wait. A
self-update skips that second look.

* craft-update: a copy that cannot be replaced gets Download

A macOS bundle, an AppImage or a Linux standalone program was offered
Update and Restart even when the update could not replace it, so the
update failed only after the whole download.

`current_at` now leaves out such a copy when `access(2)` refuses what
the update needs. On macOS that is adding a subdirectory to the parent,
and adding a subdirectory to and deleting the bundle. On Linux it is
writing and searching the folder. A macOS copy under App Translocation
is also left out. Any other error from the check leaves the update on
offer, and `CRAFT_UPDATE_DEV` still offers it.

* craft-update: a macOS standalone copy that cannot be replaced gets Download

macOS `detect` returns `Install::Standalone` for a loose program, and
the update replaces it by writing beside it, as on Linux. `can_replace`
now checks that folder on macOS too, so a copy in a folder the user
cannot write gets Download instead of failing after the download.
`CRAFT_UPDATE_DEV` still offers the update.

* craft-update: a standalone copy whose share files cannot be replaced gets Download

The update replaces a standalone copy's files under `share` as well as
the program when the app's `share/<id>` folder sits next to `bin`. The
offer now also checks those folders, the `applications` and
`icons/hicolor` folders a release writes to, and the nearest existing
parent of one that doesn't exist yet.

* craft-update: a standalone macOS program without a release tarball gets Download

On macOS a program outside an app bundle counts as a standalone copy, and
the self-update replaces it from a macOS .tar.gz. Releases carry a disk
image and a command-line zip for macOS and no .tar.gz, so the prompt showed
Update and Restart and the update stopped with "has no Tarball (.tar.gz)
for macOS".

The prompt now also asks release_installs, which on macOS offers Update and
Restart to a standalone program only when the release lists that asset.
Without it the prompt shows Download. A release whose file list was not
loaded keeps the offer, because the install fetches the full release first.
CRAFT_UPDATE_DEV still offers the update. Other platforms and other kinds of
install are not checked against the release.

* craft-update: CRAFT_UPDATE_DEV no longer skips the release check

A standalone macOS program run with CRAFT_UPDATE_DEV was still offered
Update and Restart for a release without a macOS .tar.gz, and that update
failed. release_installs now checks the release for that copy too.

release_installs_at takes the executable, the opt-in setting and the
platform, and a new test checks a loose program in a temporary folder
against releases with and without the .tar.gz. The tests use SoundCraft's
release files, since craft-launcher ships no command-line zip.

* craft-update: a failed self-update keeps its download for the next try

On macOS, Linux and FreeBSD, prepare_update removed the downloaded
release file whether or not the install succeeded, so every retry after
a failed copy, signature check, package install or swap downloaded the
whole release again. A failed install now keeps the file, as install_at
already does, and the next try reuses it through Http::download, which
checks it against the published SHA-256 first and fetches it again when
it does not match.

- The cache folder's self-update-download.txt records the name and size
  of the release file before the download starts. A self-update for a
  different release file removes the finished and partial download that
  the record names, so each app keeps the files of one release at most.
- An install that succeeds removes the file and the record. So does a
  failed install whose record could not be written, since nothing would
  remove that file later.
- On macOS, when hdiutil cannot attach the disk image, the file is still
  removed: an image left attached by an install that ended part way
  makes hdiutil refuse the same file with "Resource busy", while a new
  download of it attaches.
- Windows is unchanged: its update helper removes the download after
  the app exits.

* craft-update: a macOS install that stopped partway no longer blocks the next install

When a macOS install ends before it finishes (the app quits or
crashes), the copy it was making stays beside the app and the update's
disk image stays attached. The next attach of the same file fails with
"Resource busy".

- Each staging folder's name now carries the installing process's id,
  and the folder holds an empty marker file. Before it stages a copy, an
  install removes the marked staging folders of processes that are no
  longer running, and their empty unmarked ones. It also removes a
  staging folder from earlier launcher versions when it holds only the
  app's bundle and has not changed for a day. Backup folders and other
  apps' folders are left alone.
- Before it attaches an image, an install detaches the images that
  earlier installs of the same app left at their mount points in the
  cache, when their process has ended or, in this process, when no
  install uses them any more.
- When the attach fails, the install detaches its image if it is
  attached with no volume mounted and no other attach of the same file
  is running.
- A staging folder that can't be removed, or an image that stays
  attached, is logged, and the error of a failed install says so after
  its reason.

* craft-update: the kept-download test accepts either signature refusal

An unsigned bundle is refused as "isn't signed" today and as a failed signature check once codesign's exit status is read, so the test checks that the install stopped on its signature.

* craft-update: the share check covers nested and pixmaps folders

A standalone copy in a prefix gets Update and Restart only when the
update can write every `share` folder it replaces files in. The check
covered `share/<id>`, `share/applications` and `share/icons/hicolor`
only, but the update writes a copy of each file next to its
destination first. A folder inside `share/<id>` (such as `models`), an
icon folder such as `icons/hicolor/256x256/apps`, or `share/pixmaps`
(where an older flat release puts its icon) that the user can't write
stopped the update after the whole download.

The check now also takes in every folder inside `share/<id>`, each
`icons/hicolor/<size>/apps` folder that holds the app's icon, and
`share/pixmaps`. Icon folders of other packages and other themes are
not checked. A linked folder is checked but not walked. The walk reads
at most 4,096 folder entries, so a large tree does not slow every
start; a folder past that is reported by the install as before.

The same check runs for a standalone copy on Linux, so a Linux copy
whose nested share folder or `share/pixmaps` it can't write now gets
the download link too.

* craft-update: a release folder that can't be read stops a standalone update

A standalone update in a prefix copies every file under the release's
`share` folder. The walk skipped any folder it could not list, and the
archive keeps each folder's mode, so a folder that nobody may list left
its files out while the update reported success. An error reading an
entry of the release's `bin` folder was dropped the same way.

These are now errors that name the folder, and the update stops before
any installed file changes. A release without a `share` folder still
updates.

A release whose `share` or `bin` is a link was followed, so the update
copied files from outside the release into the prefix. A `share` or
`bin` that is a link, or a `share` that is a file, now stops the update.
Links further down under `share` are still skipped.

The unpacked release is removed after a failed update even when it holds
a folder that its owner can't list. Before, such a folder stayed in the
cache.

* craft-update: the share check covers doc, metainfo and mime, and follows links

The share check for a standalone copy in a prefix now also takes in
`share/doc/<id>` and every folder inside it, `share/metainfo` and
`share/mime/packages`. Release archives such as VectorCraft's Linux and
FreeBSD ones put files in all three.

The walk now follows links, as the update's writes do. A copy whose
`share/<id>` is itself a link had none of its folders checked, and a
folder behind a link inside `share/<id>` was not checked either. A
folder the walk reaches twice is read once.

When `doc/<id>`, `metainfo`, `mime/packages` or `pixmaps` is missing,
the check no longer falls back to `share` itself. A copy whose `share`
the user can't write, but whose app, `applications` and `icons/hicolor`
folders exist, gets Update and Restart again, as it did before the
previous commit.

The entry limit of 4,096 now applies to each of `share/<id>`,
`doc/<id>` and `icons/hicolor` separately, and it counts files and
folders. A large app folder no longer turns off the icon folder check.

In a folder with the sticky bit, such as `/tmp`, the user can rename
over a file only when the user owns the file or the folder. A copy now
gets the download link when such a folder holds one of the app's files
that another user owns: the program itself, its other programs
(`<id>-<name>`, such as `<id>-cli`), anything in `share/<id>` or
`share/doc/<id>`, or a `share` file named `<id>.<ext>` or
`<bundle id>.<ext>`.

An icon size that the installed copy doesn't have yet is still not
checked, and a read-only folder that the user added under `share/<id>`
does count. Linux runs the same checks for a standalone copy, and the
sticky check on the program file for an AppImage.

* craft-update: the app-program check has its own name

The check for the app's programs in a bin folder was named is_program, the name of the Windows check for an MZ header, so the crate did not build on Windows. It is now is_app_program.

* craft-update: a missing doc, metainfo or mime folder is checked through share

A Linux release writes files into `share/doc/<id>`, `share/metainfo`
and `share/mime/packages`, and the update creates each of these
folders when it is missing. When one was missing, the share check took
its nearest existing parent inside `share` and never `share` itself. A
copy whose `share` the user can't write and that lacked one of these
folders was offered Update and Restart, and the update failed after
the whole download.

For a missing `doc/<id>`, `metainfo` or `mime/packages`, the check now
takes the nearest existing parent with `share` included, as it already
did for `applications`. Only the old flat release layout writes to
`share/pixmaps`, so a missing `pixmaps` is still checked inside
`share` only, and a copy whose `share` the user can't write keeps
Update and Restart when every other release folder exists.

A folder in the app's folder or in `doc/<id>` that the user can't
read is still checked against its own permissions, as before. The
copy gets Download when the user can't write and search that folder.
The check can't list the folders inside it and does not check them.
A new test keeps this rule, and the check does not refuse a copy only
because a folder in it can't be read.

* craft-update: the staging cleanup's doc says which folders it removes

A marked staging folder is removed once its process has ended,
whichever app that install was for: the folder's name does not record
the app. Only staging folders of earlier launcher versions are checked
for the app's bundle.

* craft-update: a failed attach leaves its image attached when pgrep gives no answer

detach_unmounted, from #766, checked for another attach of the same
image file with is_running, which reads a pgrep that gives no answer as
"not running", and then force-detached the image's unmounted
attachments. It now uses process::running, from #753, and detaches only
when the check answers that nothing is running.
2026-11-11 00:07:01 -05:00
Dylan M. Taylor 006e4df225 Add craft-dxf: DXF tag reader and writer, from CADCraft (#796)
Moved from cadcraft-dxf (cadcraft 2f8f1861) unchanged apart from names and docs:
ASCII and binary DXF as group-code tags, sections and entities, pre-2007
code pages, versions, and writing. VectorCraft's hand-written DXF writer
can move onto it next.

Part of #778, #791.
2026-11-10 20:25:03 -05:00
Dylan M. Taylor 3b3104981b Add craft-tiff: TIFF and BigTIFF IFD reader and writer, from LightCraft (#795)
Moved from lightcraft-tiff (lightcraft b188295e) unchanged apart from names
and docs. The first step of shared RAW support (#783, #587): LightCraft's raw
and metadata crates read through it, and PhotoCraft's and FilmCraft's own
TIFF parsing can follow.

Part of #778.
2026-11-10 20:24:43 -05:00
Dylan M. Taylor 6e21c5b0b1 Add craft-heif: the HEIF/HEIC decoder shared by LightCraft and PhotoCraft (#794)
Moved from lightcraft-heif (lightcraft b188295e), the more complete of the
two copies: libheif-matching YCbCr conversion from the VUI, monochrome,
thumbnails and the box caps. PhotoCraft's grid-tile ICC inheritance tests
are ported to it.

Part of #778. Closes #784 once both apps switch.
2026-11-10 20:24:21 -05:00
FredApps 0e4943cb5e print: winprint 3c978dfc: pages at the job's resolution, QName ticket values, padded integers (#761)
winprint.rs#19 draws each page at the resolution the job's ticket asks for instead of ~96 dpi
(storytold/pdfcraft#4941, #2676). #12 writes QName property values as prefix:local, which
drivers such as the Microsoft IPP Class Driver otherwise reject when merging the ticket
(storytold/pdfcraft#3823). #3 accepts whitespace around xsd:integer values in
PrintCapabilities (Toshiba Universal Printer 2, storytold/pdfcraft#2430).

The public API craft-print uses is unchanged; only the rev and the lockfile move.
2026-11-10 01:55:59 +01:00
Zhihui Song 7ee35a9fbd chore(print): refresh internal winprint pin after bounded stream test repair 2026-11-08 19:07:09 +08:00
Zhihui Song e6489d9226 print: preserve the public GDI dependency identity while updating the PDF loader 2026-11-08 11:05:10 +08:00