| #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; |
| } |