Add experimental FPDFImageObj_GetRenderedBitmap() API.

Unlike FPDFImageObj_GetBitmap(), FPDFImageObj_GetRenderedBitmap() takes
more of the image object into account when rendering, just like when
rendering image objects normally. Hook up the new API to pdfium_test
--save-rendered-images, to make it easy to try out the new API and
extract images from PDFs.

While adding embedder tests, rename some existing test cases to have
"GetBitmap" in their names, so it is clear they are for
FPDFImageObj_GetBitmap(). Similarly, put "GetRenderedBitmap" in the
names of tests for FPDFImageObj_GetRenderedBitmap().

Bug: pdfium:1554
Change-Id: I145df5d758041c25f4cf52d4b69b202f0abf7c95
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/71511
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/fpdfsdk/fpdf_edit_embeddertest.cpp b/fpdfsdk/fpdf_edit_embeddertest.cpp
index c2e7853..86a74d3 100644
--- a/fpdfsdk/fpdf_edit_embeddertest.cpp
+++ b/fpdfsdk/fpdf_edit_embeddertest.cpp
@@ -3249,7 +3249,7 @@
   UnloadPage(page);
 }
 
-TEST_F(FPDFEditEmbedderTest, ExtractImageBitmap) {
+TEST_F(FPDFEditEmbedderTest, GetBitmap) {
   ASSERT_TRUE(OpenDocument("embedded_images.pdf"));
   FPDF_PAGE page = LoadPage(0);
   ASSERT_TRUE(page);
@@ -3310,9 +3310,7 @@
   UnloadPage(page);
 }
 
-// TODO(crbug.com/pdfium/1554): Fix FPDFImageObj_GetBitmap() to take the matrix
-// into account, or provide a new API to do that.
-TEST_F(FPDFEditEmbedderTest, ExtractImageBitmapIgnoresSetMatrix) {
+TEST_F(FPDFEditEmbedderTest, GetBitmapIgnoresSetMatrix) {
   ASSERT_TRUE(OpenDocument("embedded_images.pdf"));
   FPDF_PAGE page = LoadPage(0);
   ASSERT_TRUE(page);
@@ -3367,7 +3365,7 @@
   UnloadPage(page);
 }
 
-TEST_F(FPDFEditEmbedderTest, ExtractJBigImageBitmap) {
+TEST_F(FPDFEditEmbedderTest, GetBitmapForJBigImage) {
   ASSERT_TRUE(OpenDocument("bug_631912.pdf"));
   FPDF_PAGE page = LoadPage(0);
   ASSERT_TRUE(page);
@@ -3385,9 +3383,7 @@
   UnloadPage(page);
 }
 
-// TODO(crbug.com/pdfium/1554): Fix FPDFImageObj_GetBitmap() to take /SMask into
-// account, or provide a new API to do that.
-TEST_F(FPDFEditEmbedderTest, ExtractSMaskBitmap) {
+TEST_F(FPDFEditEmbedderTest, GetBitmapIgnoresSMask) {
   ASSERT_TRUE(OpenDocument("matte.pdf"));
   FPDF_PAGE page = LoadPage(0);
   ASSERT_TRUE(page);
@@ -3407,6 +3403,110 @@
   UnloadPage(page);
 }
 
+TEST_F(FPDFEditEmbedderTest, GetRenderedBitmapHandlesSetMatrix) {
+  ASSERT_TRUE(OpenDocument("embedded_images.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+  ASSERT_EQ(39, FPDFPage_CountObjects(page));
+
+  FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, 33);
+  ASSERT_EQ(FPDF_PAGEOBJ_IMAGE, FPDFPageObj_GetType(obj));
+
+  {
+    // Render |obj| as is.
+    ScopedFPDFBitmap bitmap(
+        FPDFImageObj_GetRenderedBitmap(document(), page, obj));
+    EXPECT_EQ(FPDFBitmap_BGRA, FPDFBitmap_GetFormat(bitmap.get()));
+    CompareBitmap(bitmap.get(), 53, 43, "90fa16c2fb2bf8ad3654c2258417664c");
+  }
+
+  // Check the matrix for |obj|.
+  double a;
+  double b;
+  double c;
+  double d;
+  double e;
+  double f;
+  EXPECT_TRUE(FPDFImageObj_GetMatrix(obj, &a, &b, &c, &d, &e, &f));
+  EXPECT_DOUBLE_EQ(53.0, a);
+  EXPECT_DOUBLE_EQ(0.0, b);
+  EXPECT_DOUBLE_EQ(0.0, c);
+  EXPECT_DOUBLE_EQ(43.0, d);
+  EXPECT_DOUBLE_EQ(72.0, e);
+  EXPECT_DOUBLE_EQ(646.510009765625, f);
+
+  // Modify the matrix for |obj|.
+  a = 120.0;
+  EXPECT_TRUE(FPDFImageObj_SetMatrix(obj, a, b, c, d, e, f));
+
+  // Make sure the matrix modification took place.
+  EXPECT_TRUE(FPDFImageObj_GetMatrix(obj, &a, &b, &c, &d, &e, &f));
+  EXPECT_DOUBLE_EQ(120.0, a);
+  EXPECT_DOUBLE_EQ(0.0, b);
+  EXPECT_DOUBLE_EQ(0.0, c);
+  EXPECT_DOUBLE_EQ(43.0, d);
+  EXPECT_DOUBLE_EQ(72.0, e);
+  EXPECT_DOUBLE_EQ(646.510009765625, f);
+
+  {
+    // Render |obj| again. Note that the FPDFImageObj_SetMatrix() call has an
+    // effect.
+    ScopedFPDFBitmap bitmap(
+        FPDFImageObj_GetRenderedBitmap(document(), page, obj));
+    EXPECT_EQ(FPDFBitmap_BGRA, FPDFBitmap_GetFormat(bitmap.get()));
+    CompareBitmap(bitmap.get(), 120, 43, "57ed8e15daa535490ff0c8b7640a36b4");
+  }
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedBitmapHandlesSMask) {
+  ASSERT_TRUE(OpenDocument("matte.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  constexpr int kExpectedObjects = 4;
+  ASSERT_EQ(kExpectedObjects, FPDFPage_CountObjects(page));
+
+  for (int i = 0; i < kExpectedObjects; ++i) {
+    FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, i);
+    ASSERT_EQ(FPDF_PAGEOBJ_IMAGE, FPDFPageObj_GetType(obj));
+    ScopedFPDFBitmap bitmap(
+        FPDFImageObj_GetRenderedBitmap(document(), page, obj));
+    ASSERT_TRUE(bitmap);
+    EXPECT_EQ(FPDFBitmap_BGRA, FPDFBitmap_GetFormat(bitmap.get()));
+    if (i == 0)
+      CompareBitmap(bitmap.get(), 40, 60, "5a3ae4a660ce919e29c42ec2258142f1");
+    else
+      CompareBitmap(bitmap.get(), 40, 60, "67504e83f5d78214ea00efc19082c5c1");
+  }
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedBitmapBadParams) {
+  ASSERT_TRUE(OpenDocument("embedded_images.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, 33);
+  ASSERT_EQ(FPDF_PAGEOBJ_IMAGE, FPDFPageObj_GetType(obj));
+
+  // Test various null parameters.
+  EXPECT_FALSE(FPDFImageObj_GetRenderedBitmap(nullptr, nullptr, nullptr));
+  EXPECT_FALSE(FPDFImageObj_GetRenderedBitmap(document(), nullptr, nullptr));
+  EXPECT_FALSE(FPDFImageObj_GetRenderedBitmap(nullptr, page, nullptr));
+  EXPECT_FALSE(FPDFImageObj_GetRenderedBitmap(nullptr, nullptr, obj));
+  EXPECT_FALSE(FPDFImageObj_GetRenderedBitmap(document(), page, nullptr));
+  EXPECT_FALSE(FPDFImageObj_GetRenderedBitmap(nullptr, page, obj));
+
+  // Test mismatch between document and page parameters.
+  ScopedFPDFDocument new_document(FPDF_CreateNewDocument());
+  EXPECT_FALSE(FPDFImageObj_GetRenderedBitmap(new_document.get(), page, obj));
+
+  UnloadPage(page);
+}
+
 TEST_F(FPDFEditEmbedderTest, GetImageData) {
   EXPECT_TRUE(OpenDocument("embedded_images.pdf"));
   FPDF_PAGE page = LoadPage(0);
diff --git a/fpdfsdk/fpdf_editimg.cpp b/fpdfsdk/fpdf_editimg.cpp
index 7637bf9..6d10ea6 100644
--- a/fpdfsdk/fpdf_editimg.cpp
+++ b/fpdfsdk/fpdf_editimg.cpp
@@ -18,6 +18,10 @@
 #include "core/fpdfapi/parser/cpdf_dictionary.h"
 #include "core/fpdfapi/parser/cpdf_name.h"
 #include "core/fpdfapi/parser/cpdf_stream_acc.h"
+#include "core/fpdfapi/render/cpdf_imagerenderer.h"
+#include "core/fpdfapi/render/cpdf_rendercontext.h"
+#include "core/fpdfapi/render/cpdf_renderstatus.h"
+#include "core/fxge/cfx_defaultrenderdevice.h"
 #include "fpdfsdk/cpdfsdk_customaccess.h"
 #include "fpdfsdk/cpdfsdk_helpers.h"
 
@@ -214,6 +218,59 @@
   return FPDFBitmapFromCFXDIBitmap(pBitmap.Leak());
 }
 
+FPDF_EXPORT FPDF_BITMAP FPDF_CALLCONV
+FPDFImageObj_GetRenderedBitmap(FPDF_DOCUMENT document,
+                               FPDF_PAGE page,
+                               FPDF_PAGEOBJECT image_object) {
+  CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document);
+  if (!doc)
+    return nullptr;
+
+  CPDF_Page* optional_page = CPDFPageFromFPDFPage(page);
+  if (optional_page && optional_page->GetDocument() != doc)
+    return nullptr;
+
+  CPDF_ImageObject* image = CPDFImageObjectFromFPDFPageObject(image_object);
+  if (!image)
+    return nullptr;
+
+  // Create |result_bitmap|.
+  const CFX_Matrix& image_matrix = image->matrix();
+  int output_width = image_matrix.a;
+  int output_height = image_matrix.d;
+  auto result_bitmap = pdfium::MakeRetain<CFX_DIBitmap>();
+  if (!result_bitmap->Create(output_width, output_height, FXDIB_Argb))
+    return nullptr;
+
+  // Set up all the rendering code.
+  CPDF_Dictionary* page_resources =
+      optional_page ? optional_page->m_pPageResources.Get() : nullptr;
+  CPDF_RenderContext context(doc, page_resources, /*pPageCache=*/nullptr);
+  CFX_DefaultRenderDevice device;
+  device.Attach(result_bitmap, /*bRgbByteOrder=*/false,
+                /*pBackdropBitmap=*/nullptr, /*bGroupKnockout=*/false);
+  CPDF_RenderStatus status(&context, &device);
+  CPDF_ImageRenderer renderer;
+
+  // Need to first flip the image, as expected by |renderer|.
+  CFX_Matrix render_matrix(1, 0, 0, -1, 0, output_height);
+
+  // Then take |image_matrix|'s offset into account.
+  render_matrix.Translate(-image_matrix.e, image_matrix.f);
+
+  // Do the actual rendering.
+  bool should_continue = renderer.Start(&status, image, render_matrix,
+                                        /*bStdCS=*/false, BlendMode::kNormal);
+  while (should_continue)
+    should_continue = renderer.Continue(/*pPause=*/nullptr);
+
+  if (!renderer.GetResult())
+    return nullptr;
+
+  // Caller takes ownership.
+  return FPDFBitmapFromCFXDIBitmap(result_bitmap.Leak());
+}
+
 FPDF_EXPORT unsigned long FPDF_CALLCONV
 FPDFImageObj_GetImageDataDecoded(FPDF_PAGEOBJECT image_object,
                                  void* buffer,
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index 3c29a89..825691d 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -158,6 +158,7 @@
     CHK(FPDFImageObj_GetImageFilterCount);
     CHK(FPDFImageObj_GetImageMetadata);
     CHK(FPDFImageObj_GetMatrix);
+    CHK(FPDFImageObj_GetRenderedBitmap);
     CHK(FPDFImageObj_LoadJpegFile);
     CHK(FPDFImageObj_LoadJpegFileInline);
     CHK(FPDFImageObj_SetBitmap);
diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h
index f842869..d415460 100644
--- a/public/fpdf_edit.h
+++ b/public/fpdf_edit.h
@@ -623,9 +623,11 @@
                        FPDF_PAGEOBJECT image_object,
                        FPDF_BITMAP bitmap);
 
-// Get a bitmap rasterisation of |image_object|. The returned bitmap will be
-// owned by the caller, and FPDFBitmap_Destroy() must be called on the returned
-// bitmap when it is no longer needed.
+// Get a bitmap rasterization of |image_object|. FPDFImageObj_GetBitmap() only
+// operates on |image_object| and does not take the associated image mask into
+// account. It also ignores the matrix for |image_object|.
+// The returned bitmap will be owned by the caller, and FPDFBitmap_Destroy()
+// must be called on the returned bitmap when it is no longer needed.
 //
 //   image_object - handle to an image object.
 //
@@ -633,6 +635,24 @@
 FPDF_EXPORT FPDF_BITMAP FPDF_CALLCONV
 FPDFImageObj_GetBitmap(FPDF_PAGEOBJECT image_object);
 
+// Experimental API.
+// Get a bitmap rasterization of |image_object| that takes the image mask and
+// image matrix into account. To render correctly, the caller must provide the
+// |document| associated with |image_object|. If there is a |page| associated
+// with |image_object| the caller should provide that as well.
+// The returned bitmap will be owned by the caller, and FPDFBitmap_Destroy()
+// must be called on the returned bitmap when it is no longer needed.
+//
+//   document     - handle to a document associated with |image_object|.
+//   page         - handle to an optional page associated with |image_object|.
+//   image_object - handle to an image object.
+//
+// Returns the bitmap.
+FPDF_EXPORT FPDF_BITMAP FPDF_CALLCONV
+FPDFImageObj_GetRenderedBitmap(FPDF_DOCUMENT document,
+                               FPDF_PAGE page,
+                               FPDF_PAGEOBJECT image_object);
+
 // Get the decoded image data of |image_object|. The decoded data is the
 // uncompressed image data, i.e. the raw image data after having all filters
 // applied. |buffer| is only modified if |buflen| is longer than the length of
diff --git a/samples/pdfium_test.cc b/samples/pdfium_test.cc
index 6c88ed9..711ea06 100644
--- a/samples/pdfium_test.cc
+++ b/samples/pdfium_test.cc
@@ -119,6 +119,7 @@
   bool reverse_byte_order = false;
   bool save_attachments = false;
   bool save_images = false;
+  bool save_rendered_images = false;
   bool save_thumbnails = false;
   bool save_thumbnails_decoded = false;
   bool save_thumbnails_raw = false;
@@ -453,6 +454,8 @@
       options->save_attachments = true;
     } else if (cur_arg == "--save-images") {
       options->save_images = true;
+    } else if (cur_arg == "--save-rendered-images") {
+      options->save_rendered_images = true;
     } else if (cur_arg == "--save-thumbs") {
       options->save_thumbnails = true;
     } else if (cur_arg == "--save-thumbs-dec") {
@@ -720,6 +723,8 @@
     SendPageEvents(form, page, events);
   if (options.save_images)
     WriteImages(page, name.c_str(), page_index);
+  if (options.save_rendered_images)
+    WriteRenderedImages(doc, page, name.c_str(), page_index);
   if (options.save_thumbnails)
     WriteThumbnail(page, name.c_str(), page_index);
   if (options.save_thumbnails_decoded)
diff --git a/samples/pdfium_test_write_helper.cc b/samples/pdfium_test_write_helper.cc
index d44a298..f1f53b4 100644
--- a/samples/pdfium_test_write_helper.cc
+++ b/samples/pdfium_test_write_helper.cc
@@ -693,6 +693,44 @@
   }
 }
 
+void WriteRenderedImages(FPDF_DOCUMENT doc,
+                         FPDF_PAGE page,
+                         const char* pdf_name,
+                         int page_num) {
+  for (int i = 0; i < FPDFPage_CountObjects(page); ++i) {
+    FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, i);
+    if (FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_IMAGE)
+      continue;
+
+    ScopedFPDFBitmap bitmap(FPDFImageObj_GetRenderedBitmap(doc, page, obj));
+    if (!bitmap) {
+      fprintf(stderr, "Image object #%d on page #%d has an empty bitmap.\n",
+              i + 1, page_num + 1);
+      continue;
+    }
+
+    char filename[256];
+    int chars_formatted = snprintf(filename, sizeof(filename), "%s.%d.%d.png",
+                                   pdf_name, page_num, i);
+    if (chars_formatted < 0 ||
+        static_cast<size_t>(chars_formatted) >= sizeof(filename)) {
+      fprintf(stderr, "Filename %s for saving image is too long.\n", filename);
+      continue;
+    }
+
+    std::vector<uint8_t> png_encoding = EncodeBitmapToPng(std::move(bitmap));
+    if (png_encoding.empty()) {
+      fprintf(stderr,
+              "Failed to convert image object #%d, on page #%d to png.\n",
+              i + 1, page_num + 1);
+      continue;
+    }
+
+    WriteBufferToFile(&png_encoding.front(), png_encoding.size(), filename,
+                      "image");
+  }
+}
+
 void WriteDecodedThumbnailStream(FPDF_PAGE page,
                                  const char* pdf_name,
                                  int page_num) {
diff --git a/samples/pdfium_test_write_helper.h b/samples/pdfium_test_write_helper.h
index b53d555..0de3418 100644
--- a/samples/pdfium_test_write_helper.h
+++ b/samples/pdfium_test_write_helper.h
@@ -48,6 +48,10 @@
 
 void WriteAttachments(FPDF_DOCUMENT doc, const std::string& name);
 void WriteImages(FPDF_PAGE page, const char* pdf_name, int page_num);
+void WriteRenderedImages(FPDF_DOCUMENT doc,
+                         FPDF_PAGE page,
+                         const char* pdf_name,
+                         int page_num);
 void WriteDecodedThumbnailStream(FPDF_PAGE page,
                                  const char* pdf_name,
                                  int page_num);