diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba7df6d9..30646ac68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- PDF streams support TIFF predictor samples packed at 1, 2, or 4 bits per + component. LZW decoding rejects invalid width parameters and dictionary + growth beyond the format's limit. + - HTML document views honor their configured output filename. Audio and video resources are served at custom locator paths without replacing the player or stylesheet, and named in-memory WebM files retain their playable MIME type. diff --git a/src/odr/internal/pdf/pdf_filter.cpp b/src/odr/internal/pdf/pdf_filter.cpp index 868dca244..5eea260e0 100644 --- a/src/odr/internal/pdf/pdf_filter.cpp +++ b/src/odr/internal/pdf/pdf_filter.cpp @@ -146,14 +146,10 @@ PredictorLayout predictor_layout(const Integer colors, const Integer bits, static_cast((pixel_bits + 7) / 8)}; } -std::string apply_tiff_predictor(std::string data, +std::string apply_tiff_predictor(std::string data, const Integer colors, + const Integer columns, const Integer bits_per_component, const PredictorLayout layout) { - if (bits_per_component != 8 && bits_per_component != 16) { - throw std::runtime_error("unsupported TIFF predictor bits per component: " + - std::to_string(bits_per_component)); - } - const std::size_t component_bytes = bits_per_component / 8; const auto [row_bytes, pixel_bytes] = layout; if (data.size() % row_bytes != 0) { @@ -161,6 +157,28 @@ std::string apply_tiff_predictor(std::string data, } for (std::size_t row = 0; row < data.size(); row += row_bytes) { + if (bits_per_component < 8) { + // ISO 32000-1 7.4.4.4: TIFF predicts components, not packed bytes. + const auto bits = static_cast(bits_per_component); + const std::uint32_t mask = (1u << bits) - 1; + const auto samples = static_cast(colors) * columns; + const auto sample = [&](const std::uint64_t index) { + const std::uint64_t bit = index * bits; + return (static_cast(data[row + bit / 8]) >> + (8 - bits - bit % 8)) & + mask; + }; + for (auto i = static_cast(colors); i < samples; ++i) { + const std::uint32_t value = (sample(i) + sample(i - colors)) & mask; + const std::uint64_t bit = i * bits; + const auto shift = static_cast(8 - bits - bit % 8); + auto &byte = data[row + bit / 8]; + byte = static_cast( + (static_cast(byte) & ~(mask << shift)) | + (value << shift)); + } + continue; + } for (std::size_t i = pixel_bytes; i < row_bytes; i += component_bytes) { if (component_bytes == 1) { data[row + i] = static_cast( @@ -417,6 +435,9 @@ std::string pdf::ascii85_decode(const std::string &input) { std::string pdf::lzw_decode(const std::string &input, const Integer early_change) { + if (early_change != 0 && early_change != 1) { + throw std::runtime_error("LZWDecode: invalid EarlyChange"); + } std::string result; std::vector table; // codes 258 and up @@ -466,7 +487,9 @@ std::string pdf::lzw_decode(const std::string &input, } } - if (!previous.empty()) { + // ISO 32000-1 7.4.4.2 ends the table at entry 4095 and asks for a clear + // code then; without one, the codes go on at 12 bits over the full table + if (!previous.empty() && table.size() < 4096 - 258) { table.push_back(previous + entry[0]); const std::uint64_t next_code = 258 + table.size(); if (next_code + early_change == 512) { @@ -519,7 +542,7 @@ std::string pdf::apply_predictor(std::string data, const Integer predictor, } if (predictor == 2) { return apply_tiff_predictor( - std::move(data), bits_per_component, + std::move(data), colors, columns, bits_per_component, predictor_layout(colors, bits_per_component, columns)); } if (predictor >= 10 && predictor <= 15) { diff --git a/test/src/internal/pdf/pdf_filter.cpp b/test/src/internal/pdf/pdf_filter.cpp index 87bebec77..379df003f 100644 --- a/test/src/internal/pdf/pdf_filter.cpp +++ b/test/src/internal/pdf/pdf_filter.cpp @@ -53,6 +53,54 @@ TEST(PdfFilter, lzw_decode) { EXPECT_EQ(lzw_decode(encoded), "-----A---B"); } +TEST(PdfFilter, lzw_full_dictionary_and_reset) { + for (const Integer early_change : {Integer{0}, Integer{1}}) { + SCOPED_TRACE(early_change); + std::string encoded; + std::size_t bit = 0; + const auto append = [&](const std::uint32_t code, + const std::uint32_t width) { + for (std::uint32_t i = width; i > 0; --i, ++bit) { + if (bit % 8 == 0) { + encoded.push_back('\0'); + } + encoded.back() = + static_cast(static_cast(encoded.back()) | + (((code >> (i - 1)) & 1u) << (7 - bit % 8))); + } + }; + append(256, 9); + // Literal codes fill entries 258 through 4095, crossing each width + // boundary. + const auto first = static_cast(255 - early_change); + for (std::size_t i = 0; i < 3839; ++i) { + const std::uint32_t width = i < first ? 9 + : i < first + 512 ? 10 + : i < first + 1536 ? 11 + : 12; + append('A', width); + } + const std::string full = encoded; + const std::size_t full_bit = bit; + // Without a clear code, decoding goes on over the full table. + append('A', 12); + append(257, 12); + EXPECT_EQ(lzw_decode(encoded, early_change), std::string(3840, 'A')); + + encoded = full; + bit = full_bit; + append(256, 12); + append('B', 9); + append(257, 9); + EXPECT_EQ(lzw_decode(encoded, early_change), std::string(3839, 'A') + 'B'); + } +} + +TEST(PdfFilter, lzw_rejects_invalid_early_change) { + EXPECT_THROW(lzw_decode("", -1), std::runtime_error); + EXPECT_THROW(lzw_decode("", 2), std::runtime_error); +} + TEST(PdfFilter, run_length_decode) { // copy 3 literal bytes, then repeat 'z' 257-254 = 3 times EXPECT_EQ(run_length_decode("\x02" @@ -129,6 +177,20 @@ TEST(PdfFilter, tiff_predictor) { std::string("\x01\x02\x03\x02\x04\x06", 6)); } +TEST(PdfFilter, tiff_predictor_packed_components) { + // Each row starts fresh; unused low bits stay untouched. + EXPECT_EQ(apply_predictor(std::string("\xEB\xEB", 2), 2, 1, 1, 5), + std::string("\xB3\xB3", 2)); + // RGB components straddle bytes: (1,2,3), (3,1,2), (0,3,1). + EXPECT_EQ(apply_predictor(std::string("\x6E\xF6\xFF", 3), 2, 3, 2, 3), + std::string("\x6F\x63\x7F", 3)); + // Four-bit addition wraps independently of adjacent components. + EXPECT_EQ(apply_predictor(std::string("\xF2\x1A", 2), 2, 1, 4, 3), + std::string("\xF1\x2A", 2)); + EXPECT_EQ(apply_predictor("", 2, 1, 2, 3), ""); + EXPECT_THROW(apply_predictor("x", 2, 3, 4, 2), std::runtime_error); +} + TEST(PdfFilter, decode_no_filter) { const DecodeResult result = decode(Object(), Object(), "hello"); EXPECT_EQ(result.data, "hello");