[Skia] Optimize DrawPath() when knockout is on

Instead of calculating geometric union and subtraction of paths which
may contain many points, composite the objects in pixel-space using
blend modes.

This updates the pdfium_tests hash in DEPS to
https://pdfium-review.googlesource.com/c/pdfium_tests/+/138210

Initially I kept the same structure of doing a union or difference based
on the stroke color and drawing the stroke on the separate layer from
the fill. However when I read ISO 32000-1:2008 I realized I could
simplify it and make the function more accurately follow the spec.

Rendering performance for the pdf in the bug using pdfium_test:

 - Before: 68.72s user 0.37s system 99% cpu 1:09.14 total
 - After: 0.52s user 0.12s system 99% cpu 0.645 total

Bug: 334774307, 460180742
Change-Id: I7cc2d264bb06182c69d74dc7fcd04fe285d47256
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/137630
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Commit-Queue: April Kallmeyer <ask@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
diff --git a/DEPS b/DEPS
index e66cb17..a363f56 100644
--- a/DEPS
+++ b/DEPS
@@ -190,7 +190,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling pdfium_tests
   # and whatever else without interference from each other.
-  'pdfium_tests_revision': '79a975f5b013ab1b3b0d1b887d9bdddcd50725e3',
+  'pdfium_tests_revision': '47d8f8f7f29b30a7abb9b116c93d6034f80e255d',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling result_adapter_revision
   # and whatever else without interference from each other.
diff --git a/core/fxge/skia/fx_skia_device.cpp b/core/fxge/skia/fx_skia_device.cpp
index 7a5bae2..6529997 100644
--- a/core/fxge/skia/fx_skia_device.cpp
+++ b/core/fxge/skia/fx_skia_device.cpp
@@ -1122,50 +1122,78 @@
   if (fill_options.full_cover) {
     path_paint.setBlendMode(SkBlendMode::kPlus);
   }
