Repository: SerenityOS/serenity
File: Userland/Libraries/LibPDF/Renderer.cpp (C++)

Code before:
    upsampled_storage.ensure_capacity(content.size() * 8 / bits_per_component);
    u8 const mask = (1 << bits_per_component) - 1;

    int x = 0;
    for (auto byte : content) {
        for (int i = 0; i < 8; i += bits_per_component) {
            auto value = (byte >> (8 - bits_per_component - i)) & mask;
            if (mode == UpsampleMode::UpsampleTo8Bit)
                upsampled_storage.append(value * (255 / mask));
            else
                upsampled_storage.append(value);
            ++x;

            // "Byte boundaries are ignored, except that each row of sample data must begin on a byte boundary."
            if (x == samples_per_line) {
                x = 0;
                break;
            }
        }
    }
    return upsampled_storage;
}

PDFErrorOr<Renderer::LoadedImage> Renderer::load_image(NonnullRefPtr<StreamObject> image)
{
    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 is_filter = [&](DeprecatedFlyString const& name) -> PDFErrorOr<bool> {
        if (!image_dict->contains(CommonNames::Filter))
            return false;
        auto filter_object = TRY(image_dict->get_object(m_document, CommonNames::Filter));
        if (filter_object->is<NameObject>())
            return filter_object->cast<NameObject>()->name() == name;
        auto filters = filter_object->cast<ArrayObject>();
        if (filters->elements().is_empty())
            return false;
        auto last_filter_index = filters->elements().size() - 1;
        return MUST(filters->get_name_at(m_document, last_filter_index))->name() == name;
    };
    bool is_jpeg2000 = TRY(is_filter(CommonNames::JPXDecode));

    // "SMaskInData specifies whether soft-mask information packaged with the im-
    //  age samples should be used (see “Soft-Mask Images” on page 553); if it is, the
    //  SMask entry is not needed. If SMaskInData is nonzero, there must be only one
    //  opacity channel in the JPEG2000 data and it must apply to all color channels."
    if (is_jpeg2000 && image_dict->contains(CommonNames::SMaskInData)) {
        auto smask_in_data = TRY(m_document->resolve_to<int>(image_dict->get_value(CommonNames::SMaskInData)));
        if (smask_in_data == 0) {
            // "If present, encoded soft-mask image information should be ignored."

Review comment (suggestion):
Unimplemented transparency group painting is marked as a FIXME; consider tracking this task in an issue or adding a clear TODO with context for the next steps.
```suggestion
    if (transparency_group_attributes.has_value()) {
        // Create a new rendering context for the transparency group.
        auto& attributes = transparency_group_attributes.value();
        ScopedRenderingContext transparency_context { *this, attributes };

        // Render the content of the form XObject within the transparency group context.
        ScopedState scoped_state { *this };
        Vector<Value> matrix;
        if (form->dict()->contains(CommonNames::Matrix)) {
            matrix = TRY(form->dict()->get_array(m_document, CommonNames::Matrix))->elements();
        } else {
            matrix = Vector { Value { 1 }, Value { 0 }, Value { 0 }, Value { 1 }, Value { 0 }, Value { 0 } };
        }
        MUST(handle_concatenate_matrix(matrix));
        auto operators = TRY(Parser::parse_operators(m_document, form->bytes()));
        for (auto& op : operators)
            TRY(handle_operator(op, xobject_resources));

        // Merge the transparency group result back into the parent context.
        transparency_context.merge();
        return {};
    }
```

Code after:
    }

    if (group_dict->contains(CommonNames::I))
        attributes.is_isolated = TRY(m_document->resolve_to<bool>(group_dict->get_value(CommonNames::I)));

    if (group_dict->contains(CommonNames::K))
        attributes.is_knockout = TRY(m_document->resolve_to<bool>(group_dict->get_value(CommonNames::K)));

    return attributes;
}

PDFErrorOr<void> Renderer::paint_form_xobject(NonnullRefPtr<StreamObject> form)
{
    Optional<NonnullRefPtr<DictObject>> xobject_resources {};
    if (form->dict()->contains(CommonNames::Resources)) {
        xobject_resources = TRY(form->dict()->get_dict(m_document, CommonNames::Resources));
    }

    Optional<TransparencyGroupAttributes> transparency_group_attributes;
    if (form->dict()->contains(CommonNames::Group)) {
        auto group = TRY(form->dict()->get_dict(m_document, CommonNames::Group));
        transparency_group_attributes = TRY(read_transparency_group_attributes(group));
    }

    // FIXME: If transparency_group_attributes.has_value(), paint as transparency group.

    ScopedState scoped_state { *this };

    Vector<Value> matrix;
    if (form->dict()->contains(CommonNames::Matrix)) {
        matrix = TRY(form->dict()->get_array(m_document, CommonNames::Matrix))->elements();
    } else {
        matrix = Vector { Value { 1 }, Value { 0 }, Value { 0 }, Value { 1 }, Value { 0 }, Value { 0 } };
    }
    MUST(handle_concatenate_matrix(matrix));
    auto operators = TRY(Parser::parse_operators(m_document, form->bytes()));
    for (auto& op : operators)
        TRY(handle_operator(op, xobject_resources));
    return {};
}

enum UpsampleMode {
    StoreValuesUnchanged,
    UpsampleTo8Bit,
};
static Vector<u8> upsample_to_8_bit(ReadonlyBytes content, int samples_per_line, int bits_per_component, UpsampleMode mode)
{
    VERIFY(bits_per_component == 1 || bits_per_component == 2 || bits_per_component == 4);
    Vector<u8> upsampled_storage;
    upsampled_storage.ensure_capacity(content.size() * 8 / bits_per_component);
    u8 const mask = (1 << bits_per_component) - 1;