1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 06:07:34 +00:00

LibPDF: Add support for array image masks

An array image mask contains a min/max range for each channel,
and if each channel of a given pixel is in that channel's range,
that pixel is masked out (i.e. transparent). (It's similar to
having a single color or palette index be transparent, but it
supports a range of transparent colors if desired.)

What makes this a bit awkward is that the range is relative to the
origin bits per pixel and the inputs to the image's color space.

So an indexed (palettized) image with 4bpp has a 2-element mask
array where both entries are between 0 and 15.

We currently apply masks after converting images to a Gfx::Bitmap,
that is after converting to 8bpp sRGB. And we do this by mapping
everything to 8bpp very early on in load_image().

This leaves us with a bunch of options that are all a bit awkward:

1. Make load_image() store the up- (or for 16bpp inputs, down-)
   sampled-to-8bpp pixel data. And also return if we expanded the
   pixel range while resampling (for color values) or not (for
   palettized images). Then, when applying the image filter,
   resample the array bounds in exactly the same way. This requires
   passing around more stuff.

2. Like 1, but pass in the mask array to load_image() and apply
   the mask right there and then. This means we'd apply mask arrays
   at a different time than other masks.

3. Make the function that computes the mask from the mask array
   work from the original, unprocessed image data. This is the most
   local change, but probably also requires the largest amount of
   code (in return, the color mask for 16bpp images is precise, in
   addition that it separates concerns the most nicely).

This goes with 3 for now.
This commit is contained in:
Nico Weber 2024-03-01 14:43:52 -05:00 committed by Tim Flynn
parent 0d6573be14
commit 9bff8abcc7
2 changed files with 47 additions and 2 deletions

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/BitStream.h>
#include <AK/Utf8View.h>
#include <LibPDF/CommonNames.h>
#include <LibPDF/Fonts/PDFFont.h>
@ -1216,6 +1217,49 @@ PDFErrorOr<Renderer::LoadedImage> Renderer::load_image(NonnullRefPtr<StreamObjec
return LoadedImage { bitmap, is_image_mask };
}
PDFErrorOr<NonnullRefPtr<Gfx::Bitmap>> Renderer::make_mask_bitmap_from_array(NonnullRefPtr<ArrayObject> array, NonnullRefPtr<StreamObject> image)
{
// PDF 1.7 spec, 4.8.5. Masked Images, Color Key Masking
// "For color key masking, the value of the Mask entry is an array of 2 × n integers, [min_1 max_1 ... min_n max_n],
// where n is the number of color components in the images color space. Each integer must be in the range 0 to 2**(BitsPerComponent 1),
// representing color values _before_ decoding with the Decode array.
// An image sample is masked [...] if min_i ≤ c_i ≤ max_i for all 1 ≤ i ≤ n."
// For indexed images, this means the array masks the index, not the color.
auto image_dict = image->dict();
auto width = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::Width)));
auto height = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::Height)));
auto bits_per_component = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::BitsPerComponent)));
VERIFY(bits_per_component == 1 || bits_per_component == 2 || bits_per_component == 4 || bits_per_component == 8 || bits_per_component == 16);
if (array->size() % 2 != 0)
return Error(Error::Type::MalformedPDF, "Mask array must have an even number of elements");
auto n_components = array->size() / 2;
Vector<int, 4> min, max;
for (size_t i = 0; i < n_components; ++i) {
min.append(array->at(i * 2).to_int());
max.append(array->at(i * 2 + 1).to_int());
}
auto mask_bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { width, height }));
auto bit_stream = make<BigEndianInputBitStream>(make<FixedMemoryStream>(image->bytes()));
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
bool is_masked = true;
for (size_t i = 0; i < n_components; ++i) {
u16 sample = TRY(bit_stream->read_bits(bits_per_component));
if (sample < min[i] || sample > max[i]) {
is_masked = false;
TRY(bit_stream->read_bits((n_components - 1 - i) * bits_per_component));
break;
}
}
mask_bitmap->set_pixel(x, y, Color::from_argb(is_masked ? 0x00'00'00'00 : 0xff'ff'ff'ff));
}
bit_stream->align_to_byte_boundary();
}
return mask_bitmap;
}
Gfx::AffineTransform Renderer::calculate_image_space_transformation(Gfx::IntSize size)
{
// Image space maps to a 1x1 unit of user space and starts at the top-left
@ -1299,7 +1343,8 @@ PDFErrorOr<void> Renderer::show_image(NonnullRefPtr<StreamObject> image)
auto mask_bitmap = TRY(load_image(mask_object->cast<StreamObject>()));
image_bitmap.bitmap = TRY(apply_alpha_channel(image_bitmap.bitmap, mask_bitmap.bitmap));
} else if (mask_object->is<ArrayObject>()) {
return Error::rendering_unsupported_error("/Mask array objects not yet implemented");
auto mask_bitmap = TRY(make_mask_bitmap_from_array(mask_object->cast<ArrayObject>(), image));
image_bitmap.bitmap = TRY(apply_alpha_channel(image_bitmap.bitmap, mask_bitmap));
}
}