2021-11-16 20:02:55 +00:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2021, Simon Woertz <simon@woertz.at>
|
|
|
|
*
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
*/
|
|
|
|
|
2023-12-16 14:19:34 +00:00
|
|
|
#include <AK/ByteString.h>
|
2021-11-16 22:51:03 +00:00
|
|
|
#include <AK/Forward.h>
|
LibPDF+Meta: Use a CMYK ICC profile to convert CMYK to RGB
CMYK data describes which inks a printer should use to print a color.
If a screen should display a color that's supposed to look similar
to what the printer produces, it results in a color very different
to what Color::from_cmyk() produces. (It's also printer-dependent.)
There are many ICC profiles describing printing processes. It doesn't
matter too much which one we use -- most of them look somewhat
similar, and they all look dramatically better than Color::from_cmyk().
This patch adds a function to download a zip file that Adobe offers
on their web site. They even have a page for redistribution:
https://www.adobe.com/support/downloads/iccprofiles/icc_eula_win_dist.html
(That one leads to a broken download though, so this downloads the
end-user version.)
In case we have to move off this download at some point, there are also
a whole bunch of profiles at https://www.color.org/registry/index.xalter
that "may be used, embedded, exchanged, and shared without restriction".
The adobe zip contains a whole bunch of other useful and fun profiles,
so I went with it.
For now, this only unzips the USWebCoatedSWOP.icc file though, and
installs it in ${CMAKE_BINARY_DIR}/Root/res/icc/Adobe/CMYK/. In
Serenity builds, this will make it to /res/icc/Adobe/CMYK in the
disk image. And in lagom build, after #23016 this is the
lagom res staging directory that tools can install via
Core::ResourceImplementation. `pdf` and `MacPDF` already do that,
`TestPDF` now does it too.
The final piece is that LibPDF then loads the profile from there
and uses it for DeviceCMYK color conversions.
(Doing file access from the bowels of a library is a bit weird,
especially in a system that has sandboxing built in. But LibGfx does
that in FontDatabase too already, and LibPDF uses that, so it's not a
new problem.)
2024-01-31 02:04:37 +00:00
|
|
|
#include <AK/LexicalPath.h>
|
2021-11-16 20:02:55 +00:00
|
|
|
#include <LibCore/MappedFile.h>
|
LibPDF+Meta: Use a CMYK ICC profile to convert CMYK to RGB
CMYK data describes which inks a printer should use to print a color.
If a screen should display a color that's supposed to look similar
to what the printer produces, it results in a color very different
to what Color::from_cmyk() produces. (It's also printer-dependent.)
There are many ICC profiles describing printing processes. It doesn't
matter too much which one we use -- most of them look somewhat
similar, and they all look dramatically better than Color::from_cmyk().
This patch adds a function to download a zip file that Adobe offers
on their web site. They even have a page for redistribution:
https://www.adobe.com/support/downloads/iccprofiles/icc_eula_win_dist.html
(That one leads to a broken download though, so this downloads the
end-user version.)
In case we have to move off this download at some point, there are also
a whole bunch of profiles at https://www.color.org/registry/index.xalter
that "may be used, embedded, exchanged, and shared without restriction".
The adobe zip contains a whole bunch of other useful and fun profiles,
so I went with it.
For now, this only unzips the USWebCoatedSWOP.icc file though, and
installs it in ${CMAKE_BINARY_DIR}/Root/res/icc/Adobe/CMYK/. In
Serenity builds, this will make it to /res/icc/Adobe/CMYK in the
disk image. And in lagom build, after #23016 this is the
lagom res staging directory that tools can install via
Core::ResourceImplementation. `pdf` and `MacPDF` already do that,
`TestPDF` now does it too.
The final piece is that LibPDF then loads the profile from there
and uses it for DeviceCMYK color conversions.
(Doing file access from the bowels of a library is a bit weird,
especially in a system that has sandboxing built in. But LibGfx does
that in FontDatabase too already, and LibPDF uses that, so it's not a
new problem.)
2024-01-31 02:04:37 +00:00
|
|
|
#include <LibCore/ResourceImplementationFile.h>
|
|
|
|
#include <LibCore/System.h>
|
2023-12-20 00:01:12 +00:00
|
|
|
#include <LibGfx/Bitmap.h>
|
2023-11-06 21:07:28 +00:00
|
|
|
#include <LibPDF/CommonNames.h>
|
2021-11-16 20:02:55 +00:00
|
|
|
#include <LibPDF/Document.h>
|
2023-11-06 21:07:28 +00:00
|
|
|
#include <LibPDF/Function.h>
|
2023-12-20 00:01:12 +00:00
|
|
|
#include <LibPDF/Renderer.h>
|
2021-11-16 20:02:55 +00:00
|
|
|
#include <LibTest/Macros.h>
|
|
|
|
#include <LibTest/TestCase.h>
|
|
|
|
|
LibPDF: Improve hex string parsing
A local (non-public) PDF I have lying around contains this in
a page's operator stream:
```
[<00b4003e> 3 <002600480051> 3 <005700550044004f0003> -29
<00330044> 3 <0055> -3 <004e0040> 4 <0003> -29 <004c00560003> -31
<0057004b> 4 <00480003> -37 <0050
>] TJ
```
That is, there's a newline in a hexstring after a character.
This led to `Parser error at offset 5184: Unexpected character`.
The spec says in 3.2.3 String Objects, Hexadecimal Strings:
"""Each pair of hexadecimal digits defines one byte of the string.
White-space characters (such as space, tab, carriage return, line feed,
and form feed) are ignored."""
But we didn't ignore whitespace before or after a character, only
in between the bytes.
The spec also says:
"""If the final digit of a hexadecimal string is missing—that is, if
there is an odd number of digits—the final digit is assumed to be 0."""
In that case, we were skipping the closing `>` twice -- or, more
accurately, we ignored the character after it too. This has been
wrong all the way back in #6974.
Add a test that fails if either of the two changes isn't present.
2024-01-02 00:31:27 +00:00
|
|
|
TEST_CASE(parse_value)
|
|
|
|
{
|
|
|
|
// document isn't really used for anything, only to check there's no security_handler.
|
|
|
|
auto file = MUST(Core::MappedFile::map("linearized.pdf"sv));
|
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
|
|
|
|
auto contents = "<50607><10\n>"sv;
|
|
|
|
PDF::Parser parser(contents.bytes());
|
|
|
|
parser.set_document(document->make_weak_ptr());
|
|
|
|
|
|
|
|
auto value1 = MUST(parser.parse_value(PDF::Parser::CanBeIndirectValue::No));
|
|
|
|
auto string1 = value1.get<NonnullRefPtr<PDF::Object>>()->cast<PDF::StringObject>();
|
|
|
|
EXPECT(string1->is_binary());
|
|
|
|
EXPECT_EQ(string1->string(), "\x50\x60\x70"sv);
|
|
|
|
|
|
|
|
auto value2 = MUST(parser.parse_value(PDF::Parser::CanBeIndirectValue::No));
|
|
|
|
auto string2 = value2.get<NonnullRefPtr<PDF::Object>>()->cast<PDF::StringObject>();
|
|
|
|
EXPECT(string2->is_binary());
|
|
|
|
EXPECT_EQ(string2->string(), "\x10"sv);
|
|
|
|
}
|
|
|
|
|
2021-11-16 20:02:55 +00:00
|
|
|
TEST_CASE(linearized_pdf)
|
|
|
|
{
|
2023-07-11 14:02:10 +00:00
|
|
|
auto file = MUST(Core::MappedFile::map("linearized.pdf"sv));
|
2023-05-07 18:14:06 +00:00
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
MUST(document->initialize());
|
|
|
|
EXPECT_EQ(document->get_page_count(), 1U);
|
2021-11-16 20:02:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE(non_linearized_pdf)
|
|
|
|
{
|
2023-07-11 14:02:10 +00:00
|
|
|
auto file = MUST(Core::MappedFile::map("non-linearized.pdf"sv));
|
2023-05-07 18:14:06 +00:00
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
MUST(document->initialize());
|
|
|
|
EXPECT_EQ(document->get_page_count(), 1U);
|
2021-11-16 20:02:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE(complex_pdf)
|
|
|
|
{
|
2023-07-11 14:02:10 +00:00
|
|
|
auto file = MUST(Core::MappedFile::map("complex.pdf"sv));
|
2023-05-07 18:14:06 +00:00
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
MUST(document->initialize());
|
|
|
|
EXPECT_EQ(document->get_page_count(), 3U);
|
2021-11-16 20:02:55 +00:00
|
|
|
}
|
2021-11-16 22:51:03 +00:00
|
|
|
|
|
|
|
TEST_CASE(empty_file_issue_10702)
|
|
|
|
{
|
|
|
|
AK::ReadonlyBytes empty;
|
|
|
|
auto document = PDF::Document::create(empty);
|
2022-03-06 00:30:55 +00:00
|
|
|
EXPECT(document.is_error());
|
2021-11-16 22:51:03 +00:00
|
|
|
}
|
|
|
|
|
2023-11-22 01:09:51 +00:00
|
|
|
TEST_CASE(encodig)
|
|
|
|
{
|
|
|
|
auto file = MUST(Core::MappedFile::map("encoding.pdf"sv));
|
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
MUST(document->initialize());
|
|
|
|
EXPECT_EQ(document->get_page_count(), 1U);
|
|
|
|
|
|
|
|
auto info_dict = MUST(document->info_dict()).value();
|
|
|
|
EXPECT_EQ(MUST(info_dict.author()).value(), "Nico Weber");
|
|
|
|
EXPECT_EQ(MUST(info_dict.producer()).value(), (char const*)u8"Manüally Created");
|
2023-11-22 02:07:35 +00:00
|
|
|
EXPECT_EQ(MUST(info_dict.title()).value(), (char const*)u8"Êñ©•ding test");
|
2023-11-22 01:34:23 +00:00
|
|
|
|
|
|
|
auto outline_dict = document->outline();
|
|
|
|
EXPECT_EQ(outline_dict->count, 3u);
|
|
|
|
EXPECT_EQ(outline_dict->children[0]->title, (char const*)u8"Titlè 1");
|
2023-11-22 02:07:35 +00:00
|
|
|
EXPECT_EQ(outline_dict->children[1]->title, (char const*)u8"Titlè 2");
|
2023-11-22 01:34:23 +00:00
|
|
|
EXPECT_EQ(outline_dict->children[2]->title, (char const*)u8"Titlè 3");
|
2023-11-22 01:09:51 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 22:51:03 +00:00
|
|
|
TEST_CASE(truncated_pdf_header_issue_10717)
|
|
|
|
{
|
2023-12-16 14:19:34 +00:00
|
|
|
AK::ByteString string { "%PDF-2.11%" };
|
2021-11-16 22:51:03 +00:00
|
|
|
auto document = PDF::Document::create(string.bytes());
|
2022-03-06 00:30:55 +00:00
|
|
|
EXPECT(document.is_error());
|
2021-11-16 22:51:03 +00:00
|
|
|
}
|
2023-07-11 14:49:38 +00:00
|
|
|
|
|
|
|
TEST_CASE(encrypted_with_aes)
|
|
|
|
{
|
|
|
|
auto file = MUST(Core::MappedFile::map("password-is-sup.pdf"sv));
|
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
EXPECT(document->security_handler()->try_provide_user_password("sup"sv));
|
|
|
|
MUST(document->initialize());
|
|
|
|
EXPECT_EQ(document->get_page_count(), 1U);
|
2023-07-11 16:00:54 +00:00
|
|
|
|
|
|
|
auto info_dict = MUST(document->info_dict()).value();
|
|
|
|
EXPECT_EQ(MUST(info_dict.title()).value(), "sup");
|
|
|
|
EXPECT_EQ(MUST(info_dict.creator()).value(), "TextEdit");
|
2023-07-11 14:49:38 +00:00
|
|
|
}
|
2023-07-11 16:02:16 +00:00
|
|
|
|
|
|
|
TEST_CASE(encrypted_object_stream)
|
|
|
|
{
|
|
|
|
auto file = MUST(Core::MappedFile::map("encryption_nocopy.pdf"sv));
|
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
MUST(document->initialize());
|
|
|
|
EXPECT_EQ(document->get_page_count(), 1U);
|
|
|
|
|
|
|
|
auto info_dict = MUST(document->info_dict()).value();
|
|
|
|
EXPECT_EQ(MUST(info_dict.author()).value(), "van der Knijff");
|
|
|
|
EXPECT_EQ(MUST(info_dict.creator()).value(), "Acrobat PDFMaker 9.1 voor Word");
|
|
|
|
}
|
2023-10-25 22:45:56 +00:00
|
|
|
|
2024-03-19 12:40:38 +00:00
|
|
|
TEST_CASE(resolve_indirect_reference_during_parsing)
|
|
|
|
{
|
|
|
|
auto file = MUST(Core::MappedFile::map("jbig2-globals.pdf"sv));
|
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
MUST(document->initialize());
|
|
|
|
EXPECT_EQ(document->get_page_count(), 1U);
|
|
|
|
|
|
|
|
auto jbig2_stream_value = MUST(document->get_or_load_value(5));
|
|
|
|
auto jbig2_stream = MUST(document->resolve_to<PDF::StreamObject>(jbig2_stream_value));
|
|
|
|
EXPECT_EQ(jbig2_stream->bytes().size(), 20'000U);
|
|
|
|
}
|
|
|
|
|
2023-10-25 22:45:56 +00:00
|
|
|
TEST_CASE(malformed_pdf_document)
|
|
|
|
{
|
|
|
|
Array test_inputs = {
|
|
|
|
"oss-fuzz-testcase-62065.pdf"sv
|
|
|
|
};
|
|
|
|
|
|
|
|
for (auto test_input : test_inputs) {
|
|
|
|
auto file = MUST(Core::MappedFile::map(test_input));
|
|
|
|
auto document_or_error = PDF::Document::create(file->bytes());
|
|
|
|
EXPECT(document_or_error.is_error());
|
|
|
|
}
|
|
|
|
}
|
2023-11-06 21:07:28 +00:00
|
|
|
|
|
|
|
static PDF::Value make_array(Vector<float> floats)
|
|
|
|
{
|
|
|
|
Vector<PDF::Value> values;
|
|
|
|
for (auto f : floats)
|
|
|
|
values.append(PDF::Value { f });
|
2023-12-19 02:31:27 +00:00
|
|
|
return PDF::Value { make_object<PDF::ArrayObject>(move(values)) };
|
2023-11-06 21:07:28 +00:00
|
|
|
}
|
|
|
|
|
2023-11-10 20:32:30 +00:00
|
|
|
static PDF::PDFErrorOr<NonnullRefPtr<PDF::Function>> make_function(int type, ReadonlyBytes data, Vector<float> domain, Vector<float> range, Function<void(HashMap<DeprecatedFlyString, PDF::Value>&)> extra_keys = nullptr)
|
2023-11-06 21:07:28 +00:00
|
|
|
{
|
|
|
|
HashMap<DeprecatedFlyString, PDF::Value> map;
|
2023-11-10 20:32:30 +00:00
|
|
|
map.set(PDF::CommonNames::FunctionType, PDF::Value { type });
|
2023-11-06 21:07:28 +00:00
|
|
|
map.set(PDF::CommonNames::Domain, make_array(move(domain)));
|
|
|
|
map.set(PDF::CommonNames::Range, make_array(move(range)));
|
2023-11-10 20:32:30 +00:00
|
|
|
if (extra_keys)
|
|
|
|
extra_keys(map);
|
2023-12-19 02:31:27 +00:00
|
|
|
auto dict = make_object<PDF::DictObject>(move(map));
|
|
|
|
auto stream = make_object<PDF::StreamObject>(dict, MUST(ByteBuffer::copy(data)));
|
2023-11-06 21:07:28 +00:00
|
|
|
|
|
|
|
// 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));
|
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
return PDF::Function::create(document, stream);
|
|
|
|
}
|
|
|
|
|
2023-11-10 20:32:30 +00:00
|
|
|
static PDF::PDFErrorOr<NonnullRefPtr<PDF::Function>> make_sampled_function(ReadonlyBytes data, Vector<float> domain, Vector<float> range, Vector<float> 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<u8> { { 0, 255, 0 } }, { 0.0f, 1.0f }, { 0.0f, 10.0f }, { 3 }));
|
|
|
|
EXPECT_EQ(MUST(f1->evaluate(Vector<float> { 0.0f })), Vector<float> { 0.0f });
|
|
|
|
EXPECT_EQ(MUST(f1->evaluate(Vector<float> { 0.25f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f1->evaluate(Vector<float> { 0.5f })), Vector<float> { 10.0f });
|
|
|
|
EXPECT_EQ(MUST(f1->evaluate(Vector<float> { 0.75f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f1->evaluate(Vector<float> { 1.0f })), Vector<float> { 0.0f });
|
|
|
|
|
2023-11-10 21:00:36 +00:00
|
|
|
auto f2 = MUST(make_sampled_function(Vector<u8> { { 0, 255, 255, 0, 0, 255 } }, { 0.0f, 1.0f }, { 0.0f, 10.0f, 0.0f, 8.0f }, { 3 }));
|
2023-11-10 20:32:30 +00:00
|
|
|
EXPECT_EQ(MUST(f2->evaluate(Vector<float> { 0.0f })), (Vector<float> { 0.0f, 8.0f }));
|
|
|
|
EXPECT_EQ(MUST(f2->evaluate(Vector<float> { 0.25f })), (Vector<float> { 5.0f, 4.0f }));
|
|
|
|
EXPECT_EQ(MUST(f2->evaluate(Vector<float> { 0.5f })), (Vector<float> { 10.0f, 0.0f }));
|
|
|
|
EXPECT_EQ(MUST(f2->evaluate(Vector<float> { 0.75f })), (Vector<float> { 5.0f, 4.0f }));
|
|
|
|
EXPECT_EQ(MUST(f2->evaluate(Vector<float> { 1.0f })), (Vector<float> { 0.0f, 8.0f }));
|
2023-11-10 21:19:58 +00:00
|
|
|
|
|
|
|
auto f3 = MUST(make_sampled_function(Vector<u8> { { 0, 255, 0, 255, 0, 255 } }, { 0.0f, 1.0f, 0.0f, 1.0f }, { 0.0f, 10.0f }, { 3, 2 }));
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.0f, 0.0f })), Vector<float> { 0.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.25f, 0.0f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.5f, 0.0f })), Vector<float> { 10.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.75f, 0.0f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 1.0f, 0.0f })), Vector<float> { 0.0f });
|
|
|
|
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.0f, 0.5f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.25f, 0.5f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.5f, 0.5f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.75f, 0.5f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 1.0f, 0.5f })), Vector<float> { 5.0f });
|
|
|
|
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.0f, 1.0f })), Vector<float> { 10.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.25f, 1.0f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.5f, 1.0f })), Vector<float> { 0.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 0.75f, 1.0f })), Vector<float> { 5.0f });
|
|
|
|
EXPECT_EQ(MUST(f3->evaluate(Vector<float> { 1.0f, 1.0f })), Vector<float> { 10.0f });
|
|
|
|
|
|
|
|
auto f4 = MUST(make_sampled_function(Vector<u8> { { 0, 255, 255, 0, 0, 255, 255, 0 } }, { 0.0f, 1.0f, 0.0f, 1.0f }, { 0.0f, 10.0f, 0.0f, 8.0f }, { 2, 2 }));
|
|
|
|
EXPECT_EQ(MUST(f4->evaluate(Vector<float> { 0.0f, 0.0f })), (Vector<float> { 0.0f, 8.0f }));
|
|
|
|
EXPECT_EQ(MUST(f4->evaluate(Vector<float> { 0.5f, 0.5f })), (Vector<float> { 5.0f, 4.0f }));
|
2023-11-10 20:32:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static PDF::PDFErrorOr<NonnullRefPtr<PDF::Function>> make_postscript_function(StringView program, Vector<float> domain, Vector<float> range)
|
|
|
|
{
|
|
|
|
return make_function(4, program.bytes(), move(domain), move(range));
|
|
|
|
}
|
|
|
|
|
2023-11-06 21:07:28 +00:00
|
|
|
static NonnullRefPtr<PDF::Function> check_postscript_function(StringView program, Vector<float> domain, Vector<float> range)
|
|
|
|
{
|
|
|
|
auto function = make_postscript_function(program, move(domain), move(range));
|
|
|
|
if (function.is_error())
|
|
|
|
FAIL(function.error().message());
|
|
|
|
return function.value();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void check_evaluate(StringView program, Vector<float> inputs, Vector<float> outputs)
|
|
|
|
{
|
|
|
|
Vector<float> domain;
|
|
|
|
for (size_t i = 0; i < inputs.size(); ++i) {
|
|
|
|
domain.append(-100.0f);
|
|
|
|
domain.append(100.0f);
|
|
|
|
}
|
|
|
|
Vector<float> range;
|
|
|
|
for (size_t i = 0; i < outputs.size(); ++i) {
|
|
|
|
range.append(-100.0f);
|
|
|
|
range.append(100.0f);
|
|
|
|
}
|
|
|
|
auto function = check_postscript_function(program, domain, range);
|
|
|
|
auto result = function->evaluate(inputs);
|
|
|
|
if (result.is_error())
|
|
|
|
FAIL(result.error().message());
|
|
|
|
EXPECT_EQ(result.value(), outputs);
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE(postscript)
|
|
|
|
{
|
|
|
|
// Arithmetic operators
|
|
|
|
check_evaluate("{ abs }"sv, { 0.5f }, { 0.5f });
|
|
|
|
check_evaluate("{ add }"sv, { 0.25f, 0.5f }, { 0.75f });
|
|
|
|
check_evaluate("{ atan }"sv, { 1.0f, 0.01f }, { AK::to_degrees(atan2f(0.01f, 1.0f)) });
|
|
|
|
check_evaluate("{ ceiling }"sv, { 0.5f }, { 1.0f });
|
|
|
|
check_evaluate("{ cos }"sv, { 1.0f }, { cosf(AK::to_radians(1.0f)) });
|
|
|
|
check_evaluate("{ cvi }"sv, { 0.5f }, { 0.0f });
|
|
|
|
check_evaluate("{ cvr }"sv, { 0.5f }, { 0.5f });
|
|
|
|
check_evaluate("{ div }"sv, { 0.5f, 1.0f }, { 0.5f });
|
|
|
|
check_evaluate("{ exp }"sv, { 0.0f }, { 1.0f });
|
|
|
|
check_evaluate("{ floor }"sv, { 0.5f }, { 0.0f });
|
|
|
|
check_evaluate("{ idiv }"sv, { 0.5f, 1.0f }, { 0.0f });
|
|
|
|
check_evaluate("{ ln }"sv, { 10.0f }, { logf(10.0f) });
|
|
|
|
check_evaluate("{ log }"sv, { 10.0f }, { log10f(10.0f) });
|
|
|
|
check_evaluate("{ mod }"sv, { 0.5f, 0.25f }, { 0.0f });
|
|
|
|
check_evaluate("{ mul }"sv, { 0.5f, 0.25f }, { 0.125f });
|
|
|
|
check_evaluate("{ neg }"sv, { 0.5f }, { -0.5f });
|
|
|
|
check_evaluate("{ round }"sv, { 0.5f }, { 1.0f });
|
|
|
|
check_evaluate("{ sin }"sv, { 1.0f }, { sinf(AK::to_radians(1.0f)) });
|
|
|
|
check_evaluate("{ sqrt }"sv, { 0.5f }, { sqrtf(0.5f) });
|
|
|
|
check_evaluate("{ sub }"sv, { 0.5f, 0.25f }, { 0.25f });
|
|
|
|
check_evaluate("{ truncate }"sv, { 0.5f }, { 0.0f });
|
|
|
|
|
|
|
|
// Relational, boolean, and bitwise operators
|
|
|
|
check_evaluate("{ and }"sv, { 0.0f, 1.0f }, { 0.0f });
|
|
|
|
check_evaluate("{ bitshift }"sv, { 1.0f, 3.0f }, { 8.0f });
|
|
|
|
check_evaluate("{ bitshift }"sv, { 8.0f, -2.0f }, { 2.0f });
|
|
|
|
check_evaluate("{ eq }"sv, { 0.5f, 0.5f }, { 1.0f });
|
|
|
|
check_evaluate("{ ge }"sv, { 0.5f, 0.5f }, { 1.0f });
|
|
|
|
check_evaluate("{ gt }"sv, { 0.5f, 0.5f }, { 0.0f });
|
|
|
|
check_evaluate("{ le }"sv, { 0.5f, 0.5f }, { 1.0f });
|
|
|
|
check_evaluate("{ lt }"sv, { 0.5f, 0.5f }, { 0.0f });
|
|
|
|
check_evaluate("{ ne }"sv, { 0.5f, 0.5f }, { 0.0f });
|
|
|
|
check_evaluate("{ not }"sv, { 0.5f }, { 0.0f });
|
|
|
|
check_evaluate("{ or }"sv, { 0.0f, 1.0f }, { 1.0f });
|
|
|
|
check_evaluate("{ xor }"sv, { 0.0f, 1.0f }, { 1.0f });
|
|
|
|
|
|
|
|
// Conditional operators
|
|
|
|
check_evaluate("{ { 4 } if }"sv, { 1.0f }, { 4.0f });
|
|
|
|
check_evaluate("{ { 4 } if }"sv, { 0.0f }, {});
|
|
|
|
check_evaluate("{ { 4 } { 5 } ifelse }"sv, { 1.0f }, { 4.0f });
|
|
|
|
check_evaluate("{ { 4 } { 5 } ifelse }"sv, { 0.0f }, { 5.0f });
|
|
|
|
|
|
|
|
// Stack operators
|
|
|
|
check_evaluate("{ 2 copy }"sv, { 8.0f, 0.5f, 1.0f }, { 8.0f, 0.5f, 1.0f, 0.5f, 1.0f });
|
|
|
|
check_evaluate("{ dup }"sv, { 1.0f, 0.5f }, { 1.0f, 0.5f, 0.5f });
|
|
|
|
check_evaluate("{ exch }"sv, { 8.0f, 1.0f, 0.5f }, { 8.0f, 0.5f, 1.0f });
|
|
|
|
check_evaluate("{ 1 index }"sv, { 8.0f, 1.0f, 0.5f }, { 8.0f, 1.0f, 0.5f, 1.0f });
|
|
|
|
check_evaluate("{ pop }"sv, { 8.0f, 1.0f, 0.5f }, { 8.0f, 1.0f });
|
|
|
|
check_evaluate("{ 3 1 roll }"sv, { 0.5f, 1.0f, 2.0f }, { 2.0f, 0.5f, 1.0f });
|
|
|
|
check_evaluate("{ 3 -1 roll }"sv, { 0.5f, 1.0f, 2.0f }, { 1.0f, 2.0f, 0.5f });
|
|
|
|
}
|
2023-12-20 00:01:12 +00:00
|
|
|
|
|
|
|
TEST_CASE(render)
|
|
|
|
{
|
LibPDF+Meta: Use a CMYK ICC profile to convert CMYK to RGB
CMYK data describes which inks a printer should use to print a color.
If a screen should display a color that's supposed to look similar
to what the printer produces, it results in a color very different
to what Color::from_cmyk() produces. (It's also printer-dependent.)
There are many ICC profiles describing printing processes. It doesn't
matter too much which one we use -- most of them look somewhat
similar, and they all look dramatically better than Color::from_cmyk().
This patch adds a function to download a zip file that Adobe offers
on their web site. They even have a page for redistribution:
https://www.adobe.com/support/downloads/iccprofiles/icc_eula_win_dist.html
(That one leads to a broken download though, so this downloads the
end-user version.)
In case we have to move off this download at some point, there are also
a whole bunch of profiles at https://www.color.org/registry/index.xalter
that "may be used, embedded, exchanged, and shared without restriction".
The adobe zip contains a whole bunch of other useful and fun profiles,
so I went with it.
For now, this only unzips the USWebCoatedSWOP.icc file though, and
installs it in ${CMAKE_BINARY_DIR}/Root/res/icc/Adobe/CMYK/. In
Serenity builds, this will make it to /res/icc/Adobe/CMYK in the
disk image. And in lagom build, after #23016 this is the
lagom res staging directory that tools can install via
Core::ResourceImplementation. `pdf` and `MacPDF` already do that,
`TestPDF` now does it too.
The final piece is that LibPDF then loads the profile from there
and uses it for DeviceCMYK color conversions.
(Doing file access from the bowels of a library is a bit weird,
especially in a system that has sandboxing built in. But LibGfx does
that in FontDatabase too already, and LibPDF uses that, so it's not a
new problem.)
2024-01-31 02:04:37 +00:00
|
|
|
#if !defined(AK_OS_SERENITY)
|
|
|
|
// Get from Build/lagom/bin/TestPDF to Build/lagom/Root/res.
|
|
|
|
auto source_root = LexicalPath(MUST(Core::System::current_executable_path())).parent().parent().string();
|
|
|
|
Core::ResourceImplementation::install(make<Core::ResourceImplementationFile>(MUST(String::formatted("{}/Root/res", source_root))));
|
|
|
|
#endif
|
|
|
|
|
2023-12-20 00:01:12 +00:00
|
|
|
auto file = MUST(Core::MappedFile::map("colorspaces.pdf"sv));
|
|
|
|
auto document = MUST(PDF::Document::create(file->bytes()));
|
|
|
|
MUST(document->initialize());
|
|
|
|
EXPECT_EQ(document->get_page_count(), 1U);
|
|
|
|
|
|
|
|
auto page = MUST(document->get_page(0));
|
|
|
|
auto page_size = Gfx::IntSize { 310, 370 };
|
|
|
|
auto bitmap = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRx8888, page_size));
|
|
|
|
MUST(PDF::Renderer::render(document, page, bitmap, Color::White, PDF::RenderingPreferences {}));
|
|
|
|
|
|
|
|
// DeviceGray
|
|
|
|
EXPECT_EQ(bitmap->get_pixel(270, 370 - 20), Gfx::Color::NamedColor::Black);
|
|
|
|
|
|
|
|
// MyCalRGB
|
|
|
|
EXPECT_EQ(bitmap->get_pixel(270, 370 - 80), Gfx::Color::NamedColor::Black);
|
|
|
|
|
|
|
|
// DeviceRGB
|
|
|
|
EXPECT_EQ(bitmap->get_pixel(270, 370 - 140), Gfx::Color::NamedColor::Black);
|
|
|
|
|
|
|
|
// DeviceCMYK (note: black one box further left)
|
|
|
|
EXPECT_EQ(bitmap->get_pixel(220, 370 - 200), Gfx::Color::NamedColor::Black);
|
|
|
|
|
|
|
|
// MyLab
|
|
|
|
EXPECT_EQ(bitmap->get_pixel(270, 370 - 260), Gfx::Color::NamedColor::Black);
|
|
|
|
|
|
|
|
// MyCalGray
|
|
|
|
EXPECT_EQ(bitmap->get_pixel(270, 370 - 320), Gfx::Color::NamedColor::Black);
|
|
|
|
}
|