Commit Graph
262 Commits
Author SHA1 Message Date
Dylan M. Taylorandbflatastic 428541a512 craft-sf2: SoundFont 2 reader from LoopCraft (#1076)
The SoundFont 2 reader of LoopCraft's loopcraft-audiofile crate (sf2.rs at
storytold/loopcraft@0d150c1), over a neutral model of its own: the sample
pool, presets and resolved key/velocity regions with tuning, loop points,
pan, gain and volume envelope. The fixture writer its tests use is the
test-support feature, so readers in other crates can test with real files.

Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-11-11 21:58:16 -05:00
2eddd3073e craft-raw: Sony ARW, ARW6, SR2 and lens corrections (#1051)
* 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.

* craft-raw: Minolta MRW, Epson ERF, Mamiya MEF and Kodak DCS520C

Four formats only LightCraft decodes, one feature each (mrw, erf, mef, kodak),
moved from its vendor modules at 5ff567a4 with their tests; the MRW block
container comes from lightcraft-meta. src/shared.rs holds the level helpers
the maker decoders share.

ERF and MEF state no white balance: the estimate LightCraft made from the
preview is develop-side work, so wb_multipliers is None for them here.

* craft-raw: shared scan budget for tagged previews, tile-sized lossy JPEG budget, bound on Deflate tile padding

Ported from storytold/lightcraft#3695. Candidate preview ranges are deduplicated and charged to one byte
budget and one header-walk budget; a lossy JPEG decoded for a tile may hold no more samples than the tile
rounded up to whole MCUs (and never more than Limits::max_lossy_tile_samples); a Deflate chunk whose nominal
size is mostly padding outside the image is refused before it is inflated.

* craft-raw: lossless JPEG sample bound per referenced byte; previews larger than the file are passed over

Ported from storytold/lightcraft#3910 (the parts whose code is in craft-raw today). check_image refuses a
lossless JPEG image that declares more than 32 samples per referenced byte, counted once per chunk that points
at the bytes so identical tiles stored once still decode. The Hasselblad RGB preview search passes over an
image whose pixel bytes exceed the file and still takes a smaller complete one.

* craft-raw: test for the Deflate padding bound

* craft-raw: Pentax PEF and Samsung SRW

Moved from LightCraft crates/raw (vendor/pef.rs, vendor/srw.rs) at 5ff567a4. The Huffman difference decoder
and bit reader that PEF shares with the Nikon, Panasonic and Sony codings become the huffman module.

* craft-raw: Canon CR2, with sRAW and mRAW

Moved from LightCraft crates/raw (vendor/cr2.rs, vendor/cr2/sraw.rs) at 5ff567a4. The white-balance presets a
CR2 lists come back as data in ColorData::kelvin_presets (the ordering from LightCraft's kelvin_presets.rs;
the temperature and tint arithmetic stays with the developer).

* craft-raw: an sRAW preview larger than the file is refused before its buffer is reserved

Ported from storytold/lightcraft#3910 (cr2::sraw::read_preview).

* WIP craft-raw: Nikon NEF/NRW (does not compile yet: limits threading, develop() calls in tests)

* craft-raw: Nikon NEF/NRW, with PhotoCraft's maker-note compression routing and 12-bit D4 white level

* craft-raw: Panasonic RW2 / RWL / RAW, every raw format

* craft-raw: RW2 format 8 refuses strips stating fewer bits than samples

Ported from storytold/lightcraft#3910.

* craft-raw: RW2 format 8 refuses samples above the declared bit depth

Ported from storytold/photocraft#11838.

* craft-raw: the RW2 format 8 robustness seed carries a valid stream

Its random payload decoded only while out-of-depth samples were clamped; mutations now start from a file that decodes.

* craft-raw: Fujifilm RAF, uncompressed and compressed, with lens corrections

* craft-raw: compressed RAF stripes cap their capacity hint at the stripe's bits

Ported from storytold/lightcraft#3910.

* craft-raw: test RAF orientation and camera from the embedded JPEG's Exif

* WIP craft-raw: Olympus ORF

* craft-raw: RAF embedded preview, and its range from the header alone (bounded_preview)

* craft-raw: Olympus ORF, the uncompressed layouts, with its maker-note preview and WB_GLevel

* WIP craft-raw: Sony ARW

* craft-raw: tiled lossless ARW decodes hold one group of tiles at a time

Ported from storytold/lightcraft#3910.

* craft-raw: dark 12-bit uncompressed ARWs keep Sony's 14-bit levels, and the DNG writer's depth holds the white level

Ported from storytold/lightcraft#3429.

* craft-raw: Sony ARW family, docs, seeds and the derivation records

---------

Co-authored-by: radialmonster <radial@gmail.com>
Co-authored-by: wnanbei <wnanbei@gmail.com>
Co-authored-by: lownlyrider <lownlyrider@gmail.com>
2026-11-11 21:42:48 -05:00
Dylan M. TaylorandLogan Jennings 06e949f30b craft-cfb, craft-media: hostile-input hammer for CFB, ISO BMFF and Matroska (#1066)
The hammer over two compound files (version 3 and 4, and their first 2 KiB),
the synthetic ISO BMFF files of the fuzz test, and a synthetic Matroska file
plus four muxed ones (no lacing, Xiph, EBML, fixed-or-EBML), each opened and
read through the whole API. No panic found.

Ported from storytold/filmcraft#1794 (crates/cfb/src/tests/hostile.rs,
crates/isobmff/tests/fuzz.rs, crates/matroska/src/synthetic_tests/hostile.rs).

Co-authored-by: Logan Jennings <loganwjennings@gmail.com>
2026-11-11 21:39:55 -05:00
Dylan M. Taylorandshuff57 49dfd6ddc0 craft-odf: OpenDocument Text documents on craft-flow (feature text), from WordCraft (stacked) (#1064)
* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-mime: detect, for bytes that name no character set

is_text, decode (BOM, unmarked UTF-16, UTF-8, else the legacy encoding
the bytes look like), legacy and legacy_label, behind a detect feature
that brings in chardetng. Moved from WordCraft's crates/formats/src/txt.rs
at 06c2ae6f with its text-sniffing tests. Also charset::is_known, so a
caller can tell a label decode would read as UTF-8 from one it knows.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.

* craft-codepage: one table per legacy code page, for craft-rtf, craft-dxf and craft-mime

craft-rtf and craft-dxf each carried tables for DOS 437, 737, 850, 852 and
857 (and craft-rtf one for 866). They are now one set in craft-codepage,
which takes craft-rtf's codepage module (Windows and Mac tables, OEM glyphs,
font charsets) and craft-dxf's DOS tables. craft_rtf::codepage re-exports it
unchanged; craft-dxf keeps its $DWGCODEPAGE names and Decoder and looks the
DOS tables up there.

Every byte of every code page either crate decodes is unchanged: the
public decoders of both crates were dumped over all 256 bytes of each page
(and the East Asian pairs) before and after, and the dumps are identical.

craft-mime now reads the DOS pages by their IANA names (IBM437, cp850,
ibm-852, 857...), which it read as UTF-8 before.

* Add craft-math: the equation tree with OMML, MathML, LaTeX and UnicodeMath

The math tree, LaTeX, linear-format, symbol and tree-walk modules come
from craft-wordmodel (storytold/craft-libs#963, WordCraft's crates/doc at
07f97737) unchanged, with the run colour (Rgb) and the symbol-font tables
they use. MathML is WordCraft's crates/formats/src/mathml.rs and OMML is
WordCraft's crates/docx/src/read/math.rs and write/math.rs (06c2ae6f /
07f97737), moved onto craft-xml. The HTML-specific MathML tests stay in
WordCraft; the rest moved with the code.

mathml::parse and omml::parse/read_str/write_str are new: string entry
points for callers that have no tree of their own.

* craft-math: read MathLive's and other spellings of LaTeX math commands

\abs{...} (an absolute value), \lvert \rvert \lVert \rVert, \nparallel,
\backprime, \infinity, \cross, \differentialD, \exponentialE and
\imaginaryI (MathLive's d, upright e and i), \emph, \textsf, \hbox,
\mathnormal and \underleftarrow / \underrightarrow /
\underleftrightarrow were unknown and read as literal text. They are read
as equaSHion's typed-input parser reads them (storytold/craft-libs#29).

* craft-math: equaSHion's equation corpus as test data

corpus/equations.tsv, corpus/latex.tsv and the OMML samples of
storytold/craft-libs#29 (7a6ef6b9), unmodified.

* craft-math: corpus round trips, GridCraft's OMML cases, entry points and limits tested; README

The corpus equations go through OMML, MathML and LaTeX and back; the OMML
samples are written back as kept and from the tree. GridCraft's OMML to
LaTeX test cases (crates/xlsx/src/omml.rs) give LaTeX that reads back the
same through the tree. omml::parse and read_str are tested against other
prefixes, the strict namespace, kept OMML, junk, the element cap and deep
nesting.

* Add craft-flow: the flow model the text formats map to, on craft-wordmodel

WordCraft's crates/formats/src/model (07f97737) moved as it is: Flow,
FBlock, Para, Inline, Fmt, tables and cells, pictures, to_doc and
from_doc, picture sniffing, colours, base64 and the Dropped report. Only
the paths changed (wordcraft_doc is craft_wordmodel); the report helpers
are public so the format crates can call them; the image re-encoding
helper stays with WordCraft's RTF writer.

* Add craft-markdown: Markdown read and written, on craft-flow

WordCraft's crates/formats/src/markdown (07f97737) with its 35 tests,
moved onto craft-flow and craft-wordmodel. Character references come from
craft-html. The table cell budget lives in craft-flow, which names it in
its warning. Two test lines that wrote plain text through WordCraft's txt
module stay in WordCraft.

* WIP: craft-latex sources from WordCraft (unmodified)

* Add craft-latex: LaTeX documents read and written, on craft-flow

WordCraft's crates/formats/src/latex (07f97737) with its 21 tests, moved
onto craft-flow and craft-wordmodel; bytes are decoded by craft-mime's
detect. The LaTeX parts of WordCraft's cross-format tests (nesting,
column specs, labels, no TeX commands from the document, the fuzz and
round-trip properties) come along as tests/hostile.rs. The ligature test
builds its three-font paragraph as a flow instead of reading HTML.

* craft-math: latex_text, LaTeX math and the linear format converted as text

