Add FPDFPageObj_GetRenderedStrokePattern() API

This allows getting the rendered bitmap of a stroke pattern of a page
object.

Change-Id: I4603be175bfe8721d4e11500afcc2be8c2c9f760
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/154910
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/core/fpdfapi/render/cpdf_rendertiling.cpp b/core/fpdfapi/render/cpdf_rendertiling.cpp
index f407f24..7702735 100644
--- a/core/fpdfapi/render/cpdf_rendertiling.cpp
+++ b/core/fpdfapi/render/cpdf_rendertiling.cpp
@@ -26,7 +26,17 @@
 
 namespace {
 
-RetainPtr<CFX_DIBitmap> DrawPatternBitmap(
+std::optional<int> CheckedFloatToInt(float value) {
+  if (!pdfium::IsValueInRangeForNumericType<int>(value)) {
+    return std::nullopt;
+  }
+  return static_cast<int>(value);
+}
+
+}  // namespace
+
+// static
+RetainPtr<CFX_DIBitmap> CPDF_RenderTiling::DrawPatternBitmap(
     CPDF_Document* doc,
     CPDF_PageImageCache* pCache,
     CPDF_TilingPattern* pPattern,
@@ -72,15 +82,6 @@
   return pBitmap;
 }
 
-std::optional<int> CheckedFloatToInt(float value) {
-  if (!pdfium::IsValueInRangeForNumericType<int>(value)) {
-    return std::nullopt;
-  }
-  return static_cast<int>(value);
-}
-
-}  // namespace
-
 // static
 RetainPtr<CFX_DIBitmap> CPDF_RenderTiling::Draw(
     CPDF_RenderStatus* pRenderStatus,
diff --git a/core/fpdfapi/render/cpdf_rendertiling.h b/core/fpdfapi/render/cpdf_rendertiling.h
index 34fb56b..2a12446 100644
--- a/core/fpdfapi/render/cpdf_rendertiling.h
+++ b/core/fpdfapi/render/cpdf_rendertiling.h
@@ -7,11 +7,14 @@
 #ifndef CORE_FPDFAPI_RENDER_CPDF_RENDERTILING_H_
 #define CORE_FPDFAPI_RENDER_CPDF_RENDERTILING_H_
 
+#include "core/fpdfapi/render/cpdf_renderoptions.h"
 #include "core/fxcrt/retain_ptr.h"
 
 class CFX_DIBitmap;
 class CFX_Matrix;
+class CPDF_Document;
 class CPDF_Form;
+class CPDF_PageImageCache;
 class CPDF_PageObject;
 class CPDF_RenderStatus;
 class CPDF_TilingPattern;
@@ -27,6 +30,17 @@
                                       const FX_RECT& clip_box,
                                       bool bStroke);
 
+  // Renders a single cell of pPattern into a width x height bitmap.
+  static RetainPtr<CFX_DIBitmap> DrawPatternBitmap(
+      CPDF_Document* doc,
+      CPDF_PageImageCache* pCache,
+      CPDF_TilingPattern* pPattern,
+      CPDF_Form* pPatternForm,
+      const CFX_Matrix& mtObject2Device,
+      int width,
+      int height,
+      const CPDF_RenderOptions::Options& draw_options);
+
   CPDF_RenderTiling() = delete;
   CPDF_RenderTiling(const CPDF_RenderTiling&) = delete;
   CPDF_RenderTiling& operator=(const CPDF_RenderTiling&) = delete;
diff --git a/fpdfsdk/fpdf_edit_embeddertest.cpp b/fpdfsdk/fpdf_edit_embeddertest.cpp
index 82485d9..ac66807 100644
--- a/fpdfsdk/fpdf_edit_embeddertest.cpp
+++ b/fpdfsdk/fpdf_edit_embeddertest.cpp
@@ -5529,6 +5529,70 @@
                                              text_object, 1));
 }
 
