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