WordCraft's crates/formats/src/latex/math.rs (07f97737): the converter its
LaTeX document reader and writer use, which keeps the source's spacing.
It moves here so craft-math is the one place that converts LaTeX math;
craft-latex calls it, and its output does not change. Its tests (from
WordCraft's latex tests and cross-format tests) come along.

* craft-math: latex_text's group finders are public, for craft-latex's length parser

* craft-latex: math through craft_math::latex_text; label timing test by ratio

The document math converter moved to craft-math (#1040) unchanged; this
crate calls it there (and its length parser uses its group finders), so
export output is the same. The label test compared one read with a
ten-second wall clock and failed on a loaded machine; it now compares the
fastest of three reads of 25,000 and 100,000 labels, which a linear read
keeps near 4x and a quadratic one near 16x.

* Cargo.lock after the merge

* WIP: WordCraft's HTML document reader and writer, unmodified (crates/formats/src/html at 07f97737)

* craft-html: HTML documents on craft-flow, behind a flow feature

WordCraft's crates/formats/src/html (07f97737) as craft_html::doc: the
tag-soup builder, CSS subset, ruby and MathML reading, page decoding and
the writer, with its 44 tests. It reads tokens from this crate's
tokenizer through a small adapter (decoded text and attributes, names in
lower case) instead of its own lexer, and its character references are
this crate's. Lexer::set_pos is new: the ruby reader hands a token back.
The HTML parts of WordCraft's MathML and cross-format tests come along as
tests/doc.rs, the linear-time check as a ratio.

* Cargo.lock after the merge

* WIP: WordCraft's ODT reader and writer, unmodified (crates/formats/src/odt at 07f97737)

* craft-odf: OpenDocument Text documents on craft-flow, behind a text feature

WordCraft's crates/formats/src/odt (07f97737) as craft_odf::text, with
its 467 tests. Moved onto this crate's own layers: packages are read with
Package and written with PackageWriter, password protection is crypt
and flat files are flat (the copies in the WordCraft module are gone),
and its XML event stream is craft-xml's pull reader instead of quick-xml.
Paths: craft_flow, craft_wordmodel, craft_math::mathml, craft_metafile.
The date helper it borrowed from the spreadsheet reader came along.
Tests read packages through Package instead of zip.

* craft-math: operator names take their LaTeX scripts directly again (restores #1019)

#1019 (22161d2d) wrote \sin^2\theta as \sin ^{2}{\theta } so it reads back as
the same tree. The squash merge of #1038, whose branch predated it, put
math_latex.rs and the corpus test back as they were before. This applies
#1019's change again, unchanged, with its test.

* Cargo.lock: craft-html's flow dependencies

---------

Co-authored-by: shuff57 <62350898+shuff57@users.noreply.github.com>
2026-11-11 21:39:13 -05:00
Dylan M. TaylorandJordi Altayó 34ba6ae4da craft-pptx: the reader (from DeckCraft), on craft-opc and craft-drawingml (#1057)
* 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.

* drawingml: the model types for fills, lines, text, tables, charts and diagrams

Moved from DeckCraft (60f91f78) crates/model/src: style.rs, text.rs,
table.rs, chart.rs, numfmt.rs and trendline.rs, plus Diagram, DiagramNode
and the MediaId and SlideId newtypes from lib.rs.

The files are unchanged except that deckcraft_color:: is crate::color:: and
Fill and TableBorder allow clippy::large_enum_variant. Their 60 tests come
with them. Readers and writers follow.

* drawingml: the theme model (colour, font and format schemes)

Moved from DeckCraft crates/model/src/theme.rs at 60f91f78: FontSet, ScriptFont, FontScheme,
FormatScheme, Theme and default_format. The app's theme gallery, its variants and its own default
theme stay in the app. Theme::default() is a plain theme built in place, so nothing here depends
on a gallery.

* drawingml: readers and writers for fills, lines, text, charts, themes, table styles and diagrams

Moved from DeckCraft crates/pptx/src at 60f91f78: read/dml.rs, write/dml.rs, read/chart.rs,
read/chartex.rs, write/chart.rs, read/theme.rs, write/theme.rs, tables.rs and dgm.rs, onto
craft-xml. Package lookups (Imp, Part, Exp, Out) are the caller's part::Source and part::Sink.
The chart frame budget and kept-part capture stay with the presentation reader.

* drawingml: what WordCraft and GridCraft read that DeckCraft's copy did not

Every ST_PresetColorVal name has its colour, in any case and with the dk/lt/med abbreviations;
a sysClr without lastClr takes the usual value of its name; a Strict percentage (50%) reads
anywhere a percentage does; a chart series keeps its c:f formulas and the writer uses them.

* craft-drawingml: extra named theme colours and explicit tint rows

Ported from the format part of storytold/deckcraft#1746 by @jagjordi: ColorScheme::{extra, tints,
computed_tints}, ExtraColor, GalleryVariant, variants_of, and the a:clrScheme extension that
keeps them in a file.

* drawingml xmlx: copy only used, bounded namespace declarations onto kept fragments and mc branches

From storytold/deckcraft#1740 (988a4920), which landed after the extraction commit 60f91f78.

* Add craft-pptx: the PresentationML model (from DeckCraft)

Presentation, masters, layouts, slides, shapes, placeholders, media,
comments, sections, show settings, properties, embedded fonts, kept parts
and drawing guides from DeckCraft's crates/model/src/lib.rs, and
transitions and animations from anim.rs. The DrawingML types come from
craft-drawingml. The placeholder matching from DeckCraft's resolve.rs
moves as placeholder::parents, without the rest of resolve.

Presentation::default() is now an empty 16:9 deck with no masters; the
starting deck stays with the application.

* craft-pptx: the reader (from DeckCraft), on craft-opc and craft-drawingml

DeckCraft's crates/pptx reader (read.rs, read/shapes.rs and the chart
frame code of read/chart.rs) moved onto craft-opc's Package and
craft-xml trees, with the DrawingML readers of craft-drawingml behind a
part::Source over the import state. What DeckCraft took from its own
defaults (a master for a file without one, the theme of a master that
names none) comes from a caller-supplied Defaults; Plain is the neutral
one. Placeholder geometry inheritance uses placeholder::parents.

Transitions, animations, notes and comments follow in later changes.

* craft-pptx: drop DeckCraft issue numbers from reader comments

* craft-pptx: link the package budgets from the tag budget's doc

---------

Co-authored-by: Jordi Altayó <jordiag@kth.se>
2026-11-11 21:36:57 -05:00
Dylan M. Taylorandshuff57 db616480e2 craft-html: HTML documents on craft-flow (feature flow), from WordCraft (#1050)
* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-mime: detect, for bytes that name no character set

is_text, decode (BOM, unmarked UTF-16, UTF-8, else the legacy encoding
the bytes look like), legacy and legacy_label, behind a detect feature
that brings in chardetng. Moved from WordCraft's crates/formats/src/txt.rs
at 06c2ae6f with its text-sniffing tests. Also charset::is_known, so a
caller can tell a label decode would read as UTF-8 from one it knows.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.

* craft-codepage: one table per legacy code page, for craft-rtf, craft-dxf and craft-mime

craft-rtf and craft-dxf each carried tables for DOS 437, 737, 850, 852 and
857 (and craft-rtf one for 866). They are now one set in craft-codepage,
which takes craft-rtf's codepage module (Windows and Mac tables, OEM glyphs,
font charsets) and craft-dxf's DOS tables. craft_rtf::codepage re-exports it
unchanged; craft-dxf keeps its $DWGCODEPAGE names and Decoder and looks the
DOS tables up there.

Every byte of every code page either crate decodes is unchanged: the
public decoders of both crates were dumped over all 256 bytes of each page
(and the East Asian pairs) before and after, and the dumps are identical.

craft-mime now reads the DOS pages by their IANA names (IBM437, cp850,
ibm-852, 857...), which it read as UTF-8 before.

* Add craft-math: the equation tree with OMML, MathML, LaTeX and UnicodeMath

The math tree, LaTeX, linear-format, symbol and tree-walk modules come
from craft-wordmodel (storytold/craft-libs#963, WordCraft's crates/doc at
07f97737) unchanged, with the run colour (Rgb) and the symbol-font tables
they use. MathML is WordCraft's crates/formats/src/mathml.rs and OMML is
WordCraft's crates/docx/src/read/math.rs and write/math.rs (06c2ae6f /
07f97737), moved onto craft-xml. The HTML-specific MathML tests stay in
WordCraft; the rest moved with the code.

mathml::parse and omml::parse/read_str/write_str are new: string entry
points for callers that have no tree of their own.

* craft-math: read MathLive's and other spellings of LaTeX math commands

\abs{...} (an absolute value), \lvert \rvert \lVert \rVert, \nparallel,
\backprime, \infinity, \cross, \differentialD, \exponentialE and
\imaginaryI (MathLive's d, upright e and i), \emph, \textsf, \hbox,
\mathnormal and \underleftarrow / \underrightarrow /
\underleftrightarrow were unknown and read as literal text. They are read
as equaSHion's typed-input parser reads them (storytold/craft-libs#29).

* craft-math: equaSHion's equation corpus as test data

corpus/equations.tsv, corpus/latex.tsv and the OMML samples of
storytold/craft-libs#29 (7a6ef6b9), unmodified.

* craft-math: corpus round trips, GridCraft's OMML cases, entry points and limits tested; README

The corpus equations go through OMML, MathML and LaTeX and back; the OMML
samples are written back as kept and from the tree. GridCraft's OMML to
LaTeX test cases (crates/xlsx/src/omml.rs) give LaTeX that reads back the
same through the tree. omml::parse and read_str are tested against other
prefixes, the strict namespace, kept OMML, junk, the element cap and deep
nesting.

* Add craft-flow: the flow model the text formats map to, on craft-wordmodel

WordCraft's crates/formats/src/model (07f97737) moved as it is: Flow,
FBlock, Para, Inline, Fmt, tables and cells, pictures, to_doc and
from_doc, picture sniffing, colours, base64 and the Dropped report. Only
the paths changed (wordcraft_doc is craft_wordmodel); the report helpers
are public so the format crates can call them; the image re-encoding
helper stays with WordCraft's RTF writer.

* Add craft-markdown: Markdown read and written, on craft-flow

WordCraft's crates/formats/src/markdown (07f97737) with its 35 tests,
moved onto craft-flow and craft-wordmodel. Character references come from
craft-html. The table cell budget lives in craft-flow, which names it in
its warning. Two test lines that wrote plain text through WordCraft's txt
module stay in WordCraft.

* WIP: craft-latex sources from WordCraft (unmodified)

* Add craft-latex: LaTeX documents read and written, on craft-flow

WordCraft's crates/formats/src/latex (07f97737) with its 21 tests, moved
onto craft-flow and craft-wordmodel; bytes are decoded by craft-mime's
detect. The LaTeX parts of WordCraft's cross-format tests (nesting,
column specs, labels, no TeX commands from the document, the fuzz and
round-trip properties) come along as tests/hostile.rs. The ligature test
builds its three-font paragraph as a flow instead of reading HTML.

* craft-math: latex_text, LaTeX math and the linear format converted as text

WordCraft's crates/formats/src/latex/math.rs (07f97737): the converter its
LaTeX document reader and writer use, which keeps the source's spacing.
It moves here so craft-math is the one place that converts LaTeX math;
craft-latex calls it, and its output does not change. Its tests (from
WordCraft's latex tests and cross-format tests) come along.

* craft-math: latex_text's group finders are public, for craft-latex's length parser

* craft-latex: math through craft_math::latex_text; label timing test by ratio

The document math converter moved to craft-math (#1040) unchanged; this
crate calls it there (and its length parser uses its group finders), so
export output is the same. The label test compared one read with a
ten-second wall clock and failed on a loaded machine; it now compares the
fastest of three reads of 25,000 and 100,000 labels, which a linear read
keeps near 4x and a quadratic one near 16x.

* Cargo.lock after the merge

* WIP: WordCraft's HTML document reader and writer, unmodified (crates/formats/src/html at 07f97737)

* craft-html: HTML documents on craft-flow, behind a flow feature

WordCraft's crates/formats/src/html (07f97737) as craft_html::doc: the
tag-soup builder, CSS subset, ruby and MathML reading, page decoding and
the writer, with its 44 tests. It reads tokens from this crate's
tokenizer through a small adapter (decoded text and attributes, names in
lower case) instead of its own lexer, and its character references are
this crate's. Lexer::set_pos is new: the ruby reader hands a token back.
The HTML parts of WordCraft's MathML and cross-format tests come along as
tests/doc.rs, the linear-time check as a ratio.

* craft-math: operator names take their LaTeX scripts directly again (restores #1019)

#1019 (22161d2d) wrote \sin^2\theta as \sin ^{2}{\theta } so it reads back as
the same tree. The squash merge of #1038, whose branch predated it, put
math_latex.rs and the corpus test back as they were before. This applies
#1019's change again, unchanged, with its test.

* Cargo.lock: craft-html's flow dependencies

---------

Co-authored-by: shuff57 <62350898+shuff57@users.noreply.github.com>
2026-11-11 21:33:47 -05:00
Dylan M. Taylor f3fe9de956 craft-docx: DrawingML colours through craft-drawingml; its preset table takes WordCraft's names (#1049)
* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.

* craft-docx: WordCraft's crates/docx as it is at wordcraft 07f97737

The source, unchanged, so the next commit shows only what the move changes.
This commit does not build on its own (it still names wordcraft-doc and
wordcraft-geom, and is not a workspace member yet).

* craft-docx: on craft-wordmodel, craft-opc, craft-xml and craft-offcrypto

- The package (zip reading, part names, relationships, content types,
  zip writing) is craft-opc's; the module keeps WordCraft's bookkeeping
  (passthrough keys, the embedded-part manifest) and its own Rel/Rels shape.
- Parts are parsed with craft-xml's canonical names (the Strict drawing
  names and mc:Choice/@Requires through its hooks), then given the tree
  shape the reader was written against.
- Encryption and Restrict Editing hashes are craft-offcrypto's; this crate
  maps its errors and the w:documentProtection attributes.
- wordcraft-doc is craft-wordmodel; wordcraft-geom's helpers are its geom.

Tests that covered what now lives in craft-opc and craft-offcrypto are left to
those crates; the DIFAT regression is craft-cfb's. Test-only zip use follows
zip 9's fallible names. The attribute-cap test times the read only.

* craft-docx: DrawingML colours through craft-drawingml

Preset colours and the colour transforms of a:srgbClr, a:schemeClr and the
other colour elements are craft-drawingml's (craft_drawingml::color::preset,
ColorTransform, apply); opacity stays here, in percent.

craft-drawingml's preset table takes WordCraft's names (19 more, and the
dk/lt/grey spellings) and is case-insensitive, as WordCraft's was.

Two behaviours change, both to what craft-drawingml and LibreOffice do:
- a:tint and a:shade mix in linear light, not in sRGB bytes (a 50% shade of
  64 128 192 is 44 92 140, not 32 64 96); two tests are updated;
- the transforms WordCraft ignored (hueMod, hueOff, satOff, comp, inv, gray,
  redMod, greenMod, blueMod) are applied.

* craft-drawingml: test the preset names in any case and their dk/lt spellings
2026-11-11 21:33:23 -05:00
Dylan M. Taylor db762f02f9 craft-rtf, craft-webp: restore the outlook and vp8 features later squash merges dropped (#1072)
#991 (craft-codepage) was squash-merged from a branch that predated #961 and carried its older
crates/rtf/Cargo.toml, dropping the outlook feature, its craft-mime dependency and the proptest
dev-dependency; the outlook module was left behind an undefined cfg. #1028 did the same to
crates/webp/Cargo.toml after #1021, dropping the vp8 feature and its craft-video dependency.
2026-11-11 21:30:17 -05:00
Dylan M. Taylor 59d72512eb craft-timeline: restore OpenTimelineIO after the FCPXML squash reverted it (#1069)
#987 (FCPXML) was squash-merged after #1002 (OpenTimelineIO), which was stacked on it. The squash
carried #987's older copies of Cargo.toml, lib.rs, model.rs, README.md and the tests, which removed
the otio feature and module wiring that #1002 had added. This puts back #1002's merged state of
crates/timeline exactly; it already contains every FCPXML change.
2026-11-11 21:29:55 -05:00
Dylan M. TaylorandJordi Altayó 5538d8a0d7 Add craft-pptx: the PresentationML model (from DeckCraft) (#1044)
* 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.

* drawingml: the model types for fills, lines, text, tables, charts and diagrams

Moved from DeckCraft (60f91f78) crates/model/src: style.rs, text.rs,
table.rs, chart.rs, numfmt.rs and trendline.rs, plus Diagram, DiagramNode
and the MediaId and SlideId newtypes from lib.rs.

The files are unchanged except that deckcraft_color:: is crate::color:: and
Fill and TableBorder allow clippy::large_enum_variant. Their 60 tests come
with them. Readers and writers follow.

* drawingml: the theme model (colour, font and format schemes)

Moved from DeckCraft crates/model/src/theme.rs at 60f91f78: FontSet, ScriptFont, FontScheme,
FormatScheme, Theme and default_format. The app's theme gallery, its variants and its own default
theme stay in the app. Theme::default() is a plain theme built in place, so nothing here depends
on a gallery.

* drawingml: readers and writers for fills, lines, text, charts, themes, table styles and diagrams

Moved from DeckCraft crates/pptx/src at 60f91f78: read/dml.rs, write/dml.rs, read/chart.rs,
read/chartex.rs, write/chart.rs, read/theme.rs, write/theme.rs, tables.rs and dgm.rs, onto
craft-xml. Package lookups (Imp, Part, Exp, Out) are the caller's part::Source and part::Sink.
The chart frame budget and kept-part capture stay with the presentation reader.

* drawingml: what WordCraft and GridCraft read that DeckCraft's copy did not

Every ST_PresetColorVal name has its colour, in any case and with the dk/lt/med abbreviations;
a sysClr without lastClr takes the usual value of its name; a Strict percentage (50%) reads
anywhere a percentage does; a chart series keeps its c:f formulas and the writer uses them.

* craft-drawingml: extra named theme colours and explicit tint rows

Ported from the format part of storytold/deckcraft#1746 by @jagjordi: ColorScheme::{extra, tints,
computed_tints}, ExtraColor, GalleryVariant, variants_of, and the a:clrScheme extension that
keeps them in a file.

* drawingml xmlx: copy only used, bounded namespace declarations onto kept fragments and mc branches

From storytold/deckcraft#1740 (988a4920), which landed after the extraction commit 60f91f78.

* Add craft-pptx: the PresentationML model (from DeckCraft)

Presentation, masters, layouts, slides, shapes, placeholders, media,
comments, sections, show settings, properties, embedded fonts, kept parts
and drawing guides from DeckCraft's crates/model/src/lib.rs, and
transitions and animations from anim.rs. The DrawingML types come from
craft-drawingml. The placeholder matching from DeckCraft's resolve.rs
moves as placeholder::parents, without the rest of resolve.

Presentation::default() is now an empty 16:9 deck with no masters; the
starting deck stays with the application.

---------

Co-authored-by: Jordi Altayó <jordiag@kth.se>
2026-11-11 21:04:26 -05:00
Dylan M. TaylorandRene Leonhardt 392b8720cd craft-psd: less copying and compression work when encoding planes (#1043)
- PackBits scans each row once; RLE planes encode their rows on worker
  threads on native targets (rayon), keeping the exact packet boundaries
  and the fallback when RLE does not pay.
- ZIP output grows in bounded blocks and is compacted to its encoded size,
  instead of reserving the raw size; native threads reuse a finished
  encoder's working buffers.
- 16/32-bit prediction works on owned buffers in one pass.
- Filter-effects planes and pattern blocks encode from borrowed rows and
  owned buffers without the intermediate copies.

The encoded bytes are unchanged.

Ported from storytold/photocraft#7797 (its crates/psd part), at
a1ee8355a4651d379286cd13f92dabe4344e295f.

Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com>
2026-11-11 21:04:07 -05:00
Dylan M. Taylor e74476f141 craft-penpot: a typed model of the export's JSON members (#1039)
Manifest, file header, page (guides, flows), shape (geometry, fills with
gradients and images, strokes, shadows, blurs, radii, constraints, flex
and grid layout fields, component links, applied tokens) and the library
records (colours, typographies, components, media), named as the files
name their fields. Every record keeps the fields it has no name for, so a
member read and written loses nothing but nulls. Archive::typed reads a
member as any of them within the archive's budgets.
2026-11-11 20:59:00 -05:00
Dylan M. Taylorandshuff57 bdf06f1029 Add craft-latex: LaTeX documents read and written, on craft-flow (stacked) (#1038)
* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-mime: detect, for bytes that name no character set

is_text, decode (BOM, unmarked UTF-16, UTF-8, else the legacy encoding
the bytes look like), legacy and legacy_label, behind a detect feature
that brings in chardetng. Moved from WordCraft's crates/formats/src/txt.rs
at 06c2ae6f with its text-sniffing tests. Also charset::is_known, so a
caller can tell a label decode would read as UTF-8 from one it knows.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.

* Add craft-math: the equation tree with OMML, MathML, LaTeX and UnicodeMath

The math tree, LaTeX, linear-format, symbol and tree-walk modules come
from craft-wordmodel (storytold/craft-libs#963, WordCraft's crates/doc at
07f97737) unchanged, with the run colour (Rgb) and the symbol-font tables
they use. MathML is WordCraft's crates/formats/src/mathml.rs and OMML is
WordCraft's crates/docx/src/read/math.rs and write/math.rs (06c2ae6f /
07f97737), moved onto craft-xml. The HTML-specific MathML tests stay in
WordCraft; the rest moved with the code.

mathml::parse and omml::parse/read_str/write_str are new: string entry
points for callers that have no tree of their own.

* craft-math: read MathLive's and other spellings of LaTeX math commands

\abs{...} (an absolute value), \lvert \rvert \lVert \rVert, \nparallel,
\backprime, \infinity, \cross, \differentialD, \exponentialE and
\imaginaryI (MathLive's d, upright e and i), \emph, \textsf, \hbox,
\mathnormal and \underleftarrow / \underrightarrow /
\underleftrightarrow were unknown and read as literal text. They are read
as equaSHion's typed-input parser reads them (storytold/craft-libs#29).

* craft-math: equaSHion's equation corpus as test data

corpus/equations.tsv, corpus/latex.tsv and the OMML samples of
storytold/craft-libs#29 (7a6ef6b9), unmodified.

* craft-math: corpus round trips, GridCraft's OMML cases, entry points and limits tested; README

The corpus equations go through OMML, MathML and LaTeX and back; the OMML
samples are written back as kept and from the tree. GridCraft's OMML to
LaTeX test cases (crates/xlsx/src/omml.rs) give LaTeX that reads back the
same through the tree. omml::parse and read_str are tested against other
prefixes, the strict namespace, kept OMML, junk, the element cap and deep
nesting.

* Add craft-flow: the flow model the text formats map to, on craft-wordmodel

WordCraft's crates/formats/src/model (07f97737) moved as it is: Flow,
FBlock, Para, Inline, Fmt, tables and cells, pictures, to_doc and
from_doc, picture sniffing, colours, base64 and the Dropped report. Only
the paths changed (wordcraft_doc is craft_wordmodel); the report helpers
are public so the format crates can call them; the image re-encoding
helper stays with WordCraft's RTF writer.

* Add craft-markdown: Markdown read and written, on craft-flow

WordCraft's crates/formats/src/markdown (07f97737) with its 35 tests,
moved onto craft-flow and craft-wordmodel. Character references come from
craft-html. The table cell budget lives in craft-flow, which names it in
its warning. Two test lines that wrote plain text through WordCraft's txt
module stay in WordCraft.

* WIP: craft-latex sources from WordCraft (unmodified)

* Add craft-latex: LaTeX documents read and written, on craft-flow

WordCraft's crates/formats/src/latex (07f97737) with its 21 tests, moved
onto craft-flow and craft-wordmodel; bytes are decoded by craft-mime's
detect. The LaTeX parts of WordCraft's cross-format tests (nesting,
column specs, labels, no TeX commands from the document, the fuzz and
round-trip properties) come along as tests/hostile.rs. The ligature test
builds its three-font paragraph as a flow instead of reading HTML.

* craft-math: latex_text, LaTeX math and the linear format converted as text

WordCraft's crates/formats/src/latex/math.rs (07f97737): the converter its
LaTeX document reader and writer use, which keeps the source's spacing.
It moves here so craft-math is the one place that converts LaTeX math;
craft-latex calls it, and its output does not change. Its tests (from
WordCraft's latex tests and cross-format tests) come along.

* craft-math: latex_text's group finders are public, for craft-latex's length parser

* craft-latex: math through craft_math::latex_text; label timing test by ratio

The document math converter moved to craft-math (#1040) unchanged; this
crate calls it there (and its length parser uses its group finders), so
export output is the same. The label test compared one read with a
ten-second wall clock and failed on a loaded machine; it now compares the
fastest of three reads of 25,000 and 100,000 labels, which a linear read
keeps near 4x and a quadratic one near 16x.

---------

Co-authored-by: shuff57 <62350898+shuff57@users.noreply.github.com>
2026-11-11 20:58:36 -05:00
cabcf2fbf3 craft-xls: read RC4 and RC4 CryptoAPI encrypted workbooks (ports of gridcraft#847, #3713) (#1018)
* Add craft-sheet: the cell positions, values and tables the spreadsheet formats share

craft-xls carried its own CellRef, Range, Value, CellError, Visibility, sheet-name
quoting, built-in number format table and indexed palette. craft-xlsx and craft-ods
need the same ones, so they move to a crate with no dependencies and craft-xls
re-exports them: an application gets one Value and one CellRef whichever file it read.

Added on the way: A1 parsing (CellRef::parse, Range::parse), Range::count and a1,
CellError::parse and the newer error values SpreadsheetML stores as text,
builtin_format_id for writers.

* craft-xls: read data validation (DVal and Dv records)

Each Dv after a DVal becomes a DataValidation in SpreadsheetML's terms: type,
operator, formulas as A1 text from the top left cell of all the rule's ranges (a
literal list quoted, items separated by commas), the four messages, alert style
and flags. Exactly the DVal's count of rules is read, as Excel does; a rule Excel
would refuse is skipped with a warning, and the rest are kept. A sheet keeps at
most 65,536 rules (the count was not capped on its own).

Ported from storytold/gridcraft#3636.

* craft-xls: read comments, conditional formats, pictures and the AutoFilter range

- Notes with their TxO text, author and visibility.
- CondFmt and CF records (cell value is, formula is), formulas from the bounding
  range's top left cell, and the DXFN as a DifferentialFormat whose unset parts are
  None.
- The drawing group's BLIP store (PNG, JPEG, EMF, WMF with its placeable header,
  DIB as BMP) and each sheet's picture shapes with their client anchors.
- The AutoFilter range from AutoFilterInfo and _FilterDatabase.

From the review of the source PR: picture bytes are shared by the shapes that show
them, pictures, comments and rules are capped per sheet, inflated metafiles share
one allowance, damaged object and CF records are skipped with a warning instead of
failing the read, and CF formulas are relative to the CondFmt's bounding range.
ValidationOperator is now Operator, shared with conditional formats.

Ported from storytold/gridcraft#3457.

* craft-xls: read workbooks encrypted with RC4, with their password

A FilePass record with RC4 (version 1.1) is checked against the password through
its MD5 verifier, and the record data after it is decrypted, rekeyed every 1024
bytes of the stream; record headers and the few records Excel leaves in the clear
are kept. read_with_password takes the password. Without one, Excel's default
password (VelvetSweatshop) is tried, as Excel opens a workbook protected only
against changes, and Error::Encrypted says a password is needed. XOR obfuscation
and RC4 CryptoAPI are Error::UnsupportedEncryption; a wrong password is
Error::WrongPassword.

Ported from storytold/gridcraft#847.

* craft-xls: read workbooks encrypted with RC4 CryptoAPI

What current Excel writes for an .xls saved with a password (FilePass version 2.2,
3.2 or 4.2): H0 = SHA-1(salt, UTF-16LE password), each 1024-byte block keyed by
SHA-1(H0, block number) cut to the key size, a 40-bit key padded with zeros to 128
bits, and the password checked against the verifier's SHA-1. Every size in the
FilePass record is checked before use; CryptoAPI with AES, another cipher or hash,
an external provider or a key outside 40 to 128 bits is named as unsupported.

Ported from storytold/gridcraft#3713.

---------

Co-authored-by: kiwi4boy <kiwi4boy@gmail.com>
Co-authored-by: BrianDo35 <haison.db35@icloud.com>
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: giuseppeps <giuseppe_000@hotmail.com>
2026-11-11 20:54:41 -05:00
45ba44f740 craft-xls: comments, conditional formats, pictures and the AutoFilter range (port of gridcraft#3457) (#1005)
* Add craft-sheet: the cell positions, values and tables the spreadsheet formats share

craft-xls carried its own CellRef, Range, Value, CellError, Visibility, sheet-name
quoting, built-in number format table and indexed palette. craft-xlsx and craft-ods
need the same ones, so they move to a crate with no dependencies and craft-xls
re-exports them: an application gets one Value and one CellRef whichever file it read.

Added on the way: A1 parsing (CellRef::parse, Range::parse), Range::count and a1,
CellError::parse and the newer error values SpreadsheetML stores as text,
builtin_format_id for writers.

* craft-xls: read data validation (DVal and Dv records)

Each Dv after a DVal becomes a DataValidation in SpreadsheetML's terms: type,
operator, formulas as A1 text from the top left cell of all the rule's ranges (a
literal list quoted, items separated by commas), the four messages, alert style
and flags. Exactly the DVal's count of rules is read, as Excel does; a rule Excel
would refuse is skipped with a warning, and the rest are kept. A sheet keeps at
most 65,536 rules (the count was not capped on its own).

Ported from storytold/gridcraft#3636.

* craft-xls: read comments, conditional formats, pictures and the AutoFilter range

- Notes with their TxO text, author and visibility.
- CondFmt and CF records (cell value is, formula is), formulas from the bounding
  range's top left cell, and the DXFN as a DifferentialFormat whose unset parts are
  None.
- The drawing group's BLIP store (PNG, JPEG, EMF, WMF with its placeable header,
  DIB as BMP) and each sheet's picture shapes with their client anchors.
- The AutoFilter range from AutoFilterInfo and _FilterDatabase.

From the review of the source PR: picture bytes are shared by the shapes that show
them, pictures, comments and rules are capped per sheet, inflated metafiles share
one allowance, damaged object and CF records are skipped with a warning instead of
failing the read, and CF formulas are relative to the CondFmt's bounding range.
ValidationOperator is now Operator, shared with conditional formats.

Ported from storytold/gridcraft#3457.

---------

Co-authored-by: kiwi4boy <kiwi4boy@gmail.com>
Co-authored-by: BrianDo35 <haison.db35@icloud.com>
2026-11-11 20:53:46 -05:00
Dylan M. Taylor d59bcf56c9 craft-timeline: FCPXML on the neutral model (#987)
* craft-timeline: a neutral timeline model and the import builder

model: a Project of bins and items (media, sequences, subclips, and items no
interchange format carries), sequences of tracks with clips, transitions,
markers and effects with keyframes. Cut down from FilmCraft's project model to
what Final Cut Pro 7 XML, FCPXML and OpenTimelineIO describe; EffectCraft
already uses that shape for its interchange. Three things stay the
application's: effects (looked up through a Catalog the caller supplies),
generators beyond the common ones (Generator::Other with an opaque payload),
and per-object extension JSON (ext) that a format can carry unread.

fx: the effect and parameter ids the formats name themselves, and Standard,
a catalog of just those.

build: FilmCraft's import Builder and shared helpers (filmcraft-interchange
common.rs at 4bec22bd) on the model, with the effect registry replaced by the
catalog, and the bounds every import ends with (Imported::bound, from lib.rs).

* craft-timeline: Final Cut Pro 7 XML (xmeml) on the neutral model

fcp7: FilmCraft's xmeml reader and writer (filmcraft-interchange fcp7.rs at
4bec22bd) moved onto craft_timeline::model, behind feature xml.

- XML is read with craft-xml, strictly: at most 256 levels of nesting (the
  limit EffectCraft checked with its own depth scan before handing documents
  over), 2 000 000 nodes, 256 MiB; a document type declaration with an internal
  subset and entities XML does not define are refused. It is written with
  craft-xml's Writer.
- Effects other than motion, opacity, time remap, audio levels and pan are
  looked up through the caller's Catalog, by id and by name, and written with
  the catalog's stable name and category.
- An application generator round-trips as <namespace>:<payload> in effectid
  (FilmCraft wrote filmcraft:<json>); transparent video, counting leader and
  bars and tone are written by name, which the reader already took.
- Finding the sequence a clip item is in is one walk of the document instead of
  an ancestors() lookup per clip item.

tests/it: FilmCraft's FCP7 integration tests (the hand-written Premiere-style
document, the rich round trip, the 48-case property round trip, split mono and
stereo channels, sub-frame spans, absurd timebases and start timecodes, the
link-group bound) on the model, with a test catalog for the four application
effects they use.

* craft-timeline: FCPXML on the neutral model

fcpxml: FilmCraft's FCPXML 1.9-1.11 reader and writer (filmcraft-interchange
fcpxml.rs at 4bec22bd) moved onto craft_timeline::model, behind feature xml.
XML is read with craft-xml as fcp7 is; filters other than the transform,
blend and volume adjustments go through the caller's Catalog by name, and an
effect resource's uid is <namespace>.<effect id> (FilmCraft wrote
filmcraft.<id>). ExportOptions gains fcpxml_version.

tests/it/fcpxml.rs: FilmCraft's FCPXML integration tests on the model (the
hand-written connected-clips document, hostile lane numbers, standalone video
stills, self-referencing compound media, round trips at 1.9, 1.10 and 1.11,
the property round trip with its regression seed, percent-decoded relative
URLs). The FCP7 tests export to FCPXML again where FilmCraft's did.
2026-11-11 20:52:52 -05:00
Dylan M. TaylorandMartin Ball d197b1e72f Add craft-dav: WebDAV multistatus reader from MailCraft (#975)
MailCraft crates/dav/src/xml.rs at a44a450, re-based onto craft-xml: a
multistatus read fail-closed into a bounded tree with fixed prefixes for
the DAV, CalDAV, CardDAV and related namespaces, and the properties of a
response's successful propstats. Its tests come along.

Co-authored-by: Martin Ball <98639962+benandez@users.noreply.github.com>
2026-11-11 20:51:12 -05:00
Dylan M. Taylor 97f4971666 craft-pdf: the PDF filter chain from PdfCraft (filters), on craft-stream-codecs (#1033)
* Add craft-stream-codecs: the codec modules of PdfCraft's stream filters

LZW, ASCII85, ASCIIHex, RunLength, the TIFF and PNG predictors, bounded CCITT
fax decoding (through hayro-ccitt) with Group 4 encoding (through fax), and
the lossless JBIG2 encoder with its MQ coder, from pdfcraft-filters at
PdfCraft revision 80e26e674d1aa33078e83c8139875047f3289eb6.

The crate sits below craft-pdf and craft-tiff: it knows no container. The
PDF filter chain, Flate and the bounded JBIG2 decoding entry are not part of
it.

* craft-pdf: the PDF filter chain from PdfCraft (`filters`)

Adds the `filters` feature: the /Filter chain of pdfcraft-filters (PdfCraft
revision 80e26e674d1aa33078e83c8139875047f3289eb6) as craft_pdf::filters,
over the codecs in craft-stream-codecs. It adds Flate, strict and tolerant
chain decoding, byte accounting and a shared work allowance, and encoding,
with the chain's unit tests, its property tests and its fuzz target.

The code is unchanged apart from the imports of the moved codecs; FilterError
is craft-stream-codecs' CodecError under its old name.
2026-11-11 20:48:55 -05:00
Dylan M. Taylor 1e3d3968c1 Add craft-markdown: Markdown read and written, on craft-flow (stacked on #1025) (#1030)
* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.

* Add craft-flow: the flow model the text formats map to, on craft-wordmodel

WordCraft's crates/formats/src/model (07f97737) moved as it is: Flow,
FBlock, Para, Inline, Fmt, tables and cells, pictures, to_doc and
from_doc, picture sniffing, colours, base64 and the Dropped report. Only
the paths changed (wordcraft_doc is craft_wordmodel); the report helpers
are public so the format crates can call them; the image re-encoding
helper stays with WordCraft's RTF writer.

* Add craft-markdown: Markdown read and written, on craft-flow

WordCraft's crates/formats/src/markdown (07f97737) with its 35 tests,
moved onto craft-flow and craft-wordmodel. Character references come from
craft-html. The table cell budget lives in craft-flow, which names it in
its warning. Two test lines that wrote plain text through WordCraft's txt
module stay in WordCraft.
2026-11-11 20:46:15 -05:00
Dylan M. Taylor aa84415bae craft-gif, craft-webp: bounded frame decoding through the image crate (feature frames) (#1028)
* craft-gif: bounded frame decoding through the image crate (feature frames)

* craft-webp: bounded frame decoding through the image crate (feature frames)

Like craft-gif's: every frame composited, a still as one frame, canvas cap
checked from the headers, stops at the frame or byte limit or at a damaged
frame and says which, optional merging of identical frames.
2026-11-11 20:43:12 -05:00
Dylan M. Taylor baf6bd58f0 Add craft-flow: the flow model the text formats map to (#1025)
* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.

* Add craft-flow: the flow model the text formats map to, on craft-wordmodel

WordCraft's crates/formats/src/model (07f97737) moved as it is: Flow,
FBlock, Para, Inline, Fmt, tables and cells, pictures, to_doc and
from_doc, picture sniffing, colours, base64 and the Dropped report. Only
the paths changed (wordcraft_doc is craft_wordmodel); the report helpers
are public so the format crates can call them; the image re-encoding
helper stays with WordCraft's RTF writer.
2026-11-11 20:41:34 -05:00
Dylan M. TaylorandJustin A. Wilson 45177e282e craft-webp: animated WebP writer, lossy through craft-video's VP8 encoder (depends on #969) (#1021)
* craft-video: VP8 key-frame encoder (`vp8-encode` feature)

Moves effectcraft's effectcraft-vp8enc here: the lossy picture of a WebP image
or animation frame, written from RFC 6386. Rate-distortion choice between the
16x16 and B_PRED intra modes, per-frame token probability updates, loop filter
level from the quantizer.

`vp8` now turns on `vp8-decode` and `vp8-encode`, as `vp9` does; the decoder's
tests and examples require `vp8-decode`. The tests decode the encoder's frames
with this crate's decoder: with the loop filter off it reproduces the encoder's
reconstruction exactly.

* craft-webp: animated WebP writer, lossy through craft-video (feature vp8)

The VP8X/ANIM/ANMF container around frames another encoder made (lossy with
an alpha plane, or lossless VP8L), and with feature vp8 whole lossy
animations from RGBA frames through craft-video's VP8 key-frame encoder.

---------

Co-authored-by: Justin A. Wilson <waj334@gmail.com>
2026-11-11 20:37:10 -05:00
Dylan M. Taylor e3e31693c7 Add craft-docx: WordCraft's DOCX reader and writer on craft-wordmodel, craft-opc and craft-xml (#1015)
* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.

* craft-docx: WordCraft's crates/docx as it is at wordcraft 07f97737

The source, unchanged, so the next commit shows only what the move changes.
This commit does not build on its own (it still names wordcraft-doc and
wordcraft-geom, and is not a workspace member yet).

* craft-docx: on craft-wordmodel, craft-opc, craft-xml and craft-offcrypto

- The package (zip reading, part names, relationships, content types,
  zip writing) is craft-opc's; the module keeps WordCraft's bookkeeping
  (passthrough keys, the embedded-part manifest) and its own Rel/Rels shape.
- Parts are parsed with craft-xml's canonical names (the Strict drawing
  names and mc:Choice/@Requires through its hooks), then given the tree
  shape the reader was written against.
- Encryption and Restrict Editing hashes are craft-offcrypto's; this crate
  maps its errors and the w:documentProtection attributes.
- wordcraft-doc is craft-wordmodel; wordcraft-geom's helpers are its geom.

Tests that covered what now lives in craft-opc and craft-offcrypto are left to
those crates; the DIFAT regression is craft-cfb's. Test-only zip use follows
zip 9's fallible names. The attribute-cap test times the read only.
2026-11-11 20:31:40 -05:00
Dylan M. Taylorandshuff57 85b4dabf0f Add craft-math: equations as one tree with OMML, MathML, LaTeX and UnicodeMath (#1013)
* Add craft-math: the equation tree with OMML, MathML, LaTeX and UnicodeMath

The math tree, LaTeX, linear-format, symbol and tree-walk modules come
from craft-wordmodel (storytold/craft-libs#963, WordCraft's crates/doc at
07f97737) unchanged, with the run colour (Rgb) and the symbol-font tables
they use. MathML is WordCraft's crates/formats/src/mathml.rs and OMML is
WordCraft's crates/docx/src/read/math.rs and write/math.rs (06c2ae6f /
07f97737), moved onto craft-xml. The HTML-specific MathML tests stay in
WordCraft; the rest moved with the code.

mathml::parse and omml::parse/read_str/write_str are new: string entry
points for callers that have no tree of their own.

* craft-math: read MathLive's and other spellings of LaTeX math commands

\abs{...} (an absolute value), \lvert \rvert \lVert \rVert, \nparallel,
\backprime, \infinity, \cross, \differentialD, \exponentialE and
\imaginaryI (MathLive's d, upright e and i), \emph, \textsf, \hbox,
\mathnormal and \underleftarrow / \underrightarrow /
\underleftrightarrow were unknown and read as literal text. They are read
as equaSHion's typed-input parser reads them (storytold/craft-libs#29).

* craft-math: equaSHion's equation corpus as test data

corpus/equations.tsv, corpus/latex.tsv and the OMML samples of
storytold/craft-libs#29 (7a6ef6b9), unmodified.

* craft-math: corpus round trips, GridCraft's OMML cases, entry points and limits tested; README

The corpus equations go through OMML, MathML and LaTeX and back; the OMML
samples are written back as kept and from the tree. GridCraft's OMML to
LaTeX test cases (crates/xlsx/src/omml.rs) give LaTeX that reads back the
same through the tree. omml::parse and read_str are tested against other
prefixes, the strict namespace, kept OMML, junk, the element cap and deep
nesting.

---------

Co-authored-by: shuff57 <62350898+shuff57@users.noreply.github.com>
2026-11-11 20:31:02 -05:00
Dylan M. Taylor dc14024a19 craft-sketch: map the layers onto the REST file JSON (#1012)
* Add craft-figma-rest: a typed model of the Figma REST file JSON

The neutral model for design files: the .fig and .sketch readers map onto
it and an application's importer takes it. Typed records for the document
tree, paints, effects, text styles, layout grids, constraints, export
settings and the component and style metadata; every record keeps the
fields it has no name for, so a file read and written loses nothing.
The vectorNetwork extension is typed and documented with the others.

The layer tree is converted level by level in both directions, and the
depth and node limits are checked on the JSON before any typed value is
built.

* craft-figma-rest: Warnings and style_name for the readers that build this shape

Moved from FigCraft's REST importer and .fig reader (crates/format/src/rest.rs
Warnings, fig.rs style_name), which the .fig and Sketch mappings share.

* craft-sketch: map the layers onto the REST file JSON

FigCraft's Sketch mapping (crates/format/src/sketch/{tree,style,text,
symbols,path,shared,stack,proto}.rs at e8b53e33) moved onto
craft-figma-rest: rest::to_rest turns an Archive's pages and layers into
the REST API file JSON, with the vector networks of its paths handed over
beside it as craft_figma_rest::ext::VectorNetwork.

The star and polygon point generators it compares a path with are copied
from FigCraft's geometry; comments and two warnings no longer name the
app.

* craft-sketch: reach the nesting limit with nested groups, not 210 masters

The same limit and warning, in 1 second instead of 58 in a debug build.

* craft-sketch: a nesting limit that fits a 1 MiB stack, in small frames

The mapping recurses once per level of layer nesting, symbol expansion
included. Nested symbol instances reach its limit with shallow page JSON,
and a debug build overflowed a 2 MiB stack at about 140 levels: a crash
on hostile input.

The work on one layer, master or instance now happens in functions that
return before its children are mapped, which takes the stack per level
from about 4.5 KB to about 2 KB for an instance and its group in a debug
build. That maps about 140 levels of nested instances on 512 KiB, so the
limit is now 128 (was 200), leaving twice the room on the 1 MiB stack of
a WebAssembly build. The test at the limit runs on a 1 MiB thread.

* Cargo.lock: craft-sketch's craft-figma-rest and kurbo after the merge with main
2026-11-11 20:30:38 -05:00
Dylan M. Taylor 6cc226e454 craft-timeline: OpenTimelineIO on the neutral model (#1002)
* craft-timeline: a neutral timeline model and the import builder

model: a Project of bins and items (media, sequences, subclips, and items no
interchange format carries), sequences of tracks with clips, transitions,
markers and effects with keyframes. Cut down from FilmCraft's project model to
what Final Cut Pro 7 XML, FCPXML and OpenTimelineIO describe; EffectCraft
already uses that shape for its interchange. Three things stay the
application's: effects (looked up through a Catalog the caller supplies),
generators beyond the common ones (Generator::Other with an opaque payload),
and per-object extension JSON (ext) that a format can carry unread.

fx: the effect and parameter ids the formats name themselves, and Standard,
a catalog of just those.

build: FilmCraft's import Builder and shared helpers (filmcraft-interchange
common.rs at 4bec22bd) on the model, with the effect registry replaced by the
catalog, and the bounds every import ends with (Imported::bound, from lib.rs).

* craft-timeline: Final Cut Pro 7 XML (xmeml) on the neutral model

fcp7: FilmCraft's xmeml reader and writer (filmcraft-interchange fcp7.rs at
4bec22bd) moved onto craft_timeline::model, behind feature xml.

- XML is read with craft-xml, strictly: at most 256 levels of nesting (the
  limit EffectCraft checked with its own depth scan before handing documents
  over), 2 000 000 nodes, 256 MiB; a document type declaration with an internal
  subset and entities XML does not define are refused. It is written with
  craft-xml's Writer.
- Effects other than motion, opacity, time remap, audio levels and pan are
  looked up through the caller's Catalog, by id and by name, and written with
  the catalog's stable name and category.
- An application generator round-trips as <namespace>:<payload> in effectid
  (FilmCraft wrote filmcraft:<json>); transparent video, counting leader and
  bars and tone are written by name, which the reader already took.
- Finding the sequence a clip item is in is one walk of the document instead of
  an ancestors() lookup per clip item.

tests/it: FilmCraft's FCP7 integration tests (the hand-written Premiere-style
document, the rich round trip, the 48-case property round trip, split mono and
stereo channels, sub-frame spans, absurd timebases and start timecodes, the
link-group bound) on the model, with a test catalog for the four application
effects they use.

* craft-timeline: FCPXML on the neutral model

fcpxml: FilmCraft's FCPXML 1.9-1.11 reader and writer (filmcraft-interchange
fcpxml.rs at 4bec22bd) moved onto craft_timeline::model, behind feature xml.
XML is read with craft-xml as fcp7 is; filters other than the transform,
blend and volume adjustments go through the caller's Catalog by name, and an
effect resource's uid is <namespace>.<effect id> (FilmCraft wrote
filmcraft.<id>). ExportOptions gains fcpxml_version.