+TEST_F(FPDFEditEmbedderTest, GetRenderedStrokePattern) {
+  ASSERT_TRUE(OpenDocument("pattern_stroke.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT path_object = FPDFPage_GetObject(page.get(), 0);
+  ASSERT_EQ(FPDF_PAGEOBJ_PATH, FPDFPageObj_GetType(path_object));
+
+  ScopedFPDFBitmap bitmap(
+      FPDFPageObj_GetRenderedStrokePattern(document(), path_object));
+  ASSERT_TRUE(bitmap);
+  // This is the case when a colored pattern has a color of its own.
+  CompareBitmapWithExpectationSuffix(bitmap.get(), "pattern_stroke");
+}
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedStrokePatternUncolored) {
+  ASSERT_TRUE(OpenDocument("pattern_stroke_uncolored.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT path_object = FPDFPage_GetObject(page.get(), 0);
+  ASSERT_EQ(FPDF_PAGEOBJ_PATH, FPDFPageObj_GetType(path_object));
+
+  ScopedFPDFBitmap bitmap(
+      FPDFPageObj_GetRenderedStrokePattern(document(), path_object));
+  ASSERT_TRUE(bitmap);
+  // This is the case when an uncolored pattern has no color of its own.
+  CompareBitmapWithExpectationSuffix(bitmap.get(), "pattern_stroke_uncolored");
+}
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedStrokePatternBadParams) {
+  ASSERT_TRUE(OpenDocument("pattern_stroke.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT path_object = FPDFPage_GetObject(page.get(), 0);
+  ASSERT_TRUE(path_object);
+
+  // Simple bad parameters testing.
+  EXPECT_FALSE(FPDFPageObj_GetRenderedStrokePattern(nullptr, nullptr));
+  EXPECT_FALSE(FPDFPageObj_GetRenderedStrokePattern(document(), nullptr));
+  EXPECT_FALSE(FPDFPageObj_GetRenderedStrokePattern(nullptr, path_object));
+
+  // A page object that is not a path has no stroke pattern, either.
+  FPDF_PAGEOBJECT text_object =
+      FPDFPageObj_NewTextObj(document(), "Arial", 12.0f);
+  ASSERT_EQ(FPDF_PAGEOBJ_TEXT, FPDFPageObj_GetType(text_object));
+  EXPECT_TRUE(FPDFPage_InsertObject(page.get(), text_object));
+  ASSERT_EQ(2, FPDFPage_CountObjects(page.get()));
+  EXPECT_FALSE(FPDFPageObj_GetRenderedStrokePattern(document(), text_object));
+}
+
+TEST_F(FPDFEditEmbedderTest, GetRenderedStrokePatternNoPattern) {
+  ASSERT_TRUE(OpenDocument("rectangles.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT path_object = FPDFPage_GetObject(page.get(), 0);
+  ASSERT_EQ(FPDF_PAGEOBJ_PATH, FPDFPageObj_GetType(path_object));
+
+  // The stroke color is a plain color, not a pattern.
+  EXPECT_FALSE(FPDFPageObj_GetRenderedStrokePattern(document(), path_object));
+}
+
 TEST_F(FPDFEditEmbedderTest, GetRenderedBitmapForRotatedImage) {
   ScopedFPDFDocument doc(FPDF_CreateNewDocument());
   ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 100, 100));
diff --git a/fpdfsdk/fpdf_editpage.cpp b/fpdfsdk/fpdf_editpage.cpp
index 758aae8..9b9474e 100644
--- a/fpdfsdk/fpdf_editpage.cpp
+++ b/fpdfsdk/fpdf_editpage.cpp
@@ -25,12 +25,15 @@
 #include "core/fpdfapi/page/cpdf_pathobject.h"
 #include "core/fpdfapi/page/cpdf_shadingobject.h"
 #include "core/fpdfapi/page/cpdf_textobject.h"
+#include "core/fpdfapi/page/cpdf_tilingpattern.h"
 #include "core/fpdfapi/parser/cpdf_array.h"
 #include "core/fpdfapi/parser/cpdf_dictionary.h"
 #include "core/fpdfapi/parser/cpdf_document.h"
 #include "core/fpdfapi/parser/cpdf_number.h"
 #include "core/fpdfapi/parser/cpdf_string.h"
 #include "core/fpdfapi/render/cpdf_docrenderdata.h"
+#include "core/fpdfapi/render/cpdf_renderoptions.h"
+#include "core/fpdfapi/render/cpdf_rendertiling.h"
 #include "core/fpdfdoc/cpdf_annot.h"
 #include "core/fpdfdoc/cpdf_annotlist.h"
 #include "core/fxcrt/compiler_specific.h"
@@ -42,6 +45,7 @@
 #include "core/fxcrt/span_util.h"
 #include "core/fxcrt/stl_util.h"
 #include "core/fxcrt/unowned_ptr.h"
+#include "core/fxge/dib/cfx_dibitmap.h"
 #include "fpdfsdk/cpdfsdk_helpers.h"
 #include "public/fpdf_formfill.h"
 
@@ -193,6 +197,64 @@
   return date;
 }
 
+FPDF_BITMAP RenderTilingPatternToBitmap(CPDF_Pattern* pattern,
+                                        CPDF_Document* doc,
+                                        CPDF_PageObject* page_obj) {
+  CPDF_TilingPattern* tiling_pattern = pattern->AsTilingPattern();
+  if (!tiling_pattern) {
+    return nullptr;
+  }
+  const std::unique_ptr<CPDF_Form> pattern_form =
+      tiling_pattern->Load(page_obj);
+  if (!pattern_form) {
+    return nullptr;
+  }
+  const FX_RECT rect = tiling_pattern->bbox().GetOuterRect();
+  if (rect.IsEmpty() || !rect.Valid()) {
+    return nullptr;
+  }
+
+  RetainPtr<CFX_DIBitmap> cell_bitmap = CPDF_RenderTiling::DrawPatternBitmap(
+      doc, /*pCache=*/nullptr, tiling_pattern, pattern_form.get(), CFX_Matrix(),
+      rect.Width(), rect.Height(), CPDF_RenderOptions::Options());
+  if (!cell_bitmap) {
+    return nullptr;
+  }
+
+  if (tiling_pattern->colored()) {
+    ValidateBitmapPremultiplyState(cell_bitmap);
+
+    // Caller takes ownership.
+    return FPDFBitmapFromCFXDIBitmap(cell_bitmap.Leak());
+  }
+
+  // An uncolored pattern gets its color from the stroke color operands.
+  const FX_COLORREF stroke_colorref =
+      page_obj->color_state().GetStrokeColorRef();
+  if (stroke_colorref == 0xFFFFFFFF) {
+    return nullptr;
+  }
+
+  auto result_bitmap = pdfium::MakeRetain<CFX_DIBitmap>();
+  if (!result_bitmap->Create(rect.Width(), rect.Height(),
+                             FXDIB_Format::kBgra)) {
+    return nullptr;
+  }
+  const int stroke_alpha =
+      static_cast<int>(page_obj->general_state().GetStrokeAlpha() * 255);
+  if (!result_bitmap->CompositeMask(
+          /*dest_left=*/0, /*dest_top=*/0, rect.Width(), rect.Height(),
+          cell_bitmap, AlphaAndColorRefToArgb(stroke_alpha, stroke_colorref),
+          /*src_left=*/0, /*src_top=*/0, BlendMode::kNormal)) {
+    return nullptr;
+  }
+
+  ValidateBitmapPremultiplyState(result_bitmap);
+
+  // Caller takes ownership.
+  return FPDFBitmapFromCFXDIBitmap(result_bitmap.Leak());
+}
+
 }  // namespace
 
 FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV FPDF_CreateNewDocument() {
@@ -1196,6 +1258,32 @@
   return true;
 }
 
+FPDF_EXPORT FPDF_BITMAP FPDF_CALLCONV
+FPDFPageObj_GetRenderedStrokePattern(FPDF_DOCUMENT document,
+                                     FPDF_PAGEOBJECT page_object) {
+  CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document);
+  if (!doc) {
+    return nullptr;
+  }
+
+  CPDF_PageObject* object = CPDFPageObjectFromFPDFPageObject(page_object);
+  if (!object) {
+    return nullptr;
+  }
+
+  const CPDF_Color* stroke = object->color_state().GetStrokeColor();
+  if (!stroke || !stroke->IsPattern()) {
+    return nullptr;
+  }
+
+  RetainPtr<CPDF_Pattern> pattern = stroke->GetPattern();
+  if (!pattern) {
+    return nullptr;
+  }
+
+  return RenderTilingPatternToBitmap(pattern.Get(), doc, object);
+}
+
 FPDF_EXPORT int FPDF_CALLCONV
 FPDFFormObj_CountObjects(FPDF_PAGEOBJECT form_object) {
   const auto* pObjectList = CPDFPageObjHolderFromFPDFFormObject(form_object);
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index b51a810..4269321 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -231,6 +231,7 @@
     CHK(FPDFPageObj_GetMark);
     CHK(FPDFPageObj_GetMarkedContentID);
     CHK(FPDFPageObj_GetMatrix);
+    CHK(FPDFPageObj_GetRenderedStrokePattern);
     CHK(FPDFPageObj_GetRotatedBounds);
     CHK(FPDFPageObj_GetStrokeColor);
     CHK(FPDFPageObj_GetStrokeWidth);
diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h
index 2dbd60d..c7fca05 100644
--- a/public/fpdf_edit.h
+++ b/public/fpdf_edit.h
@@ -1190,6 +1190,22 @@
                          size_t dash_count,
                          float phase);
 
+// Experimental API.
+// Get a bitmap rasterization of the stroke pattern of |page_object|.
+// To render correctly, the caller must provide the |document| associated with
+// |page_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.
+//
+//   document    - handle to the document containing |page_object|.
+//   page_object - handle to a page object.
+//
+// Returns the bitmap, or NULL if the stroke is not a tiling pattern or on
+// failure.
+FPDF_EXPORT FPDF_BITMAP FPDF_CALLCONV
+FPDFPageObj_GetRenderedStrokePattern(FPDF_DOCUMENT document,
+                                     FPDF_PAGEOBJECT page_object);
+
 // Get number of segments inside |path|.
 //
 //   path - handle to a path.
diff --git a/testing/resources/embedder_tests/pattern_stroke.png b/testing/resources/embedder_tests/pattern_stroke.png
new file mode 100644
index 0000000..3a84afe
--- /dev/null
+++ b/testing/resources/embedder_tests/pattern_stroke.png
Binary files differ
diff --git a/testing/resources/embedder_tests/pattern_stroke_uncolored.png b/testing/resources/embedder_tests/pattern_stroke_uncolored.png
new file mode 100644
index 0000000..fb17d2c
--- /dev/null
+++ b/testing/resources/embedder_tests/pattern_stroke_uncolored.png
Binary files differ
diff --git a/testing/resources/pattern_stroke.in b/testing/resources/pattern_stroke.in
new file mode 100644
index 0000000..3829c8b
--- /dev/null
+++ b/testing/resources/pattern_stroke.in
@@ -0,0 +1,59 @@
+{{header}}
+{{object 1 0}} <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+{{object 2 0}} <<
+  /Type /Pages
+  /MediaBox [0 0 200 300]
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+{{object 3 0}} <<
+  /Type /Page
+  /Parent 2 0 R
+  /Contents 4 0 R
+  /Resources <<
+    /Pattern <<
+      /P0 5 0 R
+    >>
+  >>
+>>
+endobj
+{{object 4 0}} <<
+  {{streamlen}}
+>>
+stream
+q
+/Pattern CS
+/P0 SCN
+40 w
+20 150 m
+180 150 l
+S
+Q
+endstream
+endobj
+{{object 5 0}} <<
+  /Type /Pattern
+  /PatternType 1
+  /PaintType 1
+  /TilingType 1
+  /BBox [0 0 72 72]
+  /XStep 72
+  /YStep 72
+  /Resources <<>>
+  {{streamlen}}
+>>
+stream
+0 1 0.91 0 k
+0 0 72 72 re
+f
+endstream
+endobj
+{{xref}}
+{{trailer}}
+{{startxref}}
+%%EOF
diff --git a/testing/resources/pattern_stroke.pdf b/testing/resources/pattern_stroke.pdf
new file mode 100644
index 0000000..de3e429
--- /dev/null
+++ b/testing/resources/pattern_stroke.pdf
@@ -0,0 +1,71 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+2 0 obj <<
+  /Type /Pages
+  /MediaBox [0 0 200 300]
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+3 0 obj <<
+  /Type /Page
+  /Parent 2 0 R
+  /Contents 4 0 R
+  /Resources <<
+    /Pattern <<
+      /P0 5 0 R
+    >>
+  >>
+>>
+endobj
+4 0 obj <<
+  /Length 49
+>>
+stream
+q
+/Pattern CS
+/P0 SCN
+40 w
+20 150 m
+180 150 l
+S
+Q
+endstream
+endobj
+5 0 obj <<
+  /Type /Pattern
+  /PatternType 1
+  /PaintType 1
+  /TilingType 1
+  /BBox [0 0 72 72]
+  /XStep 72
+  /YStep 72
+  /Resources <<>>
+  /Length 27
+>>
+stream
+0 1 0.91 0 k
+0 0 72 72 re
+f
+endstream
+endobj
+xref
+0 6
+0000000000 65535 f 
+0000000015 00000 n 
+0000000068 00000 n 
+0000000157 00000 n 
+0000000286 00000 n 
+0000000387 00000 n 
+trailer <<
+  /Root 1 0 R
+  /Size 6
+>>
+startxref
+593
+%%EOF
diff --git a/testing/resources/pattern_stroke_uncolored.in b/testing/resources/pattern_stroke_uncolored.in
new file mode 100644
index 0000000..5cd5c16
--- /dev/null
+++ b/testing/resources/pattern_stroke_uncolored.in
@@ -0,0 +1,61 @@
+{{header}}
+{{object 1 0}} <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+{{object 2 0}} <<
+  /Type /Pages
+  /MediaBox [0 0 200 300]
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+{{object 3 0}} <<
+  /Type /Page
+  /Parent 2 0 R
+  /Contents 4 0 R
+  /Resources <<
+    /ColorSpace <<
+      /CS0 [/Pattern /DeviceRGB]
+    >>
+    /Pattern <<
+      /P0 5 0 R
+    >>
+  >>
+>>
+endobj
+{{object 4 0}} <<
+  {{streamlen}}
+>>
+stream
+q
+/CS0 CS
+0 0 1 /P0 SCN
+40 w
+20 150 m
+180 150 l
+S
+Q
+endstream
+endobj
+{{object 5 0}} <<
+  /Type /Pattern
+  /PatternType 1
+  /PaintType 2
+  /TilingType 1
+  /BBox [0 0 72 72]
+  /XStep 72
+  /YStep 72
+  /Resources <<>>
+  {{streamlen}}
+>>
+stream
+0 0 72 72 re
+f
+endstream
+endobj
+{{xref}}
+{{trailer}}
+{{startxref}}
+%%EOF
diff --git a/testing/resources/pattern_stroke_uncolored.pdf b/testing/resources/pattern_stroke_uncolored.pdf
new file mode 100644
index 0000000..b167d72
--- /dev/null
+++ b/testing/resources/pattern_stroke_uncolored.pdf
@@ -0,0 +1,73 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+2 0 obj <<
+  /Type /Pages
+  /MediaBox [0 0 200 300]
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+3 0 obj <<
+  /Type /Page
+  /Parent 2 0 R
+  /Contents 4 0 R
+  /Resources <<
+    /ColorSpace <<
+      /CS0 [/Pattern /DeviceRGB]
+    >>
+    /Pattern <<
+      /P0 5 0 R
+    >>
+  >>
+>>
+endobj
+4 0 obj <<
+  /Length 51
+>>
+stream
+q
+/CS0 CS
+0 0 1 /P0 SCN
+40 w
+20 150 m
+180 150 l
+S
+Q
+endstream
+endobj
+5 0 obj <<
+  /Type /Pattern
+  /PatternType 1
+  /PaintType 2
+  /TilingType 1
+  /BBox [0 0 72 72]
+  /XStep 72
+  /YStep 72
+  /Resources <<>>
+  /Length 14
+>>
+stream
+0 0 72 72 re
+f
+endstream
+endobj
+xref
+0 6
+0000000000 65535 f 
+0000000015 00000 n 
+0000000068 00000 n 
+0000000157 00000 n 
+0000000345 00000 n 
+0000000448 00000 n 
+trailer <<
+  /Root 1 0 R
+  /Size 6
+>>
+startxref
+641
+%%EOF