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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- JPEG 2000 images ignore the PDF Decode array as required. Raster decoding
rejects invalid sample depths, including non-1-bit stencil masks.

- Conan packages include the license text. Android profiles identify NDK 28.1
as Clang 19 so dependency package IDs match the compiler.

Expand Down
18 changes: 12 additions & 6 deletions src/odr/internal/pdf/pdf_image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <cstdint>
#include <limits>
Expand All @@ -22,6 +23,11 @@ namespace odr::internal::pdf {

namespace {

bool valid_bit_depth(const std::int32_t bits) {
return bits > 0 && bits <= 16 &&
std::has_single_bit(static_cast<std::uint32_t>(bits));
}

/// 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 @@ -83,7 +89,6 @@ void unpremultiply(std::string &samples, const std::int32_t components,
/// (ISO 32000-1 8.9.5.1: `/ColorSpace` is optional for `JPXDecode`).
std::optional<EncodedImage>
encode_jpx(const std::string &data, const ColorSpaceDef *color_space,
const std::span<const double> decode_array,
const std::span<const std::uint8_t> alpha,
const std::span<const double> color_key,
const std::int32_t smask_in_data) {
Expand Down Expand Up @@ -120,7 +125,7 @@ encode_jpx(const std::string &data, const ColorSpaceDef *color_space,
: device;

const std::string png = encode_image_png(
image->samples, image->width, image->height, 8, space, decode_array,
image->samples, image->width, image->height, 8, space, {},
alpha.empty() ? std::span<const std::uint8_t>{image->alpha} : alpha,
color_key);
if (png.empty()) {
Expand All @@ -143,8 +148,8 @@ std::string pdf::encode_image_png(const std::string &samples,
const std::span<const std::uint8_t> alpha,
const std::span<const double> color_key) {
const std::int32_t components = color_space.components;
if (width <= 0 || height <= 0 || components <= 0 || bits_per_component <= 0 ||
bits_per_component > 16) {
if (width <= 0 || height <= 0 || components <= 0 ||
!valid_bit_depth(bits_per_component)) {
return {};
}

Expand Down Expand Up @@ -262,7 +267,8 @@ std::vector<std::uint8_t> pdf::decode_mask_alpha(
const std::span<const double> decode, const bool stencil,
const std::int32_t base_width, const std::int32_t base_height) {
if (width <= 0 || height <= 0 || base_width <= 0 || base_height <= 0 ||
bits_per_component <= 0 || bits_per_component > 16) {
!valid_bit_depth(bits_per_component) ||
(stencil && bits_per_component != 1)) {
return {};
}
const std::uint32_t max_sample =
Expand Down Expand Up @@ -379,7 +385,7 @@ std::optional<pdf::EncodedImage> pdf::encode_image(
if (result.stopped_at_filter != "JPXDecode") {
return std::nullopt;
}
return encode_jpx(result.data, color_space, decode_array, alpha, color_key,
return encode_jpx(result.data, color_space, alpha, color_key,
smask_in_data);
}
// A fully decodable raster: decode, assemble samples and PNG-encode.
Expand Down
35 changes: 8 additions & 27 deletions src/odr/internal/pdf/pdf_image.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,8 @@ struct EncodedImage {
std::string mime;
};

/// Turn an image's raw (still filter-encoded) bytes into browser-ready ones: a
/// `DCTDecode` JPEG passes through, any other decodable raster is re-encoded as
/// PNG through `color_space` (required only on that path). `alpha` and
/// `color_key` (see `encode_image_png`) make the raster RGBA and are ignored by
/// the JPEG pass-through. A `JPXDecode` raster comes through `decode_jpx`, its
/// own opacity channel taken only as `smask_in_data` says (Table 89: 0 ignores
/// it, 2 says the colour is premultiplied by it). A `JBIG2Decode` raster goes
/// through `decode_jbig2`, `options` carrying the globals it may need, and a
/// `CCITTFaxDecode` one through `decode_ccitt`. `nullopt` for a codec past its
/// decoder's reach or an inconsistent raster.
/// Convert encoded samples to JPEG or PNG; nullopt if unsupported or invalid.
/// JPEG passes through without masks; JPX ignores Decode (Table 89).
std::optional<EncodedImage>
encode_image(std::string raw, const Object &filter, const Object &decode_parms,
std::int32_t width, std::int32_t height,
Expand All @@ -40,14 +32,8 @@ encode_image(std::string raw, const Object &filter, const Object &decode_parms,
std::span<const double> color_key = {},
std::int32_t smask_in_data = 0, const DecodeOptions &options = {});

/// Assemble decoded image samples (ISO 32000-1 8.9.5: MSB-first, rows padded
/// to a byte boundary, `bits_per_component` of 1/2/4/8/16) into an 8-bit PNG,
/// converting through `color_space`, or a 1-bit palette PNG for a 1-bpc gray
/// image without alpha. `decode` is the `/Decode` array remapping the sample
/// range. `alpha` (one byte per pixel, row-major) and `color_key`
/// ([min0 max0 …] in raw sample units) each make the output RGBA, transparent
/// where coverage is 0 / every component is inside the ranges (8.9.6). Returns
/// "" for an inconsistent configuration, so the caller skips the image.
/// Decode 1/2/4/8/16-bit samples into PNG; empty for invalid parameters.
/// Alpha is row-major coverage; color_key is [min0 max0 …] in raw sample units.
std::string encode_image_png(const std::string &samples, std::int32_t width,
std::int32_t height,
std::int32_t bits_per_component,
Expand All @@ -56,21 +42,16 @@ std::string encode_image_png(const std::string &samples, std::int32_t width,
std::span<const std::uint8_t> alpha = {},
std::span<const double> color_key = {});

/// Resolve a `/SMask` or stencil `/Mask` sub-image into a coverage plane sized
/// to the *base* image, nearest-neighbour resampled — the two resolutions need
/// not match (ISO 32000-1 8.9.5.4 / 11.6.5.2). A soft mask's grey value is the
/// alpha; a stencil (`stencil`, 1 bpc) masks out where a sample decodes to 1.
/// Empty for an inconsistent mask.
/// Decode and resample a soft or 1-bit stencil mask to the base image size.
/// Stencil values of 1 mask out pixels; empty for invalid parameters.
std::vector<std::uint8_t>
decode_mask_alpha(const std::string &samples, std::int32_t width,
std::int32_t height, std::int32_t bits_per_component,
std::span<const double> decode, bool stencil,
std::int32_t base_width, std::int32_t base_height);

/// 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
/// time, so only the page extractor can call this. "" when inconsistent.
/// Paint a 1-bit stencil in the fill color: decoded 0 paints, 1 is transparent.
/// Decode [1 0] inverts coverage; empty for invalid dimensions (8.9.6.2).
std::string encode_stencil_png(const std::string &samples, std::int32_t width,
std::int32_t height,
const std::array<double, 3> &color,
Expand Down
12 changes: 11 additions & 1 deletion test/src/internal/pdf/pdf_image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,17 @@ TEST(PdfImage, encode_honours_decode_array) {

TEST(PdfImage, encode_rejects_bad_parameters) {
EXPECT_TRUE(encode_image_png("", 0, 1, 8, device_rgb(), {}).empty());
EXPECT_TRUE(encode_image_png("", 1, 1, 0, device_rgb(), {}).empty());
for (const std::int32_t bits : {-1, 0, 3, 7, 12, 15, 17}) {
SCOPED_TRACE(bits);
EXPECT_TRUE(encode_image_png("", 1, 1, bits, device_rgb(), {}).empty());
EXPECT_TRUE(decode_mask_alpha("", 1, 1, bits, {}, false, 1, 1).empty());
}
EXPECT_TRUE(decode_mask_alpha("", 1, 1, 8, {}, true, 1, 1).empty());
for (const std::int32_t bits : {2, 16}) {
const auto png = decode_png(
encode_image_png(bytes({255, 255}), 1, 1, bits, device_gray(), {}));
EXPECT_EQ(png.pixel(0, 0), bytes({255, 255, 255}));
}
ColorSpaceDef zero;
zero.components = 0;
EXPECT_TRUE(encode_image_png("", 1, 1, 8, zero, {}).empty());
Expand Down
15 changes: 15 additions & 0 deletions test/src/internal/pdf/pdf_jpx.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#include <odr/internal/pdf/pdf_jpx.hpp>

#include <internal/png/png_test_util.hpp>
#include <odr/internal/pdf/pdf_image.hpp>
#include <odr/internal/pdf/pdf_object.hpp>

#include <odr/internal/crypto/crypto_util.hpp>

#include <array>
Expand Down Expand Up @@ -71,5 +75,16 @@ TEST(PdfJpx, scales_samples_to_eight_bits) {
EXPECT_EQ(static_cast<std::uint8_t>(image->samples[0]), 0);
EXPECT_EQ(static_cast<std::uint8_t>(image->samples[1]), middle);
EXPECT_EQ(static_cast<std::uint8_t>(image->samples[2]), 255);

// JPX ignores the PDF bit depth and Decode array (Table 89).
const std::array<double, 2> inverted{1, 0};
const auto encoded = encode_image(
odr::internal::crypto::util::hex_decode(hex), Object(Name{"JPXDecode"}),
Object{}, 3, 1, 0, nullptr, inverted);
ASSERT_TRUE(encoded.has_value());
const auto png = odr::test::png::decode_png(encoded->data);
for (std::size_t x = 0; x < 3; ++x) {
EXPECT_EQ(png.pixel(x, 0), std::string(3, image->samples[x]));
}
}
}
Loading