tests/it/fcpxml.rs: FilmCraft's FCPXML integration tests on the model (the
hand-written connected-clips document, hostile lane numbers, standalone video
stills, self-referencing compound media, round trips at 1.9, 1.10 and 1.11,
the property round trip with its regression seed, percent-decoded relative
URLs). The FCP7 tests export to FCPXML again where FilmCraft's did.

* craft-timeline: OpenTimelineIO on the neutral model

otio: FilmCraft's OTIO reader and writer (filmcraft-interchange otio.rs at
4bec22bd) moved onto craft_timeline::model, behind feature otio (serde_json).

FilmCraft wrote its own serde types into metadata.filmcraft. Here the model's
fields are written as JSON by hand under metadata.<namespace>, with the same
keys and value shapes FilmCraft's serde produced (labels, settings, media
info, generators, effects with externally tagged parameter values, alignment,
marker kinds, channel layouts), so files FilmCraft wrote read back. Every key
the reader does not know comes back in the object's ext, and ext's keys are
written next to the model's: an application's palette, track volume, masks and
the like round-trip without the crate knowing them. ParamValue::Opaque and
Generator::Other carry values only the application reads.

The model's Sequence loses its ext: a sequence's is its item's, so there is
one place for it. ImportOptions gains name (for an unnamed timeline), as
FilmCraft's had.

