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.
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-11 13:39:28 -05:00
1 parent b59aeeb10a
commit 9dfc35965c
27 files changed
+11890

No files matched your search

Generated
+34
View File
@@ -1061,6 +1061,15 @@ dependencies = [
"serde_json",
]
[[package]]
name = "craft-metafile"
version = "0.1.0"
dependencies = [
"image",
"kurbo 0.13.1",
"thiserror 2.0.21",
]
[[package]]
name = "craft-mime"
version = "0.1.0"
@@ -1921,6 +1930,12 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "fax"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
[[package]]
name = "fdeflate"
version = "0.3.7"
@@ -2964,9 +2979,14 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"gif",
"moxcms",
"num-traits",
"png 0.18.1",
"tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
@@ -5935,6 +5955,20 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "tiff"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [
"fax",
"flate2",
"half",
"quick-error 2.0.1",
"weezl",
"zune-jpeg",
]
[[package]]
name = "time"
version = "0.3.55"
+1
View File
@@ -48,6 +48,7 @@ craft-media = { path = "crates/media", default-features = false }
craft-media-testkit = { path = "crates/media-testkit" }
craft-mime = { path = "crates/mime" }
craft-menu = { path = "crates/menu" }
craft-metafile = { path = "crates/metafile" }
craft-pcraft-format = { path = "crates/pcraft-format" }
craft-prepress = { path = "crates/prepress" }
craft-print = { path = "crates/print" }
+4
View File
@@ -88,6 +88,10 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
every field type, the IFD chain, SubIFDs and Exif/GPS/Interop pointer IFDs, maker-note IFDs
(Canon, Nikon, Sony, Fujifilm, Olympus, Panasonic, Pentax), strip and tile layout. Bounded and
panic-free on hostile input. Pixel decompression stays with the callers.
- [`craft-metafile`](crates/metafile/README.md): Windows metafiles, from DeckCraft, WordCraft and
VectorCraft: a WMF, EMF and EMF+ reader into a neutral picture (paths with line styles, pattern
fills, bitmaps, text, clip regions and clip paths) and, behind `write`, EMF and WMF writers.
Bounded and panic-free on hostile input. Mapping the picture onto a document stays in each app.
- [`craft-jpeg`](crates/jpeg/README.md): clean-room decoder for 12-bit and arithmetic-coded
JPEG (T.81 SOF0/SOF1 at 12 bits, SOF9, SOF10), from PhotoCraft, and a frame probe for routing.
Metadata stays in each app.
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "craft-metafile"
description = "Windows Metafile (WMF), Enhanced Metafile (EMF) and EMF+ reader into a neutral vector model, and EMF and WMF writers."
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
repository.workspace = true
[lints]
workspace = true
[features]
# The EMF and WMF writers.
write = []
[dependencies]
kurbo = { workspace = true }
thiserror = { workspace = true }
# JPEG and PNG bitmaps (BI_JPEG, BI_PNG) and the image files EMF+ carries.
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "tiff", "bmp"] }
+210
View File
@@ -0,0 +1,210 @@
# craft-metafile
Windows metafiles for the Craft apps: one reader for WMF, EMF and EMF+, and writers for EMF and WMF.
- `parse(bytes) -> Picture` plays a file's records on a model of the GDI device context and
returns what was drawn: paths, pattern fills, bitmaps and text, each with its clip.
- `size(bytes)` gives the picture's size from the header alone when the header decides it.
- `sniff(bytes) -> Option<Kind>` and `is_metafile(bytes)` recognise the formats.
- `Picture::fit` and `Picture::place` fit a picture (with crop fractions) into a rectangle for
callers that draw it; `crop_window` is the crop rule they share with bitmaps.
- `write::emf` and `write::wmf` (feature `write`) write a list of drawing operations.
The crate depends on `kurbo`, `thiserror` and `image` (PNG, JPEG, GIF, TIFF and BMP decoders, for
the image files that EMF+ and `BI_JPEG`/`BI_PNG` bitmaps carry). It knows no fonts, no file system
and no application.
## The model
A `Picture` has a size (points when the file states one: the EMF frame, the placeable WMF box;
device or logical units otherwise) and `items` in paint order, in `0..width` by `0..height`, y down:
| Item | What it holds |
|---|---|
| `Item::Path` | Segments (`Seg`: move, line, cubic, close), a fill colour, a stroke colour and width (0 is a hairline), the stroke's `LineStyle` (cap, join, miter limit, dashes), the fill rule. |
| `Item::Pattern` | A path filled with a repeating RGBA `Tile`: hatch and bitmap brushes, EMF+ gradients and textures. Fills with the same brush share one tile. |
| `Item::Bitmap` | RGBA8 pixels (straight alpha, rows top-down), the rectangle they fill and a `transform` that also holds a turn or a skew (then the rectangle is the box around it). |
| `Item::Text` | The string, the font the file asks for (family, height, weight, italic, underline, strike, character set), per-character advances, colours, alignment and a transform from text space. |
Every item carries the clip it was drawn under: `region` (the GDI clip region as rectangles) and
`clips` (clip paths from `SELECTCLIPPATH`, outermost first; items under the same clip share one
`Arc<ClipPath>`). An item shows inside its region and inside each of its clip paths.
`Picture::skipped_records` counts records of a type the reader does not play, so a caller can say
that part of a file was not read.
### Why this model
The three apps had two output models. WordCraft's and DeckCraft's readers produced a picture of
paths, bitmaps and text to draw. VectorCraft's reader built VectorCraft document nodes directly,
and kept what an editor needs and a renderer can lose: the pen's caps, joins and dashes, clip
paths as clips, and the turn of an image. The shared model is the picture, extended with exactly
those three things (`LineStyle`, `ClipPath`, `Item::Bitmap::transform`), so an editor can rebuild
objects from it and a renderer can draw it. Bitmaps are decoded RGBA rather than raw DIB bytes
because every caller needs pixels (two draw them, one re-encodes them as PNG), and because the
decoder is where the size, pixel and work limits have to be enforced.
## Records read
### WMF (MS-WMF)
| Group | Records |
|---|---|
| Headers | Placeable header, memory and disk metafile headers. |
| Mapping | `SETMAPMODE`, `SETWINDOWORG`, `SETWINDOWEXT`, `SETVIEWPORTORG`, `SETVIEWPORTEXT`, all map modes (text, metric, isotropic, anisotropic). A placeable file is played into its box the way Office plays it. |
| State | `SAVEDC`, `RESTOREDC`, `SETPOLYFILLMODE`, `SETROP2`, `SETBKMODE`, `SETBKCOLOR`, `SETTEXTCOLOR`, `SETTEXTALIGN`. |
| Objects | `CREATEPENINDIRECT` (style, cap and join bits), `CREATEBRUSHINDIRECT` (solid, null, hatched), `DIBCREATEPATTERNBRUSH`, `CREATEFONTINDIRECT`, `SELECTOBJECT`, `DELETEOBJECT`, stock objects. `CREATEPALETTE`, `CREATEPATTERNBRUSH` and `CREATEREGION` take their handle slot. |
| Drawing | `MOVETO`, `LINETO`, `POLYLINE`, `POLYGON`, `POLYPOLYGON`, `RECTANGLE`, `ROUNDRECT`, `ELLIPSE`, `ARC`, `PIE`, `CHORD`, `PATBLT`. |
| Bitmaps | `STRETCHDIB`, `DIBSTRETCHBLT`, `DIBBITBLT` (with or without a source). |
| Text | `EXTTEXTOUT` (rectangle, opaque, clipped, dx), `TEXTOUT`; code pages 1250, 1251, 1252, the symbol character set, double-byte lead bytes as blanks. |
| Clipping | `INTERSECTCLIPRECT`, `EXCLUDECLIPRECT`. |
| Not read | `OFFSETWINDOWORG`, `OFFSETVIEWPORTORG`, `SCALEWINDOWEXT`, `SCALEVIEWPORTEXT`, flood fills, `SETPIXEL`, region records (`FILLREGION`, `FRAMEREGION`, `PAINTREGION`, `INVERTREGION`, `SELECTCLIPREGION`, `OFFSETCLIPRGN`), the device-dependent `BITBLT`, `STRETCHBLT` and `SETDIBTODEV`. They are counted in `skipped_records`. |
### EMF (MS-EMF)
| Group | Records |
|---|---|
| Header | Frame, device size in pixels, millimetres and (when present) micrometres. |
| Mapping | `SETMAPMODE`, window and viewport origin and extent, `SCALEVIEWPORTEXTEX`, `SCALEWINDOWEXTEX`, `SETWORLDTRANSFORM`, `MODIFYWORLDTRANSFORM`. |
| State | `SAVEDC`, `RESTOREDC`, `SETPOLYFILLMODE`, `SETROP2`, `SETBKMODE`, `SETBKCOLOR`, `SETTEXTCOLOR`, `SETTEXTALIGN`, `SETMITERLIMIT`, `SETARCDIRECTION`. |
| Objects | `CREATEPEN`, `EXTCREATEPEN` (cosmetic and geometric, cap, join, user-style dashes), `CREATEBRUSHINDIRECT`, `CREATEMONOBRUSH`, `CREATEDIBPATTERNBRUSHPT`, `EXTCREATEFONTINDIRECTW`, `SELECTOBJECT`, `DELETEOBJECT`, stock objects. |
| Drawing | `MOVETOEX`, `LINETO`, `POLYLINE(16)`, `POLYLINETO(16)`, `POLYGON(16)`, `POLYBEZIER(16)`, `POLYBEZIERTO(16)`, `POLYPOLYLINE(16)`, `POLYPOLYGON(16)`, `RECTANGLE`, `ROUNDRECT`, `ELLIPSE`, `ARC`, `CHORD`, `PIE`. |
| Paths | `BEGINPATH`, `ENDPATH`, `CLOSEFIGURE`, `FILLPATH`, `STROKEPATH`, `STROKEANDFILLPATH`, `ABORTPATH`, `SELECTCLIPPATH` (`RGN_AND`, `RGN_COPY`). |
| Bitmaps | `BITBLT`, `STRETCHBLT`, `STRETCHDIBITS`, `SETDIBITSTODEVICE`, `ALPHABLEND` (constant and per-pixel alpha); source-less raster operations fill with the brush, black or white; `SRCAND`, `SRCPAINT` and `SRCINVERT` are drawn as they look over white. |
| Text | `EXTTEXTOUTA`, `EXTTEXTOUTW`, `SMALLTEXTOUT`; glyph-index strings are left out. |
| Clipping | `INTERSECTCLIPRECT`, `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN` (all five modes, on rectangles), `SELECTCLIPPATH`. |
| Not read | `ANGLEARC`, `ARCTO`, `POLYDRAW(16)`, `SETPIXELV`, flood fills, region paints, `MASKBLT`, `PLGBLT`, `TRANSPARENTBLT`, `GRADIENTFILL`, `POLYTEXTOUT`, `WIDENPATH`, `SETMETARGN`, `OFFSETCLIPRGN`, `SETLAYOUT`, `SELECTCLIPPATH` with `RGN_OR`, `RGN_XOR` or `RGN_DIFF` (the clip stays as it was). Unknown types are counted in `skipped_records`. |
### EMF+ (MS-EMFPLUS)
An EMF+ only file is played from its EMF+ records. A dual file is played from its EMF records.
| Feature | Read |
|---|---|
| Objects | Brushes (solid, hatch, texture, linear gradient with preset colours or blend factors, path gradient), pens (colour and width), paths, regions, images, fonts, string formats; objects continued over several records. |
| Drawing | `FillRects`, `DrawRects`, `FillPolygon`, `DrawLines`, `FillEllipse`, `DrawEllipse`, `FillPie`, `DrawPie`, `DrawArc`, `FillPath`, `DrawPath`, `DrawBeziers`, `DrawCurve`, `DrawClosedCurve`, `FillClosedCurve`, `FillRegion`, `Clear`. |
| Images | `DrawImage`, `DrawImagePoints`: raw pixel formats, PNG, JPEG, GIF, TIFF and BMP payloads, and nested metafiles (three levels). |
| Text | `DrawString` (alignment, wrapping to the layout width), `DrawDriverString` with character codes, and with glyph indices in the Windows core fonts. |
| State | World and page transforms, `Save`, `Restore`, containers. |
| Clipping | `SetClipRect`, `SetClipPath`, `SetClipRegion`, `ResetClip`, `OffsetClip`: rectangles and convex shapes cut the drawing itself; other shapes clip to their bounding rectangle; region nodes combine by intersect and union. |
| Not read | Pen caps, joins and dashes (EMF+ strokes have the default line style), image attributes and effects, glyph indices of other fonts, region XOR, exclude and complement, rendering hints, `SetRenderingOrigin`, the terminal-server records. |
### Bitmaps (DIB)
`BITMAPINFOHEADER` and larger headers at 1, 4, 8, 16, 24 and 32 bits with `BI_RGB` or
`BI_BITFIELDS`; `BITMAPCOREHEADER` at 1, 4, 8 and 24 bits; `BI_JPEG` and `BI_PNG`. Bottom-up and
top-down rows, source rectangles (only the rows and columns a record draws are decoded), mirrored
destinations. RLE compression is not decoded.
## Writers (feature `write`)
`write::emf(ops, region, app, title)` and `write::wmf(ops, region)` take `write::Op`s in points,
y down, in paint order: `Fill` (path, fill rule, colour), `Stroke` (path and `Pen`: colour, width,
cap, join, miter limit, dashes), `Image` (RGBA pixels, an affine placement, an opacity), `Clip`
and `Unclip`. `region` is the part of that space the picture covers.
- EMF keeps curves, writes geometric pens (`EXTCREATEPEN`; dashes as `PS_USERSTYLE` when the pen
can draw them, else one line per dash), path clips inside `SAVEDC`/`RESTOREDC`, opaque images as
`STRETCHDIBITS` and translucent ones as `ALPHABLEND`, each under a world transform.
- WMF flattens curves, writes 16-bit coordinates behind a placeable header (at most 1440 units an
inch, fewer for large pictures), and returns what it could not hold in `write::Wmf`
(`translucent_images`, `turned_images`, `clips_ignored`, `oversized_images`).
The reader reads back what the writers write; the tests round-trip both formats.
## Limits
Every size, count and offset from a file is checked before it is used, with `get` and checked
arithmetic. Past a limit the reader keeps what it has (a shorter picture) or leaves the item out;
it never panics and never allocates from an unchecked count. Each limit has a test.
| Limit | Value | Past it |
|---|---|---|
| Items in a picture | 200,000 | The parse stops. |
| Bytes of path points and pixels kept (pattern brushes and clip paths included) | 256 MiB | The parse stops. |
| Points in one path bracket; points in one record | 2,000,000 | The bracket stops growing; the record is left out. |
| Object table slots | 4,096 | The object is not stored. |
| `SAVEDC` depth | 1,024 | The parse stops. |
| Clip paths nested on one device context | 32 | The parse stops. |
| Rectangles in a clip region | 64 | Further rectangles are dropped (this can only hide drawing). |
| Pen style entries | 16 | Further entries are dropped. |
| Pixels in one bitmap | 40,000,000 | The bitmap is left out. |
| Bitmap side drawn | 30,000 | The bitmap is left out before it is decoded. |
| JPEG or PNG side | 32,768 | The image is left out before it is decoded. |
| JPEG and PNG pixels decoded in one file | 2^28 | Further images are left out before they are decoded. |
| Pattern tile side | 256 | The brush is not made. |
| Characters in one string | 16,384 (EMF+: 32,768) | The string is cut. |
| EMF+ objects, save stack | 64, 64 | Ignored. |
| EMF+ points in one record; region nodes | 100,000 | The list is cut; the region is not made. |
| EMF+ region depth | 32 | The region is not made. |
| EMF+ gradient stops | 256 | The count is cut. |
| EMF+ continued object | 64 MiB | The size is cut; nothing is reserved ahead. |
| Nested metafiles | 3 levels | The inner picture is not read. |
| Clipping work per item, and per clip area | 1,000,000 and 4,000,000 steps | The item is left out; the clip keeps its previous area. |
| Masking a bitmap to a clip that is not a rectangle | 5,000,000 point tests | The bitmap is left out. |
| Placed path elements (`Picture::place`) | 500,000 | Placing stops. |
| Placed coordinates (`Picture::place`) | ±1,000,000 | The item is left out. |
| Writers: dashes cut from one stroke | 100,000 | The stroke is written solid. |
| Writers: segments from one curve | about 4,096 | The tolerance is raised. |
The tests parse every prefix of sample files, files with each byte replaced, and random bytes.
## What stays in the apps
- Mapping a `Picture` onto the app's document: VectorCraft builds paths, clipping groups, images
and point type from the items (joining consecutive lines into one path, dropping a clip that
covers the whole frame, turning skipped records and hatch and pattern fills into its warnings);
WordCraft and DeckCraft keep a picture as a picture.
- Drawing: rasterising and PDF output of placed items, font lookup, shaping and glyph drawing
(`Text` only names a font), caching of parsed pictures.
- For the writers: walking the document into `write::Op`s. Gradients, pattern fills, type as
outlines, stroke alignment, width profiles, arrowheads, effects and opacity are decided there,
as is scaling an image down before it is written and the wording of any warning.
## Origin
| Part | From |
|---|---|
| The reader (`canvas`, `emf`, `wmf`, `geom`, `place`, `bytes`) | DeckCraft `crates/metafile` at `cf0d7fbb`, itself ported from WordCraft at `05679497`. |
| EMF+ (`emfplus`), clip regions (`clip`, `cliprect`), text (`text`), pattern and hatch brushes | DeckCraft. |
| The cropped, depth-once DIB decoder (`dib`), the bitmap side cap, `size`, `crop_window`, the free-slot object table, capacity-based budget charging, the exact reservation for poly-polygon groups | WordCraft `crates/metafile` at `639b8133`, merged three ways over the common ancestor. |
| `LineStyle`, clip paths, `Item::Bitmap::transform`, `skipped_records`, the records behind them (`EXTCREATEPEN` entries, `SETMITERLIMIT`, `SETARCDIRECTION`, `SCALEVIEWPORTEXTEX`, `SCALEWINDOWEXTEX`, `SELECTCLIPPATH`, `ABORTPATH`), closing figures in `STROKEANDFILLPATH`, the micrometre device size, core-header, JPEG and PNG bitmaps, the per-file decode budget | VectorCraft `crates/metafile` at `3deafc24` (its reader's behaviour, written anew on this model). |
| The writers (`write`) | VectorCraft `crates/metafile` (`emf.rs`, `wmf.rs`, the encoding half of `dib.rs`, `vectorcraft_geom::flatten`). |
All three sources are MIT OR Apache-2.0. The tests build every file they read in code; the crate
bundles no sample files.
## Migration
### DeckCraft (`deckcraft-metafile`)
| Before | After |
|---|---|
| `deckcraft_metafile::{parse, is_metafile, Picture, Item, Seg, Tile, Text, Font, HAlign, VAlign, SYMBOL_CHARSET, PlacedItem, MAX_BITMAP_SIDE, Error}` | The same names in `craft_metafile`. |
| `Item::Path { segs, fill, stroke, even_odd, region }` | Adds `line` and `clips`. Draw `line` when stroking; clip to each of `clips` as to `region`. |
| `Item::Bitmap { rect, .. }`, `Item::Pattern { .. }`, `Text` | Add `clips`; `Item::Bitmap` adds `transform` (equal to `rect` unless the bitmap is turned). |
| `PlacedItem::*` | Add `clips` (`PlacedClip`: a path and its fill rule); `Path` adds `line`; `Bitmap` adds `transform`. |
| `Picture { width, height, items }` | Adds `skipped_records`. |
| `Picture::fit` with crops clamped to 95% a side | `crop_window`: each axis keeps at least 0.5%. |
### WordCraft (`wordcraft-metafile`)
| Before | After |
|---|---|
| `parse`, `size`, `is_metafile`, `Picture`, `Seg`, `Error`, `MAX_BITMAP_SIDE`, `crop_window`, `Picture::{has_size, fit, place}`, `Item::take_pixels` | The same names in `craft_metafile`. |
| `Item::Path`, `Item::Bitmap` | Add `region`, `clips`, `line`, `transform` as above; new `Item::Pattern` (hatch and pattern brushes now draw). |
| `Text { text, x, y, size, cell, font, bold, italic, color, angle, halign, valign, dx }` | `Text { transform, text, dx, font: Font { family, height, weight, italic, .. }, color, background, h_align, v_align, clip, region, clips }`. The reference point and angle are in `transform`; `size` is `font.height.abs()` in text units and `cell` is `font.height > 0`; `bold` is `font.weight >= 600`. A height of 0 and an empty family are the file's "default font": WordCraft's 16-unit Arial default moves to WordCraft. |
| `Text::layout`, `TextLayout` | Stay in WordCraft, over the shared `Text`. |
| `PlacedItem::Text { index, tf }` | `PlacedItem::Text { index, transform, clip, region, clips }`. |
| Text from `EXTTEXTOUTW` only | Also `EXTTEXTOUTA`, `SMALLTEXTOUT`, WMF text, opaque backgrounds, EMF+ strings. |
### VectorCraft (`vectorcraft-metafile`)
| Before | After |
|---|---|
| `sniff`, `Kind` | `craft_metafile::{sniff, Kind}`. |
| `import(bytes) -> Imported { document, kind, warnings }` | `craft_metafile::parse(bytes)`, then VectorCraft's own mapping of `Picture` to a `Document`: `Item::Path` to a path node (`line` to cap, join, miter limit and dash), `clips` to clipping groups (`Arc::ptr_eq` groups items), `region` to a rectangle clip, `Item::Bitmap` to an image placed by `transform`, `Item::Text` to point type, `Item::Pattern` to its tile or a flat colour, `skipped_records` to the warning. |
| `export(doc, region, kind) -> Output` | VectorCraft's `scene` walk produces `craft_metafile::write::Op`s, then `write::emf(&ops, region, "VectorCraft", &doc.title)` or `write::wmf(&ops, region)`; `Wmf`'s flags become the two WMF warnings. |
| `scene::Pen { cap: LineCap, join: LineJoin, dash: Option<Dash> }` | `write::Pen { cap: Cap, join: Join, dash: Option<write::Dash> }` (`Cap::Flat` is `LineCap::Butt`). A dash with `align_corners`, or one `Dash::emf_pen_draws` refuses, is cut by VectorCraft and passed as the path with no dash, to keep its own dash cutting. |
| `dib::Rgba::fitted`, `to_png` | Stay in VectorCraft. |
| EMF+ only files: a warning and nothing imported | They are read. |
+59
View File
@@ -0,0 +1,59 @@
//! Bounds-checked little-endian access to hostile input. Every read returns `None` past the end, so
//! a truncated or lying size field stops the parse instead of panicking.
#[derive(Clone, Copy)]
pub(crate) struct Bytes<'a>(pub(crate) &'a [u8]);
impl<'a> Bytes<'a> {
pub fn slice(&self, off: usize, len: usize) -> Option<&'a [u8]> {
self.0.get(off..off.checked_add(len)?)
}
pub fn tail(&self, off: usize) -> Option<Bytes<'a>> {
self.0.get(off..).map(Bytes)
}
fn arr<const N: usize>(&self, off: usize) -> Option<[u8; N]> {
self.slice(off, N)?.try_into().ok()
}
pub fn u16(&self, off: usize) -> Option<u16> {
Some(u16::from_le_bytes(self.arr(off)?))
}
pub fn i16(&self, off: usize) -> Option<i16> {
Some(i16::from_le_bytes(self.arr(off)?))
}
pub fn u32(&self, off: usize) -> Option<u32> {
Some(u32::from_le_bytes(self.arr(off)?))
}
pub fn i32(&self, off: usize) -> Option<i32> {
Some(i32::from_le_bytes(self.arr(off)?))
}
pub fn f32(&self, off: usize) -> Option<f32> {
Some(f32::from_le_bytes(self.arr(off)?))
}
/// `n` consecutive (x, y) pairs of 16- or 32-bit signed integers starting at `off`.
pub fn points(&self, off: usize, n: usize, wide: bool) -> Option<Vec<(f64, f64)>> {
let sz = if wide { 8 } else { 4 };
let raw = self.slice(off, n.checked_mul(sz)?)?;
let mut out = Vec::with_capacity(n);
for c in raw.chunks_exact(sz) {
let p = if wide {
let x = i32::from_le_bytes(c.get(0..4)?.try_into().ok()?);
let y = i32::from_le_bytes(c.get(4..8)?.try_into().ok()?);
(f64::from(x), f64::from(y))
} else {
let x = i16::from_le_bytes(c.get(0..2)?.try_into().ok()?);
let y = i16::from_le_bytes(c.get(2..4)?.try_into().ok()?);
(f64::from(x), f64::from(y))
};
out.push(p);
}
Some(out)
}
}
File diff suppressed because it is too large. Load diff
+193
View File
@@ -0,0 +1,193 @@
//! Clipping regions for EMF and WMF (MS-EMF 2.3.12, MS-WMF 2.3.5).
//!
//! A region is a set of axis-aligned rectangles in device units: `None` is the default clip (the
//! whole surface), `Some([])` draws nothing, and `Some(rects)` is their union. That is how GDI
//! stores a region (`RGNDATA` is a list of rectangles). Counts are capped so a hostile file cannot
//! grow the set without bound; past the cap, extra rectangles are dropped, which can only hide
//! drawing.
/// Rectangles kept from one region. Past this, further rectangles are dropped.
pub(crate) const MAX_CLIP_RECTS: usize = 64;
pub(crate) const RGN_AND: u32 = 1;
pub(crate) const RGN_OR: u32 = 2;
pub(crate) const RGN_XOR: u32 = 3;
pub(crate) const RGN_DIFF: u32 = 4;
pub(crate) const RGN_COPY: u32 = 5;
/// A rectangle `[x0, y0, x1, y1]` with a positive area, or none.
pub(crate) fn normalize(rect: [f64; 4]) -> Option<[f64; 4]> {
let [l, t, r, b] = rect;
if ![l, t, r, b].iter().all(|v| v.is_finite()) {
return None;
}
let (x0, x1) = (l.min(r), l.max(r));
let (y0, y1) = (t.min(b), t.max(b));
(x1 - x0 > 1e-6 && y1 - y0 > 1e-6).then_some([x0, y0, x1, y1])
}
fn intersection(a: [f64; 4], b: [f64; 4]) -> Option<[f64; 4]> {
let (x0, y0) = (a[0].max(b[0]), a[1].max(b[1]));
let (x1, y1) = (a[2].min(b[2]), a[3].min(b[3]));
(x1 - x0 > 1e-6 && y1 - y0 > 1e-6 && [x0, y0, x1, y1].iter().all(|v| v.is_finite())).then_some([x0, y0, x1, y1])
}
/// `a` with `b` removed, as up to four rectangles.
fn subtract_one(a: [f64; 4], b: [f64; 4]) -> Vec<[f64; 4]> {
let Some(i) = intersection(a, b) else { return vec![a] };
let mut out = Vec::new();
if i[1] > a[1]
&& let Some(r) = normalize([a[0], a[1], a[2], i[1]])
{
out.push(r);
}
if i[3] < a[3]
&& let Some(r) = normalize([a[0], i[3], a[2], a[3]])
{
out.push(r);
}
if i[0] > a[0]
&& let Some(r) = normalize([a[0], i[1], i[0], i[3]])
{
out.push(r);
}
if i[2] < a[2]
&& let Some(r) = normalize([i[2], i[1], a[2], i[3]])
{
out.push(r);
}
out
}
fn cap(mut rects: Vec<[f64; 4]>) -> Vec<[f64; 4]> {
rects.truncate(MAX_CLIP_RECTS);
rects
}
fn subtract_all(subject: Vec<[f64; 4]>, cuts: &[[f64; 4]]) -> Vec<[f64; 4]> {
let mut cur = subject;
for cut in cuts {
let mut next = Vec::new();
for s in cur {
for piece in subtract_one(s, *cut) {
if next.len() >= MAX_CLIP_RECTS {
return next;
}
next.push(piece);
}
}
cur = next;
}
cur
}
/// Intersects the current region with one rectangle.
pub(crate) fn intersect(current: Option<Vec<[f64; 4]>>, rect: [f64; 4]) -> Option<Vec<[f64; 4]>> {
let Some(rect) = normalize(rect) else { return Some(Vec::new()) };
Some(match current {
None => vec![rect],
Some(rects) => cap(rects.iter().filter_map(|r| intersection(*r, rect)).collect()),
})
}
/// Removes one rectangle. `surface` is the device bounds, used when the clip is still the default
/// (the whole surface) so the remainder can be named.
pub(crate) fn exclude(current: Option<Vec<[f64; 4]>>, rect: [f64; 4], surface: Option<[f64; 4]>) -> Option<Vec<[f64; 4]>> {
let Some(rect) = normalize(rect) else { return current };
match current {
Some(rects) => Some(subtract_all(rects, &[rect])),
None => surface.and_then(normalize).map(|s| subtract_all(vec![s], &[rect])),
}
}
/// Combines `incoming` with the current region in a region mode (`RGN_*`). An unknown mode leaves
/// the region as it was. `RGN_COPY` replaces it. An `RGN_OR` or `RGN_XOR` against the default clip
/// stays the default clip: the whole surface already covers the incoming rectangles.
pub(crate) fn combine(current: Option<Vec<[f64; 4]>>, incoming: Vec<[f64; 4]>, mode: u32, surface: Option<[f64; 4]>) -> Option<Vec<[f64; 4]>> {
let incoming: Vec<[f64; 4]> = cap(incoming.iter().copied().filter_map(normalize).collect());
match mode {
RGN_COPY => Some(incoming),
RGN_AND => match current {
None => Some(incoming),
Some(rects) => {
let mut out = Vec::new();
for a in &rects {
for b in &incoming {
if out.len() >= MAX_CLIP_RECTS {
return Some(out);
}
if let Some(r) = intersection(*a, *b) {
out.push(r);
}
}
}
Some(out)
}
},
RGN_OR => match current {
None => None,
Some(mut rects) => {
rects.extend(incoming);
Some(cap(rects))
}
},
RGN_DIFF => match current {
Some(rects) => Some(subtract_all(rects, &incoming)),
None => surface.and_then(normalize).map(|s| subtract_all(vec![s], &incoming)),
},
RGN_XOR => {
let base = current?;
let mut out = subtract_all(base.clone(), &incoming);
let rest = subtract_all(incoming, &base);
for r in rest {
if out.len() >= MAX_CLIP_RECTS {
break;
}
out.push(r);
}
Some(out)
}
_ => current,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intersect_shrinks_to_the_overlap() {
let r = intersect(None, [0.0, 0.0, 100.0, 50.0]);
let r = intersect(r, [40.0, 10.0, 80.0, 40.0]);
assert_eq!(r, Some(vec![[40.0, 10.0, 80.0, 40.0]]));
assert_eq!(intersect(r, [0.0, 0.0, 10.0, 10.0]), Some(vec![]));
assert_eq!(intersect(Some(vec![[0.0, 0.0, 10.0, 20.0]]), [40.0, 0.0, 80.0, 20.0]), Some(vec![]));
}
#[test]
fn modes_copy_and_or_diff_and_xor() {
let a = Some(vec![[0.0, 0.0, 100.0, 40.0]]);
assert_eq!(combine(a.clone(), vec![[10.0, 10.0, 30.0, 20.0]], RGN_COPY, None), Some(vec![[10.0, 10.0, 30.0, 20.0]]));
let and = combine(a.clone(), vec![[50.0, 0.0, 150.0, 40.0]], RGN_AND, None);
assert_eq!(and, Some(vec![[50.0, 0.0, 100.0, 40.0]]));
let or = combine(a.clone(), vec![[200.0, 0.0, 210.0, 10.0]], RGN_OR, None).unwrap();
assert_eq!(or.len(), 2);
let diff = combine(a.clone(), vec![[20.0, 0.0, 40.0, 40.0]], RGN_DIFF, None).unwrap();
assert_eq!(diff, vec![[0.0, 0.0, 20.0, 40.0], [40.0, 0.0, 100.0, 40.0]]);
let xor = combine(a, vec![[80.0, 0.0, 120.0, 40.0]], RGN_XOR, None).unwrap();
assert!(xor.contains(&[0.0, 0.0, 80.0, 40.0]));
assert!(xor.contains(&[100.0, 0.0, 120.0, 40.0]));
}
#[test]
fn excluding_from_the_default_clip_uses_the_surface() {
let r = exclude(None, [10.0, 0.0, 20.0, 40.0], Some([0.0, 0.0, 40.0, 40.0])).unwrap();
assert_eq!(r, vec![[0.0, 0.0, 10.0, 40.0], [20.0, 0.0, 40.0, 40.0]]);
assert_eq!(exclude(None, [10.0, 0.0, 20.0, 40.0], None), None);
}
#[test]
fn a_zero_area_rectangle_clears_an_intersection() {
assert_eq!(intersect(Some(vec![[0.0, 0.0, 10.0, 10.0]]), [5.0, 5.0, 5.0, 8.0]), Some(vec![]));
}
}
+371
View File
@@ -0,0 +1,371 @@
//! Clipping drawn output to a convex device polygon (EMF+ clip rectangles, and clip paths and
//! regions that are convex; others are reduced to their bounding rectangle). Shapes wholly inside
//! pass unchanged; shapes crossing the edge are flattened and cut: fills with Sutherland–Hodgman
//! per subpath, outlines with Cyrus–Beck per segment. Bitmaps are cropped (and masked outside a
//! non-rectangular polygon) and text gets the polygon's bounding rectangle.
use crate::LineStyle;
use crate::canvas::Raw;
use crate::dib::Rgba;
use crate::geom::{Op, P};
/// Line segments per Bézier when a crossing shape is flattened.
const CURVE_STEPS: usize = 16;
/// Point-against-edge tests one clipped item may cost (all its subpaths together); an item over
/// it is dropped, as a single polygon over it already was.
const MAX_CLIP_WORK: usize = 1_000_000;
/// The same allowance for intersecting two clip regions, a budget of its own: two convex
/// 1,000-gons cost about 1.5 million tests once the cut polygon has grown.
pub(crate) const MAX_AREA_CLIP_WORK: usize = 4 * MAX_CLIP_WORK;
/// The bounding box `[l, t, r, b]` of the points in `ops` (control points included).
pub(crate) fn ops_bbox(ops: &[Op]) -> Option<[f64; 4]> {
let mut b: Option<[f64; 4]> = None;
let mut add = |p: P| {
let v = b.get_or_insert([p.0, p.1, p.0, p.1]);
*v = [v[0].min(p.0), v[1].min(p.1), v[2].max(p.0), v[3].max(p.1)];
};
for op in ops {
match *op {
Op::M(p) | Op::L(p) => add(p),
Op::C(a, c, d) => {
add(a);
add(c);
add(d);
}
Op::Z => {}
}
}
b
}
pub(crate) fn intersect(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
let r = [a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])];
// An empty intersection stays empty (zero size) so it clips everything away.
if r[2] < r[0] || r[3] < r[1] { [r[0], r[1], r[0], r[1]] } else { r }
}
/// A rectangle `[l, t, r, b]` as a polygon.
pub(crate) fn rect_poly([l, t, r, b]: [f64; 4]) -> Vec<P> {
vec![(l, t), (r, t), (r, b), (l, b)]
}
/// The bounding box of a point list.
pub(crate) fn pts_bbox(pts: &[P]) -> Option<[f64; 4]> {
let (&f, rest) = pts.split_first()?;
Some(rest.iter().fold([f.0, f.1, f.0, f.1], |v, p| [v[0].min(p.0), v[1].min(p.1), v[2].max(p.0), v[3].max(p.1)]))
}
fn cross(a: P, b: P, p: P) -> f64 {
(b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0)
}
/// A convex clip polygon with positive orientation, or None when `pts` is not convex (or
/// degenerate); duplicate and collinear points are dropped.
pub(crate) fn convex(pts: &[P]) -> Option<Vec<P>> {
let mut v: Vec<P> = Vec::with_capacity(pts.len());
for &p in pts {
if !(p.0.is_finite() && p.1.is_finite()) {
return None;
}
if v.last().is_none_or(|q: &P| (q.0 - p.0).abs() > 1e-9 || (q.1 - p.1).abs() > 1e-9) {
v.push(p);
}
}
while v.len() > 1 && v.first() == v.last() {
v.pop();
}
let area: f64 = (0..v.len()).map(|i| cross((0.0, 0.0), v[i], v[(i + 1) % v.len()])).sum();
if v.len() < 3 || area.abs() < 1e-12 {
return None;
}
if area < 0.0 {
v.reverse();
}
let n = v.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let c = cross(v[(i + n - 1) % n], v[i], v[(i + 1) % n]);
if c < -1e-9 * (1.0 + area.abs()) {
return None;
}
if c.abs() > 1e-12 {
out.push(v[i]);
}
}
(out.len() >= 3).then_some(out)
}
fn inside_poly(clip: &[P], p: P) -> bool {
(0..clip.len()).all(|i| cross(clip[i], clip[(i + 1) % clip.len()], p) >= -1e-9)
}
/// Whether the clip polygon is an axis-aligned rectangle.
fn is_rect(clip: &[P]) -> bool {
pts_bbox(clip).is_some_and(|b| clip.len() == 4 && clip.iter().all(|p| (p.0 == b[0] || p.0 == b[2]) && (p.1 == b[1] || p.1 == b[3])))
}
/// Sutherland–Hodgman: `poly` cut by convex `clip`, charging each point visited against each clip
/// edge to `budget`: the polygon
/// can gain a vertex at every edge, so its cost isn't known from its input size alone. `None`
/// once the budget is spent.
pub(crate) fn clip_polygon_within(poly: &[P], clip: &[P], budget: &mut usize) -> Option<Vec<P>> {
let mut poly = poly.to_vec();
for i in 0..clip.len() {
// The work actually done: this edge visits the polygon as it now stands.
if poly.len() > *budget {
return None;
}
*budget -= poly.len();
let (a, b) = (clip[i], clip[(i + 1) % clip.len()]);
let keep = |p: P| cross(a, b, p) >= 0.0;
let cut = |p: P, q: P| {
let (cp, cq) = (cross(a, b, p), cross(a, b, q));
let t = cp / (cp - cq);
(p.0 + (q.0 - p.0) * t, p.1 + (q.1 - p.1) * t)
};
let mut next = Vec::with_capacity(poly.len() + 4);
for j in 0..poly.len() {
let (p, q) = (poly[(j + poly.len() - 1) % poly.len()], poly[j]);
match (keep(p), keep(q)) {
(true, true) => next.push(q),
(true, false) => next.push(cut(p, q)),
(false, true) => {
next.push(cut(p, q));
next.push(q);
}
(false, false) => {}
}
}
poly = next;
if poly.is_empty() {
break;
}
}
Some(poly)
}
fn disjoint(b: [f64; 4], c: [f64; 4]) -> bool {
b[2] < c[0] || b[0] > c[2] || b[3] < c[1] || b[1] > c[3] || c[2] <= c[0] || c[3] <= c[1]
}
fn bbox_inside(b: [f64; 4], clip: &[P]) -> bool {
rect_poly(b).into_iter().all(|p| inside_poly(clip, p))
}
/// Clips one drawn item to a convex device polygon; a shape that is both filled and stroked comes
/// back as two items. An empty polygon clips everything away.
pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec<Raw> {
let Some(cb) = pts_bbox(clip).filter(|_| clip.len() >= 3) else { return Vec::new() };
match raw {
Raw::Path { ops, fill, stroke, line, even_odd, region } => {
let Some(b) = ops_bbox(&ops) else { return Vec::new() };
if bbox_inside(b, clip) {
return vec![Raw::Path { ops, fill, stroke, line, even_odd, region }];
}
if disjoint(b, cb) {
return Vec::new();
}
let subs = flatten(&ops);
// One allowance for the fill and the stroke together; the item goes once it is spent.
let mut budget = MAX_CLIP_WORK;
let mut out = Vec::new();
if fill.is_some() {
let Some(f) = clip_fill(&subs, clip, &mut budget) else { return Vec::new() };
if !f.is_empty() {
out.push(Raw::Path { ops: f, fill, stroke: None, line: LineStyle::default(), even_odd, region: region.clone() });
}
}
if stroke.is_some() {
let Some(s) = clip_stroke(&subs, clip, &mut budget) else { return Vec::new() };
if !s.is_empty() {
out.push(Raw::Path { ops: s, fill: None, stroke, line, even_odd, region });
}
}
out
}
Raw::Pattern { ops, even_odd, tile, region } => {
let Some(b) = ops_bbox(&ops) else { return Vec::new() };
if bbox_inside(b, clip) {
return vec![Raw::Pattern { ops, even_odd, tile, region }];
}
if disjoint(b, cb) {
return Vec::new();
}
let subs = flatten(&ops);
let f = clip_fill(&subs, clip, &mut { MAX_CLIP_WORK }).unwrap_or_default();
if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile, region }] }
}
Raw::Bitmap { rect, xf, img, region } => {
// A turned or skewed bitmap is not cut: it stays whole inside the clip and goes otherwise.
if xf[1] != 0.0 || xf[2] != 0.0 {
let corners =
[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)].map(|(u, v)| (xf[0] * u + xf[2] * v + xf[4], xf[1] * u + xf[3] * v + xf[5]));
let whole = pts_bbox(&corners).is_some_and(|b| bbox_inside(b, clip));
return if whole { vec![Raw::Bitmap { rect, xf, img, region }] } else { Vec::new() };
}
crop(rect, img, clip)
.map(|(r, img)| vec![Raw::Bitmap { rect: r, xf: [r[2], 0.0, 0.0, r[3], r[0], r[1]], img, region }])
.unwrap_or_default()
}
Raw::Text { text: mut t, region } => {
// Text clips to a rectangle: the one around the polygon.
let cur = t.clip.map_or(cb, |[a, b, d, e]| intersect(cb, [f64::from(a), f64::from(b), f64::from(d), f64::from(e)]));
t.clip = Some(cur.map(|v| v as f32));
vec![Raw::Text { text: t, region }]
}
}
}
/// Subpaths as point lists with their closed flag.
pub(crate) fn flatten(ops: &[Op]) -> Vec<(Vec<P>, bool)> {
let mut subs: Vec<(Vec<P>, bool)> = Vec::new();
for op in ops {
match *op {
Op::M(p) => subs.push((vec![p], false)),
Op::L(p) => match subs.last_mut() {
Some(s) => s.0.push(p),
None => subs.push((vec![p], false)),
},
Op::C(a, b, d) => {
let Some(s) = subs.last_mut() else { continue };
let Some(&p0) = s.0.last() else { continue };
for i in 1..=CURVE_STEPS {
let t = i as f64 / CURVE_STEPS as f64;
let u = 1.0 - t;
let k = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t];
s.0.push((k[0] * p0.0 + k[1] * a.0 + k[2] * b.0 + k[3] * d.0, k[0] * p0.1 + k[1] * a.1 + k[2] * b.1 + k[3] * d.1));
}
}
Op::Z => {
if let Some(s) = subs.last_mut() {
s.1 = true;
// Drawing after a close starts from the subpath's first point.
let first = s.0.first().copied();
if let Some(f) = first {
subs.push((vec![f], false));
}
}
}
}
}
subs.retain(|s| s.0.len() > 1);
subs
}
/// The clipped fill of every subpath, its clipping work charged to `budget` with that of the
/// stroke: `None` (the whole item goes) once it is spent.
fn clip_fill(subs: &[(Vec<P>, bool)], clip: &[P], budget: &mut usize) -> Option<Vec<Op>> {
let mut ops = Vec::new();
for (pts, _) in subs {
let poly = clip_polygon_within(pts, clip, budget)?;
if poly.len() >= 3 {
ops.push(Op::M(poly[0]));
ops.extend(poly[1..].iter().map(|p| Op::L(*p)));
ops.push(Op::Z);
}
}
Some(ops)
}
/// The clipped outline of every subpath; `None` once `budget` is spent.
fn clip_stroke(subs: &[(Vec<P>, bool)], clip: &[P], budget: &mut usize) -> Option<Vec<Op>> {
let mut ops = Vec::new();
for (pts, closed) in subs {
let mut pts = pts.clone();
if *closed && let Some(&f) = pts.first() {
pts.push(f);
}
let mut last: Option<P> = None;
for w in pts.windows(2) {
let Some((a, b)) = segment(w[0], w[1], clip, budget)? else {
last = None;
continue;
};
if last != Some(a) {
ops.push(Op::M(a));
}
ops.push(Op::L(b));
last = Some(b);
}
}
Some(ops)
}
/// Cyrus–Beck: the part of segment `a`–`b` inside convex `clip`, one unit of `budget` per clip edge
/// visited; the outer `None` is a spent budget.
fn segment(a: P, b: P, clip: &[P], budget: &mut usize) -> Option<Option<(P, P)>> {
let d = (b.0 - a.0, b.1 - a.1);
let (mut t0, mut t1) = (0.0f64, 1.0f64);
for i in 0..clip.len() {
*budget = budget.checked_sub(1)?;
let (e0, e1) = (clip[i], clip[(i + 1) % clip.len()]);
// Inside is cross(e0, e1, p) >= 0, linear in t along the segment.
let f0 = cross(e0, e1, a);
let df = (e1.0 - e0.0) * d.1 - (e1.1 - e0.1) * d.0;
if df == 0.0 {
if f0 < 0.0 {
return Some(None);
}
} else {
let t = -f0 / df;
if df > 0.0 {
t0 = t0.max(t);
} else {
t1 = t1.min(t);
}
}
}
Some((t0 <= t1).then_some(((a.0 + d.0 * t0, a.1 + d.1 * t0), (a.0 + d.0 * t1, a.1 + d.1 * t1))))
}
/// Point-in-polygon tests (pixels times clip corners) spent masking one bitmap to a clip that is
/// not a rectangle. A bitmap that would take more is left out.
pub(crate) const MAX_MASK_WORK: usize = 5_000_000;
/// Crops a bitmap drawn into `rect` = `[x, y, w, h]` (device, positive extents) to the clip
/// polygon's box; pixels outside a non-rectangular polygon turn transparent.
pub(crate) fn crop(rect: [f64; 4], img: Rgba, clip: &[P]) -> Option<([f64; 4], Rgba)> {
let [x, y, w, h] = rect;
let b = [x, y, x + w, y + h];
if bbox_inside(b, clip) {
return Some((rect, img));
}
let c = pts_bbox(clip)?;
if disjoint(b, c) || w <= 0.0 || h <= 0.0 || img.w == 0 || img.h == 0 {
return None;
}
let i = intersect(b, c);
let (kx, ky) = (img.w as f64 / w, img.h as f64 / h);
let x0 = (((i[0] - x) * kx).floor().max(0.0) as usize).min(img.w);
let x1 = (((i[2] - x) * kx).ceil().max(0.0) as usize).min(img.w);
let y0 = (((i[1] - y) * ky).floor().max(0.0) as usize).min(img.h);
let y1 = (((i[3] - y) * ky).ceil().max(0.0) as usize).min(img.h);
if x1 <= x0 || y1 <= y0 {
return None;
}
let mask = !is_rect(clip);
let total_pixels = (x1 - x0).saturating_mul(y1 - y0);
if mask && total_pixels.saturating_mul(clip.len()) > MAX_MASK_WORK {
return None;
}
let mut px = Vec::with_capacity((x1 - x0) * (y1 - y0) * 4);
for row in y0..y1 {
let s = (row * img.w + x0) * 4;
let start = px.len();
px.extend_from_slice(img.px.get(s..s + (x1 - x0) * 4)?);
if mask {
let cy = y + (row as f64 + 0.5) / ky;
for (k, p) in px[start..].as_chunks_mut::<4>().0.iter_mut().enumerate() {
let cx = x + ((x0 + k) as f64 + 0.5) / kx;
if !inside_poly(clip, (cx, cy)) {
p[3] = 0;
}
}
}
}
let out = [x + x0 as f64 / kx, y + y0 as f64 / ky, (x1 - x0) as f64 / kx, (y1 - y0) as f64 / ky];
Some((out, Rgba { w: x1 - x0, h: y1 - y0, px }))
}
+812
View File
@@ -0,0 +1,812 @@
//! Device-independent bitmaps. BITMAPINFOHEADER DIBs at 1/4/8/16/24/32 bpp with BI_RGB or BI_BITFIELDS,
//! BITMAPCOREHEADER DIBs at 1/4/8/24 bpp, and BI_JPEG and BI_PNG DIBs decode to RGBA8 rows, top-down.
//! RLE compression and V4/V5-only features are not decoded.
use crate::{MAX_BITMAP_SIDE, MAX_PIXELS, bytes::Bytes};
const BI_RGB: u32 = 0;
const BI_BITFIELDS: u32 = 3;
/// The bits are a whole JPEG or PNG file (MS-WMF 2.1.1.7).
const BI_JPEG: u32 = 4;
const BI_PNG: u32 = 5;
/// BITMAPCOREHEADER: 16-bit sizes, RGB triples in the palette, always bottom-up.
const CORE_HEADER: usize = 12;
/// Most palette entries a DIB can carry (the colour table never exceeds 2^8).
const MAX_PALETTE: usize = 256;
const ROP_SRCAND: u32 = 0x0088_00C6;
const ROP_SRCPAINT: u32 = 0x00EE_0086;
const ROP_SRCINVERT: u32 = 0x0066_0046;
/// A decoded picture, rows top-down.
pub(crate) struct Rgba {
pub w: usize,
pub h: usize,
pub px: Vec<u8>,
}
/// How a decoded bitmap combines with the destination.
#[derive(Clone, Copy)]
pub(crate) enum Blend {
/// A raster operation (BITBLT, STRETCHDIB and friends).
Rop(u32),
/// EMR_ALPHABLEND: a constant alpha (0..=255), times the per-pixel alpha when `per_pixel` is set
/// (AC_SRC_ALPHA: the source colours are premultiplied by it).
Alpha { constant: u8, per_pixel: bool },
}
impl Blend {
/// Whether the decoder must keep the 32-bit alpha byte of each pixel.
pub fn per_pixel(self) -> bool {
matches!(self, Blend::Alpha { per_pixel: true, .. })
}
}
struct Header {
w: usize,
h: usize,
top_down: bool,
bpp: usize,
masks: [u32; 3],
pal_off: usize,
pal_n: usize,
/// Bytes per palette entry: 4 (BGRX), or 3 (BGR) under a core header.
pal_entry: usize,
}
impl Header {
fn read(info: Bytes) -> Option<Header> {
let size = info.u32(0)? as usize;
if size == CORE_HEADER {
let (w, h, bpp) = (usize::from(info.u16(4)?), usize::from(info.u16(6)?), usize::from(info.u16(10)?));
if w == 0 || h == 0 || !matches!(bpp, 1 | 4 | 8 | 24) {
return None;
}
let pal_n = if bpp <= 8 { 1usize << bpp } else { 0 };
return Some(Header { w, h, top_down: false, bpp, masks: [0xFF_0000, 0xFF00, 0xFF], pal_off: CORE_HEADER, pal_n, pal_entry: 3 });
}
let w = info.i32(4)?;
let h = info.i32(8)?;
let bpp = info.u16(14)? as usize;
let comp = info.u32(16)?;
let clr_used = info.u32(32)? as usize;
if size < 40 || w <= 0 || h == 0 || !matches!(bpp, 1 | 4 | 8 | 16 | 24 | 32) || !matches!(comp, BI_RGB | BI_BITFIELDS) {
return None;
}
let masks = if comp == BI_BITFIELDS {
[info.u32(40)?, info.u32(44)?, info.u32(48)?]
} else if bpp == 16 {
[0x7C00, 0x03E0, 0x001F]
} else {
[0xFF_0000, 0xFF00, 0xFF]
};
let masks_len = if comp == BI_BITFIELDS && size == 40 { 12 } else { 0 };
let pal_off = size.checked_add(masks_len)?;
// Indexed formats take 2^bpp entries when clrUsed is 0. Deeper formats carry only the
// entries clrUsed names (WMF DIBs can have them), still capped.
let pal_n = match (bpp <= 8, clr_used) {
(true, 0) => 1usize << bpp,
(true, n) => n.min(1usize << bpp),
(false, n) => n.min(MAX_PALETTE),
};
Some(Header { w: w as usize, h: h.unsigned_abs() as usize, top_down: h < 0, bpp, masks, pal_off, pal_n, pal_entry: 4 })
}
}
/// Byte length of the info part (header, masks, palette) of a DIB, so the pixel bits follow it.
pub(crate) fn info_len(dib: Bytes) -> Option<usize> {
if compression(dib).is_some() {
// No palette: the bits are a whole image file.
return Some(dib.u32(0)? as usize).filter(|n| *n >= 40);
}
let h = Header::read(dib)?;
h.pal_off.checked_add(h.pal_n.checked_mul(h.pal_entry)?)
}
/// BI_JPEG or BI_PNG when the header says the bits are an image file.
fn compression(info: Bytes) -> Option<u32> {
(info.u32(0)? >= 40).then(|| info.u32(16)).flatten().filter(|c| matches!(*c, BI_JPEG | BI_PNG))
}
/// Decodes a JPEG or PNG file (the bits of a BI_JPEG or BI_PNG DIB, an EMF+ image) to top-down
/// RGBA. The size comes from the file's header first, so an image over the side or pixel cap is
/// refused before any pixel is decoded, and the decoder's own allocations are capped too.
pub(crate) fn decode_compressed(bytes: &[u8]) -> Option<Rgba> {
let reader = || {
let mut r = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
let mut limits = image::Limits::default();
limits.max_image_width = Some(MAX_COMPRESSED_SIDE);
limits.max_image_height = Some(MAX_COMPRESSED_SIDE);
limits.max_alloc = Some((MAX_PIXELS * 4) as u64);
r.limits(limits);
Some(r)
};
let (w, h) = reader()?.into_dimensions().ok()?;
let (w, h) = (w as usize, h as usize);
if w == 0 || h == 0 || w.checked_mul(h)? > MAX_PIXELS {
return None;
}
// A few bytes of JPEG or PNG decode to millions of pixels, so the pixels of one file are
// budgeted as a whole: an image past the budget is left out before it is decoded.
let left = DECODE_LEFT.with(std::cell::Cell::get).checked_sub((w * h) as u64)?;
DECODE_LEFT.with(|d| d.set(left));
let img = reader()?.decode().ok()?.to_rgba8();
Some(Rgba { w, h, px: img.into_raw() })
}
/// Pixels that the JPEG and PNG images of one file (nested metafiles included) may decode to.
pub(crate) const MAX_DECODE_PIXELS: u64 = 1 << 28;
thread_local! {
/// What is left of [`MAX_DECODE_PIXELS`] for the file being parsed on this thread.
static DECODE_LEFT: std::cell::Cell<u64> = const { std::cell::Cell::new(MAX_DECODE_PIXELS) };
}
/// Starts a file's decode budget (`MAX_DECODE_PIXELS` for every parse outside the tests).
pub(crate) fn set_decode_budget(pixels: u64) {
DECODE_LEFT.with(|d| d.set(pixels));
}
/// Largest side of a JPEG or PNG image.
pub(crate) const MAX_COMPRESSED_SIDE: u32 = 1 << 15;
/// Decodes the part of a DIB that the source rectangle `src` = `[x, y, w, h]` selects and returns it
/// (top-down) with the part of the destination `dst` that it fills; see [`clip_rect`] for the
/// coordinates. `info` is the info part (header plus masks and palette) and `bits` the pixel bits. The
/// 32-bit alpha byte is kept only when `keep_alpha` is set (per-pixel alpha blends); otherwise pixels
/// are opaque. Only the clipped rows and columns are decoded, so the work and the memory follow the
/// crop, not the whole bitmap; the whole bitmap is still validated (size cap, palette, bits length).
/// None as well when the clipped width or height is over [`MAX_BITMAP_SIDE`], which no renderer draws.
pub(crate) fn decode_clip(info: Bytes, bits: Bytes, keep_alpha: bool, src: [i64; 4], dst: [f64; 4]) -> Option<(Rgba, [f64; 4])> {
if compression(info).is_some() {
// An image file has no bottom-up row order: the source rectangle counts from the top.
let img = decode_compressed(bits.0)?;
let (part, out) = clip_blit(&img, src, dst)?;
let cap = MAX_BITMAP_SIDE as usize;
return (part.w <= cap && part.h <= cap).then_some((part, out));
}
let hd = Header::read(info)?;
let pixels = hd.w.checked_mul(hd.h)?;
if pixels == 0 || pixels > MAX_PIXELS {
return None;
}
let stride = hd.w.checked_mul(hd.bpp)?.checked_add(31)? / 32 * 4;
let data = bits.slice(0, stride.checked_mul(hd.h)?)?;
let pal = info.slice(hd.pal_off, hd.pal_n * hd.pal_entry)?;
let (clip, out) = clip_rect(hd.w, hd.h, !hd.top_down, src, dst)?;
// The renderers draw no bitmap with a side over the cap (`Item::take_pixels`); decoding one
// would only fill the parse's memory budget with pixels nobody draws.
let cap = MAX_BITMAP_SIDE as usize;
if clip.x1 - clip.x0 > cap || clip.r1 - clip.r0 > cap {
return None;
}
let (ow, oh) = (clip.x1 - clip.x0, clip.r1 - clip.r0);
let mut px = vec![0u8; ow.checked_mul(oh)?.checked_mul(4)?];
#[cfg(test)]
DECODED.with(|d| d.set(d.get() + ow * oh));
// The depth, palette and masks are the same for every pixel: pick the row decoder once.
let kind = RowKind::new(&hd, pal, keep_alpha);
for (y, line) in (clip.r0..clip.r1).zip(px.chunks_exact_mut(ow * 4)) {
let src_row = if hd.top_down { y } else { hd.h - 1 - y };
let row = data.get(src_row * stride..(src_row + 1) * stride)?;
kind.decode_row(row, clip.x0..clip.x1, line.as_chunks_mut::<4>().0)?;
}
Some((Rgba { w: ow, h: oh, px }, out))
}
/// Decodes a whole DIB (rows top-down); see [`decode_clip`].
pub(crate) fn decode(info: Bytes, bits: Bytes, keep_alpha: bool) -> Option<Rgba> {
decode_clip(info, bits, keep_alpha, [0, 0, 0, 0], [0.0, 0.0, 1.0, 1.0]).map(|(img, _)| img)
}
/// The part of a decoded (top-down) image that the source rectangle `src` = `[x, y, w, h]` selects,
/// with the part of the destination `dst` that it fills; see [`clip_rect`].
pub(crate) fn clip_blit(img: &Rgba, src: [i64; 4], dst: [f64; 4]) -> Option<(Rgba, [f64; 4])> {
let (clip, out) = clip_rect(img.w, img.h, false, src, dst)?;
let (ow, oh) = (clip.x1 - clip.x0, clip.r1 - clip.r0);
let mut px = Vec::with_capacity(ow.checked_mul(oh)?.checked_mul(4)?);
for row in clip.r0..clip.r1 {
let s = row.checked_mul(img.w)?.checked_add(clip.x0)?.checked_mul(4)?;
px.extend_from_slice(img.px.get(s..s.checked_add(ow * 4)?)?);
}
Some((Rgba { w: ow, h: oh, px }, out))
}
#[cfg(test)]
thread_local! {
/// Pixels asked of the decoder on this thread, so the tests can check that the work follows the crop.
static DECODED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// One channel of a masked 16/32 bpp pixel, with the shift and scale worked out once per bitmap.
/// Channels of up to 8 bits (every common 16 bpp layout) go through a table of their values, so a
/// pixel costs no division.
#[derive(Clone)]
struct Channel {
mask: u32,
shift: u32,
max: u64,
table: [u8; 256],
}
impl Channel {
fn new(mask: u32) -> Channel {
let shift = if mask == 0 { 0 } else { mask.trailing_zeros() };
let max = u64::from(mask >> shift);
let mut table = [0u8; 256];
if mask != 0 && max <= 255 {
for (val, t) in table.iter_mut().enumerate().take(max as usize + 1) {
*t = ((val as u64 * 255 + max / 2) / max) as u8;
}
}
Channel { mask, shift, max, table }
}
/// The channel of `v` scaled to 8 bits; 0 when the mask is empty.
fn get(&self, v: u32) -> u8 {
if self.mask == 0 {
return 0;
}
let val = u64::from((v & self.mask) >> self.shift);
if self.max <= 255 {
return self.table[usize::from(val as u8)];
}
((val * 255 + self.max / 2) / self.max) as u8
}
}
/// How the pixels of one DIB turn into RGBA, decided once from its header.
enum RowKind {
/// 1, 4 or 8 bpp: `bpp` bits per index into a table of every possible index.
Indexed { bpp: usize, table: Box<[[u8; 4]; 256]> },
/// 24 bpp: BGR triples.
Bgr24,
/// 32 bpp with the default masks: BGRA quads, alpha kept or forced opaque.
Bgra32 { keep_alpha: bool },
/// 16 bpp, or 32 bpp with other masks.
Masked { bpp: usize, ch: Box<[Channel; 3]>, keep_alpha: bool },
}
impl RowKind {
fn new(hd: &Header, pal: &[u8], keep_alpha: bool) -> RowKind {
match hd.bpp {
1 | 4 | 8 => {
// An index past the palette reads as black, as in `palette`.
let mut table = Box::new([[0u8, 0, 0, 255]; 256]);
for (i, t) in table.iter_mut().enumerate().take(1 << hd.bpp) {
let [r, g, b] = palette(pal, i, hd.pal_entry);
*t = [r, g, b, 255];
}
RowKind::Indexed { bpp: hd.bpp, table }
}
24 => RowKind::Bgr24,
32 if hd.masks == [0xFF_0000, 0xFF00, 0xFF] => RowKind::Bgra32 { keep_alpha },
_ => RowKind::Masked { bpp: hd.bpp, ch: Box::new(hd.masks.map(Channel::new)), keep_alpha },
}
}
/// Decodes the columns `cols` of one source row `row` (all of its bytes) into `out`. `None` when
/// the row is shorter than the columns need.
fn decode_row(&self, row: &[u8], cols: std::ops::Range<usize>, out: &mut [[u8; 4]]) -> Option<()> {
match self {
RowKind::Indexed { bpp: 8, table } => {
for (d, s) in out.iter_mut().zip(row.get(cols)?) {
*d = table[usize::from(*s)];
}
}
RowKind::Indexed { bpp: 4, table } => {
for (d, x) in out.iter_mut().zip(cols) {
let b = *row.get(x / 2)?;
*d = table[usize::from(if x.is_multiple_of(2) { b >> 4 } else { b & 0x0F })];
}
}
RowKind::Indexed { table, .. } => {
for (d, x) in out.iter_mut().zip(cols) {
let b = *row.get(x / 8)?;
*d = table[usize::from((b >> (7 - x % 8)) & 1)];
}
}
RowKind::Bgr24 => {
let src = row.get(cols.start.checked_mul(3)?..cols.end.checked_mul(3)?)?;
for (d, s) in out.iter_mut().zip(src.as_chunks::<3>().0) {
*d = [s[2], s[1], s[0], 255];
}
}
RowKind::Bgra32 { keep_alpha } => {
let src = row.get(cols.start.checked_mul(4)?..cols.end.checked_mul(4)?)?;
for (d, s) in out.iter_mut().zip(src.as_chunks::<4>().0) {
*d = [s[2], s[1], s[0], if *keep_alpha { s[3] } else { 255 }];
}
}
RowKind::Masked { bpp: 16, ch, .. } => {
let [r, g, b] = &**ch;
let src = row.get(cols.start.checked_mul(2)?..cols.end.checked_mul(2)?)?;
for (d, s) in out.iter_mut().zip(src.as_chunks::<2>().0) {
let v = u32::from(u16::from_le_bytes(*s));
*d = [r.get(v), g.get(v), b.get(v), 255];
}
}
RowKind::Masked { ch, keep_alpha, .. } => {
let [r, g, b] = &**ch;
let src = row.get(cols.start.checked_mul(4)?..cols.end.checked_mul(4)?)?;
for (d, s) in out.iter_mut().zip(src.as_chunks::<4>().0) {
let v = u32::from_le_bytes(*s);
*d = [r.get(v), g.get(v), b.get(v), if *keep_alpha { (v >> 24) as u8 } else { 255 }];
}
}
}
Some(())
}
}
/// Palette entries are BGRX quads (`entry` 4) or BGR triples (3, core headers); an index past the
/// table reads as black rather than failing the image.
fn palette(pal: &[u8], i: usize, entry: usize) -> [u8; 3] {
pal.get(i * entry..i * entry + 3).map_or([0, 0, 0], |c| [c[2], c[1], c[0]])
}
/// The columns `x0..x1` and top-down rows `r0..r1` of a bitmap that a clipped source rectangle covers.
struct Clip {
x0: usize,
x1: usize,
r0: usize,
r1: usize,
}
/// Clips a source rectangle `src` = `[x, y, w, h]` to a `w` x `h` bitmap and returns the covered pixels
/// with the part of the destination `dst` that they fill. `src` is in DIB pixel coordinates (`y` counted
/// from the DIB's origin, so bottom-up images count from the bottom); a non-positive source size means the
/// whole bitmap. Clipping shrinks the destination by the same ratio and shifts it by the clipped-off part,
/// so the rest of the image keeps its place. A negative destination extent is kept (the caller mirrors).
/// `None` when nothing of the bitmap is covered.
fn clip_rect(w: usize, h: usize, bottom_up: bool, src: [i64; 4], dst: [f64; 4]) -> Option<(Clip, [f64; 4])> {
let (iw, ih) = (w as i64, h as i64);
let [sx, sy, sw, sh] = if src[2] > 0 && src[3] > 0 { src } else { [0, 0, iw, ih] };
// The request's top row in top-down order.
let top = if bottom_up { ih.saturating_sub(sy).saturating_sub(sh) } else { sy };
let (x0, x1) = (sx.max(0), sx.saturating_add(sw).min(iw));
let (r0, r1) = (top.max(0), top.saturating_add(sh).min(ih));
if x1 <= x0 || r1 <= r0 {
return None;
}
let (kx, ky) = (dst[2] / sw as f64, dst[3] / sh as f64);
let out = [dst[0] + (x0 - sx) as f64 * kx, dst[1] + (r0 - top) as f64 * ky, (x1 - x0) as f64 * kx, (r1 - r0) as f64 * ky];
Some((Clip { x0: x0 as usize, x1: x1 as usize, r0: r0 as usize, r1: r1 as usize }, out))
}
/// Applies a blend to decoded pixels before they are drawn. The picture has no destination pixels, so
/// raster operations are drawn as they look over white paper: SRCAND (destination AND source) leaves
/// white source pixels out and draws the rest; SRCPAINT and SRCINVERT leave black ones out. Other
/// operations draw the source as copied. Constant alpha scales every pixel's alpha, after a
/// per-pixel (premultiplied) source is made straight.
pub(crate) fn prepare(img: &mut Rgba, blend: Blend) {
match blend {
Blend::Rop(ROP_SRCAND) => clear_where(img, [255, 255, 255]),
Blend::Rop(ROP_SRCPAINT | ROP_SRCINVERT) => clear_where(img, [0, 0, 0]),
Blend::Rop(_) => {}
Blend::Alpha { constant, per_pixel } => {
for p in img.px.as_chunks_mut::<4>().0 {
if per_pixel {
unpremultiply(p);
}
p[3] = (u16::from(p[3]) * u16::from(constant) / 255) as u8;
}
}
}
}
/// Makes the pixels of colour `rgb` transparent: where a raster operation keeps the destination.
fn clear_where(img: &mut Rgba, rgb: [u8; 3]) {
for p in img.px.as_chunks_mut::<4>().0 {
if p[..3] == rgb {
p[3] = 0;
}
}
}
/// AC_SRC_ALPHA sources are premultiplied; the model holds straight RGBA, so divide the colour by the
/// alpha (rounded, capped at 255). A transparent pixel keeps its colour bytes.
fn unpremultiply(p: &mut [u8; 4]) {
let a = u32::from(p[3]);
if a == 0 || a == 255 {
return;
}
for c in &mut p[..3] {
*c = ((u32::from(*c) * 255 + a / 2) / a).min(255) as u8;
}
}
/// Mirrors a decoded image left-right and/or top-bottom. Rows come from the buffer as chunks, so a
/// short buffer flips only the rows it has.
pub(crate) fn flip(img: &mut Rgba, horizontal: bool, vertical: bool) {
let (w, h) = (img.w, img.h);
if w == 0 || h == 0 {
return;
}
let row = w * 4;
if horizontal {
for line in img.px.chunks_exact_mut(row) {
for x in 0..w / 2 {
for c in 0..4 {
line.swap(x * 4 + c, (w - 1 - x) * 4 + c);
}
}
}
}
if vertical {
// Rows the buffer actually holds: a short buffer flips only those.
let n = h.min(img.px.len() / row);
for y in 0..n / 2 {
swap_rows(&mut img.px, y * row, (n - 1 - y) * row, row);
}
}
}
/// Swaps the `len` bytes at `a` with those at `b` (`a < b`); does nothing when either is out of range.
fn swap_rows(px: &mut [u8], a: usize, b: usize, len: usize) {
let Some((head, tail)) = px.split_at_mut_checked(b) else { return };
if let (Some(top), Some(bottom)) = (head.get_mut(a..a.saturating_add(len)), tail.get_mut(..len)) {
top.swap_with_slice(bottom);
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The pipeline before `decode_clip`: decode the whole bitmap, then copy the clipped part out.
fn decode_whole(info: Bytes, bits: Bytes, keep_alpha: bool) -> Option<Rgba> {
let hd = Header::read(info)?;
let pixels = hd.w.checked_mul(hd.h)?;
if pixels == 0 || pixels > MAX_PIXELS {
return None;
}
let stride = hd.w.checked_mul(hd.bpp)?.checked_add(31)? / 32 * 4;
let data = bits.slice(0, stride.checked_mul(hd.h)?)?;
let pal = info.slice(hd.pal_off, hd.pal_n * 4)?;
let mut px = vec![0u8; pixels * 4];
for y in 0..hd.h {
let src_row = if hd.top_down { y } else { hd.h - 1 - y };
let row = src_row * stride;
for x in 0..hd.w {
let p = pixel(data, row, x, &hd, pal, keep_alpha)?;
let o = (y * hd.w + x) * 4;
px.get_mut(o..o + 4)?.copy_from_slice(&p);
}
}
Some(Rgba { w: hd.w, h: hd.h, px })
}
fn clip_blit_whole(img: &Rgba, bottom_up: bool, src: [i64; 4], dst: [f64; 4]) -> Option<(Rgba, [f64; 4])> {
let (iw, ih) = (img.w as i64, img.h as i64);
let [sx, sy, sw, sh] = if src[2] > 0 && src[3] > 0 { src } else { [0, 0, iw, ih] };
let top = if bottom_up { ih.saturating_sub(sy).saturating_sub(sh) } else { sy };
let (x0, x1) = (sx.max(0), sx.saturating_add(sw).min(iw));
let (r0, r1) = (top.max(0), top.saturating_add(sh).min(ih));
if x1 <= x0 || r1 <= r0 {
return None;
}
let (kx, ky) = (dst[2] / sw as f64, dst[3] / sh as f64);
let out = [dst[0] + (x0 - sx) as f64 * kx, dst[1] + (r0 - top) as f64 * ky, (x1 - x0) as f64 * kx, (r1 - r0) as f64 * ky];
let (x0, x1, r0, r1) = (x0 as usize, x1 as usize, r0 as usize, r1 as usize);
let (ow, oh) = (x1 - x0, r1 - r0);
let mut px = Vec::with_capacity(ow * oh * 4);
for row in r0..r1 {
let s = (row * img.w + x0) * 4;
px.extend_from_slice(img.px.get(s..s + ow * 4)?);
}
Some((Rgba { w: ow, h: oh, px }, out))
}
/// A BITMAPINFOHEADER DIB with a palette for indexed depths; returns (info, bits).
fn make_dib(w: usize, h: i32, bpp: usize, seed: &mut u64) -> (Vec<u8>, Vec<u8>) {
let mut rnd = || {
*seed ^= *seed << 13;
*seed ^= *seed >> 7;
*seed ^= *seed << 17;
*seed
};
let mut info = Vec::new();
info.extend(40u32.to_le_bytes());
info.extend((w as i32).to_le_bytes());
info.extend(h.to_le_bytes());
info.extend(1u16.to_le_bytes());
info.extend((bpp as u16).to_le_bytes());
info.extend([0u8; 24]);
if bpp <= 8 {
for _ in 0..(1usize << bpp) * 4 {
info.push(rnd() as u8);
}
}
let stride = (w * bpp).div_ceil(32) * 4;
let bits = (0..stride * h.unsigned_abs() as usize).map(|_| rnd() as u8).collect();
(info, bits)
}
/// The per-pixel decoder `decode_clip` used before the depth was chosen once per bitmap. Kept as
/// the reference the fast decoder must match byte for byte.
fn pixel(data: &[u8], row: usize, x: usize, hd: &Header, pal: &[u8], keep_alpha: bool) -> Option<[u8; 4]> {
DECODED.with(|d| d.set(d.get() + 1));
let [m_r, m_g, m_b] = hd.masks;
let [r, g, b] = match hd.bpp {
1 => {
let b = *data.get(row + x / 8)?;
palette(pal, usize::from((b >> (7 - x % 8)) & 1), 4)
}
4 => {
let b = *data.get(row + x / 2)?;
palette(pal, usize::from(if x.is_multiple_of(2) { b >> 4 } else { b & 0x0F }), 4)
}
8 => palette(pal, usize::from(*data.get(row + x)?), 4),
24 => {
let [b, g, r] = data.get(row + x * 3..row + x * 3 + 3)?.try_into().ok()?;
[r, g, b]
}
16 => {
let o = row + x * 2;
let v = u32::from(u16::from_le_bytes(data.get(o..o + 2)?.try_into().ok()?));
[field(v, m_r), field(v, m_g), field(v, m_b)]
}
_ => {
let o = row + x * 4;
let v = u32::from_le_bytes(data.get(o..o + 4)?.try_into().ok()?);
let a = if keep_alpha { (v >> 24) as u8 } else { 255 };
return Some([field(v, m_r), field(v, m_g), field(v, m_b), a]);
}
};
Some([r, g, b, 255])
}
/// Extracts one channel from a masked pixel value and scales it to 8 bits.
fn field(v: u32, mask: u32) -> u8 {
if mask == 0 {
return 0;
}
let shift = mask.trailing_zeros();
let max = u64::from(mask >> shift);
let val = u64::from((v & mask) >> shift);
((val * 255 + max / 2) / max) as u8
}
/// `decode_clip` as it was: the same clip, then `pixel` for every output pixel.
fn decode_clip_reference(info: Bytes, bits: Bytes, keep_alpha: bool, src: [i64; 4], dst: [f64; 4]) -> Option<(Rgba, [f64; 4])> {
let hd = Header::read(info)?;
let pixels = hd.w.checked_mul(hd.h)?;
if pixels == 0 || pixels > MAX_PIXELS {
return None;
}
let stride = hd.w.checked_mul(hd.bpp)?.checked_add(31)? / 32 * 4;
let data = bits.slice(0, stride.checked_mul(hd.h)?)?;
let pal = info.slice(hd.pal_off, hd.pal_n * 4)?;
let (clip, out) = clip_rect(hd.w, hd.h, !hd.top_down, src, dst)?;
let (ow, oh) = (clip.x1 - clip.x0, clip.r1 - clip.r0);
let mut px = vec![0u8; ow.checked_mul(oh)?.checked_mul(4)?];
for (y, line) in (clip.r0..clip.r1).zip(px.chunks_exact_mut(ow * 4)) {
let src_row = if hd.top_down { y } else { hd.h - 1 - y };
let row = src_row * stride;
for (x, dst_px) in (clip.x0..clip.x1).zip(line.as_chunks_mut::<4>().0) {
*dst_px = pixel(data, row, x, &hd, pal, keep_alpha)?;
}
}
Some((Rgba { w: ow, h: oh, px }, out))
}
/// A DIB with chosen header fields: `masks` makes it BI_BITFIELDS (inline after a 40 byte
/// header, or inside a larger header), `clr_used` limits the palette, and the palette, when the
/// depth is indexed, has `pal_entries` entries (fewer than the depth names reads as black).
fn make_dib_with(
w: usize,
h: i32,
bpp: usize,
masks: Option<([u32; 3], usize)>,
clr_used: u32,
pal_entries: usize,
seed: &mut u64,
) -> (Vec<u8>, Vec<u8>) {
let mut rnd = || {
*seed ^= *seed << 13;
*seed ^= *seed >> 7;
*seed ^= *seed << 17;
*seed
};
let size = masks.map_or(40, |(_, s)| s);
let mut info = Vec::new();
info.extend((size as u32).to_le_bytes());
info.extend((w as i32).to_le_bytes());
info.extend(h.to_le_bytes());
info.extend(1u16.to_le_bytes());
info.extend((bpp as u16).to_le_bytes());
info.extend(if masks.is_some() { 3u32 } else { 0 }.to_le_bytes());
info.extend([0u8; 12]);
info.extend(clr_used.to_le_bytes());
info.extend([0u8; 4]);
info.resize(size.max(40), 0);
if let Some((m, _)) = masks {
for (i, v) in m.iter().enumerate() {
let at = 40 + i * 4;
if size == 40 {
info.extend(v.to_le_bytes());
} else if let Some(dst) = info.get_mut(at..at + 4) {
dst.copy_from_slice(&v.to_le_bytes());
}
}
}
for _ in 0..pal_entries * 4 {
info.push(rnd() as u8);
}
let stride = (w * bpp).div_ceil(32) * 4;
let bits = (0..stride * h.unsigned_abs() as usize).map(|_| rnd() as u8).collect();
(info, bits)
}
fn assert_same(info: &[u8], bits: &[u8], what: &str) {
for keep_alpha in [false, true] {
for src in [[0, 0, 0, 0], [3, 1, 20, 3], [-4, -4, 100, 100], [36, 4, 5, 5], [1, 0, 1, 5], [8, 2, 17, 2], [0, 3, 37, 1]] {
let dst = [1.0, 2.0, 30.0, 40.0];
let old = decode_clip_reference(Bytes(info), Bytes(bits), keep_alpha, src, dst);
let new = decode_clip(Bytes(info), Bytes(bits), keep_alpha, src, dst);
match (old, new) {
(None, None) => {}
(Some((a, ad)), Some((b, bd))) => {
assert_eq!((a.w, a.h, &a.px), (b.w, b.h, &b.px), "{what} alpha={keep_alpha} {src:?}");
assert_eq!(ad.map(f64::to_bits), bd.map(f64::to_bits), "{what} alpha={keep_alpha} {src:?}");
}
_ => panic!("old and new disagree on None: {what} alpha={keep_alpha} {src:?}"),
}
}
}
}
#[test]
fn decode_clip_matches_the_per_pixel_reference_at_every_depth_palette_and_mask() {
let mut seed = 0x9E37_79B9_7F4A_7C15u64;
// Widths that leave padding bits in the row, so a row's last byte is only partly used.
for w in [1, 9, 37] {
for h in [6, -6] {
for bpp in [1usize, 4, 8] {
let n = 1usize << bpp;
// Full palette, clrUsed shorter than the depth (the rest reads black), a
// palette cut short of what clrUsed names, and none at all.
for (clr_used, entries) in [(0, n), (3, 3), (n as u32 + 50, n), (0, 1), (0, 0)] {
let (info, bits) = make_dib_with(w, h, bpp, None, clr_used, entries, &mut seed);
assert_same(&info, &bits, &format!("{bpp} bpp w={w} h={h} clrUsed={clr_used} palette={entries}"));
}
}
let (info, bits) = make_dib_with(w, h, 24, None, 0, 0, &mut seed);
assert_same(&info, &bits, &format!("24 bpp w={w} h={h}"));
// A colour table on a deep DIB is carried but unused.
let (info, bits) = make_dib_with(w, h, 24, None, 5, 5, &mut seed);
assert_same(&info, &bits, &format!("24 bpp with palette w={w} h={h}"));
// Masks on depths that ignore them still move the palette and the bits.
for bpp in [1usize, 8, 24] {
let n = if bpp == 24 { 0 } else { 1 << bpp };
let (info, bits) = make_dib_with(w, h, bpp, Some(([0xF800, 0x07E0, 0x001F], 40)), 0, n, &mut seed);
assert_same(&info, &bits, &format!("{bpp} bpp with masks w={w} h={h}"));
}
let masks16: [Option<[u32; 3]>; 5] = [
None,
Some([0xF800, 0x07E0, 0x001F]),
Some([0x7C00, 0x03E0, 0x001F]),
Some([0x0F00, 0x00F0, 0x000F]),
Some([0xF800, 0x0000, 0x001F]),
];
let masks32: [Option<[u32; 3]>; 6] = [
None,
Some([0xFF_0000, 0xFF00, 0xFF]),
Some([0xFF, 0xFF00, 0xFF_0000]),
Some([0x3FF0_0000, 0x000F_FC00, 0x0000_03FF]),
Some([0xFFFF_FFFF, 0x0000_FF00, 0]),
Some([0x00FF_FF00, 0x00FF_FF00, 0x0000_00FF]),
];
for (bpp, sets) in [(16usize, masks16.as_slice()), (32, masks32.as_slice())] {
for m in sets {
// 40: the masks follow the header; 56 and 108: they are inside it.
for hsize in [40usize, 56, 108] {
let inline = m.map(|m| (m, hsize));
let (info, bits) = make_dib_with(w, h, bpp, inline, 0, 0, &mut seed);
assert_same(&info, &bits, &format!("{bpp} bpp masks={m:x?} header={hsize} w={w} h={h}"));
}
}
}
}
}
}
/// Timing of the per-pixel reference against `decode_clip` on a 40 MP bitmap:
/// `cargo test -p craft-metafile --release -- --ignored --nocapture dib_decode_speed`.
#[test]
#[ignore = "timing only"]
fn dib_decode_speed() {
let mut seed = 6u64;
for bpp in [1usize, 4, 8, 16, 24, 32] {
let (info, bits) = make_dib(8000, -5000, bpp, &mut seed);
for round in 0..4 {
let t = std::time::Instant::now();
let a = decode_clip_reference(Bytes(&info), Bytes(&bits), false, [0, 0, 8000, 5000], [0.0, 0.0, 1.0, 1.0]).map(|x| x.0.px);
let slow = t.elapsed().as_secs_f64() * 1e3;
let t = std::time::Instant::now();
let b = decode_clip(Bytes(&info), Bytes(&bits), false, [0, 0, 8000, 5000], [0.0, 0.0, 1.0, 1.0]).map(|x| x.0.px);
let fast = t.elapsed().as_secs_f64() * 1e3;
assert!(a == b);
eprintln!("{bpp:2} bpp round {round}: reference {slow:4.0} ms, decode_clip {fast:4.0} ms");
}
}
}
#[test]
fn decode_clip_matches_decode_then_clip_blit() {
let mut seed = 0x2545_F491_4F6C_DD1Du64;
let mut cases = 0;
for bpp in [1, 4, 8, 16, 24, 32] {
for h in [5, -5, 1, -1] {
let w = 7;
let (info, bits) = make_dib(w, h, bpp, &mut seed);
for keep_alpha in [false, true] {
// Source x/y from outside the bitmap on both sides; sizes include zero and negative.
for sx in [-9, -2, 0, 3, 6, 8, 20] {
for sy in [-9, -2, 0, 2, 4, 5, 20] {
for (sw, sh) in [(0, 0), (-3, 4), (4, -2), (1, 1), (3, 2), (7, 5), (30, 30), (2, 9)] {
let src = [sx, sy, sw, sh];
let dst = [10.0, 20.0, 70.0, 50.0];
let old = decode_whole(Bytes(&info), Bytes(&bits), keep_alpha).and_then(|img| clip_blit_whole(&img, h > 0, src, dst));
let new = decode_clip(Bytes(&info), Bytes(&bits), keep_alpha, src, dst);
match (old, new) {
(None, None) => {}
(Some((a, ad)), Some((b, bd))) => {
assert_eq!((a.w, a.h, &a.px), (b.w, b.h, &b.px), "{bpp} bpp h={h} {src:?}");
assert_eq!(ad.map(f64::to_bits), bd.map(f64::to_bits), "{bpp} bpp h={h} {src:?}");
}
_ => panic!("old and new disagree on None: {bpp} bpp h={h} {src:?}"),
}
cases += 1;
}
}
}
}
}
}
assert!(cases > 10_000);
}
#[test]
fn decode_clip_rejects_what_decode_rejected() {
let mut seed = 7;
let (info, bits) = make_dib(7, 5, 24, &mut seed);
let short = &bits[..bits.len() - 1];
// A short bits buffer fails the whole image even when the crop lies in the rows that exist.
assert!(decode_clip(Bytes(&info), Bytes(short), false, [0, 0, 1, 1], [0.0, 0.0, 1.0, 1.0]).is_none());
assert!(decode_whole(Bytes(&info), Bytes(short), false).is_none());
}
#[test]
fn decode_clip_decodes_only_the_cropped_pixels() {
let mut seed = 99;
let (info, bits) = make_dib(400, -300, 24, &mut seed);
DECODED.with(|d| d.set(0));
let (img, _) = decode_clip(Bytes(&info), Bytes(&bits), false, [10, 20, 8, 4], [0.0, 0.0, 8.0, 4.0]).unwrap();
assert_eq!((img.w, img.h), (8, 4));
assert_eq!(DECODED.with(|d| d.get()), 8 * 4);
// The whole-bitmap path decodes all 400 x 300.
DECODED.with(|d| d.set(0));
decode_whole(Bytes(&info), Bytes(&bits), false).unwrap();
assert_eq!(DECODED.with(|d| d.get()), 400 * 300);
}
#[test]
fn decode_clip_refuses_a_side_no_renderer_draws() {
let cap = MAX_BITMAP_SIDE as usize;
let mut seed = 11;
// 1 bpp, one row longer than the cap: 5 KB of bits, 120 KB of RGBA.
let (info, bits) = make_dib(cap + 1, 1, 1, &mut seed);
assert!(decode_clip(Bytes(&info), Bytes(&bits), false, [0, 0, 0, 0], [0.0, 0.0, 1.0, 1.0]).is_none());
// The same for the height.
let (info, bits) = make_dib(1, cap as i32 + 1, 1, &mut seed);
assert!(decode_clip(Bytes(&info), Bytes(&bits), false, [0, 0, 0, 0], [0.0, 0.0, 1.0, 1.0]).is_none());
// A crop of the wide bitmap within the cap still decodes, and so does the cap itself.
let (info, bits) = make_dib(cap + 1, 1, 1, &mut seed);
let (img, _) = decode_clip(Bytes(&info), Bytes(&bits), false, [10, 0, 100, 1], [0.0, 0.0, 1.0, 1.0]).unwrap();
assert_eq!((img.w, img.h), (100, 1));
let (info, bits) = make_dib(cap, 1, 1, &mut seed);
let (img, _) = decode_clip(Bytes(&info), Bytes(&bits), false, [0, 0, 0, 0], [0.0, 0.0, 1.0, 1.0]).unwrap();
assert_eq!((img.w, img.h), (cap, 1));
}
}
+516
View File
@@ -0,0 +1,516 @@
//! EMF reader (MS-EMF). Records are (type, size in bytes) followed by a payload; offsets inside a
//! record (bitmap info and bits) are relative to the record start, so each handler reads the record slice.
use std::sync::Arc;
use crate::bytes::Bytes;
use crate::canvas::{BS_HATCHED, BS_NULL, Brush, Canvas, LogFont, MAX_DASH_ENTRIES, Obj, PS_NULL, Pen, Xform, colorref};
use crate::dib::{self, Blend};
use crate::geom::{self, ArcKind, P};
use crate::text::{ETO_GLYPH_INDEX, ETO_PDY, Font, MAX_CHARS, decode_bytes, decode_utf16};
use crate::{Error, Frame, MAX_PATH_OPS};
/// ExtTextOut options: no rectangle in the record (EMR_SMALLTEXTOUT, EmrText), and 8-bit
/// characters (EMR_SMALLTEXTOUT).
const ETO_NO_RECT: u32 = 0x0100;
const ETO_SMALL_CHARS: u32 = 0x0200;
const EMF_SIGNATURE: u32 = 0x464D_4520;
/// Hundredths of a millimetre to points.
const HMM_TO_PT: f64 = 72.0 / 2540.0;
/// SRCCOPY, the plain copy of a bitmap.
const SRCCOPY: u32 = 0x00CC_0020;
/// LogPenEx pen type bits; PS_COSMETIC pens are one device pixel wide.
const PS_TYPE_MASK: u32 = 0x000F_0000;
const PS_COSMETIC: u32 = 0;
pub(crate) fn is_emf(b: Bytes) -> bool {
b.u32(0) == Some(1) && b.u32(40) == Some(EMF_SIGNATURE)
}
/// The picture frame: `rclFrame` (0.01 mm) mapped to device units through `szlDevice` and
/// `szlMillimeters`, sized in points. Without a usable frame, the device bounds stand in (1 unit = 1 point).
pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), Error> {
if !is_emf(input) {
return Err(Error::BadHeader);
}
let hsize = input.u32(4).ok_or(Error::BadHeader)? as usize;
let bounds = rect32(input, 8).ok_or(Error::BadHeader)?;
let [l, t, r, b] = bounds;
let frame = frame_from_header(input).or_else(|| (r > l && b > t).then_some(Frame { rect: [l, t, r - l, b - t], size: None }));
let mut cv = Canvas::new();
if r > l && b > t {
cv.set_clip_surface([l, t, r, b]);
}
if let Some(px_mm) = device_px_per_mm(input) {
cv.set_device_mm(px_mm);
}
let mut plus = crate::emfplus::Plus::new();
plus.frame = frame.as_ref().map(|f| f.rect);
let mut parsed = 0usize;
let mut pos = hsize.max(88);
while !cv.full() {
let (Some(typ), Some(size)) = (input.u32(pos), pos.checked_add(4).and_then(|o| input.u32(o))) else { break };
let size = size as usize;
if size < 8 || !size.is_multiple_of(4) {
break;
}
let Some(rec) = input.slice(pos, size) else { break };
if typ == 14 {
break;
}
if typ == 70 && Bytes(rec).u32(12) == Some(crate::emfplus::EMF_PLUS) {
if let Some(data) = Bytes(rec).tail(16) {
plus.comment(&mut cv, data);
}
} else {
record(&mut cv, typ, Bytes(rec));
}
parsed += 1;
let Some(next) = pos.checked_add(size) else { break };
pos = next;
}
if parsed == 0 {
return Err(Error::NoRecords);
}
Ok((cv, frame))
}
/// Device pixels per millimetre on each axis, from `szlDevice` (72) and the device's size:
/// `szlMicrometers` (100) when the header has that extension (a header of 108 bytes or more) and
/// it is set, which is a thousand times finer, else `szlMillimeters` (80).
fn device_px_per_mm(input: Bytes) -> Option<(f64, f64)> {
let (dw, dh) = (input.i32(72)?, input.i32(76)?);
if dw <= 0 || dh <= 0 {
return None;
}
if input.u32(4)? >= 108
&& let (Some(uw), Some(uh)) = (input.u32(100), input.u32(104))
&& uw > 0
&& uh > 0
{
return Some((f64::from(dw) * 1000.0 / f64::from(uw), f64::from(dh) * 1000.0 / f64::from(uh)));
}
let (mw, mh) = (input.i32(80)?, input.i32(84)?);
(mw > 0 && mh > 0).then(|| (f64::from(dw) / f64::from(mw), f64::from(dh) / f64::from(mh)))
}
/// The frame from `rclFrame` (offset 24), `szlDevice` (72) and `szlMillimeters` (80) in the header.
pub(crate) fn frame_from_header(input: Bytes) -> Option<Frame> {
let [fl, ft, fr, fb] = rect32(input, 24)?;
if fr <= fl || fb <= ft {
return None;
}
// Device units per hundredth of a millimetre.
let (px_mm_x, px_mm_y) = device_px_per_mm(input)?;
let (sx, sy) = (px_mm_x / 100.0, px_mm_y / 100.0);
let rect = [fl * sx, ft * sy, (fr - fl) * sx, (fb - ft) * sy];
Some(Frame { rect, size: Some(((fr - fl) * HMM_TO_PT, (fb - ft) * HMM_TO_PT)) })
}
fn record(cv: &mut Canvas, typ: u32, r: Bytes) {
if cv.full() {
return;
}
let _ = match typ {
9 => xy32(r, 8).map(|p| cv.set_window_ext(p)),
10 => xy32(r, 8).map(|p| cv.set_window_org(p)),
11 => xy32(r, 8).map(|p| cv.set_viewport_ext(p)),
12 => xy32(r, 8).map(|p| cv.set_viewport_org(p)),
17 => r.u32(8).map(|m| cv.set_map_mode(m)),
19 => r.u32(8).map(|m| cv.set_polyfill(m)),
20 => r.u32(8).map(|m| cv.set_rop2(m)),
33 => {
cv.save_dc();
Some(())
}
34 => r.i32(8).map(|n| cv.restore_dc(n)),
// SCALEVIEWPORTEXTEX (31) and SCALEWINDOWEXTEX (32): xNum, xDenom, yNum, yDenom.
31 | 32 => (|| {
let v = [r.i32(8)?, r.i32(12)?, r.i32(16)?, r.i32(20)?].map(f64::from);
cv.scale_ext(typ == 32, (v[0], v[1]), (v[2], v[3]));
Some(())
})(),
57 => r.u32(8).map(|d| cv.set_arc_direction(d)),
58 => r.u32(8).map(|m| cv.set_miter_limit(f64::from(m))),
67 => r.u32(8).map(|m| cv.select_clip_path(m)),
68 => {
cv.abort_path();
Some(())
}
35 => xform(r, 8).map(|x| cv.modify_world(x, 4)),
36 => xform(r, 8).zip(r.u32(32)).map(|(x, m)| cv.modify_world(x, m)),
37 => r.u32(8).map(|i| cv.select(i)),
38 => create_pen(cv, r),
95 => create_ext_pen(cv, r),
39 => create_brush(cv, r),
93 | 94 => create_dib_brush(cv, r, typ == 93),
82 => create_font(cv, r),
18 => r.u32(8).map(|m| cv.set_bk_mode(m)),
22 => r.u32(8).map(|a| cv.set_text_align(a)),
24 => r.u32(8).map(|c| cv.set_text_color(c)),
25 => r.u32(8).map(|c| cv.set_bk_color(c)),
83 | 84 => ext_text_out(cv, r, typ == 84),
108 => small_text_out(cv, r),
40 => r.u32(8).map(|i| cv.delete_object(i)),
29 => rect32(r, 8).map(|rect| cv.exclude_clip_rect(rect)),
30 => rect32(r, 8).map(|rect| cv.intersect_clip_rect(rect)),
75 => ext_select_clip(cv, r),
27 => xy32(r, 8).map(|p| cv.move_to(p)),
54 => xy32(r, 8).map(|p| cv.line_to(p)),
42 => rect32(r, 8).map(|[l, t, rr, b]| cv.shape(geom::ellipse(l, t, rr, b), true, true)),
43 => rect32(r, 8).map(|[l, t, rr, b]| cv.shape(geom::rect(l, t, rr, b), true, true)),
44 => {
let (cw, ch) = (r.i32(24), r.i32(28));
let (Some(cw), Some(ch)) = (cw, ch) else { return };
rect32(r, 8).map(|[l, t, rr, b]| {
cv.shape(geom::round_rect(l, t, rr, b, f64::from(cw), f64::from(ch)), true, true);
})
}
45..=47 => arc32(r).map(|(bounds, s, e)| {
let kind = match typ {
45 => ArcKind::Arc,
46 => ArcKind::Chord,
_ => ArcKind::Pie,
};
let [l, t, rr, b] = bounds;
let (s, e) = cv.arc_ends(s, e);
cv.shape(geom::arc_shape(l, t, rr, b, s, e, kind), kind != ArcKind::Arc, true);
}),
4 | 87 => counted(r, typ == 4).map(|pts| cv.poly(&pts, false, false, true)),
3 | 86 => counted(r, typ == 3).map(|pts| cv.poly(&pts, true, true, true)),
6 | 89 => counted(r, typ == 6).map(|pts| cv.line_chain(&pts, false, true)),
// POLYBEZIER starts its own figure at the first point and leaves the current point alone;
// POLYBEZIERTO continues from the current point.
2 | 85 => counted(r, typ == 2).and_then(|pts| {
let from = *pts.first()?;
cv.bezier(from, pts.get(1..)?);
Some(())
}),
5 | 88 => counted(r, typ == 5).map(|pts| cv.bezier_to(&pts)),
7 | 90 => poly_groups(r, typ == 7).map(|(counts, pts)| cv.shape(geom::subpaths(&pts, &counts, false), false, true)),
8 | 91 => poly_groups(r, typ == 8).map(|(counts, pts)| cv.shape(geom::subpaths(&pts, &counts, true), true, true)),
59 => {
cv.begin_path();
Some(())
}
60 => {
cv.end_path();
Some(())
}
61 => {
cv.close_figure();
Some(())
}
62 => {
cv.paint_path(true, false);
Some(())
}
63 => {
cv.paint_path(true, true);
Some(())
}
64 => {
cv.paint_path(false, true);
Some(())
}
76 | 77 => bitblt(cv, r, typ == 77),
80 => set_dibits(cv, r),
81 => stretch_dibits(cv, r),
114 => alphablend(cv, r),
t if IGNORED.contains(&t) => Some(()),
_ => {
cv.skipped = cv.skipped.saturating_add(1);
Some(())
}
};
}
/// Records that change nothing the picture keeps: the header, the brush origin, mapper flags,
/// stretch mode, colour adjustment, palettes, FLATTENPATH, comments (EMF+ ones are read before
/// they get here), colour management and colour spaces, pixel formats, font linking and text
/// justification. Every other record the reader does not play is counted as skipped.
const IGNORED: &[u32] = &[1, 13, 16, 21, 23, 48, 49, 50, 51, 52, 65, 70, 98, 99, 100, 101, 104, 109, 110, 111, 112, 113, 119, 120, 121, 122];
/// EMR_EXTSELECTCLIPRGN: `cbRgnData` bytes of `RGNDATA` (rectangles in logical units) and a region
/// mode. No region data selects the default clip.
fn ext_select_clip(cv: &mut Canvas, r: Bytes) -> Option<()> {
let cb = r.u32(8)? as usize;
let mode = r.u32(12)?;
if cb == 0 {
cv.reset_clip();
return Some(());
}
let header = 16;
if cb < 32 {
return Some(());
}
// RDH_RECTANGLES is the only region type GDI writes.
if r.u32(header + 4)? != 1 {
return Some(());
}
let n = (r.u32(header + 8)? as usize).min(crate::clip::MAX_CLIP_RECTS);
let end = header.checked_add(cb)?;
let mut rects = Vec::new();
for i in 0..n {
let o = header + 32 + i * 16;
if o + 16 > end {
break;
}
rects.push(rect32(r, o)?);
}
cv.select_clip_rects(&rects, mode);
Some(())
}
/// Two little-endian i32 values at `off`, as a point.
fn xy32(r: Bytes, off: usize) -> Option<P> {
Some((f64::from(r.i32(off)?), f64::from(r.i32(off + 4)?)))
}
/// RECTL (left, top, right, bottom) as i32 values widened to f64.
fn rect32(r: Bytes, off: usize) -> Option<[f64; 4]> {
Some([f64::from(r.i32(off)?), f64::from(r.i32(off + 4)?), f64::from(r.i32(off + 8)?), f64::from(r.i32(off + 12)?)])
}
/// An i32 field as f64; 0 past the end.
fn i32s(r: Bytes, off: usize) -> f64 {
r.i32(off).map_or(0.0, f64::from)
}
/// XFORM: six f32 values in M11, M12, M21, M22, Dx, Dy order.
fn xform(r: Bytes, off: usize) -> Option<Xform> {
let mut x = [0.0f64; 6];
for (i, v) in x.iter_mut().enumerate() {
*v = f64::from(r.f32(off + i * 4)?);
}
Some(x)
}
/// ARC, CHORD, PIE: bounds at 8, start point at 24, end point at 32.
fn arc32(r: Bytes) -> Option<([f64; 4], P, P)> {
Some((rect32(r, 8)?, xy32(r, 24)?, xy32(r, 32)?))
}
/// A point count from a record, rejected when it exceeds the path budget (before any allocation).
fn budget(n: u32) -> Option<usize> {
let n = n as usize;
(n <= MAX_PATH_OPS).then_some(n)
}
/// Point count at 24, then the points (32-bit or 16-bit) at 28.
fn counted(r: Bytes, wide: bool) -> Option<Vec<P>> {
let n = budget(r.u32(24)?)?;
r.tail(28)?.points(0, n, wide)
}
/// Subpath count at 24, total point count at 28, per-subpath counts at 32, then the points.
fn poly_groups(r: Bytes, wide: bool) -> Option<(Vec<usize>, Vec<P>)> {
let n = budget(r.u32(24)?)?;
// The record must hold the n counts before anything is allocated for them.
let raw = r.slice(32, n.checked_mul(4)?)?;
let counts: Vec<usize> = raw.as_chunks::<4>().0.iter().map(|c| u32::from_le_bytes(*c) as usize).collect();
let total = counts.iter().try_fold(0usize, |a, c| a.checked_add(*c))?;
let total = budget(u32::try_from(total).ok()?)?;
let pts = r.tail(n.checked_mul(4)?.checked_add(32)?)?.points(0, total, wide)?;
Some((counts, pts))
}
/// EMR_CREATEPEN: handle, LogPen (style, width x/y, colour).
fn create_pen(cv: &mut Canvas, r: Bytes) -> Option<()> {
let slot = r.u32(8)? as usize;
let style = r.u32(12)?;
let width = r.i32(16)?;
let color = r.u32(24)?;
cv.add_object(Some(slot), Obj::Pen(Pen::new(style, f64::from(width), colorref(color))));
Some(())
}
/// EMR_EXTCREATEPEN: handle, bitmap offsets (unused), LogPenEx (style, width, brush style, colour,
/// hatch, the number of style entries at 48 and the entries from 52). A cosmetic pen's width is in
/// device pixels (always 1), so it is a hairline; a BS_NULL brush draws no line at all. The style
/// word keeps its end cap and join; the entries are the dashes and gaps of a PS_USERSTYLE pen.
fn create_ext_pen(cv: &mut Canvas, r: Bytes) -> Option<()> {
let slot = r.u32(8)? as usize;
let (style, width, brush, color) = (r.u32(28)?, r.u32(32)?, r.u32(36)?, r.u32(40)?);
let width = if style & PS_TYPE_MASK == PS_COSMETIC { 0.0 } else { f64::from(width) };
let n = (r.u32(48).unwrap_or(0) as usize).min(MAX_DASH_ENTRIES);
let entries = (0..n).map_while(|i| r.u32(52 + i * 4)).map(f64::from).collect();
let style = if brush == BS_NULL { PS_NULL } else { style };
cv.add_object(Some(slot), Obj::Pen(Pen { style, width, color: colorref(color), entries }));
Some(())
}
/// EMR_CREATEBRUSHINDIRECT: handle, LogBrush32 (style, colour, hatch).
fn create_brush(cv: &mut Canvas, r: Bytes) -> Option<()> {
let slot = r.u32(8)? as usize;
let style = r.u32(12)?;
let color = r.u32(16)?;
let mut b = Brush::new(style, colorref(color));
if style == BS_HATCHED {
b.hatch = r.u32(20).unwrap_or(0);
}
cv.add_object(Some(slot), Obj::Brush(b));
Some(())
}
/// EMR_CREATEDIBPATTERNBRUSHPT (94) and EMR_CREATEMONOBRUSH (93): handle, colour usage, then the
/// bitmap info and bits as offset/size pairs. A mono brush (or one whose palette holds indices,
/// DIB_PAL_COLORS) takes the text and background colours. An undecodable bitmap leaves the slot
/// empty.
fn create_dib_brush(cv: &mut Canvas, r: Bytes, mono: bool) -> Option<()> {
let slot = r.u32(8)? as usize;
let usage = r.u32(12)?;
let (bo, bl) = (r.u32(16)? as usize, r.u32(20)? as usize);
let (po, pl) = (r.u32(24)? as usize, r.u32(28)? as usize);
let img = dib::decode(Bytes(r.slice(bo, bl)?), Bytes(r.slice(po, pl)?), false)?;
let b = Brush::pattern(img, mono || usage == 1)?;
cv.add_object(Some(slot), Obj::Brush(b));
Some(())
}
/// EMR_EXTCREATEFONTINDIRECTW: handle, then a LogFont (MS-EMF 2.2.13): height, width, escapement,
/// orientation, weight (i32 each), italic, underline, strike-out, charset and four more bytes, then
/// the face name in 32 UTF-16 units (NUL-terminated).
fn create_font(cv: &mut Canvas, r: Bytes) -> Option<()> {
let slot = r.u32(8)? as usize;
let height = r.i32(12)?;
let width = r.i32(16).unwrap_or(0);
let escapement = r.i32(20).unwrap_or(0);
let weight = r.i32(28).unwrap_or(0);
let byte = |o: usize| r.0.get(o).copied().unwrap_or(0);
let units: Vec<u16> = (0..32).map_while(|i| r.u16(40 + 2 * i)).take_while(|u| *u != 0).collect();
let (family, _) = decode_utf16(&units, &[]);
let font = Font {
family: family.trim().to_string(),
height: height as f32,
width: width as f32,
weight: weight.clamp(0, 1000) as u16,
italic: byte(32) != 0,
underline: byte(33) != 0,
strike: byte(34) != 0,
charset: byte(35),
};
cv.add_object(Some(slot), Obj::Font(Arc::new(LogFont { font, escapement: f64::from(escapement) })));
Some(())
}
/// The dx array at `off` for `n` characters: i32 advances, or (dx, dy) pairs under ETO_PDY (only
/// dx is used). Empty when absent or short.
fn dx_array(r: Bytes, off: usize, n: usize, opts: u32) -> Vec<f32> {
let step = if opts & ETO_PDY != 0 { 8 } else { 4 };
if off == 0 {
return Vec::new();
}
let dx: Vec<f32> = (0..n).map_while(|i| r.i32(off.checked_add(i.checked_mul(step)?)?)).map(|v| v as f32).collect();
if dx.len() == n { dx } else { Vec::new() }
}
/// EMR_EXTTEXTOUTA (83) and EMR_EXTTEXTOUTW (84): bounds, graphics mode and scales, then an EmrText
/// at 36: reference point, character count, string offset, options, the rectangle (absent under
/// ETO_NO_RECT) and the dx offset. Offsets count from the record start. Glyph-index strings name
/// glyphs of a font the reader cannot match, so they are left out.
fn ext_text_out(cv: &mut Canvas, r: Bytes, wide: bool) -> Option<()> {
let at = xy32(r, 36)?;
let n = (r.u32(44)? as usize).min(MAX_CHARS);
let off = r.u32(48)? as usize;
let opts = r.u32(52)?;
let (rect, dx_off) = if opts & ETO_NO_RECT != 0 { (None, r.u32(56)?) } else { (rect32(r, 56), r.u32(72)?) };
if opts & ETO_GLYPH_INDEX != 0 {
return None;
}
let dx = dx_array(r, dx_off as usize, n, opts);
let (text, dx) = if wide {
let raw = r.slice(off, n.checked_mul(2)?)?;
let units: Vec<u16> = raw.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect();
decode_utf16(&units, &dx)
} else {
decode_bytes(r.slice(off, n)?, cv.font_charset(), &dx)
};
cv.text(at, text, dx, opts, rect);
Some(())
}
/// EMR_SMALLTEXTOUT (MS-EMF 2.3.5.37): x, y, character count, options, graphics mode, scales, the
/// bounds (absent under ETO_NO_RECT), then the characters: 8-bit under ETO_SMALL_CHARS, else UTF-16.
fn small_text_out(cv: &mut Canvas, r: Bytes) -> Option<()> {
let at = xy32(r, 8)?;
let n = (r.u32(16)? as usize).min(MAX_CHARS);
let opts = r.u32(20)?;
if opts & ETO_GLYPH_INDEX != 0 {
return None;
}
let (rect, off) = if opts & ETO_NO_RECT != 0 { (None, 36) } else { (rect32(r, 36), 52) };
let (text, _) = if opts & ETO_SMALL_CHARS != 0 {
decode_bytes(r.slice(off, n)?, cv.font_charset(), &[])
} else {
let raw = r.slice(off, n.checked_mul(2)?)?;
let units: Vec<u16> = raw.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect();
decode_utf16(&units, &[])
};
cv.text(at, text, Vec::new(), opts, rect);
Some(())
}
/// BITBLT (76) and STRETCHBLT (77): dest at 24..40, rop at 40, source x/y at 44, bmi at 84, bits at
/// 92; STRETCHBLT adds the source w/h at 100. A source-less rop fills the destination instead.
fn bitblt(cv: &mut Canvas, r: Bytes, stretch: bool) -> Option<()> {
let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 32), i32s(r, 36)];
let rop = r.u32(40)?;
if Canvas::sourceless(rop) {
cv.fill_rop(dst, rop);
return Some(());
}
let (sw, sh) = if stretch { (i32s(r, 100), i32s(r, 104)) } else { (dst[2], dst[3]) };
let src = [i32s(r, 44), i32s(r, 48), sw, sh];
blit(cv, r, dst, src, (84, 92), Blend::Rop(rop))
}
/// ALPHABLEND: laid out like STRETCHBLT, with a BLENDFUNCTION at 40. Constant alpha at 42 applies to
/// every pixel; AlphaFormat at 43 with AC_SRC_ALPHA (1) also uses the per-pixel alpha.
fn alphablend(cv: &mut Canvas, r: Bytes) -> Option<()> {
let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 32), i32s(r, 36)];
let constant = *r.0.get(42)?;
let per_pixel = (*r.0.get(43)? & 0x01) != 0;
let src = [i32s(r, 44), i32s(r, 48), i32s(r, 100), i32s(r, 104)];
blit(cv, r, dst, src, (84, 92), Blend::Alpha { constant, per_pixel })
}
/// STRETCHDIBITS: dest x/y at 24, src x/y/w/h at 32..48, bmi offset/size at 48, bits at 56, rop at 68,
/// dest w/h at 72.
fn stretch_dibits(cv: &mut Canvas, r: Bytes) -> Option<()> {
let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 72), i32s(r, 76)];
let src = [i32s(r, 32), i32s(r, 36), i32s(r, 40), i32s(r, 44)];
let rop = r.u32(68)?;
blit(cv, r, dst, src, (48, 56), Blend::Rop(rop))
}
/// SETDIBITSTODEVICE (MS-EMF 2.3.1.5): dest x/y at 24, src x/y at 32, size (cxSrc, cySrc) at 40, bmi
/// at 48, bits at 56, start scan at 68. The image keeps its own size; the bits begin at scan line
/// `iStartScan`, so the source y counts from there.
fn set_dibits(cv: &mut Canvas, r: Bytes) -> Option<()> {
let (cx, cy) = (i32s(r, 40), i32s(r, 44));
if cx <= 0.0 || cy <= 0.0 {
return None;
}
let dst = [i32s(r, 24), i32s(r, 28), cx, cy];
let src = [i32s(r, 32), i32s(r, 36) - f64::from(r.u32(68)?), cx, cy];
blit(cv, r, dst, src, (48, 56), Blend::Rop(SRCCOPY))
}
/// Decodes the bitmap at `at` = (offset of the bmi offset/length pair, offset of the bits pair), both
/// inside the record, and stretches the source rectangle `src` into `dst`. Both are `[x, y, w, h]`; a
/// non-positive source size means the whole bitmap. `blend` decides how it combines.
fn blit(cv: &mut Canvas, r: Bytes, dst: [f64; 4], src: [f64; 4], at: (usize, usize), blend: Blend) -> Option<()> {
let (bo, bl) = (r.u32(at.0)? as usize, r.u32(at.0 + 4)? as usize);
let (po, pl) = (r.u32(at.1)? as usize, r.u32(at.1 + 4)? as usize);
if pl == 0 {
return None;
}
let info = Bytes(r.slice(bo, bl)?);
let pixels = Bytes(r.slice(po, pl)?);
let (mut img, dst) = dib::decode_clip(info, pixels, blend.per_pixel(), src.map(|v| v as i64), dst)?;
dib::prepare(&mut img, blend);
cv.bitmap(dst, img);
Some(())
}
File diff suppressed because it is too large. Load diff
+128
View File
@@ -0,0 +1,128 @@
//! Path operations in `f64` and the shape builders that turn WMF/EMF rectangles, ellipses and arcs
//! into cubic Béziers (the classic 4/3·tan(Δ/4) approximation, at most 90° per piece).
use std::f64::consts::{FRAC_PI_2, PI, TAU};
pub(crate) type P = (f64, f64);
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Op {
M(P),
L(P),
C(P, P, P),
Z,
}
pub(crate) fn rect(l: f64, t: f64, r: f64, b: f64) -> Vec<Op> {
vec![Op::M((l, t)), Op::L((r, t)), Op::L((r, b)), Op::L((l, b)), Op::Z]
}
/// Corner radii are half the EMF/WMF corner ellipse size, clamped so the corners never overlap.
pub(crate) fn round_rect(l: f64, t: f64, r: f64, b: f64, ew: f64, eh: f64) -> Vec<Op> {
// Reversed corners (right < left, bottom < top) describe the same rectangle.
let (l, r) = (l.min(r), l.max(r));
let (t, b) = (t.min(b), t.max(b));
let rx = (ew / 2.0).abs().min((r - l) / 2.0);
let ry = (eh / 2.0).abs().min((b - t) / 2.0);
let mut ops = vec![Op::M((l + rx, t)), Op::L((r - rx, t))];
ops.extend(arc(r - rx, t + ry, rx, ry, -FRAC_PI_2, FRAC_PI_2));
ops.push(Op::L((r, b - ry)));
ops.extend(arc(r - rx, b - ry, rx, ry, 0.0, FRAC_PI_2));
ops.push(Op::L((l + rx, b)));
ops.extend(arc(l + rx, b - ry, rx, ry, FRAC_PI_2, FRAC_PI_2));
ops.push(Op::L((l, t + ry)));
ops.extend(arc(l + rx, t + ry, rx, ry, PI, FRAC_PI_2));
ops.push(Op::Z);
ops
}
pub(crate) fn ellipse(l: f64, t: f64, r: f64, b: f64) -> Vec<Op> {
let (cx, cy, rx, ry) = center(l, t, r, b);
let mut ops = vec![Op::M((cx + rx, cy))];
ops.extend(arc(cx, cy, rx, ry, 0.0, TAU));
ops.push(Op::Z);
ops
}
/// Which closed or open shape an ARC/CHORD/PIE record draws.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ArcKind {
Arc,
Chord,
Pie,
}
/// ARC, CHORD and PIE: the arc runs counter-clockwise on screen from the start point to the end point.
pub(crate) fn arc_shape(l: f64, t: f64, r: f64, b: f64, s: P, e: P, kind: ArcKind) -> Vec<Op> {
let (cx, cy, rx, ry) = center(l, t, r, b);
let ang = |p: P| ((p.1 - cy) / ry.max(f64::MIN_POSITIVE)).atan2((p.0 - cx) / rx.max(f64::MIN_POSITIVE));
let (a0, a1) = (ang(s), ang(e));
let mut d = (a0 - a1).rem_euclid(TAU);
if d < 1e-12 {
d = TAU;
}
let sweep = -d;
let start = (cx + rx * a0.cos(), cy + ry * a0.sin());
let mut ops = if kind == ArcKind::Pie { vec![Op::M((cx, cy)), Op::L(start)] } else { vec![Op::M(start)] };
ops.extend(arc(cx, cy, rx, ry, a0, sweep));
match kind {
ArcKind::Arc => {}
ArcKind::Chord => ops.push(Op::Z),
ArcKind::Pie => ops.extend([Op::L((cx, cy)), Op::Z]),
}
ops
}
fn center(l: f64, t: f64, r: f64, b: f64) -> (f64, f64, f64, f64) {
((l + r) / 2.0, (t + b) / 2.0, (r - l).abs() / 2.0, (b - t).abs() / 2.0)
}
/// Cubic pieces along an ellipse from parameter `a0` over signed `sweep` radians. The current point
/// must already be at `(cx + rx cos a0, cy + ry sin a0)`.
fn arc(cx: f64, cy: f64, rx: f64, ry: f64, a0: f64, sweep: f64) -> Vec<Op> {
let n = ((sweep.abs() / FRAC_PI_2).ceil() as usize).clamp(1, 8);
let step = sweep / n as f64;
let k = 4.0 / 3.0 * (step / 4.0).tan();
let pt = |t: f64| (cx + rx * t.cos(), cy + ry * t.sin());
let mut ops = Vec::with_capacity(n);
for i in 0..n {
let t0 = a0 + step * i as f64;
let t1 = t0 + step;
let (p0, p3) = (pt(t0), pt(t1));
let c1 = (p0.0 - k * rx * t0.sin(), p0.1 + k * ry * t0.cos());
let c2 = (p3.0 + k * rx * t1.sin(), p3.1 - k * ry * t1.cos());
ops.push(Op::C(c1, c2, p3));
}
ops
}
/// Consecutive subpaths: `counts[i]` points each, taken in order from `pts`. Counts that run past the
/// point list are cut short rather than failing the whole record.
pub(crate) fn subpaths(pts: &[P], counts: &[usize], close: bool) -> Vec<Op> {
// The exact size first: empty groups add no ops, so a long count list of zeros (file data)
// mustn't reserve room the canvas budget never sees.
let (mut n, mut at) = (0usize, 0usize);
for &c in counts {
let end = at.saturating_add(c).min(pts.len());
if end > at {
n = n.saturating_add(end - at + usize::from(close));
}
at = end;
}
let mut ops = Vec::with_capacity(n);
let mut at = 0usize;
for &c in counts {
let end = at.saturating_add(c).min(pts.len());
let Some((first, rest)) = pts.get(at..end).and_then(<[_]>::split_first) else {
at = end;
continue;
};
ops.push(Op::M(*first));
ops.extend(rest.iter().map(|p| Op::L(*p)));
if close {
ops.push(Op::Z);
}
at = end;
}
ops
}
+452
View File
@@ -0,0 +1,452 @@
//! Windows metafiles: reads Windows Metafiles (WMF), Enhanced Metafiles (EMF) and EMF+ into a
//! neutral vector model, and (feature `write`) writes EMF and WMF.
//!
//! [`parse`] plays a file's records on a model of the device context and returns a [`Picture`]:
//! filled and stroked paths with their line styles, pattern fills, RGBA bitmaps and text strings,
//! each with the clip it was drawn under, in a picture frame with y down. Nothing in the model
//! belongs to an application: mapping it onto a document (or drawing it) is the caller's.
//! [`Picture::place`] fits a picture into a rectangle for callers that draw it.
//!
//! Text stays font-agnostic: a [`Text`] item names the font the file asks for and carries the
//! string, its advances and alignment; the caller resolves the font and draws the glyphs.
//!
//! The reader is pure Rust over byte slices (no file system), so it builds for wasm. Every read is
//! bounds-checked and every count is capped; hostile input yields a shorter picture or an [`Error`],
//! never a panic. Records follow MS-WMF, MS-EMF and MS-EMFPLUS; the README lists the records read
//! and the limits. Records of a type the reader does not know are counted in
//! [`Picture::skipped_records`].
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
mod bytes;
mod canvas;
mod clip;
mod cliprect;
mod dib;
mod emf;
mod emfplus;
mod geom;
mod place;
mod size;
mod text;
mod wmf;
#[cfg(feature = "write")]
pub mod write;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use std::sync::Arc;
use bytes::Bytes;
use canvas::Raw;
use geom::{Op, P};
pub use place::{MAX_BITMAP_SIDE, PlacedClip, PlacedItem, bez_path, crop_window};
pub use size::size;
pub use text::{Font, HAlign, SYMBOL_CHARSET, Text, VAlign};
/// Items kept from one file. Past the cap the parse stops and keeps what it has.
pub(crate) const MAX_ITEMS: usize = 200_000;
/// Object table slots (WMF handles, EMF object indices).
pub(crate) const MAX_OBJECTS: usize = 4096;
/// Largest bitmap in pixels (about 40 megapixels, 160 MB of RGBA).
pub(crate) const MAX_PIXELS: usize = 40_000_000;
/// DC save-stack depth.
pub(crate) const MAX_STACK: usize = 1024;
/// Points in one open path bracket.
pub(crate) const MAX_PATH_OPS: usize = 2_000_000;
/// Bytes of path ops and pixels kept from one file (about 256 MB).
pub(crate) const MAX_BYTES: usize = 256 * 1024 * 1024;
/// A parsed metafile. Items use coordinates in `0..width` and `0..height`, y down.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Picture {
/// Size of the drawing in its own units: points for placeable WMF and EMF (from the frame), device
/// or logical units otherwise.
pub width: f32,
pub height: f32,
pub items: Vec<Item>,
/// Records of a type the reader does not know (neither drawn nor known to change nothing that
/// is kept). A caller can tell its user that part of the file was not read.
pub skipped_records: u32,
}
/// How the ends of an open stroke and of its dashes are drawn (`PS_ENDCAP_*`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Cap {
#[default]
Round,
Square,
Flat,
}
/// How the corners of a stroke are drawn (`PS_JOIN_*`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Join {
#[default]
Round,
Bevel,
Miter,
}
/// Caps, joins and dashes of a stroke. The default is GDI's: round caps and joins, a miter limit
/// of 10, a solid line.
#[derive(Debug, Clone, PartialEq)]
pub struct LineStyle {
pub cap: Cap,
pub join: Join,
/// Longest miter as a multiple of the stroke width (for [`Join::Miter`]).
pub miter_limit: f32,
/// Dash, gap, dash, gap… lengths in the units of the stroke width, starting at the path's
/// start. Empty is a solid line.
pub dash: Vec<f32>,
}
impl Default for LineStyle {
fn default() -> Self {
LineStyle { cap: Cap::Round, join: Join::Round, miter_limit: 10.0, dash: Vec::new() }
}
}
impl LineStyle {
/// The same style with its dash lengths times `k`. None when a length stops being finite.
fn scaled(&self, k: f64) -> Option<LineStyle> {
let dash = self.dash.iter().map(|d| f(f64::from(*d) * k)).collect::<Option<Vec<f32>>>()?;
Some(LineStyle { dash, ..self.clone() })
}
}
/// A clip path (`SELECTCLIPPATH`), in picture units. Items clipped by the same path share it, so
/// `Arc::ptr_eq` tells which items a clip covers.
#[derive(Debug, Clone, PartialEq)]
pub struct ClipPath {
pub segs: Vec<Seg>,
pub even_odd: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Item {
/// A filled and/or stroked path, in picture coordinates.
Path {
segs: Vec<Seg>,
fill: Option<[u8; 4]>,
/// Colour and width in picture units.
stroke: Option<([u8; 4], f32)>,
/// Caps, joins and dashes of the stroke (dash lengths in picture units).
line: LineStyle,
even_odd: bool,
/// Clip region in picture units, each rectangle `[x0, y0, x1, y1]`. None is the default clip.
region: Option<Vec<[f32; 4]>>,
/// Clip paths: the item shows inside all of them (and inside `region`). Outermost first.
clips: Vec<Arc<ClipPath>>,
},
/// A bitmap stretched into `rect` = `[x, y, w, h]` (picture units), RGBA8 rows top-down, straight
/// alpha.
Bitmap {
rect: [f32; 4],
/// The unit square (the whole bitmap, y down) to picture space as affine coefficients
/// `[a, b, c, d, e, f]` (`x' = a·x + c·y + e`, `y' = b·x + d·y + f`, kurbo's order). For an
/// upright bitmap this is `rect` again (`[w, 0, 0, h, x, y]`); under a world transform that
/// turns or skews the bitmap it carries the turn, and `rect` is the box around the turned bitmap.
transform: [f64; 6],
width: u32,
height: u32,
rgba: Vec<u8>,
/// Clip region in picture units. None is the default clip.
region: Option<Vec<[f32; 4]>>,
/// Clip paths, as on [`Item::Path`].
clips: Vec<Arc<ClipPath>>,
},
/// A path filled with a repeating tile (pattern and hatch brushes). Items filled with the same
/// brush share its tile.
Pattern {
segs: Vec<Seg>,
even_odd: bool,
tile: Arc<Tile>,
/// Clip region in picture units. None is the default clip.
region: Option<Vec<[f32; 4]>>,
/// Clip paths, as on [`Item::Path`].
clips: Vec<Arc<ClipPath>>,
},
/// A text string.
Text(Box<Text>),
}
/// A brush tile: RGBA8 rows top-down (transparent where the brush leaves the background), each
/// pixel `cell` picture units, with a tile's top-left corner at `origin`.
#[derive(Debug, Clone, PartialEq)]
pub struct Tile {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
pub cell: [f32; 2],
pub origin: [f32; 2],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Seg {
Move(f32, f32),
Line(f32, f32),
Cubic(f32, f32, f32, f32, f32, f32),
Close,
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum Error {
#[error("not a WMF or EMF file")]
NotMetafile,
#[error("metafile header is truncated or invalid")]
BadHeader,
#[error("metafile has no readable records")]
NoRecords,
}
/// The picture frame in device units (`rect` = `[x, y, w, h]`) and, when the file states one, its
/// size in picture units.
pub(crate) struct Frame {
pub rect: [f64; 4],
pub size: Option<(f64, f64)>,
}
/// Parses a WMF (placeable or plain) or EMF file.
pub fn parse(bytes: &[u8]) -> Result<Picture, Error> {
dib::set_decode_budget(dib::MAX_DECODE_PIXELS);
parse_nested(bytes)
}
/// [`parse`] for a metafile inside the one being parsed: it draws on the same budgets.
pub(crate) fn parse_nested(bytes: &[u8]) -> Result<Picture, Error> {
let b = Bytes(bytes);
let (cv, frame) = if emf::is_emf(b) {
emf::parse(b)?
} else if wmf::is_wmf(b) {
// A WMF converted from an EMF can carry the original in MFCOMMENT escapes; Office draws
// the WMF's own records (its dx rounding shows), so the copy is not used.
wmf::parse(b)?
} else {
return Err(Error::NotMetafile);
};
let skipped = cv.skipped;
let mut pic = build(cv.items, frame);
pic.skipped_records = skipped;
Ok(pic)
}
/// True when the bytes start like a WMF (placeable or plain header) or an EMF.
pub fn is_metafile(bytes: &[u8]) -> bool {
sniff(bytes).is_some()
}
/// A metafile format.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
/// Enhanced Metafile (32-bit coordinates, Bézier curves, paths and clipping), with or without
/// EMF+ records.
Emf,
/// Windows Metafile, with or without a placeable header (16-bit coordinates, polygons).
Wmf,
}
/// The metafile format `bytes` start like, if any.
pub fn sniff(bytes: &[u8]) -> Option<Kind> {
let b = Bytes(bytes);
if emf::is_emf(b) {
Some(Kind::Emf)
} else if wmf::is_wmf(b) {
Some(Kind::Wmf)
} else {
None
}
}
/// Maps device-space output into the picture frame. Without a usable frame the frame is the bounding
/// box of the drawing, so the origin is still the top-left of what was drawn.
fn build(raw: Vec<Raw>, frame: Option<Frame>) -> Picture {
let frame = frame.filter(|f| f.rect.iter().all(|v| v.is_finite()) && f.rect[2] > 0.0 && f.rect[3] > 0.0);
let (rect, size) = match frame {
Some(f) => (f.rect, f.size.unwrap_or((f.rect[2], f.rect[3]))),
None => match bbox(&raw) {
Some([x0, y0, x1, y1]) => ([x0, y0, x1 - x0, y1 - y0], (x1 - x0, y1 - y0)),
None => return Picture::default(),
},
};
let sx = if rect[2] > 0.0 { size.0 / rect[2] } else { 1.0 };
let sy = if rect[3] > 0.0 { size.1 / rect[3] } else { 1.0 };
let tf = move |p: P| ((p.0 - rect[0]) * sx, (p.1 - rect[1]) * sy);
let stroke_scale = (sx + sy) / 2.0;
// No stroke is wider than the picture's diagonal: anything wider is a mapping artefact.
let diag = size.0.hypot(size.1);
// Tiles shared by several fills stay shared.
let mut tiles: HashMap<*const canvas::RawTile, (Arc<canvas::RawTile>, Arc<Tile>)> = HashMap::new();
// So do clip paths. None is a path with a point that is not finite: its items are left out.
let mut clip_paths: ClipCache = HashMap::new();
let mut clip = |region: canvas::RawClip| -> Option<ItemClip> {
let mut paths = Vec::with_capacity(region.paths.len());
for raw in region.paths {
let key = Arc::as_ptr(&raw);
let mapped = match clip_paths.get(&key) {
Some((_, c)) => c.clone(),
None => {
let c = raw
.ops
.iter()
.map(|op| seg(op, tf))
.collect::<Option<Vec<Seg>>>()
.map(|segs| Arc::new(ClipPath { segs, even_odd: raw.even_odd }));
// The raw path is kept with its key, so the address is never reused by another.
clip_paths.insert(key, (raw, c.clone()));
c
}
};
paths.push(mapped?);
}
Some((map_region(region.rects, &tf)?, paths))
};
let items = raw.into_iter().filter_map(|r| match r {
Raw::Path { ops, fill, stroke, line, even_odd, region } => {
let segs = ops.iter().map(|op| seg(op, tf)).collect::<Option<Vec<Seg>>>()?;
if segs.len() < 2 {
return None;
}
let stroke = stroke.and_then(|(c, w)| Some((c, f((w * stroke_scale).min(diag))?)));
let (region, clips) = clip(region)?;
Some(Item::Path { segs, fill, stroke, line: line.scaled(stroke_scale)?, even_odd, region, clips })
}
Raw::Bitmap { rect: r, xf, img, region } => {
let (x, y) = tf((r[0], r[1]));
let rect = [f(x)?, f(y)?, f(r[2] * sx)?, f(r[3] * sy)?];
let (e, g) = tf((xf[4], xf[5]));
let transform = [xf[0] * sx, xf[1] * sy, xf[2] * sx, xf[3] * sy, e, g];
if !transform.iter().all(|v| v.is_finite()) {
return None;
}
let (region, clips) = clip(region)?;
Some(Item::Bitmap { rect, transform, width: img.w as u32, height: img.h as u32, rgba: img.px, region, clips })
}
Raw::Pattern { ops, even_odd, tile, region } => {
let segs = ops.iter().map(|op| seg(op, tf)).collect::<Option<Vec<Seg>>>()?;
if segs.len() < 2 {
return None;
}
let key = Arc::as_ptr(&tile);
let tile = match tiles.get(&key) {
Some((_, t)) => t.clone(),
None => {
let (ox, oy) = tf((0.0, 0.0));
let t = Arc::new(Tile {
width: u32::try_from(tile.w).ok()?,
height: u32::try_from(tile.h).ok()?,
rgba: tile.px.clone(),
cell: [f(tile.cell * sx)?, f(tile.cell * sy)?],
origin: [f(ox)?, f(oy)?],
});
// The raw tile is kept with its key, so the address is never reused by another.
tiles.insert(key, (tile.clone(), t.clone()));
t
}
};
let (region, clips) = clip(region)?;
Some(Item::Pattern { segs, even_odd, tile, region, clips })
}
Raw::Text { mut text, region } => {
// Device to picture: scale by (sx, sy) after moving the frame's corner to the origin.
let [a, b, c, d, e, g] = text.transform;
let (e, g) = tf((e, g));
text.transform = [a * sx, b * sy, c * sx, d * sy, e, g];
if !text.transform.iter().all(|v| v.is_finite()) {
return None;
}
if let Some([x0, y0, x1, y1]) = text.clip {
let (p, q) = (tf((f64::from(x0), f64::from(y0))), tf((f64::from(x1), f64::from(y1))));
text.clip = Some([f(p.0.min(q.0))?, f(p.1.min(q.1))?, f(p.0.max(q.0))?, f(p.1.max(q.1))?]);
}
(text.region, text.clips) = clip(region)?;
Some(Item::Text(text))
}
});
let items = items.collect();
Picture { width: f(size.0).unwrap_or(0.0), height: f(size.1).unwrap_or(0.0), items, skipped_records: 0 }
}
/// An item's clip in picture units: the region rectangles and the clip paths.
type ItemClip = (Option<Vec<[f32; 4]>>, Vec<Arc<ClipPath>>);
/// Device clip paths already mapped into the picture, by address.
type ClipCache = HashMap<*const canvas::RawClipPath, (Arc<canvas::RawClipPath>, Option<Arc<ClipPath>>)>;
fn map_region(region: Option<Vec<[f64; 4]>>, tf: &impl Fn(P) -> P) -> Option<Option<Vec<[f32; 4]>>> {
let Some(rects) = region else { return Some(None) };
let mut out = Vec::with_capacity(rects.len());
for [x0, y0, x1, y1] in rects {
let (p, q) = (tf((x0, y0)), tf((x1, y1)));
out.push([f(p.0.min(q.0))?, f(p.1.min(q.1))?, f(p.0.max(q.0))?, f(p.1.max(q.1))?]);
}
Some(Some(out))
}
fn f(v: f64) -> Option<f32> {
let v = v as f32;
v.is_finite().then_some(v)
}
fn seg(op: &Op, tf: impl Fn(P) -> P) -> Option<Seg> {
let pt = |p: P| {
let (x, y) = tf(p);
Some((f(x)?, f(y)?))
};
Some(match *op {
Op::M(p) => {
let (x, y) = pt(p)?;
Seg::Move(x, y)
}
Op::L(p) => {
let (x, y) = pt(p)?;
Seg::Line(x, y)
}
Op::C(a, b, c) => {
let (x1, y1) = pt(a)?;
let (x2, y2) = pt(b)?;
let (x3, y3) = pt(c)?;
Seg::Cubic(x1, y1, x2, y2, x3, y3)
}
Op::Z => Seg::Close,
})
}
/// Bounding box `[x0, y0, x1, y1]` over the finite points of the output.
fn bbox(raw: &[Raw]) -> Option<[f64; 4]> {
let mut b: Option<[f64; 4]> = None;
let mut grow = |p: P| {
if p.0.is_finite() && p.1.is_finite() {
b = Some(match b {
None => [p.0, p.1, p.0, p.1],
Some([x0, y0, x1, y1]) => [x0.min(p.0), y0.min(p.1), x1.max(p.0), y1.max(p.1)],
});
}
};
for r in raw {
match r {
Raw::Path { ops, .. } | Raw::Pattern { ops, .. } => {
for op in ops {
match *op {
Op::M(p) | Op::L(p) => grow(p),
Op::C(a, c, d) => {
grow(a);
grow(c);
grow(d);
}
Op::Z => {}
}
}
}
Raw::Bitmap { rect, .. } => {
grow((rect[0], rect[1]));
grow((rect[0] + rect[2], rect[1] + rect[3]));
}
Raw::Text { text: t, .. } => grow((t.transform[4], t.transform[5])),
}
}
b
}
+264
View File
@@ -0,0 +1,264 @@
//! Placing a picture on a target (a page or a device): the fitted transform, paths as kurbo paths in
//! target units and bitmaps as target rectangles. The rasteriser and the PDF writer only paint the result.
use std::sync::Arc;
use kurbo::{Affine, BezPath, Rect, Shape};
use std::collections::HashMap;
use crate::{ClipPath, Item, LineStyle, Picture, Seg};
/// Largest bitmap side the renderers draw: within the PDF image limit and the rasteriser's 16-bit sides.
pub const MAX_BITMAP_SIDE: u32 = 30_000;
/// Placed coordinates past this (target units) are far off any page; such items are left out.
const MAX_COORD: f64 = 1e6;
/// Smallest placed bitmap side (target units), so the stretch onto it is never singular.
const MIN_BITMAP_SIDE: f64 = 1e-6;
/// Maximum aggregate path elements retained in placed output.
const MAX_PLACED_ELEMENTS: usize = 500_000;
/// The least of a source axis a crop keeps (0.5%). Files crop pictures to slivers (a header bar
/// that is 2.7% of a page-sized metafile). A frame of at most 4000 units a side then spans at most
/// 800,000 units for the whole source: what it covers stays inside `MAX_COORD` and is never left
/// out as far off the page.
const MIN_VISIBLE: f64 = 0.005;
/// The part of a picture's source that `crop` (left, top, right, bottom fractions of the source)
/// keeps, as fractions of the source: [left, top, width, height], bitmap or metafile alike. Each
/// side is taken as 0–1 (junk as 0). When the two sides of an axis leave less than 0.5% of it,
/// the left (top) side stands and 0.5% shows.
pub fn crop_window(crop: [f32; 4]) -> [f64; 4] {
let [l, t, r, b] = crop.map(|v| if v.is_finite() { f64::from(v.clamp(0.0, 1.0)) } else { 0.0 });
let axis = |lo: f64, hi: f64| {
let lo = lo.min(1.0 - MIN_VISIBLE);
(lo, (1.0 - lo - hi).max(MIN_VISIBLE))
};
let ((x, w), (y, h)) = (axis(l, r), axis(t, b));
[x, y, w, h]
}
/// One item of a placed picture, in target units. `region` is the device-context clip region and
/// `clips` the clip paths: an item shows inside the region and inside every clip path.
pub enum PlacedItem {
/// A filled and/or stroked path. The stroke width is in target units; 0 is a cosmetic hairline that
/// the renderer draws at its minimum width. `line` has its dash lengths in target units.
Path {
path: BezPath,
fill: Option<[u8; 4]>,
stroke: Option<([u8; 4], f64)>,
line: LineStyle,
even_odd: bool,
region: Option<Arc<BezPath>>,
clips: Vec<Arc<PlacedClip>>,
},
/// `Picture::items[index]`, a bitmap, stretched into `rect`. `transform` maps the unit square
/// (the whole bitmap) to target units: the same place when the bitmap is upright, and the
/// turned or skewed place when it is not.
Bitmap { index: usize, rect: Rect, transform: Affine, region: Option<Arc<BezPath>>, clips: Vec<Arc<PlacedClip>> },
/// `Picture::items[index]`, a pattern fill: `path` (target units) filled with the item's tile
/// repeated; `tile` maps tile pixels to target units.
Pattern { index: usize, path: BezPath, even_odd: bool, tile: Affine, region: Option<Arc<BezPath>>, clips: Vec<Arc<PlacedClip>> },
/// `Picture::items[index]`, a text string: `transform` maps its text space to target units;
/// `clip` (target units) bounds what it draws.
Text { index: usize, transform: Affine, clip: Option<Rect>, region: Option<Arc<BezPath>>, clips: Vec<Arc<PlacedClip>> },
}
/// A placed clip region: `Some(None)` is the default clip, `None` a region that does not meet the page.
type PlacedRegion = Option<Option<Arc<BezPath>>>;
/// A clip path in target units. Items under the same clip share it.
pub struct PlacedClip {
pub path: BezPath,
pub even_odd: bool,
}
impl Picture {
/// Whether the picture has a finite, positive size, so it can be fitted into a frame.
pub fn has_size(&self) -> bool {
self.width.is_finite() && self.height.is_finite() && self.width > 0.0 && self.height > 0.0
}
/// The transform that fits the picture into `r` with the crop fractions `crop` (left, top, right,
/// bottom) removed first: the visible part fills `r`. None for a degenerate size.
pub fn fit(&self, r: Rect, crop: [f32; 4]) -> Option<Affine> {
let [cl, ct, vw, vh] = crop_window(crop);
let (w, h) = (f64::from(self.width), f64::from(self.height));
let sx = r.width() / (w * vw);
let sy = r.height() / (h * vh);
(sx.is_finite() && sy.is_finite() && sx > 0.0 && sy > 0.0)
.then(|| Affine::translate((r.x0 - cl * w * sx, r.y0 - ct * h * sy)) * Affine::scale_non_uniform(sx, sy))
}
/// The items placed into `r` with `crop` applied. Items that are not finite or reach past ±1e6
/// target units are left out (they are far off any page), as are bitmaps with no area. Stroke
/// widths are scaled to target units (along the mean axis scale, so a non-uniform fit keeps
/// strokes even).
pub fn place(&self, r: Rect, crop: [f32; 4]) -> Vec<PlacedItem> {
let Some(tf) = self.fit(r, crop) else { return Vec::new() };
let c = tf.as_coeffs();
let k = (c[0].abs() + c[3].abs()) / 2.0;
let mut out = Vec::with_capacity(self.items.len());
let mut total_elements = 0usize;
let mut cached_region: Option<(*const [f32; 4], usize, PlacedRegion)> = None;
let mut get_region = |reg: &Option<Vec<[f32; 4]>>| -> PlacedRegion {
let (ptr, len) = match reg {
None => (std::ptr::null(), 0),
Some(v) => (v.as_ptr(), v.len()),
};
if let Some((c_ptr, c_len, ref res)) = cached_region
&& c_ptr == ptr
&& c_len == len
{
return res.clone();
}
let res = placed_region(&tf, reg);
cached_region = Some((ptr, len, res.clone()));
res
};
// Clip paths by address: the picture holds them for the whole call. None is a clip that is
// not finite or reaches past the coordinate limit; what it clips is left out.
let mut placed_clips: HashMap<*const ClipPath, Option<Arc<PlacedClip>>> = HashMap::new();
let mut clip_elements = 0usize;
let mut get_clips = |clips: &[Arc<ClipPath>]| -> Option<Vec<Arc<PlacedClip>>> {
clips
.iter()
.map(|c| {
placed_clips
.entry(Arc::as_ptr(c))
.or_insert_with(|| {
let path = tf * bez_path(&c.segs);
clip_elements = clip_elements.saturating_add(path.elements().len());
(clip_elements <= MAX_PLACED_ELEMENTS && in_range(path.bounding_box()))
.then(|| Arc::new(PlacedClip { path, even_odd: c.even_odd }))
})
.clone()
})
.collect()
};
for (index, item) in self.items.iter().enumerate() {
if total_elements >= MAX_PLACED_ELEMENTS {
break;
}
match item {
Item::Path { segs, fill, stroke, line, even_odd, region, clips } => {
let path = tf * bez_path(segs);
let Some(region) = get_region(region) else { continue };
let Some(clips) = get_clips(clips) else { continue };
let line = LineStyle { dash: line.dash.iter().map(|d| (f64::from(*d) * k) as f32).collect(), ..line.clone() };
if in_range(path.bounding_box()) && line.dash.iter().all(|d| d.is_finite()) {
total_elements = total_elements.saturating_add(path.elements().len());
let stroke = stroke.map(|(c, w)| (c, f64::from(w) * k));
out.push(PlacedItem::Path { path, fill: *fill, stroke, line, even_odd: *even_odd, region, clips });
}
}
Item::Pattern { segs, even_odd, tile: t, region, clips } => {
let path = tf * bez_path(segs);
let Some(region) = get_region(region) else { continue };
let Some(clips) = get_clips(clips) else { continue };
let tile = tf
* Affine::translate((f64::from(t.origin[0]), f64::from(t.origin[1])))
* Affine::scale_non_uniform(f64::from(t.cell[0]), f64::from(t.cell[1]));
let det = tile.determinant();
if in_range(path.bounding_box()) && det.is_finite() && det.abs() > 1e-12 {
total_elements = total_elements.saturating_add(path.elements().len());
out.push(PlacedItem::Pattern { index, path, even_odd: *even_odd, tile, region, clips });
}
}
Item::Text(t) => {
let transform = tf * Affine::new(t.transform);
let Some(region) = get_region(&t.region) else { continue };
let Some(clips) = get_clips(&t.clips) else { continue };
let clip =
t.clip.map(|[x0, y0, x1, y1]| tf.transform_rect_bbox(Rect::new(f64::from(x0), f64::from(y0), f64::from(x1), f64::from(y1))));
let c = transform.as_coeffs();
let origin = Rect::new(c[4], c[5], c[4], c[5]);
if in_range(origin) && c.iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD) && clip.is_none_or(in_range) {
out.push(PlacedItem::Text { index, transform, clip, region, clips });
}
}
Item::Bitmap { rect: [x, y, w, h], transform, region, clips, .. } => {
let (x0, y0) = (f64::from(*x), f64::from(*y));
let rect = tf.transform_rect_bbox(Rect::new(x0, y0, x0 + f64::from(*w), y0 + f64::from(*h)));
let Some(region) = get_region(region) else { continue };
let Some(clips) = get_clips(clips) else { continue };
let transform = tf * Affine::new(*transform);
let finite = transform.as_coeffs().iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD);
if in_range(rect) && finite && rect.width() > MIN_BITMAP_SIDE && rect.height() > MIN_BITMAP_SIDE {
out.push(PlacedItem::Bitmap { index, rect, transform, region, clips });
}
}
}
}
out
}
}
impl Item {
/// Takes a bitmap's pixels out of the item (its geometry stays) and returns them as (width, height,
/// RGBA) when a renderer can draw them: both sides in 1..=[`MAX_BITMAP_SIDE`] and exactly that many
/// pixels. None for paths and for bitmaps that cannot be drawn.
pub fn take_pixels(&mut self) -> Option<(u32, u32, Vec<u8>)> {
let Item::Bitmap { width, height, rgba, .. } = self else { return None };
let (w, h, px) = (*width, *height, std::mem::take(rgba));
let sides = (1..=MAX_BITMAP_SIDE).contains(&w) && (1..=MAX_BITMAP_SIDE).contains(&h);
(sides && px.len() == (w as usize).saturating_mul(h as usize).saturating_mul(4)).then_some((w, h, px))
}
}
/// The device-context clip in target units. `Some(None)` is the default clip. `None` means the
/// region does not meet the page, so the item draws nothing.
fn placed_region(tf: &Affine, region: &Option<Vec<[f32; 4]>>) -> PlacedRegion {
let Some(rects) = region else { return Some(None) };
let mut path = BezPath::new();
for [x0, y0, x1, y1] in rects {
let r = tf.transform_rect_bbox(Rect::new(f64::from(*x0), f64::from(*y0), f64::from(*x1), f64::from(*y1)));
if in_range(r) && r.width() > 1e-9 && r.height() > 1e-9 {
path.extend(r.to_path(0.1));
}
}
(!path.is_empty()).then_some(Some(Arc::new(path)))
}
/// Segments as a kurbo path. A segment after a close opens a new subpath at the last start point.
pub fn bez_path(segs: &[Seg]) -> BezPath {
let mut p = BezPath::new();
let mut start = (0.0, 0.0);
let mut open = false;
for s in segs {
match *s {
Seg::Move(x, y) => {
start = (f64::from(x), f64::from(y));
p.move_to(start);
open = true;
}
Seg::Line(x, y) => {
if !open {
p.move_to(start);
}
p.line_to((f64::from(x), f64::from(y)));
open = true;
}
Seg::Cubic(a, b, c, d, e, f) => {
if !open {
p.move_to(start);
}
p.curve_to((f64::from(a), f64::from(b)), (f64::from(c), f64::from(d)), (f64::from(e), f64::from(f)));
open = true;
}
Seg::Close => {
if open {
p.close_path();
}
open = false;
}
}
}
p
}
/// Whether a placed box is finite and within ±[`MAX_COORD`].
fn in_range(b: Rect) -> bool {
[b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite() && v.abs() < MAX_COORD)
}
+98
View File
@@ -0,0 +1,98 @@
//! The size of a metafile picture without playing its records.
use crate::bytes::Bytes;
use crate::{Error, emf, f, parse, wmf};
/// A placeable WMF header is 22 bytes, followed by the 9-word standard header.
const PLACEABLE_KEY: u32 = 0x9AC6_CDD7;
const PLACEABLE_LEN: usize = 22;
const DEFAULT_INCH: f64 = 1440.0;
/// The size of a WMF or EMF picture in points (device or logical units when the file states no
/// size), the same as `parse(bytes)` would put in the picture's `width` and `height`, or the error
/// `parse` would give.
///
/// An EMF and a placeable WMF carry their frame in the header, so the size is read from there
/// without decoding a bitmap or building a path. Any other file (a plain WMF, an EMF without a
/// usable frame) takes the full parse.
pub fn size(bytes: &[u8]) -> Result<(f32, f32), Error> {
if let Some(size) = header_size(Bytes(bytes)) {
return Ok(size);
}
let pic = parse(bytes)?;
Ok((pic.width, pic.height))
}
/// The size when the header decides it and the file has a first record (otherwise `parse` fails
/// with `NoRecords`, so the header alone is not enough). None sends the caller to the full parse.
pub(crate) fn header_size(b: Bytes) -> Option<(f32, f32)> {
if emf::is_emf(b) {
return emf_size(b);
}
if wmf::is_wmf(b) && b.u32(0) == Some(PLACEABLE_KEY) {
return placeable_wmf_size(b);
}
None
}
/// `rclFrame` mapped to points. The frame must pass the checks `build` applies to it.
fn emf_size(b: Bytes) -> Option<(f32, f32)> {
let hsize = b.u32(4)? as usize;
b.slice(8, 16)?;
let frame = emf::frame_from_header(b)?;
if !frame.rect.iter().all(|v| v.is_finite()) || frame.rect[2] <= 0.0 || frame.rect[3] <= 0.0 {
return None;
}
// The records start after the header; the parse needs one that is not the end record.
let pos = hsize.max(88);
let (typ, len) = (b.u32(pos)?, b.u32(pos.checked_add(4)?)? as usize);
if len < 8 || !len.is_multiple_of(4) || b.slice(pos, len).is_none() || typ == 14 {
return None;
}
let (w, h) = frame.size?;
Some((f(w).unwrap_or(0.0), f(h).unwrap_or(0.0)))
}
/// The placeable header's size in points, for a file whose records leave the target mapping alone
/// (see [`sets_the_mapping`]); the full parse decides the others.
fn placeable_wmf_size(b: Bytes) -> Option<(f32, f32)> {
let [l, t, r, bt] = [6, 8, 10, 12].map(|o| b.i16(o).map(f64::from));
let (l, t, r, bt) = (l?, t?, r?, bt?);
let inch = b.u16(14).filter(|v| *v > 0).map_or(DEFAULT_INCH, f64::from);
let (w, h) = (r - l, bt - t);
if w <= 0.0 || h <= 0.0 {
return None;
}
let hsize = b.u16(PLACEABLE_LEN + 2)? as usize;
if hsize < 9 {
return None;
}
let pos = PLACEABLE_LEN.checked_add(hsize.checked_mul(2)?)?;
let (words, func) = (b.u32(pos)? as usize, b.u16(pos.checked_add(4)?)?);
let len = words.checked_mul(2).filter(|l| *l >= 6)?;
if func == 0 || b.slice(pos, len).is_none() {
return None;
}
if sets_the_mapping(b, pos) {
return None;
}
Some((f(w * 72.0 / inch)?, f(h * 72.0 / inch)?))
}
/// Whether a record from `pos` on is SetMapMode (0x0103) or SetViewportExt (0x020E). Those can change
/// the frame the full parse ends with (a zero-size viewport leaves none), so the header size is not
/// enough. The records are only walked, not played, with the same stops as `wmf::parse`.
fn sets_the_mapping(b: Bytes, mut pos: usize) -> bool {
while let (Some(words), Some(func)) = (b.u32(pos), pos.checked_add(4).and_then(|o| b.u16(o))) {
let Some(len) = (words as usize).checked_mul(2).filter(|l| *l >= 6) else { return false };
if func == 0 || b.slice(pos, len).is_none() {
return false;
}
if matches!(func, 0x0103 | 0x020E) {
return true;
}
let Some(next) = pos.checked_add(len) else { return false };
pos = next;
}
false
}
File diff suppressed because it is too large. Load diff
+219
View File
@@ -0,0 +1,219 @@
//! One test for each cap that the other test modules do not already hold to its value.
use super::*;
use crate::emfplus;
#[test]
fn strings_are_cut_at_the_character_cap() {
let cap = crate::text::MAX_CHARS;
// EXTTEXTOUTW: the count says more than the cap; the string holds that many.
let units = vec![0x41u16; cap + 100];
let p = parse(&emf(&[etext(true, 0, 0, &units, 0, &[])], [0, 0, 200, 100], [0, 0, 5000, 2500])).unwrap();
assert_eq!(texts(&p)[0].text.chars().count(), cap);
// META_EXTTEXTOUT and META_TEXTOUT.
let bytes = vec![b'a'; cap + 100];
let p = wmf200(&[wtext(0, 0, &bytes, 0, None, &[])]);
assert_eq!(texts(&p)[0].text.chars().count(), cap);
let mut v = h16((cap + 100) as i32 as i16 as i32);
v.extend(&bytes);
v.extend(cat(vec![h16(0), h16(0)]));
let p = wmf200(&[wrec(0x0521, &v)]);
assert!(texts(&p).iter().all(|t| t.text.chars().count() <= cap));
}
#[test]
fn emfplus_strings_are_cut_at_their_character_cap() {
let cap = emfplus::MAX_CHARS;
let font = prec(0x4008, 0x0600 | 3, &font_obj("Calibri", 12.0, 2, 0));
let long: String = "a".repeat(cap + 50);
let s = prec(0x401C, 0x8000 | 3, &cat(vec![h32(0xFF00_0000), h32(9), h32((cap + 50) as u32), f32s(&[0.0, 0.0, 0.0, 0.0]), u16s(&long)]));
let p = plus_pic(vec![font, s]);
let total: usize = texts(&p).iter().map(|t| t.text.chars().count()).sum();
assert!(total > 0 && total <= cap, "{total}");
}
#[test]
fn a_clip_region_keeps_at_most_its_rectangle_cap() {
let cap = crate::clip::MAX_CLIP_RECTS;
// EXTSELECTCLIPRGN (RGN_COPY) with twice the cap of separate rectangles in its RGNDATA.
let n = cap * 2;
let mut data = cat(vec![h32(32), h32(1), h32(n as u32), h32((n * 16) as u32), vec![0; 16]]);
for i in 0..n as i32 {
data.extend(cat(vec![e32(i * 10), e32(0), e32(i * 10 + 5), e32(5)]));
}
let rgn = erec(75, &cat(vec![h32(data.len() as u32), h32(5), data]));
let draw = erec(43, &cat(vec![e32(0), e32(0), e32(10_000), e32(10)]));
let p = parse(&emf(&[rgn, draw], [0; 4], [0; 4])).unwrap();
let Item::Path { region: Some(r), .. } = &p.items[0] else { panic!("a clipped path") };
assert_eq!(r.len(), cap);
}
#[test]
fn placing_stops_at_the_element_cap() {
let segs: Vec<Seg> = std::iter::once(Seg::Move(0.0, 0.0)).chain((0..100_000).map(|i| Seg::Line((i % 7) as f32, (i % 5) as f32))).collect();
let path = || Item::Path {
segs: segs.clone(),
fill: Some([0, 0, 0, 255]),
stroke: None,
line: crate::LineStyle::default(),
even_odd: false,
region: None,
clips: Vec::new(),
};
let pic = crate::Picture { width: 10.0, height: 10.0, items: (0..8).map(|_| path()).collect(), skipped_records: 0 };
let placed = pic.place(kurbo::Rect::new(0.0, 0.0, 10.0, 10.0), [0.0; 4]);
// Five paths of 100,001 elements reach the 500,000 cap; the rest are left out.
assert_eq!(placed.len(), 5);
}
#[test]
fn a_pattern_tile_over_the_side_cap_is_no_brush() {
let side = crate::canvas::MAX_TILE_SIDE;
let tile = |w: usize, h: usize| crate::canvas::Brush::pattern(Rgba { w, h, px: vec![0; w * h * 4] }, false);
assert!(tile(side, side).is_some());
assert!(tile(side + 1, 1).is_none());
assert!(tile(1, side + 1).is_none());
}
#[test]
fn a_dib_palette_holds_at_most_256_entries() {
// 24 bpp with a colour table of 1000 entries claimed: 256 are skipped before the bits.
let mut info = info_header(1, 1, 24, 4);
info[32..36].copy_from_slice(&1000u32.to_le_bytes());
assert_eq!(crate::dib::info_len(crate::bytes::Bytes(&info)), Some(40 + 256 * 4));
}
/// `inner` as an EMF+ metafile image object drawn into (0, 0, 40, 20), in a file of its own.
fn wrap(inner: Vec<u8>) -> Vec<u8> {
let obj = cat(vec![h32(0xDBC0_1002), h32(2), h32(3), h32(inner.len() as u32), inner]);
emf(&[plus_comment(&[plus_header(false), prec(0x4008, 0x0500 | 3, &obj), draw_image(3, [0.0, 0.0, 40.0, 20.0])])], [0; 4], [0; 4])
}
#[test]
fn metafiles_nest_only_so_deep() {
let nested = |levels: usize| {
let mut file = emf(&[erec(43, &cat(vec![e32(0), e32(0), e32(10), e32(10)]))], [0; 4], [0; 4]);
for _ in 0..levels {
file = wrap(file);
}
parse(&file).map(|p| p.items.len()).unwrap_or(0)
};
assert_eq!(nested(emfplus::MAX_NESTING), 1);
assert_eq!(nested(emfplus::MAX_NESTING + 1), 0, "the innermost picture is not read");
}
#[test]
fn emfplus_object_ids_past_the_table_are_ignored() {
let draw = |id: u16| {
let pen = prec(0x4008, 0x0200 | id, &solid_pen(0xFF00_FF00, 2.0));
let lines = prec(0x400D, id, &cat(vec![h32(2), f32s(&[1.0, 1.0, 4.0, 2.0])]));
plus_pic(vec![pen, lines]).items.len()
};
assert_eq!(draw(emfplus::MAX_OBJECTS as u16 - 1), 1);
assert_eq!(draw(emfplus::MAX_OBJECTS as u16), 0);
}
#[test]
fn emfplus_saves_past_the_stack_cap_are_not_kept() {
// Saves 0..=cap, a move, then a restore of the last save: it was not kept, so the move stays.
let cap = emfplus::MAX_STACK as u32;
let fill = || prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[0.0, 0.0, 10.0, 10.0])]));
let run = |saves: u32| {
let mut recs: Vec<Vec<u8>> = (0..saves).map(|i| prec(0x4025, 0, &h32(i))).collect();
recs.extend([fill(), prec(0x402D, 0, &f32s(&[100.0, 0.0])), prec(0x4026, 0, &h32(saves - 1)), fill()]);
plus_pic(recs).width
};
assert_eq!(run(cap), 10.0, "the last save was kept: the restore undoes the move");
assert_eq!(run(cap + 1), 110.0, "one save too many: nothing to restore");
}
#[test]
fn emfplus_point_lists_are_cut_at_the_point_cap() {
let n = emfplus::MAX_POINTS + 10;
let pts: Vec<f32> = (0..n).flat_map(|i| [(i % 100) as f32, (i % 37) as f32]).collect();
let pen = prec(0x4008, 0x0200 | 3, &solid_pen(0xFF00_FF00, 1.0));
let lines = prec(0x400D, 3, &cat(vec![h32(n as u32), f32s(&pts)]));
let p = plus_pic(vec![pen, lines]);
let (segs, ..) = path_segs(&p.items[0]);
assert_eq!(segs.len(), emfplus::MAX_POINTS);
}
#[test]
fn a_continued_emfplus_object_reserves_nothing_for_the_size_it_claims() {
// 4 GB claimed (cut to the cap), a few bytes sent: the object never completes and no memory
// is set aside for it.
let first = prec(0x4008, 0x8000 | 0x0500 | 1, &cat(vec![h32(u32::MAX), vec![1, 2, 3, 4]]));
let p = plus_pic(vec![first.clone(), first, draw_image(1, [0.0, 0.0, 4.0, 4.0])]);
assert!(p.items.is_empty());
}
#[test]
fn emfplus_gradients_keep_at_most_their_stop_cap() {
// Preset colours (brush flag 4) with far more stops claimed than the data holds: the count is
// cut to the cap first, so the read is bounded and the short data makes no brush.
let cap = emfplus::MAX_STOPS as u32;
let brush = |n: u32, held: u32| {
let mut b =
cat(vec![h32(0xDBC0_1002), h32(4), h32(4), h32(0), f32s(&[0.0, 0.0, 100.0, 10.0]), h32(0xFFFF_0000), h32(0xFF00_00FF), h32(0), h32(0)]);
b.extend(h32(n));
b.extend(f32s(&(0..held).map(|i| i as f32 / held as f32).collect::<Vec<_>>()));
b.extend((0..held).flat_map(|_| h32(0xFF00_FF00)));
b
};
let fill = || prec(0x400A, 0, &cat(vec![h32(5), h32(1), f32s(&[0.0, 0.0, 100.0, 10.0])]));
let drawn = |n: u32, held: u32| plus_pic(vec![prec(0x4008, 0x0100 | 5, &brush(n, held)), fill()]).items.len();
assert_eq!(drawn(cap, cap), 1);
assert_eq!(drawn(u32::MAX, cap), 1, "a count past the cap reads the cap's worth");
assert_eq!(drawn(u32::MAX, 4), 0, "and no more than the record holds");
}
#[test]
fn emfplus_regions_nest_only_so_deep_and_only_so_many_nodes() {
let rect = || cat(vec![h32(0x1000_0000), f32s(&[0.0, 0.0, 30.0, 30.0])]);
// A chain of intersections `depth` deep: and(and(and(..., rect), rect), rect).
let chain = |depth: usize| {
let mut body = Vec::new();
for _ in 0..depth {
body.extend(h32(1));
}
for _ in 0..=depth {
body.extend(rect());
}
cat(vec![h32(0xDBC0_1002), h32(depth as u32), body])
};
let clipped = |region: Vec<u8>| {
let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[0.0, 0.0, 100.0, 100.0])]));
plus_pic(vec![prec(0x4008, 0x0400 | 6, &region), prec(0x4034, 6, &[]), fill]).width
};
assert_eq!(clipped(chain(emfplus::MAX_REGION_DEPTH)), 30.0, "a region at the depth cap clips");
assert_eq!(clipped(chain(emfplus::MAX_REGION_DEPTH + 1)), 100.0, "one deeper is no region: the fill is whole");
// A balanced tree of intersections stays shallow and has 2^(levels + 1) - 1 nodes.
fn tree(levels: usize, out: &mut Vec<u8>) {
if levels == 0 {
out.extend(cat(vec![h32(0x1000_0000), f32s(&[0.0, 0.0, 30.0, 30.0])]));
} else {
out.extend(h32(1));
tree(levels - 1, out);
tree(levels - 1, out);
}
}
let balanced = |levels: usize| {
let mut body = Vec::new();
tree(levels, &mut body);
cat(vec![h32(0xDBC0_1002), h32(0), body])
};
let levels = (emfplus::MAX_REGION_WORK + 1).ilog2() as usize;
assert_eq!(clipped(balanced(levels - 1)), 30.0, "fewer nodes than the cap");
assert_eq!(clipped(balanced(levels)), 100.0, "more nodes than the cap is no region");
}
#[test]
fn masking_a_bitmap_to_a_clip_stops_at_the_work_cap() {
// A triangle clip over a bitmap: three corners times the pixels inside the clip's box.
let tri = [(0.0, 0.0), (1000.0, 0.0), (0.0, 1000.0)];
let side = |n: usize| crate::cliprect::crop([0.0, 0.0, 1000.0, 1000.0], Rgba { w: n, h: n, px: vec![255; n * n * 4] }, &tri).is_some();
let cap = crate::cliprect::MAX_MASK_WORK;
let fits = ((cap / 3) as f64).sqrt() as usize;
assert!(side(fits));
assert!(!side(fits + 2));
}
+544
View File
@@ -0,0 +1,544 @@
//! Line styles, clip paths and the device-context records behind them, the DIB forms and the
//! header fields that came with VectorCraft's reader.
use std::sync::Arc;
use super::*;
use crate::canvas::{MAX_CLIP_PATHS, MAX_DASH_ENTRIES};
use crate::{Cap, ClipPath, Join, LineStyle, PlacedItem};
fn line_of(item: &Item) -> &LineStyle {
match item {
Item::Path { line, .. } => line,
_ => panic!("expected a path"),
}
}
fn clips_of(item: &Item) -> &Vec<Arc<ClipPath>> {
match item {
Item::Path { clips, .. } | Item::Bitmap { clips, .. } | Item::Pattern { clips, .. } => clips,
Item::Text(t) => &t.clips,
}
}
fn rect(l: i32, t: i32, r: i32, b: i32) -> Vec<u8> {
erec(43, &cat(vec![e32(l), e32(t), e32(r), e32(b)]))
}
fn move_to(x: i32, y: i32) -> Vec<u8> {
erec(27, &cat(vec![e32(x), e32(y)]))
}
fn line_to(x: i32, y: i32) -> Vec<u8> {
erec(54, &cat(vec![e32(x), e32(y)]))
}
/// EMR_EXTCREATEPEN in slot 1 with `entries` style entries.
fn ext_pen(style: u32, width: u32, entries: &[u32]) -> Vec<u8> {
let mut fields = vec![(8, 1), (28, style), (32, width), (36, 0), (40, 0x0000_00FF), (48, entries.len() as u32)];
fields.extend(entries.iter().enumerate().map(|(i, e)| (52 + i * 4, *e)));
erec_at(95, 52 + entries.len() * 4, &fields, &[])
}
/// A rectangle path bracket selected as the clip in `mode`.
fn clip_to_rect(l: i32, t: i32, r: i32, b: i32, mode: u32) -> Vec<Vec<u8>> {
vec![erec(59, &[]), rect(l, t, r, b), erec(60, &[]), erec(67, &h32(mode))]
}
fn parse_emf(recs: &[Vec<u8>]) -> crate::Picture {
parse(&emf(recs, [0; 4], [0; 4])).unwrap()
}
const PS_GEOMETRIC: u32 = 0x0001_0000;
#[test]
fn emf_geometric_pen_keeps_its_cap_join_miter_limit_and_dashes() {
// PS_USERSTYLE with flat caps and miter joins, 4 wide, dashes of 8 and gaps of 4.
let pen = ext_pen(PS_GEOMETRIC | 7 | 0x200 | 0x2000, 4, &[8, 4]);
let recs = [pen, erec(37, &h32(1)), erec(58, &h32(3)), move_to(0, 0), line_to(100, 0)];
let p = parse_emf(&recs);
let (_, _, stroke, _) = path_segs(&p.items[0]);
assert_eq!(stroke, Some(([255, 0, 0, 255], 4.0)));
assert_eq!(line_of(&p.items[0]), &LineStyle { cap: Cap::Flat, join: Join::Miter, miter_limit: 3.0, dash: vec![8.0, 4.0] });
}
#[test]
fn dash_lengths_follow_the_mapping_like_the_width() {
// A world transform of 2: the width and the dashes double.
let xf: Vec<u8> = [2.0f32, 0.0, 0.0, 2.0, 0.0, 0.0].iter().flat_map(|v| v.to_le_bytes()).collect();
let recs = [erec(35, &xf), ext_pen(PS_GEOMETRIC | 7, 4, &[8, 4]), erec(37, &h32(1)), move_to(0, 0), line_to(100, 0)];
let p = parse_emf(&recs);
let (_, _, stroke, _) = path_segs(&p.items[0]);
assert_eq!(stroke, Some(([255, 0, 0, 255], 8.0)));
assert_eq!(line_of(&p.items[0]).dash, vec![16.0, 8.0]);
}
#[test]
fn pen_styles_become_dash_patterns_of_the_pen_width() {
// CREATEPEN: slot, style, width (x, y), colour.
let pen = |style: u32, width: u32| erec_at(38, 28, &[(8, 1), (12, style), (16, width)], &[]);
let draw = |style: u32, width: u32| {
let p = parse_emf(&[pen(style, width), erec(37, &h32(1)), move_to(0, 0), line_to(100, 0)]);
line_of(&p.items[0]).clone()
};
assert_eq!(draw(0, 2), LineStyle::default(), "PS_SOLID: round caps and joins, miter limit 10, no dashes");
assert_eq!(draw(1, 2).dash, vec![6.0, 2.0], "PS_DASH");
assert_eq!(draw(2, 2).dash, vec![2.0, 2.0], "PS_DOT");
assert_eq!(draw(3, 2).dash, vec![6.0, 2.0, 2.0, 2.0], "PS_DASHDOT");
assert_eq!(draw(4, 2).dash, vec![6.0, 2.0, 2.0, 2.0, 2.0, 2.0], "PS_DASHDOTDOT");
// A cosmetic pen's dashes are in device pixels.
assert_eq!(draw(2, 0).dash, vec![1.0, 1.0]);
// PS_INSIDEFRAME and unknown styles are solid.
assert!(draw(6, 2).dash.is_empty());
}
#[test]
fn a_dash_pattern_that_draws_nothing_is_a_solid_line() {
let draw = |entries: &[u32]| {
let p = parse_emf(&[ext_pen(PS_GEOMETRIC | 7, 2, entries), erec(37, &h32(1)), move_to(0, 0), line_to(100, 0)]);
line_of(&p.items[0]).dash.clone()
};
assert!(draw(&[0, 0]).is_empty());
assert!(draw(&[]).is_empty());
assert_eq!(draw(&[0, 5]), vec![0.0, 5.0], "a zero dash with round caps draws dots");
}
#[test]
fn a_pen_keeps_at_most_sixteen_style_entries() {
let entries: Vec<u32> = (1..=100).collect();
let p = parse_emf(&[ext_pen(PS_GEOMETRIC | 7, 1, &entries), erec(37, &h32(1)), move_to(0, 0), line_to(100, 0)]);
assert_eq!(line_of(&p.items[0]).dash.len(), MAX_DASH_ENTRIES);
// A count that runs past the record keeps the entries the record holds.
let mut rec = ext_pen(PS_GEOMETRIC | 7, 1, &[3, 2]);
rec[48..52].copy_from_slice(&1000u32.to_le_bytes());
let p = parse_emf(&[rec, erec(37, &h32(1)), move_to(0, 0), line_to(100, 0)]);
assert_eq!(line_of(&p.items[0]).dash, vec![3.0, 2.0]);
}
#[test]
fn wmf_pen_style_word_carries_cap_and_join() {
// CREATEPENINDIRECT: style (PS_SOLID, flat caps, bevel joins), width (x, y), colour.
let pen = wrec(0x02FA, &cat(vec![h16(0x1200), h16(3), h16(0), h32(0)]));
let recs = [pen, wrec(0x012D, &h16(0)), wrec(0x0214, &cat(vec![h16(0), h16(0)])), wrec(0x0213, &cat(vec![h16(0), h16(50)]))];
let p = parse(&wmf(&recs, None)).unwrap();
let line = line_of(&p.items[0]);
assert_eq!((line.cap, line.join), (Cap::Flat, Join::Bevel));
}
#[test]
fn restore_dc_brings_back_the_miter_limit() {
let pen = ext_pen(PS_GEOMETRIC | 0x2000, 2, &[]);
let recs =
[pen, erec(37, &h32(1)), erec(33, &[]), erec(58, &h32(4)), move_to(0, 0), line_to(9, 0), erec(34, &e32(-1)), move_to(0, 5), line_to(9, 5)];
let p = parse_emf(&recs);
assert_eq!(line_of(&p.items[0]).miter_limit, 4.0);
assert_eq!(line_of(&p.items[1]).miter_limit, 10.0);
// A limit below 1 is invalid: the limit stays.
let p = parse_emf(&[ext_pen(PS_GEOMETRIC | 0x2000, 2, &[]), erec(37, &h32(1)), erec(58, &h32(0)), move_to(0, 0), line_to(9, 0)]);
assert_eq!(line_of(&p.items[0]).miter_limit, 10.0);
}
#[test]
fn select_clip_path_clips_what_follows_and_items_share_the_path() {
let mut recs = vec![rect(0, 0, 100, 100)];
recs.extend(clip_to_rect(10, 10, 60, 60, 1));
recs.extend([rect(0, 0, 50, 50), rect(20, 20, 80, 80)]);
let p = parse_emf(&recs);
assert_eq!(p.items.len(), 3);
assert!(clips_of(&p.items[0]).is_empty(), "drawn before the clip");
let (a, b) = (clips_of(&p.items[1]), clips_of(&p.items[2]));
assert_eq!((a.len(), b.len()), (1, 1));
assert!(Arc::ptr_eq(&a[0], &b[0]), "one clip, shared");
assert_eq!(a[0].segs, vec![Seg::Move(10.0, 10.0), Seg::Line(60.0, 10.0), Seg::Line(60.0, 60.0), Seg::Line(10.0, 60.0), Seg::Close]);
assert!(a[0].even_odd, "the path takes the fill mode in force (ALTERNATE)");
// The clip path is not painted, and the bracket is empty afterwards: FILLPATH draws nothing.
let mut recs = clip_to_rect(0, 0, 10, 10, 1);
recs.push(erec(62, &[0; 16]));
assert!(parse_emf(&recs).items.is_empty());
}
#[test]
fn clip_paths_nest_with_and_replace_with_copy_and_end_with_restore_dc() {
let mut recs = vec![erec(33, &[])];
recs.extend(clip_to_rect(0, 0, 90, 90, 1));
recs.extend(clip_to_rect(10, 10, 80, 80, 1));
recs.push(rect(0, 0, 100, 100));
recs.extend(clip_to_rect(20, 20, 70, 70, 5));
recs.push(rect(0, 0, 100, 100));
recs.push(erec(34, &e32(-1)));
recs.push(rect(0, 0, 100, 100));
let p = parse_emf(&recs);
let n: Vec<usize> = p.items.iter().map(|i| clips_of(i).len()).collect();
assert_eq!(n, vec![2, 1, 0], "RGN_AND nests, RGN_COPY replaces, RESTOREDC removes");
let outer = &clips_of(&p.items[0])[0];
assert_eq!(outer.segs.first(), Some(&Seg::Move(0.0, 0.0)), "outermost first");
assert_eq!(clips_of(&p.items[1])[0].segs.first(), Some(&Seg::Move(20.0, 20.0)));
}
#[test]
fn clip_modes_that_add_or_cut_leave_the_clip_and_a_region_copy_drops_clip_paths() {
for mode in [2, 3, 4, 0, 99] {
let mut recs = clip_to_rect(0, 0, 10, 10, mode);
recs.push(rect(0, 0, 100, 100));
let p = parse_emf(&recs);
assert!(clips_of(&p.items[0]).is_empty(), "mode {mode}");
}
// EXTSELECTCLIPRGN with no region data selects the default clip: no region, no clip paths.
let mut recs = clip_to_rect(0, 0, 10, 10, 1);
recs.push(erec(75, &cat(vec![h32(0), h32(5)])));
recs.push(rect(0, 0, 100, 100));
let p = parse_emf(&recs);
assert!(clips_of(&p.items[0]).is_empty());
}
#[test]
fn an_empty_clip_path_clips_everything_away() {
let recs = [erec(59, &[]), erec(60, &[]), erec(67, &h32(1)), rect(0, 0, 100, 100)];
assert!(parse_emf(&recs).items.is_empty());
}
#[test]
fn one_clip_path_past_the_cap_stops_the_parse() {
let nested = |n: usize| {
let mut recs = Vec::new();
for _ in 0..n {
recs.extend(clip_to_rect(0, 0, 50, 50, 1));
}
recs.push(rect(0, 0, 100, 100));
parse_emf(&recs)
};
let p = nested(MAX_CLIP_PATHS);
assert_eq!(clips_of(&p.items[0]).len(), MAX_CLIP_PATHS);
assert!(nested(MAX_CLIP_PATHS + 1).items.is_empty(), "what follows can no longer be clipped as the file asks");
}
#[test]
fn clip_path_ops_count_against_the_byte_budget() {
// Each clip path is charged once, when it is selected, so RGN_COPY in a loop cannot hold more
// than the budget (the earlier paths stay alive while items drawn under them do).
let mut cv = Canvas::new();
let big = crate::MAX_PATH_OPS;
let mut selected = 0;
while !cv.full() && selected < 100 {
cv.begin_path();
cv.line_chain(&vec![(1.0, 1.0); big - 1], false, false);
cv.end_path();
cv.select_clip_path(5);
selected += 1;
}
assert!(cv.full(), "the budget must stop a file that keeps selecting large clip paths");
assert!(selected * big * std::mem::size_of::<crate::geom::Op>() <= 2 * MAX_BYTES);
}
#[test]
fn abort_path_discards_the_bracket() {
let recs = [erec(59, &[]), rect(0, 0, 10, 10), erec(68, &[]), erec(62, &[0; 16]), rect(0, 0, 5, 5)];
let p = parse_emf(&recs);
assert_eq!(p.items.len(), 1, "only the rectangle drawn after the bracket was aborted");
assert_eq!((p.width, p.height), (5.0, 5.0));
}
#[test]
fn arc_direction_picks_the_other_part_of_the_ellipse() {
// From the right-most point to the top-most one: counter-clockwise is the quarter between
// them, clockwise the three quarters round the other way.
let arc = erec(45, &cat(vec![e32(0), e32(0), e32(100), e32(100), e32(100), e32(50), e32(50), e32(0)]));
let ccw = parse_emf(std::slice::from_ref(&arc));
assert!(close(ccw.width, 50.0) && close(ccw.height, 50.0), "{} x {}", ccw.width, ccw.height);
let cw = parse_emf(&[erec(57, &h32(2)), arc.clone()]);
assert!(close(cw.width, 100.0) && close(cw.height, 100.0), "{} x {}", cw.width, cw.height);
// RESTOREDC brings the direction back.
let p = parse_emf(&[erec(33, &[]), erec(57, &h32(2)), erec(34, &e32(-1)), arc]);
assert!(close(p.width, 50.0));
}
#[test]
fn scale_viewport_and_window_ext_change_the_mapping() {
let setup = [erec(17, &h32(8)), erec(9, &cat(vec![e32(100), e32(100)])), erec(11, &cat(vec![e32(100), e32(100)]))];
let scaled = |typ: u32, v: [i32; 4]| {
let mut recs = setup.to_vec();
recs.push(erec(typ, &cat(v.iter().map(|x| e32(*x)).collect())));
recs.push(rect(0, 0, 10, 10));
let p = parse_emf(&recs);
(p.width, p.height)
};
assert_eq!(scaled(31, [2, 1, 3, 1]), (20.0, 30.0), "SCALEVIEWPORTEXTEX");
assert_eq!(scaled(32, [2, 1, 5, 1]), (5.0, 2.0), "SCALEWINDOWEXTEX");
assert_eq!(scaled(31, [2, 0, 3, 1]), (10.0, 10.0), "a zero denominator leaves the extent");
}
#[test]
fn stroke_and_fill_path_closes_open_figures_but_stroke_path_does_not() {
let figure = |paint: u32| {
let recs =
[erec(59, &[]), move_to(0, 0), line_to(10, 0), line_to(10, 10), move_to(20, 0), line_to(30, 0), erec(60, &[]), erec(paint, &[0; 16])];
let p = parse_emf(&recs);
path_segs(&p.items[0]).0.clone()
};
let open = vec![Seg::Move(0.0, 0.0), Seg::Line(10.0, 0.0), Seg::Line(10.0, 10.0), Seg::Move(20.0, 0.0), Seg::Line(30.0, 0.0)];
assert_eq!(figure(64), open, "STROKEPATH");
assert_eq!(figure(62), open, "FILLPATH: a fill closes by itself");
let closed =
vec![Seg::Move(0.0, 0.0), Seg::Line(10.0, 0.0), Seg::Line(10.0, 10.0), Seg::Close, Seg::Move(20.0, 0.0), Seg::Line(30.0, 0.0), Seg::Close];
assert_eq!(figure(63), closed, "STROKEANDFILLPATH");
}
#[test]
fn close_figure_moves_the_current_position_to_the_figures_start() {
let recs = [erec(59, &[]), move_to(0, 0), line_to(10, 0), line_to(10, 10), erec(61, &[]), erec(60, &[]), erec(64, &[0; 16]), line_to(5, 20)];
let p = parse_emf(&recs);
assert_eq!(path_segs(&p.items[1]).0, &vec![Seg::Move(0.0, 0.0), Seg::Line(5.0, 20.0)]);
}
#[test]
fn records_of_unknown_types_are_counted_and_known_ones_are_not() {
// 200 is no EMF record; 21 (SETSTRETCHBLTMODE) and 70 (a plain comment) change nothing kept.
let recs = [rect(0, 0, 10, 10), erec(200, &[0; 8]), erec(21, &h32(1)), erec(70, &cat(vec![h32(4), h32(0)])), erec(201, &[])];
assert_eq!(parse_emf(&recs).skipped_records, 2);
let rect16 = wrec(0x041B, &cat(vec![h16(10), h16(10), h16(0), h16(0)]));
let recs = [rect16, wrec(0x0999, &[0; 4]), wrec(0x0107, &h16(1))];
assert_eq!(parse(&wmf(&recs, None)).unwrap().skipped_records, 1);
}
#[test]
fn emf_header_micrometres_set_the_device_resolution() {
// The 88-byte header says 4 pixels a millimetre; the extension says 5 (200 mm for 1000 pixels).
// A 25.4 mm frame is then 127 pixels wide, and a 127-pixel rectangle fills its 72 points.
let short = emf(&[rect(0, 0, 127, 10)], [0, 0, 127, 10], [0, 0, 2540, 1270]);
let mut file = short[..88].to_vec();
file.extend([0u8; 12]);
file.extend(200_000u32.to_le_bytes());
file.extend(100_000u32.to_le_bytes());
file.extend(&short[88..]);
file[4..8].copy_from_slice(&108u32.to_le_bytes());
let right = |bytes: &[u8]| {
let p = parse(bytes).unwrap();
assert!(close(p.width, 72.0));
path_segs(&p.items[0]).0.iter().filter_map(|s| if let Seg::Line(x, _) = s { Some(*x) } else { None }).fold(0.0, f32::max)
};
assert!(close(right(&file), 72.0), "{}", right(&file));
assert!(close(right(&short), 90.0), "without the extension the millimetres stand");
// A zero in the extension falls back to the millimetres.
file[100..104].copy_from_slice(&0u32.to_le_bytes());
assert!(close(right(&file), 90.0));
assert_eq!(crate::size(&file).unwrap(), (parse(&file).unwrap().width, parse(&file).unwrap().height));
}
/// STRETCHDIB of a 2 x 2 source into 20 x 10 with `dib` as the packed bitmap.
fn stretch_dib(dib: Vec<u8>) -> Vec<u8> {
wmf(&[wrec(0x0F43, &cat(vec![h32(0x00CC_0020), h16(0), h16(2), h16(2), h16(0), h16(0), h16(10), h16(20), h16(0), h16(0), dib]))], None)
}
#[test]
fn core_header_dibs_decode() {
// BITMAPCOREHEADER: size 12, 16-bit width and height, planes, bits per pixel; rows bottom-up.
let core = |bpp: u16| cat(vec![h32(12), h16(2), h16(2), h16(1), h16(i32::from(bpp))]);
let mut dib = core(24);
dib.extend(dib24_bottom_up().get(40..).unwrap_or_default());
let p = parse(&stretch_dib(dib)).unwrap();
let Item::Bitmap { width, height, rgba, .. } = &p.items[0] else { panic!("expected a bitmap") };
assert_eq!((*width, *height), (2, 2));
assert_eq!(rgba.as_slice(), DIB24_RGBA);
// 1 bpp: two palette entries of three bytes (blue, green, red), then padded rows.
let mut dib = core(1);
dib.extend([10, 20, 30, 40, 50, 60]);
dib.extend([0b0100_0000, 0, 0, 0, 0b1000_0000, 0, 0, 0]);
let p = parse(&stretch_dib(dib)).unwrap();
let Item::Bitmap { rgba, .. } = &p.items[0] else { panic!("expected a bitmap") };
let (a, b) = ([30, 20, 10, 255], [60, 50, 40, 255]);
assert_eq!(rgba.as_slice(), [b, a, a, b].concat(), "the file's second row is the top one");
// 16 bpp is not a core-header depth.
assert!(parse(&stretch_dib(core(16))).unwrap().items.is_empty());
}
fn png(w: u32, h: u32, px: [u8; 4]) -> Vec<u8> {
let mut out = Vec::new();
let img = image::RgbaImage::from_pixel(w, h, image::Rgba(px));
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
out
}
/// EMR_STRETCHDIBITS of the whole `w` x `h` source into (0, 0, 30, 10) with a header of
/// `compression` and `bits`.
fn stretch_dibits(w: i32, h: i32, compression: u32, bits: &[u8]) -> Vec<u8> {
let mut info = info_header(w, h, 32, bits.len() as u32);
info[16..20].copy_from_slice(&compression.to_le_bytes());
let mut payload = vec![0u8; 16];
let fields = [0, 0, 0, 0, w, h, 80, 40, 120, bits.len() as i32, 0, 0x00CC_0020, 30, 10];
payload.extend(cat(fields.iter().map(|v| e32(*v)).collect()));
payload.extend(info);
payload.extend(bits);
erec(81, &payload)
}
#[test]
fn png_and_jpeg_dibs_decode() {
let p = parse_emf(&[stretch_dibits(3, 1, 5, &png(3, 1, [1, 2, 3, 255]))]);
let Item::Bitmap { rect, width, height, rgba, .. } = &p.items[0] else { panic!("expected a bitmap") };
assert_eq!((*width, *height, rect), (3, 1, &[0.0, 0.0, 30.0, 10.0]));
assert_eq!(rgba.as_slice(), [1, 2, 3, 255].repeat(3));
let mut jpeg = Vec::new();
let img = image::RgbImage::from_pixel(8, 8, image::Rgb([200, 200, 200]));
image::DynamicImage::ImageRgb8(img).write_to(&mut std::io::Cursor::new(&mut jpeg), image::ImageFormat::Jpeg).unwrap();
let p = parse_emf(&[stretch_dibits(8, 8, 4, &jpeg)]);
let Item::Bitmap { width, height, rgba, .. } = &p.items[0] else { panic!("expected a bitmap") };
assert_eq!((*width, *height), (8, 8));
assert!(rgba.as_chunks::<4>().0.iter().all(|c| c[0].abs_diff(200) < 4 && c[3] == 255));
// Bits that are no image, and a truncated file, draw nothing.
assert!(parse_emf(&[stretch_dibits(3, 1, 5, &[1, 2, 3, 4])]).items.is_empty());
let whole = png(3, 1, [1, 2, 3, 255]);
assert!(parse_emf(&[stretch_dibits(3, 1, 5, &whole[..whole.len() / 2])]).items.is_empty());
}
#[test]
fn a_compressed_dib_over_the_side_cap_is_refused_before_decoding() {
let side = crate::dib::MAX_COMPRESSED_SIDE;
assert!(crate::dib::decode_compressed(&png(side, 1, [0; 4])).is_some());
assert!(crate::dib::decode_compressed(&png(side + 1, 1, [0; 4])).is_none());
// A side no renderer draws (MAX_BITMAP_SIDE) makes no item either.
let wide = png(crate::MAX_BITMAP_SIDE + 1, 1, [0; 4]);
assert!(parse_emf(&[stretch_dibits(crate::MAX_BITMAP_SIDE as i32 + 1, 1, 5, &wide)]).items.is_empty());
}
#[test]
fn placed_paths_carry_their_line_style_and_shared_clips() {
let mut recs = clip_to_rect(0, 0, 50, 50, 1);
recs.extend([ext_pen(PS_GEOMETRIC | 7 | 0x100, 2, &[6, 3]), erec(37, &h32(1)), rect(0, 0, 100, 100), rect(10, 10, 20, 20)]);
let p = parse_emf(&recs);
assert_eq!((p.width, p.height), (100.0, 100.0));
let placed = p.place(kurbo::Rect::new(0.0, 0.0, 200.0, 200.0), [0.0; 4]);
let (PlacedItem::Path { line, clips: a, stroke, .. }, PlacedItem::Path { clips: b, .. }) = (&placed[0], &placed[1]) else {
panic!("expected two paths")
};
assert_eq!(stroke.map(|s| s.1), Some(4.0));
assert_eq!((line.cap, &line.dash), (Cap::Square, &vec![12.0, 6.0]), "dashes scale with the width");
assert!(Arc::ptr_eq(&a[0], &b[0]));
assert_eq!(kurbo::Shape::bounding_box(&a[0].path), kurbo::Rect::new(0.0, 0.0, 100.0, 100.0));
assert!(a[0].even_odd);
}
#[test]
fn an_item_under_a_clip_far_off_the_page_is_left_out() {
let clip = Arc::new(ClipPath { segs: vec![Seg::Move(0.0, 0.0), Seg::Line(f32::MAX, 0.0), Seg::Line(0.0, 10.0), Seg::Close], even_odd: false });
let bitmap = |clips| Item::Bitmap {
rect: [0.0, 0.0, 10.0, 10.0],
transform: [10.0, 0.0, 0.0, 10.0, 0.0, 0.0],
width: 1,
height: 1,
rgba: vec![0; 4],
region: None,
clips,
};
let pic = crate::Picture { width: 10.0, height: 10.0, items: vec![bitmap(vec![clip]), bitmap(Vec::new())], skipped_records: 0 };
let placed = pic.place(kurbo::Rect::new(0.0, 0.0, 10.0, 10.0), [0.0; 4]);
assert_eq!(placed.len(), 1);
}
#[test]
fn an_upright_bitmap_has_its_rectangle_as_its_transform() {
let p = parse(&stretch_dib(dib24_bottom_up())).unwrap();
let Item::Bitmap { rect, transform, .. } = &p.items[0] else { panic!("expected a bitmap") };
assert_eq!((rect, transform), (&[0.0, 0.0, 20.0, 10.0], &[20.0, 0.0, 0.0, 10.0, 0.0, 0.0]));
}
#[test]
fn a_turned_bitmap_keeps_its_pixels_and_carries_the_turn() {
// A quarter turn (x' = -y, y' = x) moved 10 right: the 30 x 10 destination stands 10 x 30.
let xf: Vec<u8> = [0.0f32, 1.0, -1.0, 0.0, 10.0, 0.0].iter().flat_map(|v| v.to_le_bytes()).collect();
let pixels = png(3, 1, [9, 8, 7, 255]);
let p = parse(&emf(&[erec(35, &xf), stretch_dibits(3, 1, 5, &pixels)], [0, 0, 10, 30], [0, 0, 2540, 7620])).unwrap();
let Item::Bitmap { transform, width, height, .. } = &p.items[0] else { panic!("expected a bitmap") };
assert_eq!((*width, *height), (3, 1), "the pixels are not resampled");
// 4 device pixels a millimetre and a 25.4 x 76.2 mm frame of 72 x 216 points: 0.709 points a pixel.
let k = 72.0 / 101.6;
let want = [0.0, 30.0 * k, -10.0 * k, 0.0, 10.0 * k, 0.0];
assert!(transform.iter().zip(want).all(|(a, b)| (a - b).abs() < 1e-9), "{transform:?}");
let placed = p.place(kurbo::Rect::new(0.0, 0.0, 72.0, 216.0), [0.0; 4]);
let PlacedItem::Bitmap { transform, .. } = &placed[0] else { panic!("expected a bitmap") };
let corner = *transform * kurbo::Point::new(1.0, 1.0);
assert!((corner.x - 0.0).abs() < 1e-6 && (corner.y - 30.0 * k).abs() < 1e-6, "{corner:?}");
}
#[test]
fn compressed_bitmaps_share_one_decode_budget_per_file() {
let small = png(3, 1, [0, 0, 0, 255]);
crate::dib::set_decode_budget(7);
assert!(crate::dib::decode_compressed(&small).is_some());
assert!(crate::dib::decode_compressed(&small).is_some());
assert!(crate::dib::decode_compressed(&small).is_none(), "one pixel left: the third image is not decoded");
// Every parse starts with the whole budget again.
let p = parse_emf(&[stretch_dibits(3, 1, 5, &small)]);
assert_eq!(p.items.len(), 1);
}
#[test]
fn truncated_and_mutated_files_with_these_records_never_panic() {
let mut recs = vec![
ext_pen(PS_GEOMETRIC | 7 | 0x200 | 0x2000, 4, &[8, 4, 2, 1]),
erec(37, &h32(1)),
erec(58, &h32(3)),
erec(57, &h32(2)),
erec(17, &h32(8)),
erec(9, &cat(vec![e32(100), e32(100)])),
erec(11, &cat(vec![e32(100), e32(100)])),
erec(31, &cat(vec![e32(2), e32(1), e32(3), e32(1)])),
erec(32, &cat(vec![e32(1), e32(2), e32(1), e32(3)])),
erec(33, &[]),
];
recs.extend(clip_to_rect(0, 0, 50, 50, 1));
recs.extend(clip_to_rect(5, 5, 40, 40, 5));
recs.extend([
erec(45, &cat(vec![e32(0), e32(0), e32(100), e32(100), e32(100), e32(50), e32(50), e32(0)])),
erec(59, &[]),
move_to(0, 0),
line_to(10, 0),
line_to(10, 10),
erec(61, &[]),
erec(60, &[]),
erec(63, &[0; 16]),
erec(59, &[]),
rect(0, 0, 5, 5),
erec(68, &[]),
stretch_dibits(3, 1, 5, &png(3, 1, [1, 2, 3, 255])),
erec(34, &e32(-1)),
erec(200, &[0; 8]),
]);
let efile = emf(&recs, [0, 0, 100, 100], [0, 0, 2540, 2540]);
let mut core = cat(vec![h32(12), h16(2), h16(2), h16(1), h16(24)]);
core.extend(dib24_bottom_up().get(40..).unwrap_or_default());
let wfile = stretch_dib(core);
assert!(!parse(&efile).unwrap().items.is_empty() && !parse(&wfile).unwrap().items.is_empty());
let mut rng = Lcg(0x9E37_79B9_7F4A_7C15);
for file in [&efile, &wfile] {
for n in 0..=file.len() {
let _ = parse(&file[..n]);
let _ = crate::size(&file[..n]);
}
for at in 0..file.len() {
for v in [0x00, 0x7F, 0x80, 0xFF] {
let mut bad = file.clone();
bad[at] = v;
let _ = parse(&bad);
}
}
for _ in 0..2000 {
let mut bad = file.clone();
for _ in 0..6 {
let at = rng.rnd() as usize % bad.len();
bad[at] = rng.rnd() as u8;
}
if let Ok(p) = parse(&bad) {
let _ = p.place(kurbo::Rect::new(0.0, 0.0, 100.0, 100.0), [0.0; 4]);
}
}
}
}
#[test]
fn a_turned_bitmaps_rectangle_is_the_box_around_it() {
let xf: Vec<u8> = [0.0f32, 1.0, -1.0, 0.0, 10.0, 0.0].iter().flat_map(|v| v.to_le_bytes()).collect();
let p = parse(&emf(&[erec(35, &xf), stretch_dibits(3, 1, 5, &png(3, 1, [9, 8, 7, 255]))], [0, 0, 10, 30], [0, 0, 2540, 7620])).unwrap();
let Item::Bitmap { rect, .. } = &p.items[0] else { panic!("expected a bitmap") };
let k = 72.0 / 101.6;
assert!(rect.iter().zip([0.0, 0.0, 10.0 * k, 30.0 * k]).all(|(a, b)| (f64::from(*a) - b).abs() < 1e-4), "{rect:?}");
}
+220
View File
@@ -0,0 +1,220 @@
//! Hand-built files from VectorCraft's reader tests, played into the picture model: what GDI
//! draws for them is what the model must hold.
use super::*;
use crate::{Cap, Kind, sniff};
/// An EMF of `records` (type, body) with a `w` x `h` frame in 0.01 mm and a reference device of
/// 100 pixels a millimetre, so one device unit is 0.01 mm.
fn hand_emf(w: i32, h: i32, records: &[(u32, Vec<u8>)]) -> Vec<u8> {
let mut head = vec![0u8; 16];
head.extend(le(&[0, 0, w, h]));
head.extend(0x464D_4520u32.to_le_bytes());
head.extend(0x10000u32.to_le_bytes());
head.extend([0; 8]);
head.extend([1, 0, 0, 0]);
head.extend([0; 12]);
head.extend(le(&[10000, 10000, 100, 100]));
let mut out = Vec::new();
let eof = (14, vec![0, 0, 0, 0, 16, 0, 0, 0, 20, 0, 0, 0]);
for (kind, body) in [(1, head)].iter().chain(records).chain([&eof]) {
out.extend(kind.to_le_bytes());
out.extend((8 + body.len() as u32).to_le_bytes());
out.extend(body);
}
out
}
fn le(values: &[i32]) -> Vec<u8> {
values.iter().flat_map(|v| v.to_le_bytes()).collect()
}
fn le16(values: &[i16]) -> Vec<u8> {
values.iter().flat_map(|v| v.to_le_bytes()).collect()
}
/// A placeable WMF of `records` (function, parameters) in a `w` x `h` box at 1440 units an inch.
fn hand_wmf(w: i16, h: i16, records: &[(u16, Vec<u8>)]) -> Vec<u8> {
let mut o = 0x9AC6_CDD7u32.to_le_bytes().to_vec();
o.extend(le16(&[0, 0, 0, w, h, 1440]));
o.extend([0; 4]);
let sum = o.as_chunks::<2>().0.iter().fold(0u16, |a, w| a ^ u16::from_le_bytes(*w));
o.extend(sum.to_le_bytes());
o.extend([1, 0, 9, 0, 0, 3]);
o.extend([0; 12]);
for (f, body) in records.iter().chain(&[(0, vec![])]) {
o.extend((3 + body.len() as u32 / 2).to_le_bytes());
o.extend(f.to_le_bytes());
o.extend(body);
}
o
}
/// The bounding box `[x0, y0, x1, y1]` of a path item's points.
fn bounds(item: &Item) -> [f32; 4] {
let (segs, ..) = path_segs(item);
let mut b = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
let mut grow = |x: f32, y: f32| b = [b[0].min(x), b[1].min(y), b[2].max(x), b[3].max(y)];
for s in segs {
match *s {
Seg::Move(x, y) | Seg::Line(x, y) => grow(x, y),
Seg::Cubic(x1, y1, x2, y2, x, y) => {
grow(x1, y1);
grow(x2, y2);
grow(x, y);
}
Seg::Close => {}
}
}
b
}
fn near(a: [f32; 4], b: [f64; 4], tol: f64) -> bool {
a.iter().zip(b).all(|(a, b)| (f64::from(*a) - b).abs() <= tol)
}
const MM: f64 = 72.0 / 25.4;
#[test]
fn hand_built_emf_plays_like_gdi() {
let mut text = vec![0; 16];
text.extend(le(&[1, 0, 0]));
// At (1000, 1500), 2 characters at offset 76 (from the record start).
text.extend(le(&[1000, 1500, 2, 76, 0, 0, 0, 0, 0, 0]));
text.extend("Hi".encode_utf16().flat_map(u16::to_le_bytes));
let mut font = le(&[2, -1000, 0, 0, 0, 700]);
font.extend([0u8; 8]);
font.extend("Arial".encode_utf16().chain(std::iter::repeat_n(0, 27)).flat_map(u16::to_le_bytes));
let records = vec![
// A green brush in slot 1, selected; a null pen.
(39, le(&[1, 0, 0x00_ff_00, 0])),
(37, le(&[1])),
(37, le(&[0x8000_0008u32 as i32])),
// A rectangle of 10 x 5 mm at (10, 10) mm.
(43, le(&[1000, 1000, 2000, 1500])),
// A record nobody knows, and one of a kind not read (ANGLEARC).
(9999, le(&[1, 2])),
(41, le(&[0, 0, 10, 0, 0])),
// Clip to a rectangle; a black pen draws two lines one after the other.
(30, le(&[0, 0, 1500, 1500])),
(37, le(&[0x8000_0007u32 as i32])),
(27, le(&[0, 0])),
(54, le(&[500, 0])),
(54, le(&[500, 500])),
// A 10 mm bold font and red text.
(82, font),
(37, le(&[2])),
(24, le(&[0xff])),
(84, text),
];
let file = hand_emf(5000, 3000, &records);
assert_eq!(sniff(&file), Some(Kind::Emf));
let p = parse(&file).unwrap();
assert!((f64::from(p.width) - 50.0 * MM).abs() < 0.01 && (f64::from(p.height) - 30.0 * MM).abs() < 0.01);
assert_eq!(p.skipped_records, 2);
assert_eq!(p.items.len(), 4, "the rectangle, two lines and the text");
let (_, fill, stroke, _) = path_segs(&p.items[0]);
assert_eq!((fill, stroke), (Some([0, 255, 0, 255]), None), "green, with the null pen");
assert!(near(bounds(&p.items[0]), [10.0 * MM, 10.0 * MM, 20.0 * MM, 15.0 * MM], 0.01));
let Item::Path { region: None, .. } = &p.items[0] else { panic!("drawn before the clip") };
// The lines and the text are clipped to the rectangle.
for item in &p.items[1..3] {
let Item::Path { region: Some(r), fill: None, stroke: Some(([0, 0, 0, 255], _)), .. } = item else { panic!("a clipped black line") };
assert!(r.len() == 1 && near(r[0], [0.0, 0.0, 15.0 * MM, 15.0 * MM], 0.01));
}
assert!(near(bounds(&p.items[1]), [0.0, 0.0, 5.0 * MM, 0.0], 0.01));
assert!(near(bounds(&p.items[2]), [5.0 * MM, 0.0, 5.0 * MM, 5.0 * MM], 0.01));
let Item::Text(t) = &p.items[3] else { panic!("text") };
assert_eq!((t.text.as_str(), t.font.family.as_str(), t.font.weight), ("Hi", "Arial", 700));
assert_eq!(t.color, [255, 0, 0, 255]);
// The em is 1000 text units of 0.01 mm: 10 mm.
let em = f64::from(-t.font.height) * t.transform[3];
assert!((em - 10.0 * MM).abs() < 0.01, "{em}");
assert!((t.transform[4] - 10.0 * MM).abs() < 0.01 && (t.transform[5] - 15.0 * MM).abs() < 0.01);
assert!(t.region.as_ref().is_some_and(|r| r.len() == 1));
}
#[test]
fn emf_mapping_modes_and_world_transforms() {
let records = vec![
// MM_ANISOTROPIC: a 100 x 100 window on a 1000 x 2000 viewport.
(17, le(&[8])),
(9, le(&[100, 100])),
(11, le(&[1000, 2000])),
(43, le(&[0, 0, 10, 10])),
// Then a world transform moving by (5, 0) logical units.
(35, [1.0f32, 0.0, 0.0, 1.0, 5.0, 0.0].iter().flat_map(|v| v.to_le_bytes()).collect()),
(43, le(&[0, 0, 10, 10])),
];
let p = parse(&hand_emf(10000, 10000, &records)).unwrap();
// 10 units are 100 x 200 pixels: 1 x 2 mm.
assert!(near(bounds(&p.items[0]), [0.0, 0.0, MM, 2.0 * MM], 0.01));
assert!(near(bounds(&p.items[1]), [0.5 * MM, 0.0, 1.5 * MM, 2.0 * MM], 0.01));
}
#[test]
fn hand_built_wmf_plays_like_gdi() {
let records = vec![
// A blue brush (slot 0), a 20-unit red pen with square caps (slot 1), selected.
(0x02FC, [le16(&[0]), 0x00ff_0000u32.to_le_bytes().to_vec(), le16(&[0])].concat()),
(0x02FA, [le16(&[0x100, 20, 0]), 0x0000_00ffu32.to_le_bytes().to_vec()].concat()),
(0x012D, le16(&[0])),
(0x012D, le16(&[1])),
// A rectangle (bottom, right, top, left): 1 x 0.5 inches at the origin.
(0x041B, le16(&[720, 1440, 0, 0])),
(0x0999, le16(&[1, 2])),
// Delete the brush: the next object takes slot 0 again.
(0x01F0, le16(&[0])),
(0x02FC, [le16(&[0]), 0x0000_ff00u32.to_le_bytes().to_vec(), le16(&[0])].concat()),
(0x012D, le16(&[0])),
(0x0418, le16(&[1440, 1440, 720, 0])),
];
let file = hand_wmf(1440, 1440, &records);
assert_eq!(sniff(&file), Some(Kind::Wmf));
let p = parse(&file).unwrap();
assert_eq!((p.width, p.height), (72.0, 72.0));
assert_eq!(p.skipped_records, 1);
assert_eq!(p.items.len(), 2);
let Item::Path { fill, stroke, line, .. } = &p.items[0] else { panic!("a path") };
assert_eq!(*fill, Some([0, 0, 255, 255]));
assert!(near(bounds(&p.items[0]), [0.0, 0.0, 72.0, 36.0], 1e-4));
assert_eq!((*stroke, line.cap), (Some(([255, 0, 0, 255], 1.0)), Cap::Square), "20 units of 1/1440 inch are a point");
assert_eq!(path_segs(&p.items[1]).1, Some([0, 255, 0, 255]), "the new brush took the freed slot");
}
#[test]
fn wmf_window_is_mapped_onto_the_placeable_box() {
// The box is 1000 x 500 units at 1440 an inch: 50 x 25 pt.
let want = [12.5, 6.25, 37.5, 18.75];
// Window 0,0 - 4000 x 2000: the rectangle 1000,500 - 3000,1500 is the middle half.
let ext = vec![(0x020C, le16(&[2000, 4000])), (0x041B, le16(&[1500, 3000, 500, 1000]))];
// Window 10000,20000 - 1000 x 500 (the box): the rectangle 10250,20125 - 10750,20375.
let org = vec![(0x020B, le16(&[20000, 10000])), (0x020C, le16(&[500, 1000])), (0x041B, le16(&[20375, 10750, 20125, 10250]))];
// Window 0,2000 - 4000 x -2000 (y up): the same rectangle, flipped onto the same place.
let flip = vec![(0x020B, le16(&[2000, 0])), (0x020C, le16(&[-2000, 4000])), (0x041B, le16(&[1500, 3000, 500, 1000]))];
for records in [ext, org, flip] {
let p = parse(&hand_wmf(1000, 500, &records)).unwrap();
assert_eq!((p.width, p.height), (50.0, 25.0));
assert_eq!(p.items.len(), 1);
assert!(near(bounds(&p.items[0]), want, 1e-4), "{:?}", bounds(&p.items[0]));
}
}
#[test]
fn huge_counts_sizes_and_offsets_are_refused_not_allocated() {
let huge = hand_emf(100, 100, &[(3, le(&[0, 0, 0, 0, i32::MAX])), (81, le(&[0; 18])), (8, le(&[0, 0, 0, 0, i32::MAX, i32::MAX]))]);
assert!(parse(&huge).unwrap().items.is_empty());
let huge = hand_wmf(100, 100, &[(0x0324, le16(&[i16::MAX])), (0x0538, le16(&[-1]))]);
assert!(parse(&huge).unwrap().items.is_empty());
assert!(parse(b"not a metafile").is_err());
}
#[test]
fn a_run_of_clip_rectangles_stays_one_region() {
// 200,000 INTERSECTCLIPRECT records nest nothing: the region is still one rectangle.
let mut records = vec![(30, le(&[0, 0, 1500, 1500])); 200_000];
records.push((43, le(&[100, 100, 200, 200])));
let p = parse(&hand_emf(5000, 3000, &records)).unwrap();
let Item::Path { region: Some(r), clips, .. } = &p.items[0] else { panic!("a clipped path") };
assert_eq!((r.len(), clips.len()), (1, 0));
}
+205
View File
@@ -0,0 +1,205 @@
//! Text records: the font (LOGFONT), the text string decoded from its character set, and the
//! neutral [`Text`] item. The metafile crate knows no fonts, so measuring, alignment against the
//! font's metrics and glyph drawing are left to the renderer.
/// A font chosen by a CREATEFONTINDIRECT record (MS-WMF 2.2.1.2 Font, MS-EMF 2.2.13 LogFont).
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Font {
/// The face name the file asks for (`"Arial"`, `"Wingdings"`); may be empty.
pub family: String,
/// LOGFONT height in text units: negative is the character (em) height, positive the cell
/// height (ascent plus descent), 0 the default size.
pub height: f32,
/// Average character width in text units; 0 keeps the font's own proportions.
pub width: f32,
/// 100..=900 (400 regular, 700 bold); 0 is the default weight.
pub weight: u16,
pub italic: bool,
pub underline: bool,
pub strike: bool,
/// LOGFONT character set (0 ANSI, 2 SYMBOL, 238 EASTEUROPE…).
pub charset: u8,
}
/// SYMBOL_CHARSET: byte codes are the font's own symbol codes.
pub const SYMBOL_CHARSET: u8 = 2;
/// Where the reference point sits on the string horizontally (TA_LEFT, TA_CENTER, TA_RIGHT).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HAlign {
#[default]
Left,
Center,
Right,
}
/// Where the reference point sits vertically (TA_TOP, TA_BASELINE, TA_BOTTOM): the top of the
/// character cell, the baseline or the bottom of the cell.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VAlign {
#[default]
Top,
Baseline,
Bottom,
}
/// One string drawn by EXTTEXTOUT, TEXTOUT or SMALLTEXTOUT.
#[derive(Debug, Clone, PartialEq)]
pub struct Text {
/// Text space to picture space as affine coefficients `[a, b, c, d, e, f]` (`x' = a·x + c·y + e`,
/// `y' = b·x + d·y + f`, kurbo's order). Text space has x along the baseline and y down, in the
/// metafile's logical units, with the reference point at the origin. Escapement and the
/// mapping are folded in; glyphs are never mirrored.
pub transform: [f64; 6],
pub text: String,
/// Advance after each character of `text`, in text units (the dx array). Empty when the file
/// gives none: the font's own advances apply.
pub dx: Vec<f32>,
pub font: Font,
pub color: [u8; 4],
/// Colour filled behind the character cells (OPAQUE background mode); None when transparent.
pub background: Option<[u8; 4]>,
pub h_align: HAlign,
pub v_align: VAlign,
/// Clip rectangle `[x0, y0, x1, y1]` in picture units (ETO_CLIPPED).
pub clip: Option<[f32; 4]>,
/// Clip region from the device context, in picture units. None is the default clip. Drawn
/// together with [`Text::clip`] when both are set.
pub region: Option<Vec<[f32; 4]>>,
/// Clip paths from the device context, as on [`crate::Item::Path`].
pub clips: Vec<std::sync::Arc<crate::ClipPath>>,
}
/// TA_UPDATECP: the string starts at the current position, which moves past it.
pub(crate) const TA_UPDATECP: u32 = 0x0001;
/// ETO_OPAQUE fills the rectangle with the background colour; ETO_CLIPPED clips the text to it.
pub(crate) const ETO_OPAQUE: u32 = 0x0002;
pub(crate) const ETO_CLIPPED: u32 = 0x0004;
/// The string holds glyph indices, not characters (drawn only when the font matches).
pub(crate) const ETO_GLYPH_INDEX: u32 = 0x0010;
/// The dx array holds (dx, dy) pairs.
pub(crate) const ETO_PDY: u32 = 0x2000;
/// Longest string kept from one record.
pub(crate) const MAX_CHARS: usize = 16_384;
/// Horizontal and vertical alignment from a TA_* word (the vertical bits are 0 top, 8 bottom,
/// 24 baseline; the horizontal ones 0 left, 2 right, 6 center).
pub(crate) fn align(ta: u32) -> (HAlign, VAlign) {
let h = match ta & 0x06 {
6 => HAlign::Center,
2 => HAlign::Right,
_ => HAlign::Left,
};
let v = match ta & 0x18 {
0x18 => VAlign::Baseline,
0x08 => VAlign::Bottom,
_ => VAlign::Top,
};
(h, v)
}
/// Decodes 8-bit text in `charset` to one character per byte, and merges `dx` to match: a
/// double-byte character set's lead and trail bytes become one blank (its glyphs are not
/// decoded) whose advance is the sum of both. `dx` may be empty.
pub(crate) fn decode_bytes(bytes: &[u8], charset: u8, dx: &[f32]) -> (String, Vec<f32>) {
let mut s = String::with_capacity(bytes.len());
let mut out_dx = Vec::with_capacity(dx.len().min(bytes.len()));
let dbcs = matches!(charset, 128 | 129 | 130 | 134 | 136);
let mut i = 0;
while let Some(&b) = bytes.get(i) {
let n = if dbcs && b >= 0x81 && i + 1 < bytes.len() { 2 } else { 1 };
let c = if n == 2 { ' ' } else { byte_char(b, charset) };
s.push(c);
if !dx.is_empty() {
out_dx.push((i..i + n).filter_map(|k| dx.get(k)).sum());
}
i += n;
}
(s, out_dx)
}
/// UTF-16 text to a string with one dx entry per character: a surrogate pair takes the sum of
/// its two advances. Unpaired surrogates become U+FFFD.
pub(crate) fn decode_utf16(units: &[u16], dx: &[f32]) -> (String, Vec<f32>) {
let mut s = String::with_capacity(units.len());
let mut out_dx = Vec::with_capacity(dx.len().min(units.len()));
let mut i = 0;
for r in char::decode_utf16(units.iter().copied()) {
let c = r.unwrap_or(char::REPLACEMENT_CHARACTER);
let n = c.len_utf16();
s.push(c);
if !dx.is_empty() {
out_dx.push((i..i + n).filter_map(|k| dx.get(k)).sum());
}
i += n;
}
(s, out_dx)
}
/// One byte in a single-byte Windows code page. Symbol fonts keep their codes in the private-use
/// range (U+F000 + code), where symbol fonts map them.
fn byte_char(b: u8, charset: u8) -> char {
if b < 0x80 && charset != SYMBOL_CHARSET {
return char::from(b);
}
let cp = match charset {
SYMBOL_CHARSET => 0xF000 | u32::from(b),
238 => high(b, &CP1250),
204 => high(b, &CP1251),
_ => high(b, &CP1252),
};
char::from_u32(cp).unwrap_or(char::REPLACEMENT_CHARACTER)
}
/// The code point of byte `b` (0x80..=0xFF) from a table of the upper half; 0 entries are
/// undefined bytes, drawn as U+FFFD.
fn high(b: u8, table: &[u16; 128]) -> u32 {
match table.get(usize::from(b.wrapping_sub(0x80))) {
Some(0) | None => 0xFFFD,
Some(v) => u32::from(*v),
}
}
/// Windows-1252 (Western) 0x80..=0xFF: the upper half of Latin-1 with typographic marks in 0x80..=0x9F.
const CP1252: [u16; 128] = {
let mut t = [0u16; 128];
let low: [u16; 32] = [
0x20AC, 0, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0, 0x017D, 0, 0, 0x2018, 0x2019, 0x201C,
0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0, 0x017E, 0x0178,
];
let mut i = 0;
while i < 128 {
t[i] = if i < 32 { low[i] } else { 0x80 + i as u16 };
i += 1;
}
t
};
/// Windows-1250 (Central European) 0x80..=0xFF.
const CP1250: [u16; 128] = [
0x20AC, 0, 0x201A, 0, 0x201E, 0x2026, 0x2020, 0x2021, 0, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, //
0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, //
0x00A0, 0x02C7, 0x02D8, 0x0141, 0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B, //
0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C, //
0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, //
0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, //
0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, //
0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9,
];
/// Windows-1251 (Cyrillic) 0x80..=0xFF; 0xC0..=0xFF are А..я in order.
const CP1251: [u16; 128] = {
let low: [u16; 64] = [
0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, //
0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, //
0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, //
0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457,
];
let mut t = [0u16; 128];
let mut i = 0;
while i < 128 {
t[i] = if i < 64 { low[i] } else { 0x0410 + (i as u16 - 64) };
i += 1;
}
t
};
+391
View File
@@ -0,0 +1,391 @@
//! WMF reader (MS-WMF). A file is an optional 22-byte placeable header, a 9-word standard header, then
//! records of (size in 16-bit words, function, parameters). Parameters are read at byte 6 of each record.
use std::sync::Arc;
use crate::bytes::Bytes;
use crate::canvas::{BS_HATCHED, Brush, Canvas, LogFont, Obj, Pen, colorref};
use crate::dib;
use crate::geom::{self, ArcKind, P};
use crate::text::{ETO_CLIPPED, ETO_OPAQUE, Font, MAX_CHARS, decode_bytes};
use crate::{Error, Frame, MAX_PATH_OPS};
const PLACEABLE_KEY: u32 = 0x9AC6_CDD7;
const DEFAULT_INCH: f64 = 1440.0;
/// A placeable WMF plays into a target sized in points; brushes are laid out in 96-dpi pixels.
const PT_PER_PX: f64 = 72.0 / 96.0;
pub(crate) fn is_wmf(b: Bytes) -> bool {
if b.u32(0) == Some(PLACEABLE_KEY) {
return true;
}
matches!(b.u16(0), Some(1 | 2)) && b.u16(2) == Some(9) && matches!(b.u16(4), Some(0x100 | 0x300))
}
/// Reads all records. Returns the drawing state and the picture frame, or an error when no record
/// could be read at all.
pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), Error> {
let placeable = input.u32(0) == Some(PLACEABLE_KEY);
// Placeable bounds: logical corners (left, top, right, bottom) and their size in points.
let (hdr, bounds) = if placeable {
let [l, t, r, b] = [6, 8, 10, 12].map(|o| input.i16(o).map(f64::from));
let (Some(l), Some(t), Some(r), Some(b)) = (l, t, r, b) else { return Err(Error::BadHeader) };
let inch = input.u16(14).filter(|v| *v > 0).map_or(DEFAULT_INCH, f64::from);
let (w, h) = (r - l, b - t);
let bounds = (w > 0.0 && h > 0.0).then_some(([l, t, r, b], (w * 72.0 / inch, h * 72.0 / inch)));
(22, bounds)
} else {
(0, None)
};
let hsize = input.u16(hdr + 2).ok_or(Error::BadHeader)? as usize;
if hsize < 9 {
return Err(Error::BadHeader);
}
let mut pos = hdr.checked_add(hsize.checked_mul(2).ok_or(Error::BadHeader)?).ok_or(Error::BadHeader)?;
// Played into a target like Word does: the placeable bounds are the first window, the target (the
// picture size) the viewport. A plain WMF's target is its first window.
let mut cv = Canvas::new();
if let Some((c, size)) = bounds {
cv.play_into(Some([c[0], c[1], c[2] - c[0], c[3] - c[1]]), size);
cv.set_clip_surface([0.0, 0.0, size.0, size.1]);
cv.pattern_px = PT_PER_PX;
}
let mut player_viewport = bounds.is_some();
let mut parsed = 0usize;
while !cv.full() {
let (Some(words), Some(func)) = (input.u32(pos), pos.checked_add(4).and_then(|o| input.u16(o))) else { break };
let Some(len) = (words as usize).checked_mul(2).filter(|l| *l >= 6) else { break };
let Some(rec) = input.slice(pos, len) else { break };
if func == 0 {
break;
}
record(&mut cv, func, Bytes(rec));
if func == 0x020C && cv.window_set && !player_viewport && !cv.viewport_set {
let we = cv.map.window_ext();
cv.play_into(None, (we.0.abs(), we.1.abs()));
player_viewport = true;
}
parsed += 1;
let Some(next) = pos.checked_add(len) else { break };
pos = next;
}
if parsed == 0 {
return Err(Error::NoRecords);
}
let frame = match bounds {
// Played into its target: the frame is the target.
Some((_, size)) if !cv.viewport_set && cv.map.uses_extents() => Some(Frame { rect: [0.0, 0.0, size.0, size.1], size: Some(size) }),
// The file set its own viewport, so the drawing is in its device units and the logical bounds
// go through the final mapping.
Some((c, size)) => {
let (a, b) = (cv.map.device((c[0], c[1])), cv.map.device((c[2], c[3])));
let rect = [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()];
(rect[2] > 0.0 && rect[3] > 0.0).then_some(Frame { rect, size: Some(size) })
}
None if player_viewport && !cv.viewport_set && cv.map.uses_extents() => {
let ve = cv.map.viewport_ext();
Some(Frame { rect: [0.0, 0.0, ve.0, ve.1], size: None })
}
None if cv.window_set => Some(Frame { rect: cv.map.window_rect(), size: None }),
None => None,
};
Ok((cv, frame))
}
fn record(cv: &mut Canvas, func: u16, r: Bytes) {
if cv.full() {
return;
}
let _ = match func {
0x0103 => r.u16(6).map(|m| cv.set_map_mode(u32::from(m))),
0x0106 => r.u16(6).map(|m| cv.set_polyfill(u32::from(m))),
0x0104 => r.u16(6).map(|m| cv.set_rop2(u32::from(m))),
// Y is stored before X in these records.
0x020B => xy16(r, 6).map(|p| cv.set_window_org(p)),
0x020C => xy16(r, 6).map(|p| cv.set_window_ext(p)),
0x020D => xy16(r, 6).map(|p| cv.set_viewport_org(p)),
0x020E => xy16(r, 6).map(|p| cv.set_viewport_ext(p)),
0x001E => {
cv.save_dc();
Some(())
}
0x0127 => r.i16(6).map(|n| cv.restore_dc(i32::from(n))),
0x0214 => xy16(r, 6).map(|p| cv.move_to(p)),
0x0213 => xy16(r, 6).map(|p| cv.line_to(p)),
0x0325 => counted_points(r).map(|pts| cv.poly(&pts, false, false, true)),
0x0324 => counted_points(r).map(|pts| cv.poly(&pts, true, true, true)),
0x0538 => polypolygon(r).map(|ops| cv.shape(ops, true, true)),
0x041B => rect16(r, 6).map(|[l, t, rr, b]| cv.shape(geom::rect(l, t, rr, b), true, true)),
0x061C => {
let [ew, eh] = [r.i16(8), r.i16(6)];
let (Some(ew), Some(eh)) = (ew, eh) else { return };
rect16(r, 10).map(|[l, t, rr, b]| {
cv.shape(geom::round_rect(l, t, rr, b, f64::from(ew), f64::from(eh)), true, true);
})
}
0x0415 => rect16(r, 6).map(|rect| cv.exclude_clip_rect(rect)),
0x0416 => rect16(r, 6).map(|rect| cv.intersect_clip_rect(rect)),
0x0418 => rect16(r, 6).map(|[l, t, rr, b]| cv.shape(geom::ellipse(l, t, rr, b), true, true)),
0x0817 | 0x081A | 0x0830 => arc16(r).map(|(bounds, s, e)| {
let kind = match func {
0x0817 => ArcKind::Arc,
0x081A => ArcKind::Pie,
_ => ArcKind::Chord,
};
let [l, t, rr, b] = bounds;
cv.shape(geom::arc_shape(l, t, rr, b, s, e, kind), kind != ArcKind::Arc, true);
}),
0x02FA => create_pen(cv, r),
0x02FC => create_brush(cv, r),
0x02FB => {
let f = font(r).unwrap_or_default();
cv.add_object(None, Obj::Font(Arc::new(f)));
Some(())
}
0x0142 => {
// A pattern the reader cannot decode still takes its handle slot.
let b = dib_pattern_brush(r).map_or(Obj::Other, Obj::Brush);
cv.add_object(None, b);
Some(())
}
// Palettes, old-style pattern brushes and regions take a handle slot too, so later
// SELECTOBJECT handles count them; selecting one draws nothing.
0x00F7 | 0x01F9 | 0x06FF => {
cv.add_object(None, Obj::Other);
Some(())
}
0x0209 => r.u32(6).map(|c| cv.set_text_color(c)),
0x0201 => r.u32(6).map(|c| cv.set_bk_color(c)),
0x0102 => r.u16(6).map(|m| cv.set_bk_mode(u32::from(m))),
0x012E => r.u16(6).map(|a| cv.set_text_align(u32::from(a))),
0x0A32 => ext_text_out(cv, r),
0x0521 => text_out(cv, r),
// PATBLT: rop(4), height, width, y, x.
0x061D => r.u32(6).map(|rop| cv.fill_rop([i16s(r, 16), i16s(r, 14), i16s(r, 12), i16s(r, 10)], rop)),
0x012D => r.u16(6).map(|h| cv.select(wmf_handle(h))),
0x01F0 => r.u16(6).map(|h| cv.delete_object(u32::from(h))),
// STRETCHDIB: rop(4) usage(2) srcH srcW ySrc xSrc destH destW yDest xDest, then the DIB.
0x0F43 => {
let rop = r.u32(6).unwrap_or(0);
dib_blit(cv, r, 28, rop, [i16s(r, 26), i16s(r, 24), i16s(r, 22), i16s(r, 20)], [i16s(r, 18), i16s(r, 16), i16s(r, 14), i16s(r, 12)])
}
0x0B41 => dib_stretchblt(cv, r),
0x0940 => dib_bitblt(cv, r),
f if IGNORED.contains(&f) => Some(()),
_ => {
cv.skipped = cv.skipped.saturating_add(1);
Some(())
}
};
}
/// Records that change nothing the picture keeps: relative-absolute and stretch modes, character
/// spacing and justification, mapper flags, palettes, escapes and the layout. Every other record
/// the reader does not play is counted as skipped.
const IGNORED: &[u16] = &[0x0105, 0x0107, 0x0108, 0x020A, 0x0231, 0x0234, 0x0035, 0x0037, 0x0139, 0x0436, 0x0626, 0x0149];
/// A 16-bit WMF handle as a canvas object index: stock objects are `0x8000 | index`, which the
/// canvas reads with the 32-bit stock flag.
fn wmf_handle(h: u16) -> u32 {
let h = u32::from(h);
if h & 0x8000 != 0 { (h & 0x7FFF) | 0x8000_0000 } else { h }
}
/// An i16 field as f64; 0 past the end.
fn i16s(r: Bytes, off: usize) -> f64 {
r.i16(off).map_or(0.0, f64::from)
}
/// CREATEPENINDIRECT: style, width (x, y), colour. Only the width's x component is used.
fn create_pen(cv: &mut Canvas, r: Bytes) -> Option<()> {
let style = r.u16(6)?;
let width = r.i16(8)?;
let color = r.u32(12)?;
cv.add_object(None, Obj::Pen(Pen::new(u32::from(style), f64::from(width), colorref(color))));
Some(())
}
/// CREATEBRUSHINDIRECT: style, colour, hatch.
fn create_brush(cv: &mut Canvas, r: Bytes) -> Option<()> {
let style = u32::from(r.u16(6)?);
let color = r.u32(8)?;
let mut b = Brush::new(style, colorref(color));
if style == BS_HATCHED {
b.hatch = u32::from(r.u16(12).unwrap_or(0));
}
cv.add_object(None, Obj::Brush(b));
Some(())
}
/// DIBCREATEPATTERNBRUSH: style, colour usage, then a DIB (MS-WMF 2.3.4.8). The DIB's own palette
/// gives the colours; with DIB_PAL_COLORS (indices into a palette the reader does not keep) a
/// monochrome DIB takes the text and background colours.
fn dib_pattern_brush(r: Bytes) -> Option<Brush> {
let usage = r.u16(8)?;
let dib = r.tail(10)?;
let n = dib::info_len(dib)?;
let img = dib::decode(Bytes(dib.slice(0, n)?), dib.tail(n)?, false)?;
Brush::pattern(img, usage == 1)
}
/// Font object (MS-WMF 2.2.1.2): height, width, escapement, orientation, weight (i16 each), italic,
/// underline, strike-out and charset bytes, three precision/quality bytes, pitch and family, then the
/// face name (up to 32 bytes, NUL-terminated, in the font's character set).
fn font(r: Bytes) -> Option<LogFont> {
let height = r.i16(6)?;
let width = r.i16(8).unwrap_or(0);
let escapement = r.i16(10).unwrap_or(0);
let weight = r.i16(14).unwrap_or(0);
let byte = |o: usize| r.0.get(o).copied().unwrap_or(0);
let charset = byte(19);
let name = r.0.get(24..).unwrap_or(&[]);
let name = name.get(..32).unwrap_or(name);
let name = name.split(|b| *b == 0).next().unwrap_or(&[]);
// Face names are plain ASCII in practice; other bytes go through the Western code page.
let (family, _) = decode_bytes(name, 0, &[]);
Some(LogFont {
font: Font {
family: family.trim().to_string(),
height: f32::from(height),
width: f32::from(width),
weight: weight.clamp(0, 1000) as u16,
italic: byte(16) != 0,
underline: byte(17) != 0,
strike: byte(18) != 0,
charset,
},
escapement: f64::from(escapement),
})
}
/// The charset of the selected font, for decoding 8-bit text.
fn charset(cv: &Canvas) -> u8 {
cv.font_charset()
}
/// EXTTEXTOUT (MS-WMF 2.3.3.5): y, x, string length, options, the rectangle (left, top, right,
/// bottom) when ETO_OPAQUE or ETO_CLIPPED is set, the string padded to a word, then an optional dx
/// array of i16 advances.
fn ext_text_out(cv: &mut Canvas, r: Bytes) -> Option<()> {
let (y, x) = (r.i16(6)?, r.i16(8)?);
let n = usize::from(r.u16(10)?).min(MAX_CHARS);
let opts = u32::from(r.u16(12)?);
let has_rect = opts & (ETO_OPAQUE | ETO_CLIPPED) != 0;
let rect = if has_rect { Some([i16s(r, 14), i16s(r, 16), i16s(r, 18), i16s(r, 20)]) } else { None };
let at = if has_rect { 22 } else { 14 };
// A string running past the record is cut to what the record holds.
let avail = r.0.len().saturating_sub(at);
let n = n.min(avail);
let bytes = r.slice(at, n)?;
let dx_at = at + n.next_multiple_of(2);
let dx: Vec<f32> = if r.0.len() >= dx_at + 2 * n { (0..n).filter_map(|i| r.i16(dx_at + 2 * i)).map(f32::from).collect() } else { Vec::new() };
let (text, dx) = decode_bytes(bytes, charset(cv), &dx);
cv.text((f64::from(x), f64::from(y)), text, dx, opts, rect);
Some(())
}
/// TEXTOUT (MS-WMF 2.3.3.20): string length, the string padded to a word, then y and x.
fn text_out(cv: &mut Canvas, r: Bytes) -> Option<()> {
let n = usize::from(r.u16(6)?).min(MAX_CHARS);
let bytes = r.slice(8, n)?;
let at = 8 + n.next_multiple_of(2);
let (y, x) = (r.i16(at)?, r.i16(at + 2)?);
let (text, _) = decode_bytes(bytes, charset(cv), &[]);
cv.text((f64::from(x), f64::from(y)), text, Vec::new(), 0, None);
Some(())
}
/// (x, y) from a Y-then-X pair stored at `off`.
fn xy16(r: Bytes, off: usize) -> Option<P> {
Some((f64::from(r.i16(off + 2)?), f64::from(r.i16(off)?)))
}
/// Rectangle stored as bottom, right, top, left (the order MS-WMF uses for RECTANGLE and ELLIPSE).
fn rect16(r: Bytes, off: usize) -> Option<[f64; 4]> {
let (b, rr, t, l) = (r.i16(off)?, r.i16(off + 2)?, r.i16(off + 4)?, r.i16(off + 6)?);
Some([f64::from(l), f64::from(t), f64::from(rr), f64::from(b)])
}
/// ARC, PIE and CHORD: end Y, end X, start Y, start X, then the bounding rectangle (bottom, right, top, left).
fn arc16(r: Bytes) -> Option<([f64; 4], P, P)> {
let end = xy16(r, 6)?;
let start = xy16(r, 10)?;
let bounds = rect16(r, 14)?;
Some((bounds, start, end))
}
/// A count of 16-bit points, then the points (POLYLINE, POLYGON).
fn counted_points(r: Bytes) -> Option<Vec<P>> {
let n = r.u16(6)? as usize;
r.tail(8)?.points(0, n, false)
}
/// POLYPOLYGON: polygon count, one 16-bit point count per polygon, then all points.
fn polypolygon(r: Bytes) -> Option<Vec<geom::Op>> {
let n = r.u16(6)? as usize;
let mut counts = Vec::with_capacity(n);
for i in 0..n {
counts.push(r.u16(i.checked_mul(2)?.checked_add(8)?)? as usize);
}
let total = counts.iter().try_fold(0usize, |a, c| a.checked_add(*c))?;
if total > MAX_PATH_OPS {
return None;
}
let pts = r.tail(n.checked_mul(2)?.checked_add(8)?)?.points(0, total, false)?;
Some(geom::subpaths(&pts, &counts, true))
}
/// Whether a DIBBITBLT or DIBSTRETCHBLT record carries a bitmap: without one it is exactly
/// `(function >> 8) + 3` words long (MS-WMF 2.3.1.2, 2.3.1.3).
fn has_bitmap(func: u16, r: Bytes) -> bool {
r.0.len() / 2 > usize::from(func >> 8) + 3
}
/// META_DIBBITBLT: rop(4), ySrc, xSrc, then height, width, yDest, xDest and the DIB at 22. The
/// bitmap-less form has a reserved word before the height, so its fields sit 2 bytes later; it, or
/// a source-less raster operation, fills the destination.
fn dib_bitblt(cv: &mut Canvas, r: Bytes) -> Option<()> {
let rop = r.u32(6)?;
if !has_bitmap(0x0940, r) {
cv.fill_rop([i16s(r, 22), i16s(r, 20), i16s(r, 18), i16s(r, 16)], rop);
return Some(());
}
let dst = [i16s(r, 20), i16s(r, 18), i16s(r, 16), i16s(r, 14)];
if Canvas::sourceless(rop) {
cv.fill_rop(dst, rop);
return Some(());
}
let src = [i16s(r, 12), i16s(r, 10), dst[2], dst[3]];
dib_blit(cv, r, 22, rop, dst, src)
}
/// META_DIBSTRETCHBLT: rop(4), srcH, srcW, ySrc, xSrc, then destH, destW, yDest, xDest and the DIB at
/// 26. The bitmap-less form has a reserved word after xSrc, so the destination sits 2 bytes later;
/// it, or a source-less raster operation, fills the destination.
fn dib_stretchblt(cv: &mut Canvas, r: Bytes) -> Option<()> {
let rop = r.u32(6)?;
if !has_bitmap(0x0B41, r) {
cv.fill_rop([i16s(r, 26), i16s(r, 24), i16s(r, 22), i16s(r, 20)], rop);
return Some(());
}
let dst = [i16s(r, 24), i16s(r, 22), i16s(r, 20), i16s(r, 18)];
if Canvas::sourceless(rop) {
cv.fill_rop(dst, rop);
return Some(());
}
let src = [i16s(r, 16), i16s(r, 14), i16s(r, 12), i16s(r, 10)];
dib_blit(cv, r, 26, rop, dst, src)
}
/// A DIB at `at`, stretched from source rectangle `src` into `dst` (both in logical units, pixels for `src`).
/// The raster operation `rop` decides how the pixels combine (see [`dib::prepare`]).
fn dib_blit(cv: &mut Canvas, r: Bytes, at: usize, rop: u32, dst: [f64; 4], src: [f64; 4]) -> Option<()> {
let dib = r.tail(at)?;
let n = dib::info_len(dib)?;
let info = dib.slice(0, n)?;
let bits = dib.tail(n)?;
let (mut img, dst) = dib::decode_clip(Bytes(info), bits, false, src.map(|v| v as i64), dst)?;
dib::prepare(&mut img, dib::Blend::Rop(rop));
cv.bitmap(dst, img);
Some(())
}
+292
View File
@@ -0,0 +1,292 @@
//! EMF and WMF writers (feature `write`).
//!
//! The writers take a list of drawing operations ([`Op`]) in paint order, in points with y down,
//! and the region of that space the picture covers: its top-left corner is the picture's origin
//! and its size the picture's size. What an application's document holds beyond these operations
//! (gradients, patterns, type, effects, transparency of paths) is turned into them by the
//! application.
//!
//! - **EMF** ([`emf`]): paths keep their Bézier curves; fills are solid brushes; strokes are
//! geometric pens with their width, cap, join, miter limit and dashes; clips are path clips
//! inside a saved device context; images keep their pixels and their transparency and may be
//! turned or skewed. The header's frame is the region in 0.01 mm.
//! - **WMF** ([`wmf`]): the same operations with curves flattened into polygons and 16-bit
//! coordinates, behind a placeable header that gives the size. WMF has no clip paths, no
//! transparency and no turned images; [`Wmf`] says which of these the picture lost.
//!
//! The reader reads back what the writers write: the tests round-trip both formats through
//! [`crate::parse`].
mod dib;
mod emf;
mod wmf;
use std::sync::Arc;
use kurbo::{Affine, BezPath, PathEl, Point, Rect};
use crate::{Cap, Join};
/// Most line segments one curve is flattened into, whatever its size and the tolerance.
const MAX_FLATTEN_PER_CURVE: f64 = 4096.0;
/// A curve whose control polygon is longer than this, or has a point further out, is written as
/// a line: kurbo squares and cubes coordinates when it sizes its work, which overflows there.
const MAX_FLATTEN_LENGTH: f64 = 1e60;
/// Most dashes one stroke is cut into when its pen cannot draw them (the rest of the path is
/// then left solid, so a tiny pattern on a long path cannot make an endless file).
const MAX_DASHES: usize = 100_000;
/// One drawing operation.
#[derive(Clone, Debug)]
pub enum Op {
/// A path filled with one colour.
Fill {
path: BezPath,
even_odd: bool,
rgb: [u8; 3],
},
/// A path stroked with a pen.
Stroke {
path: BezPath,
pen: Pen,
},
/// An image: `xf` maps its pixel space (`0..width`, `0..height`) into the picture space; its
/// alpha is multiplied by `opacity`.
Image {
image: Arc<Image>,
xf: Affine,
opacity: f32,
},
/// What follows, up to the matching [`Op::Unclip`], is clipped to `path` (inside the clips
/// around it). WMF has no clip paths and writes what follows whole.
Clip {
path: BezPath,
even_odd: bool,
},
Unclip,
}
/// A stroke as a pen draws it.
#[derive(Clone, Debug, PartialEq)]
pub struct Pen {
pub rgb: [u8; 3],
/// Points.
pub width: f64,
pub cap: Cap,
pub join: Join,
/// Longest miter as a multiple of the width (for [`Join::Miter`]).
pub miter: f64,
pub dash: Option<Dash>,
}
/// A dash pattern.
#[derive(Clone, Debug, PartialEq)]
pub struct Dash {
/// Dash, gap, dash, gap… in points. An odd count repeats to an even one.
pub pattern: Vec<f64>,
/// How far into the pattern the path starts.
pub offset: f64,
}
impl Dash {
/// Whether the pattern draws dashes: every length finite and not negative, and not all zero.
fn is_dashed(&self) -> bool {
self.offset.is_finite() && self.pattern.iter().all(|v| v.is_finite() && *v >= 0.0) && self.pattern.iter().any(|v| *v > 0.0)
}
/// Whether an EMF pen draws this pattern itself (`PS_USERSTYLE`): it starts at the path's
/// start, has at most 16 entries once made even, and every entry is at least one device unit
/// (0.01 mm). Other patterns are written as one line per dash, cut with [`kurbo::dash`]; a
/// caller that cuts dashes differently (aligned to corners, say) asks here and passes the
/// dashes as the path with no pattern.
pub fn emf_pen_draws(&self) -> bool {
emf::dash_entries(self).is_some()
}
}
/// Pixels with straight (not premultiplied) alpha, RGBA8, row by row from the top.
#[derive(Clone, Debug, PartialEq)]
pub struct Image {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
impl Image {
/// Every pixel fully opaque?
pub fn opaque(&self) -> bool {
self.rgba.as_chunks::<4>().0.iter().all(|p| p[3] == 255)
}
}
/// A written WMF and what the format could not hold.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Wmf {
pub bytes: Vec<u8>,
/// Images with transparency were written over white.
pub translucent_images: bool,
/// Turned or skewed images were written upright, filling their bounds.
pub turned_images: bool,
/// Clips were not applied: what they cover was written whole.
pub clips_ignored: bool,
/// Images with a side over 32,767 pixels were left out.
pub oversized_images: bool,
}
/// Writes `ops` over `region` as an EMF. The header's description names the application and the
/// picture (`app`, `title`). None when the region is empty or not finite.
pub fn emf(ops: &[Op], region: Rect, app: &str, title: &str) -> Option<Vec<u8>> {
Some(emf::write(ops, sane(region)?, app, title))
}
/// Writes `ops` over `region` as a placeable WMF. None when the region is empty or not finite.
pub fn wmf(ops: &[Op], region: Rect) -> Option<Wmf> {
Some(wmf::write(ops, sane(region)?))
}
/// `region` with its corners in order, when it is finite and has an area.
fn sane(region: Rect) -> Option<Rect> {
let r = region.abs();
([r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()) && r.width() > 0.0 && r.height() > 0.0).then_some(r)
}
/// `bp` cut into its dashes (as it is when `dash` draws none), for pens that can't draw them.
fn dashed(bp: &BezPath, dash: Option<&Dash>) -> BezPath {
let Some(d) = dash.filter(|d| d.is_dashed()) else { return bp.clone() };
let mut out = BezPath::new();
let mut dashes = 0usize;
for el in kurbo::dash(bp.iter(), d.offset, &d.pattern) {
dashes += usize::from(matches!(el, PathEl::MoveTo(_)));
if dashes > MAX_DASHES {
// Too many to write one by one: the stroke stays solid.
return bp.clone();
}
out.push(el);
}
out
}
/// `bp` flattened to within `tol`: each subpath's points and whether it closes.
fn flatten(bp: &BezPath, tol: f64) -> Vec<(Vec<Point>, bool)> {
let mut out: Vec<(Vec<Point>, bool)> = vec![];
let mut cur: Vec<Point> = vec![];
flatten_capped(bp.iter(), tol, |el| match el {
PathEl::MoveTo(p) => {
if cur.len() > 1 {
out.push((std::mem::take(&mut cur), false));
}
cur = vec![p];
}
PathEl::LineTo(p) => cur.push(p),
PathEl::ClosePath => {
if cur.len() > 1 {
out.push((std::mem::take(&mut cur), true));
}
cur.clear();
}
_ => {}
});
if cur.len() > 1 {
out.push((cur, false));
}
out
}
/// [`kurbo::flatten`], each curve's tolerance raised so that it makes at most about
/// [`MAX_FLATTEN_PER_CURVE`] segments. kurbo sizes its work and a buffer from the curve's
/// coordinates before producing anything, so a small curve with far-out handles asks for
/// gigabytes. The segments a curve needs grow with the square root of its control polygon's
/// length over the tolerance; that ratio is held under the square of the cap. A curve whose
/// points aren't finite is drawn as a line to its end.
fn flatten_capped(path: impl IntoIterator<Item = PathEl>, tolerance: f64, mut callback: impl FnMut(PathEl)) {
let mut last = None;
let mut start = None;
for el in path {
let (from, to, length) = match el {
PathEl::MoveTo(p) => {
(last, start) = (Some(p), Some(p));
callback(el);
continue;
}
PathEl::LineTo(p) => {
last = Some(p);
callback(el);
continue;
}
PathEl::ClosePath => {
last = start;
callback(el);
continue;
}
PathEl::QuadTo(a, b) => (last, b, last.map(|p| p.distance(a) + a.distance(b))),
PathEl::CurveTo(a, b, c) => (last, c, last.map(|p| p.distance(a) + a.distance(b) + b.distance(c))),
};
last = Some(to);
let (Some(from), Some(length)) = (from, length) else { continue };
// A short polygon far from the origin still overflows kurbo's `p * (mt * 2.0)` products.
let far = |p: Point| !(p.x.abs() <= MAX_FLATTEN_LENGTH && p.y.abs() <= MAX_FLATTEN_LENGTH);
let controls_far = match el {
PathEl::QuadTo(a, _) => far(a),
PathEl::CurveTo(a, b, _) => far(a) || far(b),
_ => false,
};
if length.is_nan() || length > MAX_FLATTEN_LENGTH || far(from) || far(to) || controls_far {
callback(PathEl::LineTo(to));
continue;
}
let tol = tolerance.max(length / (MAX_FLATTEN_PER_CURVE * MAX_FLATTEN_PER_CURVE));
kurbo::flatten([PathEl::MoveTo(from), el], tol, |e| {
if !matches!(e, PathEl::MoveTo(_)) {
callback(e);
}
});
}
}
/// Little-endian bytes being written.
#[derive(Default)]
struct Out(Vec<u8>);
impl Out {
fn len(&self) -> usize {
self.0.len()
}
fn u16(&mut self, v: u16) {
self.0.extend_from_slice(&v.to_le_bytes());
}
fn i16(&mut self, v: i16) {
self.0.extend_from_slice(&v.to_le_bytes());
}
fn u32(&mut self, v: u32) {
self.0.extend_from_slice(&v.to_le_bytes());
}
fn i32(&mut self, v: i32) {
self.0.extend_from_slice(&v.to_le_bytes());
}
fn f32(&mut self, v: f32) {
self.0.extend_from_slice(&v.to_le_bytes());
}
fn bytes(&mut self, b: &[u8]) {
self.0.extend_from_slice(b);
}
/// Zeros up to the next multiple of `n` bytes.
fn pad(&mut self, n: usize) {
while !self.0.len().is_multiple_of(n) {
self.0.push(0);
}
}
/// Overwrite the `u32` at `at` (a size or count known only later).
fn set_u32(&mut self, at: usize, v: u32) {
if let Some(b) = self.0.get_mut(at..at + 4) {
b.copy_from_slice(&v.to_le_bytes());
}
}
fn set_i32(&mut self, at: usize, v: i32) {
if let Some(b) = self.0.get_mut(at..at + 4) {
b.copy_from_slice(&v.to_le_bytes());
}
}
}
#[cfg(test)]
mod tests;
+62
View File
@@ -0,0 +1,62 @@
//! Bitmaps as the writers store them: bottom-up, uncompressed DIBs.
use super::Image;
const BI_RGB: u32 = 0;
/// A BITMAPINFOHEADER of a bottom-up, uncompressed `width` × `height` bitmap of `bits` per pixel.
fn header(width: u32, height: u32, bits: u16, size: usize) -> Vec<u8> {
let mut h = super::Out::default();
h.u32(40);
h.i32(i32::try_from(width).unwrap_or(i32::MAX));
h.i32(i32::try_from(height).unwrap_or(i32::MAX));
h.u16(1);
h.u16(bits);
h.u32(BI_RGB);
h.u32(u32::try_from(size).unwrap_or(0));
// 2835 pixels a metre: 72 ppi (the size on the page comes from the record).
h.i32(2835);
h.i32(2835);
h.u32(0);
h.u32(0);
h.0
}
/// The bytes of a row of `width` pixels of `bits` each, padded to 4 bytes.
fn stride(width: u32, bits: u32) -> Option<usize> {
let row_bits = u64::from(width).checked_mul(u64::from(bits))?;
usize::try_from(row_bits.div_ceil(32) * 4).ok()
}
/// `img` at `opacity`, composited over white, as a 24-bit DIB → (header, bits).
pub(crate) fn rgb24(img: &Image, opacity: f32) -> (Vec<u8>, Vec<u8>) {
let row = stride(img.width, 24).unwrap_or(0);
let mut bits = vec![0u8; row * img.height as usize];
let over = |c: u8, a: u32| ((u32::from(c) * a + 255 * (255 - a) + 127) / 255) as u8;
for (y, src) in img.rgba.chunks_exact((img.width as usize * 4).max(1)).enumerate() {
// Bottom-up: the first stored row is the image's last.
let Some(at) = (img.height as usize).checked_sub(y + 1).map(|r| r * row) else { continue };
let Some(dst) = bits.get_mut(at..at + img.width as usize * 3) else { continue };
for (d, p) in dst.as_chunks_mut::<3>().0.iter_mut().zip(src.as_chunks::<4>().0) {
let a = (f32::from(p[3]) * opacity.clamp(0.0, 1.0)).round() as u32;
d.copy_from_slice(&[over(p[2], a), over(p[1], a), over(p[0], a)]);
}
}
(header(img.width, img.height, 24, bits.len()), bits)
}
/// `img` at `opacity` as a 32-bit DIB of premultiplied BGRA → (header, bits).
pub(crate) fn bgra32(img: &Image, opacity: f32) -> (Vec<u8>, Vec<u8>) {
let row = img.width as usize * 4;
let mut bits = vec![0u8; row * img.height as usize];
for (y, src) in img.rgba.chunks_exact(row.max(1)).enumerate() {
let Some(at) = (img.height as usize).checked_sub(y + 1).map(|r| r * row) else { continue };
let Some(dst) = bits.get_mut(at..at + row) else { continue };
for (d, p) in dst.as_chunks_mut::<4>().0.iter_mut().zip(src.as_chunks::<4>().0) {
let a = (f32::from(p[3]) * opacity.clamp(0.0, 1.0)).round() as u32;
let pre = |c: u8| ((u32::from(c) * a + 127) / 255) as u8;
d.copy_from_slice(&[pre(p[2]), pre(p[1]), pre(p[0]), a as u8]);
}
}
(header(img.width, img.height, 32, bits.len()), bits)
}
+498
View File
@@ -0,0 +1,498 @@
//! The EMF writer: an EMR_HEADER whose frame is the region in 0.01 mm, then the drawing in device
//! units of 0.01 mm (the reference device has 100 pixels a millimetre) and an EMR_EOF.
//!
//! Each fill and stroke is a path bracket (move-to, poly-line-to and poly-Bézier-to runs, close
//! figure) filled with a solid brush or stroked with a geometric pen; brushes and pens are made
//! when the colour or pen changes and deleted when replaced. Clips are path clips inside a saved
//! DC; images are StretchDIBits (opaque) or AlphaBlend (with transparency) under a world transform
//! that places their pixels.
use kurbo::{Affine, BezPath, PathEl, Point, Rect};
use super::{Dash, Image, Op, Out, Pen, dib};
use crate::{Cap, Join};
// Record types (MS-EMF 2.1.1).
pub(crate) const HEADER: u32 = 1;
pub(crate) const POLYBEZIERTO: u32 = 5;
pub(crate) const POLYLINETO: u32 = 6;
pub(crate) const EOF: u32 = 14;
pub(crate) const SETBKMODE: u32 = 18;
pub(crate) const SETPOLYFILLMODE: u32 = 19;
pub(crate) const MOVETOEX: u32 = 27;
pub(crate) const SAVEDC: u32 = 33;
pub(crate) const RESTOREDC: u32 = 34;
pub(crate) const SETWORLDTRANSFORM: u32 = 35;
pub(crate) const MODIFYWORLDTRANSFORM: u32 = 36;
pub(crate) const SELECTOBJECT: u32 = 37;
pub(crate) const CREATEBRUSHINDIRECT: u32 = 39;
pub(crate) const DELETEOBJECT: u32 = 40;
pub(crate) const SETMITERLIMIT: u32 = 58;
pub(crate) const BEGINPATH: u32 = 59;
pub(crate) const ENDPATH: u32 = 60;
pub(crate) const CLOSEFIGURE: u32 = 61;
pub(crate) const FILLPATH: u32 = 62;
pub(crate) const STROKEPATH: u32 = 64;
pub(crate) const SELECTCLIPPATH: u32 = 67;
pub(crate) const STRETCHDIBITS: u32 = 81;
pub(crate) const EXTCREATEPEN: u32 = 95;
pub(crate) const ALPHABLEND: u32 = 114;
/// The `" EMF"` signature in the header.
pub(crate) const SIGNATURE: u32 = 0x464D_4520;
/// Stock objects (selected to release ours before deleting them).
const WHITE_BRUSH: u32 = 0x8000_0000;
const NULL_PEN: u32 = 0x8000_0008;
/// Our two object slots (0 is reserved).
const BRUSH: u32 = 1;
const PEN: u32 = 2;
pub(crate) const PS_GEOMETRIC: u32 = 0x0001_0000;
pub(crate) const PS_USERSTYLE: u32 = 7;
pub(crate) const PS_ENDCAP_SQUARE: u32 = 0x100;
pub(crate) const PS_ENDCAP_FLAT: u32 = 0x200;
pub(crate) const PS_JOIN_BEVEL: u32 = 0x1000;
pub(crate) const PS_JOIN_MITER: u32 = 0x2000;
/// Raster operation: copy the source.
pub(crate) const SRCCOPY: u32 = 0x00CC_0020;
/// Device units (0.01 mm) per point.
pub(crate) const UNITS_PER_PT: f64 = 2540.0 / 72.0;
/// Longest dash pattern a geometric pen takes (longer ones are drawn as separate dashes).
const MAX_STYLE: usize = 16;
/// Header size with both extensions.
const HEADER_SIZE: usize = 108;
/// Coordinates are kept within this (far beyond any page).
const MAX_COORD: f64 = 1e9;
/// A COLORREF (0x00bbggrr).
pub(crate) fn colorref(rgb: [u8; 3]) -> u32 {
u32::from_le_bytes([rgb[0], rgb[1], rgb[2], 0])
}
/// The style entries (device units) of a pen that draws `d` itself; None when the dashes must be
/// written as separate lines: a pen's dashes start at the path's start, every entry is at least a
/// unit long, and there are 16 entries at most.
pub(super) fn dash_entries(d: &Dash) -> Option<Vec<u32>> {
let mut v: Vec<f64> = d.pattern.clone();
if v.len() % 2 == 1 {
v.extend_from_within(..);
}
let e: Vec<u32> = v.iter().map(|x| (x * UNITS_PER_PT).round().clamp(0.0, f64::from(u32::MAX)) as u32).collect();
(d.offset.abs() < 1e-9 && e.len() <= MAX_STYLE && e.iter().all(|x| *x >= 1)).then_some(e)
}
/// What a pen object is made of, to tell when the next stroke needs another.
#[derive(Clone, PartialEq)]
struct PenKey {
style: u32,
width: u32,
color: u32,
entries: Vec<u32>,
}
/// A device-space bounding box (inclusive).
type Bounds = [i32; 4];
fn union(a: Option<Bounds>, b: Bounds) -> Bounds {
match a {
None => b,
Some(a) => [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])],
}
}
struct Writer {
out: Out,
records: u32,
/// Picture space → device units.
to_dev: Affine,
/// The frame in device units: what the region clip lets through.
frame: Bounds,
/// Everything drawn, in device units.
bounds: Option<Bounds>,
brush: Option<u32>,
pen: Option<PenKey>,
fill_mode: Option<u32>,
miter: Option<u32>,
}
/// Write `ops` as an EMF of `region`; the header's description names `app` and `title`.
pub(crate) fn write(ops: &[Op], region: Rect, app: &str, title: &str) -> Vec<u8> {
let k = UNITS_PER_PT;
let frame = [0, 0, (region.width() * k).round() as i32, (region.height() * k).round() as i32];
let mut w = Writer {
out: Out::default(),
records: 0,
to_dev: Affine::scale(k) * Affine::translate(-region.origin().to_vec2()),
frame,
bounds: None,
brush: None,
pen: None,
fill_mode: None,
miter: None,
};
w.header(app, title);
// Dash gaps and hatches show what is below.
w.record(SETBKMODE, |o| o.u32(1));
for op in ops {
w.op(op);
}
w.release();
w.record(EOF, |o| {
o.u32(0);
o.u32(16);
o.u32(20);
});
w.finish()
}
impl Writer {
/// Append one record: its type, size and `body`, padded to 4 bytes.
fn record(&mut self, kind: u32, body: impl FnOnce(&mut Out)) {
let start = self.out.len();
self.out.u32(kind);
self.out.u32(0);
body(&mut self.out);
self.out.pad(4);
let size = u32::try_from(self.out.len() - start).unwrap_or(u32::MAX);
self.out.set_u32(start + 4, size);
self.records += 1;
}
fn header(&mut self, app: &str, title: &str) {
// "Application\0Picture\0\0", each part cut to what a description may reasonably hold.
let part = |s: &str| s.chars().filter(|c| *c != '\0').take(200).collect::<String>();
let desc: Vec<u16> = part(app).encode_utf16().chain([0]).chain(part(title).encode_utf16()).chain([0, 0]).collect();
let f = self.frame;
// The reference device: 100 pixels a millimetre, as large as the frame.
let mm = [(f[2] / 100).max(1) + 1, (f[3] / 100).max(1) + 1];
self.record(HEADER, |o| {
// Bounds, patched in `finish`.
o.bytes(&[0; 16]);
for v in f {
o.i32(v);
}
o.u32(SIGNATURE);
o.u32(0x0001_0000);
// Bytes, records and handles, patched in `finish`.
o.u32(0);
o.u32(0);
o.u16(3);
o.u16(0);
o.u32(desc.len() as u32);
o.u32(HEADER_SIZE as u32);
o.u32(0);
o.i32(mm[0] * 100);
o.i32(mm[1] * 100);
o.i32(mm[0]);
o.i32(mm[1]);
// No pixel format, not OpenGL; the device in micrometres.
o.u32(0);
o.u32(0);
o.u32(0);
o.u32(mm[0] as u32 * 1000);
o.u32(mm[1] as u32 * 1000);
for c in &desc {
o.u16(*c);
}
});
}
/// Patch the header's bounds, size and record count.
fn finish(mut self) -> Vec<u8> {
let b = self.bounds.map(|b| [b[0].max(self.frame[0]), b[1].max(self.frame[1]), b[2].min(self.frame[2]), b[3].min(self.frame[3])]);
// An empty picture has bounds (0, 0, -1, -1).
let b = b.filter(|b| b[0] <= b[2] && b[1] <= b[3]).unwrap_or([0, 0, -1, -1]);
for (i, v) in b.iter().enumerate() {
self.out.set_i32(8 + 4 * i, *v);
}
let size = u32::try_from(self.out.len()).unwrap_or(u32::MAX);
self.out.set_u32(48, size);
self.out.set_u32(52, self.records);
self.out.0
}
fn dev(&self, p: Point) -> [i32; 2] {
let q = self.to_dev * p;
let c = |v: f64| if v.is_finite() { v.round().clamp(-MAX_COORD, MAX_COORD) as i32 } else { 0 };
[c(q.x), c(q.y)]
}
fn grow(&mut self, b: Bounds) {
self.bounds = Some(union(self.bounds, b));
}
fn op(&mut self, op: &Op) {
match op {
Op::Fill { path, even_odd, rgb } => {
self.fill_mode(*even_odd);
self.brush(*rgb);
if let Some(b) = self.path(path) {
self.grow(b);
self.record(FILLPATH, |o| b.iter().for_each(|v| o.i32(*v)));
}
}
Op::Stroke { path, pen } => {
let dashed;
let path = if self.pen(pen) {
path
} else {
dashed = super::dashed(path, pen.dash.as_ref());
&dashed
};
if let Some(b) = self.path(path) {
let half = (pen.width * UNITS_PER_PT / 2.0).ceil() as i32;
let b = [b[0] - half, b[1] - half, b[2] + half, b[3] + half];
self.grow(b);
self.record(STROKEPATH, |o| b.iter().for_each(|v| o.i32(*v)));
}
}
Op::Image { image, xf, opacity } => self.image(image, *xf, *opacity),
Op::Clip { path, even_odd } => {
self.release();
self.record(SAVEDC, |_| {});
self.fill_mode(*even_odd);
if self.path(path).is_some() {
// RGN_AND: inside the clip already set.
self.record(SELECTCLIPPATH, |o| o.u32(1));
}
}
Op::Unclip => {
self.release();
self.record(RESTOREDC, |o| o.i32(-1));
// The DC's fill mode and miter limit are back to what they were at the save.
self.fill_mode = None;
self.miter = None;
}
}
}
fn fill_mode(&mut self, even_odd: bool) {
// ALTERNATE (even-odd) or WINDING (non-zero).
let mode = if even_odd { 1 } else { 2 };
if self.fill_mode != Some(mode) {
self.record(SETPOLYFILLMODE, |o| o.u32(mode));
self.fill_mode = Some(mode);
}
}
/// Select a solid brush of `rgb`, made when the colour changes.
fn brush(&mut self, rgb: [u8; 3]) {
let color = colorref(rgb);
if self.brush == Some(color) {
return;
}
self.drop_brush();
self.record(CREATEBRUSHINDIRECT, |o| {
o.u32(BRUSH);
// BS_SOLID, the colour, no hatch.
o.u32(0);
o.u32(color);
o.u32(0);
});
self.record(SELECTOBJECT, |o| o.u32(BRUSH));
self.brush = Some(color);
}
fn drop_brush(&mut self) {
if self.brush.take().is_some() {
self.record(SELECTOBJECT, |o| o.u32(WHITE_BRUSH));
self.record(DELETEOBJECT, |o| o.u32(BRUSH));
}
}
fn drop_pen(&mut self) {
if self.pen.take().is_some() {
self.record(SELECTOBJECT, |o| o.u32(NULL_PEN));
self.record(DELETEOBJECT, |o| o.u32(PEN));
}
}
/// Release our objects (before a DC is saved or restored, which would bring back deleted ones).
fn release(&mut self) {
self.drop_brush();
self.drop_pen();
}
/// Select a geometric pen for `p`, made when it changes. False when `p`'s dashes must be
/// drawn as separate lines (the pen selected is solid): a pen's dashes start at the path's
/// start, every entry at least a unit long, 16 entries at most.
fn pen(&mut self, p: &Pen) -> bool {
let k = UNITS_PER_PT;
let entries = p.dash.as_ref().map(dash_entries);
let native = !matches!(entries, Some(None));
let entries = entries.flatten().unwrap_or_default();
let cap = match p.cap {
Cap::Flat => PS_ENDCAP_FLAT,
Cap::Round => 0,
Cap::Square => PS_ENDCAP_SQUARE,
};
let join = match p.join {
Join::Miter => PS_JOIN_MITER,
Join::Round => 0,
Join::Bevel => PS_JOIN_BEVEL,
};
let style = PS_GEOMETRIC | cap | join | if entries.is_empty() { 0 } else { PS_USERSTYLE };
let key = PenKey { style, width: (p.width * k).round().clamp(1.0, 1e9) as u32, color: colorref(p.rgb), entries };
if p.join == Join::Miter {
let m = p.miter.round().clamp(1.0, 1000.0) as u32;
if self.miter != Some(m) {
self.record(SETMITERLIMIT, |o| o.u32(m));
self.miter = Some(m);
}
}
if self.pen.as_ref() != Some(&key) {
self.drop_pen();
let n = key.entries.len() as u32;
self.record(EXTCREATEPEN, |o| {
o.u32(PEN);
// No pattern bitmap: its offsets point past the record, its sizes are 0.
let end = 52 + 4 * n;
o.u32(end);
o.u32(0);
o.u32(end);
o.u32(0);
o.u32(key.style);
o.u32(key.width);
// BS_SOLID, the colour, no hatch.
o.u32(0);
o.u32(key.color);
o.u32(0);
o.u32(n);
key.entries.iter().for_each(|e| o.u32(*e));
});
self.record(SELECTOBJECT, |o| o.u32(PEN));
self.pen = Some(key);
}
native
}
/// `bp` as a path bracket → its device bounds (`None`: nothing to draw).
fn path(&mut self, bp: &BezPath) -> Option<Bounds> {
if !bp.elements().iter().any(|e| !matches!(e, PathEl::MoveTo(_) | PathEl::ClosePath)) {
return None;
}
self.record(BEGINPATH, |_| {});
let mut bounds: Option<Bounds> = None;
let mut run: Vec<[i32; 2]> = vec![];
let mut curves = false;
let mut last = Point::ZERO;
let mut start = Point::ZERO;
for el in bp.elements() {
let (pts, is_curve): (Vec<Point>, bool) = match *el {
PathEl::MoveTo(p) => {
self.flush_run(&mut run, curves);
let d = self.dev(p);
bounds = Some(union(bounds, [d[0], d[1], d[0], d[1]]));
self.record(MOVETOEX, |o| d.iter().for_each(|v| o.i32(*v)));
(last, start) = (p, p);
continue;
}
PathEl::ClosePath => {
self.flush_run(&mut run, curves);
self.record(CLOSEFIGURE, |_| {});
last = start;
continue;
}
PathEl::LineTo(p) => (vec![p], false),
PathEl::QuadTo(c, p) => (vec![last + (c - last) * (2.0 / 3.0), p + (c - p) * (2.0 / 3.0), p], true),
PathEl::CurveTo(a, b, p) => (vec![a, b, p], true),
};
if is_curve != curves {
self.flush_run(&mut run, curves);
curves = is_curve;
}
for p in &pts {
let d = self.dev(*p);
bounds = Some(union(bounds, [d[0], d[1], d[0], d[1]]));
run.push(d);
}
last = pts.last().copied().unwrap_or(last);
}
self.flush_run(&mut run, curves);
self.record(ENDPATH, |_| {});
bounds
}
/// Write the points of a run of lines or curves, then empty it.
fn flush_run(&mut self, run: &mut Vec<[i32; 2]>, curves: bool) {
if run.is_empty() {
return;
}
let pts = std::mem::take(run);
let b = pts.iter().fold(None, |acc, p| Some(union(acc, [p[0], p[1], p[0], p[1]]))).unwrap_or([0, 0, -1, -1]);
self.record(if curves { POLYBEZIERTO } else { POLYLINETO }, |o| {
b.iter().for_each(|v| o.i32(*v));
o.u32(pts.len() as u32);
for p in &pts {
o.i32(p[0]);
o.i32(p[1]);
}
});
}
/// An image placed by `xf` (its pixels → picture space): StretchDIBits when opaque, else AlphaBlend,
/// drawn at its pixel size under a world transform.
fn image(&mut self, img: &Image, xf: Affine, opacity: f32) {
if img.width == 0 || img.height == 0 || opacity <= 0.0 {
return;
}
let m = self.to_dev * xf;
if !m.as_coeffs().iter().all(|v| v.is_finite()) || m.determinant().abs() < 1e-12 {
return;
}
let (w, h) = (f64::from(img.width), f64::from(img.height));
let corners = [Point::ZERO, Point::new(w, 0.0), Point::new(0.0, h), Point::new(w, h)].map(|p| self.dev(xf * p));
let b = corners.iter().fold(None, |acc, p| Some(union(acc, [p[0], p[1], p[0], p[1]]))).unwrap_or([0, 0, -1, -1]);
self.grow(b);
let [a, bb, c, d, e, f] = m.as_coeffs();
self.record(SETWORLDTRANSFORM, |o| [a, bb, c, d, e, f].iter().for_each(|v| o.f32(*v as f32)));
let (iw, ih) = (img.width as i32, img.height as i32);
if img.opaque() && opacity >= 0.998 {
let (bmi, bits) = dib::rgb24(img, 1.0);
self.record(STRETCHDIBITS, |o| {
b.iter().for_each(|v| o.i32(*v));
// Destination and source rectangles.
[0, 0, 0, 0, iw, ih].iter().for_each(|v| o.i32(*v));
o.u32(80);
o.u32(bmi.len() as u32);
o.u32(80 + bmi.len() as u32);
o.u32(bits.len() as u32);
// DIB_RGB_COLORS, SRCCOPY, the destination size.
o.u32(0);
o.u32(SRCCOPY);
o.i32(iw);
o.i32(ih);
o.bytes(&bmi);
o.bytes(&bits);
});
} else {
let (bmi, bits) = dib::bgra32(img, opacity);
self.record(ALPHABLEND, |o| {
b.iter().for_each(|v| o.i32(*v));
[0, 0, iw, ih].iter().for_each(|v| o.i32(*v));
// AC_SRC_OVER, no flags, constant alpha 255, AC_SRC_ALPHA (per-pixel alpha).
o.bytes(&[0, 0, 255, 1]);
o.i32(0);
o.i32(0);
// The source's own transform: identity.
[1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0].iter().for_each(|v| o.f32(*v));
o.u32(0);
o.u32(0);
o.u32(108);
o.u32(bmi.len() as u32);
o.u32(108 + bmi.len() as u32);
o.u32(bits.len() as u32);
o.i32(iw);
o.i32(ih);
o.bytes(&bmi);
o.bytes(&bits);
});
}
// MWT_IDENTITY: back to drawing in device units.
self.record(MODIFYWORLDTRANSFORM, |o| {
[1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0].iter().for_each(|v| o.f32(*v));
o.u32(1);
});
}
}
+527
View File
@@ -0,0 +1,527 @@
//! The writers' output record by record, and round trips through the reader.
use std::sync::Arc;
use kurbo::{Affine, BezPath, Ellipse, Point, Rect, Shape};
use super::*;
use crate::{Item, Kind, Seg, parse, sniff};
const RED: [u8; 3] = [255, 0, 0];
const BLUE: [u8; 3] = [0, 0, 255];
fn u32_at(b: &[u8], at: usize) -> u32 {
u32::from_le_bytes(b[at..at + 4].try_into().unwrap())
}
fn i32_at(b: &[u8], at: usize) -> i32 {
i32::from_le_bytes(b[at..at + 4].try_into().unwrap())
}
fn u16_at(b: &[u8], at: usize) -> u16 {
u16::from_le_bytes(b[at..at + 2].try_into().unwrap())
}
/// The EMF's records: (type, the whole record).
fn emf_records(b: &[u8]) -> Vec<(u32, &[u8])> {
let mut out = vec![];
let mut at = 0;
while at < b.len() {
let size = u32_at(b, at + 4) as usize;
assert!(size >= 8 && size.is_multiple_of(4), "record size {size} at {at}");
out.push((u32_at(b, at), &b[at..at + size]));
at += size;
}
assert_eq!(at, b.len(), "records end at the end of the file");
out
}
/// The WMF's records after both headers: (function, the whole record).
fn wmf_records(b: &[u8]) -> Vec<(u16, &[u8])> {
let mut out = vec![];
let mut at = 22 + 18;
while at < b.len() {
let words = u32_at(b, at) as usize;
assert!(words >= 3, "record of {words} words at {at}");
out.push((u16_at(b, at + 4), &b[at..at + words * 2]));
at += words * 2;
}
assert_eq!(at, b.len());
out
}
fn kinds(recs: &[(u32, &[u8])]) -> Vec<u32> {
recs.iter().map(|r| r.0).collect()
}
fn count(k: &[u32], kind: u32) -> usize {
k.iter().filter(|x| **x == kind).count()
}
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> BezPath {
Rect::new(x0, y0, x1, y1).to_path(0.1)
}
fn ellipse(x0: f64, y0: f64, x1: f64, y1: f64) -> BezPath {
let mut p = Ellipse::from_rect(Rect::new(x0, y0, x1, y1)).to_path(0.01);
if !matches!(p.elements().last(), Some(PathEl::ClosePath)) {
p.close_path();
}
p
}
fn line(a: (f64, f64), b: (f64, f64)) -> BezPath {
let mut p = BezPath::new();
p.move_to(a);
p.line_to(b);
p
}
fn fill(path: BezPath, rgb: [u8; 3]) -> Op {
Op::Fill { path, even_odd: false, rgb }
}
fn pen(rgb: [u8; 3], width: f64) -> Pen {
Pen { rgb, width, cap: Cap::Round, join: Join::Round, miter: 10.0, dash: None }
}
fn stroke(path: BezPath, pen: Pen) -> Op {
Op::Stroke { path, pen }
}
fn image(width: u32, height: u32, px: [u8; 4]) -> Arc<Image> {
Arc::new(Image { width, height, rgba: px.repeat((width * height) as usize) })
}
fn page(w: f64, h: f64) -> Rect {
Rect::new(0.0, 0.0, w, h)
}
fn write_emf(ops: &[Op], w: f64, h: f64) -> Vec<u8> {
emf(ops, page(w, h), "Craft", "Picture").unwrap()
}
/// Red, green, blue; then half-transparent black, transparent, white.
fn sample() -> Image {
Image { width: 3, height: 2, rgba: vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 128, 9, 9, 9, 0, 255, 255, 255, 255] }
}
#[test]
fn emf_header_frame_is_the_region_in_hundredths_of_a_millimetre() {
// 72 x 144 pt is 25.4 x 50.8 mm; a rectangle from (36, 36) to (54, 72) pt.
let b = &write_emf(&[fill(rect(36.0, 36.0, 54.0, 72.0), RED)], 72.0, 144.0);
assert_eq!(sniff(b), Some(Kind::Emf));
assert_eq!(u32_at(b, 0), 1);
assert_eq!(u32_at(b, 40), 0x464D_4520, "the \" EMF\" signature");
// rclFrame.
assert_eq!([i32_at(b, 24), i32_at(b, 28), i32_at(b, 32), i32_at(b, 36)], [0, 0, 2540, 5080]);
// rclBounds, in device units of 0.01 mm: the rectangle.
assert_eq!([i32_at(b, 8), i32_at(b, 12), i32_at(b, 16), i32_at(b, 20)], [1270, 1270, 1905, 2540]);
// The reference device has 100 pixels a millimetre.
let (dev, mm) = ((i32_at(b, 72), i32_at(b, 76)), (i32_at(b, 80), i32_at(b, 84)));
assert_eq!(dev, (mm.0 * 100, mm.1 * 100));
assert_eq!((u32_at(b, 100), u32_at(b, 104)), (mm.0 as u32 * 1000, mm.1 as u32 * 1000), "micrometres");
// The description names the application and the picture.
let n = u32_at(b, 60) as usize;
let off = u32_at(b, 64) as usize;
assert_eq!(off, 108);
let desc: Vec<u16> = (0..n).map(|i| u16_at(b, off + 2 * i)).collect();
assert_eq!(String::from_utf16_lossy(&desc), "Craft\0Picture\0\0");
// An empty picture has empty bounds.
let empty = write_emf(&[], 72.0, 72.0);
assert_eq!([i32_at(&empty, 8), i32_at(&empty, 16)], [0, -1]);
// The region's origin is the picture's.
let moved = emf(&[fill(rect(136.0, 36.0, 154.0, 72.0), RED)], Rect::new(100.0, 0.0, 172.0, 144.0), "", "").unwrap();
assert_eq!([i32_at(&moved, 8), i32_at(&moved, 16)], [1270, 1905]);
}
#[test]
fn emf_record_sizes_add_up_to_the_header_totals() {
let ops = [
Op::Image { image: image(3, 2, [0, 128, 0, 255]), xf: Affine::translate((10.0, 10.0)) * Affine::scale(10.0), opacity: 1.0 },
fill(ellipse(50.0, 10.0, 150.0, 90.0), RED),
stroke(ellipse(50.0, 10.0, 150.0, 90.0), pen(BLUE, 2.0)),
];
let b = write_emf(&ops, 200.0, 100.0);
let recs = emf_records(&b);
assert_eq!(u32_at(&b, 48) as usize, b.len(), "nBytes");
assert_eq!(u32_at(&b, 52) as usize, recs.len(), "nRecords");
assert_eq!(u16_at(&b, 56), 3, "nHandles: the reserved slot, a brush and a pen");
let (last, eof) = recs.last().copied().unwrap();
assert_eq!((last, eof.len()), (14, 20), "EMR_EOF ends the file");
assert_eq!(u32_at(eof, 16), 20, "nSizeLast");
// Fixed-size records have their fixed sizes.
for (k, r) in &recs {
let fixed = match k {
18 | 19 | 34 | 37 | 40 | 58 | 67 => Some(12),
27 | 54 => Some(16),
33 | 59 | 60 | 61 => Some(8),
39 | 62 | 64 => Some(24),
35 => Some(32),
36 => Some(36),
_ => None,
};
if let Some(f) = fixed {
assert_eq!(r.len(), f, "record {k}");
}
}
// StretchDIBits: its bitmap where its offsets say, 3 x 2 pixels of 24 bits (rows of 12 bytes).
let sdib = recs.iter().find(|r| r.0 == 81).unwrap().1;
assert_eq!([u32_at(sdib, 48), u32_at(sdib, 52), u32_at(sdib, 56), u32_at(sdib, 60)], [80, 40, 120, 24]);
assert_eq!(sdib.len(), 144);
}
#[test]
fn emf_paths_keep_their_curves_brushes_and_pens() {
let e = ellipse(10.0, 10.0, 60.0, 40.0);
let p = Pen { join: Join::Bevel, ..pen(BLUE, 4.0) };
let b = write_emf(&[fill(e.clone(), RED), stroke(e, p)], 100.0, 100.0);
let recs = emf_records(&b);
let k = kinds(&recs);
assert!(k.contains(&5), "EMR_POLYBEZIERTO keeps the curves");
assert!(k.contains(&62) && k.contains(&64), "filled, then stroked");
let brush = recs.iter().find(|r| r.0 == 39).unwrap().1;
assert_eq!(u32_at(brush, 16), 0x0000_00ff, "a red COLORREF");
let pen_rec = recs.iter().find(|r| r.0 == 95).unwrap().1;
let style = u32_at(pen_rec, 28);
assert_eq!(style & 0x000f_0000, 0x0001_0000, "geometric");
assert_eq!(style & 0xf00, 0, "round caps");
assert_eq!(style & 0xf000, 0x1000, "bevel joins");
assert_eq!(u32_at(pen_rec, 32), (4.0 * 2540.0 / 72.0f64).round() as u32, "4 pt in 0.01 mm");
assert_eq!(u32_at(pen_rec, 40), 0x00ff_0000, "a blue COLORREF");
assert!(recs.iter().any(|r| r.0 == 19 && u32_at(r.1, 8) == 2), "WINDING for non-zero");
// One brush and one pen for two shapes of the same colours.
let two = [
fill(rect(0.0, 0.0, 10.0, 10.0), RED),
stroke(rect(0.0, 0.0, 10.0, 10.0), pen(BLUE, 1.0)),
fill(rect(20.0, 0.0, 30.0, 10.0), RED),
stroke(rect(20.0, 0.0, 30.0, 10.0), pen(BLUE, 1.0)),
];
let k = kinds(&emf_records(&write_emf(&two, 100.0, 100.0)));
assert_eq!((count(&k, 39), count(&k, 95)), (1, 1));
// A miter join sets the miter limit.
let miter = Pen { join: Join::Miter, miter: 4.0, ..pen(BLUE, 1.0) };
let recs_bytes = write_emf(&[stroke(rect(0.0, 0.0, 10.0, 10.0), miter)], 100.0, 100.0);
assert!(emf_records(&recs_bytes).iter().any(|r| r.0 == 58 && u32_at(r.1, 8) == 4));
}
#[test]
fn emf_dashes_are_pen_styles() {
let l = line((10.0, 10.0), (90.0, 10.0));
let dashed = |dash: Dash| write_emf(&[stroke(l.clone(), Pen { dash: Some(dash), ..pen(BLUE, 2.0) })], 100.0, 100.0);
let native = Dash { pattern: vec![6.0, 3.0], offset: 0.0 };
assert!(native.emf_pen_draws());
let b = dashed(native);
let recs = emf_records(&b);
let pen_rec = recs.iter().find(|r| r.0 == 95).unwrap().1;
assert_eq!(u32_at(pen_rec, 28) & 0xf, 7, "PS_USERSTYLE");
assert_eq!(u32_at(pen_rec, 48), 2);
assert_eq!([u32_at(pen_rec, 52), u32_at(pen_rec, 56)], [(6.0 * 2540.0 / 72.0f64).round() as u32, (3.0 * 2540.0 / 72.0f64).round() as u32]);
// With an offset, the dashes are separate lines drawn with a solid pen.
let offset = Dash { pattern: vec![6.0, 3.0], offset: 2.0 };
assert!(!offset.emf_pen_draws());
let b = dashed(offset);
let recs = emf_records(&b);
assert_eq!(u32_at(recs.iter().find(|r| r.0 == 95).unwrap().1, 28) & 0xf, 0);
assert!(count(&kinds(&recs), 27) > 5, "a move-to for each dash");
// An odd pattern repeats to an even one; more than 16 entries, or one under a device unit,
// are not a pen's.
assert!(Dash { pattern: vec![4.0; 8], offset: 0.0 }.emf_pen_draws());
assert!(!Dash { pattern: vec![4.0; 9], offset: 0.0 }.emf_pen_draws());
assert!(!Dash { pattern: vec![4.0, 0.001], offset: 0.0 }.emf_pen_draws());
}
#[test]
fn emf_clips_and_images() {
let ops = [
Op::Clip { path: rect(10.0, 10.0, 60.0, 30.0), even_odd: true },
fill(rect(0.0, 0.0, 100.0, 100.0), RED),
Op::Image { image: image(4, 4, [10, 20, 30, 255]), xf: Affine::translate((0.0, 50.0)), opacity: 1.0 },
Op::Unclip,
Op::Image { image: image(4, 4, [10, 20, 30, 100]), xf: Affine::translate((50.0, 50.0)) * Affine::rotate(0.3), opacity: 1.0 },
];
let b = write_emf(&ops, 100.0, 100.0);
let recs = emf_records(&b);
let k = kinds(&recs);
assert_eq!(count(&k, 67), 1);
assert!(recs.iter().any(|r| r.0 == 67 && u32_at(r.1, 8) == 1), "RGN_AND: inside the clips around it");
assert_eq!(count(&k, 33), count(&k, 34), "every save is restored");
// The opaque image is StretchDIBits; the translucent one AlphaBlend with per-pixel alpha.
assert_eq!(count(&k, 81), 1);
let ab = recs.iter().find(|r| r.0 == 114).unwrap().1;
assert_eq!(&ab[40..44], &[0, 0, 255, 1]);
// Images are placed by a world transform, reset after each.
assert_eq!((count(&k, 35), count(&k, 36)), (2, 2));
}
#[test]
fn wmf_placeable_header_checksum_and_record_sizes() {
let e = ellipse(10.0, 10.0, 60.0, 40.0);
let out = wmf(&[fill(e.clone(), RED), stroke(e, pen(BLUE, 1.0))], page(72.0, 144.0)).unwrap();
let b = &out.bytes;
assert_eq!(sniff(b), Some(Kind::Wmf));
assert_eq!(u32_at(b, 0), 0x9AC6_CDD7);
// The box in logical units at 1440 an inch: 1 x 2 inches.
assert_eq!([u16_at(b, 6), u16_at(b, 8), u16_at(b, 10), u16_at(b, 12)], [0, 0, 1440, 2880]);
assert_eq!(u16_at(b, 14), 1440);
let xor = (0..10).fold(0u16, |a, i| a ^ u16_at(b, 2 * i));
assert_eq!(u16_at(b, 20), xor, "the checksum");
// The WMF header: a memory metafile of 9 words, its size in words, the largest record.
assert_eq!([u16_at(b, 22), u16_at(b, 24), u16_at(b, 26)], [1, 9, 0x0300]);
assert_eq!(u32_at(b, 28) as usize * 2, b.len() - 22);
let recs = wmf_records(b);
let largest = recs.iter().map(|r| r.1.len() / 2).max().unwrap();
assert_eq!(u32_at(b, 34) as usize, largest, "MaxRecord");
assert_eq!(recs.last().map(|r| (r.0, r.1.len())), Some((0, 6)), "META_EOF ends the file");
// Objects: the null pen and brush, the brush, the pen.
assert_eq!(u16_at(b, 32), 4);
// The curve is flattened into a polygon, its outline a polygon drawn with the null brush.
let poly = recs.iter().find(|r| r.0 == 0x0538).unwrap().1;
assert_eq!(u16_at(poly, 6), 1);
assert!(u16_at(poly, 8) > 16, "many points");
assert!(recs.iter().any(|r| r.0 == 0x0324));
assert_eq!(out, Wmf { bytes: out.bytes.clone(), ..Wmf::default() }, "nothing was lost");
}
#[test]
fn wmf_sizes_fit_sixteen_bits() {
assert_eq!(super::wmf::units_per_inch(612.0, 792.0), 1440);
let inch = super::wmf::units_per_inch(3000.0, 1000.0);
assert!(inch < 1440 && f64::from(inch) * 3000.0 / 72.0 <= 32767.0);
let b = wmf(&[fill(rect(0.0, 0.0, 3000.0, 1000.0), RED)], page(3000.0, 1000.0)).unwrap().bytes;
assert_eq!(u16_at(&b, 14), inch);
assert!(u16_at(&b, 10) <= 32767);
}
#[test]
fn wmf_reports_what_it_leaves_out() {
let clipped = [Op::Clip { path: rect(0.0, 0.0, 20.0, 20.0), even_odd: false }, fill(rect(5.0, 5.0, 40.0, 40.0), RED), Op::Unclip];
let out = wmf(&clipped, page(100.0, 100.0)).unwrap();
assert!(out.clips_ignored && !out.translucent_images && !out.turned_images && !out.oversized_images);
// The art the clip covers is written whole.
assert!(wmf_records(&out.bytes).iter().any(|r| r.0 == 0x0538));
let turned = Op::Image { image: image(2, 2, [0, 0, 0, 10]), xf: Affine::translate((50.0, 50.0)) * Affine::rotate(0.5), opacity: 1.0 };
let out = wmf(&[turned], page(100.0, 100.0)).unwrap();
assert!(out.translucent_images && out.turned_images && !out.clips_ignored);
let faded = Op::Image { image: image(2, 2, [0, 0, 0, 255]), xf: Affine::IDENTITY, opacity: 0.5 };
assert!(wmf(&[faded], page(100.0, 100.0)).unwrap().translucent_images);
// An image whose side does not fit the record's 16-bit sizes is left out.
let wide = Op::Image { image: image(40_000, 1, [0, 0, 0, 255]), xf: Affine::IDENTITY, opacity: 1.0 };
let out = wmf(&[wide], page(100.0, 100.0)).unwrap();
assert!(out.oversized_images);
assert!(!wmf_records(&out.bytes).iter().any(|r| r.0 == 0x0F43));
}
#[test]
fn an_empty_or_unbounded_region_is_refused() {
for r in [Rect::new(0.0, 0.0, 0.0, 10.0), Rect::new(0.0, 0.0, f64::NAN, 10.0), Rect::new(0.0, 0.0, f64::INFINITY, 10.0)] {
assert!(emf(&[], r, "", "").is_none());
assert!(wmf(&[], r).is_none());
}
// Corners in either order are one region.
assert_eq!(emf(&[], Rect::new(72.0, 72.0, 0.0, 0.0), "", ""), emf(&[], page(72.0, 72.0), "", ""));
}
#[test]
fn rgb24_is_bottom_up_bgr_over_white() {
let (bmi, bits) = dib::rgb24(&sample(), 1.0);
assert_eq!(bmi.len(), 40);
assert_eq!(&bmi[14..16], &[24, 0], "24 bits a pixel");
// Rows of 9 bytes padded to 12, the last row first: half-transparent black over white is
// mid grey, transparent is white.
assert_eq!(bits, vec![127, 127, 127, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0]);
}
#[test]
fn bgra32_is_premultiplied() {
let (bmi, bits) = dib::bgra32(&sample(), 1.0);
assert_eq!(&bmi[14..16], &[32, 0]);
assert_eq!(&bits[..12], &[0, 0, 0, 128, 0, 0, 0, 0, 255, 255, 255, 255]);
assert_eq!(&bits[12..16], &[0, 0, 255, 255], "red, as BGRA");
// At half opacity.
let (_, half) = dib::bgra32(&sample(), 0.5);
assert_eq!(&half[12..16], &[0, 0, 128, 128]);
}
#[test]
fn written_dibs_decode_with_the_reader() {
let decode =
|(bmi, bits): (Vec<u8>, Vec<u8>), alpha: bool| crate::dib::decode(crate::bytes::Bytes(&bmi), crate::bytes::Bytes(&bits), alpha).unwrap();
let back = decode(dib::rgb24(&sample(), 1.0), false);
assert_eq!((back.w, back.h), (3, 2));
assert_eq!(&back.px[..12], &[255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]);
assert_eq!(&back.px[12..20], &[127, 127, 127, 255, 255, 255, 255, 255], "over white");
let back = decode(dib::bgra32(&sample(), 1.0), true);
assert_eq!(&back.px[..12], &sample().rgba[..12]);
assert_eq!((back.px[15], back.px[19]), (128, 0), "the alpha is kept");
// A short pixel buffer writes the rows it has and no more.
let short = Image { width: 3, height: 2, rgba: vec![255; 12] };
assert_eq!(dib::rgb24(&short, 1.0).1.len(), 24);
}
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
/// The bounding box of a path item's points.
fn bounds(segs: &[Seg]) -> Rect {
let mut pts = Vec::new();
for s in segs {
match *s {
Seg::Move(x, y) | Seg::Line(x, y) => pts.push(Point::new(f64::from(x), f64::from(y))),
Seg::Cubic(a, b, c, d, x, y) => pts.extend([(a, b), (c, d), (x, y)].map(|(x, y)| Point::new(f64::from(x), f64::from(y)))),
Seg::Close => {}
}
}
pts.iter().skip(1).fold(Rect::from_points(pts[0], pts[0]), |r, p| r.union_pt(*p))
}
fn rect_near(a: Rect, b: Rect, tol: f64) -> bool {
close(a.x0, b.x0, tol) && close(a.y0, b.y0, tol) && close(a.x1, b.x1, tol) && close(a.y1, b.y1, tol)
}
#[test]
fn emf_round_trips_through_the_reader() {
let turned = Affine::translate((120.0, 50.0)) * Affine::rotate(0.3) * Affine::scale(5.0);
let ops = [
Op::Fill { path: ellipse(10.0, 10.0, 60.0, 40.0), even_odd: true, rgb: RED },
stroke(
line((10.0, 60.0), (90.0, 60.0)),
Pen { rgb: BLUE, width: 2.0, cap: Cap::Flat, join: Join::Miter, miter: 4.0, dash: Some(Dash { pattern: vec![6.0, 3.0], offset: 0.0 }) },
),
Op::Clip { path: rect(100.0, 10.0, 150.0, 30.0), even_odd: false },
fill(rect(90.0, 0.0, 160.0, 40.0), [0, 255, 0]),
Op::Clip { path: rect(110.0, 0.0, 140.0, 40.0), even_odd: true },
fill(rect(90.0, 0.0, 160.0, 40.0), BLUE),
Op::Unclip,
Op::Unclip,
Op::Image { image: image(4, 2, [200, 100, 50, 128]), xf: turned, opacity: 1.0 },
Op::Image { image: image(3, 2, [1, 2, 3, 255]), xf: Affine::translate((10.0, 70.0)) * Affine::scale(4.0), opacity: 1.0 },
fill(rect(0.0, 90.0, 5.0, 95.0), RED),
];
let p = parse(&write_emf(&ops, 200.0, 100.0)).unwrap();
assert!(close(f64::from(p.width), 200.0, 0.02) && close(f64::from(p.height), 100.0, 0.02), "{} x {}", p.width, p.height);
assert_eq!(p.skipped_records, 0, "the reader knows every record the writer writes");
// A device unit is 0.01 mm, 0.028 pt.
let tol = 0.05;
let Item::Path { segs, fill: Some([255, 0, 0, 255]), stroke: None, even_odd: true, clips, .. } = &p.items[0] else { panic!("the ellipse") };
assert!(segs.iter().any(|s| matches!(s, Seg::Cubic(..))), "curves stay curves");
assert!(rect_near(bounds(segs), Rect::new(10.0, 10.0, 60.0, 40.0), tol) && clips.is_empty());
let Item::Path { fill: None, stroke: Some(([0, 0, 255, 255], w)), line, .. } = &p.items[1] else { panic!("the dashed line") };
assert!(close(f64::from(*w), 2.0, tol));
assert_eq!((line.cap, line.join, line.miter_limit), (crate::Cap::Flat, crate::Join::Miter, 4.0));
assert!(line.dash.len() == 2 && close(f64::from(line.dash[0]), 6.0, tol) && close(f64::from(line.dash[1]), 3.0, tol), "{:?}", line.dash);
// One clip, then a second inside it; both gone after the two restores.
let Item::Path { fill: Some([0, 255, 0, 255]), even_odd: false, clips: outer, .. } = &p.items[2] else { panic!("the clipped fill") };
assert_eq!(outer.len(), 1);
assert!(rect_near(bounds(&outer[0].segs), Rect::new(100.0, 10.0, 150.0, 30.0), tol) && !outer[0].even_odd);
let Item::Path { fill: Some([0, 0, 255, 255]), clips: both, .. } = &p.items[3] else { panic!("the twice clipped fill") };
assert_eq!(both.len(), 2);
assert!(Arc::ptr_eq(&both[0], &outer[0]) && both[1].even_odd);
// The turned image keeps its pixels, its alpha and its turn.
let Item::Bitmap { transform, width: 4, height: 2, rgba, clips, .. } = &p.items[4] else { panic!("the turned image") };
assert!(clips.is_empty());
assert!(rgba.as_chunks::<4>().0.iter().all(|c| c[3] == 128 && c[0].abs_diff(200) <= 2 && c[1].abs_diff(100) <= 2 && c[2].abs_diff(50) <= 2));
let want = (turned * Affine::scale_non_uniform(4.0, 2.0)).as_coeffs();
assert!(transform.iter().zip(want).all(|(a, b)| close(*a, b, tol)), "{transform:?} {want:?}");
let Item::Bitmap { rect: r, width: 3, height: 2, rgba, .. } = &p.items[5] else { panic!("the upright image") };
assert!(rect_near(Rect::new(r[0].into(), r[1].into(), (r[0] + r[2]).into(), (r[1] + r[3]).into()), Rect::new(10.0, 70.0, 22.0, 78.0), tol));
assert_eq!(rgba.as_slice(), [1, 2, 3, 255].repeat(6));
let Item::Path { clips, .. } = &p.items[6] else { panic!("the last fill") };
assert!(clips.is_empty());
assert_eq!(p.items.len(), 7);
}
#[test]
fn wmf_round_trips_through_the_reader() {
let mut tri = BezPath::new();
tri.move_to((10.0, 10.0));
tri.line_to((60.0, 10.0));
tri.line_to((10.0, 40.0));
tri.close_path();
let ops = [
Op::Fill { path: tri, even_odd: true, rgb: RED },
stroke(line((10.0, 50.0), (100.0, 50.0)), Pen { cap: Cap::Square, join: Join::Bevel, ..pen(BLUE, 2.0) }),
fill(ellipse(70.0, 10.0, 130.0, 40.0), [0, 255, 0]),
Op::Image { image: image(3, 2, [1, 2, 3, 255]), xf: Affine::translate((10.0, 55.0)) * Affine::scale(4.0), opacity: 1.0 },
];
let out = wmf(&ops, page(144.0, 72.0)).unwrap();
let p = parse(&out.bytes).unwrap();
assert_eq!((p.width, p.height), (144.0, 72.0));
assert_eq!(p.skipped_records, 0);
// A logical unit is 1/1440 inch, 0.05 pt.
let tol = 0.06;
let Item::Path { segs, fill: Some([255, 0, 0, 255]), stroke: None, even_odd: true, .. } = &p.items[0] else { panic!("the triangle") };
assert!(rect_near(bounds(segs), Rect::new(10.0, 10.0, 60.0, 40.0), tol));
let Item::Path { segs, fill: None, stroke: Some(([0, 0, 255, 255], w)), line, .. } = &p.items[1] else { panic!("the line") };
assert!(close(f64::from(*w), 2.0, tol) && rect_near(bounds(segs), Rect::new(10.0, 50.0, 100.0, 50.0), tol));
assert_eq!((line.cap, line.join), (crate::Cap::Square, crate::Join::Bevel));
let Item::Path { segs, fill: Some([0, 255, 0, 255]), even_odd: false, .. } = &p.items[2] else { panic!("the ellipse") };
assert!(rect_near(bounds(segs), Rect::new(70.0, 10.0, 130.0, 40.0), 0.3), "flattened to within a quarter unit");
let Item::Bitmap { rect: r, width: 3, height: 2, rgba, .. } = &p.items[3] else { panic!("the image") };
assert!(rect_near(Rect::new(r[0].into(), r[1].into(), (r[0] + r[2]).into(), (r[1] + r[3]).into()), Rect::new(10.0, 55.0, 22.0, 63.0), tol));
assert_eq!(rgba.as_slice(), [1, 2, 3, 255].repeat(6));
}
#[test]
fn every_prefix_of_a_written_file_reads_without_a_panic() {
let ops = [
Op::Clip { path: rect(0.0, 0.0, 50.0, 50.0), even_odd: false },
fill(ellipse(10.0, 10.0, 60.0, 40.0), RED),
stroke(line((0.0, 0.0), (50.0, 50.0)), Pen { dash: Some(Dash { pattern: vec![3.0, 2.0], offset: 0.0 }), ..pen(BLUE, 1.0) }),
Op::Unclip,
Op::Image { image: image(2, 2, [9, 9, 9, 90]), xf: Affine::rotate(0.2), opacity: 0.5 },
];
let e = write_emf(&ops, 100.0, 100.0);
let w = wmf(&ops, page(100.0, 100.0)).unwrap().bytes;
for file in [&e, &w] {
for n in 0..=file.len() {
let _ = parse(&file[..n]);
let _ = crate::size(&file[..n]);
}
// Each byte in turn set to 0xFF.
for at in 0..file.len() {
let mut bad = file.clone();
bad[at] = 0xFF;
let _ = parse(&bad);
}
}
}
#[test]
fn a_tiny_dash_on_a_long_path_is_written_solid() {
// Not a pen's pattern (an offset), and a hundred million dashes if cut: the line stays whole.
let tiny = Dash { pattern: vec![0.01, 0.01], offset: 0.005 };
let ops = [stroke(line((0.0, 0.0), (1.0e6, 0.0)), Pen { dash: Some(tiny), ..pen(BLUE, 1.0) })];
let b = write_emf(&ops, 100.0, 100.0);
assert!(count(&kinds(&emf_records(&b)), 27) == 1, "one move-to");
assert!(wmf(&ops, page(100.0, 100.0)).unwrap().bytes.len() < 1000);
// Up to the cap the dashes are cut.
let some = Dash { pattern: vec![1.0, 1.0], offset: 0.5 };
let b = write_emf(&[stroke(line((0.0, 0.0), (100.0, 0.0)), Pen { dash: Some(some), ..pen(BLUE, 1.0) })], 100.0, 100.0);
assert_eq!(count(&kinds(&emf_records(&b)), 27), 51);
// A pattern that draws nothing (all zero, negative, not finite) is a solid line.
for bad in [vec![0.0, 0.0], vec![-1.0, 2.0], vec![f64::NAN, 1.0], vec![]] {
let d = Dash { pattern: bad, offset: 1.0 };
let b = write_emf(&[stroke(line((0.0, 0.0), (100.0, 0.0)), Pen { dash: Some(d), ..pen(BLUE, 1.0) })], 100.0, 100.0);
assert_eq!(count(&kinds(&emf_records(&b)), 27), 1);
}
}
#[test]
fn far_out_curves_flatten_into_a_bounded_number_of_points() {
let curve = |h: f64| {
let mut p = BezPath::new();
p.move_to((0.0, 0.0));
p.curve_to((h, 0.0), (h, 50.0), (50.0, 50.0));
p.close_path();
p
};
for h in [1.0e9, 1.0e70, f64::INFINITY, f64::NAN] {
let out = wmf(&[fill(curve(h), RED), stroke(curve(h), pen(BLUE, 1.0))], page(100.0, 100.0)).unwrap();
assert!(out.bytes.len() < 100_000, "{h}: {} bytes", out.bytes.len());
let _ = parse(&out.bytes).unwrap();
let e = write_emf(&[fill(curve(h), RED)], 100.0, 100.0);
let _ = parse(&e).unwrap();
}
}
+402
View File
@@ -0,0 +1,402 @@
//! The WMF writer: a placeable header (the picture's size and logical units an inch, with its
//! checksum), the WMF header, then the drawing in 16-bit logical units with an anisotropic window
//! the size of the picture, and the end-of-file record.
//!
//! Curves are flattened into polygons (fills) and polylines (strokes, with pens of the stroke's
//! width, caps and joins; dashes as separate lines). Objects take the lowest free slot of the
//! object table, as players number them: slots 0 and 1 hold a null pen and a null brush, our pen
//! and brush come after and are deleted when replaced. Images are StretchDIB records of 24-bit
//! bitmaps composited over white.
use kurbo::{Affine, BezPath, Point, Rect};
use super::emf::{PS_ENDCAP_FLAT, PS_ENDCAP_SQUARE, PS_JOIN_BEVEL, PS_JOIN_MITER, SRCCOPY, colorref};
use super::{Image, Op, Out, Pen, Wmf, dib};
use crate::{Cap, Join};
/// The key a placeable header starts with.
pub(crate) const PLACEABLE_KEY: u32 = 0x9AC6_CDD7;
// Record functions (MS-WMF 2.1.1.1).
pub(crate) const EOF: u16 = 0x0000;
pub(crate) const SETBKMODE: u16 = 0x0102;
pub(crate) const SETMAPMODE: u16 = 0x0103;
pub(crate) const SETPOLYFILLMODE: u16 = 0x0106;
pub(crate) const SETWINDOWORG: u16 = 0x020B;
pub(crate) const SETWINDOWEXT: u16 = 0x020C;
pub(crate) const SELECTOBJECT: u16 = 0x012D;
pub(crate) const DELETEOBJECT: u16 = 0x01F0;
pub(crate) const CREATEPENINDIRECT: u16 = 0x02FA;
pub(crate) const CREATEBRUSHINDIRECT: u16 = 0x02FC;
pub(crate) const POLYGON: u16 = 0x0324;
pub(crate) const POLYLINE: u16 = 0x0325;
pub(crate) const POLYPOLYGON: u16 = 0x0538;
pub(crate) const STRETCHDIB: u16 = 0x0F43;
/// Most logical units an inch (twips, what office apps use).
const MAX_INCH: f64 = 1440.0;
/// The largest 16-bit coordinate.
const MAX_I16: f64 = 32767.0;
/// Most points a polygon or polyline record holds (its count is 16-bit).
const MAX_POINTS: usize = 32767;
/// Curves are flattened to within this many logical units.
const FLATNESS: f64 = 0.25;
/// The null pen's and the null brush's slots.
const NULL_PEN: u16 = 0;
const NULL_BRUSH: u16 = 1;
/// The logical units an inch for a picture `w` × `h` points: the most, up to 1440, that keep its
/// size within 16 bits.
pub(crate) fn units_per_inch(w: f64, h: f64) -> u16 {
let side_in = w.max(h) / 72.0;
let fit = if side_in > 0.0 { (MAX_I16 / side_in).floor() } else { MAX_INCH };
fit.clamp(1.0, MAX_INCH) as u16
}
/// The XOR of the first ten 16-bit words of a placeable header.
pub(crate) fn checksum(header: &[u8]) -> u16 {
header.as_chunks::<2>().0.iter().take(10).fold(0, |acc, w| acc ^ u16::from_le_bytes(*w))
}
/// What a pen object is made of.
#[derive(Clone, Copy, PartialEq)]
struct PenKey {
style: u16,
width: i16,
color: u32,
}
struct Writer {
/// The records after the headers.
out: Out,
max_record: u32,
to_dev: Affine,
/// Logical units per point.
k: f64,
/// Which object slots are taken.
slots: Vec<bool>,
brush: Option<(u16, u32)>,
pen: Option<(u16, PenKey)>,
sel_pen: u16,
sel_brush: u16,
fill_mode: Option<u16>,
}
/// Write `ops` (curves flattened, no clips) as a placeable WMF of `region`.
pub(crate) fn write(ops: &[Op], region: Rect) -> Wmf {
let inch = units_per_inch(region.width(), region.height());
let k = f64::from(inch) / 72.0;
let size = |v: f64| (v * k).round().clamp(1.0, MAX_I16) as i16;
let (w, h) = (size(region.width()), size(region.height()));
let mut wr = Writer {
out: Out::default(),
max_record: 0,
to_dev: Affine::scale(k) * Affine::translate(-region.origin().to_vec2()),
k,
slots: vec![],
brush: None,
pen: None,
sel_pen: NULL_PEN,
sel_brush: NULL_BRUSH,
fill_mode: None,
};
// MM_ANISOTROPIC, a window the size of the picture, transparent background.
wr.record(SETMAPMODE, |o| o.u16(8));
wr.record(SETWINDOWORG, |o| {
o.i16(0);
o.i16(0);
});
wr.record(SETWINDOWEXT, |o| {
o.i16(h);
o.i16(w);
});
wr.record(SETBKMODE, |o| o.u16(1));
// The null pen and brush (slots 0 and 1), selected while ours change.
wr.alloc();
wr.record(CREATEPENINDIRECT, |o| {
o.u16(5);
o.i16(0);
o.i16(0);
o.u32(0);
});
wr.alloc();
wr.record(CREATEBRUSHINDIRECT, |o| {
o.u16(1);
o.u32(0);
o.u16(0);
});
wr.select(NULL_PEN);
wr.select(NULL_BRUSH);
let mut lost = Wmf::default();
for op in ops {
match op {
Op::Fill { path, even_odd, rgb } => wr.fill(path, *even_odd, *rgb),
Op::Stroke { path, pen } => wr.stroke(path, pen),
Op::Image { image, xf, opacity } => {
// The record's sizes are 16-bit.
if image.width > MAX_POINTS as u32 || image.height > MAX_POINTS as u32 {
lost.oversized_images = true;
continue;
}
lost.translucent_images |= !image.opaque() || *opacity < 0.998;
lost.turned_images |= !wr.image(image, *xf, *opacity);
}
// WMF has no clip paths: what a clip covers is written whole.
Op::Clip { .. } => lost.clips_ignored = true,
Op::Unclip => {}
}
}
if let Some((slot, _)) = wr.brush.take() {
wr.select(NULL_BRUSH);
wr.delete(slot);
}
if let Some((slot, _)) = wr.pen.take() {
wr.select(NULL_PEN);
wr.delete(slot);
}
wr.record(EOF, |_| {});
Wmf { bytes: wr.finish(w, h, inch), ..lost }
}
impl Writer {
/// Append one record: its size in 16-bit words, its function and `body`.
fn record(&mut self, function: u16, body: impl FnOnce(&mut Out)) {
let start = self.out.len();
self.out.u32(0);
self.out.u16(function);
body(&mut self.out);
self.out.pad(2);
let words = u32::try_from((self.out.len() - start) / 2).unwrap_or(u32::MAX);
self.out.set_u32(start, words);
self.max_record = self.max_record.max(words);
}
/// The placeable header, the WMF header, then the records.
fn finish(self, w: i16, h: i16, inch: u16) -> Vec<u8> {
let mut o = Out::default();
o.u32(PLACEABLE_KEY);
o.u16(0);
for v in [0, 0, w, h] {
o.i16(v);
}
o.u16(inch);
o.u32(0);
let sum = checksum(&o.0);
o.u16(sum);
// A memory metafile, 9 words of header, version 3.
o.u16(1);
o.u16(9);
o.u16(0x0300);
o.u32(u32::try_from((18 + self.out.len()) / 2).unwrap_or(u32::MAX));
o.u16(u16::try_from(self.slots.len()).unwrap_or(u16::MAX));
o.u32(self.max_record);
o.u16(0);
o.bytes(&self.out.0);
o.0
}
/// The lowest free object slot, taken.
fn alloc(&mut self) -> u16 {
let i = match self.slots.iter().position(|t| !t) {
Some(i) => i,
None => {
self.slots.push(false);
self.slots.len() - 1
}
};
if let Some(s) = self.slots.get_mut(i) {
*s = true;
}
u16::try_from(i).unwrap_or(u16::MAX)
}
fn select(&mut self, slot: u16) {
self.record(SELECTOBJECT, |o| o.u16(slot));
}
fn delete(&mut self, slot: u16) {
self.record(DELETEOBJECT, |o| o.u16(slot));
if let Some(s) = self.slots.get_mut(usize::from(slot)) {
*s = false;
}
}
fn dev(&self, p: Point) -> [i16; 2] {
let q = self.to_dev * p;
let c = |v: f64| if v.is_finite() { v.round().clamp(-MAX_I16, MAX_I16) as i16 } else { 0 };
[c(q.x), c(q.y)]
}
/// The device points of `pts`, repeats dropped, thinned to what one record holds.
fn points(&self, pts: &[Point]) -> Vec<[i16; 2]> {
let mut v: Vec<[i16; 2]> = vec![];
for p in pts {
let d = self.dev(*p);
if v.last() != Some(&d) {
v.push(d);
}
}
if v.len() > MAX_POINTS {
let step = v.len().div_ceil(MAX_POINTS - 1);
let last = v.last().copied();
v = v.into_iter().step_by(step).collect();
v.extend(last);
}
v
}
fn select_pen(&mut self, slot: u16) {
if self.sel_pen != slot {
self.select(slot);
self.sel_pen = slot;
}
}
fn select_brush(&mut self, slot: u16) {
if self.sel_brush != slot {
self.select(slot);
self.sel_brush = slot;
}
}
fn use_brush(&mut self, rgb: [u8; 3]) {
let color = colorref(rgb);
let slot = match self.brush {
Some((slot, c)) if c == color => slot,
old => {
if let Some((slot, _)) = old {
self.select_brush(NULL_BRUSH);
self.delete(slot);
}
let slot = self.alloc();
self.record(CREATEBRUSHINDIRECT, |o| {
o.u16(0);
o.u32(color);
o.u16(0);
});
self.brush = Some((slot, color));
slot
}
};
self.select_brush(slot);
}
fn use_pen(&mut self, p: &Pen) {
let cap = match p.cap {
Cap::Flat => PS_ENDCAP_FLAT,
Cap::Round => 0,
Cap::Square => PS_ENDCAP_SQUARE,
};
let join = match p.join {
Join::Miter => PS_JOIN_MITER,
Join::Round => 0,
Join::Bevel => PS_JOIN_BEVEL,
};
let key = PenKey { style: (cap | join) as u16, width: (p.width * self.k).round().clamp(1.0, MAX_I16) as i16, color: colorref(p.rgb) };
let slot = match self.pen {
Some((slot, k)) if k == key => slot,
old => {
if let Some((slot, _)) = old {
self.select_pen(NULL_PEN);
self.delete(slot);
}
let slot = self.alloc();
self.record(CREATEPENINDIRECT, |o| {
o.u16(key.style);
o.i16(key.width);
o.i16(0);
o.u32(key.color);
});
self.pen = Some((slot, key));
slot
}
};
self.select_pen(slot);
}
fn fill(&mut self, bp: &BezPath, even_odd: bool, rgb: [u8; 3]) {
let polys: Vec<Vec<[i16; 2]>> = super::flatten(bp, FLATNESS / self.k).iter().map(|(p, _)| self.points(p)).filter(|p| p.len() > 2).collect();
if polys.is_empty() {
return;
}
// ALTERNATE (even-odd) or WINDING (non-zero).
let mode = if even_odd { 1 } else { 2 };
if self.fill_mode != Some(mode) {
self.record(SETPOLYFILLMODE, |o| o.u16(mode));
self.fill_mode = Some(mode);
}
self.use_brush(rgb);
self.select_pen(NULL_PEN);
for chunk in polys.chunks(usize::from(u16::MAX)) {
self.record(POLYPOLYGON, |o| {
o.u16(chunk.len() as u16);
chunk.iter().for_each(|p| o.u16(p.len() as u16));
for p in chunk.iter().flatten() {
o.i16(p[0]);
o.i16(p[1]);
}
});
}
}
fn stroke(&mut self, bp: &BezPath, pen: &Pen) {
let path = super::dashed(bp, pen.dash.as_ref());
let lines: Vec<(Vec<[i16; 2]>, bool)> =
super::flatten(&path, FLATNESS / self.k).iter().map(|(p, closed)| (self.points(p), *closed)).filter(|(p, _)| p.len() > 1).collect();
if lines.is_empty() {
return;
}
self.use_pen(pen);
self.select_brush(NULL_BRUSH);
for (pts, closed) in lines {
// A closed subpath is a polygon (with the null brush, its outline), so it joins at
// its start.
self.record(if closed { POLYGON } else { POLYLINE }, |o| {
o.i16(pts.len() as i16);
for p in &pts {
o.i16(p[0]);
o.i16(p[1]);
}
});
}
}
/// An image placed by `xf`, upright: false when `xf` rotates or skews it (then it fills its
/// bounds).
fn image(&mut self, img: &Image, xf: Affine, opacity: f32) -> bool {
if img.width == 0 || img.height == 0 || opacity <= 0.0 {
return true;
}
let m = self.to_dev * xf;
if !m.as_coeffs().iter().all(|v| v.is_finite()) {
return true;
}
let [a, b, c, d, e, f] = m.as_coeffs();
let (w, h) = (f64::from(img.width), f64::from(img.height));
let upright = b.abs() < 1e-9 && c.abs() < 1e-9;
let (x, y, dw, dh) = if upright {
(e, f, a * w, d * h)
} else {
let r = m.transform_rect_bbox(Rect::new(0.0, 0.0, w, h));
(r.x0, r.y0, r.width(), r.height())
};
let s = |v: f64| v.round().clamp(-MAX_I16, MAX_I16) as i16;
let (bmi, bits) = dib::rgb24(img, opacity);
self.record(STRETCHDIB, |o| {
o.u32(SRCCOPY);
// DIB_RGB_COLORS; the source and destination rectangles, last field first.
o.u16(0);
o.i16(img.height as i16);
o.i16(img.width as i16);
o.i16(0);
o.i16(0);
o.i16(s(dh));
o.i16(s(dw));
o.i16(s(y));
o.i16(s(x));
o.bytes(&bmi);
o.bytes(&bits);
});
upright
}
}