0

[PDF] Avoid rendering into a white bitmap for pages with transparencies

For certain PDFs, pdfium_test has no problem rendering them, while the
PDF Viewer does. Looking at the differences, when a page has
transparencies, pdfium_test fills the bitmap it renders into with
pixel value 0, while the PDF Viewer fills them with 0xFFFFFFFF.
Simply switching the PDF Viewer to match pdfium_test's fill color
results in weird smeared rendering, as this has been attempted several
times without success.

The key to fixing this is ask the PaintManager to draw the page's white
background first into the actual rendering surface, before drawing the
bitmap with the contents. Then everything renders more correctly, so
update PaintManagerTest to match the new behavior. The remaining issue
is premultiplied alpha handling.

It turns out the bitmap PDFium draws into had always been premultiplied
in the PDF Viewer, which is not right, considering PDFium assumes the
bitmap has straight alpha. However, this did not matter, because all the
pixels had 0xFF as the alpha value, so the bitmap was effectively
opaque. Now that the bitmap actually has alpha, the wrong bitmap type
causes color differences due to straight vs. premultiplied alpha
confusion. Fix this by marking the bitmap as un-premultiplied.

Considering some of this PDF Viewer code has been behaving the same way
for 10+ years, and the last change caused this bug, play it safe this
time with a kill switch. Preserve the old behavior in a fallback path,
while enabling the code path with the fix by default.

At the same time, use pdfium_test's drawing logic in
PDFiumPage::GenerateThumbnail() to fix the same issue with thumbnails.
Update tests with the new expectations.

