2021-05-10 20:50:39 +03:00
|
|
|
|
/*
|
2022-03-06 03:30:55 +03:00
|
|
|
|
* Copyright (c) 2021-2022, Matthew Olsson <mattco@serenityos.org>
|
2021-05-10 20:50:39 +03:00
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
LibPDF: Add support for array image masks
An array image mask contains a min/max range for each channel,
and if each channel of a given pixel is in that channel's range,
that pixel is masked out (i.e. transparent). (It's similar to
having a single color or palette index be transparent, but it
supports a range of transparent colors if desired.)
What makes this a bit awkward is that the range is relative to the
origin bits per pixel and the inputs to the image's color space.
So an indexed (palettized) image with 4bpp has a 2-element mask
array where both entries are between 0 and 15.
We currently apply masks after converting images to a Gfx::Bitmap,
that is after converting to 8bpp sRGB. And we do this by mapping
everything to 8bpp very early on in load_image().
This leaves us with a bunch of options that are all a bit awkward:
1. Make load_image() store the up- (or for 16bpp inputs, down-)
sampled-to-8bpp pixel data. And also return if we expanded the
pixel range while resampling (for color values) or not (for
palettized images). Then, when applying the image filter,
resample the array bounds in exactly the same way. This requires
passing around more stuff.
2. Like 1, but pass in the mask array to load_image() and apply
the mask right there and then. This means we'd apply mask arrays
at a different time than other masks.
3. Make the function that computes the mask from the mask array
work from the original, unprocessed image data. This is the most
local change, but probably also requires the largest amount of
code (in return, the color mask for 16bpp images is precise, in
addition that it separates concerns the most nicely).
This goes with 3 for now.
2024-03-01 22:43:52 +03:00
|
|
|
|
#include <AK/BitStream.h>
|
2021-05-10 20:50:39 +03:00
|
|
|
|
#include <AK/Utf8View.h>
|
2021-05-24 18:15:43 +03:00
|
|
|
|
#include <LibPDF/CommonNames.h>
|
2022-03-25 08:14:50 +03:00
|
|
|
|
#include <LibPDF/Fonts/PDFFont.h>
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
#include <LibPDF/Interpolation.h>
|
2021-05-10 20:50:39 +03:00
|
|
|
|
#include <LibPDF/Renderer.h>
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
#define RENDERER_HANDLER(name) \
|
2023-10-20 18:55:38 +03:00
|
|
|
|
PDFErrorOr<void> Renderer::handle_##name([[maybe_unused]] ReadonlySpan<Value> args, [[maybe_unused]] Optional<NonnullRefPtr<DictObject>> extra_resources)
|
2021-05-23 07:09:33 +03:00
|
|
|
|
|
2022-12-14 18:10:26 +03:00
|
|
|
|
#define RENDERER_TODO(name) \
|
|
|
|
|
RENDERER_HANDLER(name) \
|
|
|
|
|
{ \
|
|
|
|
|
return Error(Error::Type::RenderingUnsupported, "draw operation: " #name); \
|
2021-05-23 07:09:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-10 20:50:39 +03:00
|
|
|
|
namespace PDF {
|
|
|
|
|
|
2023-11-14 18:35:40 +03:00
|
|
|
|
// Use a RAII object to restore the graphics state, to make sure it gets restored even if
|
|
|
|
|
// a TRY(handle_operator()) causes us to exit the operators loop early.
|
|
|
|
|
// Explicitly resize stack size at the end so that if the recursive document contains
|
|
|
|
|
// `q q unsupportedop Q Q`, we undo the stack pushes from the inner `q q` even if
|
|
|
|
|
// `unsupportedop` terminates processing the inner instruction stream before `Q Q`
|
|
|
|
|
// would normally pop state.
|
|
|
|
|
class Renderer::ScopedState {
|
|
|
|
|
public:
|
|
|
|
|
ScopedState(Renderer& renderer)
|
|
|
|
|
: m_renderer(renderer)
|
|
|
|
|
, m_starting_stack_depth(m_renderer.m_graphics_state_stack.size())
|
|
|
|
|
{
|
|
|
|
|
MUST(m_renderer.handle_save_state({}));
|
|
|
|
|
}
|
|
|
|
|
~ScopedState()
|
|
|
|
|
{
|
|
|
|
|
m_renderer.m_graphics_state_stack.shrink(m_starting_stack_depth);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
Renderer& m_renderer;
|
|
|
|
|
size_t m_starting_stack_depth;
|
|
|
|
|
};
|
|
|
|
|
|
2023-12-07 15:53:13 +03:00
|
|
|
|
PDFErrorsOr<void> Renderer::render(Document& document, Page const& page, RefPtr<Gfx::Bitmap> bitmap, Color background_color, RenderingPreferences rendering_preferences)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2023-12-07 15:53:13 +03:00
|
|
|
|
return Renderer(document, page, bitmap, background_color, rendering_preferences).render();
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-25 17:36:21 +03:00
|
|
|
|
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> Renderer::apply_page_rotation(NonnullRefPtr<Gfx::Bitmap> bitmap, Page const& page, int extra_degrees)
|
|
|
|
|
{
|
|
|
|
|
int rotation_count = ((page.rotate + extra_degrees) / 90) % 4;
|
|
|
|
|
if (rotation_count == 1)
|
|
|
|
|
bitmap = TRY(bitmap->rotated(Gfx::RotationDirection::Clockwise));
|
|
|
|
|
else if (rotation_count == 2)
|
|
|
|
|
bitmap = TRY(bitmap->rotated(Gfx::RotationDirection::Flip));
|
|
|
|
|
else if (rotation_count == 3)
|
|
|
|
|
bitmap = TRY(bitmap->rotated(Gfx::RotationDirection::CounterClockwise));
|
|
|
|
|
return bitmap;
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-23 16:03:26 +03:00
|
|
|
|
static void rect_path(Gfx::Path& path, float x, float y, float width, float height)
|
|
|
|
|
{
|
|
|
|
|
path.move_to({ x, y });
|
|
|
|
|
path.line_to({ x + width, y });
|
|
|
|
|
path.line_to({ x + width, y + height });
|
|
|
|
|
path.line_to({ x, y + height });
|
|
|
|
|
path.close();
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-26 07:53:32 +03:00
|
|
|
|
template<typename T>
|
|
|
|
|
static void rect_path(Gfx::Path& path, Gfx::Rect<T> rect)
|
|
|
|
|
{
|
|
|
|
|
return rect_path(path, rect.x(), rect.y(), rect.width(), rect.height());
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-23 16:03:26 +03:00
|
|
|
|
template<typename T>
|
2023-02-03 02:11:27 +03:00
|
|
|
|
static Gfx::Path rect_path(Gfx::Rect<T> const& rect)
|
2022-11-23 16:03:26 +03:00
|
|
|
|
{
|
2022-11-26 07:53:32 +03:00
|
|
|
|
Gfx::Path path;
|
|
|
|
|
rect_path(path, rect);
|
|
|
|
|
return path;
|
2022-11-23 16:03:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-12-07 15:53:13 +03:00
|
|
|
|
Renderer::Renderer(RefPtr<Document> document, Page const& page, RefPtr<Gfx::Bitmap> bitmap, Color background_color, RenderingPreferences rendering_preferences)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
: m_document(document)
|
|
|
|
|
, m_bitmap(bitmap)
|
|
|
|
|
, m_page(page)
|
|
|
|
|
, m_painter(*bitmap)
|
2022-03-24 21:50:37 +03:00
|
|
|
|
, m_anti_aliasing_painter(m_painter)
|
2022-11-23 16:11:29 +03:00
|
|
|
|
, m_rendering_preferences(rendering_preferences)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
auto media_box = m_page.media_box;
|
|
|
|
|
|
2021-05-28 21:55:51 +03:00
|
|
|
|
Gfx::AffineTransform userspace_matrix;
|
|
|
|
|
userspace_matrix.translate(media_box.lower_left_x, media_box.lower_left_y);
|
2021-05-10 20:50:39 +03:00
|
|
|
|
|
2022-03-30 07:27:17 +03:00
|
|
|
|
float width = media_box.width();
|
|
|
|
|
float height = media_box.height();
|
2021-05-10 20:50:39 +03:00
|
|
|
|
float scale_x = static_cast<float>(bitmap->width()) / width;
|
|
|
|
|
float scale_y = static_cast<float>(bitmap->height()) / height;
|
2021-05-28 21:55:51 +03:00
|
|
|
|
userspace_matrix.scale(scale_x, scale_y);
|
2021-05-10 20:50:39 +03:00
|
|
|
|
|
2021-05-28 22:18:11 +03:00
|
|
|
|
// PDF user-space coordinate y axis increases from bottom to top, so we have to
|
|
|
|
|
// insert a horizontal reflection about the vertical midpoint into our transformation
|
|
|
|
|
// matrix
|
|
|
|
|
|
|
|
|
|
static Gfx::AffineTransform horizontal_reflection_matrix = { 1, 0, 0, -1, 0, 0 };
|
|
|
|
|
|
|
|
|
|
userspace_matrix.multiply(horizontal_reflection_matrix);
|
|
|
|
|
userspace_matrix.translate(0.0f, -height);
|
|
|
|
|
|
2023-02-03 02:11:27 +03:00
|
|
|
|
auto initial_clipping_path = rect_path(userspace_matrix.map(Gfx::FloatRect(0, 0, width, height)));
|
2022-11-23 16:03:26 +03:00
|
|
|
|
m_graphics_state_stack.append(GraphicsState { userspace_matrix, { initial_clipping_path, initial_clipping_path } });
|
2021-05-10 20:50:39 +03:00
|
|
|
|
|
2023-12-07 15:53:13 +03:00
|
|
|
|
m_bitmap->fill(background_color);
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
LibPDF: Switch to best-effort PDF rendering
The current rendering routine aborts as soon as an error is found during
rendering, which potentially severely limits the contents we show on
screen. Moreover, whenever an error happens the PDFViewer widget shows
an error dialog, and doesn't display the bitmap that has been painted so
far.
This commit improves the situation in both fronts, implementing
rendering now with a best-effort approach. Firstly, execution of
operations isn't halted after an operand results in an error, but
instead execution of all operations is always attempted, and all
collected errors are returned in bulk. Secondly, PDFViewer now always
displays the resulting bitmap, regardless of error being produced or
not. To communicate errors, an on_render_errors callback has been added
so clients can subscribe to these events and handle them as appropriate.
2022-12-14 18:00:40 +03:00
|
|
|
|
PDFErrorsOr<void> Renderer::render()
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2023-07-12 16:27:26 +03:00
|
|
|
|
auto operators = TRY(Parser::parse_operators(m_document, TRY(m_page.page_contents(*m_document))));
|
2021-05-10 20:50:39 +03:00
|
|
|
|
|
LibPDF: Switch to best-effort PDF rendering
The current rendering routine aborts as soon as an error is found during
rendering, which potentially severely limits the contents we show on
screen. Moreover, whenever an error happens the PDFViewer widget shows
an error dialog, and doesn't display the bitmap that has been painted so
far.
This commit improves the situation in both fronts, implementing
rendering now with a best-effort approach. Firstly, execution of
operations isn't halted after an operand results in an error, but
instead execution of all operations is always attempted, and all
collected errors are returned in bulk. Secondly, PDFViewer now always
displays the resulting bitmap, regardless of error being produced or
not. To communicate errors, an on_render_errors callback has been added
so clients can subscribe to these events and handle them as appropriate.
2022-12-14 18:00:40 +03:00
|
|
|
|
Errors errors;
|
|
|
|
|
for (auto& op : operators) {
|
|
|
|
|
auto maybe_error = handle_operator(op);
|
|
|
|
|
if (maybe_error.is_error()) {
|
|
|
|
|
errors.add_error(maybe_error.release_error());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!errors.errors().is_empty())
|
|
|
|
|
return errors;
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-21 08:28:41 +03:00
|
|
|
|
PDFErrorOr<void> Renderer::handle_operator(Operator const& op, Optional<NonnullRefPtr<DictObject>> extra_resources)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2022-03-26 01:00:11 +03:00
|
|
|
|
switch (op.type()) {
|
LibPDF: Switch to best-effort PDF rendering
The current rendering routine aborts as soon as an error is found during
rendering, which potentially severely limits the contents we show on
screen. Moreover, whenever an error happens the PDFViewer widget shows
an error dialog, and doesn't display the bitmap that has been painted so
far.
This commit improves the situation in both fronts, implementing
rendering now with a best-effort approach. Firstly, execution of
operations isn't halted after an operand results in an error, but
instead execution of all operations is always attempted, and all
collected errors are returned in bulk. Secondly, PDFViewer now always
displays the resulting bitmap, regardless of error being produced or
not. To communicate errors, an on_render_errors callback has been added
so clients can subscribe to these events and handle them as appropriate.
2022-12-14 18:00:40 +03:00
|
|
|
|
#define V(name, snake_name, symbol) \
|
|
|
|
|
case OperatorType::name: \
|
|
|
|
|
TRY(handle_##snake_name(op.arguments(), extra_resources)); \
|
2021-05-23 07:09:33 +03:00
|
|
|
|
break;
|
2022-03-26 01:00:11 +03:00
|
|
|
|
ENUMERATE_OPERATORS(V)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
#undef V
|
2022-03-26 01:00:11 +03:00
|
|
|
|
case OperatorType::TextNextLineShowString:
|
LibPDF: Switch to best-effort PDF rendering
The current rendering routine aborts as soon as an error is found during
rendering, which potentially severely limits the contents we show on
screen. Moreover, whenever an error happens the PDFViewer widget shows
an error dialog, and doesn't display the bitmap that has been painted so
far.
This commit improves the situation in both fronts, implementing
rendering now with a best-effort approach. Firstly, execution of
operations isn't halted after an operand results in an error, but
instead execution of all operations is always attempted, and all
collected errors are returned in bulk. Secondly, PDFViewer now always
displays the resulting bitmap, regardless of error being produced or
not. To communicate errors, an on_render_errors callback has been added
so clients can subscribe to these events and handle them as appropriate.
2022-12-14 18:00:40 +03:00
|
|
|
|
TRY(handle_text_next_line_show_string(op.arguments()));
|
2021-05-23 07:09:33 +03:00
|
|
|
|
break;
|
2022-03-26 01:00:11 +03:00
|
|
|
|
case OperatorType::TextNextLineShowStringSetSpacing:
|
LibPDF: Switch to best-effort PDF rendering
The current rendering routine aborts as soon as an error is found during
rendering, which potentially severely limits the contents we show on
screen. Moreover, whenever an error happens the PDFViewer widget shows
an error dialog, and doesn't display the bitmap that has been painted so
far.
This commit improves the situation in both fronts, implementing
rendering now with a best-effort approach. Firstly, execution of
operations isn't halted after an operand results in an error, but
instead execution of all operations is always attempted, and all
collected errors are returned in bulk. Secondly, PDFViewer now always
displays the resulting bitmap, regardless of error being produced or
not. To communicate errors, an on_render_errors callback has been added
so clients can subscribe to these events and handle them as appropriate.
2022-12-14 18:00:40 +03:00
|
|
|
|
TRY(handle_text_next_line_show_string_set_spacing(op.arguments()));
|
2021-05-23 07:09:33 +03:00
|
|
|
|
break;
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
2022-03-06 04:12:58 +03:00
|
|
|
|
|
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(save_state)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
m_graphics_state_stack.append(state());
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(restore_state)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
m_graphics_state_stack.take_last();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(concatenate_matrix)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
Gfx::AffineTransform new_transform(
|
|
|
|
|
args[0].to_float(),
|
|
|
|
|
args[1].to_float(),
|
|
|
|
|
args[2].to_float(),
|
|
|
|
|
args[3].to_float(),
|
|
|
|
|
args[4].to_float(),
|
|
|
|
|
args[5].to_float());
|
|
|
|
|
|
|
|
|
|
state().ctm.multiply(new_transform);
|
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(set_line_width)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
state().line_width = args[0].to_float();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(set_line_cap)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2021-09-19 21:56:05 +03:00
|
|
|
|
state().line_cap_style = static_cast<LineCapStyle>(args[0].get<int>());
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(set_line_join)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2021-09-19 21:56:05 +03:00
|
|
|
|
state().line_join_style = static_cast<LineJoinStyle>(args[0].get<int>());
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(set_miter_limit)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
state().miter_limit = args[0].to_float();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(set_dash_pattern)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2022-03-06 03:30:55 +03:00
|
|
|
|
auto dash_array = MUST(m_document->resolve_to<ArrayObject>(args[0]));
|
2021-05-10 20:50:39 +03:00
|
|
|
|
Vector<int> pattern;
|
|
|
|
|
for (auto& element : *dash_array)
|
2022-11-22 21:38:24 +03:00
|
|
|
|
pattern.append(element.to_int());
|
2023-03-24 23:56:09 +03:00
|
|
|
|
state().line_dash_pattern = LineDashPattern { pattern, args[1].to_int() };
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-19 14:58:22 +03:00
|
|
|
|
RENDERER_HANDLER(set_color_rendering_intent)
|
|
|
|
|
{
|
|
|
|
|
state().color_rendering_intent = MUST(m_document->resolve_to<NameObject>(args[0]))->name();
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2023-07-12 21:25:22 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_flatness_tolerance)
|
|
|
|
|
{
|
|
|
|
|
state().flatness_tolerance = args[0].to_float();
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2021-05-28 00:22:24 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_graphics_state_from_dict)
|
|
|
|
|
{
|
2022-11-21 08:28:41 +03:00
|
|
|
|
auto resources = extra_resources.value_or(m_page.resources);
|
2022-03-06 03:30:55 +03:00
|
|
|
|
auto dict_name = MUST(m_document->resolve_to<NameObject>(args[0]))->name();
|
2022-11-21 08:28:41 +03:00
|
|
|
|
auto ext_gstate_dict = MUST(resources->get_dict(m_document, CommonNames::ExtGState));
|
2022-03-06 03:30:55 +03:00
|
|
|
|
auto target_dict = MUST(ext_gstate_dict->get_dict(m_document, dict_name));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
TRY(set_graphics_state_from_dict(target_dict));
|
|
|
|
|
return {};
|
2021-05-28 00:22:24 +03:00
|
|
|
|
}
|
2021-05-10 20:50:39 +03:00
|
|
|
|
|
2021-05-23 21:43:28 +03:00
|
|
|
|
RENDERER_HANDLER(path_move)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2021-05-23 21:43:28 +03:00
|
|
|
|
m_current_path.move_to(map(args[0].to_float(), args[1].to_float()));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_line)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2021-05-23 21:43:28 +03:00
|
|
|
|
VERIFY(!m_current_path.segments().is_empty());
|
|
|
|
|
m_current_path.line_to(map(args[0].to_float(), args[1].to_float()));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-21 08:19:26 +03:00
|
|
|
|
RENDERER_HANDLER(path_cubic_bezier_curve)
|
|
|
|
|
{
|
|
|
|
|
VERIFY(args.size() == 6);
|
|
|
|
|
m_current_path.cubic_bezier_curve_to(
|
|
|
|
|
map(args[0].to_float(), args[1].to_float()),
|
|
|
|
|
map(args[2].to_float(), args[3].to_float()),
|
|
|
|
|
map(args[4].to_float(), args[5].to_float()));
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(path_cubic_bezier_curve_no_first_control)
|
|
|
|
|
{
|
|
|
|
|
VERIFY(args.size() == 4);
|
|
|
|
|
VERIFY(!m_current_path.segments().is_empty());
|
2023-03-06 16:17:01 +03:00
|
|
|
|
auto current_point = (*m_current_path.segments().rbegin())->point();
|
2022-11-21 08:19:26 +03:00
|
|
|
|
m_current_path.cubic_bezier_curve_to(
|
|
|
|
|
current_point,
|
|
|
|
|
map(args[0].to_float(), args[1].to_float()),
|
|
|
|
|
map(args[2].to_float(), args[3].to_float()));
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(path_cubic_bezier_curve_no_second_control)
|
|
|
|
|
{
|
|
|
|
|
VERIFY(args.size() == 4);
|
|
|
|
|
VERIFY(!m_current_path.segments().is_empty());
|
|
|
|
|
auto first_control_point = map(args[0].to_float(), args[1].to_float());
|
|
|
|
|
auto second_control_point = map(args[2].to_float(), args[3].to_float());
|
|
|
|
|
m_current_path.cubic_bezier_curve_to(
|
|
|
|
|
first_control_point,
|
|
|
|
|
second_control_point,
|
|
|
|
|
second_control_point);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2021-05-23 07:09:33 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(path_close)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2021-05-23 21:43:28 +03:00
|
|
|
|
m_current_path.close();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_append_rect)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2022-11-26 07:53:32 +03:00
|
|
|
|
auto rect = Gfx::FloatRect(args[0].to_float(), args[1].to_float(), args[2].to_float(), args[3].to_float());
|
2024-01-16 23:32:26 +03:00
|
|
|
|
// Note: The path of the rectangle is mapped (rather than the rectangle).
|
|
|
|
|
// This is because negative width/heights are possible, and result in different
|
|
|
|
|
// winding orders, but this is lost by Gfx::AffineTransform::map().
|
|
|
|
|
m_current_path.append_path(map(rect_path(rect)));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-17 04:38:13 +03:00
|
|
|
|
void Renderer::activate_clip()
|
2022-11-23 16:03:26 +03:00
|
|
|
|
{
|
2023-02-03 02:11:27 +03:00
|
|
|
|
auto bounding_box = state().clipping_paths.current.bounding_box();
|
2022-11-23 16:03:26 +03:00
|
|
|
|
m_painter.clear_clip_rect();
|
2022-11-23 16:11:29 +03:00
|
|
|
|
if (m_rendering_preferences.show_clipping_paths) {
|
|
|
|
|
m_painter.stroke_path(rect_path(bounding_box), Color::Black, 1);
|
|
|
|
|
}
|
2022-11-23 16:03:26 +03:00
|
|
|
|
m_painter.add_clip_rect(bounding_box.to_type<int>());
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-17 04:38:13 +03:00
|
|
|
|
void Renderer::deactivate_clip()
|
2022-11-23 16:03:26 +03:00
|
|
|
|
{
|
|
|
|
|
m_painter.clear_clip_rect();
|
|
|
|
|
state().clipping_paths.current = state().clipping_paths.next;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-17 04:38:13 +03:00
|
|
|
|
///
|
|
|
|
|
// Path painting operations
|
|
|
|
|
///
|
|
|
|
|
|
|
|
|
|
void Renderer::begin_path_paint()
|
|
|
|
|
{
|
2024-01-17 04:45:12 +03:00
|
|
|
|
if (m_rendering_preferences.clip_paths)
|
|
|
|
|
activate_clip();
|
2024-01-17 04:38:13 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::end_path_paint()
|
|
|
|
|
{
|
|
|
|
|
m_current_path.clear();
|
2024-01-17 04:45:12 +03:00
|
|
|
|
if (m_rendering_preferences.clip_paths)
|
|
|
|
|
deactivate_clip();
|
2024-01-17 04:38:13 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_stroke)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2022-11-23 16:03:26 +03:00
|
|
|
|
begin_path_paint();
|
2023-12-05 15:51:42 +03:00
|
|
|
|
if (state().stroke_style.has<NonnullRefPtr<Gfx::PaintStyle>>()) {
|
2024-02-21 05:38:45 +03:00
|
|
|
|
m_anti_aliasing_painter.stroke_path(m_current_path, state().stroke_style.get<NonnullRefPtr<Gfx::PaintStyle>>(), line_width());
|
2023-12-05 15:51:42 +03:00
|
|
|
|
} else {
|
2024-02-21 05:38:45 +03:00
|
|
|
|
m_anti_aliasing_painter.stroke_path(m_current_path, state().stroke_style.get<Color>(), line_width());
|
2023-12-05 15:51:42 +03:00
|
|
|
|
}
|
2022-11-23 16:03:26 +03:00
|
|
|
|
end_path_paint();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_close_and_stroke)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2021-05-23 21:43:28 +03:00
|
|
|
|
m_current_path.close();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
TRY(handle_path_stroke(args));
|
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_fill_nonzero)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2022-11-23 16:03:26 +03:00
|
|
|
|
begin_path_paint();
|
2023-07-24 21:02:36 +03:00
|
|
|
|
m_current_path.close_all_subpaths();
|
2023-12-05 15:51:42 +03:00
|
|
|
|
if (state().paint_style.has<NonnullRefPtr<Gfx::PaintStyle>>()) {
|
|
|
|
|
m_anti_aliasing_painter.fill_path(m_current_path, state().paint_style.get<NonnullRefPtr<Gfx::PaintStyle>>(), 1.0, Gfx::Painter::WindingRule::Nonzero);
|
|
|
|
|
} else {
|
|
|
|
|
m_anti_aliasing_painter.fill_path(m_current_path, state().paint_style.get<Color>(), Gfx::Painter::WindingRule::Nonzero);
|
|
|
|
|
}
|
2022-11-23 16:03:26 +03:00
|
|
|
|
end_path_paint();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_fill_nonzero_deprecated)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2022-11-24 04:38:23 +03:00
|
|
|
|
return handle_path_fill_nonzero(args);
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_fill_evenodd)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2022-11-23 16:03:26 +03:00
|
|
|
|
begin_path_paint();
|
2023-07-24 21:02:36 +03:00
|
|
|
|
m_current_path.close_all_subpaths();
|
2023-12-05 15:51:42 +03:00
|
|
|
|
if (state().paint_style.has<NonnullRefPtr<Gfx::PaintStyle>>()) {
|
|
|
|
|
m_anti_aliasing_painter.fill_path(m_current_path, state().paint_style.get<NonnullRefPtr<Gfx::PaintStyle>>(), 1.0, Gfx::Painter::WindingRule::EvenOdd);
|
|
|
|
|
} else {
|
|
|
|
|
m_anti_aliasing_painter.fill_path(m_current_path, state().paint_style.get<Color>(), Gfx::Painter::WindingRule::EvenOdd);
|
|
|
|
|
}
|
2022-11-23 16:03:26 +03:00
|
|
|
|
end_path_paint();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_fill_stroke_nonzero)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2023-12-05 15:51:42 +03:00
|
|
|
|
if (state().stroke_style.has<NonnullRefPtr<Gfx::PaintStyle>>()) {
|
2024-02-21 05:38:45 +03:00
|
|
|
|
m_anti_aliasing_painter.stroke_path(m_current_path, state().stroke_style.get<NonnullRefPtr<Gfx::PaintStyle>>(), line_width());
|
2023-12-05 15:51:42 +03:00
|
|
|
|
} else {
|
2024-02-21 05:38:45 +03:00
|
|
|
|
m_anti_aliasing_painter.stroke_path(m_current_path, state().stroke_style.get<Color>(), line_width());
|
2023-12-05 15:51:42 +03:00
|
|
|
|
}
|
2022-11-24 04:38:23 +03:00
|
|
|
|
return handle_path_fill_nonzero(args);
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_fill_stroke_evenodd)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2023-12-05 15:51:42 +03:00
|
|
|
|
if (state().stroke_style.has<NonnullRefPtr<Gfx::PaintStyle>>()) {
|
2024-02-21 05:38:45 +03:00
|
|
|
|
m_anti_aliasing_painter.stroke_path(m_current_path, state().stroke_style.get<NonnullRefPtr<Gfx::PaintStyle>>(), line_width());
|
2023-12-05 15:51:42 +03:00
|
|
|
|
} else {
|
2024-02-21 05:38:45 +03:00
|
|
|
|
m_anti_aliasing_painter.stroke_path(m_current_path, state().stroke_style.get<Color>(), line_width());
|
2023-12-05 15:51:42 +03:00
|
|
|
|
}
|
2022-11-24 04:38:23 +03:00
|
|
|
|
return handle_path_fill_evenodd(args);
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_close_fill_stroke_nonzero)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2021-05-23 21:43:28 +03:00
|
|
|
|
m_current_path.close();
|
2022-11-24 04:38:23 +03:00
|
|
|
|
return handle_path_fill_stroke_nonzero(args);
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_close_fill_stroke_evenodd)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2021-05-23 21:43:28 +03:00
|
|
|
|
m_current_path.close();
|
2022-11-24 04:38:23 +03:00
|
|
|
|
return handle_path_fill_stroke_evenodd(args);
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(path_end)
|
|
|
|
|
{
|
2022-11-23 16:03:26 +03:00
|
|
|
|
begin_path_paint();
|
|
|
|
|
end_path_paint();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 07:09:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 21:43:28 +03:00
|
|
|
|
RENDERER_HANDLER(path_intersect_clip_nonzero)
|
|
|
|
|
{
|
2022-11-23 16:03:26 +03:00
|
|
|
|
// FIXME: Support arbitrary path clipping in Path and utilize that here
|
|
|
|
|
auto next_clipping_bbox = state().clipping_paths.next.bounding_box();
|
|
|
|
|
next_clipping_bbox.intersect(m_current_path.bounding_box());
|
|
|
|
|
state().clipping_paths.next = rect_path(next_clipping_bbox);
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 21:43:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(path_intersect_clip_evenodd)
|
|
|
|
|
{
|
2022-11-23 16:03:26 +03:00
|
|
|
|
// FIXME: Should have different behavior than path_intersect_clip_nonzero
|
|
|
|
|
return handle_path_intersect_clip_nonzero(args);
|
2021-05-23 21:43:28 +03:00
|
|
|
|
}
|
2021-05-23 07:09:33 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(text_begin)
|
|
|
|
|
{
|
|
|
|
|
m_text_matrix = Gfx::AffineTransform();
|
|
|
|
|
m_text_line_matrix = Gfx::AffineTransform();
|
2024-01-10 17:45:23 +03:00
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 07:09:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(text_end)
|
|
|
|
|
{
|
|
|
|
|
// FIXME: Do we need to do anything here?
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 07:09:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(text_set_char_space)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
text_state().character_spacing = args[0].to_float();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_set_word_space)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
text_state().word_spacing = args[0].to_float();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_set_horizontal_scale)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
|
|
|
|
text_state().horizontal_scaling = args[0].to_float() / 100.0f;
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_set_leading)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
text_state().leading = args[0].to_float();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-17 04:15:58 +03:00
|
|
|
|
PDFErrorOr<NonnullRefPtr<PDFFont>> Renderer::get_font(FontCacheKey const& key)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
LibPDF: Cache fonts per page
Previously, every time a page switched fonts, we'd completely
re-parse the font.
Now, we cache fonts in Renderer, effectively caching them per page.
It'd be nice to have an LRU cache across pages too, but that's a
bigger change, and this already helps a lot.
Font size is part of the cache key, which means we re-parse the same
font at different font sizes. That could be better too, but again,
it's a big help as-is already.
Takes rendering the 1310 pages of the PDF 1.7 reference with
Build/lagom/bin/pdf --debugging-stats \
~/Downloads/pdf_reference_1-7.pdf
from 71 s to 11s :^)
Going through pages especially in the index is noticeably snappier.
(On the PDF 2.0 spec, ISO_32000-2-2020_sponsored.pdf, it's less
dramatic: From 19s to 16s.)
2023-10-10 20:55:35 +03:00
|
|
|
|
auto it = m_font_cache.find(key);
|
LibPDF: Update font size after getting font from cache
Page 1 of 0000277.pdf does:
BT 22 0 0 22 59 28 Tm /TT2 1 Tf
(Presented at Photonics West OPTO, February 17, 2016) Tj ET
BT 32 0 0 32 269 426 Tm /TT1 1 Tf
(Robert W. Boyd) Tj ET
BT 22 0 0 22 253 357 Tm /TT2 1 Tf
(Department of Physics and) Tj ET
BT 22 0 0 22 105 326 Tm /TT2 1 Tf
(Max-Planck Centre for Extreme and Quantum Photonics) Tj ET
Every line begins a text operation, then updates the font matrix,
selects a font (TT2, TT1, TT2, TT1), draws some text and ends the text
operation.
`Tm` (which sets the font matrix) contains a scale, and uses that
to update the font size of the currently-active font (cf #20084).
But in this file, we `Tm` first and `Tf` (font selection) second,
so this updates the size of the old font. So when we pull it out
of the cache again on line 3, it would still have the old size
from the `Tm` on line 2.
(The whole text scaling logic in LibPDF imho needs a rethink; the
current approach also causes issues with zero-width glyphs which
currently lead to divisions by zero. But that's for another PR.)
Fixes another regression from c8510b58a366320 (which I've accidentally
referred to by 2340e834cd in another commit).
2023-11-27 02:07:45 +03:00
|
|
|
|
if (it != m_font_cache.end()) {
|
|
|
|
|
// Update the potentially-stale size set in text_set_matrix_and_line_matrix().
|
|
|
|
|
it->value->set_font_size(key.font_size);
|
LibPDF: Cache fonts per page
Previously, every time a page switched fonts, we'd completely
re-parse the font.
Now, we cache fonts in Renderer, effectively caching them per page.
It'd be nice to have an LRU cache across pages too, but that's a
bigger change, and this already helps a lot.
Font size is part of the cache key, which means we re-parse the same
font at different font sizes. That could be better too, but again,
it's a big help as-is already.
Takes rendering the 1310 pages of the PDF 1.7 reference with
Build/lagom/bin/pdf --debugging-stats \
~/Downloads/pdf_reference_1-7.pdf
from 71 s to 11s :^)
Going through pages especially in the index is noticeably snappier.
(On the PDF 2.0 spec, ISO_32000-2-2020_sponsored.pdf, it's less
dramatic: From 19s to 16s.)
2023-10-10 20:55:35 +03:00
|
|
|
|
return it->value;
|
LibPDF: Update font size after getting font from cache
Page 1 of 0000277.pdf does:
BT 22 0 0 22 59 28 Tm /TT2 1 Tf
(Presented at Photonics West OPTO, February 17, 2016) Tj ET
BT 32 0 0 32 269 426 Tm /TT1 1 Tf
(Robert W. Boyd) Tj ET
BT 22 0 0 22 253 357 Tm /TT2 1 Tf
(Department of Physics and) Tj ET
BT 22 0 0 22 105 326 Tm /TT2 1 Tf
(Max-Planck Centre for Extreme and Quantum Photonics) Tj ET
Every line begins a text operation, then updates the font matrix,
selects a font (TT2, TT1, TT2, TT1), draws some text and ends the text
operation.
`Tm` (which sets the font matrix) contains a scale, and uses that
to update the font size of the currently-active font (cf #20084).
But in this file, we `Tm` first and `Tf` (font selection) second,
so this updates the size of the old font. So when we pull it out
of the cache again on line 3, it would still have the old size
from the `Tm` on line 2.
(The whole text scaling logic in LibPDF imho needs a rethink; the
current approach also causes issues with zero-width glyphs which
currently lead to divisions by zero. But that's for another PR.)
Fixes another regression from c8510b58a366320 (which I've accidentally
referred to by 2340e834cd in another commit).
2023-11-27 02:07:45 +03:00
|
|
|
|
}
|
LibPDF: Cache fonts per page
Previously, every time a page switched fonts, we'd completely
re-parse the font.
Now, we cache fonts in Renderer, effectively caching them per page.
It'd be nice to have an LRU cache across pages too, but that's a
bigger change, and this already helps a lot.
Font size is part of the cache key, which means we re-parse the same
font at different font sizes. That could be better too, but again,
it's a big help as-is already.
Takes rendering the 1310 pages of the PDF 1.7 reference with
Build/lagom/bin/pdf --debugging-stats \
~/Downloads/pdf_reference_1-7.pdf
from 71 s to 11s :^)
Going through pages especially in the index is noticeably snappier.
(On the PDF 2.0 spec, ISO_32000-2-2020_sponsored.pdf, it's less
dramatic: From 19s to 16s.)
2023-10-10 20:55:35 +03:00
|
|
|
|
|
2023-11-17 04:15:58 +03:00
|
|
|
|
auto font = TRY(PDFFont::create(m_document, key.font_dictionary, key.font_size));
|
LibPDF: Cache fonts per page
Previously, every time a page switched fonts, we'd completely
re-parse the font.
Now, we cache fonts in Renderer, effectively caching them per page.
It'd be nice to have an LRU cache across pages too, but that's a
bigger change, and this already helps a lot.
Font size is part of the cache key, which means we re-parse the same
font at different font sizes. That could be better too, but again,
it's a big help as-is already.
Takes rendering the 1310 pages of the PDF 1.7 reference with
Build/lagom/bin/pdf --debugging-stats \
~/Downloads/pdf_reference_1-7.pdf
from 71 s to 11s :^)
Going through pages especially in the index is noticeably snappier.
(On the PDF 2.0 spec, ISO_32000-2-2020_sponsored.pdf, it's less
dramatic: From 19s to 16s.)
2023-10-10 20:55:35 +03:00
|
|
|
|
m_font_cache.set(key, font);
|
|
|
|
|
return font;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(text_set_font)
|
|
|
|
|
{
|
|
|
|
|
auto target_font_name = MUST(m_document->resolve_to<NameObject>(args[0]))->name();
|
2021-05-10 20:50:39 +03:00
|
|
|
|
|
2021-05-28 21:55:51 +03:00
|
|
|
|
text_state().font_size = args[1].to_float();
|
2022-11-22 21:46:29 +03:00
|
|
|
|
|
|
|
|
|
auto& text_rendering_matrix = calculate_text_rendering_matrix();
|
2024-01-17 02:10:36 +03:00
|
|
|
|
auto font_size = text_rendering_matrix.x_scale() * text_state().font_size / text_state().horizontal_scaling;
|
LibPDF: Cache fonts per page
Previously, every time a page switched fonts, we'd completely
re-parse the font.
Now, we cache fonts in Renderer, effectively caching them per page.
It'd be nice to have an LRU cache across pages too, but that's a
bigger change, and this already helps a lot.
Font size is part of the cache key, which means we re-parse the same
font at different font sizes. That could be better too, but again,
it's a big help as-is already.
Takes rendering the 1310 pages of the PDF 1.7 reference with
Build/lagom/bin/pdf --debugging-stats \
~/Downloads/pdf_reference_1-7.pdf
from 71 s to 11s :^)
Going through pages especially in the index is noticeably snappier.
(On the PDF 2.0 spec, ISO_32000-2-2020_sponsored.pdf, it's less
dramatic: From 19s to 16s.)
2023-10-10 20:55:35 +03:00
|
|
|
|
|
2023-11-17 04:15:58 +03:00
|
|
|
|
auto resources = extra_resources.value_or(m_page.resources);
|
|
|
|
|
auto fonts_dictionary = MUST(resources->get_dict(m_document, CommonNames::Font));
|
|
|
|
|
auto font_dictionary = MUST(fonts_dictionary->get_dict(m_document, target_font_name));
|
|
|
|
|
|
|
|
|
|
FontCacheKey cache_key { move(font_dictionary), font_size };
|
|
|
|
|
text_state().font = TRY(get_font(cache_key));
|
2021-05-10 20:50:39 +03:00
|
|
|
|
|
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_set_rendering_mode)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2021-09-19 21:56:05 +03:00
|
|
|
|
text_state().rendering_mode = static_cast<TextRenderingMode>(args[0].get<int>());
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_set_rise)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
|
|
|
|
text_state().rise = args[0].to_float();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_next_line_offset)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
Gfx::AffineTransform transform(1.0f, 0.0f, 0.0f, 1.0f, args[0].to_float(), args[1].to_float());
|
2022-03-24 21:14:31 +03:00
|
|
|
|
m_text_line_matrix.multiply(transform);
|
|
|
|
|
m_text_matrix = m_text_line_matrix;
|
2024-01-15 05:17:12 +03:00
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_next_line_and_set_leading)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
text_state().leading = -args[1].to_float();
|
2022-03-06 04:12:58 +03:00
|
|
|
|
TRY(handle_text_next_line_offset(args));
|
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_set_matrix_and_line_matrix)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
Gfx::AffineTransform new_transform(
|
|
|
|
|
args[0].to_float(),
|
|
|
|
|
args[1].to_float(),
|
|
|
|
|
args[2].to_float(),
|
|
|
|
|
args[3].to_float(),
|
|
|
|
|
args[4].to_float(),
|
|
|
|
|
args[5].to_float());
|
|
|
|
|
m_text_line_matrix = new_transform;
|
|
|
|
|
m_text_matrix = new_transform;
|
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
2023-07-19 04:35:53 +03:00
|
|
|
|
|
|
|
|
|
// Settings the text/line matrix retroactively affects fonts
|
|
|
|
|
if (text_state().font) {
|
|
|
|
|
auto new_text_rendering_matrix = calculate_text_rendering_matrix();
|
2024-01-17 02:10:36 +03:00
|
|
|
|
text_state().font->set_font_size(text_state().font_size * new_text_rendering_matrix.x_scale() / text_state().horizontal_scaling);
|
2023-07-19 04:35:53 +03:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_next_line)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2023-10-20 18:55:38 +03:00
|
|
|
|
TRY(handle_text_next_line_offset(Array<Value, 2> { 0.0f, -text_state().leading }));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_show_string)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2022-03-06 03:30:55 +03:00
|
|
|
|
auto text = MUST(m_document->resolve_to<StringObject>(args[0]))->string();
|
2023-01-29 05:57:21 +03:00
|
|
|
|
TRY(show_text(text));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 07:09:33 +03:00
|
|
|
|
RENDERER_HANDLER(text_next_line_show_string)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2022-03-06 04:12:58 +03:00
|
|
|
|
TRY(handle_text_next_line(args));
|
|
|
|
|
TRY(handle_text_show_string(args));
|
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-20 18:57:37 +03:00
|
|
|
|
RENDERER_HANDLER(text_next_line_show_string_set_spacing)
|
|
|
|
|
{
|
|
|
|
|
TRY(handle_text_set_word_space(args.slice(0, 1)));
|
|
|
|
|
TRY(handle_text_set_char_space(args.slice(1, 1)));
|
|
|
|
|
TRY(handle_text_next_line_show_string(args.slice(2)));
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2021-05-28 00:53:10 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(text_show_string_array)
|
|
|
|
|
{
|
2022-03-06 03:30:55 +03:00
|
|
|
|
auto elements = MUST(m_document->resolve_to<ArrayObject>(args[0]))->elements();
|
2021-05-28 00:53:10 +03:00
|
|
|
|
|
|
|
|
|
for (auto& element : elements) {
|
2024-03-01 18:57:38 +03:00
|
|
|
|
if (element.has_number()) {
|
|
|
|
|
float shift = element.to_float() / 1000.0f;
|
2024-03-01 19:13:58 +03:00
|
|
|
|
if (text_state().font->writing_mode() == WritingMode::Horizontal)
|
|
|
|
|
m_text_matrix.translate(-shift * text_state().font_size * text_state().horizontal_scaling, 0.0f);
|
|
|
|
|
else
|
|
|
|
|
m_text_matrix.translate(0.0f, -shift * text_state().font_size);
|
2024-01-15 05:36:50 +03:00
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
2023-11-14 02:39:10 +03:00
|
|
|
|
} else {
|
2022-03-06 03:30:55 +03:00
|
|
|
|
auto str = element.get<NonnullRefPtr<Object>>()->cast<StringObject>()->string();
|
2023-01-29 05:57:21 +03:00
|
|
|
|
TRY(show_text(str));
|
2021-05-28 00:53:10 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2022-03-06 04:12:58 +03:00
|
|
|
|
|
|
|
|
|
return {};
|
2021-05-28 00:53:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-15 15:47:17 +03:00
|
|
|
|
RENDERER_HANDLER(type3_font_set_glyph_width)
|
|
|
|
|
{
|
|
|
|
|
// FIXME: Do something with this.
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(type3_font_set_glyph_width_and_bbox)
|
|
|
|
|
{
|
|
|
|
|
// FIXME: Do something with this.
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2021-05-23 22:53:38 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_stroking_space)
|
|
|
|
|
{
|
LibPDF: Refactor parsing of ColorSpaces
ColorSpaces can be specified in two ways: with a stream as operands of
the color space operations (CS/cs), or as a separate PDF object, which
is then referred to by other means (e.g., from Image XObjects and other
entities). These two modes of addressing a ColorSpace are slightly
different and need to be addressed separately. However, the current
implementation embedded the full logic of the first case in the routine
that created ColorSpace objects.
This commit refactors the creation of ColorSpace to support both cases.
First, a new ColorSpaceFamily class encapsulates the static aspects of a
family, like its name or whether color space construction never requires
parameters. Then we define the supported ColorSpaceFamily objects.
On top of this also sit a breakage on how ColorSpaces are created. Two
methods are now offered: one only providing construction of no-argument
color spaces (and thus taking a simple name), and another taking an
ArrayObject, hence used to create ColorSpaces requiring arguments.
Finally, on top of *that* two ways to get a color space in the Renderer
are made available: the first creates a ColorSpace with a name and a
Resources dictionary, and another takes an Object. These model the two
addressing modes described above.
2022-11-24 07:40:24 +03:00
|
|
|
|
state().stroke_color_space = TRY(get_color_space_from_resources(args[0], extra_resources.value_or(m_page.resources)));
|
2021-05-28 00:01:37 +03:00
|
|
|
|
VERIFY(state().stroke_color_space);
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_painting_space)
|
|
|
|
|
{
|
LibPDF: Refactor parsing of ColorSpaces
ColorSpaces can be specified in two ways: with a stream as operands of
the color space operations (CS/cs), or as a separate PDF object, which
is then referred to by other means (e.g., from Image XObjects and other
entities). These two modes of addressing a ColorSpace are slightly
different and need to be addressed separately. However, the current
implementation embedded the full logic of the first case in the routine
that created ColorSpace objects.
This commit refactors the creation of ColorSpace to support both cases.
First, a new ColorSpaceFamily class encapsulates the static aspects of a
family, like its name or whether color space construction never requires
parameters. Then we define the supported ColorSpaceFamily objects.
On top of this also sit a breakage on how ColorSpaces are created. Two
methods are now offered: one only providing construction of no-argument
color spaces (and thus taking a simple name), and another taking an
ArrayObject, hence used to create ColorSpaces requiring arguments.
Finally, on top of *that* two ways to get a color space in the Renderer
are made available: the first creates a ColorSpace with a name and a
Resources dictionary, and another takes an Object. These model the two
addressing modes described above.
2022-11-24 07:40:24 +03:00
|
|
|
|
state().paint_color_space = TRY(get_color_space_from_resources(args[0], extra_resources.value_or(m_page.resources)));
|
2021-05-28 00:01:37 +03:00
|
|
|
|
VERIFY(state().paint_color_space);
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_stroking_color)
|
|
|
|
|
{
|
2023-12-05 15:51:42 +03:00
|
|
|
|
state().stroke_style = TRY(state().stroke_color_space->style(args));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-24 20:24:00 +03:00
|
|
|
|
RENDERER_HANDLER(set_stroking_color_extended)
|
|
|
|
|
{
|
2023-12-20 02:49:12 +03:00
|
|
|
|
// FIXME: Handle Pattern color spaces
|
|
|
|
|
auto last_arg = args.last();
|
|
|
|
|
if (last_arg.has<NonnullRefPtr<Object>>() && last_arg.get<NonnullRefPtr<Object>>()->is<NameObject>()) {
|
|
|
|
|
dbgln("pattern space {}", last_arg.get<NonnullRefPtr<Object>>()->cast<NameObject>()->name());
|
|
|
|
|
return Error::rendering_unsupported_error("Pattern color spaces not yet implemented");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state().stroke_style = TRY(state().stroke_color_space->style(args));
|
2022-03-24 20:24:00 +03:00
|
|
|
|
return {};
|
|
|
|
|
}
|
2021-05-23 22:53:38 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_painting_color)
|
|
|
|
|
{
|
2023-12-05 15:51:42 +03:00
|
|
|
|
state().paint_style = TRY(state().paint_color_space->style(args));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-24 20:24:00 +03:00
|
|
|
|
RENDERER_HANDLER(set_painting_color_extended)
|
|
|
|
|
{
|
2023-12-20 02:49:12 +03:00
|
|
|
|
// FIXME: Handle Pattern color spaces
|
|
|
|
|
auto last_arg = args.last();
|
|
|
|
|
if (last_arg.has<NonnullRefPtr<Object>>() && last_arg.get<NonnullRefPtr<Object>>()->is<NameObject>()) {
|
|
|
|
|
dbgln("pattern space {}", last_arg.get<NonnullRefPtr<Object>>()->cast<NameObject>()->name());
|
|
|
|
|
return Error::rendering_unsupported_error("Pattern color spaces not yet implemented");
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-05 15:51:42 +03:00
|
|
|
|
state().paint_style = TRY(state().paint_color_space->style(args));
|
2022-03-24 20:24:00 +03:00
|
|
|
|
return {};
|
|
|
|
|
}
|
2021-05-23 22:53:38 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_stroking_color_and_space_to_gray)
|
|
|
|
|
{
|
2021-05-28 00:01:37 +03:00
|
|
|
|
state().stroke_color_space = DeviceGrayColorSpace::the();
|
2023-12-05 15:51:42 +03:00
|
|
|
|
state().stroke_style = TRY(state().stroke_color_space->style(args));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_painting_color_and_space_to_gray)
|
|
|
|
|
{
|
2021-05-28 00:01:37 +03:00
|
|
|
|
state().paint_color_space = DeviceGrayColorSpace::the();
|
2023-12-05 15:51:42 +03:00
|
|
|
|
state().paint_style = TRY(state().paint_color_space->style(args));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_stroking_color_and_space_to_rgb)
|
|
|
|
|
{
|
2021-05-28 00:01:37 +03:00
|
|
|
|
state().stroke_color_space = DeviceRGBColorSpace::the();
|
2023-12-05 15:51:42 +03:00
|
|
|
|
state().stroke_style = TRY(state().stroke_color_space->style(args));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_painting_color_and_space_to_rgb)
|
|
|
|
|
{
|
2021-05-28 00:01:37 +03:00
|
|
|
|
state().paint_color_space = DeviceRGBColorSpace::the();
|
2023-12-05 15:51:42 +03:00
|
|
|
|
state().paint_style = TRY(state().paint_color_space->style(args));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_stroking_color_and_space_to_cmyk)
|
|
|
|
|
{
|
2024-01-31 04:59:50 +03:00
|
|
|
|
state().stroke_color_space = TRY(DeviceCMYKColorSpace::the());
|
2023-12-05 15:51:42 +03:00
|
|
|
|
state().stroke_style = TRY(state().stroke_color_space->style(args));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(set_painting_color_and_space_to_cmyk)
|
|
|
|
|
{
|
2024-01-31 04:59:50 +03:00
|
|
|
|
state().paint_color_space = TRY(DeviceCMYKColorSpace::the());
|
2023-12-05 15:51:42 +03:00
|
|
|
|
state().paint_style = TRY(state().paint_color_space->style(args));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_TODO(shade)
|
2023-12-19 00:36:29 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(inline_image_begin)
|
|
|
|
|
{
|
|
|
|
|
// The parser only calls the inline_image_end handler for inline images.
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(inline_image_begin_data)
|
|
|
|
|
{
|
|
|
|
|
// The parser only calls the inline_image_end handler for inline images.
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-19 05:13:03 +03:00
|
|
|
|
static PDFErrorOr<Value> expand_inline_image_value(Value const& value, HashMap<DeprecatedFlyString, DeprecatedFlyString> const& value_expansions)
|
|
|
|
|
{
|
|
|
|
|
if (!value.has<NonnullRefPtr<Object>>())
|
|
|
|
|
return value;
|
|
|
|
|
|
|
|
|
|
auto const& object = value.get<NonnullRefPtr<Object>>();
|
|
|
|
|
if (object->is<NameObject>()) {
|
|
|
|
|
auto const& name = object->cast<NameObject>()->name();
|
|
|
|
|
auto expanded_name = value_expansions.get(name);
|
|
|
|
|
if (!expanded_name.has_value())
|
|
|
|
|
return value;
|
|
|
|
|
return Value { make_object<NameObject>(expanded_name.value()) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For the Filters array.
|
|
|
|
|
if (object->is<ArrayObject>()) {
|
|
|
|
|
auto const& array = object->cast<ArrayObject>()->elements();
|
|
|
|
|
Vector<Value> expanded_array;
|
|
|
|
|
for (auto const& element : array) {
|
|
|
|
|
auto expanded_element = TRY(expand_inline_image_value(element, value_expansions));
|
|
|
|
|
expanded_array.append(expanded_element);
|
|
|
|
|
}
|
|
|
|
|
return Value { make_object<ArrayObject>(move(expanded_array)) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For the DecodeParms dict. It might be fine to just `return value` here, I'm not sure if there can really be abbreviations in here.
|
|
|
|
|
if (object->is<DictObject>()) {
|
|
|
|
|
auto const& dict = object->cast<DictObject>()->map();
|
|
|
|
|
HashMap<DeprecatedFlyString, Value> expanded_dict;
|
|
|
|
|
for (auto const& [key, value] : dict) {
|
|
|
|
|
auto expanded_value = TRY(expand_inline_image_value(value, value_expansions));
|
|
|
|
|
expanded_dict.set(key, expanded_value);
|
|
|
|
|
}
|
|
|
|
|
return Value { make_object<DictObject>(move(expanded_dict)) };
|
|
|
|
|
}
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static PDFErrorOr<Value> expand_inline_image_colorspace(Value color_space_value, NonnullRefPtr<DictObject> resources, RefPtr<Document> document)
|
|
|
|
|
{
|
|
|
|
|
// PDF 1.7 spec, 4.8.6 Inline Images:
|
|
|
|
|
// "Beginning with PDF 1.2, the value of the ColorSpace entry may also be the name
|
|
|
|
|
// of a color space in the ColorSpace subdictionary of the current resource dictionary."
|
|
|
|
|
|
|
|
|
|
// But PDF 1.7 spec, 4.5.2 Color Space Families:
|
|
|
|
|
// "Outside a content stream, certain objects, such as image XObjects,
|
|
|
|
|
// specify a color space as an explicit parameter, often associated with
|
|
|
|
|
// the key ColorSpace. In this case, the color space array or name is
|
|
|
|
|
// always defined directly as a PDF object, not by an entry in the
|
|
|
|
|
// ColorSpace resource subdictionary."
|
|
|
|
|
|
|
|
|
|
// This converts a named color space of an inline image to an explicit color space object,
|
|
|
|
|
// so that the regular image drawing code tolerates it.
|
|
|
|
|
|
|
|
|
|
if (!color_space_value.has<NonnullRefPtr<Object>>())
|
|
|
|
|
return color_space_value;
|
|
|
|
|
|
|
|
|
|
auto const& object = color_space_value.get<NonnullRefPtr<Object>>();
|
|
|
|
|
if (!object->is<NameObject>())
|
|
|
|
|
return color_space_value;
|
|
|
|
|
|
|
|
|
|
auto const& name = object->cast<NameObject>()->name();
|
|
|
|
|
if (name == "DeviceGray" || name == "DeviceRGB" || name == "DeviceCMYK")
|
|
|
|
|
return color_space_value;
|
|
|
|
|
|
|
|
|
|
auto color_space_resource_dict = TRY(resources->get_dict(document, CommonNames::ColorSpace));
|
|
|
|
|
return color_space_resource_dict->get_object(document, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static PDFErrorOr<NonnullRefPtr<StreamObject>> expand_inline_image_abbreviations(NonnullRefPtr<StreamObject> inline_stream, NonnullRefPtr<DictObject> resources, RefPtr<Document> document)
|
|
|
|
|
{
|
|
|
|
|
// TABLE 4.43 Entries in an inline image object
|
|
|
|
|
static HashMap<DeprecatedFlyString, DeprecatedFlyString> key_expansions {
|
|
|
|
|
{ "BPC", "BitsPerComponent" },
|
|
|
|
|
{ "CS", "ColorSpace" },
|
|
|
|
|
{ "D", "Decode" },
|
|
|
|
|
{ "DP", "DecodeParms" },
|
|
|
|
|
{ "F", "Filter" },
|
|
|
|
|
{ "H", "Height" },
|
|
|
|
|
{ "IM", "ImageMask" },
|
|
|
|
|
{ "I", "Interpolate" },
|
|
|
|
|
{ "Intent", "Intent" }, // "No abbreviation"
|
|
|
|
|
{ "L", "Length" }, // PDF 2.0; would make more sense to read in Parser.
|
|
|
|
|
{ "W", "Width" },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// TABLE 4.44 Additional abbreviations in an inline image object
|
|
|
|
|
// "Also note that JBIG2Decode and JPXDecode are not listed in Table 4.44
|
|
|
|
|
// because those filters can be applied only to image XObjects."
|
|
|
|
|
static HashMap<DeprecatedFlyString, DeprecatedFlyString> value_expansions {
|
|
|
|
|
{ "G", "DeviceGray" },
|
|
|
|
|
{ "RGB", "DeviceRGB" },
|
|
|
|
|
{ "CMYK", "DeviceCMYK" },
|
|
|
|
|
{ "I", "Indexed" },
|
|
|
|
|
{ "AHx", "ASCIIHexDecode" },
|
|
|
|
|
{ "A85", "ASCII85Decode" },
|
|
|
|
|
{ "LZW", "LZWDecode" },
|
|
|
|
|
{ "Fl", "FlateDecode" },
|
|
|
|
|
{ "RL", "RunLengthDecode" },
|
|
|
|
|
{ "CCF", "CCITTFaxDecode" },
|
|
|
|
|
{ "DCT", "DCTDecode" },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// The values in key_expansions, that is the final expansions, are the valid keys in an inline image dict.
|
|
|
|
|
HashTable<DeprecatedFlyString> valid_keys;
|
|
|
|
|
for (auto const& [key, value] : key_expansions)
|
|
|
|
|
valid_keys.set(value);
|
|
|
|
|
|
|
|
|
|
HashMap<DeprecatedFlyString, Value> expanded_dict;
|
|
|
|
|
for (auto const& [key, value] : inline_stream->dict()->map()) {
|
|
|
|
|
DeprecatedFlyString expanded_key = key_expansions.get(key).value_or(key);
|
|
|
|
|
|
|
|
|
|
// "Entries other than those listed are ignored"
|
|
|
|
|
if (!valid_keys.contains(expanded_key)) {
|
|
|
|
|
dbgln("PDF: Ignoring invalid inline image key '{}'", expanded_key);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Value expanded_value = TRY(expand_inline_image_value(value, value_expansions));
|
|
|
|
|
if (expanded_key == "ColorSpace")
|
|
|
|
|
expanded_value = TRY(expand_inline_image_colorspace(expanded_value, resources, document));
|
|
|
|
|
|
|
|
|
|
expanded_dict.set(expanded_key, expanded_value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto map_object = make_object<DictObject>(move(expanded_dict));
|
|
|
|
|
return make_object<StreamObject>(move(map_object), MUST(ByteBuffer::copy(inline_stream->bytes())));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(inline_image_end)
|
|
|
|
|
{
|
|
|
|
|
VERIFY(args.size() == 1);
|
|
|
|
|
auto inline_stream = args[0].get<NonnullRefPtr<Object>>()->cast<StreamObject>();
|
|
|
|
|
|
|
|
|
|
auto resources = extra_resources.value_or(m_page.resources);
|
|
|
|
|
auto expanded_inline_stream = TRY(expand_inline_image_abbreviations(inline_stream, resources, m_document));
|
|
|
|
|
TRY(m_document->unfilter_stream(expanded_inline_stream));
|
|
|
|
|
|
|
|
|
|
TRY(show_image(expanded_inline_stream));
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-21 08:32:09 +03:00
|
|
|
|
RENDERER_HANDLER(paint_xobject)
|
|
|
|
|
{
|
|
|
|
|
VERIFY(args.size() > 0);
|
|
|
|
|
auto resources = extra_resources.value_or(m_page.resources);
|
|
|
|
|
auto xobject_name = args[0].get<NonnullRefPtr<Object>>()->cast<NameObject>()->name();
|
2023-02-12 09:50:41 +03:00
|
|
|
|
auto xobjects_dict = TRY(resources->get_dict(m_document, CommonNames::XObject));
|
|
|
|
|
auto xobject = TRY(xobjects_dict->get_stream(m_document, xobject_name));
|
2022-11-21 08:32:09 +03:00
|
|
|
|
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
Optional<NonnullRefPtr<DictObject>> xobject_resources {};
|
|
|
|
|
if (xobject->dict()->contains(CommonNames::Resources)) {
|
|
|
|
|
xobject_resources = xobject->dict()->get_dict(m_document, CommonNames::Resources).value();
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-21 08:32:09 +03:00
|
|
|
|
auto subtype = MUST(xobject->dict()->get_name(m_document, CommonNames::Subtype))->name();
|
|
|
|
|
if (subtype == CommonNames::Image) {
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
TRY(show_image(xobject));
|
2022-11-21 08:32:09 +03:00
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-18 18:46:11 +03:00
|
|
|
|
ScopedState scoped_state { *this };
|
|
|
|
|
|
2022-11-21 08:32:09 +03:00
|
|
|
|
Vector<Value> matrix;
|
|
|
|
|
if (xobject->dict()->contains(CommonNames::Matrix)) {
|
|
|
|
|
matrix = xobject->dict()->get_array(m_document, CommonNames::Matrix).value()->elements();
|
|
|
|
|
} else {
|
|
|
|
|
matrix = Vector { Value { 1 }, Value { 0 }, Value { 0 }, Value { 1 }, Value { 0 }, Value { 0 } };
|
|
|
|
|
}
|
|
|
|
|
MUST(handle_concatenate_matrix(matrix));
|
|
|
|
|
auto operators = TRY(Parser::parse_operators(m_document, xobject->bytes()));
|
|
|
|
|
for (auto& op : operators)
|
|
|
|
|
TRY(handle_operator(op, xobject_resources));
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2022-03-06 08:35:33 +03:00
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(marked_content_point)
|
|
|
|
|
{
|
|
|
|
|
// nop
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(marked_content_designate)
|
|
|
|
|
{
|
|
|
|
|
// nop
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(marked_content_begin)
|
|
|
|
|
{
|
|
|
|
|
// nop
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(marked_content_begin_with_property_list)
|
|
|
|
|
{
|
|
|
|
|
// nop
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(marked_content_end)
|
|
|
|
|
{
|
|
|
|
|
// nop
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 22:16:26 +03:00
|
|
|
|
RENDERER_HANDLER(compatibility_begin)
|
|
|
|
|
{
|
|
|
|
|
// We're supposed to ignore unknown operands in compatibility_begin / compatibility_end sections.
|
|
|
|
|
// But we want to know about all operands, so we just ignore this.
|
|
|
|
|
// In practice, it seems like compatibility_begin / compatibility_end were introduced when
|
|
|
|
|
// `sh` was added, and they're used exlusively around `sh`.
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RENDERER_HANDLER(compatibility_end)
|
|
|
|
|
{
|
|
|
|
|
// See comment in compatibility_begin.
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2021-05-23 07:09:33 +03:00
|
|
|
|
|
2021-05-10 20:50:39 +03:00
|
|
|
|
template<typename T>
|
|
|
|
|
Gfx::Point<T> Renderer::map(T x, T y) const
|
|
|
|
|
{
|
2022-11-26 07:53:32 +03:00
|
|
|
|
return state().ctm.map(Gfx::Point<T> { x, y });
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename T>
|
|
|
|
|
Gfx::Size<T> Renderer::map(Gfx::Size<T> size) const
|
|
|
|
|
{
|
|
|
|
|
return state().ctm.map(size);
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 22:53:38 +03:00
|
|
|
|
template<typename T>
|
|
|
|
|
Gfx::Rect<T> Renderer::map(Gfx::Rect<T> rect) const
|
|
|
|
|
{
|
|
|
|
|
return state().ctm.map(rect);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-16 23:32:26 +03:00
|
|
|
|
Gfx::Path Renderer::map(Gfx::Path const& path) const
|
|
|
|
|
{
|
|
|
|
|
return path.copy_transformed(state().ctm);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-21 05:38:45 +03:00
|
|
|
|
float Renderer::line_width() const
|
|
|
|
|
{
|
|
|
|
|
// PDF 1.7 spec, 4.3.2 Details of Graphics State Parameters, Line Width:
|
|
|
|
|
// "A line width of 0 denotes the thinnest line that can be rendered at device resolution: 1 device pixel wide."
|
|
|
|
|
if (state().line_width == 0)
|
|
|
|
|
return 1;
|
|
|
|
|
return state().ctm.x_scale() * state().line_width;
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-06 04:12:58 +03:00
|
|
|
|
PDFErrorOr<void> Renderer::set_graphics_state_from_dict(NonnullRefPtr<DictObject> dict)
|
2021-05-28 00:22:24 +03:00
|
|
|
|
{
|
2023-07-21 02:38:01 +03:00
|
|
|
|
// ISO 32000 (PDF 2.0), 8.4.5 Graphics state parameter dictionaries
|
|
|
|
|
|
2021-05-28 00:22:24 +03:00
|
|
|
|
if (dict->contains(CommonNames::LW))
|
2023-10-20 18:55:38 +03:00
|
|
|
|
TRY(handle_set_line_width(Array { dict->get_value(CommonNames::LW) }));
|
2021-05-28 00:22:24 +03:00
|
|
|
|
|
|
|
|
|
if (dict->contains(CommonNames::LC))
|
2023-10-20 18:55:38 +03:00
|
|
|
|
TRY(handle_set_line_cap(Array { dict->get_value(CommonNames::LC) }));
|
2021-05-28 00:22:24 +03:00
|
|
|
|
|
|
|
|
|
if (dict->contains(CommonNames::LJ))
|
2023-10-20 18:55:38 +03:00
|
|
|
|
TRY(handle_set_line_join(Array { dict->get_value(CommonNames::LJ) }));
|
2021-05-28 00:22:24 +03:00
|
|
|
|
|
|
|
|
|
if (dict->contains(CommonNames::ML))
|
2023-10-20 18:55:38 +03:00
|
|
|
|
TRY(handle_set_miter_limit(Array { dict->get_value(CommonNames::ML) }));
|
2021-05-28 00:22:24 +03:00
|
|
|
|
|
2022-03-06 04:12:58 +03:00
|
|
|
|
if (dict->contains(CommonNames::D)) {
|
|
|
|
|
auto array = MUST(dict->get_array(m_document, CommonNames::D));
|
|
|
|
|
TRY(handle_set_dash_pattern(array->elements()));
|
|
|
|
|
}
|
2021-05-28 00:22:24 +03:00
|
|
|
|
|
2023-10-19 14:58:22 +03:00
|
|
|
|
if (dict->contains(CommonNames::RI))
|
2023-10-20 18:55:38 +03:00
|
|
|
|
TRY(handle_set_color_rendering_intent(Array { dict->get_value(CommonNames::RI) }));
|
2023-10-19 14:58:22 +03:00
|
|
|
|
|
2023-07-21 02:38:01 +03:00
|
|
|
|
// FIXME: OP
|
|
|
|
|
// FIXME: op
|
|
|
|
|
// FIXME: OPM
|
|
|
|
|
// FIXME: Font
|
|
|
|
|
// FIXME: BG
|
|
|
|
|
// FIXME: BG2
|
|
|
|
|
// FIXME: UCR
|
|
|
|
|
// FIXME: UCR2
|
|
|
|
|
// FIXME: TR
|
|
|
|
|
// FIXME: TR2
|
|
|
|
|
// FIXME: HT
|
|
|
|
|
|
2021-05-28 00:22:24 +03:00
|
|
|
|
if (dict->contains(CommonNames::FL))
|
2023-10-20 18:55:38 +03:00
|
|
|
|
TRY(handle_set_flatness_tolerance(Array { dict->get_value(CommonNames::FL) }));
|
2022-03-06 04:12:58 +03:00
|
|
|
|
|
2023-07-21 02:38:01 +03:00
|
|
|
|
// FIXME: SM
|
|
|
|
|
// FIXME: SA
|
|
|
|
|
// FIXME: BM
|
|
|
|
|
// FIXME: SMask
|
|
|
|
|
// FIXME: CA
|
|
|
|
|
// FIXME: ca
|
|
|
|
|
// FIXME: AIS
|
|
|
|
|
// FIXME: TK
|
|
|
|
|
// FIXME: UseBlackPtComp
|
|
|
|
|
// FIXME: HTO
|
|
|
|
|
|
2022-03-06 04:12:58 +03:00
|
|
|
|
return {};
|
2021-05-28 00:22:24 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
|
PDFErrorOr<void> Renderer::show_text(ByteString const& string)
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
2023-02-12 09:51:26 +03:00
|
|
|
|
if (!text_state().font)
|
|
|
|
|
return Error::rendering_unsupported_error("Can't draw text because an invalid font was in use");
|
|
|
|
|
|
2024-01-20 07:01:23 +03:00
|
|
|
|
OwnPtr<ClipRAII> clip_raii;
|
|
|
|
|
if (m_rendering_preferences.clip_text)
|
|
|
|
|
clip_raii = make<ClipRAII>(*this);
|
|
|
|
|
|
2024-01-17 02:50:02 +03:00
|
|
|
|
auto start_position = Gfx::FloatPoint { 0.0f, 0.0f };
|
2023-11-14 18:11:39 +03:00
|
|
|
|
auto end_position = TRY(text_state().font->draw_string(m_painter, start_position, string, *this));
|
2021-05-28 21:55:51 +03:00
|
|
|
|
|
2024-01-17 02:50:02 +03:00
|
|
|
|
// Update text matrix.
|
2024-03-01 18:44:29 +03:00
|
|
|
|
auto delta = end_position - start_position;
|
2021-05-28 21:55:51 +03:00
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
2024-03-01 18:44:29 +03:00
|
|
|
|
m_text_matrix.translate(delta);
|
2023-01-29 05:57:21 +03:00
|
|
|
|
return {};
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-12-07 02:47:00 +03:00
|
|
|
|
enum UpsampleMode {
|
|
|
|
|
StoreValuesUnchanged,
|
|
|
|
|
UpsampleTo8Bit,
|
|
|
|
|
};
|
2023-12-07 02:53:19 +03:00
|
|
|
|
static Vector<u8> upsample_to_8_bit(ReadonlyBytes content, int samples_per_line, int bits_per_component, UpsampleMode mode)
|
2023-11-15 22:21:26 +03:00
|
|
|
|
{
|
|
|
|
|
VERIFY(bits_per_component == 1 || bits_per_component == 2 || bits_per_component == 4);
|
|
|
|
|
Vector<u8> upsampled_storage;
|
|
|
|
|
upsampled_storage.ensure_capacity(content.size() * 8 / bits_per_component);
|
|
|
|
|
u8 const mask = (1 << bits_per_component) - 1;
|
2023-12-07 02:53:19 +03:00
|
|
|
|
|
|
|
|
|
int x = 0;
|
2023-11-15 22:21:26 +03:00
|
|
|
|
for (auto byte : content) {
|
|
|
|
|
for (int i = 0; i < 8; i += bits_per_component) {
|
|
|
|
|
auto value = (byte >> (8 - bits_per_component - i)) & mask;
|
2023-12-07 02:47:00 +03:00
|
|
|
|
if (mode == UpsampleMode::UpsampleTo8Bit)
|
|
|
|
|
upsampled_storage.append(value * (255 / mask));
|
|
|
|
|
else
|
|
|
|
|
upsampled_storage.append(value);
|
2023-12-07 02:53:19 +03:00
|
|
|
|
++x;
|
|
|
|
|
|
|
|
|
|
// "Byte boundaries are ignored, except that each row of sample data must begin on a byte boundary."
|
|
|
|
|
if (x == samples_per_line) {
|
|
|
|
|
x = 0;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2023-11-15 22:21:26 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return upsampled_storage;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-23 02:56:42 +03:00
|
|
|
|
PDFErrorOr<Renderer::LoadedImage> Renderer::load_image(NonnullRefPtr<StreamObject> image)
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
{
|
|
|
|
|
auto image_dict = image->dict();
|
2023-10-25 07:03:15 +03:00
|
|
|
|
auto width = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::Width)));
|
|
|
|
|
auto height = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::Height)));
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
|
2023-07-24 05:06:48 +03:00
|
|
|
|
auto is_filter = [&](DeprecatedFlyString const& name) -> PDFErrorOr<bool> {
|
|
|
|
|
if (!image_dict->contains(CommonNames::Filter))
|
|
|
|
|
return false;
|
|
|
|
|
auto filter_object = TRY(image_dict->get_object(m_document, CommonNames::Filter));
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
if (filter_object->is<NameObject>())
|
|
|
|
|
return filter_object->cast<NameObject>()->name() == name;
|
|
|
|
|
auto filters = filter_object->cast<ArrayObject>();
|
2023-12-20 03:30:13 +03:00
|
|
|
|
if (filters->elements().is_empty())
|
|
|
|
|
return false;
|
2023-11-12 07:35:26 +03:00
|
|
|
|
auto last_filter_index = filters->elements().size() - 1;
|
|
|
|
|
return MUST(filters->get_name_at(m_document, last_filter_index))->name() == name;
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
};
|
2023-07-24 05:06:48 +03:00
|
|
|
|
if (TRY(is_filter(CommonNames::JPXDecode))) {
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
return Error(Error::Type::RenderingUnsupported, "JPXDecode filter");
|
|
|
|
|
}
|
2023-12-23 02:56:42 +03:00
|
|
|
|
|
|
|
|
|
bool is_image_mask = false;
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
if (image_dict->contains(CommonNames::ImageMask)) {
|
2023-12-23 02:56:42 +03:00
|
|
|
|
is_image_mask = TRY(m_document->resolve_to<bool>(image_dict->get_value(CommonNames::ImageMask)));
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-20 04:54:18 +03:00
|
|
|
|
// "(Required for images, except those that use the JPXDecode filter; not allowed for image masks) [...]
|
|
|
|
|
// it can be any type of color space except Pattern."
|
2023-12-23 02:56:42 +03:00
|
|
|
|
NonnullRefPtr<ColorSpace> color_space = DeviceGrayColorSpace::the();
|
|
|
|
|
if (!is_image_mask) {
|
|
|
|
|
auto color_space_object = MUST(image_dict->get_object(m_document, CommonNames::ColorSpace));
|
|
|
|
|
color_space = TRY(get_color_space_from_document(color_space_object));
|
|
|
|
|
}
|
2023-10-20 02:53:08 +03:00
|
|
|
|
|
|
|
|
|
auto color_rendering_intent = state().color_rendering_intent;
|
|
|
|
|
if (image_dict->contains(CommonNames::Intent))
|
|
|
|
|
color_rendering_intent = TRY(image_dict->get_name(m_document, CommonNames::Intent))->name();
|
|
|
|
|
// FIXME: Do something with color_rendering_intent.
|
|
|
|
|
|
2023-10-20 02:53:55 +03:00
|
|
|
|
// "Valid values are 1, 2, 4, 8, and (in PDF 1.5) 16."
|
2023-12-23 02:56:42 +03:00
|
|
|
|
// Per spec, this is required even for /Mask images, but it's required to be 1 there.
|
|
|
|
|
// In practice, it's sometimes missing for /Mask images.
|
|
|
|
|
auto bits_per_component = 1;
|
|
|
|
|
if (!is_image_mask)
|
|
|
|
|
bits_per_component = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::BitsPerComponent)));
|
2023-11-15 22:21:26 +03:00
|
|
|
|
switch (bits_per_component) {
|
|
|
|
|
case 1:
|
|
|
|
|
case 2:
|
|
|
|
|
case 4:
|
|
|
|
|
case 8:
|
|
|
|
|
case 16:
|
|
|
|
|
// Ok!
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return Error(Error::Type::MalformedPDF, "Image's /BitsPerComponent invalid");
|
|
|
|
|
}
|
|
|
|
|
auto content = image->bytes();
|
|
|
|
|
|
2023-12-07 02:53:19 +03:00
|
|
|
|
int const n_components = color_space->number_of_components();
|
|
|
|
|
|
2024-01-12 17:02:50 +03:00
|
|
|
|
Vector<u8> resampled_storage;
|
2023-11-15 22:21:26 +03:00
|
|
|
|
if (bits_per_component < 8) {
|
2023-12-07 02:47:00 +03:00
|
|
|
|
UpsampleMode mode = color_space->family() == ColorSpaceFamily::Indexed ? UpsampleMode::StoreValuesUnchanged : UpsampleMode::UpsampleTo8Bit;
|
2024-01-12 17:02:50 +03:00
|
|
|
|
resampled_storage = upsample_to_8_bit(content, width * n_components, bits_per_component, mode);
|
|
|
|
|
content = resampled_storage;
|
2023-11-15 22:21:26 +03:00
|
|
|
|
bits_per_component = 8;
|
2023-12-23 02:56:42 +03:00
|
|
|
|
|
|
|
|
|
if (is_image_mask) {
|
|
|
|
|
// "a sample value of 0 marks the page with the current color, and a 1 leaves the previous contents unchanged."
|
|
|
|
|
// That's opposite of the normal alpha convention, and we're upsampling masks to 8 bit and use that as normal alpha.
|
2024-01-12 17:02:50 +03:00
|
|
|
|
for (u8& byte : resampled_storage)
|
2023-12-23 02:56:42 +03:00
|
|
|
|
byte = ~byte;
|
|
|
|
|
}
|
2024-01-12 17:02:50 +03:00
|
|
|
|
} else if (bits_per_component == 16) {
|
|
|
|
|
if (color_space->family() == ColorSpaceFamily::Indexed)
|
|
|
|
|
return Error(Error::Type::RenderingUnsupported, "16 bpp indexed images not yet supported");
|
|
|
|
|
|
|
|
|
|
// PDF 1.7 spec, 4.8.2 Sample Representation:
|
|
|
|
|
// "units of 16 bits are given with the most significant byte first"
|
|
|
|
|
// FIXME: Eventually use all 16 bits instead of throwing away the lower 8 bits.
|
|
|
|
|
resampled_storage.ensure_capacity(content.size() / 2);
|
|
|
|
|
for (size_t i = 0; i < content.size(); i += 2)
|
|
|
|
|
resampled_storage.append(content[i]);
|
|
|
|
|
|
|
|
|
|
content = resampled_storage;
|
|
|
|
|
bits_per_component = 8;
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector<float> decode_array;
|
|
|
|
|
if (image_dict->contains(CommonNames::Decode)) {
|
|
|
|
|
decode_array = MUST(image_dict->get_array(m_document, CommonNames::Decode))->float_elements();
|
|
|
|
|
} else {
|
|
|
|
|
decode_array = color_space->default_decode();
|
|
|
|
|
}
|
|
|
|
|
Vector<LinearInterpolation1D> component_value_decoders;
|
|
|
|
|
component_value_decoders.ensure_capacity(decode_array.size());
|
|
|
|
|
for (size_t i = 0; i < decode_array.size(); i += 2) {
|
|
|
|
|
auto dmin = decode_array[i];
|
|
|
|
|
auto dmax = decode_array[i + 1];
|
|
|
|
|
component_value_decoders.empend(0.0f, 255.0f, dmin, dmax);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 22:30:42 +03:00
|
|
|
|
auto bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { width, height }));
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
int x = 0;
|
|
|
|
|
int y = 0;
|
|
|
|
|
auto const bytes_per_component = bits_per_component / 8;
|
2024-01-09 05:36:21 +03:00
|
|
|
|
Vector<float> component_values;
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
component_values.resize(n_components);
|
|
|
|
|
while (!content.is_empty() && y < height) {
|
|
|
|
|
auto sample = content.slice(0, bytes_per_component * n_components);
|
|
|
|
|
content = content.slice(bytes_per_component * n_components);
|
|
|
|
|
for (int i = 0; i < n_components; ++i) {
|
|
|
|
|
auto component = sample.slice(0, bytes_per_component);
|
|
|
|
|
sample = sample.slice(bytes_per_component);
|
2024-01-09 05:36:21 +03:00
|
|
|
|
component_values[i] = component_value_decoders[i].interpolate(component[0]);
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
}
|
2024-01-09 06:26:40 +03:00
|
|
|
|
auto color = TRY(color_space->style(component_values)).get<Color>();
|
|
|
|
|
bitmap->set_pixel(x, y, color);
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
++x;
|
|
|
|
|
if (x == width) {
|
|
|
|
|
x = 0;
|
|
|
|
|
++y;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-23 02:56:42 +03:00
|
|
|
|
return LoadedImage { bitmap, is_image_mask };
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
}
|
|
|
|
|
|
LibPDF: Add support for array image masks
An array image mask contains a min/max range for each channel,
and if each channel of a given pixel is in that channel's range,
that pixel is masked out (i.e. transparent). (It's similar to
having a single color or palette index be transparent, but it
supports a range of transparent colors if desired.)
What makes this a bit awkward is that the range is relative to the
origin bits per pixel and the inputs to the image's color space.
So an indexed (palettized) image with 4bpp has a 2-element mask
array where both entries are between 0 and 15.
We currently apply masks after converting images to a Gfx::Bitmap,
that is after converting to 8bpp sRGB. And we do this by mapping
everything to 8bpp very early on in load_image().
This leaves us with a bunch of options that are all a bit awkward:
1. Make load_image() store the up- (or for 16bpp inputs, down-)
sampled-to-8bpp pixel data. And also return if we expanded the
pixel range while resampling (for color values) or not (for
palettized images). Then, when applying the image filter,
resample the array bounds in exactly the same way. This requires
passing around more stuff.
2. Like 1, but pass in the mask array to load_image() and apply
the mask right there and then. This means we'd apply mask arrays
at a different time than other masks.
3. Make the function that computes the mask from the mask array
work from the original, unprocessed image data. This is the most
local change, but probably also requires the largest amount of
code (in return, the color mask for 16bpp images is precise, in
addition that it separates concerns the most nicely).
This goes with 3 for now.
2024-03-01 22:43:52 +03:00
|
|
|
|
PDFErrorOr<NonnullRefPtr<Gfx::Bitmap>> Renderer::make_mask_bitmap_from_array(NonnullRefPtr<ArrayObject> array, NonnullRefPtr<StreamObject> image)
|
|
|
|
|
{
|
|
|
|
|
// PDF 1.7 spec, 4.8.5. Masked Images, Color Key Masking
|
|
|
|
|
// "For color key masking, the value of the Mask entry is an array of 2 × n integers, [min_1 max_1 ... min_n max_n],
|
|
|
|
|
// where n is the number of color components in the image’s color space. Each integer must be in the range 0 to 2**(BitsPerComponent − 1),
|
|
|
|
|
// representing color values _before_ decoding with the Decode array.
|
|
|
|
|
// An image sample is masked [...] if min_i ≤ c_i ≤ max_i for all 1 ≤ i ≤ n."
|
|
|
|
|
// For indexed images, this means the array masks the index, not the color.
|
|
|
|
|
auto image_dict = image->dict();
|
|
|
|
|
auto width = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::Width)));
|
|
|
|
|
auto height = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::Height)));
|
|
|
|
|
auto bits_per_component = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::BitsPerComponent)));
|
|
|
|
|
VERIFY(bits_per_component == 1 || bits_per_component == 2 || bits_per_component == 4 || bits_per_component == 8 || bits_per_component == 16);
|
|
|
|
|
|
|
|
|
|
if (array->size() % 2 != 0)
|
|
|
|
|
return Error(Error::Type::MalformedPDF, "Mask array must have an even number of elements");
|
|
|
|
|
auto n_components = array->size() / 2;
|
|
|
|
|
Vector<int, 4> min, max;
|
|
|
|
|
for (size_t i = 0; i < n_components; ++i) {
|
|
|
|
|
min.append(array->at(i * 2).to_int());
|
|
|
|
|
max.append(array->at(i * 2 + 1).to_int());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto mask_bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { width, height }));
|
|
|
|
|
auto bit_stream = make<BigEndianInputBitStream>(make<FixedMemoryStream>(image->bytes()));
|
|
|
|
|
for (int y = 0; y < height; ++y) {
|
|
|
|
|
for (int x = 0; x < width; ++x) {
|
|
|
|
|
bool is_masked = true;
|
|
|
|
|
for (size_t i = 0; i < n_components; ++i) {
|
|
|
|
|
u16 sample = TRY(bit_stream->read_bits(bits_per_component));
|
|
|
|
|
if (sample < min[i] || sample > max[i]) {
|
|
|
|
|
is_masked = false;
|
|
|
|
|
TRY(bit_stream->read_bits((n_components - 1 - i) * bits_per_component));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
mask_bitmap->set_pixel(x, y, Color::from_argb(is_masked ? 0x00'00'00'00 : 0xff'ff'ff'ff));
|
|
|
|
|
}
|
|
|
|
|
bit_stream->align_to_byte_boundary();
|
|
|
|
|
}
|
|
|
|
|
return mask_bitmap;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-27 15:54:15 +03:00
|
|
|
|
Gfx::AffineTransform Renderer::calculate_image_space_transformation(Gfx::IntSize size)
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
{
|
|
|
|
|
// Image space maps to a 1x1 unit of user space and starts at the top-left
|
|
|
|
|
auto image_space = state().ctm;
|
|
|
|
|
image_space.multiply(Gfx::AffineTransform(
|
2024-02-27 15:54:15 +03:00
|
|
|
|
1.0f / size.width(),
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
0.0f,
|
|
|
|
|
0.0f,
|
2024-02-27 15:54:15 +03:00
|
|
|
|
-1.0f / size.height(),
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
0.0f,
|
|
|
|
|
1.0f));
|
|
|
|
|
return image_space;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-27 15:54:15 +03:00
|
|
|
|
void Renderer::show_empty_image(Gfx::IntSize size)
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
{
|
2024-02-27 15:54:15 +03:00
|
|
|
|
auto image_space_transformation = calculate_image_space_transformation(size);
|
|
|
|
|
auto image_border = image_space_transformation.map(Gfx::IntRect { {}, size });
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
m_painter.stroke_path(rect_path(image_border), Color::Black, 1);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-27 15:45:13 +03:00
|
|
|
|
static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> apply_alpha_channel(NonnullRefPtr<Gfx::Bitmap> image_bitmap, NonnullRefPtr<const Gfx::Bitmap> mask_bitmap)
|
2023-12-23 02:31:20 +03:00
|
|
|
|
{
|
|
|
|
|
// Make alpha mask same size as image.
|
2024-02-27 15:45:13 +03:00
|
|
|
|
if (mask_bitmap->size() != image_bitmap->size()) {
|
|
|
|
|
// Some files have 2x2 images for color and huge masks that contain rendered text outlines.
|
|
|
|
|
// So resize to the larger of the two.
|
|
|
|
|
auto new_size = Gfx::IntSize { max(image_bitmap->width(), mask_bitmap->width()), max(image_bitmap->height(), mask_bitmap->height()) };
|
|
|
|
|
if (image_bitmap->size() != new_size)
|
|
|
|
|
image_bitmap = TRY(image_bitmap->scaled_to_size(new_size));
|
|
|
|
|
if (mask_bitmap->size() != new_size)
|
|
|
|
|
mask_bitmap = TRY(mask_bitmap->scaled_to_size(new_size));
|
|
|
|
|
}
|
2023-12-23 02:31:20 +03:00
|
|
|
|
|
|
|
|
|
image_bitmap->add_alpha_channel();
|
|
|
|
|
for (int j = 0; j < image_bitmap->height(); ++j) {
|
|
|
|
|
for (int i = 0; i < image_bitmap->width(); ++i) {
|
|
|
|
|
auto image_color = image_bitmap->get_pixel(i, j);
|
|
|
|
|
auto mask_color = mask_bitmap->get_pixel(i, j);
|
|
|
|
|
image_color = image_color.with_alpha(mask_color.luminosity());
|
|
|
|
|
image_bitmap->set_pixel(i, j, image_color);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-27 15:45:13 +03:00
|
|
|
|
return image_bitmap;
|
2023-12-23 02:31:20 +03:00
|
|
|
|
}
|
|
|
|
|
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
PDFErrorOr<void> Renderer::show_image(NonnullRefPtr<StreamObject> image)
|
|
|
|
|
{
|
|
|
|
|
auto image_dict = image->dict();
|
|
|
|
|
|
2024-01-17 04:45:12 +03:00
|
|
|
|
OwnPtr<ClipRAII> clip_raii;
|
|
|
|
|
if (m_rendering_preferences.clip_images)
|
|
|
|
|
clip_raii = make<ClipRAII>(*this);
|
2024-01-17 04:38:41 +03:00
|
|
|
|
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
if (!m_rendering_preferences.show_images) {
|
2024-02-27 15:54:15 +03:00
|
|
|
|
auto width = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::Width)));
|
|
|
|
|
auto height = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::Height)));
|
|
|
|
|
show_empty_image({ width, height });
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
auto image_bitmap = TRY(load_image(image));
|
2024-01-10 03:17:02 +03:00
|
|
|
|
if (image_bitmap.is_image_mask) {
|
|
|
|
|
// PDF 1.7 spec, 4.8.5 Masked Images, Stencil Masking:
|
|
|
|
|
// "An image mask (an image XObject whose ImageMask entry is true) [...] is treated as a stencil mask [...].
|
|
|
|
|
// Sample values [...] designate places on the page that should either be marked with the current color or masked out (not marked at all)."
|
|
|
|
|
if (!state().paint_style.has<Gfx::Color>())
|
|
|
|
|
return Error(Error::Type::RenderingUnsupported, "Image masks with pattern fill not yet implemented");
|
|
|
|
|
|
|
|
|
|
// Move mask to alpha channel, and put current color in RGB.
|
|
|
|
|
auto current_color = state().paint_style.get<Gfx::Color>();
|
|
|
|
|
for (auto& pixel : *image_bitmap.bitmap) {
|
|
|
|
|
u8 mask_alpha = Color::from_argb(pixel).luminosity();
|
|
|
|
|
pixel = current_color.with_alpha(mask_alpha).value();
|
|
|
|
|
}
|
|
|
|
|
} else if (image_dict->contains(CommonNames::SMask)) {
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
auto smask_bitmap = TRY(load_image(TRY(image_dict->get_stream(m_document, CommonNames::SMask))));
|
2024-02-27 15:45:13 +03:00
|
|
|
|
image_bitmap.bitmap = TRY(apply_alpha_channel(image_bitmap.bitmap, smask_bitmap.bitmap));
|
2023-11-16 16:58:38 +03:00
|
|
|
|
} else if (image_dict->contains(CommonNames::Mask)) {
|
|
|
|
|
auto mask_object = TRY(image_dict->get_object(m_document, CommonNames::Mask));
|
|
|
|
|
if (mask_object->is<StreamObject>()) {
|
2023-12-23 02:34:17 +03:00
|
|
|
|
auto mask_bitmap = TRY(load_image(mask_object->cast<StreamObject>()));
|
2024-02-27 15:45:13 +03:00
|
|
|
|
image_bitmap.bitmap = TRY(apply_alpha_channel(image_bitmap.bitmap, mask_bitmap.bitmap));
|
2023-11-16 16:58:38 +03:00
|
|
|
|
} else if (mask_object->is<ArrayObject>()) {
|
LibPDF: Add support for array image masks
An array image mask contains a min/max range for each channel,
and if each channel of a given pixel is in that channel's range,
that pixel is masked out (i.e. transparent). (It's similar to
having a single color or palette index be transparent, but it
supports a range of transparent colors if desired.)
What makes this a bit awkward is that the range is relative to the
origin bits per pixel and the inputs to the image's color space.
So an indexed (palettized) image with 4bpp has a 2-element mask
array where both entries are between 0 and 15.
We currently apply masks after converting images to a Gfx::Bitmap,
that is after converting to 8bpp sRGB. And we do this by mapping
everything to 8bpp very early on in load_image().
This leaves us with a bunch of options that are all a bit awkward:
1. Make load_image() store the up- (or for 16bpp inputs, down-)
sampled-to-8bpp pixel data. And also return if we expanded the
pixel range while resampling (for color values) or not (for
palettized images). Then, when applying the image filter,
resample the array bounds in exactly the same way. This requires
passing around more stuff.
2. Like 1, but pass in the mask array to load_image() and apply
the mask right there and then. This means we'd apply mask arrays
at a different time than other masks.
3. Make the function that computes the mask from the mask array
work from the original, unprocessed image data. This is the most
local change, but probably also requires the largest amount of
code (in return, the color mask for 16bpp images is precise, in
addition that it separates concerns the most nicely).
This goes with 3 for now.
2024-03-01 22:43:52 +03:00
|
|
|
|
auto mask_bitmap = TRY(make_mask_bitmap_from_array(mask_object->cast<ArrayObject>(), image));
|
|
|
|
|
image_bitmap.bitmap = TRY(apply_alpha_channel(image_bitmap.bitmap, mask_bitmap));
|
2023-11-16 16:58:38 +03:00
|
|
|
|
}
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-27 15:54:15 +03:00
|
|
|
|
auto image_space = calculate_image_space_transformation(image_bitmap.bitmap->size());
|
|
|
|
|
auto image_rect = Gfx::FloatRect { image_bitmap.bitmap->rect() };
|
2023-12-23 02:56:42 +03:00
|
|
|
|
m_painter.draw_scaled_bitmap_with_transform(image_bitmap.bitmap->rect(), image_bitmap.bitmap, image_rect, image_space);
|
LibPDF: Add initial image display support
After adding support for XObject Form rendering, the next was to display
XObject images. This commit adds this initial support,
Images come in many shapes and forms: encodings: color spaces, bits per
component, width, height, etc. This initial support is constrained to
the color spaces we currently support, to images that use 8 bits per
component, to images that do *not* use the JPXDecode filter, and that
are not Masks. There are surely other constraints that aren't considered
in this initial support, so expect breakage here and there.
In addition to supporting images, we also support applying an alpha mask
(SMask) on them. Additionally, a new rendering preference allows to skip
image loading and rendering altogether, instead showing an empty
rectangle as a placeholder (useful for when actual images are not
supported). Since RenderingPreferences is becoming a bit more complex,
we add a hash option that will allow us to keep track of different
preferences (e.g., in a HashMap).
2022-11-24 21:01:53 +03:00
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
LibPDF: Refactor parsing of ColorSpaces
ColorSpaces can be specified in two ways: with a stream as operands of
the color space operations (CS/cs), or as a separate PDF object, which
is then referred to by other means (e.g., from Image XObjects and other
entities). These two modes of addressing a ColorSpace are slightly
different and need to be addressed separately. However, the current
implementation embedded the full logic of the first case in the routine
that created ColorSpace objects.
This commit refactors the creation of ColorSpace to support both cases.
First, a new ColorSpaceFamily class encapsulates the static aspects of a
family, like its name or whether color space construction never requires
parameters. Then we define the supported ColorSpaceFamily objects.
On top of this also sit a breakage on how ColorSpaces are created. Two
methods are now offered: one only providing construction of no-argument
color spaces (and thus taking a simple name), and another taking an
ArrayObject, hence used to create ColorSpaces requiring arguments.
Finally, on top of *that* two ways to get a color space in the Renderer
are made available: the first creates a ColorSpace with a name and a
Resources dictionary, and another takes an Object. These model the two
addressing modes described above.
2022-11-24 07:40:24 +03:00
|
|
|
|
PDFErrorOr<NonnullRefPtr<ColorSpace>> Renderer::get_color_space_from_resources(Value const& value, NonnullRefPtr<DictObject> resources)
|
2021-05-23 22:53:38 +03:00
|
|
|
|
{
|
LibPDF: Refactor parsing of ColorSpaces
ColorSpaces can be specified in two ways: with a stream as operands of
the color space operations (CS/cs), or as a separate PDF object, which
is then referred to by other means (e.g., from Image XObjects and other
entities). These two modes of addressing a ColorSpace are slightly
different and need to be addressed separately. However, the current
implementation embedded the full logic of the first case in the routine
that created ColorSpace objects.
This commit refactors the creation of ColorSpace to support both cases.
First, a new ColorSpaceFamily class encapsulates the static aspects of a
family, like its name or whether color space construction never requires
parameters. Then we define the supported ColorSpaceFamily objects.
On top of this also sit a breakage on how ColorSpaces are created. Two
methods are now offered: one only providing construction of no-argument
color spaces (and thus taking a simple name), and another taking an
ArrayObject, hence used to create ColorSpaces requiring arguments.
Finally, on top of *that* two ways to get a color space in the Renderer
are made available: the first creates a ColorSpace with a name and a
Resources dictionary, and another takes an Object. These model the two
addressing modes described above.
2022-11-24 07:40:24 +03:00
|
|
|
|
auto color_space_name = value.get<NonnullRefPtr<Object>>()->cast<NameObject>()->name();
|
|
|
|
|
auto maybe_color_space_family = ColorSpaceFamily::get(color_space_name);
|
|
|
|
|
if (!maybe_color_space_family.is_error()) {
|
|
|
|
|
auto color_space_family = maybe_color_space_family.release_value();
|
2023-10-20 18:11:03 +03:00
|
|
|
|
if (color_space_family.may_be_specified_directly()) {
|
2023-12-07 16:45:44 +03:00
|
|
|
|
return ColorSpace::create(color_space_name, *this);
|
LibPDF: Refactor parsing of ColorSpaces
ColorSpaces can be specified in two ways: with a stream as operands of
the color space operations (CS/cs), or as a separate PDF object, which
is then referred to by other means (e.g., from Image XObjects and other
entities). These two modes of addressing a ColorSpace are slightly
different and need to be addressed separately. However, the current
implementation embedded the full logic of the first case in the routine
that created ColorSpace objects.
This commit refactors the creation of ColorSpace to support both cases.
First, a new ColorSpaceFamily class encapsulates the static aspects of a
family, like its name or whether color space construction never requires
parameters. Then we define the supported ColorSpaceFamily objects.
On top of this also sit a breakage on how ColorSpaces are created. Two
methods are now offered: one only providing construction of no-argument
color spaces (and thus taking a simple name), and another taking an
ArrayObject, hence used to create ColorSpaces requiring arguments.
Finally, on top of *that* two ways to get a color space in the Renderer
are made available: the first creates a ColorSpace with a name and a
Resources dictionary, and another takes an Object. These model the two
addressing modes described above.
2022-11-24 07:40:24 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
auto color_space_resource_dict = TRY(resources->get_dict(m_document, CommonNames::ColorSpace));
|
2023-07-23 19:35:03 +03:00
|
|
|
|
if (!color_space_resource_dict->contains(color_space_name)) {
|
|
|
|
|
dbgln("missing key {}", color_space_name);
|
|
|
|
|
return Error::rendering_unsupported_error("Missing entry for color space name");
|
|
|
|
|
}
|
2023-10-21 17:42:16 +03:00
|
|
|
|
return get_color_space_from_document(TRY(color_space_resource_dict->get_object(m_document, color_space_name)));
|
LibPDF: Refactor parsing of ColorSpaces
ColorSpaces can be specified in two ways: with a stream as operands of
the color space operations (CS/cs), or as a separate PDF object, which
is then referred to by other means (e.g., from Image XObjects and other
entities). These two modes of addressing a ColorSpace are slightly
different and need to be addressed separately. However, the current
implementation embedded the full logic of the first case in the routine
that created ColorSpace objects.
This commit refactors the creation of ColorSpace to support both cases.
First, a new ColorSpaceFamily class encapsulates the static aspects of a
family, like its name or whether color space construction never requires
parameters. Then we define the supported ColorSpaceFamily objects.
On top of this also sit a breakage on how ColorSpaces are created. Two
methods are now offered: one only providing construction of no-argument
color spaces (and thus taking a simple name), and another taking an
ArrayObject, hence used to create ColorSpaces requiring arguments.
Finally, on top of *that* two ways to get a color space in the Renderer
are made available: the first creates a ColorSpace with a name and a
Resources dictionary, and another takes an Object. These model the two
addressing modes described above.
2022-11-24 07:40:24 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PDFErrorOr<NonnullRefPtr<ColorSpace>> Renderer::get_color_space_from_document(NonnullRefPtr<Object> color_space_object)
|
|
|
|
|
{
|
2023-12-07 16:45:44 +03:00
|
|
|
|
return ColorSpace::create(m_document, color_space_object, *this);
|
2021-05-23 22:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-14 18:11:39 +03:00
|
|
|
|
Gfx::AffineTransform const& Renderer::calculate_text_rendering_matrix() const
|
2021-05-10 20:50:39 +03:00
|
|
|
|
{
|
|
|
|
|
if (m_text_rendering_matrix_is_dirty) {
|
2024-01-17 00:41:51 +03:00
|
|
|
|
// PDF 1.7, 5.3.3. Text Space Details
|
2024-01-17 01:14:56 +03:00
|
|
|
|
Gfx::AffineTransform parameter_matrix {
|
2021-05-10 20:50:39 +03:00
|
|
|
|
text_state().horizontal_scaling,
|
|
|
|
|
0.0f,
|
|
|
|
|
0.0f,
|
|
|
|
|
1.0f,
|
|
|
|
|
0.0f,
|
2024-01-17 01:14:56 +03:00
|
|
|
|
text_state().rise
|
|
|
|
|
};
|
|
|
|
|
m_text_rendering_matrix = state().ctm;
|
2022-03-07 00:27:04 +03:00
|
|
|
|
m_text_rendering_matrix.multiply(m_text_matrix);
|
2024-01-17 01:14:56 +03:00
|
|
|
|
m_text_rendering_matrix.multiply(parameter_matrix);
|
2021-05-10 20:50:39 +03:00
|
|
|
|
m_text_rendering_matrix_is_dirty = false;
|
|
|
|
|
}
|
|
|
|
|
return m_text_rendering_matrix;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-15 15:51:56 +03:00
|
|
|
|
PDFErrorOr<void> Renderer::render_type3_glyph(Gfx::FloatPoint point, StreamObject const& glyph_data, Gfx::AffineTransform const& font_matrix, Optional<NonnullRefPtr<DictObject>> resources)
|
|
|
|
|
{
|
|
|
|
|
ScopedState scoped_state { *this };
|
|
|
|
|
|
|
|
|
|
auto text_rendering_matrix = calculate_text_rendering_matrix();
|
|
|
|
|
text_rendering_matrix.set_translation(point);
|
|
|
|
|
state().ctm = text_rendering_matrix;
|
|
|
|
|
state().ctm.scale(text_state().font_size, text_state().font_size);
|
|
|
|
|
state().ctm.multiply(font_matrix);
|
|
|
|
|
m_text_rendering_matrix_is_dirty = true;
|
|
|
|
|
|
|
|
|
|
auto operators = TRY(Parser::parse_operators(m_document, glyph_data.bytes()));
|
|
|
|
|
for (auto& op : operators)
|
|
|
|
|
TRY(handle_operator(op, resources));
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-10 20:50:39 +03:00
|
|
|
|
}
|