Add experimental FPDFTextObj_GetRenderedBitmap() API.

This API lets embedders render text objects to a bitmap, similar to
FPDFImageObj_GetRenderedBitmap(). FPDFTextObj_GetRenderedBitmap() has
an additional scale parameter, so embedders can take advantage of
PDFium's internal rendering code and get high quality output.

Bug: pdfium:1834
Change-Id: I3689702ae7c009b00e92a2f22d7704a23c11e62a
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/93872
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/core/fpdfapi/page/cpdf_page.cpp b/core/fpdfapi/page/cpdf_page.cpp
index abacbc4..4f11c91 100644
--- a/core/fpdfapi/page/cpdf_page.cpp
+++ b/core/fpdfapi/page/cpdf_page.cpp
@@ -225,5 +225,6 @@
     : m_pPage(pPage) {}
 
 CPDF_Page::RenderContextClearer::~RenderContextClearer() {
-  m_pPage->ClearRenderContext();
+  if (m_pPage)
+    m_pPage->ClearRenderContext();
 }
diff --git a/fpdfsdk/fpdf_edit_embeddertest.cpp b/fpdfsdk/fpdf_edit_embeddertest.cpp
index f6cdc99..070dadd 100644
--- a/fpdfsdk/fpdf_edit_embeddertest.cpp
+++ b/fpdfsdk/fpdf_edit_embeddertest.cpp
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <limits>
 #include <memory>
 #include <string>
 #include <utility>
