From adc45635e933821f13e771a1f6422f4f255d0324 Mon Sep 17 00:00:00 2001 From: Rodrigo Tobar Date: Fri, 25 Nov 2022 02:01:53 +0800 Subject: [PATCH] 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). --- Userland/Libraries/LibPDF/CommonNames.h | 4 + Userland/Libraries/LibPDF/Renderer.cpp | 139 +++++++++++++++++++++++- Userland/Libraries/LibPDF/Renderer.h | 10 ++ 3 files changed, 148 insertions(+), 5 deletions(-) diff --git a/Userland/Libraries/LibPDF/CommonNames.h b/Userland/Libraries/LibPDF/CommonNames.h index 7d56bfc679d..857bacd7b2f 100644 --- a/Userland/Libraries/LibPDF/CommonNames.h +++ b/Userland/Libraries/LibPDF/CommonNames.h @@ -37,6 +37,7 @@ A(DW) \ A(DCTDecode) \ A(DecodeParms) \ + A(Decode) \ A(DescendantFonts) \ A(Dest) \ A(Dests) \ @@ -70,11 +71,13 @@ A(FontFile3) \ A(Gamma) \ A(H) \ + A(Height) \ A(HT) \ A(HTO) \ A(ICCBased) \ A(ID) \ A(Image) \ + A(ImageMask) \ A(Index) \ A(JBIG2Decode) \ A(JPXDecode) \ @@ -133,6 +136,7 @@ A(UserUnit) \ A(W) \ A(WhitePoint) \ + A(Width) \ A(Widths) \ A(XObject) \ A(XYZ) \ diff --git a/Userland/Libraries/LibPDF/Renderer.cpp b/Userland/Libraries/LibPDF/Renderer.cpp index 0fde4418edc..de597c6e213 100644 --- a/Userland/Libraries/LibPDF/Renderer.cpp +++ b/Userland/Libraries/LibPDF/Renderer.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #define RENDERER_HANDLER(name) \ @@ -623,9 +624,14 @@ RENDERER_HANDLER(paint_xobject) auto xobjects_dict = MUST(resources->get_dict(m_document, CommonNames::XObject)); auto xobject = MUST(xobjects_dict->get_stream(m_document, xobject_name)); + Optional> xobject_resources {}; + if (xobject->dict()->contains(CommonNames::Resources)) { + xobject_resources = xobject->dict()->get_dict(m_document, CommonNames::Resources).value(); + } + auto subtype = MUST(xobject->dict()->get_name(m_document, CommonNames::Subtype))->name(); if (subtype == CommonNames::Image) { - dbgln("Skipping image"); + TRY(show_image(xobject)); return {}; } @@ -637,10 +643,6 @@ RENDERER_HANDLER(paint_xobject) matrix = Vector { Value { 1 }, Value { 0 }, Value { 0 }, Value { 1 }, Value { 0 }, Value { 0 } }; } MUST(handle_concatenate_matrix(matrix)); - Optional> xobject_resources {}; - if (xobject->dict()->contains(CommonNames::Resources)) { - xobject_resources = xobject->dict()->get_dict(m_document, CommonNames::Resources).value(); - } auto operators = TRY(Parser::parse_operators(m_document, xobject->bytes())); for (auto& op : operators) TRY(handle_operator(op, xobject_resources)); @@ -759,6 +761,133 @@ void Renderer::show_text(DeprecatedString const& string) m_text_matrix.translate(delta_x / text_rendering_matrix.x_scale(), 0.0f); } +PDFErrorOr> Renderer::load_image(NonnullRefPtr image) +{ + auto image_dict = image->dict(); + auto filter_object = TRY(image_dict->get_object(m_document, CommonNames::Filter)); + auto width = image_dict->get_value(CommonNames::Width).get(); + auto height = image_dict->get_value(CommonNames::Height).get(); + + auto is_filter = [&](FlyString const& name) { + if (filter_object->is()) + return filter_object->cast()->name() == name; + auto filters = filter_object->cast(); + return MUST(filters->get_name_at(m_document, 0))->name() == name; + }; + if (is_filter(CommonNames::JPXDecode)) { + return Error(Error::Type::RenderingUnsupported, "JPXDecode filter"); + } + if (image_dict->contains(CommonNames::ImageMask)) { + auto is_mask = image_dict->get_value(CommonNames::ImageMask).get(); + if (is_mask) { + return Error(Error::Type::RenderingUnsupported, "Image masks"); + } + } + + auto color_space_object = MUST(image_dict->get_object(m_document, CommonNames::ColorSpace)); + auto color_space = TRY(get_color_space_from_document(color_space_object)); + auto bits_per_component = image_dict->get_value(CommonNames::BitsPerComponent).get(); + if (bits_per_component != 8) { + return Error(Error::Type::RenderingUnsupported, "Image's bit per component != 8"); + } + + Vector 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 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); + } + + if (is_filter(CommonNames::DCTDecode)) { + // TODO: stream objects could store Variant to avoid seialisation/deserialisation here + return TRY(Gfx::Bitmap::try_create_from_serialized_bytes(image->bytes())); + } + + auto bitmap = MUST(Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, { width, height })); + int x = 0; + int y = 0; + int const n_components = color_space->number_of_components(); + auto const bytes_per_component = bits_per_component / 8; + Vector component_values; + component_values.resize(n_components); + auto content = image->bytes(); + 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); + component_values[i] = Value { component_value_decoders[i].interpolate(component[0]) }; + } + auto color = color_space->color(component_values); + bitmap->set_pixel(x, y, color); + ++x; + if (x == width) { + x = 0; + ++y; + } + } + return bitmap; +} + +Gfx::AffineTransform Renderer::calculate_image_space_transformation(int width, int height) +{ + // 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( + 1.0f / width, + 0.0f, + 0.0f, + -1.0f / height, + 0.0f, + 1.0f)); + return image_space; +} + +void Renderer::show_empty_image(int width, int height) +{ + auto image_space_transofmation = calculate_image_space_transformation(width, height); + auto image_border = image_space_transofmation.map(Gfx::IntRect { 0, 0, width, height }); + m_painter.stroke_path(rect_path(image_border), Color::Black, 1); +} + +PDFErrorOr Renderer::show_image(NonnullRefPtr image) +{ + auto image_dict = image->dict(); + auto width = image_dict->get_value(CommonNames::Width).get(); + auto height = image_dict->get_value(CommonNames::Height).get(); + + if (!m_rendering_preferences.show_images) { + show_empty_image(width, height); + return {}; + } + auto image_bitmap = TRY(load_image(image)); + if (image_dict->contains(CommonNames::SMask)) { + auto smask_bitmap = TRY(load_image(TRY(image_dict->get_stream(m_document, CommonNames::SMask)))); + VERIFY(smask_bitmap->rect() == image_bitmap->rect()); + 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 smask_color = smask_bitmap->get_pixel(i, j); + image_color = image_color.with_alpha(smask_color.luminosity()); + image_bitmap->set_pixel(i, j, image_color); + } + } + } + + auto image_space = calculate_image_space_transformation(width, height); + auto image_rect = Gfx::FloatRect { 0, 0, width, height }; + m_painter.draw_scaled_bitmap_with_transform(image_bitmap->rect(), image_bitmap, image_rect, image_space); + return {}; +} + PDFErrorOr> Renderer::get_color_space_from_resources(Value const& value, NonnullRefPtr resources) { auto color_space_name = value.get>()->cast()->name(); diff --git a/Userland/Libraries/LibPDF/Renderer.h b/Userland/Libraries/LibPDF/Renderer.h index 762c2207c8a..c8d0814b64b 100644 --- a/Userland/Libraries/LibPDF/Renderer.h +++ b/Userland/Libraries/LibPDF/Renderer.h @@ -86,6 +86,12 @@ struct GraphicsState { struct RenderingPreferences { bool show_clipping_paths { false }; + bool show_images { true }; + + unsigned hash() const + { + return static_cast(show_clipping_paths) | static_cast(show_images) << 1; + } }; class Renderer { @@ -109,6 +115,9 @@ private: void end_path_paint(); PDFErrorOr set_graphics_state_from_dict(NonnullRefPtr); void show_text(DeprecatedString const&); + PDFErrorOr> load_image(NonnullRefPtr); + PDFErrorOr show_image(NonnullRefPtr); + void show_empty_image(int width, int height); PDFErrorOr> get_color_space_from_resources(Value const&, NonnullRefPtr); PDFErrorOr> get_color_space_from_document(NonnullRefPtr); @@ -127,6 +136,7 @@ private: ALWAYS_INLINE Gfx::Rect map(Gfx::Rect) const; Gfx::AffineTransform const& calculate_text_rendering_matrix(); + Gfx::AffineTransform calculate_image_space_transformation(int width, int height); RefPtr m_document; RefPtr m_bitmap;