1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 06:57:45 +00: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).
This commit is contained in:
Rodrigo Tobar 2022-11-25 02:01:53 +08:00 committed by Andreas Kling
parent 2331fe5e68
commit adc45635e9
3 changed files with 148 additions and 5 deletions

View file

@ -37,6 +37,7 @@
A(DW) \ A(DW) \
A(DCTDecode) \ A(DCTDecode) \
A(DecodeParms) \ A(DecodeParms) \
A(Decode) \
A(DescendantFonts) \ A(DescendantFonts) \
A(Dest) \ A(Dest) \
A(Dests) \ A(Dests) \
@ -70,11 +71,13 @@
A(FontFile3) \ A(FontFile3) \
A(Gamma) \ A(Gamma) \
A(H) \ A(H) \
A(Height) \
A(HT) \ A(HT) \
A(HTO) \ A(HTO) \
A(ICCBased) \ A(ICCBased) \
A(ID) \ A(ID) \
A(Image) \ A(Image) \
A(ImageMask) \
A(Index) \ A(Index) \
A(JBIG2Decode) \ A(JBIG2Decode) \
A(JPXDecode) \ A(JPXDecode) \
@ -133,6 +136,7 @@
A(UserUnit) \ A(UserUnit) \
A(W) \ A(W) \
A(WhitePoint) \ A(WhitePoint) \
A(Width) \
A(Widths) \ A(Widths) \
A(XObject) \ A(XObject) \
A(XYZ) \ A(XYZ) \

View file

@ -7,6 +7,7 @@
#include <AK/Utf8View.h> #include <AK/Utf8View.h>
#include <LibPDF/CommonNames.h> #include <LibPDF/CommonNames.h>
#include <LibPDF/Fonts/PDFFont.h> #include <LibPDF/Fonts/PDFFont.h>
#include <LibPDF/Interpolation.h>
#include <LibPDF/Renderer.h> #include <LibPDF/Renderer.h>
#define RENDERER_HANDLER(name) \ #define RENDERER_HANDLER(name) \
@ -623,9 +624,14 @@ RENDERER_HANDLER(paint_xobject)
auto xobjects_dict = MUST(resources->get_dict(m_document, CommonNames::XObject)); auto xobjects_dict = MUST(resources->get_dict(m_document, CommonNames::XObject));
auto xobject = MUST(xobjects_dict->get_stream(m_document, xobject_name)); auto xobject = MUST(xobjects_dict->get_stream(m_document, xobject_name));
Optional<NonnullRefPtr<DictObject>> 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(); auto subtype = MUST(xobject->dict()->get_name(m_document, CommonNames::Subtype))->name();
if (subtype == CommonNames::Image) { if (subtype == CommonNames::Image) {
dbgln("Skipping image"); TRY(show_image(xobject));
return {}; return {};
} }
@ -637,10 +643,6 @@ RENDERER_HANDLER(paint_xobject)
matrix = Vector { Value { 1 }, Value { 0 }, Value { 0 }, Value { 1 }, Value { 0 }, Value { 0 } }; matrix = Vector { Value { 1 }, Value { 0 }, Value { 0 }, Value { 1 }, Value { 0 }, Value { 0 } };
} }
MUST(handle_concatenate_matrix(matrix)); MUST(handle_concatenate_matrix(matrix));
Optional<NonnullRefPtr<DictObject>> 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())); auto operators = TRY(Parser::parse_operators(m_document, xobject->bytes()));
for (auto& op : operators) for (auto& op : operators)
TRY(handle_operator(op, xobject_resources)); 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); m_text_matrix.translate(delta_x / text_rendering_matrix.x_scale(), 0.0f);
} }
PDFErrorOr<NonnullRefPtr<Gfx::Bitmap>> Renderer::load_image(NonnullRefPtr<StreamObject> 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<int>();
auto height = image_dict->get_value(CommonNames::Height).get<int>();
auto is_filter = [&](FlyString const& name) {
if (filter_object->is<NameObject>())
return filter_object->cast<NameObject>()->name() == name;
auto filters = filter_object->cast<ArrayObject>();
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<bool>();
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<int>();
if (bits_per_component != 8) {
return Error(Error::Type::RenderingUnsupported, "Image's bit per component != 8");
}
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);
}
if (is_filter(CommonNames::DCTDecode)) {
// TODO: stream objects could store Variant<bytes/Bitmap> 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<Value> 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<void> Renderer::show_image(NonnullRefPtr<StreamObject> image)
{
auto image_dict = image->dict();
auto width = image_dict->get_value(CommonNames::Width).get<int>();
auto height = image_dict->get_value(CommonNames::Height).get<int>();
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<NonnullRefPtr<ColorSpace>> Renderer::get_color_space_from_resources(Value const& value, NonnullRefPtr<DictObject> resources) PDFErrorOr<NonnullRefPtr<ColorSpace>> Renderer::get_color_space_from_resources(Value const& value, NonnullRefPtr<DictObject> resources)
{ {
auto color_space_name = value.get<NonnullRefPtr<Object>>()->cast<NameObject>()->name(); auto color_space_name = value.get<NonnullRefPtr<Object>>()->cast<NameObject>()->name();

View file

@ -86,6 +86,12 @@ struct GraphicsState {
struct RenderingPreferences { struct RenderingPreferences {
bool show_clipping_paths { false }; 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 { class Renderer {
@ -109,6 +115,9 @@ private:
void end_path_paint(); void end_path_paint();
PDFErrorOr<void> set_graphics_state_from_dict(NonnullRefPtr<DictObject>); PDFErrorOr<void> set_graphics_state_from_dict(NonnullRefPtr<DictObject>);
void show_text(DeprecatedString const&); 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_resources(Value const&, NonnullRefPtr<DictObject>);
PDFErrorOr<NonnullRefPtr<ColorSpace>> get_color_space_from_document(NonnullRefPtr<Object>); PDFErrorOr<NonnullRefPtr<ColorSpace>> get_color_space_from_document(NonnullRefPtr<Object>);
@ -127,6 +136,7 @@ private:
ALWAYS_INLINE Gfx::Rect<T> map(Gfx::Rect<T>) const; ALWAYS_INLINE Gfx::Rect<T> map(Gfx::Rect<T>) const;
Gfx::AffineTransform const& calculate_text_rendering_matrix(); Gfx::AffineTransform const& calculate_text_rendering_matrix();
Gfx::AffineTransform calculate_image_space_transformation(int width, int height);
RefPtr<Document> m_document; RefPtr<Document> m_document;
RefPtr<Gfx::Bitmap> m_bitmap; RefPtr<Gfx::Bitmap> m_bitmap;