Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ The release run heads these entries with the version and opens a fresh
- A spreadsheet decodes in less memory: 626 MB peak instead of 914 MB on a
297 MB `content.xml`. Rendered output is unchanged.

- A StarView metafile draws its bitmaps (#194): `BMP`, `BMPEX` and their
scaling and part variants, transparency mask included. Such a chart used to
render as an empty frame.

- A text action in a StarView metafile draws the run it names, not the whole
string. Text drawn in runs used to overprint itself into a smear.

Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ set(ODR_SOURCE_FILES
"src/odr/internal/util/number_util.cpp"
"src/odr/internal/util/odr_meta_util.cpp"
"src/odr/internal/util/stream_util.cpp"
"src/odr/internal/util/png_util.cpp"
"src/odr/internal/util/string_util.cpp"
"src/odr/internal/util/xml_util.cpp"

Expand Down
58 changes: 3 additions & 55 deletions src/odr/internal/pdf/pdf_image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <odr/internal/pdf/pdf_jpx.hpp>
#include <odr/internal/pdf/pdf_object.hpp>
#include <odr/internal/util/byte_string.hpp>
#include <odr/internal/util/png_util.hpp>

#include <algorithm>
#include <array>
Expand All @@ -18,17 +19,6 @@ namespace odr::internal::pdf {

namespace {

/// Append a PNG chunk: length, four-byte type, data, CRC over type+data.
void write_chunk(std::string &out, const std::string_view type,
const std::string &data) {
util::byte_string::put_u32_be(out, static_cast<std::uint32_t>(data.size()));
const std::size_t crc_start = out.size();
out.append(type);
out.append(data);
util::byte_string::put_u32_be(
out, crypto::util::crc32(std::string_view(out).substr(crc_start)));
}

/// Reads fixed-width big-endian sample values out of a byte buffer, MSB first.
/// Constructed at a row offset; rows are byte-aligned (8.9.5.2). Reads past the
/// end yield zero (lenient for a truncated stream).
Expand Down Expand Up @@ -144,48 +134,6 @@ encode_jpx(const std::string &data, const ColorSpaceDef *color_space,

namespace odr::internal {

std::string pdf::write_png(const std::string &pixels, const std::int32_t width,
const std::int32_t height,
const std::int32_t channels) {
if (width <= 0 || height <= 0 || (channels != 3 && channels != 4)) {
return {};
}
const auto stride =
static_cast<std::size_t>(width) * static_cast<std::size_t>(channels);
if (pixels.size() < stride * static_cast<std::size_t>(height)) {
return {};
}

// Filter type 0 (None) prefixes each scanline (PNG 9.2); the rows are then
// deflated as one zlib stream into the single IDAT.
std::string raw;
raw.reserve((stride + 1) * static_cast<std::size_t>(height));
for (std::int32_t y = 0; y < height; ++y) {
raw.push_back(0);
raw.append(pixels, static_cast<std::size_t>(y) * stride, stride);
}

static constexpr std::array<char, 8> signature = {
static_cast<char>(0x89), 'P', 'N', 'G', '\r', '\n',
static_cast<char>(0x1A), '\n'};
std::string out;
out.append(signature.data(), signature.size());

std::string ihdr;
util::byte_string::put_u32_be(ihdr, static_cast<std::uint32_t>(width));
util::byte_string::put_u32_be(ihdr, static_cast<std::uint32_t>(height));
ihdr.push_back(8); // bit depth
// Colour type: 2 = truecolour (RGB), 6 = truecolour with alpha (RGBA).
ihdr.push_back(channels == 4 ? 6 : 2);
ihdr.push_back(0); // compression: deflate
ihdr.push_back(0); // filter method: adaptive
ihdr.push_back(0); // interlace: none
write_chunk(out, "IHDR", ihdr);
write_chunk(out, "IDAT", crypto::util::zlib_deflate(raw));
write_chunk(out, "IEND", "");
return out;
}

std::string pdf::encode_image_png(const std::string &samples,
const std::int32_t width,
const std::int32_t height,
Expand Down Expand Up @@ -270,7 +218,7 @@ std::string pdf::encode_image_png(const std::string &samples,
}
}

return write_png(out, width, height, has_alpha ? 4 : 3);
return util::png::write(out, width, height, has_alpha ? 4 : 3);
}

std::vector<std::uint8_t> pdf::decode_mask_alpha(
Expand Down Expand Up @@ -363,7 +311,7 @@ std::string pdf::encode_stencil_png(const std::string &samples,
rgba[out_index++] = static_cast<char>(paint ? 0xFF : 0x00);
}
}
return write_png(rgba, width, height, 4);
return util::png::write(rgba, width, height, 4);
}

std::optional<pdf::EncodedImage> pdf::encode_image(
Expand Down
5 changes: 0 additions & 5 deletions src/odr/internal/pdf/pdf_image.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,6 @@ decode_mask_alpha(const std::string &samples, std::int32_t width,
std::span<const double> decode, bool stencil,
std::int32_t base_width, std::int32_t base_height);

/// Wrap 8-bit pixels (row-major, unpadded) into a PNG: single `IDAT`, no
/// interlacing. `channels` is 3 (RGB) or 4 (RGBA); anything else yields "".
std::string write_png(const std::string &pixels, std::int32_t width,
std::int32_t height, std::int32_t channels);

/// Paint a 1-bpc stencil mask (ISO 32000-1 8.9.6.2) into an RGBA PNG: a sample
/// decoding to 0 paints `color` (sRGB in [0, 1]) opaquely, a 1 is transparent,
/// and a `/Decode` of `[1 0]` swaps that. `color` is the fill colour at draw
Expand Down
14 changes: 14 additions & 0 deletions src/odr/internal/svm/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ The format is not specified anywhere. The references, best first:
file's own bytes for every encoding but `UCS2`, so a latin-1 label emits
invalid utf-8 and the parser refuses the document all the same.

## Bitmaps do not go through a decoder

A dib in a metafile carries its own `BITMAPFILEHEADER`, so it *is* a `.bmp`
file β€” no pixel decoding is needed to show one, only its length, which the
header's `biSizeImage` gives and whose `bfSize` does not (that one is written
from the uncompressed size). Where the pixels are plain enough to copy out row
by row they are re-packed as a png, which for a chart is fifty times smaller;
anything compressed goes out as the bmp it is.

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` β€” `filter="url(#odr-invert)"`, written
once per document.

## Testing

`svm_test.cpp` builds its input as bytes inline through `SvmBuilder`, so an
Expand Down
32 changes: 21 additions & 11 deletions src/odr/internal/svm/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ Each stage is one pull request, stacked on the one before it.
which costs the image exactly as an unescaped `&` did.
4. **Clipping.** `CLIPREGION`, `ISECTRECTCLIPREGION`,
`ISECTREGIONCLIPREGION`, `MOVECLIPREGION`.
5. **Bitmaps** (#194). See the shortcut below.
5. **Bitmaps** (#194) - done for the `BMP` and `BMPEX` families; `MASK`,
which stencils one colour through a bitmap, and `ZCOMPRESS`, which needs
inflating first, are still open. See the shortcut below.
6. **Primitives.** `PIXEL`, `POINT`, `LINE`, `ROUNDRECT`, `ELLIPSE`, `ARC`,
`PIE`, `CHORD` β€” one `svgwriter.cxx` case each.
7. **Fills and transparency.** `GRADIENT`, `GRADIENTEX`, `HATCH`,
Expand Down Expand Up @@ -97,16 +99,24 @@ are worth writing down:

## Shortcuts worth taking

- **Bitmaps are `.bmp` files already.** `SvmReader` reads them with
`ReadDIB(…, bFileHeader=true)`, i.e. the action body holds a DIB *with* its
`BITMAPFILEHEADER` β€” `"BM"`, `bfSize`, `bfOffBits`. So a `BMP` action needs
no pixel decoding at all: read the header far enough to know the byte length,
hand the bytes to the browser as `data:image/bmp;base64,…` inside an
`<image>`. Palettes, RLE4/RLE8 and bit fields are then the browser's problem,
not ours. Two cases still need work: `ZCOMPRESS` (a LibreOffice-only
compression β€” inflate with miniz, then rewrite the header), and the alpha
mask of `BMPEX` (a second DIB, `1` = transparent) which becomes an SVG
`<mask>` over an inverting `feColorMatrix`.
- **Bitmaps are `.bmp` files already** β€” this one is taken. `SvmReader` reads
them with `ReadDIB(…, bFileHeader=true)`, i.e. the action body holds a dib
*with* its `BITMAPFILEHEADER`, so the bytes are a `.bmp` file and the browser
can read 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 width, height and bit count) is
the answer.

The bytes then 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. That is
not decoding so much as re-packing, and it is worth it: the one real bitmap
in the corpus is 1.59 MB as a bmp and 33 KB as a png, so the page it sits on
goes from 2.9 MB to 158 KB. A compressed dib (RLE, bit fields) still goes out
as the bmp it is; browsers read those.

What is left: `ZCOMPRESS`, a LibreOffice-only compression whose zlib stream
would have to be inflated (miniz is already a dependency) before any of the
above, and the `MASK` family, which stencils one colour through a bitmap.
- **BΓ©ziers are cheap once the flags are read.** A polygon flag of
`PolyFlags::Control` marks a control point, so a flagged polygon maps onto an
SVG path's `C` segments directly. The reader is the part that is missing.
Expand Down
Loading
Loading