@@ -4174,3 +4175,186 @@
 
   UnloadPage(page);
 }
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedBitmapForHelloWorldText) {
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  {
+    FPDF_PAGEOBJECT text_object = FPDFPage_GetObject(page, 0);
+    ASSERT_EQ(FPDF_PAGEOBJ_TEXT, FPDFPageObj_GetType(text_object));
+
+    ScopedFPDFBitmap bitmap(
+        FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 1));
+    ASSERT_TRUE(bitmap);
+    const char kChecksum[] = "bb0abe1accca1cfeaaf78afa35762350";
+    CompareBitmap(bitmap.get(), 64, 11, kChecksum);
+
+    ScopedFPDFBitmap x2_bitmap(
+        FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 2.4f));
+    ASSERT_TRUE(x2_bitmap);
+    const char kX2Checksum[] = "80db528ec7146d92247f2339a8f10ba5";
+    CompareBitmap(x2_bitmap.get(), 153, 25, kX2Checksum);
+
+    ScopedFPDFBitmap x10_bitmap(
+        FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 10));
+    ASSERT_TRUE(x10_bitmap);
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+    const char kX10Checksum[] = "bfabf04699139d05c3924526beeb4b95";
+#else
+    const char kX10Checksum[] = "149f63de758ab01d3b75605cdfd4c176";
+#endif
+    CompareBitmap(x10_bitmap.get(), 631, 103, kX10Checksum);
+  }
+
+  {
+    FPDF_PAGEOBJECT text_object = FPDFPage_GetObject(page, 1);
+    ASSERT_EQ(FPDF_PAGEOBJ_TEXT, FPDFPageObj_GetType(text_object));
+
+    ScopedFPDFBitmap bitmap(
+        FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 1));
+    ASSERT_TRUE(bitmap);
+    const char kChecksum[] = "3fc1101b2408c5484adc24ba0a11ff3d";
+    CompareBitmap(bitmap.get(), 116, 16, kChecksum);
+
+    ScopedFPDFBitmap x2_bitmap(
+        FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 2.4f));
+    ASSERT_TRUE(x2_bitmap);
+    const char kX2Checksum[] = "429960ae7b822f0c630432535e637465";
+    CompareBitmap(x2_bitmap.get(), 276, 36, kX2Checksum);
+
+    ScopedFPDFBitmap x10_bitmap(
+        FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 10));
+    ASSERT_TRUE(x10_bitmap);
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+    const char kX10Checksum[] = "c7eef7859332c75ab793ebae1c6e7221";
+#else
+    const char kX10Checksum[] = "f5f93bf64de579b59e775d7076ca0a5a";
+#endif
+    CompareBitmap(x10_bitmap.get(), 1143, 150, kX10Checksum);
+  }
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedBitmapForRotatedText) {
+  ASSERT_TRUE(OpenDocument("rotated_text.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT text_object = FPDFPage_GetObject(page, 0);
+  ASSERT_EQ(FPDF_PAGEOBJ_TEXT, FPDFPageObj_GetType(text_object));
+
+  ScopedFPDFBitmap bitmap(
+      FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 1));
+  ASSERT_TRUE(bitmap);
+  const char kChecksum[] = "08ada0802f780d3fefb161dc6fb45977";
+  CompareBitmap(bitmap.get(), 29, 28, kChecksum);
+
+  ScopedFPDFBitmap x2_bitmap(
+      FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 2.4f));
+  ASSERT_TRUE(x2_bitmap);
+  const char kX2Checksum[] = "09d7ddb647b8653cb59aede349a0c3e1";
+  CompareBitmap(x2_bitmap.get(), 67, 67, kX2Checksum);
+
+  ScopedFPDFBitmap x10_bitmap(
+      FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 10));
+  ASSERT_TRUE(x10_bitmap);
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+  const char kX10Checksum[] = "4816dd6782e9a977c58fb1ca0ced74d3";
+#else
+  const char kX10Checksum[] = "bbd3842a4b50dbfcbce4eee2b067a297";
+#endif
+  CompareBitmap(x10_bitmap.get(), 275, 275, kX10Checksum);
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedBitmapForColorText) {
+  ASSERT_TRUE(OpenDocument("text_color.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT text_object = FPDFPage_GetObject(page, 0);
+  ASSERT_EQ(FPDF_PAGEOBJ_TEXT, FPDFPageObj_GetType(text_object));
+
+  ScopedFPDFBitmap bitmap(
+      FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 7.3f));
+  ASSERT_TRUE(bitmap);
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+  const char kChecksum[] = "bb3778ba739c921525de44e9ab412868";
+#else
+  const char kChecksum[] = "e8154fa8ededf4d9b8b35b5260897b6c";
+#endif
+  CompareBitmap(bitmap.get(), 120, 186, kChecksum);
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedBitmapForNewlyCreatedText) {
+  // Start with a blank document.
+  ASSERT_TRUE(CreateNewDocument());
+
+  // Create a new text object.
+  ScopedFPDFPageObject text_object(
+      FPDFPageObj_NewTextObj(document(), "Arial", 12.0f));
+  ASSERT_EQ(FPDF_PAGEOBJ_TEXT, FPDFPageObj_GetType(text_object.get()));
+  ScopedFPDFWideString text = GetFPDFWideString(kBottomText);
+  EXPECT_TRUE(FPDFText_SetText(text_object.get(), text.get()));
+
+  ScopedFPDFBitmap bitmap(
+      FPDFTextObj_GetRenderedBitmap(document(), nullptr, text_object.get(), 1));
+  ASSERT_TRUE(bitmap);
+  const char kChecksum[] = "fa947759dab76d68a07ccf6f97b2d9c2";
+  CompareBitmap(bitmap.get(), 151, 12, kChecksum);
+}
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedBitmapForTextWithBadParameters) {
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT text_object = FPDFPage_GetObject(page, 0);
+  ASSERT_TRUE(text_object);
+
+  // Simple bad parameters testing.
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(nullptr, nullptr, nullptr, 0));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(document(), nullptr, nullptr, 0));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(nullptr, page, nullptr, 0));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(nullptr, nullptr, text_object, 0));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(nullptr, nullptr, nullptr, 1));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(document(), page, nullptr, 0));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(document(), nullptr, nullptr, 1));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(nullptr, page, text_object, 0));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(nullptr, page, nullptr, 1));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(nullptr, nullptr, text_object, 1));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(document(), page, nullptr, 1));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(nullptr, page, text_object, 1));
+
+  // Test bad scale values.
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 0));
+  EXPECT_FALSE(
+      FPDFTextObj_GetRenderedBitmap(document(), page, text_object, -1));
+  EXPECT_FALSE(
+      FPDFTextObj_GetRenderedBitmap(document(), page, text_object, 10000));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(
+      document(), page, text_object, std::numeric_limits<float>::max()));
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(
+      document(), page, text_object, std::numeric_limits<float>::infinity()));
+
+  {
+    // `text_object` will render without `page`, but may not render correctly
+    // without the resources from `page`. Although it does in this simple case.
+    ScopedFPDFBitmap bitmap(
+        FPDFTextObj_GetRenderedBitmap(document(), nullptr, text_object, 1));
+    EXPECT_TRUE(bitmap);
+  }
+
+  // Mismatch between the document and the page fails too.
+  ScopedFPDFDocument empty_document(FPDF_CreateNewDocument());
+  EXPECT_FALSE(FPDFTextObj_GetRenderedBitmap(empty_document.get(), page,
+                                             text_object, 1));
+
+  UnloadPage(page);
+}
diff --git a/fpdfsdk/fpdf_edittext.cpp b/fpdfsdk/fpdf_edittext.cpp
index 04c242c..f8adf3c 100644
--- a/fpdfsdk/fpdf_edittext.cpp
+++ b/fpdfsdk/fpdf_edittext.cpp
@@ -22,11 +22,16 @@
 #include "core/fpdfapi/parser/cpdf_stream.h"
 #include "core/fpdfapi/parser/cpdf_string.h"
 #include "core/fpdfapi/render/charposlist.h"
