Add experimental FPDFText_SetPositions() API

Add a new API so embedders can adjust the individual glyph positioning
in a text object. The API uses absolute positions to avoid precision
issues with float math that can happen with relative positions.

Bug: 491191097
Change-Id: I7e79df5a60e6f615ec7836ff873f20997886369c
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/144892
Commit-Queue: Lei Zhang <thestig@chromium.org>
Reviewed-by: Andy Phan <andyphan@chromium.org>
diff --git a/core/fpdfapi/page/cpdf_textobject.cpp b/core/fpdfapi/page/cpdf_textobject.cpp
index 09fb12d..68f4216 100644
--- a/core/fpdfapi/page/cpdf_textobject.cpp
+++ b/core/fpdfapi/page/cpdf_textobject.cpp
@@ -14,6 +14,7 @@
 #include "core/fxcrt/fx_coordinates.h"
 #include "core/fxcrt/span.h"
 #include "core/fxcrt/span_util.h"
+#include "core/fxcrt/zip.h"
 
 #define ISLATINWORD(u) (u != 0x20 && u <= 0x28FF)
 
@@ -157,6 +158,48 @@
   return this;
 }
 
+bool CPDF_TextObject::SetAbsolutePositions(
+    pdfium::span<const float> absolute_positions) {
+  if (char_codes_.size() <= 1) {
+    return false;
+  }
+
+  if (absolute_positions.size() != char_codes_.size() - 1) {
+    return false;
+  }
+
+  float font_size = GetFontSize();
+  if (font_size == 0.0f) {
+    return false;
+  }
+  font_size /= 1000;
+
+  RetainPtr<CPDF_Font> font = GetFont();
+  const CPDF_CIDFont* cid_font = font->AsCIDFont();
+  const float char_space = text_state().GetCharSpace();
+  const float word_space = text_state().GetWordSpace();
+
+  // Re-calculate `char_kernings_` based on `absolute_positions`. Then let
+  // CalcPositionDataInternal() re-calculate `char_positions_`.
+  auto char_codes = pdfium::span(char_codes_).first(absolute_positions.size());
+  auto char_kernings =
+      pdfium::span(char_kernings_).first(absolute_positions.size());
+  float current_position = 0;
+  for (auto [char_code, next_position, kerning] :
+       fxcrt::Zip(char_codes, absolute_positions, char_kernings)) {
+    current_position += GetCharWidth(char_code);
+    current_position += GetWordSpaceIfNeeded(cid_font, char_code, word_space);
+    current_position += char_space;
+
+    kerning = (current_position - next_position) / font_size;
+    current_position = next_position;
+  }
+  char_kernings_.back() = 0;
+
+  CalcPositionDataInternal(font);
+  return true;
+}
+
 CFX_Matrix CPDF_TextObject::GetTextMatrix() const {
   pdfium::span<const float, 4> text_matrix = text_state().GetMatrix();
   return CFX_Matrix(text_matrix[0], text_matrix[2], text_matrix[1],
diff --git a/core/fpdfapi/page/cpdf_textobject.h b/core/fpdfapi/page/cpdf_textobject.h
index 768bc75..af9d854 100644
--- a/core/fpdfapi/page/cpdf_textobject.h
+++ b/core/fpdfapi/page/cpdf_textobject.h
@@ -69,7 +69,11 @@
   const std::vector<float>& GetCharKernings() const { return char_kernings_; }
   const std::vector<float>& GetCharPositions() const { return char_positions_; }
 
-  // Caller is expected to call SetDirty(true) when done changing the object.
+  // Caller is expected to call SetDirty(true) when done changing the object
+  // using these setters.
+  //
+  // `absolute_positions` must be one less than the `char_codes_` size.
+  bool SetAbsolutePositions(pdfium::span<const float> absolute_positions);
   void SetTextMatrix(const CFX_Matrix& matrix);
 
   void SetSegments(pdfium::span<const ByteString> strings,
diff --git a/fpdfsdk/fpdf_edit_embeddertest.cpp b/fpdfsdk/fpdf_edit_embeddertest.cpp
index 9907a7c..63c7ecd 100644
--- a/fpdfsdk/fpdf_edit_embeddertest.cpp
+++ b/fpdfsdk/fpdf_edit_embeddertest.cpp
@@ -800,6 +800,241 @@
   EXPECT_FALSE(FPDFText_SetText(nullptr, hi_text.get()));
 }
 
+TEST_F(FPDFEditEmbedderTest, SetPositions) {
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  // Get the "Hello, world!" text object and change its character spacing.
+  ASSERT_EQ(2, FPDFPage_CountObjects(page.get()));
+  FPDF_PAGEOBJECT page_object = FPDFPage_GetObject(page.get(), 0);
+  ASSERT_TRUE(page_object);
+  static constexpr auto kAbsolutePositions = std::to_array<const float>({
+      9.863999f,
+      15.191999f,
+      20.927999f,
+      24.264000f,
+      28.464001f,
+      31.464001f,
+      34.464001f,
+      43.127998f,
+      49.127998f,
+      53.123997f,
+      56.459995f,
+      68.459991f,
+  });
+  ASSERT_TRUE(FPDFText_SetPositions(page_object, kAbsolutePositions.data(),
+                                    kAbsolutePositions.size()));
+
+  // Remove the other text object, just like in the Bug410996566 test case.
+  {
+    ScopedFPDFPageObject obj(FPDFPage_GetObject(page.get(), 1));
+    ASSERT_TRUE(FPDFPage_RemoveObject(page.get(), obj.get()));
+  }
+  ASSERT_TRUE(FPDFPage_GenerateContent(page.get()));
+
+  static constexpr char kExpected[] = "bug_410996566";
+  {
+    ScopedFPDFBitmap bitmap = RenderPage(page.get());
+    CompareBitmapWithExpectationSuffix(bitmap.get(), kExpected);
+  }
+
+  ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+  VerifySavedDocumentWithExpectationSuffix(kExpected);
+}
+
+TEST_F(FPDFEditEmbedderTest, SetPositionsZeroFontSize) {
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  FPDF_PAGEOBJECT text_object_size_zero =
+      FPDFPageObj_NewTextObj(document(), "Arial", 0.0f);
+  ASSERT_TRUE(text_object_size_zero);
+  FPDFPage_InsertObject(page.get(), text_object_size_zero);
+
+  static constexpr auto kCharCodes = std::to_array<const uint32_t>({
+      'H',
+      'e',
+      'l',
+      'l',
+      'o',
+  });
+  ASSERT_TRUE(FPDFText_SetCharcodes(text_object_size_zero, kCharCodes.data(),
+                                    kCharCodes.size()));
+
+  static constexpr auto kAbsolutePositions = std::to_array<const float>({
+      1.0f,
+      2.0f,
+      3.0f,
+      4.0f,
+  });
+  // This fails because the font size is 0.
+  EXPECT_FALSE(FPDFText_SetPositions(text_object_size_zero,
+                                     kAbsolutePositions.data(),
+                                     kAbsolutePositions.size()));
+}
+
+TEST_F(FPDFEditEmbedderTest, SetPositionsVertical) {
+  ASSERT_TRUE(OpenDocument("vertical_text.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  ASSERT_EQ(3, FPDFPage_CountObjects(page.get()));
+  FPDF_PAGEOBJECT page_object = FPDFPage_GetObject(page.get(), 0);
+  ASSERT_TRUE(page_object);
+
+  static constexpr auto kAbsolutePositions = std::to_array<const float>({
+      -12.216003f,
+      -19.692005f,
+      -30.708008f,
+      -44.124011f,
+      -52.800013f,
+  });
+  ASSERT_TRUE(FPDFText_SetPositions(page_object, kAbsolutePositions.data(),
+                                    kAbsolutePositions.size()));
+
+  static constexpr char kExpected[] = "vertical_text_positioned";
+  {
+    ScopedFPDFBitmap bitmap = RenderPage(page.get());
+    CompareBitmapWithFuzzyExpectationSuffix(bitmap.get(), kExpected);
+  }
+  ASSERT_TRUE(FPDFPage_GenerateContent(page.get()));
+
+  ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+  VerifySavedDocumentWithFuzzyExpectationSuffix(kExpected);
+}
+
+TEST_F(FPDFEditEmbedderTest, SetPositionsBengali2) {
+  CreateEmptyDocument();
+  ScopedFPDFPage page(FPDFPage_New(document(), 0, 200, 200));
+
+  std::string font_path = PathService::GetThirdPartyFilePath(
+      "NotoSansBengali2/NotoSansBengali-Regular.subset.ttf");
+  ASSERT_FALSE(font_path.empty());
+  std::vector<uint8_t> font_data = GetFileContents(font_path.c_str());
+  ASSERT_FALSE(font_data.empty());
+
+  ScopedFPDFFont font(FPDFText_LoadFont(document(), font_data.data(),
+                                        font_data.size(), FPDF_FONT_TRUETYPE,
+                                        /*cid=*/true));
+  ASSERT_TRUE(font);
+
+  static constexpr float kFontSize = 20.0f;
+
+  // Calculated by HarfBuzz for "পরিকল্পনা". Then normalize to points and summed
+  // up as absolute values.
+  static constexpr auto kCharCodes =
+      std::to_array<const uint32_t>({3, 9, 5, 1, 30, 2, 8});
+  static constexpr auto kAbsolutePositions = std::to_array<const float>(
+      {14.32f, 19.64f, 31.56f, 47.7f, 62.66f, 74.74f});
+
+  ScopedFPDFPageObject text_object(
+      FPDFPageObj_CreateTextObj(document(), font.get(), kFontSize));
+  ASSERT_TRUE(text_object);
+  EXPECT_TRUE(FPDFText_SetCharcodes(text_object.get(), kCharCodes.data(),
+                                    kCharCodes.size()));
+  EXPECT_TRUE(FPDFText_SetPositions(
+      text_object.get(), kAbsolutePositions.data(), kAbsolutePositions.size()));
+
+  static constexpr FS_MATRIX kMatrix{1.0f, 0.0f, 0.0f, 1.0f, 40.0f, 60.0f};
+  ASSERT_TRUE(FPDFPageObj_TransformF(text_object.get(), &kMatrix));
+  EXPECT_TRUE(FPDFPage_InsertObject(page.get(), text_object.release()));
+
+  EXPECT_TRUE(FPDFPage_GenerateContent(page.get()));
+
+  static constexpr char kExpected[] = "set_positions_bengali2";
+  ScopedFPDFBitmap page_bitmap = RenderPage(page.get());
+  CompareBitmapWithExpectationSuffix(page_bitmap.get(), kExpected);
+
+  ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+  VerifySavedDocumentWithExpectationSuffix(kExpected);
+}
+
+TEST_F(FPDFEditEmbedderTest, SetPositionsBengali3) {
+  CreateEmptyDocument();
+  ScopedFPDFPage page(FPDFPage_New(document(), 0, 200, 200));
+
+  std::string font_path = PathService::GetThirdPartyFilePath(
+      "NotoSansBengali3/NotoSansBengali-Regular.subset.ttf");
+  ASSERT_FALSE(font_path.empty());
+  std::vector<uint8_t> font_data = GetFileContents(font_path.c_str());
+  ASSERT_FALSE(font_data.empty());
+
+  ScopedFPDFFont font(FPDFText_LoadFont(document(), font_data.data(),
+                                        font_data.size(), FPDF_FONT_TRUETYPE,
+                                        /*cid=*/true));
+  ASSERT_TRUE(font);
+
+  static constexpr float kFontSize = 20.0f;
+
+  // Calculated by HarfBuzz for "পরিকল্পনা". Then normalize to points and summed
+  // up as absolute values.
+  static constexpr auto kCharCodes1 =
+      std::to_array<const uint32_t>({4, 10, 6, 2, 21, 24});
+  static constexpr auto kCharCodes2 = std::to_array<const uint32_t>({36});
+  static constexpr auto kCharCodes3 = std::to_array<const uint32_t>({3, 9});
+  static constexpr FS_POINTF kStartPosition1{40.0f, 60.0f};
+  static constexpr auto kAbsolutePositions1 =
+      std::to_array<const float>({14.32f, 19.64f, 31.56f, 47.7f, 55.74f});
+  static constexpr FS_POINTF kStartPosition2{88.74f, 58.4f};
+  static constexpr FS_POINTF kStartPosition3{101.02f, 60.0f};
+  static constexpr auto kAbsolutePositions3 =
+      std::to_array<const float>({12.04f});
+
+  {
+    ScopedFPDFPageObject text_object(
+        FPDFPageObj_CreateTextObj(document(), font.get(), kFontSize));
+    ASSERT_TRUE(text_object);
+    EXPECT_TRUE(FPDFText_SetCharcodes(text_object.get(), kCharCodes1.data(),
+                                      kCharCodes1.size()));
+    EXPECT_TRUE(FPDFText_SetPositions(text_object.get(),
+                                      kAbsolutePositions1.data(),
+                                      kAbsolutePositions1.size()));
+
+    static constexpr FS_MATRIX kMatrix{
+        1.0f, 0.0f, 0.0f, 1.0f, kStartPosition1.x, kStartPosition1.y};
+    ASSERT_TRUE(FPDFPageObj_TransformF(text_object.get(), &kMatrix));
+    EXPECT_TRUE(FPDFPage_InsertObject(page.get(), text_object.release()));
+  }
+  {
+    ScopedFPDFPageObject text_object(
+        FPDFPageObj_CreateTextObj(document(), font.get(), kFontSize));
+    ASSERT_TRUE(text_object);
+    EXPECT_TRUE(FPDFText_SetCharcodes(text_object.get(), kCharCodes2.data(),
+                                      kCharCodes2.size()));
+
+    static constexpr FS_MATRIX kMatrix{
+        1.0f, 0.0f, 0.0f, 1.0f, kStartPosition2.x, kStartPosition2.y};
+    ASSERT_TRUE(FPDFPageObj_TransformF(text_object.get(), &kMatrix));
+    EXPECT_TRUE(FPDFPage_InsertObject(page.get(), text_object.release()));
+  }
+  {
+    ScopedFPDFPageObject text_object(
+        FPDFPageObj_CreateTextObj(document(), font.get(), kFontSize));
+    ASSERT_TRUE(text_object);
+    EXPECT_TRUE(FPDFText_SetCharcodes(text_object.get(), kCharCodes3.data(),
+                                      kCharCodes3.size()));
+    EXPECT_TRUE(FPDFText_SetPositions(text_object.get(),
+                                      kAbsolutePositions3.data(),
+                                      kAbsolutePositions3.size()));
+
+    static constexpr FS_MATRIX kMatrix{
+        1.0f, 0.0f, 0.0f, 1.0f, kStartPosition3.x, kStartPosition3.y};
+    ASSERT_TRUE(FPDFPageObj_TransformF(text_object.get(), &kMatrix));
+    EXPECT_TRUE(FPDFPage_InsertObject(page.get(), text_object.release()));
+  }
+
+  EXPECT_TRUE(FPDFPage_GenerateContent(page.get()));
+
+  static constexpr char kExpected[] = "set_positions_bengali3";
+  ScopedFPDFBitmap page_bitmap = RenderPage(page.get());
+  CompareBitmapWithFuzzyExpectationSuffix(page_bitmap.get(), kExpected);
+
+  ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+  VerifySavedDocumentWithExpectationSuffix(kExpected);
+}
+
 TEST_F(FPDFEditEmbedderTest, SetCharcodesBadParams) {
   ASSERT_TRUE(OpenDocument("hello_world.pdf"));
   ScopedPage page = LoadScopedPage(0);
@@ -819,6 +1054,52 @@
   EXPECT_FALSE(FPDFText_SetCharcodes(page_object, &kPlaceholderValue, 0));
 }
 
+TEST_F(FPDFEditEmbedderTest, SetPositionsBadParams) {
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  ASSERT_EQ(2, FPDFPage_CountObjects(page.get()));
+  FPDF_PAGEOBJECT page_object = FPDFPage_GetObject(page.get(), 0);
+  ASSERT_TRUE(page_object);
+
+  static constexpr auto kData = std::to_array<const float>(
+      {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f});
+  EXPECT_FALSE(FPDFText_SetPositions(nullptr, nullptr, 0));
+  EXPECT_FALSE(FPDFText_SetPositions(nullptr, nullptr, kData.size()));
+  EXPECT_FALSE(FPDFText_SetPositions(nullptr, kData.data(), 0));
+  EXPECT_FALSE(FPDFText_SetPositions(nullptr, kData.data(), kData.size()));
+  EXPECT_FALSE(FPDFText_SetPositions(page_object, nullptr, 1));
+
+  // "Hello, world!" has 13 characters, so it needs exactly 12 positions.
+  // Deliberately pass in the wrong positions count to make the API fail.
+  EXPECT_FALSE(FPDFText_SetPositions(page_object, kData.data(), 11));
+  EXPECT_FALSE(FPDFText_SetPositions(page_object, kData.data(), 13));
+
+  // `empty_text_object` has no characters, so passing in 12 positions fails.
+  ScopedFPDFPageObject empty_text_object(
+      FPDFPageObj_NewTextObj(document(), "Arial", 12.0f));
+  EXPECT_FALSE(FPDFText_SetPositions(empty_text_object.get(), kData.data(),
+                                     kData.size()));
+
+  // `one_char_text_object` has only 1 character, so there is no way for
+  // `FPDFText_SetPositions()` to succeed.
+  ScopedFPDFPageObject one_char_text_object(
+      FPDFPageObj_NewTextObj(document(), "Arial", 12.0f));
+  EXPECT_FALSE(FPDFText_SetPositions(empty_text_object.get(), kData.data(),
+                                     kData.size()));
+  static constexpr auto kCharcodes = std::to_array<const uint32_t>({88});
+  EXPECT_TRUE(FPDFText_SetCharcodes(one_char_text_object.get(),
+                                    kCharcodes.data(), kCharcodes.size()));
+  EXPECT_FALSE(FPDFText_SetPositions(one_char_text_object.get(), nullptr, 0));
+  EXPECT_FALSE(
+      FPDFText_SetPositions(one_char_text_object.get(), kData.data(), 0));
+  EXPECT_FALSE(
+      FPDFText_SetPositions(one_char_text_object.get(), kData.data(), 1));
+  EXPECT_FALSE(
+      FPDFText_SetPositions(one_char_text_object.get(), kData.data(), 2));
+}
+
 TEST_F(FPDFEditEmbedderTest, SetTextKeepClippingPath) {
   // Load document with some text, with parts clipped.
   ASSERT_TRUE(OpenDocument("bug_1558.pdf"));
diff --git a/fpdfsdk/fpdf_edittext.cpp b/fpdfsdk/fpdf_edittext.cpp
index 053a252..eb06dd7 100644
--- a/fpdfsdk/fpdf_edittext.cpp
+++ b/fpdfsdk/fpdf_edittext.cpp
@@ -414,6 +414,29 @@
   return true;
 }
 
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFText_SetPositions(FPDF_PAGEOBJECT text_object,
+                      const float* positions,
+                      size_t count) {
+  CPDF_TextObject* text_obj = CPDFTextObjectFromFPDFPageObject(text_object);
+  if (!text_obj) {
+    return false;
+  }
+
+  if (!positions && count) {
+    return false;
+  }
+
+  // SAFETY: required from caller.
+  auto positions_span = UNSAFE_BUFFERS(pdfium::span(positions, count));
+  if (!text_obj->SetAbsolutePositions(positions_span)) {
+    return false;
+  }
+
+  text_obj->SetDirty(true);
+  return true;
+}
+
 FPDF_EXPORT FPDF_FONT FPDF_CALLCONV FPDFText_LoadFont(FPDF_DOCUMENT document,
                                                       const uint8_t* data,
                                                       uint32_t size,
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index 10a7057..282512d 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -279,6 +279,7 @@
     CHK(FPDFText_LoadFont);
     CHK(FPDFText_LoadStandardFont);
     CHK(FPDFText_SetCharcodes);
+    CHK(FPDFText_SetPositions);
     CHK(FPDFText_SetText);
     CHK(FPDF_CreateNewDocument);
     CHK(FPDF_MovePages);
diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h
index 7c1f324..d723b81 100644
--- a/public/fpdf_edit.h
+++ b/public/fpdf_edit.h
@@ -1334,6 +1334,28 @@
                       const uint32_t* charcodes,
                       size_t count);
 
+// Experimental API.
+// Set the character positions for a text object.
+//
+// text_object  - handle to the text object.
+// positions    - pointer to an array of character positions to be set.
+// count        - number of elements in |positions|.
+//
+// The |positions| array specifies the position in points for each character
+// except the first one. The first character has an implied position value of 0.
+// All positions are relative to the origin of the text object. The direction is
+// either horizontal or vertical, depending on the direction of text in
+// |text_object|.
+//
+// For a text object with N characters, |count| must be N - 1. Therefore this
+// API fails when N <= 1.
+//
+// Returns TRUE on success.
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFText_SetPositions(FPDF_PAGEOBJECT text_object,
+                      const float* positions,
+                      size_t count);
+
 // Returns a font object loaded from a stream of data. The font is loaded
 // into the document. Various font data structures, such as the ToUnicode data,
 // are auto-generated based on the inputs.
diff --git a/testing/resources/embedder_tests/set_positions_bengali2.png b/testing/resources/embedder_tests/set_positions_bengali2.png
new file mode 100644
index 0000000..36b42b7
--- /dev/null
+++ b/testing/resources/embedder_tests/set_positions_bengali2.png
Binary files differ
diff --git a/testing/resources/embedder_tests/set_positions_bengali2_skia.png b/testing/resources/embedder_tests/set_positions_bengali2_skia.png
new file mode 100644
index 0000000..0adb5be
--- /dev/null
+++ b/testing/resources/embedder_tests/set_positions_bengali2_skia.png
Binary files differ
diff --git a/testing/resources/embedder_tests/set_positions_bengali2_skia_mac.png b/testing/resources/embedder_tests/set_positions_bengali2_skia_mac.png
new file mode 100644
index 0000000..36b42b7
--- /dev/null
+++ b/testing/resources/embedder_tests/set_positions_bengali2_skia_mac.png
Binary files differ
diff --git a/testing/resources/embedder_tests/set_positions_bengali2_skia_win.png b/testing/resources/embedder_tests/set_positions_bengali2_skia_win.png
new file mode 100644
index 0000000..e00a15f
--- /dev/null
+++ b/testing/resources/embedder_tests/set_positions_bengali2_skia_win.png
Binary files differ
diff --git a/testing/resources/embedder_tests/set_positions_bengali3.png b/testing/resources/embedder_tests/set_positions_bengali3.png
new file mode 100644
index 0000000..49a70e8
--- /dev/null
+++ b/testing/resources/embedder_tests/set_positions_bengali3.png
Binary files differ
diff --git a/testing/resources/embedder_tests/set_positions_bengali3_skia.png b/testing/resources/embedder_tests/set_positions_bengali3_skia.png
new file mode 100644
index 0000000..55beb2b
--- /dev/null
+++ b/testing/resources/embedder_tests/set_positions_bengali3_skia.png
Binary files differ
diff --git a/testing/resources/embedder_tests/set_positions_bengali3_skia_mac.png b/testing/resources/embedder_tests/set_positions_bengali3_skia_mac.png
new file mode 100644
index 0000000..b07d1d6
--- /dev/null
+++ b/testing/resources/embedder_tests/set_positions_bengali3_skia_mac.png
Binary files differ
diff --git a/testing/resources/embedder_tests/set_positions_bengali3_skia_mac_x86.png b/testing/resources/embedder_tests/set_positions_bengali3_skia_mac_x86.png
new file mode 100644
index 0000000..f3620cd
--- /dev/null
+++ b/testing/resources/embedder_tests/set_positions_bengali3_skia_mac_x86.png
Binary files differ
diff --git a/testing/resources/embedder_tests/set_positions_bengali3_skia_win.png b/testing/resources/embedder_tests/set_positions_bengali3_skia_win.png
new file mode 100644
index 0000000..c517861
--- /dev/null
+++ b/testing/resources/embedder_tests/set_positions_bengali3_skia_win.png
Binary files differ
diff --git a/testing/resources/embedder_tests/vertical_text_positioned.png b/testing/resources/embedder_tests/vertical_text_positioned.png
new file mode 100644
index 0000000..1d356fc
--- /dev/null
+++ b/testing/resources/embedder_tests/vertical_text_positioned.png
Binary files differ
diff --git a/testing/resources/embedder_tests/vertical_text_positioned_skia.png b/testing/resources/embedder_tests/vertical_text_positioned_skia.png
new file mode 100644
index 0000000..db6af7b
--- /dev/null
+++ b/testing/resources/embedder_tests/vertical_text_positioned_skia.png
Binary files differ
diff --git a/testing/resources/embedder_tests/vertical_text_positioned_skia_mac.png b/testing/resources/embedder_tests/vertical_text_positioned_skia_mac.png
new file mode 100644
index 0000000..40853b5
--- /dev/null
+++ b/testing/resources/embedder_tests/vertical_text_positioned_skia_mac.png
Binary files differ
diff --git a/testing/resources/embedder_tests/vertical_text_positioned_skia_win.png b/testing/resources/embedder_tests/vertical_text_positioned_skia_win.png
new file mode 100644
index 0000000..f9aa63f
--- /dev/null
+++ b/testing/resources/embedder_tests/vertical_text_positioned_skia_win.png
Binary files differ