Add experimental FPDFText_SetCharcodes() API.

Instead of calling FPDFText_SetText() and letting it convert the input
text characters to charcodes, allow the caller to set the charcodes
directly.

Bug: pdfium:1705
Change-Id: I1368def473f76e829b622aa75bd11efaad193cf6
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/83491
Commit-Queue: Lei Zhang <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
diff --git a/fpdfsdk/fpdf_edit_embeddertest.cpp b/fpdfsdk/fpdf_edit_embeddertest.cpp
index 4ea5c14..350d655 100644
--- a/fpdfsdk/fpdf_edit_embeddertest.cpp
+++ b/fpdfsdk/fpdf_edit_embeddertest.cpp
@@ -250,7 +250,7 @@
 }  // namespace
 
 TEST_F(FPDFEditEmbedderTest, EmbedNotoSansSCFont) {
-  EXPECT_TRUE(CreateEmptyDocument());
+  ASSERT_TRUE(CreateEmptyDocument());
   ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400));
   std::string font_path;
   ASSERT_TRUE(PathService::GetThirdPartyFilePath(
@@ -259,7 +259,7 @@
   size_t file_length = 0;
   std::unique_ptr<char, pdfium::FreeDeleter> font_data =
       GetFileContents(font_path.c_str(), &file_length);
-  DCHECK(font_data);
+  ASSERT_TRUE(font_data);
 
   ScopedFPDFFont font(FPDFText_LoadFont(
       document(), reinterpret_cast<const uint8_t*>(font_data.get()),
@@ -280,20 +280,69 @@
 #if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
 #if defined(OS_APPLE)
   const char kChecksum[] = "9a31fb87d1c6d2346bba22d1196041cd";
-#else
+#else   // defined(OS_APPLE)
   const char kChecksum[] = "5bb65e15fc0a685934cd5006dec08a76";
 #endif  // defined(OS_APPLE)
-#else
+#else   // defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
 #if defined(OS_WIN)
   const char kChecksum[] = "89e8eef5d6ad18c542a92a0519954d0f";
-#else
+#else  // defined(OS_WIN)
   const char kChecksum[] = "9a31fb87d1c6d2346bba22d1196041cd";
 #endif  // defined(OS_WIN)
 #endif  // defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
   ScopedFPDFBitmap page_bitmap = RenderPage(page.get());
   CompareBitmap(page_bitmap.get(), 400, 400, kChecksum);
 
-  EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+  ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+  VerifySavedDocument(400, 400, kChecksum);
+}
+
+TEST_F(FPDFEditEmbedderTest, EmbedNotoSansSCFontWithCharcodes) {
+  ASSERT_TRUE(CreateEmptyDocument());
+  ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400));
+  std::string font_path;
+  ASSERT_TRUE(PathService::GetThirdPartyFilePath(
+      "NotoSansCJK/NotoSansSC-Regular.subset.otf", &font_path));
+
+  size_t file_length = 0;
+  std::unique_ptr<char, pdfium::FreeDeleter> font_data =
+      GetFileContents(font_path.c_str(), &file_length);
+  ASSERT_TRUE(font_data);
+
+  ScopedFPDFFont font(FPDFText_LoadFont(
+      document(), reinterpret_cast<const uint8_t*>(font_data.get()),
+      file_length, FPDF_FONT_TRUETYPE, /*cid=*/true));
+  FPDF_PAGEOBJECT text_object =
+      FPDFPageObj_CreateTextObj(document(), font.get(), 20.0f);
+  EXPECT_TRUE(text_object);
+
+  // Same as `text` in the EmbedNotoSansSCFont test case above.
+  const std::vector<uint32_t> charcodes = {9, 6, 7, 3, 5, 2, 1,
+                                           9, 6, 7, 4, 8, 2};
+  EXPECT_TRUE(
+      FPDFText_SetCharcodes(text_object, charcodes.data(), charcodes.size()));
+
+  FPDFPageObj_Transform(text_object, 1, 0, 0, 1, 50, 200);
+  FPDFPage_InsertObject(page.get(), text_object);
+  EXPECT_TRUE(FPDFPage_GenerateContent(page.get()));
+
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+#if defined(OS_APPLE)
+  const char kChecksum[] = "9a31fb87d1c6d2346bba22d1196041cd";
+#else   // defined(OS_APPLE)
+  const char kChecksum[] = "5bb65e15fc0a685934cd5006dec08a76";
+#endif  // defined(OS_APPLE)
+#else   // defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+#if defined(OS_WIN)
+  const char kChecksum[] = "89e8eef5d6ad18c542a92a0519954d0f";
+#else   // defined(OS_WIN)
+  const char kChecksum[] = "9a31fb87d1c6d2346bba22d1196041cd";
+#endif  // defined(OS_WIN)
+#endif  // defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+  ScopedFPDFBitmap page_bitmap = RenderPage(page.get());
+  CompareBitmap(page_bitmap.get(), 400, 400, kChecksum);
+
+  ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
   VerifySavedDocument(400, 400, kChecksum);
 }
 
@@ -749,6 +798,25 @@
   CloseSavedDocument();
 }
 
