// Copyright (C) 2018-2020 Jakub Melka // // This file is part of PdfForQt. // // PdfForQt is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // PdfForQt is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with PDFForQt. If not, see . #include "pdfcatalog.h" #include "pdfdocument.h" #include "pdfexception.h" #include "pdfnumbertreeloader.h" #include "pdfnametreeloader.h" namespace pdf { static constexpr const char* PDF_VIEWER_PREFERENCES_DICTIONARY = "ViewerPreferences"; static constexpr const char* PDF_VIEWER_PREFERENCES_HIDE_TOOLBAR = "HideToolbar"; static constexpr const char* PDF_VIEWER_PREFERENCES_HIDE_MENUBAR = "HideMenubar"; static constexpr const char* PDF_VIEWER_PREFERENCES_HIDE_WINDOW_UI = "HideWindowUI"; static constexpr const char* PDF_VIEWER_PREFERENCES_FIT_WINDOW = "FitWindow"; static constexpr const char* PDF_VIEWER_PREFERENCES_CENTER_WINDOW = "CenterWindow"; static constexpr const char* PDF_VIEWER_PREFERENCES_DISPLAY_DOCUMENT_TITLE = "DisplayDocTitle"; static constexpr const char* PDF_VIEWER_PREFERENCES_NON_FULLSCREEN_PAGE_MODE = "NonFullScreenPageMode"; static constexpr const char* PDF_VIEWER_PREFERENCES_DIRECTION = "Direction"; static constexpr const char* PDF_VIEWER_PREFERENCES_VIEW_AREA = "ViewArea"; static constexpr const char* PDF_VIEWER_PREFERENCES_VIEW_CLIP = "ViewClip"; static constexpr const char* PDF_VIEWER_PREFERENCES_PRINT_AREA = "PrintArea"; static constexpr const char* PDF_VIEWER_PREFERENCES_PRINT_CLIP = "PrintClip"; static constexpr const char* PDF_VIEWER_PREFERENCES_PRINT_SCALING = "PrintScaling"; static constexpr const char* PDF_VIEWER_PREFERENCES_DUPLEX = "Duplex"; static constexpr const char* PDF_VIEWER_PREFERENCES_PICK_TRAY_BY_PDF_SIZE = "PickTrayByPDFSize"; static constexpr const char* PDF_VIEWER_PREFERENCES_NUMBER_OF_COPIES = "NumCopies"; static constexpr const char* PDF_VIEWER_PREFERENCES_PRINT_PAGE_RANGE = "PrintPageRange"; size_t PDFCatalog::getPageIndexFromPageReference(PDFObjectReference reference) const { auto it = std::find_if(m_pages.cbegin(), m_pages.cend(), [reference](const PDFPage& page) { return page.getPageReference() == reference; }); if (it != m_pages.cend()) { return std::distance(m_pages.cbegin(), it); } return INVALID_PAGE_INDEX; } const PDFDestination* PDFCatalog::getDestination(const QByteArray& key) const { auto it = m_destinations.find(key); if (it != m_destinations.cend()) { return &it->second; } return nullptr; } PDFCatalog PDFCatalog::parse(const PDFObject& catalog, const PDFDocument* document) { if (!catalog.isDictionary()) { throw PDFException(PDFTranslationContext::tr("Catalog must be a dictionary.")); } const PDFDictionary* catalogDictionary = catalog.getDictionary(); Q_ASSERT(catalogDictionary); PDFCatalog catalogObject; catalogObject.m_viewerPreferences = PDFViewerPreferences::parse(catalog, document); catalogObject.m_pages = PDFPage::parse(document, catalogDictionary->get("Pages")); catalogObject.m_pageLabels = PDFNumberTreeLoader::parse(document, catalogDictionary->get("PageLabels")); if (catalogDictionary->hasKey("OCProperties")) { catalogObject.m_optionalContentProperties = PDFOptionalContentProperties::create(document, catalogDictionary->get("OCProperties")); } if (catalogDictionary->hasKey("Outlines")) { catalogObject.m_outlineRoot = PDFOutlineItem::parse(document, catalogDictionary->get("Outlines")); } if (catalogDictionary->hasKey("OpenAction")) { PDFObject openAction = document->getObject(catalogDictionary->get("OpenAction")); if (openAction.isArray()) { catalogObject.m_openAction.reset(new PDFActionGoTo(PDFDestination::parse(&document->getStorage(), openAction))); } if (openAction.isDictionary()) { catalogObject.m_openAction = PDFAction::parse(&document->getStorage(), openAction); } } PDFDocumentDataLoaderDecorator loader(document); if (catalogDictionary->hasKey("PageLayout")) { constexpr const std::array, 6> pageLayouts = { std::pair{ "SinglePage", PageLayout::SinglePage }, std::pair{ "OneColumn", PageLayout::OneColumn }, std::pair{ "TwoColumnLeft", PageLayout::TwoColumnLeft }, std::pair{ "TwoColumnRight", PageLayout::TwoColumnRight }, std::pair{ "TwoPageLeft", PageLayout::TwoPagesLeft }, std::pair{ "TwoPageRight", PageLayout::TwoPagesRight } }; catalogObject.m_pageLayout = loader.readEnumByName(catalogDictionary->get("PageLayout"), pageLayouts.begin(), pageLayouts.end(), PageLayout::SinglePage); } if (catalogDictionary->hasKey("PageMode")) { constexpr const std::array, 6> pageModes = { std::pair{ "UseNone", PageMode::UseNone }, std::pair{ "UseOutlines", PageMode::UseOutlines }, std::pair{ "UseThumbs", PageMode::UseThumbnails }, std::pair{ "FullScreen", PageMode::Fullscreen }, std::pair{ "UseOC", PageMode::UseOptionalContent }, std::pair{ "UseAttachments", PageMode::UseAttachments } }; catalogObject.m_pageMode = loader.readEnumByName(catalogDictionary->get("PageMode"), pageModes.begin(), pageModes.end(), PageMode::UseNone); } catalogObject.m_version = loader.readNameFromDictionary(catalogDictionary, "Version"); if (const PDFDictionary* namesDictionary = document->getDictionaryFromObject(catalogDictionary->get("Names"))) { auto parseDestination = [](const PDFObjectStorage* storage, PDFObject object) { object = storage->getObject(object); if (object.isDictionary()) { object = object.getDictionary()->get("D"); } return PDFDestination::parse(storage, qMove(object)); }; catalogObject.m_destinations = PDFNameTreeLoader::parse(&document->getStorage(), namesDictionary->get("Dests"), parseDestination); catalogObject.m_javaScriptActions = PDFNameTreeLoader::parse(&document->getStorage(), namesDictionary->get("JavaScript"), &PDFAction::parse); catalogObject.m_embeddedFiles = PDFNameTreeLoader::parse(&document->getStorage(), namesDictionary->get("EmbeddedFiles"), &PDFFileSpecification::parse); } // Examine "Dests" dictionary if (const PDFDictionary* destsDictionary = document->getDictionaryFromObject(catalogDictionary->get("Dests"))) { const size_t count = destsDictionary->getCount(); for (size_t i = 0; i < count; ++i) { catalogObject.m_destinations[destsDictionary->getKey(i)] = PDFDestination::parse(&document->getStorage(), destsDictionary->getValue(i)); } } // Examine "URI" dictionary if (const PDFDictionary* URIDictionary = document->getDictionaryFromObject(catalogDictionary->get("URI"))) { catalogObject.m_baseURI = loader.readStringFromDictionary(URIDictionary, "Base"); } catalogObject.m_formObject = catalogDictionary->get("AcroForm"); return catalogObject; } PDFViewerPreferences PDFViewerPreferences::parse(const PDFObject& catalogDictionary, const PDFDocument* document) { PDFViewerPreferences result; if (!catalogDictionary.isDictionary()) { throw PDFException(PDFTranslationContext::tr("Catalog must be a dictionary.")); } const PDFDictionary* dictionary = catalogDictionary.getDictionary(); if (dictionary->hasKey(PDF_VIEWER_PREFERENCES_DICTIONARY)) { const PDFObject& viewerPreferencesObject = document->getObject(dictionary->get(PDF_VIEWER_PREFERENCES_DICTIONARY)); if (viewerPreferencesObject.isDictionary()) { // Load the viewer preferences object const PDFDictionary* viewerPreferencesDictionary = viewerPreferencesObject.getDictionary(); auto addFlag = [&result, viewerPreferencesDictionary, document] (const char* name, OptionFlag flag) { const PDFObject& flagObject = document->getObject(viewerPreferencesDictionary->get(name)); if (!flagObject.isNull()) { if (flagObject.isBool()) { result.m_optionFlags.setFlag(flag, flagObject.getBool()); } else { throw PDFException(PDFTranslationContext::tr("Expected boolean value.")); } } }; addFlag(PDF_VIEWER_PREFERENCES_HIDE_TOOLBAR, HideToolbar); addFlag(PDF_VIEWER_PREFERENCES_HIDE_MENUBAR, HideMenubar); addFlag(PDF_VIEWER_PREFERENCES_HIDE_WINDOW_UI, HideWindowUI); addFlag(PDF_VIEWER_PREFERENCES_FIT_WINDOW, FitWindow); addFlag(PDF_VIEWER_PREFERENCES_CENTER_WINDOW, CenterWindow); addFlag(PDF_VIEWER_PREFERENCES_DISPLAY_DOCUMENT_TITLE, DisplayDocTitle); addFlag(PDF_VIEWER_PREFERENCES_PICK_TRAY_BY_PDF_SIZE, PickTrayByPDFSize); // Non-fullscreen page mode const PDFObject& nonFullscreenPageMode = document->getObject(viewerPreferencesDictionary->get(PDF_VIEWER_PREFERENCES_NON_FULLSCREEN_PAGE_MODE)); if (!nonFullscreenPageMode.isNull()) { if (!nonFullscreenPageMode.isName()) { throw PDFException(PDFTranslationContext::tr("Expected name.")); } QByteArray enumName = nonFullscreenPageMode.getString(); if (enumName == "UseNone") { result.m_nonFullScreenPageMode = NonFullScreenPageMode::UseNone; } else if (enumName == "UseOutlines") { result.m_nonFullScreenPageMode = NonFullScreenPageMode::UseOutline; } else if (enumName == "UseThumbs") { result.m_nonFullScreenPageMode = NonFullScreenPageMode::UseThumbnails; } else if (enumName == "UseOC") { result.m_nonFullScreenPageMode = NonFullScreenPageMode::UseOptionalContent; } else { throw PDFException(PDFTranslationContext::tr("Unknown viewer preferences settings.")); } } // Direction const PDFObject& direction = document->getObject(viewerPreferencesDictionary->get(PDF_VIEWER_PREFERENCES_DIRECTION)); if (!direction.isNull()) { if (!direction.isName()) { throw PDFException(PDFTranslationContext::tr("Expected name.")); } QByteArray enumName = direction.getString(); if (enumName == "L2R") { result.m_direction = Direction::LeftToRight; } else if (enumName == "R2L") { result.m_direction = Direction::RightToLeft; } else { throw PDFException(PDFTranslationContext::tr("Unknown viewer preferences settings.")); } } auto addProperty = [&result, viewerPreferencesDictionary, document] (const char* name, Properties property) { const PDFObject& propertyObject = document->getObject(viewerPreferencesDictionary->get(name)); if (!propertyObject.isNull()) { if (propertyObject.isName()) { result.m_properties[property] = propertyObject.getString(); } else { throw PDFException(PDFTranslationContext::tr("Expected name.")); } } }; addProperty(PDF_VIEWER_PREFERENCES_VIEW_AREA, ViewArea); addProperty(PDF_VIEWER_PREFERENCES_VIEW_CLIP, ViewClip); addProperty(PDF_VIEWER_PREFERENCES_PRINT_AREA, PrintArea); addProperty(PDF_VIEWER_PREFERENCES_PRINT_CLIP, PrintClip); // Print scaling const PDFObject& printScaling = document->getObject(viewerPreferencesDictionary->get(PDF_VIEWER_PREFERENCES_PRINT_SCALING)); if (!printScaling.isNull()) { if (!printScaling.isName()) { throw PDFException(PDFTranslationContext::tr("Expected name.")); } QByteArray enumName = printScaling.getString(); if (enumName == "None") { result.m_printScaling = PrintScaling::None; } else if (enumName == "AppDefault") { result.m_printScaling = PrintScaling::AppDefault; } else { throw PDFException(PDFTranslationContext::tr("Unknown viewer preferences settings.")); } } // Duplex const PDFObject& duplex = document->getObject(viewerPreferencesDictionary->get(PDF_VIEWER_PREFERENCES_DUPLEX)); if (!duplex.isNull()) { if (!duplex.isName()) { throw PDFException(PDFTranslationContext::tr("Expected name.")); } QByteArray enumName = duplex.getString(); if (enumName == "Simplex") { result.m_duplex = Duplex::Simplex; } else if (enumName == "DuplexFlipShortEdge") { result.m_duplex = Duplex::DuplexFlipShortEdge; } else if (enumName == "DuplexFlipLongEdge") { result.m_duplex = Duplex::DuplexFlipLongEdge; } else { throw PDFException(PDFTranslationContext::tr("Unknown viewer preferences settings.")); } } // Print page range const PDFObject& printPageRange = document->getObject(viewerPreferencesDictionary->get(PDF_VIEWER_PREFERENCES_PRINT_PAGE_RANGE)); if (!printPageRange.isNull()) { if (!duplex.isArray()) { throw PDFException(PDFTranslationContext::tr("Expected array of integers.")); } // According to PDF Reference 1.7, this entry is ignored in following cases: // 1) Array size is odd // 2) Array contains negative numbers // // But what should we do, if we get 0? Pages in the PDF file are numbered from 1. // So if this situation occur, we also ignore the entry. const PDFArray* array = duplex.getArray(); const size_t count = array->getCount(); if (count % 2 == 0 && count > 0) { bool badPageNumber = false; int scanned = 0; PDFInteger start = -1; for (size_t i = 0; i < count; ++i) { const PDFObject& number = document->getObject(array->getItem(i)); if (number.isInt()) { PDFInteger current = number.getInteger(); if (current <= 0) { badPageNumber = true; break; } switch (scanned++) { case 0: { start = current; break; } case 1: { scanned = 0; result.m_printPageRanges.emplace_back(start, current); break; } default: Q_ASSERT(false); } } else { throw PDFException(PDFTranslationContext::tr("Expected integer.")); } } // Did we get negative or zero value? If yes, clear the range. if (badPageNumber) { result.m_printPageRanges.clear(); } } } // Number of copies const PDFObject& numberOfCopies = document->getObject(viewerPreferencesDictionary->get(PDF_VIEWER_PREFERENCES_NUMBER_OF_COPIES)); if (!numberOfCopies.isNull()) { if (numberOfCopies.isInt()) { result.m_numberOfCopies = numberOfCopies.getInteger(); } else { throw PDFException(PDFTranslationContext::tr("Expected integer.")); } } } else if (!viewerPreferencesObject.isNull()) { throw PDFException(PDFTranslationContext::tr("Viewer preferences must be a dictionary.")); } } return result; } PDFPageLabel PDFPageLabel::parse(PDFInteger pageIndex, const PDFDocument* document, const PDFObject& object) { const PDFObject& dereferencedObject = document->getObject(object); if (dereferencedObject.isDictionary()) { std::array, 5> numberingStyles = { std::pair{ "D", NumberingStyle::DecimalArabic}, std::pair{ "R", NumberingStyle::UppercaseRoman }, std::pair{ "r", NumberingStyle::LowercaseRoman }, std::pair{ "A", NumberingStyle::UppercaseLetters}, std::pair{ "a", NumberingStyle::LowercaseLetters} }; const PDFDictionary* dictionary = dereferencedObject.getDictionary(); const PDFDocumentDataLoaderDecorator loader(document); const NumberingStyle numberingStyle = loader.readEnumByName(dictionary->get("S"), numberingStyles.cbegin(), numberingStyles.cend(), NumberingStyle::None); const QString prefix = loader.readTextString(dictionary->get("P"), QString()); const PDFInteger startNumber = loader.readInteger(dictionary->get("St"), 1); return PDFPageLabel(numberingStyle, prefix, pageIndex, startNumber); } else { throw PDFException(PDFTranslationContext::tr("Expected page label dictionary.")); } return PDFPageLabel(); } } // namespace pdf