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;