Commit Graph
3 Commits
Author SHA1 Message Date
01cbc50269 metafile: dual EMF+ files draw their EMF+ records (stacked on #843) (#845)
* metafile: hold a placed pen width to what covers the frame

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

Ported from storytold/wordcraft#5427.

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

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

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

* metafile: a negative crop side pads the picture

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

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

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

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

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

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

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

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

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

Ported from storytold/deckcraft#1464.

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: David Lin <woei66@gmail.com>
Co-authored-by: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com>
2026-11-11 15:26:10 -05:00
75895c0321 metafile: pen width cap, shared byte budget, negative crops, code pages (#843)
* metafile: hold a placed pen width to what covers the frame

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

Ported from storytold/wordcraft#5427.

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

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

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

* metafile: a negative crop side pads the picture

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

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

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

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

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

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: David Lin <woei66@gmail.com>
2026-11-11 14:25:35 -05:00
Dylan M. Taylor 9dfc35965c Add craft-metafile: WMF and EMF reader and writer (#830)
* craft-metafile: DeckCraft's WMF, EMF and EMF+ reader with WordCraft's changes merged

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

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

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

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

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

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

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

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

The region node cap gets a test of its own (a balanced tree of intersections),
and a turned bitmap's rectangle is the box around its four corners, so a caller
that draws rectangles still has an area to draw into.
2026-11-11 13:39:28 -05:00