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 @@ -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.
Expand Down
39 changes: 31 additions & 8 deletions src/odr/internal/pdf/pdf_filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,21 +146,39 @@ PredictorLayout predictor_layout(const Integer colors, const Integer bits,
static_cast<std::size_t>((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) {
throw std::runtime_error("TIFF predictor: data not a whole number of rows");
}

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<std::uint32_t>(bits_per_component);
const std::uint32_t mask = (1u << bits) - 1;
const auto samples = static_cast<std::uint64_t>(colors) * columns;
const auto sample = [&](const std::uint64_t index) {
const std::uint64_t bit = index * bits;
return (static_cast<std::uint8_t>(data[row + bit / 8]) >>
(8 - bits - bit % 8)) &
mask;
};
for (auto i = static_cast<std::uint64_t>(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<std::uint32_t>(8 - bits - bit % 8);
auto &byte = data[row + bit / 8];
byte = static_cast<char>(
(static_cast<std::uint8_t>(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<char>(
Expand Down Expand Up @@ -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<std::string> table; // codes 258 and up
Expand Down Expand 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) {
Expand Down Expand Up @@ -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) {
Expand Down
62 changes: 62 additions & 0 deletions test/src/internal/pdf/pdf_filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<char>(static_cast<std::uint8_t>(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<std::size_t>(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"
Expand Down Expand Up @@ -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");
Expand Down
Loading