+#include "core/fpdfapi/render/cpdf_pagerendercontext.h"
+#include "core/fpdfapi/render/cpdf_rendercontext.h"
+#include "core/fpdfapi/render/cpdf_renderstatus.h"
+#include "core/fpdfapi/render/cpdf_textrenderer.h"
 #include "core/fpdftext/cpdf_textpage.h"
 #include "core/fxcrt/fx_extension.h"
 #include "core/fxcrt/fx_string_wrappers.h"
 #include "core/fxcrt/span_util.h"
 #include "core/fxcrt/stl_util.h"
+#include "core/fxge/cfx_defaultrenderdevice.h"
 #include "core/fxge/cfx_fontmgr.h"
 #include "core/fxge/fx_font.h"
 #include "core/fxge/text_char_pos.h"
@@ -583,6 +588,71 @@
   return Utf16EncodeMaybeCopyAndReturnLength(text, buffer, length);
 }
 
+FPDF_EXPORT FPDF_BITMAP FPDF_CALLCONV
+FPDFTextObj_GetRenderedBitmap(FPDF_DOCUMENT document,
+                              FPDF_PAGE page,
+                              FPDF_PAGEOBJECT text_object,
+                              float scale) {
+  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_TextObject* text = CPDFTextObjectFromFPDFPageObject(text_object);
+  if (!text)
+    return nullptr;
+
+  if (scale <= 0)
+    return nullptr;
+
+  const CFX_Matrix scale_matrix(scale, 0, 0, scale, 0, 0);
+  const CFX_FloatRect& text_rect = text->GetRect();
+  const CFX_FloatRect scaled_text_rect = scale_matrix.TransformRect(text_rect);
+
+  // `rect` has to use integer values. Round up as needed.
+  const FX_RECT rect = scaled_text_rect.GetOuterRect();
+  if (rect.IsEmpty())
+    return nullptr;
+
+  auto result_bitmap = pdfium::MakeRetain<CFX_DIBitmap>();
+  if (!result_bitmap->Create(rect.Width(), rect.Height(), FXDIB_Format::kArgb))
+    return nullptr;
+
+  auto render_context = std::make_unique<CPDF_PageRenderContext>();
+  CPDF_PageRenderContext* render_context_ptr = render_context.get();
+  CPDF_Page::RenderContextClearer clearer(optional_page);
+  if (optional_page)
+    optional_page->SetRenderContext(std::move(render_context));
+
+  CPDF_Dictionary* page_resources =
+      optional_page ? optional_page->GetPageResources() : nullptr;
+
+  auto device = std::make_unique<CFX_DefaultRenderDevice>();
+  CFX_DefaultRenderDevice* device_ptr = device.get();
+  render_context_ptr->m_pDevice = std::move(device);
+  render_context_ptr->m_pContext = std::make_unique<CPDF_RenderContext>(
+      doc, page_resources, /*pPageCache=*/nullptr);
+
+  device_ptr->Attach(result_bitmap, /*bRgbByteOrder=*/false,
+                     /*pBackdropBitmap=*/nullptr, /*bGroupKnockout=*/false);
+
+  CFX_Matrix device_matrix(rect.Width(), 0, 0, rect.Height(), 0, 0);
+  CPDF_RenderStatus status(render_context_ptr->m_pContext.get(), device_ptr);
+  status.SetDeviceMatrix(device_matrix);
+  status.Initialize(nullptr, nullptr);
+
+  // Need to flip the rendering and also move it to fit within `result_bitmap`.
+  CFX_Matrix render_matrix(1, 0, 0, -1, -text_rect.left, text_rect.top);
+  render_matrix *= scale_matrix;
+  status.RenderSingleObject(text, render_matrix);
+
+  // Caller takes ownership.
+  return FPDFBitmapFromCFXDIBitmap(result_bitmap.Leak());
+}
+
 FPDF_EXPORT void FPDF_CALLCONV FPDFFont_Close(FPDF_FONT font) {
   // Take back ownership from caller and release.
   RetainPtr<CPDF_Font>().Unleak(CPDFFontFromFPDFFont(font));
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index 4e3627f..e524f30 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -249,6 +249,7 @@
     CHK(FPDFPath_SetDrawMode);
     CHK(FPDFTextObj_GetFont);
     CHK(FPDFTextObj_GetFontSize);
+    CHK(FPDFTextObj_GetRenderedBitmap);
     CHK(FPDFTextObj_GetText);
     CHK(FPDFTextObj_GetTextRenderMode);
     CHK(FPDFTextObj_SetTextRenderMode);
diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h
index b68b7c9..8f4e7f0 100644
--- a/public/fpdf_edit.h
+++ b/public/fpdf_edit.h
@@ -647,7 +647,7 @@
 // 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.
+// 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.
 //
@@ -655,7 +655,7 @@
 //   page         - handle to an optional page associated with |image_object|.
 //   image_object - handle to an image object.
 //
-// Returns the bitmap.
+// Returns the bitmap or NULL on failure.
 FPDF_EXPORT FPDF_BITMAP FPDF_CALLCONV
 FPDFImageObj_GetRenderedBitmap(FPDF_DOCUMENT document,
                                FPDF_PAGE page,
@@ -1219,6 +1219,25 @@
                     unsigned long length);
 
 // Experimental API.
+// Get a bitmap rasterization of |text_object|. To render correctly, the caller
+// must provide the |document| associated with |text_object|. If there is a
+// |page| associated with |text_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 |text_object|.
+//   page        - handle to an optional page associated with |text_object|.
+//   text_object - handle to a text object.
+//   scale       - the scaling factor, which must be greater than 0.
+//
+// Returns the bitmap or NULL on failure.
+FPDF_EXPORT FPDF_BITMAP FPDF_CALLCONV
+FPDFTextObj_GetRenderedBitmap(FPDF_DOCUMENT document,
+                              FPDF_PAGE page,
+                              FPDF_PAGEOBJECT text_object,
+                              float scale);
+
+// Experimental API.
 // Get the font of a text object.
 //
 // text - the handle to the text object.