tests: FilmCraft's OTIO integration tests on the model (the golden document,
the lossless round trip now through ext, collections and bare tracks, items at
an audio sample rate, generator labels and identity, the property round trip,
marker URLs, percent-decoded relative URLs); its palette tests stay with
FilmCraft, whose palette merge they test, and are replaced by one ext round
trip. The OTIO unit tests (exact rational times, the nested-copies limit) and
a JSON nesting test.
2026-11-11 20:24:37 -05:00
Dylan M. Taylor 99b5141700 craft-exr: deep data and Cryptomatte samples from PhotoCraft (feature decode, stacked on #992) (#1001)
* craft-exr: bounded part listing, flat decoding and writing (feature decode)

PhotoCraft's exr-crate wrapper (preview check, per-part and total size limits,
file-size plausibility, part ranking, one-part decode) and EffectCraft's
streaming channel filter, as one reader that returns a neutral model: part
headers with text attributes, and the kept channels in their stored type.

* craft-exr: deep data and Cryptomatte samples (feature decode)

PhotoCraft's deep OpenEXR reader (NONE, RLE and ZIPS deep scan lines and
tiles, every allocation admitted against the byte budget) and its flattening,
and its Cryptomatte layer listing and rank decoding, on the decode module.
2026-11-11 20:23:00 -05:00
Dylan M. Taylor a4b57d47b9 Add craft-crs: Camera Raw crs: settings in XMP, read and written, from PhotoCraft (#996)
* Add craft-crs: Camera Raw crs: settings in XMP, read and written, from PhotoCraft

The format half of PhotoCraft's Camera Raw settings files (crates/io/src/camera_raw_xmp.rs at
abeb16c96): the crs: settings of an XMP packet as written, Camera Raw's number and curve point
text, the crop and its orientation, a writer, and the toolkit marker. Which setting drives
which control stays in the app.

