Add API to set document language in PDF document

Add experimental API FPDFCatalog_SetLanguage() that allows the user to
set the /Lang entry in a PDF document's catalog. Make
CPDF_Document::SetRootForTesting() public so that it can be used in new
embedder tests without CPDF_TestDocument.

Bug: 42270891
Change-Id: I1ea20bf80f9045bd9fd34e55fcaf58cf82023d3c
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/123112
Commit-Queue: Andy Phan <andyphan@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h
index a9a2922..4ddd6da 100644
--- a/core/fpdfapi/parser/cpdf_document.h
+++ b/core/fpdfapi/parser/cpdf_document.h
@@ -161,10 +161,11 @@
   void IncrementParsedPageCount() { ++m_ParsedPageCount; }
   uint32_t GetParsedPageCountForTesting() { return m_ParsedPageCount; }
 
+  void SetRootForTesting(RetainPtr<CPDF_Dictionary> root);
+
  protected:
   void SetParser(std::unique_ptr<CPDF_Parser> pParser);
 
-  void SetRootForTesting(RetainPtr<CPDF_Dictionary> root);
   void ResizePageListForTesting(size_t size);
 
  private:
diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn
index b18ca03..abf055f 100644
--- a/fpdfsdk/BUILD.gn
+++ b/fpdfsdk/BUILD.gn
@@ -127,6 +127,7 @@
     "cpdfsdk_baannot_embeddertest.cpp",
     "fpdf_annot_embeddertest.cpp",
     "fpdf_attachment_embeddertest.cpp",
+    "fpdf_catalog_embeddertest.cpp",
     "fpdf_dataavail_embeddertest.cpp",
     "fpdf_doc_embeddertest.cpp",
     "fpdf_edit_embeddertest.cpp",
diff --git a/fpdfsdk/fpdf_catalog.cpp b/fpdfsdk/fpdf_catalog.cpp
index 6147de8..6153346 100644
--- a/fpdfsdk/fpdf_catalog.cpp
+++ b/fpdfsdk/fpdf_catalog.cpp
@@ -6,6 +6,8 @@
 
 #include "core/fpdfapi/parser/cpdf_dictionary.h"
 #include "core/fpdfapi/parser/cpdf_document.h"
+#include "core/fpdfapi/parser/cpdf_string.h"
+#include "core/fxcrt/retain_ptr.h"
 #include "fpdfsdk/cpdfsdk_helpers.h"
 
 FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
@@ -21,3 +23,23 @@
   RetainPtr<const CPDF_Dictionary> pMarkInfo = pCatalog->GetDictFor("MarkInfo");
   return pMarkInfo && pMarkInfo->GetIntegerFor("Marked") != 0;
 }
+
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFCatalog_SetLanguage(FPDF_DOCUMENT document, FPDF_BYTESTRING language) {
+  if (!language) {
+    return false;
+  }
+
+  CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document);
+  if (!doc) {
+    return false;
+  }
+
+  RetainPtr<CPDF_Dictionary> catalog = doc->GetMutableRoot();
+  if (!catalog) {
+    return false;
+  }
+
+  catalog->SetNewFor<CPDF_String>("Lang", language);
+  return true;
+}
diff --git a/fpdfsdk/fpdf_catalog_embeddertest.cpp b/fpdfsdk/fpdf_catalog_embeddertest.cpp
new file mode 100644
index 0000000..ae8e697
--- /dev/null
+++ b/fpdfsdk/fpdf_catalog_embeddertest.cpp
@@ -0,0 +1,68 @@
+// Copyright 2024 The PDFium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "public/fpdf_catalog.h"
+
+#include "core/fpdfapi/parser/cpdf_dictionary.h"
+#include "core/fpdfapi/parser/cpdf_document.h"
+#include "core/fpdfapi/parser/cpdf_string.h"
+#include "fpdfsdk/cpdfsdk_helpers.h"
+#include "public/fpdf_edit.h"
+#include "testing/embedder_test.h"
+
+using FPDFCatalogTest = EmbedderTest;
+
+TEST_F(FPDFCatalogTest, SetLanguageInvalidDocument) {
+  // Document cannot be nullptr.
+  EXPECT_FALSE(FPDFCatalog_SetLanguage(nullptr, "en-US"));
+
+  ScopedFPDFDocument doc(FPDF_CreateNewDocument());
+  CPDF_Document* cpdf_doc = CPDFDocumentFromFPDFDocument(doc.get());
+
+  // Language cannot be null.
+  ASSERT_TRUE(cpdf_doc->GetRoot());
+  EXPECT_FALSE(FPDFCatalog_SetLanguage(doc.get(), nullptr));
+
+  // Catalog cannot be nullptr.
+  cpdf_doc->SetRootForTesting(nullptr);
+  EXPECT_FALSE(FPDFCatalog_SetLanguage(doc.get(), "en-US"));
+}
+
+TEST_F(FPDFCatalogTest, SetLanguageNewDocument) {
+  ScopedFPDFDocument doc(FPDF_CreateNewDocument());
+
+  const CPDF_Dictionary* catalog =
+      CPDFDocumentFromFPDFDocument(doc.get())->GetRoot();
+  ASSERT_TRUE(catalog);
+
+  // The new document shouldn't have any entry for /Lang.
+  EXPECT_FALSE(catalog->GetStringFor("Lang"));
+
+  // Add a new entry.
+  EXPECT_TRUE(FPDFCatalog_SetLanguage(doc.get(), "en-US"));
+
+  RetainPtr<const CPDF_String> result_language = catalog->GetStringFor("Lang");
+  ASSERT_TRUE(result_language);
+  EXPECT_EQ("en-US", result_language->GetString());
+}
+
+TEST_F(FPDFCatalogTest, SetLanguageExistingDocument) {
+  ASSERT_TRUE(OpenDocument("tagged_table.pdf"));
+
+  const CPDF_Dictionary* catalog =
+      CPDFDocumentFromFPDFDocument(document())->GetRoot();
+  ASSERT_TRUE(catalog);
+
+  // The PDF already has an existing entry for /Lang.
+  RetainPtr<const CPDF_String> result_language = catalog->GetStringFor("Lang");
+  ASSERT_TRUE(result_language);
+  EXPECT_EQ("en-US", result_language->GetString());
+
+  // Replace the existing entry.
+  EXPECT_TRUE(FPDFCatalog_SetLanguage(document(), "hu"));
+
+  result_language = catalog->GetStringFor("Lang");
+  ASSERT_TRUE(result_language);
+  EXPECT_EQ("hu", result_language->GetString());
+}
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index a440d79..27ad99a 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -120,6 +120,7 @@
 
     // fpdf_catalog.h
     CHK(FPDFCatalog_IsTagged);
+    CHK(FPDFCatalog_SetLanguage);
 
     // fpdf_dataavail.h
     CHK(FPDFAvail_Create);
diff --git a/public/fpdf_catalog.h b/public/fpdf_catalog.h
index 20725dc..033cca5 100644
--- a/public/fpdf_catalog.h
+++ b/public/fpdf_catalog.h
@@ -25,6 +25,16 @@
 FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
 FPDFCatalog_IsTagged(FPDF_DOCUMENT document);
 
+// Experimental API.
+// Sets the language of |document| to |language|.
+//
+// document - handle to a document.
+// language - the language to set to.
+//
+// Returns TRUE on success.
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFCatalog_SetLanguage(FPDF_DOCUMENT document, FPDF_BYTESTRING language);
+
 #ifdef __cplusplus
 }  // extern "C"
 #endif  // __cplusplus