1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-21 19:15:07 +00:00
serenity/Userland/Libraries/LibPDF/Renderer.cpp
Matthew Olsson 4d509ff365 LibPDF: Fix "incorrect" matrix multiplication in Renderer
Incorrect is in quotes because the spec (both 1.7 and 2.0) specify this
multiplication as it was originally! However, flipping the order of
operations here makes the text in all of my test cases render in the
correct position.

The CTM is a transformation matrix between the text coordinate system
and the device coordinate system. However, being on the right-side of
the multiplication means that the CTM scale parameters don't have any
influence on the translation component of the left-side matrix. This
oddity is what originally led to me just trying this change to see if
it worked.
2022-03-07 10:53:57 +01:00

704 lines
19 KiB
C++

/*
* Copyright (c) 2021-2022, Matthew Olsson <mattco@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Utf8View.h>
#include <LibPDF/CommonNames.h>
#include <LibPDF/Renderer.h>
#define RENDERER_HANDLER(name) \
PDFErrorOr<void> Renderer::handle_##name([[maybe_unused]] Vector<Value> const& args)
#define RENDERER_TODO(name) \
RENDERER_HANDLER(name) \
{ \
dbgln("[PDF::Renderer] Unsupported draw operation " #name); \
TODO(); \
}
namespace PDF {
PDFErrorOr<void> Renderer::render(Document& document, Page const& page, RefPtr<Gfx::Bitmap> bitmap)
{
return Renderer(document, page, bitmap).render();
}
Renderer::Renderer(RefPtr<Document> document, Page const& page, RefPtr<Gfx::Bitmap> bitmap)
: m_document(document)
, m_bitmap(bitmap)
, m_page(page)
, m_painter(*bitmap)
{
auto media_box = m_page.media_box;
Gfx::AffineTransform userspace_matrix;
userspace_matrix.translate(media_box.lower_left_x, media_box.lower_left_y);
float width = media_box.upper_right_x - media_box.lower_left_x;
float height = media_box.upper_right_y - media_box.lower_left_y;
float scale_x = static_cast<float>(bitmap->width()) / width;
float scale_y = static_cast<float>(bitmap->height()) / height;
userspace_matrix.scale(scale_x, scale_y);
// 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);
m_graphics_state_stack.append(GraphicsState { userspace_matrix });
m_bitmap->fill(Gfx::Color::NamedColor::White);
}
PDFErrorOr<void> Renderer::render()
{
// Use our own vector, as the /Content can be an array with multiple
// streams which gets concatenated
// FIXME: Text operators are supposed to only have effects on the current
// stream object. Do the text operators treat this concatenated stream
// as one stream or multiple?
ByteBuffer byte_buffer;
if (m_page.contents->is<ArrayObject>()) {
auto contents = m_page.contents->cast<ArrayObject>();
for (auto& ref : *contents) {
auto bytes = TRY(m_document->resolve_to<StreamObject>(ref))->bytes();
byte_buffer.append(bytes.data(), bytes.size());
}
} else {
auto bytes = m_page.contents->cast<StreamObject>()->bytes();
byte_buffer.append(bytes.data(), bytes.size());
}
auto commands = TRY(Parser::parse_graphics_commands(byte_buffer));
for (auto& command : commands)
TRY(handle_command(command));
return {};
}
PDFErrorOr<void> Renderer::handle_command(Command const& command)
{
switch (command.command_type()) {
#define V(name, snake_name, symbol) \
case CommandType::name: \
TRY(handle_##snake_name(command.arguments())); \
break;
ENUMERATE_COMMANDS(V)
#undef V
case CommandType::TextNextLineShowString:
TRY(handle_text_next_line_show_string(command.arguments()));
break;
case CommandType::TextNextLineShowStringSetSpacing:
TRY(handle_text_next_line_show_string_set_spacing(command.arguments()));
break;
}
return {};
}
RENDERER_HANDLER(save_state)
{
m_graphics_state_stack.append(state());
return {};
}
RENDERER_HANDLER(restore_state)
{
m_graphics_state_stack.take_last();
return {};
}
RENDERER_HANDLER(concatenate_matrix)
{
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;
return {};
}
RENDERER_HANDLER(set_line_width)
{
state().line_width = args[0].to_float();
return {};
}
RENDERER_HANDLER(set_line_cap)
{
state().line_cap_style = static_cast<LineCapStyle>(args[0].get<int>());
return {};
}
RENDERER_HANDLER(set_line_join)
{
state().line_join_style = static_cast<LineJoinStyle>(args[0].get<int>());
return {};
}
RENDERER_HANDLER(set_miter_limit)
{
state().miter_limit = args[0].to_float();
return {};
}
RENDERER_HANDLER(set_dash_pattern)
{
auto dash_array = MUST(m_document->resolve_to<ArrayObject>(args[0]));
Vector<int> pattern;
for (auto& element : *dash_array)
pattern.append(element.get<int>());
state().line_dash_pattern = LineDashPattern { pattern, args[1].get<int>() };
return {};
}
RENDERER_TODO(set_color_rendering_intent)
RENDERER_TODO(set_flatness_tolerance)
RENDERER_HANDLER(set_graphics_state_from_dict)
{
VERIFY(m_page.resources->contains(CommonNames::ExtGState));
auto dict_name = MUST(m_document->resolve_to<NameObject>(args[0]))->name();
auto ext_gstate_dict = MUST(m_page.resources->get_dict(m_document, CommonNames::ExtGState));
auto target_dict = MUST(ext_gstate_dict->get_dict(m_document, dict_name));
TRY(set_graphics_state_from_dict(target_dict));
return {};
}
RENDERER_HANDLER(path_move)
{
m_current_path.move_to(map(args[0].to_float(), args[1].to_float()));
return {};
}
RENDERER_HANDLER(path_line)
{
VERIFY(!m_current_path.segments().is_empty());
m_current_path.line_to(map(args[0].to_float(), args[1].to_float()));
return {};
}
RENDERER_TODO(path_cubic_bezier_curve)
RENDERER_TODO(path_cubic_bezier_curve_no_first_control)
RENDERER_TODO(path_cubic_bezier_curve_no_second_control)
RENDERER_HANDLER(path_close)
{
m_current_path.close();
return {};
}
RENDERER_HANDLER(path_append_rect)
{
auto pos = map(args[0].to_float(), args[1].to_float());
auto size = map(Gfx::FloatSize { args[2].to_float(), args[3].to_float() });
m_current_path.move_to(pos);
m_current_path.line_to({ pos.x() + size.width(), pos.y() });
m_current_path.line_to({ pos.x() + size.width(), pos.y() + size.height() });
m_current_path.line_to({ pos.x(), pos.y() + size.height() });
m_current_path.close();
return {};
}
RENDERER_HANDLER(path_stroke)
{
m_painter.stroke_path(m_current_path, state().stroke_color, state().line_width);
m_current_path.clear();
return {};
}
RENDERER_HANDLER(path_close_and_stroke)
{
m_current_path.close();
TRY(handle_path_stroke(args));
return {};
}
RENDERER_HANDLER(path_fill_nonzero)
{
m_painter.fill_path(m_current_path, state().paint_color, Gfx::Painter::WindingRule::Nonzero);
m_current_path.clear();
return {};
}
RENDERER_HANDLER(path_fill_nonzero_deprecated)
{
TRY(handle_path_fill_nonzero(args));
return {};
}
RENDERER_HANDLER(path_fill_evenodd)
{
m_painter.fill_path(m_current_path, state().paint_color, Gfx::Painter::WindingRule::EvenOdd);
m_current_path.clear();
return {};
}
RENDERER_HANDLER(path_fill_stroke_nonzero)
{
m_painter.stroke_path(m_current_path, state().stroke_color, state().line_width);
TRY(handle_path_fill_nonzero(args));
return {};
}
RENDERER_HANDLER(path_fill_stroke_evenodd)
{
m_painter.stroke_path(m_current_path, state().stroke_color, state().line_width);
TRY(handle_path_fill_evenodd(args));
return {};
}
RENDERER_HANDLER(path_close_fill_stroke_nonzero)
{
m_current_path.close();
TRY(handle_path_fill_stroke_nonzero(args));
return {};
}
RENDERER_HANDLER(path_close_fill_stroke_evenodd)
{
m_current_path.close();
TRY(handle_path_fill_stroke_evenodd(args));
return {};
}
RENDERER_HANDLER(path_end)
{
return {};
}
RENDERER_HANDLER(path_intersect_clip_nonzero)
{
// FIXME: Support arbitrary path clipping in the painter and utilize that here
auto bounding_box = map(m_current_path.bounding_box());
m_painter.add_clip_rect(bounding_box.to_type<int>());
return {};
}
RENDERER_HANDLER(path_intersect_clip_evenodd)
{
// FIXME: Support arbitrary path clipping in the painter and utilize that here
auto bounding_box = map(m_current_path.bounding_box());
m_painter.add_clip_rect(bounding_box.to_type<int>());
return {};
}
RENDERER_HANDLER(text_begin)
{
m_text_matrix = Gfx::AffineTransform();
m_text_line_matrix = Gfx::AffineTransform();
return {};
}
RENDERER_HANDLER(text_end)
{
// FIXME: Do we need to do anything here?
return {};
}
RENDERER_HANDLER(text_set_char_space)
{
text_state().character_spacing = args[0].to_float();
return {};
}
RENDERER_HANDLER(text_set_word_space)
{
text_state().word_spacing = args[0].to_float();
return {};
}
RENDERER_HANDLER(text_set_horizontal_scale)
{
m_text_rendering_matrix_is_dirty = true;
text_state().horizontal_scaling = args[0].to_float() / 100.0f;
return {};
}
RENDERER_HANDLER(text_set_leading)
{
text_state().leading = args[0].to_float();
return {};
}
RENDERER_HANDLER(text_set_font)
{
auto target_font_name = MUST(m_document->resolve_to<NameObject>(args[0]))->name();
auto fonts_dictionary = MUST(m_page.resources->get_dict(m_document, CommonNames::Font));
auto font_dictionary = MUST(fonts_dictionary->get_dict(m_document, target_font_name));
// FIXME: We do not yet have the standard 14 fonts, as some of them are not open fonts,
// so we just use LiberationSerif for everything
auto font_name = MUST(font_dictionary->get_name(m_document, CommonNames::BaseFont))->name().to_lowercase();
auto font_view = font_name.view();
bool is_bold = font_view.contains("bold");
bool is_italic = font_view.contains("italic");
String font_variant;
if (is_bold && is_italic) {
font_variant = "BoldItalic";
} else if (is_bold) {
font_variant = "Bold";
} else if (is_italic) {
font_variant = "Italic";
} else {
font_variant = "Regular";
}
text_state().font_size = args[1].to_float();
text_state().font_variant = font_variant;
m_text_rendering_matrix_is_dirty = true;
return {};
}
RENDERER_HANDLER(text_set_rendering_mode)
{
text_state().rendering_mode = static_cast<TextRenderingMode>(args[0].get<int>());
return {};
}
RENDERER_HANDLER(text_set_rise)
{
m_text_rendering_matrix_is_dirty = true;
text_state().rise = args[0].to_float();
return {};
}
RENDERER_HANDLER(text_next_line_offset)
{
Gfx::AffineTransform transform(1.0f, 0.0f, 0.0f, 1.0f, args[0].to_float(), args[1].to_float());
transform.multiply(m_text_line_matrix);
m_text_matrix = transform;
m_text_line_matrix = transform;
m_text_rendering_matrix_is_dirty = true;
return {};
}
RENDERER_HANDLER(text_next_line_and_set_leading)
{
text_state().leading = -args[1].to_float();
TRY(handle_text_next_line_offset(args));
return {};
}
RENDERER_HANDLER(text_set_matrix_and_line_matrix)
{
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;
return {};
}
RENDERER_HANDLER(text_next_line)
{
TRY(handle_text_next_line_offset({ 0.0f, -text_state().leading }));
return {};
}
RENDERER_HANDLER(text_show_string)
{
auto text = MUST(m_document->resolve_to<StringObject>(args[0]))->string();
show_text(text);
return {};
}
RENDERER_HANDLER(text_next_line_show_string)
{
TRY(handle_text_next_line(args));
TRY(handle_text_show_string(args));
return {};
}
RENDERER_TODO(text_next_line_show_string_set_spacing)
RENDERER_HANDLER(text_show_string_array)
{
auto elements = MUST(m_document->resolve_to<ArrayObject>(args[0]))->elements();
float next_shift = 0.0f;
for (auto& element : elements) {
if (element.has<int>()) {
next_shift = element.get<int>();
} else if (element.has<float>()) {
next_shift = element.get<float>();
} else {
auto str = element.get<NonnullRefPtr<Object>>()->cast<StringObject>()->string();
show_text(str, next_shift);
}
}
return {};
}
RENDERER_TODO(type3_font_set_glyph_width)
RENDERER_TODO(type3_font_set_glyph_width_and_bbox)
RENDERER_HANDLER(set_stroking_space)
{
state().stroke_color_space = MUST(get_color_space(args[0]));
VERIFY(state().stroke_color_space);
return {};
}
RENDERER_HANDLER(set_painting_space)
{
state().paint_color_space = MUST(get_color_space(args[0]));
VERIFY(state().paint_color_space);
return {};
}
RENDERER_HANDLER(set_stroking_color)
{
state().stroke_color = state().stroke_color_space->color(args);
return {};
}
RENDERER_TODO(set_stroking_color_extended)
RENDERER_HANDLER(set_painting_color)
{
state().paint_color = state().paint_color_space->color(args);
return {};
}
RENDERER_TODO(set_painting_color_extended)
RENDERER_HANDLER(set_stroking_color_and_space_to_gray)
{
state().stroke_color_space = DeviceGrayColorSpace::the();
state().stroke_color = state().stroke_color_space->color(args);
return {};
}
RENDERER_HANDLER(set_painting_color_and_space_to_gray)
{
state().paint_color_space = DeviceGrayColorSpace::the();
state().paint_color = state().paint_color_space->color(args);
return {};
}
RENDERER_HANDLER(set_stroking_color_and_space_to_rgb)
{
state().stroke_color_space = DeviceRGBColorSpace::the();
state().stroke_color = state().stroke_color_space->color(args);
return {};
}
RENDERER_HANDLER(set_painting_color_and_space_to_rgb)
{
state().paint_color_space = DeviceRGBColorSpace::the();
state().paint_color = state().paint_color_space->color(args);
return {};
}
RENDERER_HANDLER(set_stroking_color_and_space_to_cmyk)
{
state().stroke_color_space = DeviceCMYKColorSpace::the();
state().stroke_color = state().stroke_color_space->color(args);
return {};
}
RENDERER_HANDLER(set_painting_color_and_space_to_cmyk)
{
state().paint_color_space = DeviceCMYKColorSpace::the();
state().paint_color = state().paint_color_space->color(args);
return {};
}
RENDERER_TODO(shade)
RENDERER_TODO(inline_image_begin)
RENDERER_TODO(inline_image_begin_data)
RENDERER_TODO(inline_image_end)
RENDERER_TODO(paint_xobject)
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 {};
}
RENDERER_TODO(compatibility_begin)
RENDERER_TODO(compatibility_end)
template<typename T>
Gfx::Point<T> Renderer::map(T x, T y) const
{
auto mapped = state().ctm.map(Gfx::Point<T> { x, y });
return { mapped.x(), static_cast<T>(m_bitmap->height()) - mapped.y() };
}
template<typename T>
Gfx::Size<T> Renderer::map(Gfx::Size<T> size) const
{
return state().ctm.map(size);
}
template<typename T>
Gfx::Rect<T> Renderer::map(Gfx::Rect<T> rect) const
{
return state().ctm.map(rect);
}
PDFErrorOr<void> Renderer::set_graphics_state_from_dict(NonnullRefPtr<DictObject> dict)
{
if (dict->contains(CommonNames::LW))
TRY(handle_set_line_width({ dict->get_value(CommonNames::LW) }));
if (dict->contains(CommonNames::LC))
TRY(handle_set_line_cap({ dict->get_value(CommonNames::LC) }));
if (dict->contains(CommonNames::LJ))
TRY(handle_set_line_join({ dict->get_value(CommonNames::LJ) }));
if (dict->contains(CommonNames::ML))
TRY(handle_set_miter_limit({ dict->get_value(CommonNames::ML) }));
if (dict->contains(CommonNames::D)) {
auto array = MUST(dict->get_array(m_document, CommonNames::D));
TRY(handle_set_dash_pattern(array->elements()));
}
if (dict->contains(CommonNames::FL))
TRY(handle_set_flatness_tolerance({ dict->get_value(CommonNames::FL) }));
return {};
}
void Renderer::show_text(String const& string, float shift)
{
auto utf = Utf8View(string);
auto& text_rendering_matrix = calculate_text_rendering_matrix();
auto font_size = static_cast<int>(text_rendering_matrix.x_scale() * text_state().font_size);
auto font = Gfx::FontDatabase::the().get(text_state().font_family, text_state().font_variant, font_size);
VERIFY(font);
auto glyph_position = text_rendering_matrix.map(Gfx::FloatPoint { 0.0f, 0.0f });
// Account for the reversed font baseline
glyph_position.set_y(glyph_position.y() - static_cast<float>(font->baseline()));
auto original_position = glyph_position;
for (auto code_point : utf) {
if (code_point != 0x20)
m_painter.draw_glyph(glyph_position.to_type<int>(), code_point, *font, state().paint_color);
auto glyph_width = static_cast<float>(font->glyph_width(code_point));
auto tx = (glyph_width - shift / 1000.0f);
tx += text_state().character_spacing;
if (code_point == ' ')
tx += text_state().word_spacing;
tx *= text_state().horizontal_scaling;
glyph_position += { tx, 0.0f };
}
// Update text matrix
auto delta_x = glyph_position.x() - original_position.x();
m_text_rendering_matrix_is_dirty = true;
m_text_matrix = Gfx::AffineTransform(1, 0, 0, 1, delta_x, 0).multiply(m_text_matrix);
}
PDFErrorOr<NonnullRefPtr<ColorSpace>> Renderer::get_color_space(Value const& value)
{
auto name = value.get<NonnullRefPtr<Object>>()->cast<NameObject>()->name();
// Simple color spaces with no parameters, which can be specified directly
if (name == CommonNames::DeviceGray)
return DeviceGrayColorSpace::the();
if (name == CommonNames::DeviceRGB)
return DeviceRGBColorSpace::the();
if (name == CommonNames::DeviceCMYK)
return DeviceCMYKColorSpace::the();
if (name == CommonNames::Pattern)
TODO();
// The color space is a complex color space with parameters that resides in
// the resource dictionary
auto color_space_resource_dict = TRY(m_page.resources->get_dict(m_document, CommonNames::ColorSpace));
if (!color_space_resource_dict->contains(name))
TODO();
auto color_space_array = TRY(color_space_resource_dict->get_array(m_document, name));
name = TRY(color_space_array->get_name_at(m_document, 0))->name();
Vector<Value> parameters;
parameters.ensure_capacity(color_space_array->size() - 1);
for (size_t i = 1; i < color_space_array->size(); i++)
parameters.unchecked_append(color_space_array->at(i));
if (name == CommonNames::CalRGB)
return TRY(CalRGBColorSpace::create(m_document, move(parameters)));
TODO();
}
Gfx::AffineTransform const& Renderer::calculate_text_rendering_matrix()
{
if (m_text_rendering_matrix_is_dirty) {
m_text_rendering_matrix = Gfx::AffineTransform(
text_state().horizontal_scaling,
0.0f,
0.0f,
1.0f,
0.0f,
text_state().rise);
m_text_rendering_matrix.multiply(state().ctm);
m_text_rendering_matrix.multiply(m_text_matrix);
m_text_rendering_matrix_is_dirty = false;
}
return m_text_rendering_matrix;
}
}