* craft-crs: structures carry their element and are written back

Local corrections and other structured settings were dropped, and an rdf:Seq of descriptions
read as empty strings. A structure now holds its crs: element, a sequence is only plain text
items, descriptions inside a structure are not read as top-level settings, and to_xmp writes
structures back.

* craft-crs: read packets through craft-metadata's XMP reader

One XMP reader for every crate: Settings::parse goes through craft_metadata::parse_xmp and
Settings::from_xmp takes its XmpData. Settings are kept by name (the reader's map has no file
order), structures are craft-metadata's XmpValue and are written back as rdf:parseType
Resource elements and rdf:Seq arrays. toolkit stays a bounded first-start-tag scan, as the
reader does not return x:xmptk.
2026-11-11 20:19:00 -05:00
Dylan M. Taylor c516452b47 craft-codepage: one table per legacy code page, for craft-rtf, craft-dxf and craft-mime (#991)
craft-rtf and craft-dxf each carried tables for DOS 437, 737, 850, 852 and
857 (and craft-rtf one for 866). They are now one set in craft-codepage,
which takes craft-rtf's codepage module (Windows and Mac tables, OEM glyphs,
font charsets) and craft-dxf's DOS tables. craft_rtf::codepage re-exports it
unchanged; craft-dxf keeps its $DWGCODEPAGE names and Decoder and looks the
DOS tables up there.

