blob: 2167e032e88c9ea78db78f24225dabb5eb36bfa7 [file] [edit]
#include "GlobalParams.h"
#include "PDFDocFactory.h"
#include "goo/GooString.h"
#include <Form.h>
#include <FontSubsetter.h>
#include <ft2build.h>
#include FT_FREETYPE_H
static std::vector<unsigned long> convertUnicodetoNormalStr(const std::string &s)
{
std::vector<unsigned long> res;
size_t i = 0;
if (s.size() > 2) {
if (s.starts_with("\xFE\xFF")) {
i = 2;
}
}
while (i < s.size()) {
unsigned long unicodeByte = (static_cast<unsigned char>(s[i]) << 8) | (static_cast<unsigned char>(s[i + 1]));
res.emplace_back(unicodeByte);
i += 2;
}
return res;
}
static std::string getFontDataFromFont(std::shared_ptr<GfxFont> &font, XRef *xref)
{
Ref streamRef;
bool isEmbedded = font->getEmbeddedFontID(&streamRef);
if (!isEmbedded) {
// Font may be externally linked
return "";
}
Object streamObj = xref->fetch(streamRef);
Stream *stream = streamObj.getStream();
std::string s;
stream->fillString(s);
return s;
}
static void setDefaultAppearanceToNative(AnnotFreeText *ftextann, Page *pdfPage)
{
const double pointSize = 12;
auto textColor = std::make_unique<AnnotColor>(0, 0, 0);
std::string fontName = "Invalid_font";
std::string textFont = "Noto Sans";
Form *form = pdfPage->getDoc()->getCatalog()->getCreateForm();
if (form) {
fontName = form->findFontInDefaultResources(textFont, "");
if (fontName.empty()) {
fontName = form->addFontToDefaultResources(textFont, "").fontName;
}
if (!fontName.empty()) {
form->ensureFontsForAllCharacters(ftextann->getContents().toStr(), fontName);
} else {
fontName = "Invalid_font";
}
}
assert(fontName != "Invalid_font");
DefaultAppearance da { fontName, pointSize, std::move(textColor) };
ftextann->setDefaultAppearance(da);
}
struct GlyphsResult
{
bool ok;
int numGlyphs = 0;
std::vector<unsigned long> codepoints;
};
static GlyphsResult getGlyphsInfo(std::string &s)
{
FT_Library ft_library;
FT_Error ft_error = FT_Init_FreeType(&ft_library);
if (ft_error) {
return { .ok = false, .numGlyphs = 0, .codepoints = {} };
}
const std::unique_ptr<FT_Library, void (*)(FT_Library *)> freetypeLibDeleter(&ft_library, [](FT_Library *l) { FT_Done_FreeType(*l); });
const auto *data = const_cast<const FT_Byte *>(reinterpret_cast<FT_Byte *>(s.data()));
FT_Face face;
ft_error = FT_New_Memory_Face(ft_library, data, s.size(), 0, &face);
if (ft_error) {
return { .ok = false, .numGlyphs = -1, .codepoints = {} };
}
const std::unique_ptr<FT_Face, void (*)(FT_Face *)> faceDeleter(&face, [](FT_Face *f) { FT_Done_Face(*f); });
FT_Select_Charmap(face, FT_ENCODING_UNICODE);
FT_ULong charcode;
FT_UInt gid;
charcode = FT_Get_First_Char(face, &gid);
int numGlyphs = 0;
std::vector<unsigned long> codepoints;
while (gid != 0) {
numGlyphs++;
codepoints.emplace_back(charcode);
charcode = FT_Get_Next_Char(face, charcode, &gid);
}
return { .ok = true, .numGlyphs = numGlyphs, .codepoints = codepoints };
}
static std::vector<std::shared_ptr<GfxFont>> getAllFontsFromResources(const Dict *fontDict, XRef *xref)
{
std::vector<std::shared_ptr<GfxFont>> result;
for (int i = 0; i < fontDict->getLength(); i++) {
Object fontObj = fontDict->getVal(i);
Ref fontId = fontDict->getValNF(i).getRef();
auto font = GfxFont::makeFont(xref, fontDict->getKey(i), fontId, *fontObj.getDict());
result.emplace_back(std::shared_ptr<GfxFont>(std::move(font)));
}
return result;
}
static bool match(std::vector<unsigned long> &v1, std::vector<unsigned long> &v2)
{
std::set<unsigned long> s1(v1.begin(), v1.end());
std::set<unsigned long> s2(v2.begin(), v2.end());
if (s1.size() != s2.size()) {
return false;
}
auto it1 = s1.begin();
auto it2 = s2.begin();
while (it1 != s1.end() && it2 != s2.end()) {
if (*it1 != *it2) {
return false;
}
it1++;
it2++;
}
return true;
}
/*
* Checks if font subsetting is properly done for an annotation
* Goes over all the fonts used by the annotation and collects all the glyphs
* Checks if the glyphs are only what we need
*/
static bool subsettingWorksInAnnot(Annot *annot, XRef *xref, bool transparency)
{
Object resources = annot->getAppearanceResDict();
const Dict *fontDict = getFontDictFromResourcesDict(resources, xref, transparency);
if (!fontDict) {
return false;
}
auto allFonts = getAllFontsFromResources(fontDict, xref);
std::vector<unsigned long> allChars;
for (auto &font : allFonts) {
std::string data = getFontDataFromFont(font, xref);
if (data.empty()) {
continue;
}
auto result = getGlyphsInfo(data);
if (!result.ok) {
return false;
}
for (unsigned long code : result.codepoints) {
allChars.emplace_back(code);
}
}
std::vector<unsigned long> neededChars = convertUnicodetoNormalStr(annot->getContents().toStr());
return match(allChars, neededChars);
}
// Test functions
/*
* - Add a freetext annotation with some text.
* - Save the file.
* - Check if the font used by the annotation only has characters for the text we entered.
*/
static bool testSingleAnnotationSubsetting(std::shared_ptr<GooString> &filename)
{
/*
* This is a very temporary workaround to use unicode characters such as japanese and chinese for the test
* If we simply do const char *TEXT = "Hello 1234 日本語";
* This is bad because the last 3 characters simply can't be represented in a single byte
* unicodeVals here is an integer list representation of an already unicode encoded string containing "Hello 1234 日本語"
* TODO: Change this after making proper to-and-from unicode conversion functions
*/
std::string UNICODE_TEXT;
int unicodeVals[] = { -2, -1, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 49, 0, 50, 0, 51, 0, 52, 0, 32, 101, -27, 103, 44, -118, -98 };
for (int val : unicodeVals) {
UNICODE_TEXT += static_cast<char>(val);
}
auto doc = PDFDocFactory().createPDFDoc(*filename);
if (!doc->isOk() || doc->getNumPages() < 1) {
return false;
}
Page *page = doc->getPage(1);
auto ftextann = std::make_shared<AnnotFreeText>(doc.get(), PDFRectangle(50, 50, 100, 100));
ftextann->setContents(std::make_unique<GooString>(UNICODE_TEXT));
setDefaultAppearanceToNative(ftextann.get(), page);
page->addAnnot(ftextann);
auto tempFilePath = std::make_unique<GooString>(std::filesystem::current_path() / "temp.pdf");
doc->saveAs(tempFilePath->toStr());
// Re-open the file and check if the fonts only contain the needed glyphs
doc = PDFDocFactory().createPDFDoc(*tempFilePath);
if (!doc->isOk() || doc->getNumPages() < 1) {
return false;
}
page = doc->getPage(1);
Annots *annotsPtr = page->getAnnots();
if (!annotsPtr) {
return false;
}
auto annots = annotsPtr->getAnnots();
if (annots.size() != 1) {
return false;
}
auto &annot = annots[0];
if (annot->getType() != Annot::typeFreeText || annot->getContents().toStr() != UNICODE_TEXT) {
return false;
}
return subsettingWorksInAnnot(annot.get(), doc->getXRef(), false);
}
/*
* Same as testSingleAnnotationSubsetting but with added transparency
*/
static bool testSingleAnnotationSubsettingWithOpacity(std::shared_ptr<GooString> &filename)
{
std::string UNICODE_TEXT;
int unicodeVals[] = { -2, -1, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 49, 0, 50, 0, 51, 0, 52, 0, 32, 101, -27, 103, 44, -118, -98 };
for (int val : unicodeVals) {
UNICODE_TEXT += static_cast<char>(val);
}
auto doc = PDFDocFactory().createPDFDoc(*filename);
if (!doc->isOk() || doc->getNumPages() < 1) {
return false;
}
Page *page = doc->getPage(1);
auto ftextann = std::make_shared<AnnotFreeText>(doc.get(), PDFRectangle(50, 50, 100, 100));
ftextann->setContents(std::make_unique<GooString>(UNICODE_TEXT));
ftextann->setOpacity(0.5);
setDefaultAppearanceToNative(ftextann.get(), page);
page->addAnnot(ftextann);
auto tempFilePath = std::make_unique<GooString>(std::filesystem::current_path() / "temp.pdf");
doc->saveAs(tempFilePath->toStr());
// Re-open the file and check if the fonts only contain the needed glyphs
doc = PDFDocFactory().createPDFDoc(*tempFilePath);
if (!doc->isOk() || doc->getNumPages() < 1) {
return false;
}
page = doc->getPage(1);
Annots *annotsPtr = page->getAnnots();
if (!annotsPtr) {
return false;
}
auto annots = annotsPtr->getAnnots();
if (annots.size() != 1) {
return false;
}
auto &annot = annots[0];
if (annot->getType() != Annot::typeFreeText || annot->getContents().toStr() != UNICODE_TEXT) {
return false;
}
return subsettingWorksInAnnot(annot.get(), doc->getXRef(), true);
}
int main(int argc, char *argv[])
{
if (argc != 2) {
return 1;
}
globalParams = std::make_unique<GlobalParams>();
// For test1 and test2
auto filename = std::make_shared<GooString>(argv[1]);
auto test1 = testSingleAnnotationSubsetting(filename);
auto test2 = testSingleAnnotationSubsettingWithOpacity(filename);
if (!(test1 && test2)) {
return 1;
}
return 0;
}