+TEST_F(FPDFEditEmbedderTest, SetCharcodesBadParams) {
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  ASSERT_EQ(2, FPDFPage_CountObjects(page));
+  FPDF_PAGEOBJECT page_object = FPDFPage_GetObject(page, 0);
+  ASSERT_TRUE(page_object);
+
+  const uint32_t kDummyValue = 42;
+  EXPECT_FALSE(FPDFText_SetCharcodes(nullptr, nullptr, 0));
+  EXPECT_FALSE(FPDFText_SetCharcodes(nullptr, nullptr, 1));
+  EXPECT_FALSE(FPDFText_SetCharcodes(nullptr, &kDummyValue, 0));
+  EXPECT_FALSE(FPDFText_SetCharcodes(nullptr, &kDummyValue, 1));
+  EXPECT_FALSE(FPDFText_SetCharcodes(page_object, nullptr, 1));
+
+  UnloadPage(page);
+}
+
 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 0f4b7a0..c7fb03c 100644
--- a/fpdfsdk/fpdf_edittext.cpp
+++ b/fpdfsdk/fpdf_edittext.cpp
@@ -482,6 +482,27 @@
   return true;
 }
 
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFText_SetCharcodes(FPDF_PAGEOBJECT text_object,
+                      const uint32_t* charcodes,
+                      size_t count) {
+  CPDF_TextObject* pTextObj = CPDFTextObjectFromFPDFPageObject(text_object);
+  if (!pTextObj)
+    return false;
+
+  if (!charcodes && count)
+    return false;
+
+  ByteString byte_text;
+  if (charcodes) {
+    for (size_t i = 0; i < count; ++i) {
+      pTextObj->GetFont()->AppendChar(&byte_text, charcodes[i]);
+    }
+  }
+  pTextObj->SetText(byte_text);
+  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 de6b15e..c1fdbfa 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -241,6 +241,7 @@
     CHK(FPDFTextObj_SetTextRenderMode);
     CHK(FPDFText_LoadFont);
     CHK(FPDFText_LoadStandardFont);
+    CHK(FPDFText_SetCharcodes);
     CHK(FPDFText_SetText);
     CHK(FPDF_CreateNewDocument);
 
diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h
index c0b5282..40bb11f 100644
--- a/public/fpdf_edit.h
+++ b/public/fpdf_edit.h
@@ -1093,7 +1093,7 @@
                        FPDF_BYTESTRING font,
                        float font_size);
 
-// Set the text for a textobject. If it had text, it will be replaced.
+// Set the text for a text object. If it had text, it will be replaced.
 //
 // text_object  - handle to the text object.
 // text         - the UTF-16LE encoded string containing the text to be added.
@@ -1102,6 +1102,20 @@
 FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
 FPDFText_SetText(FPDF_PAGEOBJECT text_object, FPDF_WIDESTRING text);
 
+// Experimental API.
+// Set the text using charcodes for a text object. If it had text, it will be
+// replaced.
+//
+// text_object  - handle to the text object.
+// charcodes    - pointer to an array of charcodes to be added.
+// count        - number of elements in |charcodes|.
+//
+// Returns TRUE on success
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFText_SetCharcodes(FPDF_PAGEOBJECT text_object,
+                      const uint32_t* charcodes,
+                      size_t count);
+
 // Returns a font object loaded from a stream of data. The font is loaded
 // into the document.
 //