Every byte of every code page either crate decodes is unchanged: the
public decoders of both crates were dumped over all 256 bytes of each page
(and the East Asian pairs) before and after, and the dumps are identical.

craft-mime now reads the DOS pages by their IANA names (IBM437, cp850,
ibm-852, 857...), which it read as UTF-8 before.
2026-11-11 20:17:35 -05:00
Dylan M. Taylor 0d68632c9b Add craft-ods: OpenDocument spreadsheet tables on craft-odf (#990)
* Add craft-sheet: the cell positions, values and tables the spreadsheet formats share

craft-xls carried its own CellRef, Range, Value, CellError, Visibility, sheet-name
quoting, built-in number format table and indexed palette. craft-xlsx and craft-ods
need the same ones, so they move to a crate with no dependencies and craft-xls
re-exports them: an application gets one Value and one CellRef whichever file it read.

Added on the way: A1 parsing (CellRef::parse, Range::parse), Range::count and a1,
CellError::parse and the newer error values SpreadsheetML stores as text,
builtin_format_id for writers.

* Add craft-ods: OpenDocument spreadsheet tables on craft-odf

read() checks the spreadsheet mimetype, refuses an encrypted package, and reads the
tables of content.xml into sheets of cells that keep the file's terms: value type,
the raw value, displayed text, formula text, style name, covered cells, with
repeats expanded and merged ranges. Cell::value() gives the shown value, dates and
durations as serial numbers.

From GridCraft crates/xlsx/src/ods.rs and its tests (7e0d96d7), split from
GridCraft's document model and moved onto craft-odf and craft-xml's pull reader.
Options::only_sheet covers WordCraft's reader of one chosen sheet.
2026-11-11 20:16:51 -05:00
6f51ec7f8d craft-fig: map the .fig message onto the REST file JSON (#980)
* Add craft-figma-rest: a typed model of the Figma REST file JSON

The neutral model for design files: the .fig and .sketch readers map onto
it and an application's importer takes it. Typed records for the document
tree, paints, effects, text styles, layout grids, constraints, export
settings and the component and style metadata; every record keeps the
fields it has no name for, so a file read and written loses nothing.
The vectorNetwork extension is typed and documented with the others.

The layer tree is converted level by level in both directions, and the
depth and node limits are checked on the JSON before any typed value is
built.

* craft-figma-rest: Warnings and style_name for the readers that build this shape

Moved from FigCraft's REST importer and .fig reader (crates/format/src/rest.rs
Warnings, fig.rs style_name), which the .fig and Sketch mappings share.

* craft-fig: map the message onto the REST file JSON

FigCraft's .fig mapping (crates/format/src/fig/map and fig/text_path.rs
at e8b53e33) moved onto craft-figma-rest: rest::to_rest turns a decoded
message into the REST API file JSON, with the vector networks layers name
by vectorNetworkRef handed over beside it.

Connector routing and the text path measure check are the caller's
geometry (rest::Geometry; rest::Straight when it has none). The plugin
data key under which a writer keeps an outlined stroke is an option.
The geometry blob is read with craft_fig::path instead of a second
decoder.

* craft-fig: an empty overriddenSymbolID is no swap

The empty GUID (4294967295:4294967295) in overriddenSymbolID means "no
swap". The mapping took it as the main, found no node and made the
instance an empty frame; it now keeps the main its symbolData names, in
the instance mapping and in the list of mains a file's instances use.

Ported from storytold/figcraft#2208.

* craft-fig: a swapped nested instance takes its new main's look

A nested instance swapped to another main stores the previous main's
look on its own node, so the mapping drew that. Each group of the look
(fills, strokes, stroke settings, effects, corners) whose stored values
are a copy of the previous main's now takes the new main's; a group the
node or a row above it changes is kept, and a style link is decided by
the topmost layer that names it.

Ported from storytold/figcraft#2204 (the fig/map part; the warning's
locale entry stays in the app).

* craft-fig: a vector point without a corner radius of its own takes the layer's

A point whose style sets no cornerRadius, or that has no style, takes the
layer's cornerRadius, as the file draws it; a point's own value (an
explicit 0 included) still wins. The layer's radius is part of the
network cache key, so an instance overriding the radius doesn't share its
main's network.

Ported from storytold/figcraft#2115 (the fig/map part; its export change
stays in the app).

* craft-fig: map deep files within a 1 MiB stack

