blob: 755f928ee340e449f54ab73def31aaa162fb36af [file]
// Copyright 2026 The PDFium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "core/fpdfapi/edit/cpdf_fontsubsetter.h"
#include <stdint.h>
#include <numeric>
#include <string>
#include <vector>
#include "core/fpdfapi/parser/cpdf_document.h"
#include "core/fpdfapi/parser/cpdf_stream.h"
#include "core/fxcrt/retain_ptr.h"
#include "public/fpdf_edit.h"
#include "public/fpdfview.h"
#include "testing/embedder_test.h"
#include "testing/fx_string_testhelpers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/utils/file_util.h"
#include "testing/utils/path_service.h"
using ::testing::Contains;
using ::testing::IsEmpty;
using ::testing::Matcher;
using ::testing::UnorderedElementsAre;
namespace {
CPDF_Document* CPDFDocumentFromFPDFDocument(FPDF_DOCUMENT document) {
// This is cheating slightly to avoid a layering violation, since this file
// cannot include fpdfsdk/cpdfsdk_helpers.h to get access to
// CPDFDocumentFromFPDFDocument().
return reinterpret_cast<CPDF_Document*>(document);
}
// Returns the file path for a test font provided by the third-party test_fonts.
std::string GetTestFontFilePath(const std::string& file_name) {
return PathService::GetThirdPartyFilePath("test_fonts/test_fonts/" +
file_name);
}
// Returns a list of new object nums used for testing. Since the object nums
// list is used to see if an object num is new, this can exceed the amount of
// objects in the document. This uses a large enough number sufficient for these
// tests. As a result, tests can avoid having to explicitly write the new object
// numbers added when writing text to the document.
std::vector<uint32_t> GetTestNewObjNums() {
std::vector<uint32_t> test_obj_nums(100);
std::iota(test_obj_nums.begin(), test_obj_nums.end(), 1);
return test_obj_nums;
}
// Matcher that verifies the stream size is strictly within the range min
// inclusive, max exclusive.
MATCHER_P2(StreamSizeIsWithinRange, min_size, max_size, "") {
const auto& [obj_num, obj] = arg;
if (!obj || !obj->IsStream()) {
*result_listener << "is not a stream (obj_num: " << obj_num << ")";
return false;
}
size_t actual_size = obj->AsStream()->GetRawSize();
if (actual_size < min_size || actual_size >= max_size) {
*result_listener << "stream size " << actual_size
<< " is outside expected range (" << min_size << ", "
<< max_size << ")";
return false;
}
return true;
}
} // namespace
// Prints overrides nicely for debugging purposes.
void PrintTo(const RetainPtr<const CPDF_Object>& obj, std::ostream* os) {
if (!obj) {
*os << "nullptr";
return;
}
*os << "(Obj type=" << obj->GetType();
if (obj->IsStream()) {
*os << " size=" << obj->AsStream()->GetRawSize();
}
*os << ")";
}
class CPDFFontSubsetterTest : public EmbedderTest {
public:
void InsertNewTextObject(const std::wstring& text,
FPDF_PAGE page,
FPDF_FONT font) {
FPDF_PAGEOBJECT text_object =
FPDFPageObj_CreateTextObj(document(), font, 20.0f);
EXPECT_TRUE(text_object);
ScopedFPDFWideString fpdf_text = GetFPDFWideString(text);
EXPECT_TRUE(FPDFText_SetText(text_object, fpdf_text.get()));
const FS_MATRIX matrix{1.0f, 0.0f, 0.0f, 1.0f, 50.0f, 200.0f};
ASSERT_TRUE(FPDFPageObj_TransformF(text_object, &matrix));
FPDFPage_InsertObject(page, text_object);
EXPECT_TRUE(FPDFPage_GenerateContent(page));
}
};
TEST_F(CPDFFontSubsetterTest, NoNewText) {
CreateEmptyDocument();
ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400));
CPDF_FontSubsetter subsetter(CPDFDocumentFromFPDFDocument(document()));
EXPECT_THAT(subsetter.GenerateObjectOverrides({}), IsEmpty());
EXPECT_THAT(subsetter.GenerateObjectOverrides(GetTestNewObjNums()),
IsEmpty());
// Not a text object.
FPDF_PAGEOBJECT rect = FPDFPageObj_CreateNewRect(20, 100, 50, 50);
FPDFPage_InsertObject(page.get(), rect);
EXPECT_THAT(subsetter.GenerateObjectOverrides(GetTestNewObjNums()),
IsEmpty());
}
TEST_F(CPDFFontSubsetterTest, OpenType) {
CreateEmptyDocument();
ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400));
const std::string font_path =
GetTestFontFilePath("NotoSansCJKjp-Regular.otf");
ASSERT_FALSE(font_path.empty());
std::vector<uint8_t> font_data = GetFileContents(font_path.c_str());
const size_t original_size = font_data.size();
ASSERT_EQ(16427228u, original_size);
ScopedFPDFFont font(FPDFText_LoadFont(document(), font_data.data(),
font_data.size(), FPDF_FONT_TRUETYPE,
/*cid=*/true));
ASSERT_TRUE(font);
ASSERT_NO_FATAL_FAILURE(InsertNewTextObject(L"这", page.get(), font.get()));
CPDF_FontSubsetter subsetter(CPDFDocumentFromFPDFDocument(document()));
auto overrides = subsetter.GenerateObjectOverrides(GetTestNewObjNums());
ASSERT_EQ(1u, overrides.size());
// Subset size is ~2.5% of the original font file, i.e. ~450 KB.
EXPECT_THAT(overrides, Contains(StreamSizeIsWithinRange(
original_size * 0.02, original_size * 0.03)));
}
TEST_F(CPDFFontSubsetterTest, TrueType) {
CreateEmptyDocument();
ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400));
const std::string font_path = GetTestFontFilePath("Arimo-Regular.ttf");
ASSERT_FALSE(font_path.empty());
std::vector<uint8_t> font_data = GetFileContents(font_path.c_str());
const size_t original_size = font_data.size();
ASSERT_EQ(436180u, original_size);
ScopedFPDFFont font(FPDFText_LoadFont(document(), font_data.data(),
font_data.size(), FPDF_FONT_TRUETYPE,
/*cid=*/true));
ASSERT_TRUE(font);
ASSERT_NO_FATAL_FAILURE(
InsertNewTextObject(L"Hello world", page.get(), font.get()));
CPDF_FontSubsetter subsetter(CPDFDocumentFromFPDFDocument(document()));
auto overrides = subsetter.GenerateObjectOverrides(GetTestNewObjNums());
ASSERT_EQ(1u, overrides.size());
// Subset size is ~3% of the original font file, i.e. ~13 KB.
EXPECT_THAT(overrides, Contains(StreamSizeIsWithinRange(
original_size * 0.025, original_size * 0.035)));
}
TEST_F(CPDFFontSubsetterTest, SingleFontMultipleTexts) {
CreateEmptyDocument();
ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400));
const std::string font_path = GetTestFontFilePath("Arimo-Regular.ttf");
ASSERT_FALSE(font_path.empty());
std::vector<uint8_t> font_data = GetFileContents(font_path.c_str());
const size_t original_size = font_data.size();
ASSERT_EQ(436180u, original_size);
ScopedFPDFFont font(FPDFText_LoadFont(document(), font_data.data(),
font_data.size(), FPDF_FONT_TRUETYPE,
/*cid=*/true));
ASSERT_TRUE(font);
ASSERT_NO_FATAL_FAILURE(
InsertNewTextObject(L"Abcdefg", page.get(), font.get()));
ASSERT_NO_FATAL_FAILURE(
InsertNewTextObject(L"Hijklmnop", page.get(), font.get()));
CPDF_FontSubsetter subsetter(CPDFDocumentFromFPDFDocument(document()));
auto overrides = subsetter.GenerateObjectOverrides(GetTestNewObjNums());
ASSERT_EQ(1u, overrides.size());
// Subset size is ~3.5% of the original font file, i.e. ~15 KB.
EXPECT_THAT(overrides, Contains(StreamSizeIsWithinRange(
original_size * 0.03, original_size * 0.04)));
}
TEST_F(CPDFFontSubsetterTest, MultipleFontsMultipleTexts) {
CreateEmptyDocument();
ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400));
const std::string font_path1 = GetTestFontFilePath("Lohit-Tamil.ttf");
ASSERT_FALSE(font_path1.empty());
const std::string font_path2 = GetTestFontFilePath("Arimo-Regular.ttf");
ASSERT_FALSE(font_path2.empty());
std::vector<uint8_t> font_data1 = GetFileContents(font_path1.c_str());
const size_t original_size1 = font_data1.size();
ASSERT_EQ(48908u, original_size1);
std::vector<uint8_t> font_data2 = GetFileContents(font_path2.c_str());
const size_t original_size2 = font_data2.size();
ASSERT_EQ(436180u, original_size2);
ScopedFPDFFont font1(FPDFText_LoadFont(document(), font_data1.data(),
font_data1.size(), FPDF_FONT_TRUETYPE,
/*cid=*/true));
ASSERT_TRUE(font1);
ScopedFPDFFont font2(FPDFText_LoadFont(document(), font_data2.data(),
font_data2.size(), FPDF_FONT_TRUETYPE,
/*cid=*/true));
ASSERT_TRUE(font2);
ASSERT_NO_FATAL_FAILURE(
InsertNewTextObject(L"வணக்கம்", page.get(), font1.get()));
ASSERT_NO_FATAL_FAILURE(
InsertNewTextObject(L"Goodbye", page.get(), font2.get()));
CPDF_FontSubsetter subsetter(CPDFDocumentFromFPDFDocument(document()));
auto overrides = subsetter.GenerateObjectOverrides(GetTestNewObjNums());
ASSERT_EQ(2u, overrides.size());
// Subset size for `font_data1` is ~6% of the original file, i.e. ~3 KB.
// Subset size for `font_data2` is ~3% of the original file, i.e. ~13.3 KB.
EXPECT_THAT(overrides, UnorderedElementsAre(
StreamSizeIsWithinRange(original_size1 * 0.055,
original_size1 * 0.065),
StreamSizeIsWithinRange(original_size2 * 0.025,
original_size2 * 0.035)));
}