Skip to content

feat(svm): draw the bitmaps a metafile carries, as png where that is smaller - #786

Merged
andiwand merged 3 commits into
mainfrom
feat/svm-bitmaps
Aug 30, 2026
Merged

feat(svm): draw the bitmaps a metafile carries, as png where that is smaller#786
andiwand merged 3 commits into
mainfrom
feat/svm-bitmaps

Conversation

@andiwand

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Closes #194. Stage 4 of #772, stacked on #785#784#779review those
first
; this branch's base is feat/svm-text.

The empty frame

odr-private/svm/Vyplaty.svm is a 1.63 MB file, and 1.59 MB of it is a single
BMPEXSCALE: the whole data area of the chart is one bitmap. We drew the axes,
the grid and 79 labels around nothing. It is the only bitmap in 1125 harvested
metafiles, and it is exactly the file #194 named.

No decoder was needed

SvmReader reads a dib with ReadDIB(…, bFileHeader=true), so the action body
holds it with its BITMAPFILEHEADER — the bytes already are a .bmp file
and a browser reads them as one. The only thing to work out is the length, and
bfSize is no help: ImplWriteDIBFileHeader computes it from the
uncompressed size, so for a compressed dib it lies (this file's mask claims
522214 bytes and occupies 29478). The header's own biSizeImage — or width,
height and bit count — is the answer.

png, because 2.9 MB per page is not a rendering

The bytes go out as a png rather than a bmp wherever the pixels can be copied
row by row: uncompressed, 1/4/8/24/32 bits, palette expanded, rows turned right
way up, bgr swapped. Re-packing, not decoding.

bmp png
the bitmap 1.59 MB 33 KB
Vyplaty.ods/sheet1.html 2.9 MB 158 KB

Anything compressed (RLE, bit fields) still goes out as the bmp it is —
browsers read those, verified in Chrome for the RLE8 mask this very file
carries.

The png writing pdf_image.cpp already had moves to util::png::write so both
callers can reach it, with its two tests.

The mask

A BMPEX may carry a transparency mask: a second dib, white where the bitmap
does not show
(Bitmap::Bitmap(rBmp, rMask) inverts it into an alpha
channel). An svg <mask> keeps what is white, so the mask image goes through
an inverting feColorMatrix, written once per document.

Still open

Neither occurs in the corpus, both are noted in svm/PLAN.md: ZCOMPRESS, a
LibreOffice-only compression whose zlib stream would have to be inflated first
(miniz is already a dependency), and the MASK family, which stencils one
colour through a bitmap.

Verification

2 new tests, both from inline bytes — a dib built by hand in the test, drawn
where the action puts it and re-packed as a png; and a BMPEX whose mask
element, inverting filter and reference all come out.

Rendered in headless Chrome: the chart now matches what LibreOffice's own
--convert-to svg makes of the same file. The reference output moves for the
two pages of ods/Vyplaty.ods and nothing else; the pin is advanced in this
branch.

…smaller

Closes #194. A chart whose data area is one big bitmap - `Vyplaty.svm` is
1.59 MB of `BMPEXSCALE` in a 1.63 MB file - rendered as an empty frame with
axes and labels around nothing.

No pixel decoder was needed. `SvmReader` reads a dib with
`ReadDIB(…, bFileHeader=true)`, so the action body holds it *with* its
`BITMAPFILEHEADER`: the bytes already are a `.bmp` file, and a browser reads
them as one. Only the length has to be worked out, and `bfSize` is no help -
it is written from the uncompressed size, so for a compressed dib it lies. The
header's own `biSizeImage`, or its width, height and bit count, is the answer.

They go out as a png rather than a bmp wherever the pixels can be copied row by
row - uncompressed, 1/4/8/24/32 bits, palette expanded, rows turned right way
up and bgr swapped. That is re-packing rather than decoding, and it earns its
keep: the bitmap above is 1.59 MB as a bmp and 33 KB as a png, so the page it
sits on is 158 KB instead of 2.9 MB. Anything compressed still goes out as the
bmp it is.

The png writing `pdf_image.cpp` already had moves to `util::png::write`, where
both callers can reach it; its two tests move with it.

A `BMPEX` may carry a transparency mask: a second dib, white where the bitmap
does not show. An svg `<mask>` keeps what is white, so the mask image goes
through an inverting `feColorMatrix`, written once per document.

Still open, and neither occurs in the corpus: `ZCOMPRESS`, whose zlib stream
would have to be inflated first, and the `MASK` family, which stencils one
colour through a bitmap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmCr22NW6wPQKiQidk96bq
The bitmap comments that told the story twice: the inverting mask is
explained in `AGENTS.md`, `BitmapBox` needs no gloss of its own field names,
and `assumed_dpi` only has to say what the file does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQpLmJpJ87qbKoG8B7kbLY
A bitmap action with no size of its own is drawn at 96 dpi, and the
hundredths of a millimetre a pixel takes there is 26.458. Truncating that to
26 before multiplying lost 1.7% of the size: sixteen pixels came out 416 wide
where they measure 423. The pixel count multiplies first now.

The reference output is unchanged - the corpus carries `BMPEXSCALE`, which
names its own size, and no unsized action - so the pins stay where they are.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQpLmJpJ87qbKoG8B7kbLY
@andiwand
andiwand merged commit 8a81b53 into main Aug 30, 2026
25 checks passed
@andiwand
andiwand deleted the feat/svm-bitmaps branch August 30, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

svm support bitmaps

1 participant