The mapping recursed once per level of layer nesting in a frame of about
2 KB (an unoptimised build): 300 nested frames, or a chain of instances
of mains holding each other, overflowed a 1 MiB stack before the 256
level limit could stop them, a crash on hostile input.

The work on one layer now happens in layer_head and layer_tail, which
return before and after its children are mapped, and what they hand over
is boxed, so the recursive frame is about 800 bytes. An unoptimised build
now maps past the limit on 512 KiB; the limit stays 256. The test at the
limit runs on a 1 MiB thread, for frames and for instances, and
overflowed before this change.

---------

Co-authored-by: Trevor Brown <trevorbrown.dev@gmail.com>
Co-authored-by: Giovanni Almeida <gioalmeida07@gmail.com>
2026-11-11 20:15:28 -05:00
Dylan M. Taylor 00efe17d1a craft-drawingml: readers and writers on craft-xml (#979)
* 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.

* drawingml: the model types for fills, lines, text, tables, charts and diagrams

Moved from DeckCraft (60f91f78) crates/model/src: style.rs, text.rs,
table.rs, chart.rs, numfmt.rs and trendline.rs, plus Diagram, DiagramNode
and the MediaId and SlideId newtypes from lib.rs.

The files are unchanged except that deckcraft_color:: is crate::color:: and
Fill and TableBorder allow clippy::large_enum_variant. Their 60 tests come
with them. Readers and writers follow.

* drawingml: the theme model (colour, font and format schemes)

Moved from DeckCraft crates/model/src/theme.rs at 60f91f78: FontSet, ScriptFont, FontScheme,
FormatScheme, Theme and default_format. The app's theme gallery, its variants and its own default
theme stay in the app. Theme::default() is a plain theme built in place, so nothing here depends
on a gallery.

* drawingml: readers and writers for fills, lines, text, charts, themes, table styles and diagrams

Moved from DeckCraft crates/pptx/src at 60f91f78: read/dml.rs, write/dml.rs, read/chart.rs,
read/chartex.rs, write/chart.rs, read/theme.rs, write/theme.rs, tables.rs and dgm.rs, onto
craft-xml. Package lookups (Imp, Part, Exp, Out) are the caller's part::Source and part::Sink.
The chart frame budget and kept-part capture stay with the presentation reader.

* drawingml xmlx: copy only used, bounded namespace declarations onto kept fragments and mc branches

From storytold/deckcraft#1740 (988a4920), which landed after the extraction commit 60f91f78.
2026-11-11 20:15:06 -05:00
Dylan M. Taylor 384da3e951 craft-timeline: Final Cut Pro 7 XML (xmeml) on the neutral model (#978)
* craft-timeline: a neutral timeline model and the import builder

model: a Project of bins and items (media, sequences, subclips, and items no
interchange format carries), sequences of tracks with clips, transitions,
markers and effects with keyframes. Cut down from FilmCraft's project model to
what Final Cut Pro 7 XML, FCPXML and OpenTimelineIO describe; EffectCraft
already uses that shape for its interchange. Three things stay the
application's: effects (looked up through a Catalog the caller supplies),
generators beyond the common ones (Generator::Other with an opaque payload),
and per-object extension JSON (ext) that a format can carry unread.

fx: the effect and parameter ids the formats name themselves, and Standard,
a catalog of just those.

build: FilmCraft's import Builder and shared helpers (filmcraft-interchange
common.rs at 4bec22bd) on the model, with the effect registry replaced by the
catalog, and the bounds every import ends with (Imported::bound, from lib.rs).

* craft-timeline: Final Cut Pro 7 XML (xmeml) on the neutral model

fcp7: FilmCraft's xmeml reader and writer (filmcraft-interchange fcp7.rs at
4bec22bd) moved onto craft_timeline::model, behind feature xml.

- XML is read with craft-xml, strictly: at most 256 levels of nesting (the
  limit EffectCraft checked with its own depth scan before handing documents
  over), 2 000 000 nodes, 256 MiB; a document type declaration with an internal
  subset and entities XML does not define are refused. It is written with
  craft-xml's Writer.
- Effects other than motion, opacity, time remap, audio levels and pan are
  looked up through the caller's Catalog, by id and by name, and written with
  the catalog's stable name and category.
- An application generator round-trips as <namespace>:<payload> in effectid
  (FilmCraft wrote filmcraft:<json>); transparent video, counting leader and
  bars and tone are written by name, which the reader already took.
- Finding the sequence a clip item is in is one walk of the document instead of
  an ancestors() lookup per clip item.