Bug: 40216952
Change-Id: Ieadf2e76b9253351b74c5b7ccf21e74d3c4e3c05
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5940452
Reviewed-by: Andy Phan <andyphan@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/heads/main@{#1370910}
This commit is contained in:
Lei Zhang
2024-10-19 00:19:02 +00:00
committed by Chromium LUCI CQ
parent d87fed1412
commit 0942c4d6e2
11 changed files with 89 additions and 25 deletions

@ -14,12 +14,14 @@
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "pdf/paint_ready_rect.h"
#include "pdf/pdf_features.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkRect.h"
@ -285,7 +287,18 @@ void PaintManager::DoPaint() {
}
for (const auto& ready_rect : ready_now) {
SkRect skia_rect = gfx::RectToSkRect(ready_rect.rect());
const SkRect skia_rect = gfx::RectToSkRect(ready_rect.rect());
if (base::FeatureList::IsEnabled(
features::kPdfPaintManagerDrawsBackground)) {
// Paint the page's white background, and then paint the page's contents.
// If `ready_rect.image()` has transparencies, this is necessary to paint
// over the stale data in `skia_rect` in `surface_`.
SkPaint paint;
paint.setColor(SK_ColorWHITE);
surface_->getCanvas()->drawRect(skia_rect, paint);
}
surface_->getCanvas()->drawImageRect(
ready_rect.image(), skia_rect, skia_rect, SkSamplingOptions(), nullptr,
SkCanvas::kStrict_SrcRectConstraint);

@ -88,6 +88,28 @@ class PaintManagerTest : public testing::Test {
return saved_snapshot;
}
// Generates the expectations for use with TestPaintImage().
//
// - `plugin_size`: The expected image size.
// - `paint_rect`: Expected to be painted white.
// - `overlapped_rect`: Expected to be painted red.
// May paint over `paint_rect`.
SkBitmap GeneratePaintImageExpectation(const gfx::Size& plugin_size,
const gfx::Rect& paint_rect,
const gfx::Rect& overlapped_rect) {
sk_sp<SkSurface> surface =
CreateSkiaSurfaceForTesting(plugin_size, SK_ColorMAGENTA);
SkCanvas* canvas = surface->getCanvas();
canvas->clipIRect(gfx::RectToSkIRect(paint_rect));
canvas->clear(SK_ColorWHITE);
canvas->clipIRect(gfx::RectToSkIRect(overlapped_rect));
canvas->clear(SK_ColorRED);
SkBitmap bitmap;
EXPECT_TRUE(surface->makeImageSnapshot()->asLegacyBitmap(&bitmap));
return bitmap;
}
void TestPaintImage(const gfx::Size& plugin_size,
const gfx::Size& source_size,
const gfx::Rect& paint_rect,
@ -105,7 +127,7 @@ class PaintManagerTest : public testing::Test {
/*fake_pending=*/{});
ASSERT_TRUE(snapshot);
// Check if snapshot has `overlapped_rect` painted red.
// Check if `snapshot` matches `expected_bitmap`.
snapshot = snapshot->makeSubset(
static_cast<GrDirectContext*>(nullptr),
SkIRect::MakeWH(plugin_size.width(), plugin_size.height()));
@ -114,16 +136,8 @@ class PaintManagerTest : public testing::Test {
SkBitmap snapshot_bitmap;
ASSERT_TRUE(snapshot->asLegacyBitmap(&snapshot_bitmap));
sk_sp<SkSurface> expected_surface =
CreateSkiaSurfaceForTesting(plugin_size, SK_ColorMAGENTA);
expected_surface->getCanvas()->clipIRect(
gfx::RectToSkIRect(overlapped_rect));
expected_surface->getCanvas()->clear(SK_ColorRED);
SkBitmap expected_bitmap;
ASSERT_TRUE(expected_surface->makeImageSnapshot()->asLegacyBitmap(
&expected_bitmap));
SkBitmap expected_bitmap =
GeneratePaintImageExpectation(plugin_size, paint_rect, overlapped_rect);
EXPECT_TRUE(cc::MatchesBitmap(snapshot_bitmap, expected_bitmap,
cc::ExactPixelComparator()));
}

@ -27,6 +27,12 @@ BASE_FEATURE(kPdfIncrementalLoading,
BASE_FEATURE(kPdfOopif, "PdfOopif", base::FEATURE_DISABLED_BY_DEFAULT);
// Kill switch in case this goes horribly wrong.
// TODO(crbug.com/40216952): Remove after this lands safely in a Stable release.
BASE_FEATURE(kPdfPaintManagerDrawsBackground,
"PdfPaintManagerDrawsBackground",
base::FEATURE_ENABLED_BY_DEFAULT);
// "Partial loading" refers to loading only specific parts of the PDF.
// TODO(crbug.com/40123601): Remove this once partial loading is fixed.
BASE_FEATURE(kPdfPartialLoading,

@ -19,6 +19,7 @@ BASE_DECLARE_FEATURE(kAccessiblePDFForm);
BASE_DECLARE_FEATURE(kPdfCr23);
BASE_DECLARE_FEATURE(kPdfIncrementalLoading);
BASE_DECLARE_FEATURE(kPdfOopif);
BASE_DECLARE_FEATURE(kPdfPaintManagerDrawsBackground);
BASE_DECLARE_FEATURE(kPdfPartialLoading);
BASE_DECLARE_FEATURE(kPdfPortfolio);
BASE_DECLARE_FEATURE(kPdfSearchify);

@ -2153,8 +2153,15 @@ void PdfViewWebPlugin::OnViewportChanged(
const gfx::Size new_image_size =
PaintManager::GetNewContextSize(old_image_size, plugin_rect_.size());
if (new_image_size != old_image_size) {
image_data_.allocPixels(
SkImageInfo::MakeN32Premul(gfx::SizeToSkISize(new_image_size)));
SkAlphaType alpha_type;
if (base::FeatureList::IsEnabled(
features::kPdfPaintManagerDrawsBackground)) {
alpha_type = kUnpremul_SkAlphaType;
} else {
alpha_type = kPremul_SkAlphaType;
}
image_data_.allocPixels(SkImageInfo::MakeN32(
new_image_size.width(), new_image_size.height(), alpha_type));
first_paint_ = true;
}

@ -3147,12 +3147,24 @@ bool PDFiumEngine::ContinuePaint(size_t progressive_index,
const gfx::Rect& dirty = paint.rect();
const gfx::Rect pdfium_rect = GetPDFiumRect(paint.page_index(), dirty);
bool has_alpha = !!FPDFPage_HasTransparency(page);
const bool has_alpha = !!FPDFPage_HasTransparency(page);
uint32_t fill_color;
if (base::FeatureList::IsEnabled(features::kPdfPaintManagerDrawsBackground)) {
// When `has_alpha` is true, use a transparent bitmap to render correctly.
// If the bitmap was painted white, then certain blend operations would
// blend into the white color and draw incorrectly. Instead, PaintManager
// will draw the white page under this bitmap.
fill_color = has_alpha ? 0x00000000 : 0xFFFFFFFF;
} else {
// In the old code, which is here as a fallback, `fill_color` is always
// white, as PaintManager does not draw the page's background.
fill_color = 0xFFFFFFFF;
}
ScopedFPDFBitmap new_bitmap = CreateBitmap(dirty, has_alpha, image_data);
FPDF_BITMAP new_bitmap_ptr = new_bitmap.get();
paint.SetBitmapAndImageData(std::move(new_bitmap), image_data);
FPDFBitmap_FillRect(new_bitmap_ptr, pdfium_rect.x(), pdfium_rect.y(),
pdfium_rect.width(), pdfium_rect.height(), 0xFFFFFFFF);
pdfium_rect.width(), pdfium_rect.height(), fill_color);
return FPDF_RenderPageBitmap_Start(
new_bitmap_ptr, page, pdfium_rect.x(), pdfium_rect.y(),
pdfium_rect.width(), pdfium_rect.height(),

@ -6,6 +6,7 @@
#include <math.h>
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <memory>
@ -13,6 +14,7 @@
#include "base/check_op.h"
#include "base/containers/to_vector.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
@ -24,6 +26,7 @@
#include "pdf/accessibility_helper.h"
#include "pdf/accessibility_structs.h"
#include "pdf/buildflags.h"
#include "pdf/pdf_features.h"
#include "pdf/pdfium/pdfium_api_string_buffer_adapter.h"
#include "pdf/pdfium/pdfium_engine.h"
#include "pdf/pdfium/pdfium_ocr.h"
@ -1766,29 +1769,38 @@ void PDFiumPage::RequestThumbnail(float device_pixel_ratio,
Thumbnail PDFiumPage::GenerateThumbnail(float device_pixel_ratio) {
DCHECK(available());
FPDF_PAGE page = GetPage();
const bool has_alpha = !!FPDFPage_HasTransparency(page);
const int format = has_alpha ? FPDFBitmap_BGRA : FPDFBitmap_BGRx;
uint32_t fill_color;
if (base::FeatureList::IsEnabled(features::kPdfPaintManagerDrawsBackground)) {
fill_color = has_alpha ? 0x00000000 : 0xFFFFFFFF;
} else {
fill_color = 0xFFFFFFFF;
}
Thumbnail thumbnail = GetThumbnail(device_pixel_ratio);
const gfx::Size& image_size = thumbnail.image_size();
ScopedFPDFBitmap fpdf_bitmap(FPDFBitmap_CreateEx(
image_size.width(), image_size.height(), FPDFBitmap_BGRA,
thumbnail.GetImageData().data(), thumbnail.stride()));
// Create and initialize the bitmap.
ScopedFPDFBitmap fpdf_bitmap(
FPDFBitmap_CreateEx(image_size.width(), image_size.height(), format,
thumbnail.GetImageData().data(), thumbnail.stride()));
// Clear the bitmap.
FPDFBitmap_FillRect(fpdf_bitmap.get(), /*left=*/0, /*top=*/0,
image_size.width(), image_size.height(),
/*color=*/0xFFFFFFFF);
image_size.width(), image_size.height(), fill_color);
// The combination of the `FPDF_REVERSE_BYTE_ORDER` rendering flag and the
// `FPDFBitmap_BGRA` format when initializing `fpdf_bitmap` results in an RGBA
// rendering, which is the format required by HTML <canvas>.
constexpr int kRenderingFlags = FPDF_ANNOT | FPDF_REVERSE_BYTE_ORDER;
FPDF_RenderPageBitmap(fpdf_bitmap.get(), GetPage(), /*start_x=*/0,
FPDF_RenderPageBitmap(fpdf_bitmap.get(), page, /*start_x=*/0,
/*start_y=*/0, image_size.width(), image_size.height(),
ToPDFiumRotation(PageOrientation::kOriginal),
kRenderingFlags);
// Draw the forms.
FPDF_FFLDraw(engine_->form(), fpdf_bitmap.get(), GetPage(), /*start_x=*/0,
FPDF_FFLDraw(engine_->form(), fpdf_bitmap.get(), page, /*start_x=*/0,
/*start_y=*/0, image_size.width(), image_size.height(),
ToPDFiumRotation(PageOrientation::kOriginal), kRenderingFlags);

@ -1179,7 +1179,6 @@ TEST_P(PDFiumPageThumbnailTest, GenerateThumbnailForAnnotation) {
"signature_widget");
}
// TODO(crbug.com/40216952): The thumbnail should not be blank.
TEST_P(PDFiumPageThumbnailTest, GenerateThumbnailWithTransparency) {
TestClient client;
std::unique_ptr<PDFiumEngine> engine =

Binary file not shown.

Before

(image error) Size: 144 B

After

(image error) Size: 201 B

Binary file not shown.

Before

(image error) Size: 97 B

After

(image error) Size: 149 B

Binary file not shown.

Before

(image error) Size: 97 B

After

(image error) Size: 153 B