From 84e99622dd9bae2b8fa7c355854886ea85d0ead0 Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Fri, 10 Nov 2023 21:32:30 +0100 Subject: [PATCH] LibPDF: Add test for SampledFunction and fix bugs found by it * SampledFunction now keeps the StreamObject it gets data from alive (doesn't matter too much in practice, but does matter in the test, where nothing else keeps the stream alive). * If a sample is an integer, we would previously sample that value twice and then divide by zero when interpolating. Make sure to sample 1 unit apart. --- Tests/LibPDF/TestPDF.cpp | 38 ++++++++++++++++++++++++-- Userland/Libraries/LibPDF/Function.cpp | 18 ++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/Tests/LibPDF/TestPDF.cpp b/Tests/LibPDF/TestPDF.cpp index 433241f84805312..72de4085ca19e6e 100644 --- a/Tests/LibPDF/TestPDF.cpp +++ b/Tests/LibPDF/TestPDF.cpp @@ -97,14 +97,16 @@ static PDF::Value make_array(Vector floats) return PDF::Value { adopt_ref(*new PDF::ArrayObject(move(values))) }; } -static PDF::PDFErrorOr> make_postscript_function(StringView program, Vector domain, Vector range) +static PDF::PDFErrorOr> make_function(int type, ReadonlyBytes data, Vector domain, Vector range, Function&)> extra_keys = nullptr) { HashMap map; - map.set(PDF::CommonNames::FunctionType, PDF::Value { 4 }); + map.set(PDF::CommonNames::FunctionType, PDF::Value { type }); map.set(PDF::CommonNames::Domain, make_array(move(domain))); map.set(PDF::CommonNames::Range, make_array(move(range))); + if (extra_keys) + extra_keys(map); auto dict = adopt_ref(*new PDF::DictObject(move(map))); - auto stream = adopt_ref(*new PDF::StreamObject(dict, MUST(ByteBuffer::copy(program.bytes())))); + auto stream = adopt_ref(*new PDF::StreamObject(dict, MUST(ByteBuffer::copy(data)))); // document isn't used for anything, but UBSan complains about a (harmless) method call on a null object without it. auto file = MUST(Core::MappedFile::map("linearized.pdf"sv)); @@ -112,6 +114,36 @@ static PDF::PDFErrorOr> make_postscript_function(St return PDF::Function::create(document, stream); } +static PDF::PDFErrorOr> make_sampled_function(ReadonlyBytes data, Vector domain, Vector range, Vector sizes) +{ + return make_function(0, data, move(domain), move(range), [&sizes](auto& map) { + map.set(PDF::CommonNames::Size, make_array(sizes)); + map.set(PDF::CommonNames::BitsPerSample, PDF::Value { 8 }); + });; +} + +TEST_CASE(sampled) +{ + auto f1 = MUST(make_sampled_function(Vector { { 0, 255, 0 } }, { 0.0f, 1.0f }, { 0.0f, 10.0f }, { 3 })); + EXPECT_EQ(MUST(f1->evaluate(Vector { 0.0f })), Vector { 0.0f }); + EXPECT_EQ(MUST(f1->evaluate(Vector { 0.25f })), Vector { 5.0f }); + EXPECT_EQ(MUST(f1->evaluate(Vector { 0.5f })), Vector { 10.0f }); + EXPECT_EQ(MUST(f1->evaluate(Vector { 0.75f })), Vector { 5.0f }); + EXPECT_EQ(MUST(f1->evaluate(Vector { 1.0f })), Vector { 0.0f }); + + auto f2 = MUST(make_sampled_function(Vector { { 0, 255, 0, 255, 0, 255 } }, { 0.0f, 1.0f }, { 0.0f, 10.0f, 0.0f, 8.0f }, { 3 })); + EXPECT_EQ(MUST(f2->evaluate(Vector { 0.0f })), (Vector { 0.0f, 8.0f })); + EXPECT_EQ(MUST(f2->evaluate(Vector { 0.25f })), (Vector { 5.0f, 4.0f })); + EXPECT_EQ(MUST(f2->evaluate(Vector { 0.5f })), (Vector { 10.0f, 0.0f })); + EXPECT_EQ(MUST(f2->evaluate(Vector { 0.75f })), (Vector { 5.0f, 4.0f })); + EXPECT_EQ(MUST(f2->evaluate(Vector { 1.0f })), (Vector { 0.0f, 8.0f })); +} + +static PDF::PDFErrorOr> make_postscript_function(StringView program, Vector domain, Vector range) +{ + return make_function(4, program.bytes(), move(domain), move(range)); +} + static NonnullRefPtr check_postscript_function(StringView program, Vector domain, Vector range) { auto function = make_postscript_function(program, move(domain), move(range)); diff --git a/Userland/Libraries/LibPDF/Function.cpp b/Userland/Libraries/LibPDF/Function.cpp index 03da5d8c33e5213..d09bba047286509 100644 --- a/Userland/Libraries/LibPDF/Function.cpp +++ b/Userland/Libraries/LibPDF/Function.cpp @@ -26,6 +26,8 @@ class SampledFunction final : public Function { virtual PDFErrorOr> evaluate(ReadonlySpan) const override; private: + SampledFunction(NonnullRefPtr); + Vector m_domain; Vector m_range; @@ -41,11 +43,18 @@ class SampledFunction final : public Function { Vector m_encode; Vector m_decode; + NonnullRefPtr m_stream; ReadonlyBytes m_sample_data; Vector mutable m_outputs; }; +SampledFunction::SampledFunction(NonnullRefPtr stream) + : m_stream(move(stream)) + , m_sample_data(m_stream->bytes()) +{ +} + PDFErrorOr> SampledFunction::create(Document* document, Vector domain, Optional> range, NonnullRefPtr stream) { @@ -127,7 +136,7 @@ SampledFunction::create(Document* document, Vector domain, Optionalbytes().size() < ceil_div(total_bits, 8ull)) return Error { Error::Type::MalformedPDF, "Function type 0 stream too small" }; - auto function = adopt_ref(*new SampledFunction()); + auto function = adopt_ref(*new SampledFunction(stream)); function->m_domain = move(domain); function->m_range = move(range.value()); function->m_sizes = move(sizes); @@ -135,7 +144,6 @@ SampledFunction::create(Document* document, Vector domain, Optionalm_order = order; function->m_encode = move(encode); function->m_decode = move(decode); - function->m_sample_data = stream->bytes(); function->m_outputs.resize(function->m_range.size()); return function; } @@ -164,6 +172,12 @@ PDFErrorOr> SampledFunction::evaluate(ReadonlySpan x) float e0 = floor(ec); float e1 = ceil(ec); + if (e0 == e1) { + if (e0 == 0.0f) + e1 = 1.0f; + else + e0 = e1 - 1.0f; + } size_t plane_size = m_sizes[0]; for (size_t i = 0; i < m_range.size(); ++i) { float s0 = m_sample_data[(size_t)e0 + i * plane_size];