tests/it: FilmCraft's FCP7 integration tests (the hand-written Premiere-style
document, the rich round trip, the 48-case property round trip, split mono and
stereo channels, sub-frame spans, absurd timebases and start timecodes, the
link-group bound) on the model, with a test catalog for the four application
effects they use.
2026-11-11 20:13:25 -05:00
Dylan M. Taylor 026256d8cc craft-mime: detect, for bytes that name no character set (#973)
is_text, decode (BOM, unmarked UTF-16, UTF-8, else the legacy encoding
the bytes look like), legacy and legacy_label, behind a detect feature
that brings in chardetng. Moved from WordCraft's crates/formats/src/txt.rs
at 06c2ae6f with its text-sniffing tests. Also charset::is_known, so a
caller can tell a label decode would read as UTF-8 from one it knows.
2026-11-11 20:10:17 -05:00
75471d19bd craft-metadata: fixes ported from open LightCraft pull requests (#967)
* craft-metadata: EXIF, IPTC-IIM, XMP and GPX from LightCraft

lightcraft-meta at 5b61ce1e, as one crate with a module per format: exif
(read, write, maker-note lens, focus point, tag listing), iptc, xmp (parse,
write, merge), gpx, and the JPEG, PNG, WebP, TIFF, RAF and CR3 containers.
They share Metadata, DateTime, Gps and Orientation.

Changed in the move:
- TIFF comes from craft-tiff; Point, Rect and Orientation are the crate's own.
- The app namespace is a parameter: parse_xmp_with, write_xmp_with and
  MergeRules::namespaces take it, and XmpData no longer has lc_settings.
- The HEIF, PSD and MRW readers stay out, since craft-heif, craft-psd and
  craft-raw read those containers; from_embedded takes the blocks they find.

* craft-metadata: a RW2 reads ISO and the rest of its Exif from the JpgFromRaw

A RW2's own Exif IFD is a short subset. What it lacks is filled from the Exif
of the embedded JpgFromRaw, then from the ISO tags of the Panasonic IFD0
(0x0017, 0x0037), each only in its recorded field type. 65535 in a SHORT
ISOSpeedRatings is the overflow marker and states no ISO. The private tags
are read only in a RW2 stream, under the caller's parse options.

Ported from storytold/lightcraft#3518.

* craft-metadata: the Exif rating is written, and 65535 reads as rejected

write_exif writes Rating (0x4746), with 0xFFFF for a rejected photo, and
from_tiff reads 65535 as -1 instead of clamping it to 5.

Ported from storytold/lightcraft#1995.

* craft-metadata: one shutter-speed formatter, exact to four digits

format_exposure_time shows a duration within 0.1% of 1/n second as 1/n and
anything else to four significant digits, so 0.3 s, 1/2.8 s and 0.625 s are
no longer rounded to a reciprocal or one decimal. Metadata::exposure_display
and the ExposureTime tag row both use it.

Ported from storytold/lightcraft#1692.

* craft-metadata: JPEG XL containers give their Exif and xml boxes

The first Exif and xml box of a JPEG XL container, plain or Brotli-wrapped
in brob, honouring the Exif box's TIFF header offset. A brob payload is
capped at 32 MiB. A bare codestream has no metadata.

Ported from storytold/lightcraft#3756.

* craft-metadata: XMP Resource structs flatten their attribute fields

A property with rdf:parseType="Resource" may give its fields as attributes
as well as child elements. Only the children were flattened into
XmpData::properties; the attributes now are too, as for a nested
rdf:Description.

Ported from storytold/lightcraft#1226 (the XMP part).

* craft-metadata: a capture date of just a year or a month stays as stated

XMP dates may stop at the year (1983) or the month (1983-04). DateTime now
has a DatePrecision; such a date writes back to XMP as stated and only in
photoshop:DateCreated, and write_exif leaves it out, since an Exif date
cannot say it.

Ported from storytold/lightcraft#3502.

* craft-metadata: the linear-time scan test compares times, not a fixed bound

The test asserted that 200 000 attributes scan in under 5 s, which failed on
a loaded machine. It now scans 25 000 and 100 000 attributes, takes the best
of three runs of each, and asserts that four times the attributes take less
than ten times as long. Linear scans give about 4; a quadratic duplicate
check gave 18.6 and fails.

* craft-metadata: interleave the linear-time scan test's runs

The best-of-three ratio still failed once under load. The test now alternates
five runs of 50 000 and 200 000 attributes and compares the fastest of each,
so a burst of load slows both sizes. Six runs at load average 55 gave ratios
of 3.9 to 5.4 against the bound of 10.

---------

Co-authored-by: radialmonster <radial@gmail.com>
Co-authored-by: Sebastian Berschneider <berschi@gmail.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Michael Anderson <1427851+endthestart@users.noreply.github.com>
Co-authored-by: Ilya Dorman <ilya@codeforhumans.dev>
Co-authored-by: Clyde C Brown <clyde.c.brown@pm.me>
2026-11-11 20:09:09 -05:00
Dylan M. Taylor 55d120f201 Add craft-figma-rest: a typed model of the Figma REST file JSON (#954)
* Add craft-figma-rest: a typed model of the Figma REST file JSON

The neutral model for design files: the .fig and .sketch readers map onto
it and an application's importer takes it. Typed records for the document
tree, paints, effects, text styles, layout grids, constraints, export
settings and the component and style metadata; every record keeps the
fields it has no name for, so a file read and written loses nothing.
The vectorNetwork extension is typed and documented with the others.

The layer tree is converted level by level in both directions, and the
depth and node limits are checked on the JSON before any typed value is
built.

* craft-figma-rest: Warnings and style_name for the readers that build this shape

Moved from FigCraft's REST importer and .fig reader (crates/format/src/rest.rs
Warnings, fig.rs style_name), which the .fig and Sketch mappings share.
2026-11-11 20:05:20 -05:00
Dylan M. Taylor 6f05ad84a3 Move craft-opc, -odf, -kra, -ora, -penpot, -fig and -sketch onto craft-zip (#995)
* craft-kra, craft-ora: read and write through craft-zip

Both drop their own bounded entry read (and craft-ora its zip writer). New: the declared entry
count is checked before the archive is indexed (MAX_ENTRIES, 100,000), and an archive with two
entries of one name is refused. craft-ora writes a fixed timestamp. zip_mimetype moves into
craft-zip as stored_mimetype; both crates keep their function.

* craft-zip: Archive is Clone; compressed_size and read_head

For the package crates moving onto it: a clone reads behind &self with its own count,
compressed_size gives a format the ratio an entry claims, and read_head reads a signature
without inflating the rest.

* craft-sketch: read through craft-zip

Its own budgets (ratio, per-member, JSON total, image total) stay. New: the declared entry
count is checked before the archive is indexed (MAX_ENTRIES, 100,000), also when sniffing, and
an archive with two entries of one name is refused.

* craft-fig: read and write through craft-zip

The image budget (per image, total, charged for failed reads, skipped count) stays this crate's.
A failed image read is now charged its declared size, which bounds it because the read stops one
byte past it; the separate checksum check at an exact budget goes, as the read now always reaches
the end of the entry. New: the declared entry count is checked before the archive is indexed
(MAX_ENTRIES, 1,000,000), also when sniffing, and two entries of one name are refused. The
writer writes a fixed timestamp.

* craft-penpot: read and write through craft-zip

The 1 GiB byte budget is now the archive's total in craft-zip, counted from bytes actually read;
the JSON value budget and the sticky exhaustion flag stay here. Duplicate names are found by
craft-zip (the hand-read end record goes). A read that does not fit what is left is refused
before it starts and fails every later check(); one that fails otherwise is charged its declared
size. embedded_fonts reads four bytes of each candidate with read_head. New: the entry count is
checked before the archive is indexed.

* craft-opc: read and write through craft-zip

The entry-count guard and its test sample move to craft-zip; check_entry_count and
zip64_entry_count_sample stay here as wrappers. Package and LazyPackage keep their own counts and
rules and read through craft-zip's bounded read. A part that declares more than is left is now
refused before it is inflated, so it no longer spends the inflate budget (the test that pinned
the old charge is updated). An exactly repeated entry name is logged, not refused. ZipOut wraps
craft-zip's Writer; its output is unchanged in kind (level 6, fixed timestamp).

* craft-odf: read and write through craft-zip

The entry-count guard moves to craft-zip; zip_guard::check and zip64_entry_count_sample stay
public here as wrappers. Package keeps its own count and reads through craft-zip's bounded
read: a damaged file is still Ok(None), now charged its declared size. Two files of one name
are refused. encrypt read every file whole and decrypt cut a file at the limit without saying
so; both now refuse a file declaring more than the per-file limit.

* craft-ora: a needless borrow in a test; Cargo.lock for the crates now on craft-zip
2026-11-11 19:48:05 -05:00
Dylan M. Taylor 83eb8f4422 craft-exr: bounded part listing, flat decoding and writing (feature decode) (#992)
PhotoCraft's exr-crate wrapper (preview check, per-part and total size limits,
file-size plausibility, part ranking, one-part decode) and EffectCraft's
streaming channel filter, as one reader that returns a neutral model: part
headers with text attributes, and the kept channels in their stored type.
2026-11-11 19:47:07 -05:00
Dylan M. TaylorandMartin Ball 977208a013 Add craft-ews: the EWS SOAP layer from MailCraft (#988)
MailCraft crates/ews at a44a450: the request envelope and the escaping
element builder (soap.rs), and the judging of an answer's envelope, faults
and response messages (reply.rs, the response codes of error.rs), now read
with craft-xml in place of MailCraft's own XML reader. The operations, HTTP,
Autodiscover, authentication and sync stay in MailCraft.

Co-authored-by: Martin Ball <98639962+benandez@users.noreply.github.com>
2026-11-11 19:46:19 -05:00
Dylan M. TaylorandMartin Ball 8c6dbccb8b Add craft-jmap: the JMAP protocol core from MailCraft (#982)
MailCraft crates/jmap's protocol core at a44a450, moved as it is with all
its tests: session, requests with result references, responses, ids,
addresses and templates, credentials, retry and the judgement of HTTP
answers, over any HttpExchange. The mail client on top stays in MailCraft.

Co-authored-by: Martin Ball <98639962+benandez@users.noreply.github.com>
2026-11-11 19:45:03 -05:00
Dylan M. Taylor 08882273d1 Add craft-wordmodel: WordCraft's document model without its editing code (#963)
* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.
2026-11-11 19:43:08 -05:00
Dylan M. Taylor 01173ddad7 craft-webp: WebP container reader and writer (#958)
The WebP container code the apps wrote themselves: EffectCraft's chunk walk
(canvas, alpha, animation frames and their durations) and PhotoCraft's RIFF
writer around an encoded VP8 frame with alpha, ICC, EXIF and XMP chunks. The
codecs are not part of the crate.
2026-11-11 19:42:34 -05:00
Dylan M. Taylorandkiwi4boy 119415f46f Add craft-xlsx: XLSX and XLSB part layer (workbook, strings, styles, streamed rows) (#976)
* Add craft-sheet: the cell positions, values and tables the spreadsheet formats share

craft-xls carried its own CellRef, Range, Value, CellError, Visibility, sheet-name
quoting, built-in number format table and indexed palette. craft-xlsx and craft-ods
need the same ones, so they move to a crate with no dependencies and craft-xls
re-exports them: an application gets one Value and one CellRef whichever file it read.

Added on the way: A1 parsing (CellRef::parse, Range::parse), Range::count and a1,
CellError::parse and the newer error values SpreadsheetML stores as text,
builtin_format_id for writers.

* Add craft-xlsx: the part layer of XLSX and XLSB workbooks

Package::open finds the workbook part through craft-opc and reads the workbook
part, shared strings (runs and phonetic readings), the styles tables as indexed
tables, and a worksheet's rows one at a time with the rest of the part as a tree.
Binary workbooks give the same workbook, strings, rows and merged cells from their
records. Every other part stays reachable through Package::opc.

From GridCraft crates/xlsx (7e0d96d7): read.rs, sheet_read.rs, styles.rs, xml.rs
(string items, xstring escapes), xlsb.rs and its tests, split from GridCraft's
document model.

* craft-xlsx: XLSB opens up to 4 million cells, records counted once over the workbook

Binary workbooks over 1 million cells were refused, a quarter of what .xls allows,
and an allowance of 5 million records per part refused ordinary files: Excel writes
four records per non-empty row besides the cells, and one BrtCellBlank for every
formatted empty cell. Cells are now 4 million over the workbook (also the most
shared strings), text 256 MiB, and records 40 million for every part together, as
in craft-xls.

Ported from storytold/gridcraft#3807.

---------

Co-authored-by: kiwi4boy <kiwi4boy@gmail.com>
2026-11-11 19:32:04 -05:00
Dylan M. Taylor fa26a2a4c6 Add craft-pdn: Paint.NET PDN3 reader and a data-only MS-NRBF reader, from PhotoCraft (#968)
The format half of PhotoCraft's Paint.NET import (crates/io/src/pdn.rs and pdn/nrbf.rs at
abeb16c96): the PDN3 header, the .NET binary serialization graph, the layer list and blend
modes, and the chunked gzip pixel blocks. Surfaces, blend-mode mapping and warnings stay in
the app.
2026-11-11 19:30:55 -05:00
Dylan M. TaylorandMartin Ball ed16993b3b Add craft-ical: iCalendar lines, values, rules and VTIMEZONE from MailCraft (#966)
MailCraft crates/ical at a44a450: content lines, DATE/DATE-TIME values,
durations and text, the common subset of recurrence rules, and VTIMEZONE
definitions read and written, with their tests. Times are chrono values; the
zone database the writer needs is the caller's, through ZoneRules. The event
and to-do mapping onto MailCraft's model stays in MailCraft.

Co-authored-by: Martin Ball <98639962+benandez@users.noreply.github.com>
2026-11-11 19:30:23 -05:00
Dylan M. TaylorandMartin Ball 53e1717523 craft-rtf: Outlook compressed RTF and body text, from MailCraft (#961)
The outlook feature adds MailCraft's reader of PidTagRtfCompressed
([MS-OXRTFCP]) and of the text and encapsulated HTML ([MS-OXRTFEX]) of the
RTF inside, from crates/pst/src/rtf.rs at a44a450. Font charsets use this
crate's charset_code_page table; code pages are decoded by craft-mime.

Co-authored-by: Martin Ball <98639962+benandez@users.noreply.github.com>
2026-11-11 19:25:27 -05:00
Dylan M. Taylor 219f63f64c Add craft-xcf: GIMP XCF reader from PhotoCraft (#960)
* Add craft-xcf: GIMP XCF reader from PhotoCraft

The format half of PhotoCraft's XCF import (crates/io/src/xcf.rs at abeb16c96): the header,
precisions, property lists with their length repairs, parasites, layer structures and item
paths, channels, and the uncompressed, RLE and zlib tiles. The mapping onto a document (blend
modes, surfaces, profiles, warnings) stays in the app.

* craft-xcf: an RLE run of zero length is an error

A long run (op 127 or 128) with a 16-bit length of 0 consumed input without producing output,
so a tile of them was work the pixel budget never charged.

Ported from storytold/photocraft#11138.

* craft-xcf: channel names are charged to the metadata budget

Layer names already were. Many channel pointers to one channel with a large name made a copy
per read, multiplying a small file into gigabytes of retained names.

Ported from storytold/photocraft#11138.
2026-11-11 19:25:01 -05:00
Dylan M. Taylor eb640fd992 Add craft-sheet: the cell positions, values and tables the spreadsheet formats share (#956)
craft-xls carried its own CellRef, Range, Value, CellError, Visibility, sheet-name
quoting, built-in number format table and indexed palette. craft-xlsx and craft-ods
need the same ones, so they move to a crate with no dependencies and craft-xls
re-exports them: an application gets one Value and one CellRef whichever file it read.

Added on the way: A1 parsing (CellRef::parse, Range::parse), Range::count and a1,
CellError::parse and the newer error values SpreadsheetML stores as text,
builtin_format_id for writers.
2026-11-11 19:24:39 -05:00
Dylan M. Taylor 16936716aa Add craft-stream-codecs: the codec modules of PdfCraft's stream filters (#953)
LZW, ASCII85, ASCIIHex, RunLength, the TIFF and PNG predictors, bounded CCITT
fax decoding (through hayro-ccitt) with Group 4 encoding (through fax), and
the lossless JBIG2 encoder with its MQ coder, from pdfcraft-filters at
PdfCraft revision 80e26e674d1aa33078e83c8139875047f3289eb6.

The crate sits below craft-pdf and craft-tiff: it knows no container. The
PDF filter chain, Flate and the bounded JBIG2 decoding entry are not part of
it.
2026-11-11 19:24:06 -05:00
Dylan M. Taylorandskylar taylor-barrick 58f6c4b9e8 craft-idml, craft-epub: read and write through craft-zip (#950)
* craft-zip: ZIP preflight, bounded entry read and Budget (from craft-archive, #129)

The ZIP half of the craft-archive proposal (#126, #129): an allocation-free check of the end
records and central directory that runs before a ZIP reader indexes the archive, a bounded
entry read that distrusts the declared size, and the named Budget both charge. The XML half
of #129 is not carried: craft-xml bounds XML now.

* craft-zip: Archive (entry-count guard, duplicate names, budgeted reads) and Writer

check_entry_count is craft-opc's guard: the declared entry count, ZIP64 included, checked on
the raw bytes before zip parses the central directory. Archive reads entries within a per-entry
limit and a total Budget through read_entry. Duplicate names are found by counting the central
directory headers against the entries zip kept. Writer is craft-opc's ZipOut: stored or deflated
entries, ZIP64 for large ones, one fixed timestamp.

* craft-idml, craft-epub: read and write through craft-zip

Both crates drop their own bounded ZIP reads and zip writers. They gain the entry-count check
before the archive is indexed (ZIP64 included) and archives whose bytes depend only on their
content (one fixed timestamp).

---------

Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-11-11 19:23:15 -05:00