Add XPS support to pdfium_test

Adds pdfium_test support for XPS output (--xps). This outputs an XPS
file per page, and requires Skia and Windows.

Fixed: pdfium:2059
Change-Id: Ifbb1befa7f74ae0dd03048eda9cdd8042279d79c
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/109232
Reviewed-by: Nigi <nigi@chromium.org>
Commit-Queue: K. Moon <kmoon@chromium.org>
diff --git a/samples/BUILD.gn b/samples/BUILD.gn
index 83b62dd..577d685 100644
--- a/samples/BUILD.gn
+++ b/samples/BUILD.gn
@@ -57,6 +57,13 @@
   ]
   configs += [ ":pdfium_samples_config" ]
 
+  if (is_win) {
+    sources += [
+      "helpers/win32/com_factory.cc",
+      "helpers/win32/com_factory.h",
+    ]
+  }
+
   if (pdf_enable_v8) {
     deps += [
       "//v8:v8_headers",
diff --git a/samples/helpers/win32/com_factory.cc b/samples/helpers/win32/com_factory.cc
new file mode 100644
index 0000000..7da80a7
--- /dev/null
+++ b/samples/helpers/win32/com_factory.cc
@@ -0,0 +1,51 @@
+// Copyright 2023 The PDFium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "samples/helpers/win32/com_factory.h"
+
+#include <combaseapi.h>
+#include <objbase.h>
+#include <winerror.h>
+#include <xpsobjectmodel.h>
+
+#include "third_party/base/check_op.h"
+
+ComFactory::ComFactory() = default;
+
+ComFactory::~ComFactory() {
+  if (xps_om_object_factory_) {
+    xps_om_object_factory_->Release();
+  }
+
+  if (initialized_) {
+    CoUninitialize();
+  }
+}
+
+bool ComFactory::Initialize() {
+  if (!initialized_) {
+    HRESULT result = CoInitializeEx(
+        nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
+    DCHECK_NE(RPC_E_CHANGED_MODE, result);
+    initialized_ = SUCCEEDED(result);
+  }
+
+  return initialized_;
+}
+
+IXpsOMObjectFactory* ComFactory::GetXpsOMObjectFactory() {
+  if (!xps_om_object_factory_ && Initialize()) {
+    HRESULT result =
+        CoCreateInstance(__uuidof(XpsOMObjectFactory), /*pUnkOuter=*/nullptr,
+                         CLSCTX_INPROC_SERVER, __uuidof(IXpsOMObjectFactory),
+                         reinterpret_cast<LPVOID*>(&xps_om_object_factory_));
+    if (SUCCEEDED(result)) {
+      DCHECK(xps_om_object_factory_);
+    } else {
+      DCHECK(!xps_om_object_factory_);
+    }
+  }
+
+  return xps_om_object_factory_;
+}
diff --git a/samples/helpers/win32/com_factory.h b/samples/helpers/win32/com_factory.h
new file mode 100644
index 0000000..eabaffe
--- /dev/null
+++ b/samples/helpers/win32/com_factory.h
@@ -0,0 +1,25 @@
+// Copyright 2023 The PDFium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SAMPLES_HELPERS_WIN32_COM_FACTORY_H_
+#define SAMPLES_HELPERS_WIN32_COM_FACTORY_H_
+
+struct IXpsOMObjectFactory;
+
+// Factory for COM instances.
+class ComFactory final {
+ public:
+  ComFactory();
+  ~ComFactory();
+
+  IXpsOMObjectFactory* GetXpsOMObjectFactory();
+
+ private:
+  bool Initialize();
+
+  bool initialized_ = false;
+  IXpsOMObjectFactory* xps_om_object_factory_ = nullptr;
+};
+
+#endif  // SAMPLES_HELPERS_WIN32_COM_FACTORY_H_
diff --git a/samples/helpers/write.cc b/samples/helpers/write.cc
index aeeac25..13223d2 100644
--- a/samples/helpers/write.cc
+++ b/samples/helpers/write.cc
@@ -595,16 +595,43 @@
 #endif  // _WIN32
 
 #ifdef PDF_ENABLE_SKIA
-std::string WriteSkp(const char* pdf_name, int num, const SkPicture& picture) {
-  std::string filename = GeneratePageOutputFilename(pdf_name, num, "skp");
+std::unique_ptr<SkWStream> WriteToSkWStream(const std::string& pdf_name,
+                                            int num,
+                                            const std::string& extension) {
+  std::string discarded_filename;
+  return WriteToSkWStream(pdf_name, num, extension, discarded_filename);
+}
+
+std::unique_ptr<SkWStream> WriteToSkWStream(const std::string& pdf_name,
+                                            int num,
+                                            const std::string& extension,
+                                            std::string& filename) {
+  filename =
+      GeneratePageOutputFilename(pdf_name.c_str(), num, extension.c_str());
   if (filename.empty()) {
-    return filename;
+    return nullptr;
   }
-  SkFILEWStream wStream(filename.c_str());
-  picture.serialize(&wStream);
+
+  auto stream = std::make_unique<SkFILEWStream>(filename.c_str());
+  if (!stream->isValid()) {
+    return nullptr;
+  }
+
+  return stream;
+}
+
+std::string WriteSkp(const char* pdf_name, int num, const SkPicture& picture) {
+  std::string filename;
+  std::unique_ptr<SkWStream> stream =
+      WriteToSkWStream(pdf_name, num, "skp", filename);
+  if (!stream) {
+    return "";
+  }
+
+  picture.serialize(stream.get());
   return filename;
 }
-#endif
+#endif  // PDF_ENABLE_SKIA
 
 enum class ThumbnailDecodeType { kBitmap, kRawStream, kDecodedStream };
 
diff --git a/samples/helpers/write.h b/samples/helpers/write.h
index 20212ad..b23760e 100644
--- a/samples/helpers/write.h
+++ b/samples/helpers/write.h
@@ -5,13 +5,15 @@
 #ifndef SAMPLES_HELPERS_WRITE_H_
 #define SAMPLES_HELPERS_WRITE_H_
 
+#include <memory>
 #include <string>
 
 #include "public/fpdfview.h"
 
 #ifdef PDF_ENABLE_SKIA
 class SkPicture;
-#endif
+class SkWStream;
+#endif  // PDF_ENABLE_SKIA
 
 std::string WritePpm(const char* pdf_name,
                      int num,
@@ -40,6 +42,13 @@
 #endif  // _WIN32
 
 #ifdef PDF_ENABLE_SKIA
+std::unique_ptr<SkWStream> WriteToSkWStream(const std::string& pdf_name,
+                                            int num,
+                                            const std::string& extension);
+std::unique_ptr<SkWStream> WriteToSkWStream(const std::string& pdf_name,
+                                            int num,
+                                            const std::string& extension,
+                                            std::string& filename);
 std::string WriteSkp(const char* pdf_name, int num, const SkPicture& picture);
 #endif  // PDF_ENABLE_SKIA
 
diff --git a/samples/pdfium_test.cc b/samples/pdfium_test.cc
index e724ba2..1dfc70c 100644
--- a/samples/pdfium_test.cc
+++ b/samples/pdfium_test.cc
@@ -52,6 +52,7 @@
 #include <io.h>
 #include <wingdi.h>
 
+#include "samples/helpers/win32/com_factory.h"
 #include "third_party/base/win/scoped_select_object.h"
 #else
 #include <unistd.h>
@@ -64,12 +65,18 @@
 #ifdef PDF_ENABLE_SKIA
 #include "third_party/skia/include/core/SkCanvas.h"           // nogncheck
 #include "third_party/skia/include/core/SkColor.h"            // nogncheck
+#include "third_party/skia/include/core/SkDocument.h"         // nogncheck
 #include "third_party/skia/include/core/SkPicture.h"          // nogncheck
 #include "third_party/skia/include/core/SkPictureRecorder.h"  // nogncheck
 #include "third_party/skia/include/core/SkPixmap.h"           // nogncheck
 #include "third_party/skia/include/core/SkRefCnt.h"           // nogncheck
+#include "third_party/skia/include/core/SkStream.h"           // nogncheck
 #include "third_party/skia/include/core/SkSurface.h"          // nogncheck
 
+#ifdef _WIN32
+#include "third_party/skia/include/docs/SkXPSDocument.h"  // nogncheck
+#endif
+
 #ifdef BUILD_WITH_CHROMIUM
 #include "samples/chromium_support/discardable_memory_allocator.h"  // nogncheck
 #endif
@@ -130,7 +137,10 @@
 #endif
 #ifdef PDF_ENABLE_SKIA
   kSkp,
-#endif
+#ifdef _WIN32
+  kXps,
+#endif  // _WIN32
+#endif  // PDF_ENABLE_SKIA
 };
 
 struct Options {
@@ -578,6 +588,14 @@
         return false;
       }
       options->output_format = OutputFormat::kSkp;
+#ifdef _WIN32
+    } else if (cur_arg == "--xps") {
+      if (options->output_format != OutputFormat::kNone) {
+        fprintf(stderr, "Duplicate or conflicting --xps argument\n");
+        return false;
+      }
+      options->output_format = OutputFormat::kXps;
+#endif  // _WIN32
 #endif  // PDF_ENABLE_SKIA
     } else if (ParseSwitchKeyValue(cur_arg, "--font-dir=", &value)) {
       if (!options->font_directory.empty()) {
@@ -795,6 +813,10 @@
   const Options& options() const { return *options_; }
   const std::function<void()>& idler() const { return *idler_; }
 
+#ifdef _WIN32
+  ComFactory& com_factory() { return com_factory_; }
+#endif  // _WIN32
+
   // Invokes `idler()`.
   void Idle() const { idler()(); }
 
@@ -806,6 +828,10 @@
  private:
   const Options* options_;
   const std::function<void()>* idler_;
+
+#ifdef _WIN32
+  ComFactory com_factory_;
+#endif  // _WIN32
 };
 
 class PdfProcessor final {
@@ -837,6 +863,10 @@
   const Options& options() const { return processor_->options(); }
   const std::function<void()>& idler() const { return processor_->idler(); }
 
+#ifdef _WIN32
+  ComFactory& com_factory() { return processor_->com_factory(); }
+#endif  // _WIN32
+
   // Per PDF state.
   const std::string& name() const { return *name_; }
   const std::string& events() const { return *events_; }
@@ -1183,33 +1213,42 @@
 #endif  // _WIN32
 
 #ifdef PDF_ENABLE_SKIA
-class SkPicturePageRenderer : public PageRenderer {
+class SkCanvasPageRenderer : public PageRenderer {
+ public:
+  bool Start() override {
+    FPDF_RenderPageSkia(reinterpret_cast<FPDF_SKIA_CANVAS>(canvas()), page(),
+                        width(), height());
+    return true;
+  }
+
+  void Finish(FPDF_FORMHANDLE form) override {
+    FPDF_FFLDrawSkia(form, reinterpret_cast<FPDF_SKIA_CANVAS>(canvas()), page(),
+                     /*start_x=*/0, /*start_y=*/0, width(), height(),
+                     /*rotate=*/0, flags());
+  }
+
+ protected:
+  SkCanvasPageRenderer(FPDF_PAGE page, int width, int height, int flags)
+      : PageRenderer(page, width, height, flags) {}
+
+  virtual SkCanvas* canvas() = 0;
+};
+
+class SkPicturePageRenderer final : public SkCanvasPageRenderer {
  public:
   SkPicturePageRenderer(FPDF_PAGE page, int width, int height, int flags)
-      : PageRenderer(page,
-                     /*width=*/width,
-                     /*height=*/height,
-                     /*flags=*/flags) {}
+      : SkCanvasPageRenderer(page, width, height, flags) {}
 
   bool HasOutput() const override { return !!picture_; }
 
   bool Start() override {
     recorder_ = std::make_unique<SkPictureRecorder>();
     recorder_->beginRecording(width(), height());
-
-    FPDF_RenderPageSkia(
-        reinterpret_cast<FPDF_SKIA_CANVAS>(recorder_->getRecordingCanvas()),
-        page(), /*size_x=*/width(), /*size_y=*/height());
-    return true;
+    return SkCanvasPageRenderer::Start();
   }
 
   void Finish(FPDF_FORMHANDLE form) override {
-    FPDF_FFLDrawSkia(
-        form,
-        reinterpret_cast<FPDF_SKIA_CANVAS>(recorder_->getRecordingCanvas()),
-        page(), /*start_x=*/0, /*start_y=*/0, /*size_x=*/width(),
-        /*size_y=*/height(), /*rotate=*/0, /*flags=*/0);
-
+    SkCanvasPageRenderer::Finish(form);
     picture_ = recorder_->finishRecordingAsPicture();
     recorder_.reset();
   }
@@ -1242,10 +1281,80 @@
     return true;
   }
 
+ protected:
+  SkCanvas* canvas() override { return recorder_->getRecordingCanvas(); }
+
  private:
   std::unique_ptr<SkPictureRecorder> recorder_;
   sk_sp<SkPicture> picture_;
 };
+
+class SkDocumentPageRenderer final : public SkCanvasPageRenderer {
+ public:
+  SkDocumentPageRenderer(std::unique_ptr<SkWStream> stream,
+                         sk_sp<SkDocument> document,
+                         FPDF_PAGE page,
+                         int width,
+                         int height,
+                         int flags)
+      : SkCanvasPageRenderer(page, width, height, flags),
+        stream_(std::move(stream)),
+        document_(std::move(document)) {
+    DCHECK(stream_);
+    DCHECK(document_);
+  }
+
+  bool HasOutput() const override { return has_output_; }
+
+  bool Start() override {
+    if (!document_) {
+      return false;
+    }
+
+    DCHECK(!canvas_);
+    canvas_ = document_->beginPage(width(), height());
+    if (!canvas_) {
+      return false;
+    }
+
+    return SkCanvasPageRenderer::Start();
+  }
+
+  void Finish(FPDF_FORMHANDLE form) override {
+    SkCanvasPageRenderer::Finish(form);
+
+    DCHECK(canvas_);
+    canvas_ = nullptr;
+    document_->endPage();
+
+    has_output_ = true;
+  }
+
+  bool Write(const std::string& /*name*/,
+             int /*page_index*/,
+             bool /*md5*/) override {
+    bool success = HasOutput();
+    if (success) {
+      document_->close();
+    } else {
+      document_->abort();
+    }
+
+    document_.reset();
+    stream_.reset();
+    return success;
+  }
+
+ protected:
+  SkCanvas* canvas() override { return canvas_; }
+
+ private:
+  std::unique_ptr<SkWStream> stream_;
+  sk_sp<SkDocument> document_;
+
+  SkCanvas* canvas_ = nullptr;
+  bool has_output_ = false;
+};
 #endif  // PDF_ENABLE_SKIA
 
 bool PdfProcessor::ProcessPage(const int page_index) {
@@ -1332,6 +1441,31 @@
       renderer = std::make_unique<SkPicturePageRenderer>(
           page, /*width=*/width, /*height=*/height, /*flags=*/flags);
       break;
+
+#ifdef _WIN32
+    case OutputFormat::kXps: {
+      IXpsOMObjectFactory* xps_factory = com_factory().GetXpsOMObjectFactory();
+      if (!xps_factory) {
+        break;
+      }
+
+      std::unique_ptr<SkWStream> stream =
+          WriteToSkWStream(name(), page_index, "xps");
+      if (!stream) {
+        break;
+      }
+
+      sk_sp<SkDocument> document =
+          SkXPS::MakeDocument(stream.get(), xps_factory);
+      if (!document) {
+        break;
+      }
+
+      renderer = std::make_unique<SkDocumentPageRenderer>(
+          std::move(stream), std::move(document), page, width, height, flags);
+      break;
+    }
+#endif  // _WIN32
 #endif  // PDF_ENABLE_SKIA
 
     default:
@@ -1674,7 +1808,10 @@
     "  --annot - write annotation info <pdf-name>.<page-number>.annot.txt\n"
 #ifdef PDF_ENABLE_SKIA
     "  --skp   - write page images <pdf-name>.<page-number>.skp\n"
-#endif
+#ifdef _WIN32
+    "  --xps   - write page images <pdf-name>.<page-number>.xps\n"
+#endif  // _WIN32
+#endif  // PDF_ENABLE_SKIA
     "  --md5   - write output image paths and their md5 hashes to stdout.\n"
     "  --time=<number> - Seconds since the epoch to set system time.\n"
     "";
diff --git a/skia/BUILD.gn b/skia/BUILD.gn
index bcb7e07..afdd734 100644
--- a/skia/BUILD.gn
+++ b/skia/BUILD.gn
@@ -174,6 +174,7 @@
   sources += skia_sksl_gpu_sources
   sources += skia_sksl_sources
   sources += skia_utils_private
+  sources += skia_xps_sources
   sources += skia_codec_core
   sources += skia_codec_decode_bmp
   sources += skia_encode_srcs
@@ -213,13 +214,15 @@
     "//third_party/skia/src/utils/SkParsePath.cpp",
   ]
 
+  if (is_win) {
+    libs = [ "fontsub.lib" ]
+  }
+
   # need separate win section to handle chromes auto gn filter
   # (build/config/BUILDCONFIG.gn)
   if (is_win) {
     sources -= [
       #windows
-      "//third_party/skia/src/utils/win/SkAutoCoInitialize.cpp",
-      "//third_party/skia/src/utils/win/SkIStream.cpp",
       "//third_party/skia/src/utils/win/SkWGL_win.cpp",
     ]
   }