mirror of
https://github.com/RGBCube/serenity
synced 2025-05-21 17:05:11 +00:00

This is a very inefficient implementation: Every time a type 3 font glyph is drawn, we parse its operator stream and execute all the operators therein. We'll want to instead cache the glyphs in bitmaps (at least in most cases), like we do for other fonts. But it's a good first step, and all the coordinate math seems to work in the files I've tested. Good test files from pdfa dataset 0000.zip: - 0000559.pdf page 1 (and 2): Has a non-default font matrix; text appears mirrored if the font matrix isn't handled correctly - 0000425.pdf, page 1: Draws several glyphs in a single run; glyphs overlap if Renderer::render_type3_glyph() ignores the passed-in point - 0000211.pdf, any page: Uses type 3 glyphs for all text. Good perf test (already "reasonably fast") - 0000521.pdf, page 5 (or 7 or or 16): The little red flag in the purple box is a type 3 font glyph, and it's colored (which in part means the first operator is `d0`, while all the other documents above use `d1`)
304 lines
10 KiB
C++
304 lines
10 KiB
C++
/*
|
|
* Copyright (c) 2021-2022, Matthew Olsson <mattco@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <AK/Format.h>
|
|
#include <LibGfx/AffineTransform.h>
|
|
#include <LibGfx/AntiAliasingPainter.h>
|
|
#include <LibGfx/Bitmap.h>
|
|
#include <LibGfx/Font/Font.h>
|
|
#include <LibGfx/Font/FontDatabase.h>
|
|
#include <LibGfx/Painter.h>
|
|
#include <LibGfx/Path.h>
|
|
#include <LibGfx/Point.h>
|
|
#include <LibGfx/Rect.h>
|
|
#include <LibGfx/Size.h>
|
|
#include <LibPDF/ColorSpace.h>
|
|
#include <LibPDF/Document.h>
|
|
#include <LibPDF/Fonts/PDFFont.h>
|
|
#include <LibPDF/Object.h>
|
|
|
|
namespace PDF {
|
|
|
|
enum class LineCapStyle : u8 {
|
|
ButtCap = 0,
|
|
RoundCap = 1,
|
|
SquareCap = 2,
|
|
};
|
|
|
|
enum class LineJoinStyle : u8 {
|
|
Miter = 0,
|
|
Round = 1,
|
|
Bevel = 2,
|
|
};
|
|
|
|
struct LineDashPattern {
|
|
Vector<int> pattern;
|
|
int phase;
|
|
};
|
|
|
|
enum class TextRenderingMode : u8 {
|
|
Fill = 0,
|
|
Stroke = 1,
|
|
FillThenStroke = 2,
|
|
Invisible = 3,
|
|
FillAndClip = 4,
|
|
StrokeAndClip = 5,
|
|
FillStrokeAndClip = 6,
|
|
Clip = 7,
|
|
};
|
|
|
|
struct TextState {
|
|
float character_spacing { 0.0f };
|
|
float word_spacing { 0.0f };
|
|
float horizontal_scaling { 1.0f };
|
|
float leading { 0.0f };
|
|
float font_size { 12.0f };
|
|
RefPtr<PDFFont> font;
|
|
TextRenderingMode rendering_mode { TextRenderingMode::Fill };
|
|
float rise { 0.0f };
|
|
bool knockout { true };
|
|
};
|
|
|
|
struct ClippingPaths {
|
|
Gfx::Path current;
|
|
Gfx::Path next;
|
|
};
|
|
|
|
struct GraphicsState {
|
|
Gfx::AffineTransform ctm;
|
|
ClippingPaths clipping_paths;
|
|
RefPtr<ColorSpace> stroke_color_space { DeviceGrayColorSpace::the() };
|
|
RefPtr<ColorSpace> paint_color_space { DeviceGrayColorSpace::the() };
|
|
Gfx::Color stroke_color { Gfx::Color::NamedColor::Black };
|
|
Gfx::Color paint_color { Gfx::Color::NamedColor::Black };
|
|
DeprecatedString color_rendering_intent { "RelativeColorimetric"sv };
|
|
float flatness_tolerance { 0.0f };
|
|
float line_width { 1.0f };
|
|
LineCapStyle line_cap_style { LineCapStyle::ButtCap };
|
|
LineJoinStyle line_join_style { LineJoinStyle::Miter };
|
|
float miter_limit { 10.0f };
|
|
LineDashPattern line_dash_pattern { {}, 0 };
|
|
TextState text_state {};
|
|
};
|
|
|
|
struct RenderingPreferences {
|
|
bool show_clipping_paths { false };
|
|
bool show_images { true };
|
|
|
|
unsigned hash() const
|
|
{
|
|
return static_cast<unsigned>(show_clipping_paths) | static_cast<unsigned>(show_images) << 1;
|
|
}
|
|
};
|
|
|
|
class Renderer {
|
|
public:
|
|
static PDFErrorsOr<void> render(Document&, Page const&, RefPtr<Gfx::Bitmap>, RenderingPreferences preferences);
|
|
|
|
struct FontCacheKey {
|
|
NonnullRefPtr<DictObject> font_dictionary;
|
|
float font_size;
|
|
|
|
bool operator==(FontCacheKey const&) const = default;
|
|
};
|
|
|
|
ALWAYS_INLINE GraphicsState const& state() const { return m_graphics_state_stack.last(); }
|
|
ALWAYS_INLINE TextState const& text_state() const { return state().text_state; }
|
|
|
|
Gfx::AffineTransform const& calculate_text_rendering_matrix() const;
|
|
|
|
PDFErrorOr<void> render_type3_glyph(Gfx::FloatPoint, StreamObject const&, Gfx::AffineTransform const&, Optional<NonnullRefPtr<DictObject>>);
|
|
|
|
private:
|
|
Renderer(RefPtr<Document>, Page const&, RefPtr<Gfx::Bitmap>, RenderingPreferences);
|
|
|
|
PDFErrorsOr<void> render();
|
|
|
|
PDFErrorOr<void> handle_operator(Operator const&, Optional<NonnullRefPtr<DictObject>> = {});
|
|
#define V(name, snake_name, symbol) \
|
|
PDFErrorOr<void> handle_##snake_name(ReadonlySpan<Value> args, Optional<NonnullRefPtr<DictObject>> = {});
|
|
ENUMERATE_OPERATORS(V)
|
|
#undef V
|
|
PDFErrorOr<void> handle_text_next_line_show_string(ReadonlySpan<Value> args, Optional<NonnullRefPtr<DictObject>> = {});
|
|
PDFErrorOr<void> handle_text_next_line_show_string_set_spacing(ReadonlySpan<Value> args, Optional<NonnullRefPtr<DictObject>> = {});
|
|
|
|
void begin_path_paint();
|
|
void end_path_paint();
|
|
PDFErrorOr<void> set_graphics_state_from_dict(NonnullRefPtr<DictObject>);
|
|
PDFErrorOr<void> show_text(DeprecatedString const&);
|
|
PDFErrorOr<NonnullRefPtr<Gfx::Bitmap>> load_image(NonnullRefPtr<StreamObject>);
|
|
PDFErrorOr<void> show_image(NonnullRefPtr<StreamObject>);
|
|
void show_empty_image(int width, int height);
|
|
PDFErrorOr<NonnullRefPtr<ColorSpace>> get_color_space_from_resources(Value const&, NonnullRefPtr<DictObject>);
|
|
PDFErrorOr<NonnullRefPtr<ColorSpace>> get_color_space_from_document(NonnullRefPtr<Object>);
|
|
|
|
ALWAYS_INLINE GraphicsState& state() { return m_graphics_state_stack.last(); }
|
|
ALWAYS_INLINE TextState& text_state() { return state().text_state; }
|
|
|
|
template<typename T>
|
|
ALWAYS_INLINE Gfx::Point<T> map(T x, T y) const;
|
|
|
|
template<typename T>
|
|
ALWAYS_INLINE Gfx::Size<T> map(Gfx::Size<T>) const;
|
|
|
|
template<typename T>
|
|
ALWAYS_INLINE Gfx::Rect<T> map(Gfx::Rect<T>) const;
|
|
|
|
Gfx::AffineTransform calculate_image_space_transformation(int width, int height);
|
|
|
|
PDFErrorOr<NonnullRefPtr<PDFFont>> get_font(FontCacheKey const&);
|
|
|
|
class ScopedState;
|
|
|
|
RefPtr<Document> m_document;
|
|
RefPtr<Gfx::Bitmap> m_bitmap;
|
|
Page const& m_page;
|
|
Gfx::Painter m_painter;
|
|
Gfx::AntiAliasingPainter m_anti_aliasing_painter;
|
|
RenderingPreferences m_rendering_preferences;
|
|
|
|
Gfx::Path m_current_path;
|
|
Vector<GraphicsState> m_graphics_state_stack;
|
|
Gfx::AffineTransform m_text_matrix;
|
|
Gfx::AffineTransform m_text_line_matrix;
|
|
|
|
bool mutable m_text_rendering_matrix_is_dirty { true };
|
|
Gfx::AffineTransform mutable m_text_rendering_matrix;
|
|
|
|
HashMap<FontCacheKey, NonnullRefPtr<PDFFont>> m_font_cache;
|
|
};
|
|
|
|
}
|
|
|
|
namespace AK {
|
|
|
|
template<>
|
|
struct Traits<PDF::Renderer::FontCacheKey> : public DefaultTraits<PDF::Renderer::FontCacheKey> {
|
|
static unsigned hash(PDF::Renderer::FontCacheKey const& key)
|
|
{
|
|
return pair_int_hash(ptr_hash(key.font_dictionary.ptr()), int_hash(bit_cast<u32>(key.font_size)));
|
|
}
|
|
};
|
|
|
|
template<>
|
|
struct Formatter<PDF::LineCapStyle> : Formatter<StringView> {
|
|
ErrorOr<void> format(FormatBuilder& builder, PDF::LineCapStyle const& style)
|
|
{
|
|
switch (style) {
|
|
case PDF::LineCapStyle::ButtCap:
|
|
return builder.put_string("LineCapStyle::ButtCap"sv);
|
|
case PDF::LineCapStyle::RoundCap:
|
|
return builder.put_string("LineCapStyle::RoundCap"sv);
|
|
case PDF::LineCapStyle::SquareCap:
|
|
return builder.put_string("LineCapStyle::SquareCap"sv);
|
|
}
|
|
}
|
|
};
|
|
|
|
template<>
|
|
struct Formatter<PDF::LineJoinStyle> : Formatter<StringView> {
|
|
ErrorOr<void> format(FormatBuilder& builder, PDF::LineJoinStyle const& style)
|
|
{
|
|
switch (style) {
|
|
case PDF::LineJoinStyle::Miter:
|
|
return builder.put_string("LineJoinStyle::Miter"sv);
|
|
case PDF::LineJoinStyle::Round:
|
|
return builder.put_string("LineJoinStyle::Round"sv);
|
|
case PDF::LineJoinStyle::Bevel:
|
|
return builder.put_string("LineJoinStyle::Bevel"sv);
|
|
}
|
|
}
|
|
};
|
|
|
|
template<>
|
|
struct Formatter<PDF::LineDashPattern> : Formatter<StringView> {
|
|
ErrorOr<void> format(FormatBuilder& format_builder, PDF::LineDashPattern const& pattern)
|
|
{
|
|
StringBuilder builder;
|
|
builder.append('[');
|
|
bool first = true;
|
|
|
|
for (auto& i : pattern.pattern) {
|
|
if (!first)
|
|
builder.append(", "sv);
|
|
first = false;
|
|
builder.appendff("{}", i);
|
|
}
|
|
|
|
builder.appendff("] {}", pattern.phase);
|
|
return format_builder.put_string(builder.to_deprecated_string());
|
|
}
|
|
};
|
|
|
|
template<>
|
|
struct Formatter<PDF::TextRenderingMode> : Formatter<StringView> {
|
|
ErrorOr<void> format(FormatBuilder& builder, PDF::TextRenderingMode const& style)
|
|
{
|
|
switch (style) {
|
|
case PDF::TextRenderingMode::Fill:
|
|
return builder.put_string("TextRenderingMode::Fill"sv);
|
|
case PDF::TextRenderingMode::Stroke:
|
|
return builder.put_string("TextRenderingMode::Stroke"sv);
|
|
case PDF::TextRenderingMode::FillThenStroke:
|
|
return builder.put_string("TextRenderingMode::FillThenStroke"sv);
|
|
case PDF::TextRenderingMode::Invisible:
|
|
return builder.put_string("TextRenderingMode::Invisible"sv);
|
|
case PDF::TextRenderingMode::FillAndClip:
|
|
return builder.put_string("TextRenderingMode::FillAndClip"sv);
|
|
case PDF::TextRenderingMode::StrokeAndClip:
|
|
return builder.put_string("TextRenderingMode::StrokeAndClip"sv);
|
|
case PDF::TextRenderingMode::FillStrokeAndClip:
|
|
return builder.put_string("TextRenderingMode::FillStrokeAndClip"sv);
|
|
case PDF::TextRenderingMode::Clip:
|
|
return builder.put_string("TextRenderingMode::Clip"sv);
|
|
}
|
|
}
|
|
};
|
|
|
|
template<>
|
|
struct Formatter<PDF::TextState> : Formatter<StringView> {
|
|
ErrorOr<void> format(FormatBuilder& format_builder, PDF::TextState const& state)
|
|
{
|
|
StringBuilder builder;
|
|
builder.append("TextState {\n"sv);
|
|
builder.appendff(" character_spacing={}\n", state.character_spacing);
|
|
builder.appendff(" word_spacing={}\n", state.word_spacing);
|
|
builder.appendff(" horizontal_scaling={}\n", state.horizontal_scaling);
|
|
builder.appendff(" leading={}\n", state.leading);
|
|
builder.appendff(" font_size={}\n", state.font_size);
|
|
builder.appendff(" rendering_mode={}\n", state.rendering_mode);
|
|
builder.appendff(" rise={}\n", state.rise);
|
|
builder.appendff(" knockout={}\n", state.knockout);
|
|
builder.append(" }"sv);
|
|
return format_builder.put_string(builder.to_deprecated_string());
|
|
}
|
|
};
|
|
|
|
template<>
|
|
struct Formatter<PDF::GraphicsState> : Formatter<StringView> {
|
|
ErrorOr<void> format(FormatBuilder& format_builder, PDF::GraphicsState const& state)
|
|
{
|
|
StringBuilder builder;
|
|
builder.append("GraphicsState {\n"sv);
|
|
builder.appendff(" ctm={}\n", state.ctm);
|
|
builder.appendff(" stroke_color={}\n", state.stroke_color);
|
|
builder.appendff(" paint_color={}\n", state.paint_color);
|
|
builder.appendff(" color_rendering_intent={}\n", state.color_rendering_intent);
|
|
builder.appendff(" flatness_tolerance={}\n", state.flatness_tolerance);
|
|
builder.appendff(" line_width={}\n", state.line_width);
|
|
builder.appendff(" line_cap_style={}\n", state.line_cap_style);
|
|
builder.appendff(" line_join_style={}\n", state.line_join_style);
|
|
builder.appendff(" miter_limit={}\n", state.miter_limit);
|
|
builder.appendff(" line_dash_pattern={}\n", state.line_dash_pattern);
|
|
builder.appendff(" text_state={}\n", state.text_state);
|
|
builder.append('}');
|
|
return format_builder.put_string(builder.to_deprecated_string());
|
|
}
|
|
};
|
|
|
|
}
|