+
+  SkPaint stroke_paint = path_paint;
   int stroke_alpha = FXARGB_A(stroke_color);
   if (stroke_alpha) {
     const CFX_GraphStateData stroke_options_copy =
         stroke_options ? *stroke_options : CFX_GraphStateData();
-    SetupStrokePaint(&path_paint, &stroke_options_copy, transform_matrix,
+    SetupStrokePaint(&stroke_paint, &stroke_options_copy, transform_matrix,
                      fill_options);
   }
 
   SkAutoCanvasRestore scoped_save_restore(canvas_, /*doSave=*/true);
   canvas_->concat(transform_matrix);
-  bool do_stroke = true;
-  if (fill_options.fill_type != CFX_FillRenderOptions::FillType::kNoFill &&
+
+  // Draw the fill (and the stroke if it's a knockout group)
+  const bool is_point_path = IsPathAPoint(path);
+  if (!is_point_path &&
+      fill_options.fill_type != CFX_FillRenderOptions::FillType::kNoFill &&
       fill_color) {
-    SkPath path_outline;
-    const SkPath* fill_path = &path;
-    if (stroke_alpha) {
-      if (group_knockout_) {
-        skpathutils::FillPathWithPaint(path, path_paint, &path_outline);
-        if (stroke_color == fill_color &&
-            Op(path, path_outline, SkPathOp::kUnion_SkPathOp, &path_outline)) {
-          fill_path = &path_outline;
-          do_stroke = false;
-        } else if (Op(path, path_outline, SkPathOp::kDifference_SkPathOp,
-                      &path_outline)) {
-          fill_path = &path_outline;
-        }
-      }
+    // Knockout Group is a PDF feature that means the elements of the group
+    // should not affect each other with transparency.
+    //
+    // See section 11.4.6 of in ISO 32000-1:2008:
+    // "At any given point, only the topmost object enclosing the point shall
+    // contribute to the result colour and opacity of the group as a whole"
+    if (stroke_alpha && group_knockout_) {
+      // Draw the knockout group path on a separate layer so blend modes can be
+      // adjusted. When restore() is called path_paint is used to composite the
+      // layer.
+      canvas_->saveLayer(nullptr, &path_paint);
+      SkPaint layer_paint = stroke_paint;
+
+      // kSrc means the top-most object fully overwrites any pixels it draws.
+      // See https://skia.org/docs/user/api/skblendmode_overview/
+      layer_paint.setBlendMode(SkBlendMode::kSrc);
+      layer_paint.setStyle(SkPaint::kFill_Style);
+
+      layer_paint.setColor(fill_color);
+      DrawPathImpl(path, layer_paint);
+
+      // Compute the stroke outline and draw with fill style so that if the
+      // path self-intersects the anti-aliasing pixels won't stack their
+      // transparency.
+      //
+      // Drawing it as a stroke normally already operates in the knockout way
+      // but not for the AA pixels in some cases.
+      SkPath stroke_outline;
+      skpathutils::FillPathWithPaint(path, stroke_paint, &stroke_outline);
+      layer_paint.setColor(stroke_color);
+      DrawPathImpl(stroke_outline, layer_paint);
+
+      return true;  // `scoped_save_restore` restores two layers here
     }
-    path_paint.setStyle(SkPaint::kFill_Style);
-    path_paint.setColor(fill_color);
-    DrawPathImpl(*fill_path, path_paint);
+
+    // Draw the fill normally if it isn't a knockout group
+    stroke_paint.setStyle(SkPaint::kFill_Style);
+    stroke_paint.setColor(fill_color);
+    DrawPathImpl(path, stroke_paint);
   }
-  if (stroke_alpha && do_stroke) {
-    path_paint.setStyle(SkPaint::kStroke_Style);
-    path_paint.setColor(stroke_color);
-    if (!path.isLastContourClosed() && IsPathAPoint(path)) {
-      DCHECK_GE(path.countPoints(), 1);
-      canvas_->drawPoint(path.getPoint(0), path_paint);
-    } else if (IsPathAPoint(path) &&
-               path_paint.getStrokeCap() != SkPaint::kRound_Cap) {
+
+  // Draw the stroke
+  if (stroke_alpha) {
+    stroke_paint.setStyle(SkPaint::kStroke_Style);
+    stroke_paint.setColor(stroke_color);
+    if (!path.isLastContourClosed() && is_point_path) {
+      CHECK_GE(path.countPoints(), 1);
+      canvas_->drawPoint(path.getPoint(0), stroke_paint);
+    } else if (is_point_path &&
+               stroke_paint.getStrokeCap() != SkPaint::kRound_Cap) {
       // Do nothing. A closed 0-length closed path can be rendered only if
       // its line cap type is round.
     } else {
-      DrawPathImpl(path, path_paint);
+      DrawPathImpl(path, stroke_paint);
     }
   }
   return true;
diff --git a/fpdfsdk/fpdf_edit_embeddertest.cpp b/fpdfsdk/fpdf_edit_embeddertest.cpp
index 57e9d78..b78d8bd 100644
--- a/fpdfsdk/fpdf_edit_embeddertest.cpp
+++ b/fpdfsdk/fpdf_edit_embeddertest.cpp
@@ -2697,7 +2697,11 @@
     ScopedFPDFBitmap page_bitmap = RenderPage(page);
     const char* checksum_3 = []() {
       if (CFX_DefaultRenderDevice::UseSkiaRenderer()) {
-        return "a5de6ddefcbae60924bebc99347e460b";
+#if BUILDFLAG(IS_APPLE) && defined(ARCH_CPU_ARM64)
+        return "a4b4b739462a471cad72656e7e8c2af8";
+#else
+        return "037f5b38d8b612abf0833eb9ac8adf69";
+#endif
       }
       return "ff3e6a22326754944cc6e56609acd73b";
     }();
@@ -3943,7 +3947,11 @@
 TEST_F(FPDFEditEmbedderTest, SaveAndRender) {
   const char* checksum = []() {
     if (CFX_DefaultRenderDevice::UseSkiaRenderer()) {
-      return "edd4aed776c0eaf8c79dd24d9654af95";
+#if BUILDFLAG(IS_APPLE) && defined(ARCH_CPU_ARM64)
+      return "0b8b84f8da13bf8a5bbaff3087685bed";
+#else
+      return "a6e8827e9fda09151765130e2f5531eb";
+#endif
     }
     return "3c20472b0552c0c22b88ab1ed8c6202b";
   }();
diff --git a/testing/SUPPRESSIONS b/testing/SUPPRESSIONS
index 2abdb60..8c8b44b 100644
--- a/testing/SUPPRESSIONS
+++ b/testing/SUPPRESSIONS
@@ -348,6 +348,7 @@
 example_042.pdf mac_x86 * * skia
 font_1_embedded_font_en_feature.pdf mac_x86 * * skia
 font_2_embedded_font_en_size14.pdf mac_x86 * * skia
+transparent1.pdf mac_x86 * * skia
 xfermodes.pdf mac_x86 * * skia
 xfermodes2.pdf mac_x86 * * skia
 xfermodes3.pdf mac_x86 * * skia
@@ -817,4 +818,5 @@
 # rendering that fuzzy matching is not enough for it to match. The difference
 # looks like it may be due to several rounding errors combining for a bigger
 # than normal pixel value difference.
+bug_1430333.in mac_x86 * * skia
 bug_1746.in mac_x86 * * skia
diff --git a/testing/SUPPRESSIONS_EXACT_MATCHING b/testing/SUPPRESSIONS_EXACT_MATCHING
index db0a0fc..023eb63 100644
--- a/testing/SUPPRESSIONS_EXACT_MATCHING
+++ b/testing/SUPPRESSIONS_EXACT_MATCHING
@@ -167,6 +167,7 @@
 new_textmarkup2.pdf mac * * skia
 new_textmarkup8.pdf mac * * skia
 path_5_pattern.pdf mac * * skia
+path_9.pdf mac_x86 * * skia
 quick_start_guide.pdf mac * * skia
 shading_2_15.pdf mac * * skia
 shading_2_17.pdf mac * * skia
diff --git a/testing/resources/pixel/bug_1430333_expected_skia.pdf.0.png b/testing/resources/pixel/bug_1430333_expected_skia.pdf.0.png
index e6a1827..509eff5 100644
--- a/testing/resources/pixel/bug_1430333_expected_skia.pdf.0.png
+++ b/testing/resources/pixel/bug_1430333_expected_skia.pdf.0.png
Binary files differ
diff --git a/testing/resources/pixel/bug_1430333_expected_skia_mac.pdf.0.png b/testing/resources/pixel/bug_1430333_expected_skia_mac.pdf.0.png
new file mode 100644
index 0000000..2e892d6
--- /dev/null
+++ b/testing/resources/pixel/bug_1430333_expected_skia_mac.pdf.0.png
Binary files differ
diff --git a/testing/resources/pixel/same_color_knockout_fill_expected_skia.pdf.0.png b/testing/resources/pixel/same_color_knockout_fill_expected_skia.pdf.0.png
deleted file mode 100644
index c0ecfbd..0000000
--- a/testing/resources/pixel/same_color_knockout_fill_expected_skia.pdf.0.png
+++ /dev/null
Binary files differ
diff --git a/testing/resources/pixel/single_point_paths_expected_skia.pdf.0.png b/testing/resources/pixel/single_point_paths_expected_skia.pdf.0.png
index 9034434..12b05e6 100644
--- a/testing/resources/pixel/single_point_paths_expected_skia.pdf.0.png
+++ b/testing/resources/pixel/single_point_paths_expected_skia.pdf.0.png
Binary files differ