diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:50:49 +0000 |
commit | c853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch) | |
tree | 7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/extension/internal/pdfinput | |
parent | Initial commit. (diff) | |
download | inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip |
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/extension/internal/pdfinput')
-rw-r--r-- | src/extension/internal/pdfinput/enums.h | 34 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/pdf-input.cpp | 861 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/pdf-input.h | 158 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/pdf-parser.cpp | 3196 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/pdf-parser.h | 339 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/pdf-utils.cpp | 115 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/pdf-utils.h | 56 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/poppler-cairo-font-engine.cpp | 779 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/poppler-cairo-font-engine.h | 166 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/poppler-transition-api.h | 95 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/poppler-utils.cpp | 599 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/poppler-utils.h | 95 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/svg-builder.cpp | 2311 | ||||
-rw-r--r-- | src/extension/internal/pdfinput/svg-builder.h | 296 |
14 files changed, 9100 insertions, 0 deletions
diff --git a/src/extension/internal/pdfinput/enums.h b/src/extension/internal/pdfinput/enums.h new file mode 100644 index 0000000..b68f038 --- /dev/null +++ b/src/extension/internal/pdfinput/enums.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF Parsing utility functions and classes. + *//* + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef PDF_ENUMS_H +#define PDF_ENUMS_H + +#include <map> + +enum class FontStrategy : unsigned char +{ + RENDER_MISSING, + RENDER_ALL, + SUBSTITUTE_MISSING, + KEEP_MISSING, + DELETE_MISSING, + DELETE_ALL +}; +enum class FontFallback : unsigned char +{ + DELETE_TEXT = 0, + AS_SHAPES, + AS_TEXT, + AS_SUB, +}; +typedef std::map<int, FontFallback> FontStrategies; + +#endif /* PDF_ENUMS_H */ diff --git a/src/extension/internal/pdfinput/pdf-input.cpp b/src/extension/internal/pdfinput/pdf-input.cpp new file mode 100644 index 0000000..302af06 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-input.cpp @@ -0,0 +1,861 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Native PDF import using libpoppler. + * + * Authors: + * miklos erdelyi + * Abhishek Sharma + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "pdf-input.h" + +#ifdef HAVE_POPPLER +#include <poppler/Catalog.h> +#include <poppler/ErrorCodes.h> +#include <poppler/FontInfo.h> +#include <poppler/GfxFont.h> +#include <poppler/GlobalParams.h> +#include <poppler/OptionalContent.h> +#include <poppler/PDFDoc.h> +#include <poppler/Page.h> +#include <poppler/goo/GooString.h> + +#ifdef HAVE_POPPLER_CAIRO +#include <poppler/glib/poppler.h> +#include <poppler/glib/poppler-document.h> +#include <poppler/glib/poppler-page.h> +#endif + +#include <gdkmm/general.h> +#include <glibmm/convert.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> +#include <gtk/gtk.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/frame.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scale.h> +#include <utility> + +#include "document-undo.h" +#include "extension/input.h" +#include "extension/system.h" +#include "inkscape.h" +#include "object/sp-root.h" +#include "pdf-parser.h" +#include "ui/builder-utils.h" +#include "ui/dialog-events.h" +#include "ui/widget/frame.h" +#include "ui/widget/spinbutton.h" +#include "util/parse-int-range.h" +#include "util/units.h" + +using namespace Inkscape::UI; + +namespace { + +void sanitize_page_number(int &page_num, const int num_pages) { + if (page_num < 1 || page_num > num_pages) { + std::cerr << "Inkscape::Extension::Internal::PdfInput::open: Bad page number " + << page_num + << ". Import first page instead." + << std::endl; + page_num = 1; + } +} + +} + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class FontModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + FontModelColumns() + { + add(id); + add(family); + add(style); + add(weight); + add(stretch); + add(proc_label); + add(proc_id); + add(icon); + add(em); + } + ~FontModelColumns() override = default; + Gtk::TreeModelColumn<int> id; + Gtk::TreeModelColumn<Glib::ustring> family; + Gtk::TreeModelColumn<Glib::ustring> style; + Gtk::TreeModelColumn<Glib::ustring> weight; + Gtk::TreeModelColumn<Glib::ustring> stretch; + Gtk::TreeModelColumn<Glib::ustring> proc_label; + Gtk::TreeModelColumn<int> proc_id; + Gtk::TreeModelColumn<Glib::ustring> icon; + Gtk::TreeModelColumn<bool> em; +}; + +/** + * \brief The PDF import dialog + * FIXME: Probably this should be placed into src/ui/dialog + */ + +PdfImportDialog::PdfImportDialog(std::shared_ptr<PDFDoc> doc, const gchar * /*uri*/) + : _pdf_doc(std::move(doc)) + , _builder(UI::create_builder("extension-pdfinput.glade")) + , _page_numbers(UI::get_widget<Gtk::Entry>(_builder, "page-numbers")) + , _preview_area(UI::get_widget<Gtk::DrawingArea>(_builder, "preview-area")) + , _embed_images(UI::get_widget<Gtk::CheckButton>(_builder, "embed-images")) + , _mesh_slider(UI::get_widget<Gtk::Scale>(_builder, "mesh-slider")) + , _mesh_label(UI::get_widget<Gtk::Label>(_builder, "mesh-label")) + , _next_page(UI::get_widget<Gtk::Button>(_builder, "next-page")) + , _prev_page(UI::get_widget<Gtk::Button>(_builder, "prev-page")) + , _current_page(UI::get_widget<Gtk::Label>(_builder, "current-page")) + , _font_model(UI::get_object<Gtk::ListStore>(_builder, "font-list")) + , _font_col(new FontModelColumns()) +{ + assert(_pdf_doc); + + _setFonts(getPdfFonts(_pdf_doc)); + + auto okbutton = Gtk::manage(new Gtk::Button(_("_OK"), true)); + + get_content_area()->set_homogeneous(false); + get_content_area()->set_spacing(0); + + get_content_area()->pack_start(UI::get_widget<Gtk::Box>(_builder, "content")); + + this->set_title(_("PDF Import Settings")); + this->set_modal(true); + sp_transientize(GTK_WIDGET(this->gobj())); //Make transient + this->property_window_position().set_value(Gtk::WIN_POS_NONE); + this->set_resizable(true); + this->property_destroy_with_parent().set_value(false); + + this->add_action_widget(*Gtk::manage(new Gtk::Button(_("_Cancel"), true)), -6); + this->add_action_widget(*okbutton, -5); + + this->show_all(); + + _render_thumb = false; + + // Connect signals + _next_page.signal_clicked().connect([=] { _setPreviewPage(_preview_page + 1); }); + _prev_page.signal_clicked().connect([=] { _setPreviewPage(_preview_page - 1); }); + _preview_area.signal_draw().connect(sigc::mem_fun(*this, &PdfImportDialog::_onDraw)); + _page_numbers.signal_changed().connect(sigc::mem_fun(*this, &PdfImportDialog::_onPageNumberChanged)); + _mesh_slider.get_adjustment()->signal_value_changed().connect( + sigc::mem_fun(*this, &PdfImportDialog::_onPrecisionChanged)); + +#ifdef HAVE_POPPLER_CAIRO + _render_thumb = true; + + // Disable the page selector when there's only one page + _total_pages = _pdf_doc->getCatalog()->getNumPages(); + _page_numbers.set_sensitive(_total_pages > 1); + + // Create PopplerDocument + std::string filename = _pdf_doc->getFileName()->getCString(); + if (!Glib::path_is_absolute(filename)) { + filename = Glib::build_filename(Glib::get_current_dir(),filename); + } + Glib::ustring full_uri = Glib::filename_to_uri(filename); + + if (!full_uri.empty()) { + _poppler_doc = poppler_document_new_from_file(full_uri.c_str(), NULL, NULL); + } +#endif + + // Set default preview size + _preview_width = 200; + _preview_height = 300; + + // Init preview + _thumb_data = nullptr; + _current_pages = "all"; + _setPreviewPage(1); + + okbutton->set_can_focus(); + okbutton->set_can_default(); + set_default(*okbutton); + set_focus(*okbutton); + + auto &font_strat = UI::get_object_raw<Gtk::CellRendererCombo>(_builder, "cell-strat"); + font_strat.signal_changed().connect([=](const Glib::ustring &path, const Gtk::TreeModel::iterator &source) { + if (auto target = _font_model->get_iter(path)) { + (*target)[_font_col->proc_id] = int((*source)[_font_col->id]); + (*target)[_font_col->proc_label] = Glib::ustring((*source)[_font_col->family]); + } + }); + + auto &font_render = UI::get_widget<Gtk::ComboBox>(_builder, "font-rendering"); + font_render.signal_changed().connect(sigc::mem_fun(*this, &PdfImportDialog::_fontRenderChanged)); + _fontRenderChanged(); +} + +PdfImportDialog::~PdfImportDialog() { +#ifdef HAVE_POPPLER_CAIRO + if (_cairo_surface) { + cairo_surface_destroy(_cairo_surface); + } + if (_poppler_doc) { + g_object_unref(G_OBJECT(_poppler_doc)); + } +#endif + if (_thumb_data) { + gfree(_thumb_data); + } +} + +bool PdfImportDialog::showDialog() { + show(); + gint b = run(); + hide(); + if ( b == Gtk::RESPONSE_OK ) { + return TRUE; + } else { + return FALSE; + } +} + +std::string PdfImportDialog::getSelectedPages() { + if (_page_numbers.get_sensitive()) { + return _current_pages; + } + return "all"; +} + +PdfImportType PdfImportDialog::getImportMethod() +{ + auto &import_type = UI::get_widget<Gtk::Notebook>(_builder, "import-type"); + return (PdfImportType)import_type.get_current_page(); +} + +/** + * \brief Retrieves the current settings into a repr which SvgBuilder will use + * for determining the behaviour desired by the user + */ +void PdfImportDialog::getImportSettings(Inkscape::XML::Node *prefs) { + prefs->setAttribute("selectedPages", _current_pages); + + auto &clip_to = UI::get_widget<Gtk::ComboBox>(_builder, "clip-to"); + + prefs->setAttribute("cropTo", clip_to.get_active_id()); + prefs->setAttributeSvgDouble("approximationPrecision", _mesh_slider.get_value()); + prefs->setAttributeBoolean("embedImages", _embed_images.get_active()); +} + +/** + * \brief Redisplay the comment on the current approximation precision setting + * Evenly divides the interval of possible values between the available labels. + */ +void PdfImportDialog::_onPrecisionChanged() { + static Glib::ustring labels[] = { + Glib::ustring(C_("PDF input precision", "rough")), Glib::ustring(C_("PDF input precision", "medium")), + Glib::ustring(C_("PDF input precision", "fine")), Glib::ustring(C_("PDF input precision", "very fine"))}; + + auto adj = _mesh_slider.get_adjustment(); + double min = adj->get_lower(); + double value = adj->get_value() - min; + double max = adj->get_upper() - min; + double interval_len = max / (double)(sizeof(labels) / sizeof(labels[0])); + int comment_idx = (int)floor(value / interval_len); + _mesh_label.set_label(labels[comment_idx]); +} + +void PdfImportDialog::_onPageNumberChanged() +{ + _current_pages = _page_numbers.get_text(); + auto nums = parseIntRange(_current_pages, 1, _total_pages); + if (!nums.empty()) { + _setPreviewPage(*nums.begin()); + } +} + +/** + * Set a full list of all fonts in use for the whole PDF document. + */ +void PdfImportDialog::_setFonts(const FontList &fonts) +{ + _font_model->clear(); + _font_list = fonts; + + // Find all fonts on this one page + /*std::set<int> found; + FontInfoScanner page_scanner(_pdf_doc.get(), page-1); + for (const FontInfo *font : page_scanner.scan(page)) { + found.insert(font->getRef().num); + delete font; + }*/ + + // Now add all fonts and mark the ones from this page + for (auto pair : *fonts) { + auto font = pair.first; + auto &data = pair.second; + auto row = *_font_model->append(); + + row[_font_col->id] = font->getID()->num; + row[_font_col->em] = false; + row[_font_col->family] = data.family; + row[_font_col->style] = data.style; + row[_font_col->weight] = data.weight; + row[_font_col->stretch] = data.stretch; + // row[_font_col->pages] = data.pages; + + if (font->isCIDFont()) { + row[_font_col->icon] = Glib::ustring("text-convert-to-regular"); + } else { + row[_font_col->icon] = Glib::ustring(data.found ? "on" : "off-outline"); + } + } +} + +void PdfImportDialog::_fontRenderChanged() +{ + auto &font_render = UI::get_widget<Gtk::ComboBox>(_builder, "font-rendering"); + FontStrategy choice = (FontStrategy)std::stoi(font_render.get_active_id().c_str()); + setFontStrategies(SvgBuilder::autoFontStrategies(choice, _font_list)); +} + +/** + * Saves each decided font strategy to the Svg Builder object. + */ +FontStrategies PdfImportDialog::getFontStrategies() +{ + FontStrategies fs; + for (auto child : _font_model->children()) { + auto value = (FontFallback) int(child[_font_col->proc_id]); + fs[child[_font_col->id]] = value; + } + return fs; +} + +/** + * Update the font strats. + */ +void PdfImportDialog::setFontStrategies(const FontStrategies &fs) +{ + for (auto child : _font_model->children()) { + auto value = fs.at(child[_font_col->id]); + child[_font_col->proc_id] = (int)value; + switch (value) { + case FontFallback::AS_SHAPES: + child[_font_col->proc_label] = _("Convert to paths"); + break; + case FontFallback::AS_TEXT: + child[_font_col->proc_label] = _("Keep original font name"); + break; + case FontFallback::AS_SUB: + child[_font_col->proc_label] = _("Replace by closest-named installed font"); + break; + case FontFallback::DELETE_TEXT: + child[_font_col->proc_label] = _("Delete text"); + break; + } + } +} + +#ifdef HAVE_POPPLER_CAIRO +/** + * \brief Copies image data from a Cairo surface to a pixbuf + * + * Borrowed from libpoppler, from the file poppler-page.cc + * Copyright (C) 2005, Red Hat, Inc. + * + */ +static void copy_cairo_surface_to_pixbuf (cairo_surface_t *surface, + unsigned char *data, + GdkPixbuf *pixbuf) +{ + int cairo_width, cairo_height, cairo_rowstride; + unsigned char *pixbuf_data, *dst, *cairo_data; + int pixbuf_rowstride, pixbuf_n_channels; + unsigned int *src; + int x, y; + + cairo_width = cairo_image_surface_get_width (surface); + cairo_height = cairo_image_surface_get_height (surface); + cairo_rowstride = cairo_width * 4; + cairo_data = data; + + pixbuf_data = gdk_pixbuf_get_pixels (pixbuf); + pixbuf_rowstride = gdk_pixbuf_get_rowstride (pixbuf); + pixbuf_n_channels = gdk_pixbuf_get_n_channels (pixbuf); + + if (cairo_width > gdk_pixbuf_get_width (pixbuf)) + cairo_width = gdk_pixbuf_get_width (pixbuf); + if (cairo_height > gdk_pixbuf_get_height (pixbuf)) + cairo_height = gdk_pixbuf_get_height (pixbuf); + for (y = 0; y < cairo_height; y++) + { + src = reinterpret_cast<unsigned int *>(cairo_data + y * cairo_rowstride); + dst = pixbuf_data + y * pixbuf_rowstride; + for (x = 0; x < cairo_width; x++) + { + dst[0] = (*src >> 16) & 0xff; + dst[1] = (*src >> 8) & 0xff; + dst[2] = (*src >> 0) & 0xff; + if (pixbuf_n_channels == 4) + dst[3] = (*src >> 24) & 0xff; + dst += pixbuf_n_channels; + src++; + } + } +} + +#endif + +bool PdfImportDialog::_onDraw(const Cairo::RefPtr<Cairo::Context>& cr) { + // Check if we have a thumbnail at all + if (!_thumb_data) { + return true; + } + + // Create the pixbuf for the thumbnail + Glib::RefPtr<Gdk::Pixbuf> thumb; + + if (_render_thumb) { + thumb = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB, true, + 8, _thumb_width, _thumb_height); + } else { + thumb = Gdk::Pixbuf::create_from_data(_thumb_data, Gdk::COLORSPACE_RGB, + false, 8, _thumb_width, _thumb_height, _thumb_rowstride); + } + if (!thumb) { + return true; + } + + // Set background to white + if (_render_thumb) { + thumb->fill(0xffffffff); + Gdk::Cairo::set_source_pixbuf(cr, thumb, 0, 0); + cr->paint(); + } +#ifdef HAVE_POPPLER_CAIRO + // Copy the thumbnail image from the Cairo surface + if (_render_thumb) { + copy_cairo_surface_to_pixbuf(_cairo_surface, _thumb_data, thumb->gobj()); + } +#endif + + Gdk::Cairo::set_source_pixbuf(cr, thumb, 0, _render_thumb ? 0 : 20); + cr->paint(); + return true; +} + +/** + * \brief Renders the given page's thumbnail using Cairo + */ +void PdfImportDialog::_setPreviewPage(int page) { + _previewed_page = _pdf_doc->getCatalog()->getPage(page); + g_return_if_fail(_previewed_page); + + // Update the UI to select a different page + _preview_page = page; + _next_page.set_sensitive(page < _total_pages); + _prev_page.set_sensitive(page > 1); + std::ostringstream example; + example << page << " / " << _total_pages; + _current_page.set_label(example.str()); + + // Update the font list with per-page highlighting + // XXX Update this psuedo code with real code + /*for (auto iter : _font_model->children()) { + std::unorderd_list<int> *pages = row[_font_col->pages]; + row[_font_col->em] = bool(page in pages); + }*/ + + // Try to get a thumbnail from the PDF if possible + if (!_render_thumb) { + if (_thumb_data) { + gfree(_thumb_data); + _thumb_data = nullptr; + } + if (!_previewed_page->loadThumb(&_thumb_data, + &_thumb_width, &_thumb_height, &_thumb_rowstride)) { + return; + } + // Redraw preview area + _preview_area.set_size_request(_thumb_width, _thumb_height + 20); + _preview_area.queue_draw(); + return; + } +#ifdef HAVE_POPPLER_CAIRO + // Get page size by accounting for rotation + double width, height; + int rotate = _previewed_page->getRotate(); + if ( rotate == 90 || rotate == 270 ) { + height = _previewed_page->getCropWidth(); + width = _previewed_page->getCropHeight(); + } else { + width = _previewed_page->getCropWidth(); + height = _previewed_page->getCropHeight(); + } + // Calculate the needed scaling for the page + double scale_x = (double)_preview_width / width; + double scale_y = (double)_preview_height / height; + double scale_factor = ( scale_x > scale_y ) ? scale_y : scale_x; + // Create new Cairo surface + _thumb_width = (int)ceil( width * scale_factor ); + _thumb_height = (int)ceil( height * scale_factor ); + _thumb_rowstride = _thumb_width * 4; + if (_thumb_data) { + gfree(_thumb_data); + } + _thumb_data = reinterpret_cast<unsigned char *>(gmalloc(_thumb_rowstride * _thumb_height)); + if (_cairo_surface) { + cairo_surface_destroy(_cairo_surface); + } + _cairo_surface = cairo_image_surface_create_for_data(_thumb_data, + CAIRO_FORMAT_ARGB32, _thumb_width, _thumb_height, _thumb_rowstride); + cairo_t *cr = cairo_create(_cairo_surface); + cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0); // Set fill color to white + cairo_paint(cr); // Clear it + cairo_scale(cr, scale_factor, scale_factor); // Use Cairo for resizing the image + // Render page + if (_poppler_doc != NULL) { + PopplerPage *poppler_page = poppler_document_get_page(_poppler_doc, page-1); + poppler_page_render(poppler_page, cr); + g_object_unref(G_OBJECT(poppler_page)); + } + // Clean up + cairo_destroy(cr); + // Redraw preview area + _preview_area.set_size_request(_preview_width, _preview_height); + _preview_area.queue_draw(); +#endif +} + +//////////////////////////////////////////////////////////////////////////////// + +#ifdef HAVE_POPPLER_CAIRO +/// helper method +static cairo_status_t + _write_ustring_cb(void *closure, const unsigned char *data, unsigned int length) +{ + Glib::ustring* stream = static_cast<Glib::ustring*>(closure); + stream->append(reinterpret_cast<const char*>(data), length); + + return CAIRO_STATUS_SUCCESS; +} +#endif + +/** + * Parses the selected page of the given PDF document using PdfParser. + */ +SPDocument * +PdfInput::open(::Inkscape::Extension::Input * /*mod*/, const gchar * uri) { + + // Initialize the globalParams variable for poppler + if (!globalParams) { + globalParams = _POPPLER_NEW_GLOBAL_PARAMS(); + } + + + // Open the file using poppler + // PDFDoc is from poppler. PDFDoc is used for preview and for native import. + std::shared_ptr<PDFDoc> pdf_doc; + + // poppler does not use glib g_open. So on win32 we must use unicode call. code was copied from + // glib gstdio.c + pdf_doc = _POPPLER_MAKE_SHARED_PDFDOC(uri); // TODO: Could ask for password + + if (!pdf_doc->isOk()) { + int error = pdf_doc->getErrorCode(); + if (error == errEncrypted) { + g_message("Document is encrypted."); + } else if (error == errOpenFile) { + g_message("couldn't open the PDF file."); + } else if (error == errBadCatalog) { + g_message("couldn't read the page catalog."); + } else if (error == errDamaged) { + g_message("PDF file was damaged and couldn't be repaired."); + } else if (error == errHighlightFile) { + g_message("nonexistent or invalid highlight file."); + } else if (error == errBadPrinter) { + g_message("invalid printer."); + } else if (error == errPrinting) { + g_message("Error during printing."); + } else if (error == errPermission) { + g_message("PDF file does not allow that operation."); + } else if (error == errBadPageNum) { + g_message("invalid page number."); + } else if (error == errFileIO) { + g_message("file IO error."); + } else { + g_message("Failed to load document from data (error %d)", error); + } + + return nullptr; + } + + + std::unique_ptr<PdfImportDialog> dlg; + if (INKSCAPE.use_gui()) { + dlg = std::make_unique<PdfImportDialog>(pdf_doc, uri); + if (!dlg->showDialog()) { + throw Input::open_cancelled(); + } + } + + // Get options + std::string page_nums = "1"; + PdfImportType import_method = PdfImportType::PDF_IMPORT_INTERNAL; + FontStrategies font_strats; + if (dlg) { + page_nums = dlg->getSelectedPages(); + import_method = dlg->getImportMethod(); + font_strats = dlg->getFontStrategies(); + } else { + page_nums = INKSCAPE.get_pages(); + auto strat = (FontStrategy)INKSCAPE.get_pdf_font_strategy(); + font_strats = SvgBuilder::autoFontStrategies(strat, getPdfFonts(pdf_doc)); +#ifdef HAVE_POPPLER_CAIRO + import_method = (PdfImportType)INKSCAPE.get_pdf_poppler(); +#endif + } + // Both poppler and poppler+cairo can get page num info from poppler. + auto pages = parseIntRange(page_nums, 1, pdf_doc->getCatalog()->getNumPages()); + if (pages.empty()) { + g_warning("No pages selected, getting first page only."); + pages.insert(1); + } + + // Create Inkscape document from file + SPDocument *doc = nullptr; + bool saved = false; + if (import_method == PdfImportType::PDF_IMPORT_INTERNAL) { + // Create document + doc = SPDocument::createNewDoc(nullptr, true, true); + saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); // No need to undo in this temporary document + + // Create builder + gchar *docname = g_path_get_basename(uri); + gchar *dot = g_strrstr(docname, "."); + if (dot) { + *dot = 0; + } + SvgBuilder *builder = new SvgBuilder(doc, docname, pdf_doc->getXRef()); + builder->setFontStrategies(font_strats); + + // Get preferences + Inkscape::XML::Node *prefs = builder->getPreferences(); + if (dlg) + dlg->getImportSettings(prefs); + + for (auto p : pages) { + // And then add each of the pages + add_builder_page(pdf_doc, builder, doc, p); + } + + delete builder; + g_free(docname); +#ifdef HAVE_POPPLER_CAIRO + } else if (import_method == PdfImportType::PDF_IMPORT_CAIRO) { + // the poppler import + + std::string full_path = uri; + if (!Glib::path_is_absolute(uri)) { + full_path = Glib::build_filename(Glib::get_current_dir(),uri); + } + Glib::ustring full_uri = Glib::filename_to_uri(full_path); + + GError *error = NULL; + /// @todo handle password + /// @todo check if win32 unicode needs special attention + PopplerDocument* document = poppler_document_new_from_file(full_uri.c_str(), NULL, &error); + + if(error != NULL) { + std::cerr << "PDFInput::open: error opening document: " << full_uri.raw() << std::endl; + g_error_free (error); + return nullptr; + } + + int page_num = *pages.begin(); + if (PopplerPage* page = poppler_document_get_page(document, page_num - 1)) { + double width, height; + poppler_page_get_size(page, &width, &height); + + Glib::ustring output; + cairo_surface_t* surface = cairo_svg_surface_create_for_stream(Inkscape::Extension::Internal::_write_ustring_cb, + &output, width, height); + + // Reset back to PT for cairo 1.17.6 and above which sets to UNIT_USER + cairo_svg_surface_set_document_unit(surface, CAIRO_SVG_UNIT_PT); + + // This magical function results in more fine-grain fallbacks. In particular, a mesh + // gradient won't necessarily result in the whole PDF being rasterized. Of course, SVG + // 1.2 never made it as a standard, but hey, we'll take what we can get. This trick was + // found by examining the 'pdftocairo' code. + cairo_svg_surface_restrict_to_version( surface, CAIRO_SVG_VERSION_1_2 ); + + cairo_t* cr = cairo_create(surface); + + poppler_page_render_for_printing(page, cr); + cairo_show_page(cr); + + cairo_destroy(cr); + cairo_surface_destroy(surface); + + doc = SPDocument::createNewDocFromMem(output.c_str(), output.length(), TRUE); + + g_object_unref(G_OBJECT(page)); + } else if (document) { + std::cerr << "PDFInput::open: error opening page " << page_num << " of document: " << full_uri.raw() << std::endl; + } + g_object_unref(G_OBJECT(document)); + + if (!doc) { + return nullptr; + } + + saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); // No need to undo in this temporary document +#endif + } + + // Set viewBox if it doesn't exist + if (!doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + + // Restore undo + DocumentUndo::setUndoSensitive(doc, saved); + + return doc; +} + +/** + * Parses the selected page object of the given PDF document using PdfParser. + */ +void +PdfInput::add_builder_page(std::shared_ptr<PDFDoc>pdf_doc, SvgBuilder *builder, SPDocument *doc, int page_num) +{ + Inkscape::XML::Node *prefs = builder->getPreferences(); + + // Check page exists + Catalog *catalog = pdf_doc->getCatalog(); + sanitize_page_number(page_num, catalog->getNumPages()); + Page *page = catalog->getPage(page_num); + if (!page) { + std::cerr << "PDFInput::open: error opening page " << page_num << std::endl; + return; + } + + // Apply crop settings + _POPPLER_CONST PDFRectangle *clipToBox = nullptr; + + switch (prefs->getAttributeInt("cropTo", -1)) { + case 0: // Media box + clipToBox = page->getMediaBox(); + break; + case 1: // Crop box + clipToBox = page->getCropBox(); + break; + case 2: // Trim box + clipToBox = page->getTrimBox(); + break; + case 3: // Bleed box + clipToBox = page->getBleedBox(); + break; + case 4: // Art box + clipToBox = page->getArtBox(); + break; + default: + break; + } + + // Create parser (extension/internal/pdfinput/pdf-parser.h) + PdfParser *pdf_parser = new PdfParser(pdf_doc, builder, page, clipToBox); + + // Set up approximation precision for parser. Used for converting Mesh Gradients into tiles. + double color_delta = prefs->getAttributeDouble("approximationPrecision", 2.0); + if ( color_delta <= 0.0 ) { + color_delta = 1.0 / 2.0; + } else { + color_delta = 1.0 / color_delta; + } + for ( int i = 1 ; i <= pdfNumShadingTypes ; i++ ) { + pdf_parser->setApproximationPrecision(i, color_delta, 6); + } + + // Parse the document structure +#if defined(POPPLER_NEW_OBJECT_API) + Object obj = page->getContents(); +#else + Object obj; + page->getContents(&obj); +#endif + if (!obj.isNull()) { + pdf_parser->parse(&obj); + } + + // Cleanup +#if !defined(POPPLER_NEW_OBJECT_API) + obj.free(); +#endif + delete pdf_parser; +} + +#include "../clear-n_.h" + +void PdfInput::init() { + /* PDF in */ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("PDF Input") "</name>\n" + "<id>org.inkscape.input.pdf</id>\n" + "<input>\n" + "<extension>.pdf</extension>\n" + "<mimetype>application/pdf</mimetype>\n" + "<filetypename>" N_("Portable Document Format (*.pdf)") "</filetypename>\n" + "<filetypetooltip>" N_("Portable Document Format") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new PdfInput()); + // clang-format on + + /* AI in */ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("AI Input") "</name>\n" + "<id>org.inkscape.input.ai</id>\n" + "<input>\n" + "<extension>.ai</extension>\n" + "<mimetype>image/x-adobe-illustrator</mimetype>\n" + "<filetypename>" N_("Adobe Illustrator 9.0 and above (*.ai)") "</filetypename>\n" + "<filetypetooltip>" N_("Open files saved in Adobe Illustrator 9.0 and newer versions") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new PdfInput()); + // clang-format on +} // init + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* HAVE_POPPLER */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/pdfinput/pdf-input.h b/src/extension/internal/pdfinput/pdf-input.h new file mode 100644 index 0000000..e6d6c55 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-input.h @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_EXTENSION_INTERNAL_PDFINPUT_H +#define SEEN_EXTENSION_INTERNAL_PDFINPUT_H + +/* + * Authors: + * miklos erdelyi + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +#include <gtkmm.h> +#include <gtkmm/dialog.h> + +#include "../../implementation/implementation.h" +#include "poppler-transition-api.h" +#include "poppler-utils.h" +#include "svg-builder.h" + +#ifdef HAVE_POPPLER_CAIRO +struct _PopplerDocument; +typedef struct _PopplerDocument PopplerDocument; +#endif + +struct _GdkEventExpose; +typedef _GdkEventExpose GdkEventExpose; + +class Page; +class PDFDoc; + +namespace Gtk { + class Button; + class CheckButton; + class ComboBoxText; + class DrawingArea; + class Frame; + class Scale; + class RadioButton; + class Box; + class Label; + class Entry; +} + +namespace Inkscape { + +namespace UI { +namespace Widget { + class SpinButton; + class Frame; +} +} + +enum class PdfImportType : unsigned char +{ + PDF_IMPORT_INTERNAL, + PDF_IMPORT_CAIRO, +}; + +namespace Extension { +namespace Internal { + +class FontModelColumns; + +/** + * PDF import using libpoppler. + */ +class PdfImportDialog : public Gtk::Dialog +{ +public: + PdfImportDialog(std::shared_ptr<PDFDoc> doc, const gchar *uri); + ~PdfImportDialog() override; + + bool showDialog(); + std::string getSelectedPages(); + PdfImportType getImportMethod(); + void getImportSettings(Inkscape::XML::Node *prefs); + FontStrategies getFontStrategies(); + void setFontStrategies(const FontStrategies &fs); + +private: + void _fontRenderChanged(); + void _setPreviewPage(int page); + void _setFonts(const FontList &fonts); + + // Signal handlers + bool _onDraw(const Cairo::RefPtr<Cairo::Context>& cr); + void _onPageNumberChanged(); + void _onPrecisionChanged(); + + Glib::RefPtr<Gtk::Builder> _builder; + + Gtk::Entry &_page_numbers; + Gtk::DrawingArea &_preview_area; + Gtk::CheckButton &_embed_images; + Gtk::Scale &_mesh_slider; + Gtk::Label &_mesh_label; + Gtk::Button &_next_page; + Gtk::Button &_prev_page; + Gtk::Label &_current_page; + Glib::RefPtr<Gtk::ListStore> _font_model; + FontModelColumns *_font_col; + + std::shared_ptr<PDFDoc> _pdf_doc; // Document to be imported + std::string _current_pages; // Current selected pages + FontList _font_list; // List of fonts and the pages they appear on + int _total_pages = 0; + int _preview_page = 1; + Page *_previewed_page; // Currently previewed page + unsigned char *_thumb_data; // Thumbnail image data + int _thumb_width, _thumb_height; // Thumbnail size + int _thumb_rowstride; + int _preview_width, _preview_height; // Size of the preview area + bool _render_thumb; // Whether we can/shall render thumbnails +#ifdef HAVE_POPPLER_CAIRO + cairo_surface_t *_cairo_surface = nullptr; + PopplerDocument *_poppler_doc = nullptr; +#endif +}; + + +class PdfInput: public Inkscape::Extension::Implementation::Implementation { + PdfInput () = default;; +public: + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); +private: + void add_builder_page( + std::shared_ptr<PDFDoc> pdf_doc, + SvgBuilder *builder, SPDocument *doc, + int page_num); +}; + +} // namespace Implementation +} // namespace Extension +} // namespace Inkscape + +#endif // HAVE_POPPLER + +#endif // SEEN_EXTENSION_INTERNAL_PDFINPUT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/pdfinput/pdf-parser.cpp b/src/extension/internal/pdfinput/pdf-parser.cpp new file mode 100644 index 0000000..1d1df91 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-parser.cpp @@ -0,0 +1,3196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF parsing using libpoppler. + *//* + * Authors: + * Derived from poppler's Gfx.cc, which was derived from Xpdf by 1996-2003 Glyph & Cog, LLC + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER + +#ifdef USE_GCC_PRAGMAS +#pragma implementation +#endif + +#include <cmath> +#include <cstddef> +#include <cstdio> +#include <cstdlib> +#include <cstring> + +#include "2geom/transforms.h" +#include "Annot.h" +#include "Array.h" +#include "CharTypes.h" +#include "Dict.h" +#include "Error.h" +#include "Gfx.h" +#include "GfxFont.h" +#include "GfxState.h" +#include "GlobalParams.h" +#include "Lexer.h" +#include "Object.h" +#include "OutputDev.h" +#include "PDFDoc.h" +#include "Page.h" +#include "Parser.h" +#include "Stream.h" +#include "glib/poppler-features.h" +#include "goo/GooString.h" +#include "goo/gmem.h" +#include "pdf-parser.h" +#include "pdf-utils.h" +#include "poppler-cairo-font-engine.h" +#include "poppler-transition-api.h" +#include "poppler-utils.h" +#include "svg-builder.h" +#include "util/units.h" + +// the MSVC math.h doesn't define this +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +//------------------------------------------------------------------------ +// constants +//------------------------------------------------------------------------ + +// Default max delta allowed in any color component for a shading fill. +#define defaultShadingColorDelta (dblToCol( 1 / 2.0 )) + +// Default max recursive depth for a shading fill. +#define defaultShadingMaxDepth 6 + +// Max number of operators kept in the history list. +#define maxOperatorHistoryDepth 16 + +//------------------------------------------------------------------------ +// Operator table +//------------------------------------------------------------------------ + +PdfOperator PdfParser::opTab[] = { + {"\"", 3, {tchkNum, tchkNum, tchkString}, + &PdfParser::opMoveSetShowText}, + {"'", 1, {tchkString}, + &PdfParser::opMoveShowText}, + {"B", 0, {tchkNone}, + &PdfParser::opFillStroke}, + {"B*", 0, {tchkNone}, + &PdfParser::opEOFillStroke}, + {"BDC", 2, {tchkName, tchkProps}, + &PdfParser::opBeginMarkedContent}, + {"BI", 0, {tchkNone}, + &PdfParser::opBeginImage}, + {"BMC", 1, {tchkName}, + &PdfParser::opBeginMarkedContent}, + {"BT", 0, {tchkNone}, + &PdfParser::opBeginText}, + {"BX", 0, {tchkNone}, + &PdfParser::opBeginIgnoreUndef}, + {"CS", 1, {tchkName}, + &PdfParser::opSetStrokeColorSpace}, + {"DP", 2, {tchkName, tchkProps}, + &PdfParser::opMarkPoint}, + {"Do", 1, {tchkName}, + &PdfParser::opXObject}, + {"EI", 0, {tchkNone}, + &PdfParser::opEndImage}, + {"EMC", 0, {tchkNone}, + &PdfParser::opEndMarkedContent}, + {"ET", 0, {tchkNone}, + &PdfParser::opEndText}, + {"EX", 0, {tchkNone}, + &PdfParser::opEndIgnoreUndef}, + {"F", 0, {tchkNone}, + &PdfParser::opFill}, + {"G", 1, {tchkNum}, + &PdfParser::opSetStrokeGray}, + {"ID", 0, {tchkNone}, + &PdfParser::opImageData}, + {"J", 1, {tchkInt}, + &PdfParser::opSetLineCap}, + {"K", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetStrokeCMYKColor}, + {"M", 1, {tchkNum}, + &PdfParser::opSetMiterLimit}, + {"MP", 1, {tchkName}, + &PdfParser::opMarkPoint}, + {"Q", 0, {tchkNone}, + &PdfParser::opRestore}, + {"RG", 3, {tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetStrokeRGBColor}, + {"S", 0, {tchkNone}, + &PdfParser::opStroke}, + {"SC", -4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetStrokeColor}, + {"SCN", -33, {tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN,}, + &PdfParser::opSetStrokeColorN}, + {"T*", 0, {tchkNone}, + &PdfParser::opTextNextLine}, + {"TD", 2, {tchkNum, tchkNum}, + &PdfParser::opTextMoveSet}, + {"TJ", 1, {tchkArray}, + &PdfParser::opShowSpaceText}, + {"TL", 1, {tchkNum}, + &PdfParser::opSetTextLeading}, + {"Tc", 1, {tchkNum}, + &PdfParser::opSetCharSpacing}, + {"Td", 2, {tchkNum, tchkNum}, + &PdfParser::opTextMove}, + {"Tf", 2, {tchkName, tchkNum}, + &PdfParser::opSetFont}, + {"Tj", 1, {tchkString}, + &PdfParser::opShowText}, + {"Tm", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opSetTextMatrix}, + {"Tr", 1, {tchkInt}, + &PdfParser::opSetTextRender}, + {"Ts", 1, {tchkNum}, + &PdfParser::opSetTextRise}, + {"Tw", 1, {tchkNum}, + &PdfParser::opSetWordSpacing}, + {"Tz", 1, {tchkNum}, + &PdfParser::opSetHorizScaling}, + {"W", 0, {tchkNone}, + &PdfParser::opClip}, + {"W*", 0, {tchkNone}, + &PdfParser::opEOClip}, + {"b", 0, {tchkNone}, + &PdfParser::opCloseFillStroke}, + {"b*", 0, {tchkNone}, + &PdfParser::opCloseEOFillStroke}, + {"c", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opCurveTo}, + {"cm", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opConcat}, + {"cs", 1, {tchkName}, + &PdfParser::opSetFillColorSpace}, + {"d", 2, {tchkArray, tchkNum}, + &PdfParser::opSetDash}, + {"d0", 2, {tchkNum, tchkNum}, + &PdfParser::opSetCharWidth}, + {"d1", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opSetCacheDevice}, + {"f", 0, {tchkNone}, + &PdfParser::opFill}, + {"f*", 0, {tchkNone}, + &PdfParser::opEOFill}, + {"g", 1, {tchkNum}, + &PdfParser::opSetFillGray}, + {"gs", 1, {tchkName}, + &PdfParser::opSetExtGState}, + {"h", 0, {tchkNone}, + &PdfParser::opClosePath}, + {"i", 1, {tchkNum}, + &PdfParser::opSetFlat}, + {"j", 1, {tchkInt}, + &PdfParser::opSetLineJoin}, + {"k", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetFillCMYKColor}, + {"l", 2, {tchkNum, tchkNum}, + &PdfParser::opLineTo}, + {"m", 2, {tchkNum, tchkNum}, + &PdfParser::opMoveTo}, + {"n", 0, {tchkNone}, + &PdfParser::opEndPath}, + {"q", 0, {tchkNone}, + &PdfParser::opSave}, + {"re", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opRectangle}, + {"rg", 3, {tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetFillRGBColor}, + {"ri", 1, {tchkName}, + &PdfParser::opSetRenderingIntent}, + {"s", 0, {tchkNone}, + &PdfParser::opCloseStroke}, + {"sc", -4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetFillColor}, + {"scn", -33, {tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN,}, + &PdfParser::opSetFillColorN}, + {"sh", 1, {tchkName}, + &PdfParser::opShFill}, + {"v", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opCurveTo1}, + {"w", 1, {tchkNum}, + &PdfParser::opSetLineWidth}, + {"y", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opCurveTo2} +}; + +#define numOps (sizeof(opTab) / sizeof(PdfOperator)) + +namespace { + +GfxPatch blankPatch() +{ + GfxPatch patch; + memset(&patch, 0, sizeof(patch)); // quick-n-dirty + return patch; +} + +} // namespace + +//------------------------------------------------------------------------ +// PdfParser +//------------------------------------------------------------------------ + +PdfParser::PdfParser(std::shared_ptr<PDFDoc> pdf_doc, Inkscape::Extension::Internal::SvgBuilder *builderA, Page *page, + _POPPLER_CONST PDFRectangle *cropBox) + : _pdf_doc(pdf_doc) + , xref(pdf_doc->getXRef()) + , builder(builderA) + , subPage(false) + , printCommands(false) + , res(new GfxResources(xref, page->getResourceDict(), nullptr)) + , // start the resource stack + state(new GfxState(96.0, 96.0, page->getCropBox(), page->getRotate(), true)) + , fontChanged(gFalse) + , clip(clipNone) + , ignoreUndef(0) + , formDepth(0) + , parser(nullptr) + , colorDeltas() + , maxDepths() + , operatorHistory(nullptr) +{ + setDefaultApproximationPrecision(); + loadOptionalContentLayers(page->getResourceDict()); + loadColorProfile(); + baseMatrix = stateToAffine(state); + + if (page) { + // Increment the page building here and set page label + Catalog *catalog = pdf_doc->getCatalog(); + GooString *label = new GooString(""); + catalog->indexToLabel(page->getNum() - 1, label); + builder->pushPage(label->getCString(), state); + } + + // Must come after pushPage! + builder->setDocumentSize(state->getPageWidth(), state->getPageHeight()); + + // Set margins, bleeds and page-cropping + auto page_box = getRect(page->getCropBox()); + auto scale = Geom::Scale(state->getPageWidth() / page_box.width(), + state->getPageHeight() / page_box.height()); + builder->setMargins(getRect(page->getTrimBox()) * scale, + getRect(page->getArtBox()) * scale, + getRect(page->getBleedBox()) * scale); + if (cropBox && getRect(cropBox) != page_box) { + builder->cropPage(getRect(cropBox) * scale); + } + + saveState(); + formDepth = 0; + + pushOperator("startPage"); +} + +PdfParser::PdfParser(XRef *xrefA, Inkscape::Extension::Internal::SvgBuilder *builderA, Dict *resDict, + _POPPLER_CONST PDFRectangle *box) + : xref(xrefA) + , builder(builderA) + , subPage(true) + , printCommands(false) + , res(new GfxResources(xref, resDict, nullptr)) + , // start the resource stack + state(new GfxState(72, 72, box, 0, false)) + , fontChanged(gFalse) + , clip(clipNone) + , ignoreUndef(0) + , formDepth(0) + , parser(nullptr) + , colorDeltas() + , maxDepths() + , operatorHistory(nullptr) +{ + setDefaultApproximationPrecision(); + baseMatrix = stateToAffine(state); + formDepth = 0; +} + +PdfParser::~PdfParser() { + while(operatorHistory) { + OpHistoryEntry *tmp = operatorHistory->next; + delete operatorHistory; + operatorHistory = tmp; + } + + while (state && state->hasSaves()) { + restoreState(); + } + + if (!subPage) { + //out->endPage(); + } + + while (res) { + popResources(); + } + + if (state) { + delete state; + state = nullptr; + } +} + +void PdfParser::parse(Object *obj, GBool topLevel) { + Object obj2; + + if (obj->isArray()) { + for (int i = 0; i < obj->arrayGetLength(); ++i) { + _POPPLER_CALL_ARGS(obj2, obj->arrayGet, i); + if (!obj2.isStream()) { + error(errInternal, -1, "Weird page contents"); + _POPPLER_FREE(obj2); + return; + } + _POPPLER_FREE(obj2); + } + } else if (!obj->isStream()) { + error(errInternal, -1, "Weird page contents"); + return; + } + parser = new _POPPLER_NEW_PARSER(xref, obj); + go(topLevel); + delete parser; + parser = nullptr; +} + +void PdfParser::go(GBool /*topLevel*/) +{ + Object obj; + Object args[maxArgs]; + + // scan a sequence of objects + int numArgs = 0; + _POPPLER_CALL(obj, parser->getObj); + while (!obj.isEOF()) { + + // got a command - execute it + if (obj.isCmd()) { + if (printCommands) { + obj.print(stdout); + for (int i = 0; i < numArgs; ++i) { + printf(" "); + args[i].print(stdout); + } + printf("\n"); + fflush(stdout); + } + + // Run the operation + execOp(&obj, args, numArgs); + +#if !defined(POPPLER_NEW_OBJECT_API) + _POPPLER_FREE(obj); + for (int i = 0; i < numArgs; ++i) + _POPPLER_FREE(args[i]); +#endif + numArgs = 0; + + // got an argument - save it + } else if (numArgs < maxArgs) { + args[numArgs++] = std::move(obj); + + // too many arguments - something is wrong + } else { + error(errSyntaxError, getPos(), "Too many args in content stream"); + if (printCommands) { + printf("throwing away arg: "); + obj.print(stdout); + printf("\n"); + fflush(stdout); + } + _POPPLER_FREE(obj); + } + + // grab the next object + _POPPLER_CALL(obj, parser->getObj); + } + _POPPLER_FREE(obj); + + // args at end with no command + if (numArgs > 0) { + error(errSyntaxError, getPos(), "Leftover args in content stream"); + if (printCommands) { + printf("%d leftovers:", numArgs); + for (int i = 0; i < numArgs; ++i) { + printf(" "); + args[i].print(stdout); + } + printf("\n"); + fflush(stdout); + } +#if !defined(POPPLER_NEW_OBJECT_API) + for (int i = 0; i < numArgs; ++i) + _POPPLER_FREE(args[i]); +#endif + } +} + +void PdfParser::pushOperator(const char *name) +{ + OpHistoryEntry *newEntry = new OpHistoryEntry; + newEntry->name = name; + newEntry->state = nullptr; + newEntry->depth = (operatorHistory != nullptr ? (operatorHistory->depth+1) : 0); + newEntry->next = operatorHistory; + operatorHistory = newEntry; + + // Truncate list if needed + if (operatorHistory->depth > maxOperatorHistoryDepth) { + OpHistoryEntry *curr = operatorHistory; + OpHistoryEntry *prev = nullptr; + while (curr && curr->next != nullptr) { + curr->depth--; + prev = curr; + curr = curr->next; + } + if (prev) { + if (curr->state != nullptr) + delete curr->state; + delete curr; + prev->next = nullptr; + } + } +} + +const char *PdfParser::getPreviousOperator(unsigned int look_back) { + OpHistoryEntry *prev = nullptr; + if (operatorHistory != nullptr && look_back > 0) { + prev = operatorHistory->next; + while (--look_back > 0 && prev != nullptr) { + prev = prev->next; + } + } + if (prev != nullptr) { + return prev->name; + } else { + return ""; + } +} + +void PdfParser::execOp(Object *cmd, Object args[], int numArgs) { + PdfOperator *op; + const char *name; + Object *argPtr; + int i; + + // find operator + name = cmd->getCmd(); + if (!(op = findOp(name))) { + if (ignoreUndef == 0) + error(errSyntaxError, getPos(), "Unknown operator '{0:s}'", name); + return; + } + + // type check args + argPtr = args; + if (op->numArgs >= 0) { + if (numArgs < op->numArgs) { + error(errSyntaxError, getPos(), "Too few ({0:d}) args to '{1:d}' operator", numArgs, name); + return; + } + if (numArgs > op->numArgs) { +#if 0 + error(errSyntaxError, getPos(), "Too many ({0:d}) args to '{1:s}' operator", numArgs, name); +#endif + argPtr += numArgs - op->numArgs; + numArgs = op->numArgs; + } + } else { + if (numArgs > -op->numArgs) { + error(errSyntaxError, getPos(), "Too many ({0:d}) args to '{1:s}' operator", + numArgs, name); + return; + } + } + for (i = 0; i < numArgs; ++i) { + if (!checkArg(&argPtr[i], op->tchk[i])) { + error(errSyntaxError, getPos(), "Arg #{0:d} to '{1:s}' operator is wrong type ({2:s})", + i, name, argPtr[i].getTypeName()); + return; + } + } + + // add to history + pushOperator((char*)&op->name); + + // do it + (this->*op->func)(argPtr, numArgs); +} + +PdfOperator* PdfParser::findOp(const char *name) { + int a = -1; + int b = numOps; + int cmp = -1; + // invariant: opTab[a] < name < opTab[b] + while (b - a > 1) { + const int m = (a + b) / 2; + cmp = strcmp(opTab[m].name, name); + if (cmp < 0) + a = m; + else if (cmp > 0) + b = m; + else + a = b = m; + } + if (cmp != 0) { + return nullptr; + } + return &opTab[a]; +} + +GBool PdfParser::checkArg(Object *arg, TchkType type) { + switch (type) { + case tchkBool: return arg->isBool(); + case tchkInt: return arg->isInt(); + case tchkNum: return arg->isNum(); + case tchkString: return arg->isString(); + case tchkName: return arg->isName(); + case tchkArray: return arg->isArray(); + case tchkProps: return arg->isDict() || arg->isName(); + case tchkSCN: return arg->isNum() || arg->isName(); + case tchkNone: return gFalse; + } + return gFalse; +} + +int PdfParser::getPos() { + return parser ? parser->getPos() : -1; +} + +//------------------------------------------------------------------------ +// graphics state operators +//------------------------------------------------------------------------ + +void PdfParser::opSave(Object /*args*/[], int /*numArgs*/) +{ + saveState(); +} + +void PdfParser::opRestore(Object /*args*/[], int /*numArgs*/) +{ + restoreState(); +} + +// TODO not good that numArgs is ignored but args[] is used: +/** + * Concatenate transformation matrix to the current state + */ +void PdfParser::opConcat(Object args[], int /*numArgs*/) +{ + state->concatCTM(args[0].getNum(), args[1].getNum(), + args[2].getNum(), args[3].getNum(), + args[4].getNum(), args[5].getNum()); + fontChanged = gTrue; +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetDash(Object args[], int /*numArgs*/) +{ + double *dash = nullptr; + + Array *a = args[0].getArray(); + int length = a->getLength(); + if (length != 0) { + dash = (double *)gmallocn(length, sizeof(double)); + for (int i = 0; i < length; ++i) { + Object obj; + dash[i] = _POPPLER_CALL_ARGS_DEREF(obj, a->get, i).getNum(); + _POPPLER_FREE(obj); + } + } +#if POPPLER_CHECK_VERSION(22, 9, 0) + state->setLineDash(std::vector<double> (dash, dash + length), args[1].getNum()); +#else + state->setLineDash(dash, length, args[1].getNum()); +#endif + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFlat(Object args[], int /*numArgs*/) +{ + state->setFlatness((int)args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetLineJoin(Object args[], int /*numArgs*/) +{ + state->setLineJoin(args[0].getInt()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetLineCap(Object args[], int /*numArgs*/) +{ + state->setLineCap(args[0].getInt()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetMiterLimit(Object args[], int /*numArgs*/) +{ + state->setMiterLimit(args[0].getNum()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetLineWidth(Object args[], int /*numArgs*/) +{ + state->setLineWidth(args[0].getNum()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetExtGState(Object args[], int /*numArgs*/) +{ + Object obj1, obj2, obj3, obj4, obj5; + Function *funcs[4] = {nullptr, nullptr, nullptr, nullptr}; + GfxColor backdropColor; + GBool haveBackdropColor = gFalse; + GBool alpha = gFalse; + + _POPPLER_CALL_ARGS(obj1, res->lookupGState, args[0].getName()); + if (obj1.isNull()) { + return; + } + if (!obj1.isDict()) { + error(errSyntaxError, getPos(), "ExtGState '{0:s}' is wrong type"), args[0].getName(); + _POPPLER_FREE(obj1); + return; + } + if (printCommands) { + printf(" gfx state dict: "); + obj1.print(); + printf("\n"); + } + + // transparency support: blend mode, fill/stroke opacity + if (!_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "BM").isNull()) { + GfxBlendMode mode = gfxBlendNormal; + if (state->parseBlendMode(&obj2, &mode)) { + state->setBlendMode(mode); + } else { + error(errSyntaxError, getPos(), "Invalid blend mode in ExtGState"); + } + } + _POPPLER_FREE(obj2); + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "ca").isNum()) { + state->setFillOpacity(obj2.getNum()); + } + _POPPLER_FREE(obj2); + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "CA").isNum()) { + state->setStrokeOpacity(obj2.getNum()); + } + _POPPLER_FREE(obj2); + + // fill/stroke overprint + GBool haveFillOP = gFalse; + if ((haveFillOP = _POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "op").isBool())) { + state->setFillOverprint(obj2.getBool()); + } + _POPPLER_FREE(obj2); + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "OP").isBool()) { + state->setStrokeOverprint(obj2.getBool()); + if (!haveFillOP) { + state->setFillOverprint(obj2.getBool()); + } + } + _POPPLER_FREE(obj2); + + // stroke adjust + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "SA").isBool()) { + state->setStrokeAdjust(obj2.getBool()); + } + _POPPLER_FREE(obj2); + + // transfer function + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "TR2").isNull()) { + _POPPLER_FREE(obj2); + _POPPLER_CALL_ARGS(obj2, obj1.dictLookup, "TR"); + } + if (obj2.isName(const_cast<char*>("Default")) || + obj2.isName(const_cast<char*>("Identity"))) { + funcs[0] = funcs[1] = funcs[2] = funcs[3] = nullptr; + state->setTransfer(funcs); + } else if (obj2.isArray() && obj2.arrayGetLength() == 4) { + int pos = 4; + for (int i = 0; i < 4; ++i) { + _POPPLER_CALL_ARGS(obj3, obj2.arrayGet, i); + funcs[i] = Function::parse(&obj3); + _POPPLER_FREE(obj3); + if (!funcs[i]) { + pos = i; + break; + } + } + if (pos == 4) { + state->setTransfer(funcs); + } + } else if (obj2.isName() || obj2.isDict() || obj2.isStream()) { + if ((funcs[0] = Function::parse(&obj2))) { + funcs[1] = funcs[2] = funcs[3] = nullptr; + state->setTransfer(funcs); + } + } else if (!obj2.isNull()) { + error(errSyntaxError, getPos(), "Invalid transfer function in ExtGState"); + } + _POPPLER_FREE(obj2); + + // soft mask + if (!_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "SMask").isNull()) { + if (obj2.isName(const_cast<char*>("None"))) { + // Do nothing. + } else if (obj2.isDict()) { + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "S").isName("Alpha")) { + alpha = gTrue; + } else { // "Luminosity" + alpha = gFalse; + } + _POPPLER_FREE(obj3); + funcs[0] = nullptr; + if (!_POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "TR").isNull()) { + funcs[0] = Function::parse(&obj3); + if (funcs[0]->getInputSize() != 1 || + funcs[0]->getOutputSize() != 1) { + error(errSyntaxError, getPos(), "Invalid transfer function in soft mask in ExtGState"); + delete funcs[0]; + funcs[0] = nullptr; + } + } + _POPPLER_FREE(obj3); + if ((haveBackdropColor = _POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "BC").isArray())) { + for (int & i : backdropColor.c) { + i = 0; + } + for (int i = 0; i < obj3.arrayGetLength() && i < gfxColorMaxComps; ++i) { + _POPPLER_CALL_ARGS(obj4, obj3.arrayGet, i); + if (obj4.isNum()) { + backdropColor.c[i] = dblToCol(obj4.getNum()); + } + _POPPLER_FREE(obj4); + } + } + _POPPLER_FREE(obj3); + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "G").isStream()) { + if (_POPPLER_CALL_ARGS_DEREF(obj4, obj3.streamGetDict()->lookup, "Group").isDict()) { + GfxColorSpace *blendingColorSpace = nullptr; + GBool isolated = gFalse; + GBool knockout = gFalse; + if (!_POPPLER_CALL_ARGS_DEREF(obj5, obj4.dictLookup, "CS").isNull()) { + blendingColorSpace = GfxColorSpace::parse(nullptr, &obj5, nullptr, state); + } + _POPPLER_FREE(obj5); + if (_POPPLER_CALL_ARGS_DEREF(obj5, obj4.dictLookup, "I").isBool()) { + isolated = obj5.getBool(); + } + _POPPLER_FREE(obj5); + if (_POPPLER_CALL_ARGS_DEREF(obj5, obj4.dictLookup, "K").isBool()) { + knockout = obj5.getBool(); + } + _POPPLER_FREE(obj5); + if (!haveBackdropColor) { + if (blendingColorSpace) { + blendingColorSpace->getDefaultColor(&backdropColor); + } else { + //~ need to get the parent or default color space (?) + for (int & i : backdropColor.c) { + i = 0; + } + } + } + doSoftMask(&obj3, alpha, blendingColorSpace, + isolated, knockout, funcs[0], &backdropColor); + if (funcs[0]) { + delete funcs[0]; + } + } else { + error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState - missing group"); + } + _POPPLER_FREE(obj4); + } else { + error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState - missing group"); + } + _POPPLER_FREE(obj3); + } else if (!obj2.isNull()) { + error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState"); + } + } + _POPPLER_FREE(obj2); + + _POPPLER_FREE(obj1); +} + +void PdfParser::doSoftMask(Object *str, GBool alpha, + GfxColorSpace *blendingColorSpace, + GBool isolated, GBool knockout, + Function *transferFunc, GfxColor *backdropColor) { + Dict *dict, *resDict; + double m[6], bbox[4]; + Object obj1, obj2; + int i; + + // check for excessive recursion + if (formDepth > 20) { + return; + } + + // get stream dict + dict = str->streamGetDict(); + + // check form type + _POPPLER_CALL_ARGS(obj1, dict->lookup, "FormType"); + if (!(obj1.isNull() || (obj1.isInt() && obj1.getInt() == 1))) { + error(errSyntaxError, getPos(), "Unknown form type"); + } + _POPPLER_FREE(obj1); + + // get bounding box + _POPPLER_CALL_ARGS(obj1, dict->lookup, "BBox"); + if (!obj1.isArray()) { + _POPPLER_FREE(obj1); + error(errSyntaxError, getPos(), "Bad form bounding box"); + return; + } + for (i = 0; i < 4; ++i) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, i); + bbox[i] = obj2.getNum(); + _POPPLER_FREE(obj2); + } + _POPPLER_FREE(obj1); + + // get matrix + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Matrix"); + if (obj1.isArray()) { + for (i = 0; i < 6; ++i) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, i); + m[i] = obj2.getNum(); + _POPPLER_FREE(obj2); + } + } else { + m[0] = 1; m[1] = 0; + m[2] = 0; m[3] = 1; + m[4] = 0; m[5] = 0; + } + _POPPLER_FREE(obj1); + + // get resources + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Resources"); + resDict = obj1.isDict() ? obj1.getDict() : (Dict *)nullptr; + + // draw it + ++formDepth; + doForm1(str, resDict, m, bbox, gTrue, gTrue, + blendingColorSpace, isolated, knockout, + alpha, transferFunc, backdropColor); + --formDepth; + + if (blendingColorSpace) { + delete blendingColorSpace; + } + _POPPLER_FREE(obj1); +} + +void PdfParser::opSetRenderingIntent(Object /*args*/[], int /*numArgs*/) +{ +} + +//------------------------------------------------------------------------ +// color operators +//------------------------------------------------------------------------ + +/** + * Get a newly allocated color space instance by CS operation argument. + * + * Maintains a cache for named color spaces to avoid expensive re-parsing. + */ +GfxColorSpace *PdfParser::lookupColorSpaceCopy(Object &arg) +{ + assert(!arg.isNull()); + GfxColorSpace *colorSpace = nullptr; + + if (char const *name = arg.isName() ? arg.getName() : nullptr) { + auto cache_name = std::to_string(formDepth) + "-" + std::string(name); + if (colorSpace = colorSpacesCache[cache_name].get()) { + return colorSpace->copy(); + } + + Object obj = res->lookupColorSpace(name); + if (obj.isNull()) { + colorSpace = GfxColorSpace::parse(res, &arg, nullptr, state); + } else { + colorSpace = GfxColorSpace::parse(res, &obj, nullptr, state); + } + + if (colorSpace && colorSpace->getMode() != csPattern) { + colorSpacesCache[cache_name].reset(colorSpace->copy()); + } + } else { + // We were passed in an object directly. + colorSpace = GfxColorSpace::parse(res, &arg, nullptr, state); + } + return colorSpace; +} + +/** + * Look up pattern/gradients from the GfxResource dictionary + */ +GfxPattern *PdfParser::lookupPattern(Object *obj, GfxState *state) +{ + if (!obj->isName()) + return nullptr; + return res->lookupPattern(obj->getName(), nullptr, state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillGray(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setFillPattern(nullptr); + state->setFillColorSpace(new GfxDeviceGrayColorSpace()); + color.c[0] = dblToCol(args[0].getNum()); + state->setFillColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeGray(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setStrokePattern(nullptr); + state->setStrokeColorSpace(new GfxDeviceGrayColorSpace()); + color.c[0] = dblToCol(args[0].getNum()); + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillCMYKColor(Object args[], int /*numArgs*/) +{ + GfxColor color; + int i; + + state->setFillPattern(nullptr); + state->setFillColorSpace(new GfxDeviceCMYKColorSpace()); + for (i = 0; i < 4; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setFillColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeCMYKColor(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setStrokePattern(nullptr); + state->setStrokeColorSpace(new GfxDeviceCMYKColorSpace()); + for (int i = 0; i < 4; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillRGBColor(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setFillPattern(nullptr); + state->setFillColorSpace(new GfxDeviceRGBColorSpace()); + for (int i = 0; i < 3; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setFillColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeRGBColor(Object args[], int /*numArgs*/) { + GfxColor color; + + state->setStrokePattern(nullptr); + state->setStrokeColorSpace(new GfxDeviceRGBColorSpace()); + for (int i = 0; i < 3; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillColorSpace(Object args[], int numArgs) +{ + assert(numArgs >= 1); + GfxColorSpace *colorSpace = lookupColorSpaceCopy(args[0]); + + state->setFillPattern(nullptr); + + if (colorSpace) { + GfxColor color; + state->setFillColorSpace(colorSpace); + colorSpace->getDefaultColor(&color); + state->setFillColor(&color); + builder->updateStyle(state); + } else { + error(errSyntaxError, getPos(), "Bad color space (fill)"); + } +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeColorSpace(Object args[], int numArgs) +{ + assert(numArgs >= 1); + GfxColorSpace *colorSpace = lookupColorSpaceCopy(args[0]); + + state->setStrokePattern(nullptr); + + if (colorSpace) { + GfxColor color; + state->setStrokeColorSpace(colorSpace); + colorSpace->getDefaultColor(&color); + state->setStrokeColor(&color); + builder->updateStyle(state); + } else { + error(errSyntaxError, getPos(), "Bad color space (stroke)"); + } +} + +void PdfParser::opSetFillColor(Object args[], int numArgs) { + GfxColor color; + int i; + + if (numArgs != state->getFillColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'sc' command"); + return; + } + state->setFillPattern(nullptr); + for (i = 0; i < numArgs; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setFillColor(&color); + builder->updateStyle(state); +} + +void PdfParser::opSetStrokeColor(Object args[], int numArgs) { + GfxColor color; + int i; + + if (numArgs != state->getStrokeColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SC' command"); + return; + } + state->setStrokePattern(nullptr); + for (i = 0; i < numArgs; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +void PdfParser::opSetFillColorN(Object args[], int numArgs) { + GfxColor color; + int i; + + if (state->getFillColorSpace()->getMode() == csPattern) { + if (numArgs > 1) { + if (!((GfxPatternColorSpace *)state->getFillColorSpace())->getUnder() || + numArgs - 1 != ((GfxPatternColorSpace *)state->getFillColorSpace()) + ->getUnder()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'scn' command"); + return; + } + for (i = 0; i < numArgs - 1 && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setFillColor(&color); + builder->updateStyle(state); + } + if (auto pattern = lookupPattern(&(args[numArgs - 1]), state)) { + state->setFillPattern(pattern); + builder->updateStyle(state); + } + + } else { + if (numArgs != state->getFillColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'scn' command"); + return; + } + state->setFillPattern(nullptr); + for (i = 0; i < numArgs && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setFillColor(&color); + builder->updateStyle(state); + } +} + +void PdfParser::opSetStrokeColorN(Object args[], int numArgs) { + GfxColor color; + int i; + + if (state->getStrokeColorSpace()->getMode() == csPattern) { + if (numArgs > 1) { + if (!((GfxPatternColorSpace *)state->getStrokeColorSpace()) + ->getUnder() || + numArgs - 1 != ((GfxPatternColorSpace *)state->getStrokeColorSpace()) + ->getUnder()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SCN' command"); + return; + } + for (i = 0; i < numArgs - 1 && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setStrokeColor(&color); + builder->updateStyle(state); + } + if (auto pattern = lookupPattern(&(args[numArgs - 1]), state)) { + state->setStrokePattern(pattern); + builder->updateStyle(state); + } + + } else { + if (numArgs != state->getStrokeColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SCN' command"); + return; + } + state->setStrokePattern(nullptr); + for (i = 0; i < numArgs && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setStrokeColor(&color); + builder->updateStyle(state); + } +} + +//------------------------------------------------------------------------ +// path segment operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opMoveTo(Object args[], int /*numArgs*/) +{ + state->moveTo(args[0].getNum(), args[1].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opLineTo(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in lineto"); + return; + } + state->lineTo(args[0].getNum(), args[1].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opCurveTo(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in curveto"); + return; + } + double x1 = args[0].getNum(); + double y1 = args[1].getNum(); + double x2 = args[2].getNum(); + double y2 = args[3].getNum(); + double x3 = args[4].getNum(); + double y3 = args[5].getNum(); + state->curveTo(x1, y1, x2, y2, x3, y3); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opCurveTo1(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in curveto1"); + return; + } + double x1 = state->getCurX(); + double y1 = state->getCurY(); + double x2 = args[0].getNum(); + double y2 = args[1].getNum(); + double x3 = args[2].getNum(); + double y3 = args[3].getNum(); + state->curveTo(x1, y1, x2, y2, x3, y3); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opCurveTo2(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in curveto2"); + return; + } + double x1 = args[0].getNum(); + double y1 = args[1].getNum(); + double x2 = args[2].getNum(); + double y2 = args[3].getNum(); + double x3 = x2; + double y3 = y2; + state->curveTo(x1, y1, x2, y2, x3, y3); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opRectangle(Object args[], int /*numArgs*/) +{ + double x = args[0].getNum(); + double y = args[1].getNum(); + double w = args[2].getNum(); + double h = args[3].getNum(); + state->moveTo(x, y); + state->lineTo(x + w, y); + state->lineTo(x + w, y + h); + state->lineTo(x, y + h); + state->closePath(); +} + +void PdfParser::opClosePath(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in closepath"); + return; + } + state->closePath(); +} + +//------------------------------------------------------------------------ +// path painting operators +//------------------------------------------------------------------------ + +void PdfParser::opEndPath(Object /*args*/[], int /*numArgs*/) +{ + doEndPath(); +} + +void PdfParser::opStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in stroke")); + return; + } + if (state->isPath()) { + if (state->getStrokeColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getStrokePattern())) { + doPatternStrokeFallback(); + } else { + builder->addPath(state, false, true); + } + } + doEndPath(); +} + +void PdfParser::opCloseStroke(Object * /*args[]*/, int /*numArgs*/) { + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in closepath/stroke")); + return; + } + state->closePath(); + if (state->isPath()) { + if (state->getStrokeColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getStrokePattern())) { + doPatternStrokeFallback(); + } else { + builder->addPath(state, false, true); + } + } + doEndPath(); +} + +void PdfParser::opFill(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in fill")); + return; + } + if (state->isPath()) { + if (state->getFillColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getFillPattern())) { + doPatternFillFallback(gFalse); + } else { + builder->addPath(state, true, false); + } + } + doEndPath(); +} + +void PdfParser::opEOFill(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in eofill")); + return; + } + if (state->isPath()) { + if (state->getFillColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getFillPattern())) { + doPatternFillFallback(gTrue); + } else { + builder->addPath(state, true, false, true); + } + } + doEndPath(); +} + +void PdfParser::opFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in fill/stroke")); + return; + } + if (state->isPath()) { + doFillAndStroke(gFalse); + } else { + builder->addPath(state, true, true); + } + doEndPath(); +} + +void PdfParser::opCloseFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in closepath/fill/stroke")); + return; + } + if (state->isPath()) { + state->closePath(); + doFillAndStroke(gFalse); + } + doEndPath(); +} + +void PdfParser::opEOFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in eofill/stroke")); + return; + } + if (state->isPath()) { + doFillAndStroke(gTrue); + } + doEndPath(); +} + +void PdfParser::opCloseEOFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in closepath/eofill/stroke")); + return; + } + if (state->isPath()) { + state->closePath(); + doFillAndStroke(gTrue); + } + doEndPath(); +} + +void PdfParser::doFillAndStroke(GBool eoFill) { + GBool fillOk = gTrue, strokeOk = gTrue; + if (state->getFillColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getFillPattern())) { + fillOk = gFalse; + } + if (state->getStrokeColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getStrokePattern())) { + strokeOk = gFalse; + } + if (fillOk && strokeOk) { + builder->addPath(state, true, true, eoFill); + } else { + doPatternFillFallback(eoFill); + doPatternStrokeFallback(); + } +} + +void PdfParser::doPatternFillFallback(GBool eoFill) { + GfxPattern *pattern; + + if (!(pattern = state->getFillPattern())) { + return; + } + switch (pattern->getType()) { + case 1: + break; + case 2: + doShadingPatternFillFallback(static_cast<GfxShadingPattern *>(pattern), gFalse, eoFill); + break; + default: + error(errUnimplemented, getPos(), "Unimplemented pattern type (%d) in fill", + pattern->getType()); + break; + } +} + +void PdfParser::doPatternStrokeFallback() { + GfxPattern *pattern; + + if (!(pattern = state->getStrokePattern())) { + return; + } + switch (pattern->getType()) { + case 1: + break; + case 2: + doShadingPatternFillFallback(static_cast<GfxShadingPattern *>(pattern), gTrue, gFalse); + break; + default: + error(errUnimplemented, getPos(), "Unimplemented pattern type ({0:d}) in stroke", + pattern->getType()); + break; + } +} + +void PdfParser::doShadingPatternFillFallback(GfxShadingPattern *sPat, + GBool stroke, GBool eoFill) { + GfxShading *shading; + GfxPath *savedPath; + + shading = sPat->getShading(); + + // save current graphics state + savedPath = state->getPath()->copy(); + saveState(); + + // clip to bbox + /*if (false ){//shading->getHasBBox()) { + double xMin, yMin, xMax, yMax; + shading->getBBox(&xMin, &yMin, &xMax, &yMax); + state->moveTo(xMin, yMin); + state->lineTo(xMax, yMin); + state->lineTo(xMax, yMax); + state->lineTo(xMin, yMax); + state->closePath(); + state->clip(); + state->setPath(savedPath->copy()); + }*/ + + // clip to current path + if (stroke) { + state->clipToStrokePath(); + } else { + state->clip(); + // XXX WARNING WE HAVE REMOVED THE SET CLIP + /*if (eoFill) { + builder->setClipPath(state, true); + } else { + builder->setClipPath(state); + }*/ + } + + // set the color space + state->setFillColorSpace(shading->getColorSpace()->copy()); + + // background color fill + if (shading->getHasBackground()) { + state->setFillColor(shading->getBackground()); + builder->addPath(state, true, false); + } + state->clearPath(); + + // construct a (pattern space) -> (current space) transform matrix + auto ptr = ctmToAffine(sPat->getMatrix()); + auto m = (ptr * baseMatrix) * stateToAffine(state).inverse(); + + // Set the new matrix + state->concatCTM(m[0], m[1], m[2], m[3], m[4], m[5]); + + // do shading type-specific operations + switch (shading->getType()) { + case 1: + doFunctionShFill(static_cast<GfxFunctionShading *>(shading)); + break; + case 2: + case 3: + // no need to implement these + break; + case 4: + case 5: + doGouraudTriangleShFill(static_cast<GfxGouraudTriangleShading *>(shading)); + break; + case 6: + case 7: + doPatchMeshShFill(static_cast<GfxPatchMeshShading *>(shading)); + break; + } + + // restore graphics state + restoreState(); + state->setPath(savedPath); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opShFill(Object args[], int /*numArgs*/) +{ + GfxShading *shading = nullptr; + GfxPath *savedPath = nullptr; + bool savedState = false; + + if (!(shading = res->lookupShading(args[0].getName(), nullptr, state))) { + return; + } + + // save current graphics state + if (shading->getType() != 2 && shading->getType() != 3) { + savedPath = state->getPath()->copy(); + saveState(); + savedState = true; + } + + // clip to bbox + /*if (shading->getHasBBox()) { + double xMin, yMin, xMax, yMax; + shading->getBBox(&xMin, &yMin, &xMax, &yMax); + state->moveTo(xMin, yMin); + state->lineTo(xMax, yMin); + state->lineTo(xMax, yMax); + state->lineTo(xMin, yMax); + state->closePath(); + state->clip(); + builder->setClip(state); + state->clearPath(); + }*/ + + // set the color space + if (savedState) + state->setFillColorSpace(shading->getColorSpace()->copy()); + + // do shading type-specific operations + switch (shading->getType()) { + case 1: // Function-based shading + doFunctionShFill(static_cast<GfxFunctionShading *>(shading)); + break; + case 2: // Axial shading + case 3: // Radial shading + builder->addClippedFill(shading, stateToAffine(state)); + break; + case 4: // Free-form Gouraud-shaded triangle mesh + case 5: // Lattice-form Gouraud-shaded triangle mesh + doGouraudTriangleShFill(static_cast<GfxGouraudTriangleShading *>(shading)); + break; + case 6: // Coons patch mesh + case 7: // Tensor-product patch mesh + doPatchMeshShFill(static_cast<GfxPatchMeshShading *>(shading)); + break; + } + + // restore graphics state + if (savedState) { + restoreState(); + state->setPath(savedPath); + } + + delete shading; +} + +void PdfParser::doFunctionShFill(GfxFunctionShading *shading) { + double x0, y0, x1, y1; + GfxColor colors[4]; + + shading->getDomain(&x0, &y0, &x1, &y1); + shading->getColor(x0, y0, &colors[0]); + shading->getColor(x0, y1, &colors[1]); + shading->getColor(x1, y0, &colors[2]); + shading->getColor(x1, y1, &colors[3]); + doFunctionShFill1(shading, x0, y0, x1, y1, colors, 0); +} + +void PdfParser::doFunctionShFill1(GfxFunctionShading *shading, + double x0, double y0, + double x1, double y1, + GfxColor *colors, int depth) { + GfxColor fillColor; + GfxColor color0M, color1M, colorM0, colorM1, colorMM; + GfxColor colors2[4]; + double functionColorDelta = colorDeltas[pdfFunctionShading-1]; + const double *matrix; + double xM, yM; + int nComps, i, j; + + nComps = shading->getColorSpace()->getNComps(); + matrix = shading->getMatrix(); + + // compare the four corner colors + for (i = 0; i < 4; ++i) { + for (j = 0; j < nComps; ++j) { + if (abs(colors[i].c[j] - colors[(i+1)&3].c[j]) > functionColorDelta) { + break; + } + } + if (j < nComps) { + break; + } + } + + // center of the rectangle + xM = 0.5 * (x0 + x1); + yM = 0.5 * (y0 + y1); + + // the four corner colors are close (or we hit the recursive limit) + // -- fill the rectangle; but require at least one subdivision + // (depth==0) to avoid problems when the four outer corners of the + // shaded region are the same color + if ((i == 4 && depth > 0) || depth == maxDepths[pdfFunctionShading-1]) { + + // use the center color + shading->getColor(xM, yM, &fillColor); + state->setFillColor(&fillColor); + + // fill the rectangle + state->moveTo(x0 * matrix[0] + y0 * matrix[2] + matrix[4], + x0 * matrix[1] + y0 * matrix[3] + matrix[5]); + state->lineTo(x1 * matrix[0] + y0 * matrix[2] + matrix[4], + x1 * matrix[1] + y0 * matrix[3] + matrix[5]); + state->lineTo(x1 * matrix[0] + y1 * matrix[2] + matrix[4], + x1 * matrix[1] + y1 * matrix[3] + matrix[5]); + state->lineTo(x0 * matrix[0] + y1 * matrix[2] + matrix[4], + x0 * matrix[1] + y1 * matrix[3] + matrix[5]); + state->closePath(); + builder->addPath(state, true, false); + state->clearPath(); + + // the four corner colors are not close enough -- subdivide the + // rectangle + } else { + + // colors[0] colorM0 colors[2] + // (x0,y0) (xM,y0) (x1,y0) + // +----------+----------+ + // | | | + // | UL | UR | + // color0M | colorMM | color1M + // (x0,yM) +----------+----------+ (x1,yM) + // | (xM,yM) | + // | LL | LR | + // | | | + // +----------+----------+ + // colors[1] colorM1 colors[3] + // (x0,y1) (xM,y1) (x1,y1) + + shading->getColor(x0, yM, &color0M); + shading->getColor(x1, yM, &color1M); + shading->getColor(xM, y0, &colorM0); + shading->getColor(xM, y1, &colorM1); + shading->getColor(xM, yM, &colorMM); + + // upper-left sub-rectangle + colors2[0] = colors[0]; + colors2[1] = color0M; + colors2[2] = colorM0; + colors2[3] = colorMM; + doFunctionShFill1(shading, x0, y0, xM, yM, colors2, depth + 1); + + // lower-left sub-rectangle + colors2[0] = color0M; + colors2[1] = colors[1]; + colors2[2] = colorMM; + colors2[3] = colorM1; + doFunctionShFill1(shading, x0, yM, xM, y1, colors2, depth + 1); + + // upper-right sub-rectangle + colors2[0] = colorM0; + colors2[1] = colorMM; + colors2[2] = colors[2]; + colors2[3] = color1M; + doFunctionShFill1(shading, xM, y0, x1, yM, colors2, depth + 1); + + // lower-right sub-rectangle + colors2[0] = colorMM; + colors2[1] = colorM1; + colors2[2] = color1M; + colors2[3] = colors[3]; + doFunctionShFill1(shading, xM, yM, x1, y1, colors2, depth + 1); + } +} + +void PdfParser::doGouraudTriangleShFill(GfxGouraudTriangleShading *shading) { + double x0, y0, x1, y1, x2, y2; + GfxColor color0, color1, color2; + int i; + + for (i = 0; i < shading->getNTriangles(); ++i) { + shading->getTriangle(i, &x0, &y0, &color0, + &x1, &y1, &color1, + &x2, &y2, &color2); + gouraudFillTriangle(x0, y0, &color0, x1, y1, &color1, x2, y2, &color2, + shading->getColorSpace()->getNComps(), 0); + } +} + +void PdfParser::gouraudFillTriangle(double x0, double y0, GfxColor *color0, + double x1, double y1, GfxColor *color1, + double x2, double y2, GfxColor *color2, + int nComps, int depth) { + double x01, y01, x12, y12, x20, y20; + double gouraudColorDelta = colorDeltas[pdfGouraudTriangleShading-1]; + GfxColor color01, color12, color20; + int i; + + for (i = 0; i < nComps; ++i) { + if (abs(color0->c[i] - color1->c[i]) > gouraudColorDelta || + abs(color1->c[i] - color2->c[i]) > gouraudColorDelta) { + break; + } + } + if (i == nComps || depth == maxDepths[pdfGouraudTriangleShading-1]) { + state->setFillColor(color0); + state->moveTo(x0, y0); + state->lineTo(x1, y1); + state->lineTo(x2, y2); + state->closePath(); + builder->addPath(state, true, false); + state->clearPath(); + } else { + x01 = 0.5 * (x0 + x1); + y01 = 0.5 * (y0 + y1); + x12 = 0.5 * (x1 + x2); + y12 = 0.5 * (y1 + y2); + x20 = 0.5 * (x2 + x0); + y20 = 0.5 * (y2 + y0); + //~ if the shading has a Function, this should interpolate on the + //~ function parameter, not on the color components + for (i = 0; i < nComps; ++i) { + color01.c[i] = (color0->c[i] + color1->c[i]) / 2; + color12.c[i] = (color1->c[i] + color2->c[i]) / 2; + color20.c[i] = (color2->c[i] + color0->c[i]) / 2; + } + gouraudFillTriangle(x0, y0, color0, x01, y01, &color01, + x20, y20, &color20, nComps, depth + 1); + gouraudFillTriangle(x01, y01, &color01, x1, y1, color1, + x12, y12, &color12, nComps, depth + 1); + gouraudFillTriangle(x01, y01, &color01, x12, y12, &color12, + x20, y20, &color20, nComps, depth + 1); + gouraudFillTriangle(x20, y20, &color20, x12, y12, &color12, + x2, y2, color2, nComps, depth + 1); + } +} + +void PdfParser::doPatchMeshShFill(GfxPatchMeshShading *shading) { + int start, i; + + if (shading->getNPatches() > 128) { + start = 3; + } else if (shading->getNPatches() > 64) { + start = 2; + } else if (shading->getNPatches() > 16) { + start = 1; + } else { + start = 0; + } + for (i = 0; i < shading->getNPatches(); ++i) { + fillPatch(shading->getPatch(i), shading->getColorSpace()->getNComps(), + start); + } +} + +void PdfParser::fillPatch(_POPPLER_CONST GfxPatch *patch, int nComps, int depth) { + GfxPatch patch00 = blankPatch(); + GfxPatch patch01 = blankPatch(); + GfxPatch patch10 = blankPatch(); + GfxPatch patch11 = blankPatch(); + GfxColor color = {{0}}; + double xx[4][8]; + double yy[4][8]; + double xxm; + double yym; + double patchColorDelta = colorDeltas[pdfPatchMeshShading - 1]; + + int i; + + for (i = 0; i < nComps; ++i) { + if (std::abs(patch->color[0][0].c[i] - patch->color[0][1].c[i]) + > patchColorDelta || + std::abs(patch->color[0][1].c[i] - patch->color[1][1].c[i]) + > patchColorDelta || + std::abs(patch->color[1][1].c[i] - patch->color[1][0].c[i]) + > patchColorDelta || + std::abs(patch->color[1][0].c[i] - patch->color[0][0].c[i]) + > patchColorDelta) { + break; + } + color.c[i] = GfxColorComp(patch->color[0][0].c[i]); + } + if (i == nComps || depth == maxDepths[pdfPatchMeshShading-1]) { + state->setFillColor(&color); + state->moveTo(patch->x[0][0], patch->y[0][0]); + state->curveTo(patch->x[0][1], patch->y[0][1], + patch->x[0][2], patch->y[0][2], + patch->x[0][3], patch->y[0][3]); + state->curveTo(patch->x[1][3], patch->y[1][3], + patch->x[2][3], patch->y[2][3], + patch->x[3][3], patch->y[3][3]); + state->curveTo(patch->x[3][2], patch->y[3][2], + patch->x[3][1], patch->y[3][1], + patch->x[3][0], patch->y[3][0]); + state->curveTo(patch->x[2][0], patch->y[2][0], + patch->x[1][0], patch->y[1][0], + patch->x[0][0], patch->y[0][0]); + state->closePath(); + builder->addPath(state, true, false); + state->clearPath(); + } else { + for (i = 0; i < 4; ++i) { + xx[i][0] = patch->x[i][0]; + yy[i][0] = patch->y[i][0]; + xx[i][1] = 0.5 * (patch->x[i][0] + patch->x[i][1]); + yy[i][1] = 0.5 * (patch->y[i][0] + patch->y[i][1]); + xxm = 0.5 * (patch->x[i][1] + patch->x[i][2]); + yym = 0.5 * (patch->y[i][1] + patch->y[i][2]); + xx[i][6] = 0.5 * (patch->x[i][2] + patch->x[i][3]); + yy[i][6] = 0.5 * (patch->y[i][2] + patch->y[i][3]); + xx[i][2] = 0.5 * (xx[i][1] + xxm); + yy[i][2] = 0.5 * (yy[i][1] + yym); + xx[i][5] = 0.5 * (xxm + xx[i][6]); + yy[i][5] = 0.5 * (yym + yy[i][6]); + xx[i][3] = xx[i][4] = 0.5 * (xx[i][2] + xx[i][5]); + yy[i][3] = yy[i][4] = 0.5 * (yy[i][2] + yy[i][5]); + xx[i][7] = patch->x[i][3]; + yy[i][7] = patch->y[i][3]; + } + for (i = 0; i < 4; ++i) { + patch00.x[0][i] = xx[0][i]; + patch00.y[0][i] = yy[0][i]; + patch00.x[1][i] = 0.5 * (xx[0][i] + xx[1][i]); + patch00.y[1][i] = 0.5 * (yy[0][i] + yy[1][i]); + xxm = 0.5 * (xx[1][i] + xx[2][i]); + yym = 0.5 * (yy[1][i] + yy[2][i]); + patch10.x[2][i] = 0.5 * (xx[2][i] + xx[3][i]); + patch10.y[2][i] = 0.5 * (yy[2][i] + yy[3][i]); + patch00.x[2][i] = 0.5 * (patch00.x[1][i] + xxm); + patch00.y[2][i] = 0.5 * (patch00.y[1][i] + yym); + patch10.x[1][i] = 0.5 * (xxm + patch10.x[2][i]); + patch10.y[1][i] = 0.5 * (yym + patch10.y[2][i]); + patch00.x[3][i] = 0.5 * (patch00.x[2][i] + patch10.x[1][i]); + patch00.y[3][i] = 0.5 * (patch00.y[2][i] + patch10.y[1][i]); + patch10.x[0][i] = patch00.x[3][i]; + patch10.y[0][i] = patch00.y[3][i]; + patch10.x[3][i] = xx[3][i]; + patch10.y[3][i] = yy[3][i]; + } + for (i = 4; i < 8; ++i) { + patch01.x[0][i-4] = xx[0][i]; + patch01.y[0][i-4] = yy[0][i]; + patch01.x[1][i-4] = 0.5 * (xx[0][i] + xx[1][i]); + patch01.y[1][i-4] = 0.5 * (yy[0][i] + yy[1][i]); + xxm = 0.5 * (xx[1][i] + xx[2][i]); + yym = 0.5 * (yy[1][i] + yy[2][i]); + patch11.x[2][i-4] = 0.5 * (xx[2][i] + xx[3][i]); + patch11.y[2][i-4] = 0.5 * (yy[2][i] + yy[3][i]); + patch01.x[2][i-4] = 0.5 * (patch01.x[1][i-4] + xxm); + patch01.y[2][i-4] = 0.5 * (patch01.y[1][i-4] + yym); + patch11.x[1][i-4] = 0.5 * (xxm + patch11.x[2][i-4]); + patch11.y[1][i-4] = 0.5 * (yym + patch11.y[2][i-4]); + patch01.x[3][i-4] = 0.5 * (patch01.x[2][i-4] + patch11.x[1][i-4]); + patch01.y[3][i-4] = 0.5 * (patch01.y[2][i-4] + patch11.y[1][i-4]); + patch11.x[0][i-4] = patch01.x[3][i-4]; + patch11.y[0][i-4] = patch01.y[3][i-4]; + patch11.x[3][i-4] = xx[3][i]; + patch11.y[3][i-4] = yy[3][i]; + } + //~ if the shading has a Function, this should interpolate on the + //~ function parameter, not on the color components + for (i = 0; i < nComps; ++i) { + patch00.color[0][0].c[i] = patch->color[0][0].c[i]; + patch00.color[0][1].c[i] = (patch->color[0][0].c[i] + + patch->color[0][1].c[i]) / 2; + patch01.color[0][0].c[i] = patch00.color[0][1].c[i]; + patch01.color[0][1].c[i] = patch->color[0][1].c[i]; + patch01.color[1][1].c[i] = (patch->color[0][1].c[i] + + patch->color[1][1].c[i]) / 2; + patch11.color[0][1].c[i] = patch01.color[1][1].c[i]; + patch11.color[1][1].c[i] = patch->color[1][1].c[i]; + patch11.color[1][0].c[i] = (patch->color[1][1].c[i] + + patch->color[1][0].c[i]) / 2; + patch10.color[1][1].c[i] = patch11.color[1][0].c[i]; + patch10.color[1][0].c[i] = patch->color[1][0].c[i]; + patch10.color[0][0].c[i] = (patch->color[1][0].c[i] + + patch->color[0][0].c[i]) / 2; + patch00.color[1][0].c[i] = patch10.color[0][0].c[i]; + patch00.color[1][1].c[i] = (patch00.color[1][0].c[i] + + patch01.color[1][1].c[i]) / 2; + patch01.color[1][0].c[i] = patch00.color[1][1].c[i]; + patch11.color[0][0].c[i] = patch00.color[1][1].c[i]; + patch10.color[0][1].c[i] = patch00.color[1][1].c[i]; + } + fillPatch(&patch00, nComps, depth + 1); + fillPatch(&patch10, nComps, depth + 1); + fillPatch(&patch01, nComps, depth + 1); + fillPatch(&patch11, nComps, depth + 1); + } +} + +void PdfParser::doEndPath() { + if (state->isCurPt() && clip != clipNone) { + state->clip(); + builder->setClip(state, clip); + clip = clipNone; + } + state->clearPath(); +} + +//------------------------------------------------------------------------ +// path clipping operators +//------------------------------------------------------------------------ + +void PdfParser::opClip(Object /*args*/[], int /*numArgs*/) +{ + clip = clipNormal; +} + +void PdfParser::opEOClip(Object /*args*/[], int /*numArgs*/) +{ + clip = clipEO; +} + +//------------------------------------------------------------------------ +// text object operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginText(Object /*args*/[], int /*numArgs*/) +{ + state->setTextMat(1, 0, 0, 1, 0, 0); + state->textMoveTo(0, 0); + builder->updateTextPosition(0.0, 0.0); + fontChanged = gTrue; + builder->beginTextObject(state); +} + +void PdfParser::opEndText(Object /*args*/[], int /*numArgs*/) +{ + builder->endTextObject(state); +} + +//------------------------------------------------------------------------ +// text state operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetCharSpacing(Object args[], int /*numArgs*/) +{ + state->setCharSpace(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFont(Object args[], int /*numArgs*/) +{ + auto font = res->lookupFont(args[0].getName()); + + if (!font) { + // unsetting the font (drawing no text) is better than using the + // previous one and drawing random glyphs from it + state->setFont(nullptr, args[1].getNum()); + fontChanged = gTrue; + return; + } + if (printCommands) { + printf(" font: tag=%s name='%s' %g\n", +#if POPPLER_CHECK_VERSION(21,11,0) + font->getTag().c_str(), +#else + font->getTag()->getCString(), +#endif + font->getName() ? font->getName()->getCString() : "???", + args[1].getNum()); + fflush(stdout); + } + +#if !POPPLER_CHECK_VERSION(22, 4, 0) + font->incRefCnt(); +#endif + state->setFont(font, args[1].getNum()); + fontChanged = gTrue; +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextLeading(Object args[], int /*numArgs*/) +{ + state->setLeading(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextRender(Object args[], int /*numArgs*/) +{ + state->setRender(args[0].getInt()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextRise(Object args[], int /*numArgs*/) +{ + state->setRise(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetWordSpacing(Object args[], int /*numArgs*/) +{ + state->setWordSpace(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetHorizScaling(Object args[], int /*numArgs*/) +{ + state->setHorizScaling(args[0].getNum()); + builder->updateTextMatrix(state, !subPage); + fontChanged = gTrue; +} + +//------------------------------------------------------------------------ +// text positioning operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opTextMove(Object args[], int /*numArgs*/) +{ + double tx, ty; + + tx = state->getLineX() + args[0].getNum(); + ty = state->getLineY() + args[1].getNum(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opTextMoveSet(Object args[], int /*numArgs*/) +{ + double tx, ty; + + tx = state->getLineX() + args[0].getNum(); + ty = args[1].getNum(); + state->setLeading(-ty); + ty += state->getLineY(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextMatrix(Object args[], int /*numArgs*/) +{ + state->setTextMat(args[0].getNum(), args[1].getNum(), + args[2].getNum(), args[3].getNum(), + args[4].getNum(), args[5].getNum()); + state->textMoveTo(0, 0); + builder->updateTextMatrix(state, !subPage); + builder->updateTextPosition(0.0, 0.0); + fontChanged = gTrue; +} + +void PdfParser::opTextNextLine(Object /*args*/[], int /*numArgs*/) +{ + double tx, ty; + + tx = state->getLineX(); + ty = state->getLineY() - state->getLeading(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); +} + +//------------------------------------------------------------------------ +// text string operators +//------------------------------------------------------------------------ + +void PdfParser::doUpdateFont() +{ + if (fontChanged) { + auto font = getFontEngine()->getFont(state->getFont(), _pdf_doc.get(), true, xref); + builder->updateFont(state, font, !subPage); + fontChanged = false; + } +} + +std::shared_ptr<CairoFontEngine> PdfParser::getFontEngine() +{ + // poppler/CairoOutputDev.cc claims the FT Library needs to be kept around + // for a while. It's unclear if this is sure for our case. + static FT_Library ft_lib; + static std::once_flag ft_lib_once_flag; + std::call_once(ft_lib_once_flag, FT_Init_FreeType, &ft_lib); + if (!_font_engine) { + // This will make a new font engine per form1, in the future we could + // share this between PdfParser instances for the same PDF file. + _font_engine = std::make_shared<CairoFontEngine>(ft_lib); + } + return _font_engine; +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opShowText(Object args[], int /*numArgs*/) +{ + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in show"); + return; + } + doUpdateFont(); + doShowText(args[0].getString()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opMoveShowText(Object args[], int /*numArgs*/) +{ + double tx = 0; + double ty = 0; + + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in move/show"); + return; + } + doUpdateFont(); + tx = state->getLineX(); + ty = state->getLineY() - state->getLeading(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); + doShowText(args[0].getString()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opMoveSetShowText(Object args[], int /*numArgs*/) +{ + double tx = 0; + double ty = 0; + + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in move/set/show"); + return; + } + doUpdateFont(); + state->setWordSpace(args[0].getNum()); + state->setCharSpace(args[1].getNum()); + tx = state->getLineX(); + ty = state->getLineY() - state->getLeading(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); + doShowText(args[2].getString()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opShowSpaceText(Object args[], int /*numArgs*/) +{ + Array *a = nullptr; + Object obj; + int wMode = 0; + + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in show/space"); + return; + } + doUpdateFont(); + wMode = state->getFont()->getWMode(); + a = args[0].getArray(); + for (int i = 0; i < a->getLength(); ++i) { + _POPPLER_CALL_ARGS(obj, a->get, i); + if (obj.isNum()) { + // this uses the absolute value of the font size to match + // Acrobat's behavior + if (wMode) { + state->textShift(0, -obj.getNum() * 0.001 * + fabs(state->getFontSize())); + } else { + state->textShift(-obj.getNum() * 0.001 * + fabs(state->getFontSize()), 0); + } + builder->updateTextShift(state, obj.getNum()); + } else if (obj.isString()) { + doShowText(obj.getString()); + } else { + error(errSyntaxError, getPos(), "Element of show/space array must be number or string"); + } + _POPPLER_FREE(obj); + } +} + +#if POPPLER_CHECK_VERSION(0,64,0) +void PdfParser::doShowText(const GooString *s) { +#else +void PdfParser::doShowText(GooString *s) { +#endif + int wMode; + double riseX, riseY; + CharCode code; + Unicode _POPPLER_CONST_82 *u = nullptr; + double dx, dy, tdx, tdy; + double originX, originY, tOriginX, tOriginY; + Object charProc; +#if POPPLER_CHECK_VERSION(0,64,0) + const char *p; +#else + char *p; +#endif + int len, n, uLen; + + auto font = state->getFont(); + wMode = font->getWMode(); + + builder->beginString(state, s->getLength()); + + // handle a Type 3 char + if (font->getType() == fontType3) { + g_warning("PDF fontType3 information ignored."); + } + + state->textTransformDelta(0, state->getRise(), &riseX, &riseY); + p = s->getCString(); + len = s->getLength(); + while (len > 0) { + /* TODO: This looks like a memory leak for u. */ + n = font->getNextChar(p, len, &code, &u, &uLen, &dx, &dy, &originX, &originY); + + if (wMode) { + dx *= state->getFontSize(); + dy = dy * state->getFontSize() + state->getCharSpace(); + if (n == 1 && *p == ' ') { + dy += state->getWordSpace(); + } + } else { + dx = dx * state->getFontSize() + state->getCharSpace(); + if (n == 1 && *p == ' ') { + dx += state->getWordSpace(); + } + dx *= state->getHorizScaling(); + dy *= state->getFontSize(); + } + state->textTransformDelta(dx, dy, &tdx, &tdy); + originX *= state->getFontSize(); + originY *= state->getFontSize(); + state->textTransformDelta(originX, originY, &tOriginX, &tOriginY); + // In Gfx.cc this is drawChar(...) + builder->addChar(state, state->getCurX() + riseX, state->getCurY() + riseY, dx, dy, tOriginX, tOriginY, code, n, + u, uLen); + state->shift(tdx, tdy); + p += n; + len -= n; + } + builder->endString(state); +} + + +//------------------------------------------------------------------------ +// XObject operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opXObject(Object args[], int /*numArgs*/) +{ + Object obj1, obj2, obj3, refObj; + +#if POPPLER_CHECK_VERSION(0,64,0) + const char *name = args[0].getName(); +#else + char *name = args[0].getName(); +#endif + _POPPLER_CALL_ARGS(obj1, res->lookupXObject, name); + if (obj1.isNull()) { + return; + } + if (!obj1.isStream()) { + error(errSyntaxError, getPos(), "XObject '{0:s}' is wrong type", name); + _POPPLER_FREE(obj1); + return; + } + _POPPLER_CALL_ARGS(obj2, obj1.streamGetDict()->lookup, "Subtype"); + if (obj2.isName(const_cast<char*>("Image"))) { + _POPPLER_CALL_ARGS(refObj, res->lookupXObjectNF, name); + doImage(&refObj, obj1.getStream(), gFalse); + _POPPLER_FREE(refObj); + } else if (obj2.isName(const_cast<char*>("Form"))) { + doForm(&obj1); + } else if (obj2.isName(const_cast<char*>("PS"))) { + _POPPLER_CALL_ARGS(obj3, obj1.streamGetDict()->lookup, "Level1"); +/* out->psXObject(obj1.getStream(), + obj3.isStream() ? obj3.getStream() : (Stream *)NULL);*/ + } else if (obj2.isName()) { + error(errSyntaxError, getPos(), "Unknown XObject subtype '{0:s}'", obj2.getName()); + } else { + error(errSyntaxError, getPos(), "XObject subtype is missing or wrong type"); + } + _POPPLER_FREE(obj2); + _POPPLER_FREE(obj1); +} + +void PdfParser::doImage(Object * /*ref*/, Stream *str, GBool inlineImg) +{ + Dict *dict; + int width, height; + int bits; + GBool interpolate; + StreamColorSpaceMode csMode; + GBool mask; + GBool invert; + Object maskObj, smaskObj; + GBool haveColorKeyMask, haveExplicitMask, haveSoftMask; + GBool maskInvert; + GBool maskInterpolate; + Object obj1, obj2; + + // get info from the stream + bits = 0; + csMode = streamCSNone; + str->getImageParams(&bits, &csMode); + + // get stream dict + dict = str->getDict(); + + // get size + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Width"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "W"); + } + if (obj1.isInt()){ + width = obj1.getInt(); + } + else if (obj1.isReal()) { + width = (int)obj1.getReal(); + } + else { + goto err2; + } + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Height"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "H"); + } + if (obj1.isInt()) { + height = obj1.getInt(); + } + else if (obj1.isReal()){ + height = static_cast<int>(obj1.getReal()); + } + else { + goto err2; + } + _POPPLER_FREE(obj1); + + // image interpolation + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Interpolate"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "I"); + } + if (obj1.isBool()) + interpolate = obj1.getBool(); + else + interpolate = gFalse; + _POPPLER_FREE(obj1); + maskInterpolate = gFalse; + + // image or mask? + _POPPLER_CALL_ARGS(obj1, dict->lookup, "ImageMask"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "IM"); + } + mask = gFalse; + if (obj1.isBool()) { + mask = obj1.getBool(); + } + else if (!obj1.isNull()) { + goto err2; + } + _POPPLER_FREE(obj1); + + // bit depth + if (bits == 0) { + _POPPLER_CALL_ARGS(obj1, dict->lookup, "BitsPerComponent"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "BPC"); + } + if (obj1.isInt()) { + bits = obj1.getInt(); + } else if (mask) { + bits = 1; + } else { + goto err2; + } + _POPPLER_FREE(obj1); + } + + // display a mask + if (mask) { + // check for inverted mask + if (bits != 1) { + goto err1; + } + invert = gFalse; + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "D"); + } + if (obj1.isArray()) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, 0); + if (obj2.isInt() && obj2.getInt() == 1) { + invert = gTrue; + } + _POPPLER_FREE(obj2); + } else if (!obj1.isNull()) { + goto err2; + } + _POPPLER_FREE(obj1); + + // draw it + builder->addImageMask(state, str, width, height, invert, interpolate); + + } else { + // get color space and color map + GfxColorSpace *colorSpace; + _POPPLER_CALL_ARGS(obj1, dict->lookup, "ColorSpace"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "CS"); + } + if (!obj1.isNull()) { + colorSpace = lookupColorSpaceCopy(obj1); + } else if (csMode == streamCSDeviceGray) { + colorSpace = new GfxDeviceGrayColorSpace(); + } else if (csMode == streamCSDeviceRGB) { + colorSpace = new GfxDeviceRGBColorSpace(); + } else if (csMode == streamCSDeviceCMYK) { + colorSpace = new GfxDeviceCMYKColorSpace(); + } else { + colorSpace = nullptr; + } + _POPPLER_FREE(obj1); + if (!colorSpace) { + goto err1; + } + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "D"); + } + GfxImageColorMap *colorMap = new GfxImageColorMap(bits, &obj1, colorSpace); + _POPPLER_FREE(obj1); + if (!colorMap->isOk()) { + delete colorMap; + goto err1; + } + + // get the mask + int maskColors[2*gfxColorMaxComps]; + haveColorKeyMask = haveExplicitMask = haveSoftMask = gFalse; + Stream *maskStr = nullptr; + int maskWidth = 0; + int maskHeight = 0; + maskInvert = gFalse; + GfxImageColorMap *maskColorMap = nullptr; + _POPPLER_CALL_ARGS(maskObj, dict->lookup, "Mask"); + _POPPLER_CALL_ARGS(smaskObj, dict->lookup, "SMask"); + Dict* maskDict; + if (smaskObj.isStream()) { + // soft mask + if (inlineImg) { + goto err1; + } + maskStr = smaskObj.getStream(); + maskDict = smaskObj.streamGetDict(); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Width"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "W"); + } + if (!obj1.isInt()) { + goto err2; + } + maskWidth = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Height"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "H"); + } + if (!obj1.isInt()) { + goto err2; + } + maskHeight = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "BitsPerComponent"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "BPC"); + } + if (!obj1.isInt()) { + goto err2; + } + int maskBits = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Interpolate"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "I"); + } + if (obj1.isBool()) + maskInterpolate = obj1.getBool(); + else + maskInterpolate = gFalse; + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "ColorSpace"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "CS"); + } + GfxColorSpace *maskColorSpace = lookupColorSpaceCopy(obj1); + _POPPLER_FREE(obj1); + if (!maskColorSpace || maskColorSpace->getMode() != csDeviceGray) { + goto err1; + } + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "D"); + } + maskColorMap = new GfxImageColorMap(maskBits, &obj1, maskColorSpace); + _POPPLER_FREE(obj1); + if (!maskColorMap->isOk()) { + delete maskColorMap; + goto err1; + } + //~ handle the Matte entry + haveSoftMask = gTrue; + } else if (maskObj.isArray()) { + // color key mask + int i; + for (i = 0; i < maskObj.arrayGetLength() && i < 2*gfxColorMaxComps; ++i) { + _POPPLER_CALL_ARGS(obj1, maskObj.arrayGet, i); + maskColors[i] = obj1.getInt(); + _POPPLER_FREE(obj1); + } + haveColorKeyMask = gTrue; + } else if (maskObj.isStream()) { + // explicit mask + if (inlineImg) { + goto err1; + } + maskStr = maskObj.getStream(); + maskDict = maskObj.streamGetDict(); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Width"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "W"); + } + if (!obj1.isInt()) { + goto err2; + } + maskWidth = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Height"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "H"); + } + if (!obj1.isInt()) { + goto err2; + } + maskHeight = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "ImageMask"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "IM"); + } + if (!obj1.isBool() || !obj1.getBool()) { + goto err2; + } + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Interpolate"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "I"); + } + if (obj1.isBool()) + maskInterpolate = obj1.getBool(); + else + maskInterpolate = gFalse; + _POPPLER_FREE(obj1); + maskInvert = gFalse; + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "D"); + } + if (obj1.isArray()) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, 0); + if (obj2.isInt() && obj2.getInt() == 1) { + maskInvert = gTrue; + } + _POPPLER_FREE(obj2); + } else if (!obj1.isNull()) { + goto err2; + } + _POPPLER_FREE(obj1); + haveExplicitMask = gTrue; + } + + // draw it + if (haveSoftMask) { + builder->addSoftMaskedImage(state, str, width, height, colorMap, interpolate, + maskStr, maskWidth, maskHeight, maskColorMap, maskInterpolate); + delete maskColorMap; + } else if (haveExplicitMask) { + builder->addMaskedImage(state, str, width, height, colorMap, interpolate, + maskStr, maskWidth, maskHeight, maskInvert, maskInterpolate); + } else { + builder->addImage(state, str, width, height, colorMap, interpolate, + haveColorKeyMask ? maskColors : static_cast<int *>(nullptr)); + } + delete colorMap; + + _POPPLER_FREE(maskObj); + _POPPLER_FREE(smaskObj); + } + + return; + + err2: + _POPPLER_FREE(obj1); + err1: + error(errSyntaxError, getPos(), "Bad image parameters"); +} + +void PdfParser::doForm(Object *str) { + Dict *dict; + GBool transpGroup, isolated, knockout; + GfxColorSpace *blendingColorSpace; + Object matrixObj, bboxObj; + double m[6], bbox[4]; + Object resObj; + Dict *resDict; + Object obj1, obj2, obj3; + int i; + + // check for excessive recursion + if (formDepth > 20) { + return; + } + + // get stream dict + dict = str->streamGetDict(); + + // check form type + _POPPLER_CALL_ARGS(obj1, dict->lookup, "FormType"); + if (!(obj1.isNull() || (obj1.isInt() && obj1.getInt() == 1))) { + error(errSyntaxError, getPos(), "Unknown form type"); + } + _POPPLER_FREE(obj1); + + // get bounding box + _POPPLER_CALL_ARGS(bboxObj, dict->lookup, "BBox"); + if (!bboxObj.isArray()) { + _POPPLER_FREE(bboxObj); + error(errSyntaxError, getPos(), "Bad form bounding box"); + return; + } + for (i = 0; i < 4; ++i) { + _POPPLER_CALL_ARGS(obj1, bboxObj.arrayGet, i); + bbox[i] = obj1.getNum(); + _POPPLER_FREE(obj1); + } + _POPPLER_FREE(bboxObj); + + // get matrix + _POPPLER_CALL_ARGS(matrixObj, dict->lookup, "Matrix"); + if (matrixObj.isArray()) { + for (i = 0; i < 6; ++i) { + _POPPLER_CALL_ARGS(obj1, matrixObj.arrayGet, i); + m[i] = obj1.getNum(); + _POPPLER_FREE(obj1); + } + } else { + m[0] = 1; m[1] = 0; + m[2] = 0; m[3] = 1; + m[4] = 0; m[5] = 0; + } + _POPPLER_FREE(matrixObj); + + // get resources + _POPPLER_CALL_ARGS(resObj, dict->lookup, "Resources"); + resDict = resObj.isDict() ? resObj.getDict() : (Dict *)nullptr; + + // check for a transparency group + transpGroup = isolated = knockout = gFalse; + blendingColorSpace = nullptr; + if (_POPPLER_CALL_ARGS_DEREF(obj1, dict->lookup, "Group").isDict()) { + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "S").isName("Transparency")) { + transpGroup = gTrue; + if (!_POPPLER_CALL_ARGS_DEREF(obj3, obj1.dictLookup, "CS").isNull()) { + blendingColorSpace = GfxColorSpace::parse(nullptr, &obj3, nullptr, state); + } + _POPPLER_FREE(obj3); + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj1.dictLookup, "I").isBool()) { + isolated = obj3.getBool(); + } + _POPPLER_FREE(obj3); + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj1.dictLookup, "K").isBool()) { + knockout = obj3.getBool(); + } + _POPPLER_FREE(obj3); + } + _POPPLER_FREE(obj2); + } + _POPPLER_FREE(obj1); + + // draw it + ++formDepth; + doForm1(str, resDict, m, bbox, + transpGroup, gFalse, blendingColorSpace, isolated, knockout); + --formDepth; + + if (blendingColorSpace) { + delete blendingColorSpace; + } + _POPPLER_FREE(resObj); +} + +void PdfParser::doForm1(Object *str, Dict *resDict, double *matrix, double *bbox, GBool transpGroup, GBool softMask, + GfxColorSpace *blendingColorSpace, GBool isolated, GBool knockout, GBool alpha, + Function *transferFunc, GfxColor *backdropColor) +{ + Parser *oldParser; + + // push new resources on stack + pushResources(resDict); + + // Add a new container group before saving the state + builder->startGroup(state, bbox, blendingColorSpace, isolated, knockout, softMask); + + // save current graphics state + saveState(); + + // kill any pre-existing path + state->clearPath(); + + // save current parser + oldParser = parser; + + // set form transformation matrix + state->concatCTM(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]); + + // set form bounding box + state->moveTo(bbox[0], bbox[1]); + state->lineTo(bbox[2], bbox[1]); + state->lineTo(bbox[2], bbox[3]); + state->lineTo(bbox[0], bbox[3]); + state->closePath(); + state->clip(); + builder->setClip(state, clipNormal, true); + state->clearPath(); + + if (softMask || transpGroup) { + if (state->getBlendMode() != gfxBlendNormal) { + state->setBlendMode(gfxBlendNormal); + } + if (state->getFillOpacity() != 1) { + builder->setGroupOpacity(state->getFillOpacity()); + state->setFillOpacity(1); + } + if (state->getStrokeOpacity() != 1) { + state->setStrokeOpacity(1); + } + } + + // set new base matrix + auto oldBaseMatrix = baseMatrix; + baseMatrix = stateToAffine(state); + + // draw the form + parse(str, gFalse); + + // restore base matrix + baseMatrix = oldBaseMatrix; + + // restore parser + parser = oldParser; + + // restore graphics state + restoreState(); + + // pop resource stack + popResources(); + + // complete any masking + builder->finishGroup(state, softMask); +} + +//------------------------------------------------------------------------ +// in-line image operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginImage(Object /*args*/[], int /*numArgs*/) +{ + // build dict/stream + Stream *str = buildImageStream(); + + // display the image + if (str) { + doImage(nullptr, str, gTrue); + + // skip 'EI' tag + int c1 = str->getUndecodedStream()->getChar(); + int c2 = str->getUndecodedStream()->getChar(); + while (!(c1 == 'E' && c2 == 'I') && c2 != EOF) { + c1 = c2; + c2 = str->getUndecodedStream()->getChar(); + } + delete str; + } +} + +Stream *PdfParser::buildImageStream() { + Object dict; + Object obj; + Stream *str; + + // build dictionary +#if defined(POPPLER_NEW_OBJECT_API) + dict = Object(new Dict(xref)); +#else + dict.initDict(xref); +#endif + _POPPLER_CALL(obj, parser->getObj); + while (!obj.isCmd(const_cast<char*>("ID")) && !obj.isEOF()) { + if (!obj.isName()) { + error(errSyntaxError, getPos(), "Inline image dictionary key must be a name object"); + _POPPLER_FREE(obj); + } else { + Object obj2; + _POPPLER_CALL(obj2, parser->getObj); + if (obj2.isEOF() || obj2.isError()) { + _POPPLER_FREE(obj); + break; + } + _POPPLER_DICTADD(dict, obj.getName(), obj2); + _POPPLER_FREE(obj); + _POPPLER_FREE(obj2); + } + _POPPLER_CALL(obj, parser->getObj); + } + if (obj.isEOF()) { + error(errSyntaxError, getPos(), "End of file in inline image"); + _POPPLER_FREE(obj); + _POPPLER_FREE(dict); + return nullptr; + } + _POPPLER_FREE(obj); + + // make stream +#if defined(POPPLER_NEW_OBJECT_API) + str = new EmbedStream(parser->getStream(), dict.copy(), gFalse, 0); + str = str->addFilters(dict.getDict()); +#else + str = new EmbedStream(parser->getStream(), &dict, gFalse, 0); + str = str->addFilters(&dict); +#endif + + return str; +} + +void PdfParser::opImageData(Object /*args*/[], int /*numArgs*/) +{ + error(errInternal, getPos(), "Internal: got 'ID' operator"); +} + +void PdfParser::opEndImage(Object /*args*/[], int /*numArgs*/) +{ + error(errInternal, getPos(), "Internal: got 'EI' operator"); +} + +//------------------------------------------------------------------------ +// type 3 font operators +//------------------------------------------------------------------------ + +void PdfParser::opSetCharWidth(Object /*args*/[], int /*numArgs*/) +{ +} + +void PdfParser::opSetCacheDevice(Object /*args*/[], int /*numArgs*/) +{ +} + +//------------------------------------------------------------------------ +// compatibility operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginIgnoreUndef(Object /*args*/[], int /*numArgs*/) +{ + ++ignoreUndef; +} + +void PdfParser::opEndIgnoreUndef(Object /*args*/[], int /*numArgs*/) +{ + if (ignoreUndef > 0) + --ignoreUndef; +} + +//------------------------------------------------------------------------ +// marked content operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginMarkedContent(Object args[], int numArgs) { + if (formDepth != 0) + return; + if (printCommands) { + printf(" marked content: %s ", args[0].getName()); + if (numArgs == 2) + args[2].print(stdout); + printf("\n"); + fflush(stdout); + } + if (numArgs == 2 && args[1].isName()) { + // Optional content (OC) to add objects to layer. + builder->beginMarkedContent(args[0].getName(), args[1].getName()); + } else { + builder->beginMarkedContent(); + } +} + +void PdfParser::opEndMarkedContent(Object /*args*/[], int /*numArgs*/) +{ + if (formDepth == 0) + builder->endMarkedContent(); +} + +void PdfParser::opMarkPoint(Object args[], int numArgs) { + if (printCommands) { + printf(" mark point: %s ", args[0].getName()); + if (numArgs == 2) + args[2].print(stdout); + printf("\n"); + fflush(stdout); + } + + if(numArgs == 2) { + //out->markPoint(args[0].getName(),args[1].getDict()); + } else { + //out->markPoint(args[0].getName()); + } + +} + +//------------------------------------------------------------------------ +// misc +//------------------------------------------------------------------------ + +void PdfParser::saveState() { + bool is_radial = false; + GfxPattern *pattern = state->getFillPattern(); + + if (pattern && pattern->getType() == 2) { + GfxShadingPattern *shading_pattern = static_cast<GfxShadingPattern *>(pattern); + GfxShading *shading = shading_pattern->getShading(); + if (shading->getType() == 3) + is_radial = true; + } + + if (is_radial) + state->save(); // nasty hack to prevent GfxRadialShading from getting corrupted during copy operation + else + state = state->save(); // see LP Bug 919176 comment 8 + builder->saveState(state); +} + +void PdfParser::restoreState() { + builder->restoreState(state); + state = state->restore(); +} + +void PdfParser::pushResources(Dict *resDict) { + res = new GfxResources(xref, resDict, res); +} + +void PdfParser::popResources() { + GfxResources *resPtr; + + resPtr = res->getNext(); + delete res; + res = resPtr; +} + +void PdfParser::setDefaultApproximationPrecision() { + for (int i = 1; i <= pdfNumShadingTypes; ++i) { + setApproximationPrecision(i, defaultShadingColorDelta, defaultShadingMaxDepth); + } +} + +void PdfParser::setApproximationPrecision(int shadingType, double colorDelta, + int maxDepth) { + + if (shadingType > pdfNumShadingTypes || shadingType < 1) { + return; + } + colorDeltas[shadingType-1] = dblToCol(colorDelta); + maxDepths[shadingType-1] = maxDepth; +} + +/** + * Optional content groups are often used in ai files, but + * not always and can be useful ways of collecting objects. + */ +void PdfParser::loadOptionalContentLayers(Dict *resources) +{ + auto props = resources->lookup("Properties"); + if (!props.isDict()) + return; + + auto cat = _pdf_doc->getCatalog(); + auto ocgs = cat->getOptContentConfig(); + auto dict = props.getDict(); + + for (auto j = 0; j < dict->getLength(); j++) { + auto val = dict->getVal(j); + if (!val.isDict()) + continue; + auto dict2 = val.getDict(); + if (dict2->lookup("Type").isName("OCG") && ocgs) { + std::string label = getDictString(dict2, "Name"); + auto visible = true; + // Normally we'd use poppler optContentIsVisible, but these dict + // objects don't retain their references so can't be used directly. + for (auto &[ref, ocg] : ocgs->getOCGs()) { + if (ocg->getName()->cmp(label) == 0) + visible = ocg->getState() == OptionalContentGroup::On; + } + builder->addOptionalGroup(dict->getKey(j), label, visible); + } + } +} + +/** + * Load the internal ICC profile from the PDF file. + */ +void PdfParser::loadColorProfile() +{ + Object catDict = xref->getCatalog(); + if (!catDict.isDict()) + return; + + Object outputIntents = catDict.dictLookup("OutputIntents"); + if (!outputIntents.isArray() || outputIntents.arrayGetLength() != 1) + return; + + Object firstElement = outputIntents.arrayGet(0); + if (!firstElement.isDict()) + return; + + Object profile = firstElement.dictLookup("DestOutputProfile"); + if (!profile.isStream()) + return; + + Stream *iccStream = profile.getStream(); +#if POPPLER_CHECK_VERSION(22, 4, 0) + std::vector<unsigned char> profBuf = iccStream->toUnsignedChars(65536, 65536); + builder->addColorProfile(profBuf.data(), profBuf.size()); +#else + int length = 0; + unsigned char *profBuf = iccStream->toUnsignedChars(&length, 65536, 65536); + builder->addColorProfile(profBuf, length); +#endif +} + +void PdfParser::debug_array(const Array *array, int depth, XRef *xref) +{ + if (depth > 20) { + std::cout << "[ ... ]"; + return; + } + std::cout << "[\n"; + for (int i = 0; i < array->getLength(); ++i) { + for (int x = depth; x > -1; x--) + std::cout << " "; + std::cout << i << ": "; + Object obj = array->get(i); + PdfParser::debug_object(&obj, depth + 1, xref); + std::cout << ",\n"; + } + for (int x = depth; x > 0; x--) + std::cout << " "; + std::cout << "]"; +} + +void PdfParser::debug_dict(const Dict *dict, int depth, XRef *xref) +{ + if (depth > 20) { + std::cout << "{ ... }"; + return; + } + std::cout << "{\n"; + for (auto j = 0; j < dict->getLength(); j++) { + auto key = dict->getKey(j); + auto val = dict->getVal(j); + for (int x = depth; x > -1; x--) + std::cout << " "; + std::cout << key << ": "; + PdfParser::debug_object(&val, depth + 1, xref); + std::cout << ",\n"; + } + for (int x = depth; x > 0; x--) + std::cout << " "; + std::cout << "}"; +} + +void PdfParser::debug_object(const Object *obj, int depth, XRef *xref) +{ + if (obj->isRef()) { + std::cout << " > REF(" << obj->getRef().num << "):"; + if (xref) { + auto ref = obj->fetch(xref); + PdfParser::debug_object(&ref, depth + 1, xref); + } + } else if (obj->isDict()) { + PdfParser::debug_dict(obj->getDict(), depth, xref); + } else if (obj->isArray()) { + PdfParser::debug_array(obj->getArray(), depth, xref); + } else if (obj->isString()) { + std::cout << " STR '" << obj->getString()->getCString() << "'"; + } else if (obj->isName()) { + std::cout << " NAME '" << obj->getName() << "'"; + } else if (obj->isBool()) { + std::cout << " BOOL " << (obj->getBool() ? "true" : "false"); + } else if (obj->isNum()) { + std::cout << " NUM " << obj->getNum(); + } else { + std::cout << " > ? " << obj->getType() << ""; + } +} + +#endif /* HAVE_POPPLER */ diff --git a/src/extension/internal/pdfinput/pdf-parser.h b/src/extension/internal/pdfinput/pdf-parser.h new file mode 100644 index 0000000..77c73f2 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-parser.h @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF parsing using libpoppler. + *//* + * Authors: + * see git history + * + * Derived from Gfx.h from poppler (?) which derives from Xpdf, Copyright 1996-2003 Glyph & Cog, LLC, which is under GPL2+. + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef PDF_PARSER_H +#define PDF_PARSER_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +#include "poppler-transition-api.h" + +#ifdef USE_GCC_PRAGMAS +#pragma interface +#endif + +namespace Inkscape { + namespace Extension { + namespace Internal { + class SvgBuilder; + } + } +} + +// TODO clean up and remove using: +using Inkscape::Extension::Internal::SvgBuilder; + +#include "glib/poppler-features.h" +#include "Object.h" + +#include <map> +#include <memory> +#include <string> + +#define Operator Operator_Gfx +#include <Gfx.h> +#undef Operator + +class PDFDoc; +class Page; +class GooString; +class XRef; +class Array; +class Stream; +class Parser; +class Dict; +class Function; +class OutputDev; +class GfxFont; +class GfxPattern; +class GfxTilingPattern; +class GfxShadingPattern; +class GfxShading; +class GfxFunctionShading; +class GfxAxialShading; +class GfxRadialShading; +class GfxGouraudTriangleShading; +class GfxPatchMeshShading; +struct GfxPatch; +class GfxState; +struct GfxColor; +class GfxColorSpace; +class Gfx; +class GfxResources; +class PDFRectangle; +class AnnotBorderStyle; +class CairoFontEngine; + +class PdfParser; + +//------------------------------------------------------------------------ + +#define maxOperatorArgs 33 + +struct PdfOperator { + char name[4]; + int numArgs; + TchkType tchk[maxOperatorArgs]; + void (PdfParser::*func)(Object args[], int numArgs); +}; + +#undef maxOperatorArgs + +struct OpHistoryEntry { + const char *name; // operator's name + GfxState *state; // saved state, NULL if none + GBool executed; // whether the operator has been executed + + OpHistoryEntry *next; // next entry on stack + unsigned depth; // total number of entries descending from this +}; + +//------------------------------------------------------------------------ +// PdfParser +//------------------------------------------------------------------------ + +//------------------------------------------------------------------------ +// constants +//------------------------------------------------------------------------ + +#define pdfFunctionShading 1 +#define pdfAxialShading 2 +#define pdfRadialShading 3 +#define pdfGouraudTriangleShading 4 +#define pdfPatchMeshShading 5 +#define pdfNumShadingTypes 5 + +/** + * PDF parsing module using libpoppler's facilities. + */ +class PdfParser { +public: + + // Constructor for regular output. + PdfParser(std::shared_ptr<PDFDoc> pdf_doc, SvgBuilder *builderA, Page *page, _POPPLER_CONST PDFRectangle *cropBox); + // Constructor for a sub-page object. + PdfParser(XRef *xrefA, SvgBuilder *builderA, Dict *resDict, _POPPLER_CONST PDFRectangle *box); + + virtual ~PdfParser(); + + // Interpret a stream or array of streams. + void parse(Object *obj, GBool topLevel = gTrue); + + // Save graphics state. + void saveState(); + + // Restore graphics state. + void restoreState(); + + // Get the current graphics state object. + GfxState *getState() { return state; } + + // Set the precision of approximation for specific shading fills. + void setApproximationPrecision(int shadingType, double colorDelta, int maxDepth); + void loadOptionalContentLayers(Dict *resources); + void loadPatternColorProfiles(Dict *resources); + void loadColorProfile(); + void loadColorSpaceProfile(GfxColorSpace *space, Object *obj); + GfxPattern *lookupPattern(Object *obj, GfxState *state); + + static void debug_array(const Array *array, int depth = 0, XRef *xref = nullptr); + static void debug_dict(const Dict *dict, int depth = 0, XRef *xref = nullptr); + static void debug_object(const Object *obj, int depth = 0, XRef *xref = nullptr); + + std::shared_ptr<CairoFontEngine> getFontEngine(); +private: + std::shared_ptr<PDFDoc> _pdf_doc; + std::shared_ptr<CairoFontEngine> _font_engine; + + XRef *xref; // the xref table for this PDF file + SvgBuilder *builder; // SVG generator + GBool subPage; // is this a sub-page object? + GBool printCommands; // print the drawing commands (for debugging) + GfxResources *res; // resource stack + + GfxState *state; // current graphics state + GBool fontChanged; // set if font or text matrix has changed + GfxClipType clip; // do a clip? + int ignoreUndef; // current BX/EX nesting level + Geom::Affine baseMatrix; // default matrix for most recent + // page/form/pattern + int formDepth; + + Parser *parser; // parser for page content stream(s) + + static PdfOperator opTab[]; // table of operators + + int colorDeltas[pdfNumShadingTypes]; + // max deltas allowed in any color component + // for the approximation of shading fills + int maxDepths[pdfNumShadingTypes]; // max recursive depths + + OpHistoryEntry *operatorHistory; // list containing the last N operators + + //! Caches color spaces by name + std::map<std::string, std::unique_ptr<GfxColorSpace>> colorSpacesCache; + + GfxColorSpace *lookupColorSpaceCopy(Object &); + + void setDefaultApproximationPrecision(); // init color deltas + void pushOperator(const char *name); + OpHistoryEntry *popOperator(); + const char *getPreviousOperator(unsigned int look_back = 1); // returns the nth previous operator's name + + void go(GBool topLevel); + void execOp(Object *cmd, Object args[], int numArgs); + PdfOperator *findOp(const char *name); + GBool checkArg(Object *arg, TchkType type); + int getPos(); + + void opOptionalContentGroup(Object args[], int numArgs); + + // graphics state operators + void opSave(Object args[], int numArgs); + void opRestore(Object args[], int numArgs); + void opConcat(Object args[], int numArgs); + void opSetDash(Object args[], int numArgs); + void opSetFlat(Object args[], int numArgs); + void opSetLineJoin(Object args[], int numArgs); + void opSetLineCap(Object args[], int numArgs); + void opSetMiterLimit(Object args[], int numArgs); + void opSetLineWidth(Object args[], int numArgs); + void opSetExtGState(Object args[], int numArgs); + void doSoftMask(Object *str, GBool alpha, GfxColorSpace *blendingColorSpace, GBool isolated, GBool knockout, + Function *transferFunc, GfxColor *backdropColor); + void opSetRenderingIntent(Object args[], int numArgs); + + // color operators + void opSetFillGray(Object args[], int numArgs); + void opSetStrokeGray(Object args[], int numArgs); + void opSetFillCMYKColor(Object args[], int numArgs); + void opSetStrokeCMYKColor(Object args[], int numArgs); + void opSetFillRGBColor(Object args[], int numArgs); + void opSetStrokeRGBColor(Object args[], int numArgs); + void opSetFillColorSpace(Object args[], int numArgs); + void opSetStrokeColorSpace(Object args[], int numArgs); + void opSetFillColor(Object args[], int numArgs); + void opSetStrokeColor(Object args[], int numArgs); + void opSetFillColorN(Object args[], int numArgs); + void opSetStrokeColorN(Object args[], int numArgs); + + // path segment operators + void opMoveTo(Object args[], int numArgs); + void opLineTo(Object args[], int numArgs); + void opCurveTo(Object args[], int numArgs); + void opCurveTo1(Object args[], int numArgs); + void opCurveTo2(Object args[], int numArgs); + void opRectangle(Object args[], int numArgs); + void opClosePath(Object args[], int numArgs); + + // path painting operators + void opEndPath(Object args[], int numArgs); + void opStroke(Object args[], int numArgs); + void opCloseStroke(Object args[], int numArgs); + void opFill(Object args[], int numArgs); + void opEOFill(Object args[], int numArgs); + void opFillStroke(Object args[], int numArgs); + void opCloseFillStroke(Object args[], int numArgs); + void opEOFillStroke(Object args[], int numArgs); + void opCloseEOFillStroke(Object args[], int numArgs); + void doFillAndStroke(GBool eoFill); + void doPatternFillFallback(GBool eoFill); + void doPatternStrokeFallback(); + void doShadingPatternFillFallback(GfxShadingPattern *sPat, GBool stroke, GBool eoFill); + void opShFill(Object args[], int numArgs); + void doFunctionShFill(GfxFunctionShading *shading); + void doFunctionShFill1(GfxFunctionShading *shading, double x0, double y0, double x1, double y1, GfxColor *colors, + int depth); + void doGouraudTriangleShFill(GfxGouraudTriangleShading *shading); + void gouraudFillTriangle(double x0, double y0, GfxColor *color0, double x1, double y1, GfxColor *color1, double x2, + double y2, GfxColor *color2, int nComps, int depth); + void doPatchMeshShFill(GfxPatchMeshShading *shading); + void fillPatch(_POPPLER_CONST GfxPatch *patch, int nComps, int depth); + void doEndPath(); + + // path clipping operators + void opClip(Object args[], int numArgs); + void opEOClip(Object args[], int numArgs); + + // text object operators + void opBeginText(Object args[], int numArgs); + void opEndText(Object args[], int numArgs); + + // text state operators + void opSetCharSpacing(Object args[], int numArgs); + void opSetFont(Object args[], int numArgs); + void opSetTextLeading(Object args[], int numArgs); + void opSetTextRender(Object args[], int numArgs); + void opSetTextRise(Object args[], int numArgs); + void opSetWordSpacing(Object args[], int numArgs); + void opSetHorizScaling(Object args[], int numArgs); + + // text positioning operators + void opTextMove(Object args[], int numArgs); + void opTextMoveSet(Object args[], int numArgs); + void opSetTextMatrix(Object args[], int numArgs); + void opTextNextLine(Object args[], int numArgs); + + // text string operators + void doUpdateFont(); + void opShowText(Object args[], int numArgs); + void opMoveShowText(Object args[], int numArgs); + void opMoveSetShowText(Object args[], int numArgs); + void opShowSpaceText(Object args[], int numArgs); +#if POPPLER_CHECK_VERSION(0,64,0) + void doShowText(const GooString *s); +#else + void doShowText(GooString *s); +#endif + + + // XObject operators + void opXObject(Object args[], int numArgs); + void doImage(Object *ref, Stream *str, GBool inlineImg); + void doForm(Object *str); + void doForm1(Object *str, Dict *resDict, double *matrix, double *bbox, + GBool transpGroup = gFalse, GBool softMask = gFalse, + GfxColorSpace *blendingColorSpace = nullptr, + GBool isolated = gFalse, GBool knockout = gFalse, + GBool alpha = gFalse, Function *transferFunc = nullptr, + GfxColor *backdropColor = nullptr); + + // in-line image operators + void opBeginImage(Object args[], int numArgs); + Stream *buildImageStream(); + void opImageData(Object args[], int numArgs); + void opEndImage(Object args[], int numArgs); + + // type 3 font operators + void opSetCharWidth(Object args[], int numArgs); + void opSetCacheDevice(Object args[], int numArgs); + + // compatibility operators + void opBeginIgnoreUndef(Object args[], int numArgs); + void opEndIgnoreUndef(Object args[], int numArgs); + + // marked content operators + void opBeginMarkedContent(Object args[], int numArgs); + void opEndMarkedContent(Object args[], int numArgs); + void opMarkPoint(Object args[], int numArgs); + + void pushResources(Dict *resDict); + void popResources(); +}; + +#endif /* HAVE_POPPLER */ + +#endif /* PDF_PARSER_H */ diff --git a/src/extension/internal/pdfinput/pdf-utils.cpp b/src/extension/internal/pdfinput/pdf-utils.cpp new file mode 100644 index 0000000..05c4ac5 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-utils.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Utility structures and functions for pdf parsing. + *//* + * + * Copyright (C) 2023 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include "pdf-utils.h" + +#include "poppler-utils.h" + +//------------------------------------------------------------------------ +// ClipHistoryEntry +//------------------------------------------------------------------------ + +ClipHistoryEntry::ClipHistoryEntry(GfxPath *clipPathA, GfxClipType clipTypeA) + : saved(nullptr) + , clipPath((clipPathA) ? clipPathA->copy() : nullptr) + , clipType(clipTypeA) +{} + +ClipHistoryEntry::~ClipHistoryEntry() +{ + if (clipPath) { + delete clipPath; + clipPath = nullptr; + } +} + +void ClipHistoryEntry::setClip(GfxState *state, GfxClipType clipTypeA, bool bbox) +{ + const GfxPath *clipPathA = state->getPath(); + + if (clipPath) { + if (copied) { + // Free previously copied clip path. + delete clipPath; + } else { + // This indicates a bad use of the ClipHistory API + g_error("Clip path is already set!"); + return; + } + } + + cleared = false; + copied = false; + if (clipPathA) { + affine = stateToAffine(state); + clipPath = clipPathA->copy(); + clipType = clipTypeA; + is_bbox = bbox; + } else { + affine = Geom::identity(); + clipPath = nullptr; + clipType = clipNormal; + is_bbox = false; + } +} + +/** + * Create a new clip-history, appending it to the stack. + * + * If cleared is set to true, it will not remember the current clipping path. + */ +ClipHistoryEntry *ClipHistoryEntry::save(bool cleared) +{ + ClipHistoryEntry *newEntry = new ClipHistoryEntry(this, cleared); + newEntry->saved = this; + return newEntry; +} + +ClipHistoryEntry *ClipHistoryEntry::restore() +{ + ClipHistoryEntry *oldEntry; + + if (saved) { + oldEntry = saved; + saved = nullptr; + delete this; // TODO really should avoid deleting from inside. + } else { + oldEntry = this; + } + + return oldEntry; +} + +ClipHistoryEntry::ClipHistoryEntry(ClipHistoryEntry *other, bool cleared) +{ + if (other && other->clipPath) { + this->affine = other->affine; + this->clipPath = other->clipPath->copy(); + this->clipType = other->clipType; + this->cleared = cleared; + this->copied = true; + this->is_bbox = other->is_bbox; + } else { + this->affine = Geom::identity(); + this->clipPath = nullptr; + this->clipType = clipNormal; + this->cleared = false; + this->copied = false; + this->is_bbox = false; + } + saved = nullptr; +} + +Geom::Rect getRect(_POPPLER_CONST PDFRectangle *box) +{ + return Geom::Rect(box->x1, box->y1, box->x2, box->y2); +} + diff --git a/src/extension/internal/pdfinput/pdf-utils.h b/src/extension/internal/pdfinput/pdf-utils.h new file mode 100644 index 0000000..e1a449a --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-utils.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF Parsing utility functions and classes. + *//* + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef PDF_UTILS_H +#define PDF_UTILS_H + +#include <2geom/rect.h> +#include "poppler-transition-api.h" +#include "2geom/affine.h" +#include "Gfx.h" +#include "GfxState.h" +#include "Page.h" + +class ClipHistoryEntry +{ +public: + ClipHistoryEntry(GfxPath *clipPath = nullptr, GfxClipType clipType = clipNormal); + virtual ~ClipHistoryEntry(); + + // Manipulate clip path stack + ClipHistoryEntry *save(bool cleared = false); + ClipHistoryEntry *restore(); + bool hasSaves() { return saved != nullptr; } + bool hasClipPath() { return clipPath != nullptr && !cleared; } + bool isCopied() { return copied; } + bool isBoundingBox() { return is_bbox; } + void setClip(GfxState *state, GfxClipType newClipType = clipNormal, bool bbox = false); + GfxPath *getClipPath() { return clipPath; } + GfxClipType getClipType() { return clipType; } + const Geom::Affine &getAffine() { return affine; } + bool evenOdd() { return clipType != clipNormal; } + void clear() { cleared = true; } + +private: + ClipHistoryEntry *saved; // next clip path on stack + + Geom::Affine affine = Geom::identity(); // Saved affine state of the clipPath + GfxPath *clipPath; // used as the path to be filled for an 'sh' operator + GfxClipType clipType; + bool is_bbox = false; + bool cleared = false; + bool copied = false; + + ClipHistoryEntry(ClipHistoryEntry *other, bool cleared = false); +}; + +Geom::Rect getRect(_POPPLER_CONST PDFRectangle *box); + +#endif /* PDF_UTILS_H */ diff --git a/src/extension/internal/pdfinput/poppler-cairo-font-engine.cpp b/src/extension/internal/pdfinput/poppler-cairo-font-engine.cpp new file mode 100644 index 0000000..7f9183b --- /dev/null +++ b/src/extension/internal/pdfinput/poppler-cairo-font-engine.cpp @@ -0,0 +1,779 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +//======================================================================== +// +// CairoFontEngine.cc +// +// Copied into Inkscape from poppler-22.09.0 2022 +// - poppler/CairoFontEngine.* +// - goo/ft_utils.* +// +// Copyright 2003 Glyph & Cog, LLC +// Copyright 2004 Red Hat, Inc +// +//======================================================================== + +//======================================================================== +// +// Modified under the Poppler project - http://poppler.freedesktop.org +// +// All changes made under the Poppler project to this file are licensed +// under GPL version 2 or later +// +// Copyright (C) 2005-2007 Jeff Muizelaar <jeff@infidigm.net> +// Copyright (C) 2005, 2006 Kristian Høgsberg <krh@redhat.com> +// Copyright (C) 2005 Martin Kretzschmar <martink@gnome.org> +// Copyright (C) 2005, 2009, 2012, 2013, 2015, 2017-2019, 2021, 2022 Albert Astals Cid <aacid@kde.org> +// Copyright (C) 2006, 2007, 2010, 2011 Carlos Garcia Campos <carlosgc@gnome.org> +// Copyright (C) 2007 Koji Otani <sho@bbr.jp> +// Copyright (C) 2008, 2009 Chris Wilson <chris@chris-wilson.co.uk> +// Copyright (C) 2008, 2012, 2014, 2016, 2017, 2022 Adrian Johnson <ajohnson@redneon.com> +// Copyright (C) 2009 Darren Kenny <darren.kenny@sun.com> +// Copyright (C) 2010 Suzuki Toshiya <mpsuzuki@hiroshima-u.ac.jp> +// Copyright (C) 2010 Jan Kümmel <jan+freedesktop@snorc.org> +// Copyright (C) 2012 Hib Eris <hib@hiberis.nl> +// Copyright (C) 2013 Thomas Freitag <Thomas.Freitag@alfa.de> +// Copyright (C) 2015, 2016 Jason Crain <jason@aquaticape.us> +// Copyright (C) 2018 Adam Reichold <adam.reichold@t-online.de> +// Copyright (C) 2019 Christian Persch <chpe@src.gnome.org> +// Copyright (C) 2020 Michal <sudolskym@gmail.com> +// Copyright (C) 2021, 2022 Oliver Sander <oliver.sander@tu-dresden.de> +// Copyright (C) 2022 Marcel Fabian Krüger <tex@2krueger.de> +// +//======================================================================== + +#include "poppler-cairo-font-engine.h" + +#include <config.h> +#include <cstring> +#include <fofi/FoFiTrueType.h> +#include <fofi/FoFiType1C.h> +#include <fstream> + +#include "Error.h" +#include "Gfx.h" +#include "GlobalParams.h" +#include "Page.h" +#include "XRef.h" +#include "goo/gfile.h" + +//======================================================================== +// +// ft_util.cc +// +// FreeType helper functions. +// +// This file is licensed under the GPLv2 or later +// +// Copyright (C) 2022 Adrian Johnson <ajohnson@redneon.com> +// +//======================================================================== + +#include <cstdio> + +FT_Error ft_new_face_from_file(FT_Library library, const char *filename_utf8, FT_Long face_index, FT_Face *aface); + +#ifdef _WIN32 +static unsigned long ft_stream_read(FT_Stream stream, unsigned long offset, unsigned char *buffer, unsigned long count) +{ + FILE *file = (FILE *)stream->descriptor.pointer; + fseek(file, offset, SEEK_SET); + return fread(buffer, 1, count, file); +} + +static void ft_stream_close(FT_Stream stream) +{ + FILE *file = (FILE *)stream->descriptor.pointer; + fclose(file); + delete stream; +} +#endif + +// Same as FT_New_Face() but handles UTF-8 filenames on Windows +FT_Error ft_new_face_from_file(FT_Library library, const char *filename_utf8, FT_Long face_index, FT_Face *aface) +{ +#ifdef _WIN32 + FILE *file; + long size; + + if (!filename_utf8) + return FT_Err_Invalid_Argument; + + file = openFile(filename_utf8, "rb"); + if (!file) + return FT_Err_Cannot_Open_Resource; + + fseek(file, 0, SEEK_END); + size = ftell(file); + rewind(file); + + if (size <= 0) + return FT_Err_Cannot_Open_Stream; + + FT_StreamRec *stream = new FT_StreamRec; + *stream = {}; + stream->size = size; + stream->read = ft_stream_read; + stream->close = ft_stream_close; + stream->descriptor.pointer = file; + + FT_Open_Args args = {}; + args.flags = FT_OPEN_STREAM; + args.stream = stream; + + return FT_Open_Face(library, &args, face_index, aface); +#else + // On POSIX, FT_New_Face mmaps font files. If not Windows, prefer FT_New_Face over our stdio.h based FT_Open_Face. + return FT_New_Face(library, filename_utf8, face_index, aface); +#endif +} + +//------------------------------------------------------------------------ +// CairoFont +//------------------------------------------------------------------------ + +CairoFont::CairoFont(Ref refA, cairo_font_face_t *cairo_font_faceA, std::vector<int> &&codeToGIDA, bool substituteA, + bool printingA) + : ref(refA) + , cairo_font_face(cairo_font_faceA) + , substitute(substituteA) + , printing(printingA) +{ + codeToGID = std::move(codeToGIDA); +} + +CairoFont::~CairoFont() +{ + cairo_font_face_destroy(cairo_font_face); +} + +bool CairoFont::matches(Ref &other, bool printingA) +{ + return (other == ref); +} + +cairo_font_face_t *CairoFont::getFontFace() +{ + return cairo_font_face; +} + +unsigned long CairoFont::getGlyph(CharCode code, const Unicode *u, int uLen) +{ + FT_UInt gid; + + if (code < codeToGID.size()) { + gid = (FT_UInt)codeToGID[code]; + } else { + gid = (FT_UInt)code; + } + return gid; +} + +#if POPPLER_CHECK_VERSION(22, 4, 0) +double CairoFont::getSubstitutionCorrection(const std::shared_ptr<GfxFont> &gfxFont) +#else +double CairoFont::getSubstitutionCorrection(GfxFont *gfxFont) +#endif +{ + double w1, w2, w3; + CharCode code; + const char *name; + +#if POPPLER_CHECK_VERSION(22, 4, 0) + auto gfx8bit = std::static_pointer_cast<Gfx8BitFont>(gfxFont); +#else + auto gfx8bit = dynamic_cast<Gfx8BitFont *>(gfxFont); +#endif + + // for substituted fonts: adjust the font matrix -- compare the + // width of 'm' in the original font and the substituted font + if (isSubstitute() && !gfxFont->isCIDFont()) { + for (code = 0; code < 256; ++code) { + if ((name = gfx8bit->getCharName(code)) && name[0] == 'm' && name[1] == '\0') { + break; + } + } + if (code < 256) { + w1 = gfx8bit->getWidth(code); + { + cairo_matrix_t m; + cairo_matrix_init_identity(&m); + cairo_font_options_t *options = cairo_font_options_create(); + cairo_font_options_set_hint_style(options, CAIRO_HINT_STYLE_NONE); + cairo_font_options_set_hint_metrics(options, CAIRO_HINT_METRICS_OFF); + cairo_scaled_font_t *scaled_font = cairo_scaled_font_create(cairo_font_face, &m, &m, options); + + cairo_text_extents_t extents; + cairo_scaled_font_text_extents(scaled_font, "m", &extents); + + cairo_scaled_font_destroy(scaled_font); + cairo_font_options_destroy(options); + w2 = extents.x_advance; + } + w3 = gfx8bit->getWidth(0); + if (!gfxFont->isSymbolic() && w2 > 0 && w1 > w3) { + // if real font is substantially narrower than substituted + // font, reduce the font size accordingly + if (w1 > 0.01 && w1 < 0.9 * w2) { + w1 /= w2; + return w1; + } + } + } + } + return 1.0; +} + +//------------------------------------------------------------------------ +// CairoFreeTypeFont +//------------------------------------------------------------------------ + +static cairo_user_data_key_t ft_cairo_key; + +// Font resources to be freed when cairo_font_face_t is destroyed +struct FreeTypeFontResource +{ + FT_Face face; + std::vector<unsigned char> font_data; +}; + +// cairo callback for when cairo_font_face_t is destroyed +static void _ft_done_face(void *closure) +{ + FreeTypeFontResource *resource = (FreeTypeFontResource *)closure; + + FT_Done_Face(resource->face); + delete resource; +} + +CairoFreeTypeFont::CairoFreeTypeFont(Ref refA, cairo_font_face_t *cairo_font_faceA, std::vector<int> &&codeToGIDA, + bool substituteA) + : CairoFont(refA, cairo_font_faceA, std::move(codeToGIDA), substituteA, true) +{} + +CairoFreeTypeFont::~CairoFreeTypeFont() {} + +// Create a cairo_font_face_t for the given font filename OR font data. +static std::optional<FreeTypeFontFace> createFreeTypeFontFace(FT_Library lib, const std::string &filename, + std::vector<unsigned char> &&font_data) +{ + FreeTypeFontResource *resource = new FreeTypeFontResource; + FreeTypeFontFace font_face; + + if (font_data.empty()) { + FT_Error err = ft_new_face_from_file(lib, filename.c_str(), 0, &resource->face); + if (err) { + delete resource; + return {}; + } + } else { + resource->font_data = std::move(font_data); + FT_Error err = FT_New_Memory_Face(lib, (FT_Byte *)resource->font_data.data(), resource->font_data.size(), 0, + &resource->face); + if (err) { + delete resource; + return {}; + } + } + + font_face.cairo_font_face = + cairo_ft_font_face_create_for_ft_face(resource->face, FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP); + if (cairo_font_face_set_user_data(font_face.cairo_font_face, &ft_cairo_key, resource, _ft_done_face)) { + cairo_font_face_destroy(font_face.cairo_font_face); + _ft_done_face(resource); + return {}; + } + + font_face.face = resource->face; + return font_face; +} + +// Create a cairo_font_face_t for the given font filename OR font data. First checks if external font +// is in the cache. +std::optional<FreeTypeFontFace> CairoFreeTypeFont::getFreeTypeFontFace(CairoFontEngine *fontEngine, FT_Library lib, + const std::string &filename, + std::vector<unsigned char> &&font_data) +{ + if (font_data.empty()) { + return fontEngine->getExternalFontFace(lib, filename); + } + + return createFreeTypeFontFace(lib, filename, std::move(font_data)); +} + +#if POPPLER_CHECK_VERSION(22, 4, 0) +CairoFreeTypeFont *CairoFreeTypeFont::create(const std::shared_ptr<GfxFont> &gfxFont, XRef *xref, FT_Library lib, + CairoFontEngine *fontEngine, bool useCIDs) +#else +CairoFreeTypeFont *CairoFreeTypeFont::create(GfxFont *gfxFont, XRef *xref, FT_Library lib, CairoFontEngine *fontEngine, + bool useCIDs) +#endif +{ + std::string fileName; + std::vector<unsigned char> font_data; + int i, n; +#if POPPLER_CHECK_VERSION(22, 2, 0) + std::optional<GfxFontLoc> fontLoc; +#else + GfxFontLoc *fontLoc; +#endif + char **enc; + const char *name; + FoFiType1C *ff1c; + std::optional<FreeTypeFontFace> font_face; + std::vector<int> codeToGID; + bool substitute = false; + +#if POPPLER_CHECK_VERSION(22, 4, 0) + auto gfxcid = std::static_pointer_cast<GfxCIDFont>(gfxFont); + auto gfx8bit = std::static_pointer_cast<Gfx8BitFont>(gfxFont); + typedef unsigned char *fontchar; +#else + auto gfxcid = dynamic_cast<GfxCIDFont *>(gfxFont); + auto gfx8bit = dynamic_cast<Gfx8BitFont *>(gfxFont); + typedef char *fontchar; +#endif + + Ref ref = *gfxFont->getID(); + Ref embFontID = Ref::INVALID(); + gfxFont->getEmbeddedFontID(&embFontID); + GfxFontType fontType = gfxFont->getType(); + + if (!(fontLoc = gfxFont->locateFont(xref, nullptr))) { + error(errSyntaxError, -1, "Couldn't find a font for '{0:s}'", + gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)"); + goto err2; + } + + // embedded font + if (fontLoc->locType == gfxFontLocEmbedded) { +#if POPPLER_CHECK_VERSION(22, 4, 0) + auto fd = gfxFont->readEmbFontFile(xref); + if (!fd || fd->empty()) { + goto err2; + } + font_data = std::move(fd.value()); +#else + int nSize = 0; + char *chars = gfxFont->readEmbFontFile(xref, &nSize); + if (!nSize || !chars) { + goto err2; + } + font_data = {chars, chars + nSize}; + gfree(chars); +#endif + + // external font + } else { // gfxFontLocExternal + +#if POPPLER_CHECK_VERSION(22, 1, 0) + fileName = fontLoc->path; +#else + fileName = fontLoc->path->toNonConstStr(); +#endif + fontType = fontLoc->fontType; + substitute = true; + } + + switch (fontType) { + case fontType1: + case fontType1C: + case fontType1COT: + font_face = getFreeTypeFontFace(fontEngine, lib, fileName, std::move(font_data)); + if (!font_face) { + error(errSyntaxError, -1, "could not create type1 face"); + goto err2; + } + + enc = gfx8bit->getEncoding(); + + codeToGID.resize(256); + for (i = 0; i < 256; ++i) { + codeToGID[i] = 0; + if ((name = enc[i])) { + codeToGID[i] = FT_Get_Name_Index(font_face->face, (char *)name); + if (codeToGID[i] == 0) { + Unicode u; + u = globalParams->mapNameToUnicodeText(name); + codeToGID[i] = FT_Get_Char_Index(font_face->face, u); + } + if (codeToGID[i] == 0) { + name = GfxFont::getAlternateName(name); + if (name) { + codeToGID[i] = FT_Get_Name_Index(font_face->face, (char *)name); + } + } + } + } + break; + case fontCIDType2: + case fontCIDType2OT: + if (gfxcid->getCIDToGID()) { + n = gfxcid->getCIDToGIDLen(); + if (n) { + const int *src = gfxcid->getCIDToGID(); + codeToGID.reserve(n); + codeToGID.insert(codeToGID.begin(), src, src + n); + } + } else { +#if POPPLER_CHECK_VERSION(22, 1, 0) + std::unique_ptr<FoFiTrueType> ff; +#else + FoFiTrueType *ff; +#endif + if (!font_data.empty()) { + ff = FoFiTrueType::make((fontchar)font_data.data(), font_data.size()); + } else { + ff = FoFiTrueType::load(fileName.c_str()); + } + if (!ff) { + goto err2; + } +#if POPPLER_CHECK_VERSION(22, 1, 0) + int *src = gfxcid->getCodeToGIDMap(ff.get(), &n); +#else + int *src = gfxcid->getCodeToGIDMap(ff, &n); +#endif + codeToGID.reserve(n); + codeToGID.insert(codeToGID.begin(), src, src + n); + gfree(src); + } + /* Fall through */ + case fontTrueType: + case fontTrueTypeOT: { +#if POPPLER_CHECK_VERSION(22, 1, 0) + std::unique_ptr<FoFiTrueType> ff; +#else + FoFiTrueType *ff; +#endif + if (!font_data.empty()) { + ff = FoFiTrueType::make((fontchar)font_data.data(), font_data.size()); + } else { + ff = FoFiTrueType::load(fileName.c_str()); + } + if (!ff) { + error(errSyntaxError, -1, "failed to load truetype font\n"); + goto err2; + } + /* This might be set already for the CIDType2 case */ + if (fontType == fontTrueType || fontType == fontTrueTypeOT) { +#if POPPLER_CHECK_VERSION(22, 1, 0) + int *src = gfx8bit->getCodeToGIDMap(ff.get()); +#else + int *src = gfx8bit->getCodeToGIDMap(ff); +#endif + codeToGID.reserve(256); + codeToGID.insert(codeToGID.begin(), src, src + 256); + gfree(src); + } + font_face = getFreeTypeFontFace(fontEngine, lib, fileName, std::move(font_data)); + if (!font_face) { + error(errSyntaxError, -1, "could not create truetype face\n"); + goto err2; + } + break; + } + case fontCIDType0: + case fontCIDType0C: + if (!useCIDs) { + if (!font_data.empty()) { + ff1c = FoFiType1C::make((fontchar)font_data.data(), font_data.size()); + } else { + ff1c = FoFiType1C::load(fileName.c_str()); + } + if (ff1c) { + int *src = ff1c->getCIDToGIDMap(&n); + codeToGID.reserve(n); + codeToGID.insert(codeToGID.begin(), src, src + n); + gfree(src); + delete ff1c; + } + } + + font_face = getFreeTypeFontFace(fontEngine, lib, fileName, std::move(font_data)); + if (!font_face) { + error(errSyntaxError, -1, "could not create cid face\n"); + goto err2; + } + break; + + case fontCIDType0COT: + if (gfxcid->getCIDToGID()) { + n = gfxcid->getCIDToGIDLen(); + if (n) { + const int *src = gfxcid->getCIDToGID(); + codeToGID.reserve(n); + codeToGID.insert(codeToGID.begin(), src, src + n); + } + } + + if (codeToGID.empty()) { + if (!useCIDs) { +#if POPPLER_CHECK_VERSION(22, 1, 0) + std::unique_ptr<FoFiTrueType> ff; +#else + FoFiTrueType *ff; +#endif + if (!font_data.empty()) { + ff = FoFiTrueType::make((fontchar)font_data.data(), font_data.size()); + } else { + ff = FoFiTrueType::load(fileName.c_str()); + } + if (ff) { + if (ff->isOpenTypeCFF()) { + int *src = ff->getCIDToGIDMap(&n); + codeToGID.reserve(n); + codeToGID.insert(codeToGID.begin(), src, src + n); + gfree(src); + } + } + } + } + font_face = getFreeTypeFontFace(fontEngine, lib, fileName, std::move(font_data)); + if (!font_face) { + error(errSyntaxError, -1, "could not create cid (OT) face\n"); + goto err2; + } + break; + + default: + fprintf(stderr, "font type %d not handled\n", (int)fontType); + goto err2; + break; + } + + return new CairoFreeTypeFont(ref, font_face->cairo_font_face, std::move(codeToGID), substitute); + +err2: + fprintf(stderr, "some font thing failed\n"); + return nullptr; +} + +//------------------------------------------------------------------------ +// CairoType3Font +//------------------------------------------------------------------------ + +static const cairo_user_data_key_t type3_font_key = {0}; + +typedef struct _type3_font_info +{ +#if POPPLER_CHECK_VERSION(22, 4, 0) + _type3_font_info(const std::shared_ptr<GfxFont> &fontA, PDFDoc *docA, CairoFontEngine *fontEngineA, bool printingA, + XRef *xrefA) + : font(fontA) + , doc(docA) + , fontEngine(fontEngineA) + , printing(printingA) + , xref(xrefA) + {} + + std::shared_ptr<GfxFont> font; +#else + _type3_font_info(GfxFont *fontA, PDFDoc *docA, CairoFontEngine *fontEngineA, bool printingA, XRef *xrefA) + : font(fontA) + , doc(docA) + , fontEngine(fontEngineA) + , printing(printingA) + , xref(xrefA) + {} + + GfxFont *font; +#endif + + PDFDoc *doc; + CairoFontEngine *fontEngine; + bool printing; + XRef *xref; +} type3_font_info_t; + +static void _free_type3_font_info(void *closure) +{ + type3_font_info_t *info = (type3_font_info_t *)closure; + delete info; +} + +static cairo_status_t _init_type3_glyph(cairo_scaled_font_t *scaled_font, cairo_t *cr, cairo_font_extents_t *extents) +{ + type3_font_info_t *info; + + info = (type3_font_info_t *)cairo_font_face_get_user_data(cairo_scaled_font_get_font_face(scaled_font), + &type3_font_key); + const double *mat = info->font->getFontBBox(); + extents->ascent = mat[3]; /* y2 */ + extents->descent = -mat[3]; /* -y1 */ + extents->height = extents->ascent + extents->descent; + extents->max_x_advance = mat[2] - mat[1]; /* x2 - x1 */ + extents->max_y_advance = 0; + + return CAIRO_STATUS_SUCCESS; +} + +static cairo_status_t _render_type3_glyph(cairo_scaled_font_t *scaled_font, unsigned long glyph, cairo_t *cr, + cairo_text_extents_t *metrics, bool color) +{ + // We have stripped out the type3 glyph support here, because it calls back + // into CairoOutputDev which is private and would pull in the entire poppler codebase. + return CAIRO_STATUS_USER_FONT_ERROR; +} + +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 17, 6) +static cairo_status_t _render_type3_color_glyph(cairo_scaled_font_t *scaled_font, unsigned long glyph, cairo_t *cr, + cairo_text_extents_t *metrics) +{ + return _render_type3_glyph(scaled_font, glyph, cr, metrics, true); +} +#endif + +static cairo_status_t _render_type3_noncolor_glyph(cairo_scaled_font_t *scaled_font, unsigned long glyph, cairo_t *cr, + cairo_text_extents_t *metrics) +{ + return _render_type3_glyph(scaled_font, glyph, cr, metrics, false); +} + +#if POPPLER_CHECK_VERSION(22, 4, 0) +CairoType3Font *CairoType3Font::create(const std::shared_ptr<GfxFont> &gfxFont, PDFDoc *doc, + CairoFontEngine *fontEngine, bool printing, XRef *xref) +{ + auto gfx8bit = std::static_pointer_cast<Gfx8BitFont>(gfxFont); +#else +CairoType3Font *CairoType3Font::create(GfxFont *gfxFont, PDFDoc *doc, CairoFontEngine *fontEngine, bool printing, + XRef *xref) +{ + auto gfx8bit = dynamic_cast<Gfx8BitFont *>(gfxFont); +#endif + + std::vector<int> codeToGID; + char *name; + + Dict *charProcs = gfx8bit->getCharProcs(); + Ref ref = *gfxFont->getID(); + cairo_font_face_t *font_face = cairo_user_font_face_create(); + cairo_user_font_face_set_init_func(font_face, _init_type3_glyph); +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 17, 6) + // When both callbacks are set, Cairo will call the color glyph + // callback first. If that returns NOT_IMPLEMENTED, Cairo will + // then call the non-color glyph callback. + cairo_user_font_face_set_render_color_glyph_func(font_face, _render_type3_color_glyph); +#endif + cairo_user_font_face_set_render_glyph_func(font_face, _render_type3_noncolor_glyph); + type3_font_info_t *info = new type3_font_info_t(gfxFont, doc, fontEngine, printing, xref); + + cairo_font_face_set_user_data(font_face, &type3_font_key, (void *)info, _free_type3_font_info); + + char **enc = gfx8bit->getEncoding(); + codeToGID.resize(256); + for (int i = 0; i < 256; ++i) { + codeToGID[i] = 0; + if (charProcs && (name = enc[i])) { + for (int j = 0; j < charProcs->getLength(); j++) { + if (strcmp(name, charProcs->getKey(j)) == 0) { + codeToGID[i] = j; + } + } + } + } + + return new CairoType3Font(ref, font_face, std::move(codeToGID), printing, xref); +} + +CairoType3Font::CairoType3Font(Ref refA, cairo_font_face_t *cairo_font_faceA, std::vector<int> &&codeToGIDA, + bool printingA, XRef *xref) + : CairoFont(refA, cairo_font_faceA, std::move(codeToGIDA), false, printingA) +{} + +CairoType3Font::~CairoType3Font() {} + +bool CairoType3Font::matches(Ref &other, bool printingA) +{ + return (other == ref && printing == printingA); +} + +//------------------------------------------------------------------------ +// CairoFontEngine +//------------------------------------------------------------------------ + +std::unordered_map<std::string, FreeTypeFontFace> CairoFontEngine::fontFileCache; +std::recursive_mutex CairoFontEngine::fontFileCacheMutex; + +CairoFontEngine::CairoFontEngine(FT_Library libA) +{ + lib = libA; + fontCache.reserve(cairoFontCacheSize); + + FT_Int major, minor, patch; + // as of FT 2.1.8, CID fonts are indexed by CID instead of GID + FT_Library_Version(lib, &major, &minor, &patch); + useCIDs = major > 2 || (major == 2 && (minor > 1 || (minor == 1 && patch > 7))); +} + +CairoFontEngine::~CairoFontEngine() {} + +#if POPPLER_CHECK_VERSION(22, 4, 0) +std::shared_ptr<CairoFont> CairoFontEngine::getFont(const std::shared_ptr<GfxFont> &gfxFont, PDFDoc *doc, bool printing, + XRef *xref) +#else +std::shared_ptr<CairoFont> CairoFontEngine::getFont(GfxFont *gfxFont, PDFDoc *doc, bool printing, XRef *xref) +#endif +{ + std::scoped_lock lock(mutex); + Ref ref = *gfxFont->getID(); + std::shared_ptr<CairoFont> font; + + // Check if font is in the MRU cache, and move it to the end if it is. + for (auto it = fontCache.rbegin(); it != fontCache.rend(); ++it) { + if ((*it)->matches(ref, printing)) { + font = *it; + // move it to the end + if (it != fontCache.rbegin()) { + // https://stackoverflow.com/questions/1830158/how-to-call-erase-with-a-reverse-iterator + fontCache.erase(std::next(it).base()); + fontCache.push_back(font); + } + return font; + } + } + + GfxFontType fontType = gfxFont->getType(); + if (fontType == fontType3) { + font = std::shared_ptr<CairoFont>(CairoType3Font::create(gfxFont, doc, this, printing, xref)); + } else { + font = std::shared_ptr<CairoFont>(CairoFreeTypeFont::create(gfxFont, xref, lib, this, useCIDs)); + } + + if (font) { + if (fontCache.size() == cairoFontCacheSize) { + fontCache.erase(fontCache.begin()); + } + fontCache.push_back(font); + } + return font; +} + +std::optional<FreeTypeFontFace> CairoFontEngine::getExternalFontFace(FT_Library ftlib, const std::string &filename) +{ + std::scoped_lock lock(fontFileCacheMutex); + + auto it = fontFileCache.find(filename); + if (it != fontFileCache.end()) { + FreeTypeFontFace font = it->second; + cairo_font_face_reference(font.cairo_font_face); + return font; + } + + std::optional<FreeTypeFontFace> font_face = createFreeTypeFontFace(ftlib, filename, {}); + if (font_face) { + cairo_font_face_reference(font_face->cairo_font_face); + fontFileCache[filename] = *font_face; + } + + it = fontFileCache.begin(); + while (it != fontFileCache.end()) { + if (cairo_font_face_get_reference_count(it->second.cairo_font_face) == 1) { + cairo_font_face_destroy(it->second.cairo_font_face); + it = fontFileCache.erase(it); + } else { + ++it; + } + } + + return font_face; +} diff --git a/src/extension/internal/pdfinput/poppler-cairo-font-engine.h b/src/extension/internal/pdfinput/poppler-cairo-font-engine.h new file mode 100644 index 0000000..d3e1a94 --- /dev/null +++ b/src/extension/internal/pdfinput/poppler-cairo-font-engine.h @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +//======================================================================== +// +// CairoFontEngine.h +// +// Copyright 2003 Glyph & Cog, LLC +// Copyright 2004 Red Hat, Inc +// +//======================================================================== + +//======================================================================== +// +// Modified under the Poppler project - http://poppler.freedesktop.org +// +// All changes made under the Poppler project to this file are licensed +// under GPL version 2 or later +// +// Copyright (C) 2005, 2006 Kristian Høgsberg <krh@redhat.com> +// Copyright (C) 2005, 2018, 2019, 2021 Albert Astals Cid <aacid@kde.org> +// Copyright (C) 2006, 2007 Jeff Muizelaar <jeff@infidigm.net> +// Copyright (C) 2006, 2010 Carlos Garcia Campos <carlosgc@gnome.org> +// Copyright (C) 2008, 2017, 2022 Adrian Johnson <ajohnson@redneon.com> +// Copyright (C) 2013 Thomas Freitag <Thomas.Freitag@alfa.de> +// Copyright (C) 2018 Adam Reichold <adam.reichold@t-online.de> +// Copyright (C) 2022 Oliver Sander <oliver.sander@tu-dresden.de> +// +// To see a description of the changes please see the Changelog file that +// came with your tarball or type make ChangeLog if you are building from git +// +//======================================================================== + +#ifndef CAIROFONTENGINE_H +#define CAIROFONTENGINE_H + +#include <cairo-ft.h> +#include <memory> +#include <mutex> +#include <optional> +#include <unordered_map> +#include <vector> + +#include "GfxFont.h" +#include "PDFDoc.h" +#include "poppler-config.h" +#include "poppler-transition-api.h" + +class CairoFontEngine; + +class CairoFont +{ +public: + CairoFont(Ref refA, cairo_font_face_t *cairo_font_faceA, std::vector<int> &&codeToGIDA, bool substituteA, + bool printingA); + virtual ~CairoFont(); + CairoFont(const CairoFont &) = delete; + CairoFont &operator=(const CairoFont &other) = delete; + + virtual bool matches(Ref &other, bool printing); + cairo_font_face_t *getFontFace(); + unsigned long getGlyph(CharCode code, const Unicode *u, int uLen); +#if POPPLER_CHECK_VERSION(22, 4, 0) + double getSubstitutionCorrection(const std::shared_ptr<GfxFont> &gfxFont); +#else + double getSubstitutionCorrection(GfxFont *gfxFont); +#endif + + bool isSubstitute() { return substitute; } + +protected: + Ref ref; + cairo_font_face_t *cairo_font_face; + + std::vector<int> codeToGID; + + bool substitute; + bool printing; +}; + +//------------------------------------------------------------------------ + +struct FreeTypeFontFace +{ + FT_Face face; + cairo_font_face_t *cairo_font_face; +}; + +class CairoFreeTypeFont : public CairoFont +{ +public: +#if POPPLER_CHECK_VERSION(22, 4, 0) + static CairoFreeTypeFont *create(const std::shared_ptr<GfxFont> &gfxFont, XRef *xref, FT_Library lib, + CairoFontEngine *fontEngine, bool useCIDs); +#else + static CairoFreeTypeFont *create(GfxFont *gfxFont, XRef *xref, FT_Library lib, CairoFontEngine *fontEngine, + bool useCIDs); +#endif + ~CairoFreeTypeFont() override; + +private: + CairoFreeTypeFont(Ref ref, cairo_font_face_t *cairo_font_face, std::vector<int> &&codeToGID, bool substitute); + + static std::optional<FreeTypeFontFace> getFreeTypeFontFace(CairoFontEngine *fontEngine, FT_Library lib, + const std::string &filename, + std::vector<unsigned char> &&data); +}; + +//------------------------------------------------------------------------ + +class CairoType3Font : public CairoFont +{ +public: +#if POPPLER_CHECK_VERSION(22, 4, 0) + static CairoType3Font *create(const std::shared_ptr<GfxFont> &gfxFont, PDFDoc *doc, CairoFontEngine *fontEngine, + bool printing, XRef *xref); +#else + static CairoType3Font *create(GfxFont *gfxFont, PDFDoc *doc, CairoFontEngine *fontEngine, bool printing, + XRef *xref); +#endif + ~CairoType3Font() override; + + bool matches(Ref &other, bool printing) override; + +private: + CairoType3Font(Ref ref, cairo_font_face_t *cairo_font_face, std::vector<int> &&codeToGIDA, bool printing, + XRef *xref); +}; + +//------------------------------------------------------------------------ + +//------------------------------------------------------------------------ +// CairoFontEngine +//------------------------------------------------------------------------ + +class CairoFontEngine +{ +public: + // Create a font engine. + explicit CairoFontEngine(FT_Library libA); + ~CairoFontEngine(); + CairoFontEngine(const CairoFontEngine &) = delete; + CairoFontEngine &operator=(const CairoFontEngine &other) = delete; + +#if POPPLER_CHECK_VERSION(22, 4, 0) + std::shared_ptr<CairoFont> getFont(const std::shared_ptr<GfxFont> &gfxFont, PDFDoc *doc, bool printing, XRef *xref); +#else + std::shared_ptr<CairoFont> getFont(GfxFont *gfxFont, PDFDoc *doc, bool printing, XRef *xref); +#endif + + static std::optional<FreeTypeFontFace> getExternalFontFace(FT_Library ftlib, const std::string &filename); + +private: + FT_Library lib; + bool useCIDs; + mutable std::mutex mutex; + + // Cache of CairoFont for current document + // Most recently used is at the end of the vector. + static const size_t cairoFontCacheSize = 64; + std::vector<std::shared_ptr<CairoFont>> fontCache; + + // Global cache of cairo_font_face_t for external font files. + static std::unordered_map<std::string, FreeTypeFontFace> fontFileCache; + static std::recursive_mutex fontFileCacheMutex; +}; + +#endif diff --git a/src/extension/internal/pdfinput/poppler-transition-api.h b/src/extension/internal/pdfinput/poppler-transition-api.h new file mode 100644 index 0000000..dc9e47e --- /dev/null +++ b/src/extension/internal/pdfinput/poppler-transition-api.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO short description + *//* + * Authors: + * see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_POPPLER_TRANSITION_API_H +#define SEEN_POPPLER_TRANSITION_API_H + +#include <glib/poppler-features.h> + +#if POPPLER_CHECK_VERSION(22, 4, 0) +#define _POPPLER_FONTPTR_TO_GFX8(font_ptr) ((Gfx8BitFont *)font_ptr.get()) +#else +#define _POPPLER_FONTPTR_TO_GFX8(font_ptr) ((Gfx8BitFont *)font_ptr) +#endif + +#if POPPLER_CHECK_VERSION(22, 3, 0) +#define _POPPLER_MAKE_SHARED_PDFDOC(uri) std::make_shared<PDFDoc>(std::make_unique<GooString>(uri)) +#else +#define _POPPLER_MAKE_SHARED_PDFDOC(uri) std::make_shared<PDFDoc>(new GooString(uri), nullptr, nullptr, nullptr) +#endif + +#if POPPLER_CHECK_VERSION(0, 83, 0) +#define _POPPLER_CONST_83 const +#else +#define _POPPLER_CONST_83 +#endif + +#if POPPLER_CHECK_VERSION(0, 82, 0) +#define _POPPLER_CONST_82 const +#else +#define _POPPLER_CONST_82 +#endif + +#if POPPLER_CHECK_VERSION(0, 76, 0) +#define _POPPLER_NEW_PARSER(xref, obj) Parser(xref, obj, gFalse) +#else +#define _POPPLER_NEW_PARSER(xref, obj) Parser(xref, new Lexer(xref, obj), gFalse) +#endif + +#if POPPLER_CHECK_VERSION(0, 83, 0) +#define _POPPLER_NEW_GLOBAL_PARAMS(args...) std::unique_ptr<GlobalParams>(new GlobalParams(args)) +#else +#define _POPPLER_NEW_GLOBAL_PARAMS(args...) new GlobalParams(args) +#endif + + +#if POPPLER_CHECK_VERSION(0, 72, 0) +#define getCString c_str +#endif + +#if POPPLER_CHECK_VERSION(0,71,0) +typedef bool GBool; +#define gTrue true +#define gFalse false +#endif + +#if POPPLER_CHECK_VERSION(0,70,0) +#define _POPPLER_CONST const +#else +#define _POPPLER_CONST +#endif + +#if POPPLER_CHECK_VERSION(0,69,0) +#define _POPPLER_DICTADD(dict, key, obj) (dict).dictAdd(key, std::move(obj)) +#elif POPPLER_CHECK_VERSION(0,58,0) +#define _POPPLER_DICTADD(dict, key, obj) (dict).dictAdd(copyString(key), std::move(obj)) +#else +#define _POPPLER_DICTADD(dict, key, obj) (dict).dictAdd(copyString(key), &obj) +#endif + +#if POPPLER_CHECK_VERSION(0,58,0) +#define POPPLER_NEW_OBJECT_API +#define _POPPLER_FREE(obj) +#define _POPPLER_CALL(ret, func) (ret = func()) +#define _POPPLER_CALL_ARGS(ret, func, ...) (ret = func(__VA_ARGS__)) +#define _POPPLER_CALL_ARGS_DEREF _POPPLER_CALL_ARGS +#else +#define _POPPLER_FREE(obj) (obj).free() +#define _POPPLER_CALL(ret, func) (*func(&ret)) +#define _POPPLER_CALL_ARGS(ret, func, ...) (func(__VA_ARGS__, &ret)) +#define _POPPLER_CALL_ARGS_DEREF(...) (*_POPPLER_CALL_ARGS(__VA_ARGS__)) +#endif + +#if !POPPLER_CHECK_VERSION(0, 29, 0) +#error "Requires poppler >= 0.29" +#endif + +#endif diff --git a/src/extension/internal/pdfinput/poppler-utils.cpp b/src/extension/internal/pdfinput/poppler-utils.cpp new file mode 100644 index 0000000..26746dc --- /dev/null +++ b/src/extension/internal/pdfinput/poppler-utils.cpp @@ -0,0 +1,599 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF parsing utilities for libpoppler. + *//* + * Authors: + * Martin Owens + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "poppler-utils.h" + +#include "2geom/affine.h" +#include "GfxFont.h" +#include "GfxState.h" +#include "PDFDoc.h" +#include "libnrtype/font-factory.h" + +/** + * Get the default transformation state from the GfxState + */ +Geom::Affine stateToAffine(GfxState *state) +{ + return ctmToAffine(state->getCTM()); +} + +/** + * Convert a transformation matrix to a lib2geom affine object. + */ +Geom::Affine ctmToAffine(const double *ctm) +{ + if (!ctm) + return Geom::identity(); + return Geom::Affine(ctm[0], ctm[1], ctm[2], ctm[3], ctm[4], ctm[5]); +} + +void ctmout(const char *label, const double *ctm) +{ + std::cout << "C:" << label << ":" << ctm[0] << "," << ctm[1] << "," << ctm[2] << "," << ctm[3] << "," << ctm[4] + << "," << ctm[5] << "\n"; +} + +void affout(const char *label, Geom::Affine ctm) +{ + std::cout << "A:" << label << ":" << ctm[0] << "," << ctm[1] << "," << ctm[2] << "," << ctm[3] << "," << ctm[4] + << "," << ctm[5] << "\n"; +} + +//------------------------------------------------------------------------ +// GfxFontDict from GfxFont.cc in poppler 22.09 +// +// Modified under the Poppler project - http://poppler.freedesktop.org +// +// All changes made under the Poppler project to this file are licensed +// under GPL version 2 or later +// +// See poppler source code for full list of copyright holders. +//------------------------------------------------------------------------ + +InkFontDict::InkFontDict(XRef *xref, Ref *fontDictRef, Dict *fontDict) +{ + Ref r; + + fonts.resize(fontDict->getLength()); + for (std::size_t i = 0; i < fonts.size(); ++i) { + const Object &obj1 = fontDict->getValNF(i); + Object obj2 = obj1.fetch(xref); + if (obj2.isDict()) { + if (obj1.isRef()) { + r = obj1.getRef(); + } else if (fontDictRef) { + // legal generation numbers are five digits, so we use a + // 6-digit number here + r.gen = 100000 + fontDictRef->num; + r.num = i; + } else { + // no indirect reference for this font, or for the containing + // font dict, so hash the font and use that + r.gen = 100000; + r.num = hashFontObject(&obj2); + } + // Newer poppler will require some reworking as it gives a shared ptr. + fonts[i] = GfxFont::makeFont(xref, fontDict->getKey(i), r, obj2.getDict()); + if (fonts[i] && !fonts[i]->isOk()) { + fonts[i] = nullptr; + } + } else { + error(errSyntaxError, -1, "font resource is not a dictionary"); + fonts[i] = nullptr; + } + } +} + +FontPtr InkFontDict::lookup(const char *tag) const +{ + for (const auto &font : fonts) { + if (font && font->matches(tag)) { + return font; + } + } + return nullptr; +} + +// FNV-1a hash +class FNVHash +{ +public: + FNVHash() { h = 2166136261U; } + + void hash(char c) + { + h ^= c & 0xff; + h *= 16777619; + } + + void hash(const char *p, int n) + { + int i; + for (i = 0; i < n; ++i) { + hash(p[i]); + } + } + + int get31() { return (h ^ (h >> 31)) & 0x7fffffff; } + +private: + unsigned int h; +}; + +int InkFontDict::hashFontObject(Object *obj) +{ + FNVHash h; + + hashFontObject1(obj, &h); + return h.get31(); +} + +void InkFontDict::hashFontObject1(const Object *obj, FNVHash *h) +{ + const GooString *s; + const char *p; + double r; + int n, i; + + switch (obj->getType()) { + case objBool: + h->hash('b'); + h->hash(obj->getBool() ? 1 : 0); + break; + case objInt: + h->hash('i'); + n = obj->getInt(); + h->hash((char *)&n, sizeof(int)); + break; + case objReal: + h->hash('r'); + r = obj->getReal(); + h->hash((char *)&r, sizeof(double)); + break; + case objString: + h->hash('s'); + s = obj->getString(); + h->hash(s->c_str(), s->getLength()); + break; + case objName: + h->hash('n'); + p = obj->getName(); + h->hash(p, (int)strlen(p)); + break; + case objNull: + h->hash('z'); + break; + case objArray: + h->hash('a'); + n = obj->arrayGetLength(); + h->hash((char *)&n, sizeof(int)); + for (i = 0; i < n; ++i) { + const Object &obj2 = obj->arrayGetNF(i); + hashFontObject1(&obj2, h); + } + break; + case objDict: + h->hash('d'); + n = obj->dictGetLength(); + h->hash((char *)&n, sizeof(int)); + for (i = 0; i < n; ++i) { + p = obj->dictGetKey(i); + h->hash(p, (int)strlen(p)); + const Object &obj2 = obj->dictGetValNF(i); + hashFontObject1(&obj2, h); + } + break; + case objStream: + // this should never happen - streams must be indirect refs + break; + case objRef: + h->hash('f'); + n = obj->getRefNum(); + h->hash((char *)&n, sizeof(int)); + n = obj->getRefGen(); + h->hash((char *)&n, sizeof(int)); + break; + default: + h->hash('u'); + break; + } +} + +std::string getNameWithoutSubsetTag(FontPtr font) +{ + if (!font->getName()) + return {}; + + std::string tagname = font->getName()->c_str(); + unsigned int i; + for (i = 0; i < tagname.size(); ++i) { + if (tagname[i] < 'A' || tagname[i] > 'Z') { + break; + } + } + if (i != 6 || tagname.size() <= 7 || tagname[6] != '+') + return tagname; + return tagname.substr(7); +} + +/** + * Extract all the useful information from the GfxFont object + */ +FontData::FontData(FontPtr font) +{ + // Level one parsing is taking the data from the PDF font, although this + // information is almost always missing. Perhaps sometimes it's not. + found = false; + + // Style: italic, oblique, normal + style = font->isItalic() ? "italic" : ""; + + // Weight: normal, bold, etc + weight = "normal"; + switch (font->getWeight()) { + case GfxFont::WeightNotDefined: + break; + case GfxFont::W400: + weight = "normal"; + break; + case GfxFont::W700: + weight = "bold"; + break; + default: + weight = std::to_string(font->getWeight() * 100); + break; + } + + // Stretch: condensed or expanded + stretch = ""; + switch (font->getStretch()) { + case GfxFont::UltraCondensed: + stretch = "ultra-condensed"; + break; + case GfxFont::ExtraCondensed: + stretch = "extra-condensed"; + break; + case GfxFont::Condensed: + stretch = "condensed"; + break; + case GfxFont::SemiCondensed: + stretch = "semi-condensed"; + break; + case GfxFont::Normal: + stretch = "normal"; + break; + case GfxFont::SemiExpanded: + stretch = "semi-expanded"; + break; + case GfxFont::Expanded: + stretch = "expanded"; + break; + case GfxFont::ExtraExpanded: + stretch = "extra-expanded"; + break; + case GfxFont::UltraExpanded: + stretch = "ultra-expanded"; + break; + } + + name = getNameWithoutSubsetTag(font); + // Use this when min-poppler version is newer: + // name = font->getNameWithoutSubsetTag(); + + // Embeded CID Fonts don't have family names + if (!font->getFamily()) + return; + + family = font->getFamily()->c_str(); + + // Level two parsing, we break off the font description part of the name + // which often contains font data and use it as a pango font description. + auto desc_str = family; + auto pos = name.find("-"); + if (pos != std::string::npos) { + // Insert spaces where we see capital letters. + std::stringstream ret; + auto str = name.substr(pos + 1, name.size()); + for (char l : str) { + if (l >= 'A' && l <= 'Z') + ret << " "; + ret << l; + } + desc_str = desc_str + ret.str(); + } + + // Now we pull data out of the description. + if (auto desc = pango_font_description_from_string(desc_str.c_str())) { + auto new_family = pango_font_description_get_family(desc); + if (FontFactory::get().hasFontFamily(new_family)) { + family = new_family; + + // Style from pango description + switch (pango_font_description_get_style(desc)) { + case PANGO_STYLE_ITALIC: + style = "italic"; + break; + case PANGO_STYLE_OBLIQUE: + style = "oblique"; + break; + } + + // Weight from pango description + auto pw = pango_font_description_get_weight(desc); + if (pw != PANGO_WEIGHT_NORMAL) { + weight = std::to_string(pw); // Number 100-1000 + } + + // Stretch from pango description + switch (pango_font_description_get_stretch(desc)) { + case PANGO_STRETCH_ULTRA_CONDENSED: + stretch = "ultra-condensed"; + break; + case PANGO_STRETCH_EXTRA_CONDENSED: + stretch = "extra-condensed"; + break; + case PANGO_STRETCH_CONDENSED: + stretch = "condensed"; + break; + case PANGO_STRETCH_SEMI_CONDENSED: + stretch = "semi-condensed"; + break; + case PANGO_STRETCH_SEMI_EXPANDED: + stretch = "semi-expanded"; + break; + case PANGO_STRETCH_EXPANDED: + stretch = "expanded"; + break; + case PANGO_STRETCH_EXTRA_EXPANDED: + stretch = "extra-expanded"; + break; + case PANGO_STRETCH_ULTRA_EXPANDED: + stretch = "ultra-expanded"; + break; + } + + // variant = TODO Convert to variant pango_font_description_get_variant(desc) + + found = true; + // All information has been processed, don't over-write with level three. + return; + } + // Sometimes it's possible to match the description string directly. + if (auto desc = pango_font_description_from_string(family.c_str())) { + auto new_family = pango_font_description_get_family(desc); + if (FontFactory::get().hasFontFamily(new_family)) { + family = new_family; + } + } + } + + found = FontFactory::get().hasFontFamily(family); + // TODO: If !found we could suggest a substitute + + // Level three parsing, we take our name and attempt to match known style names + // Copy id-name stored in PDF and make it lower case and strip whitespaces + std::string source = name; + transform(source.begin(), source.end(), source.begin(), ::tolower); + source.erase(std::remove_if(source.begin(), source.end(), ::isspace), source.end()); + auto contains = [=](const std::string &other) { return source.find(other) != std::string::npos; }; + + if (contains("italic") || contains("slanted")) { + style = "italic"; + } else if (contains("oblique")) { + style = "oblique"; + } + + // Ordered by string matching pass through. + static std::map<std::string, std::string> weights{ + // clang-format off + {"bold", "bold"}, + {"ultrabold", "800"}, + {"extrabold", "800"}, + {"demibold", "600"}, + {"semibold", "600"}, + {"thin", "100"}, + {"ultralight", "200"}, + {"extralight", "200"}, + {"light", "300"}, + {"black", "900"}, + {"heavy", "900"}, + {"medium", "500"}, + {"book", "normal"}, + {"regular", "normal"}, + {"roman", "normal"}, + {"normal", "normal"}, + // clang-format on + }; + // Apply the font weight translations + for (auto w : weights) { + if (contains(w.first)) + weight = w.second; + } + + static std::map<std::string, std::string> stretches{ + // clang-format off + {"ultracondensed", "ultra-condensed"}, + {"extracondensed", "extra-condensed"}, + {"semicondensed", "semi-condensed"}, + {"condensed", "condensed"}, + {"ultraexpanded", "ultra-expanded"}, + {"extraexpanded", "extra-expanded"}, + {"semiexpanded", "semi-expanded"}, + {"expanded", "expanded"}, + // clang-format on + }; + // Apply the font weight translations + for (auto s : stretches) { + if (contains(s.first)) + stretch = s.second; + } +} + +/* + MatchingChars + Count for how many characters s1 matches sp taking into account + that a space in sp may be removed or replaced by some other tokens + specified in the code. (Bug LP #179589) +*/ +static size_t MatchingChars(std::string s1, std::string sp) +{ + size_t is = 0; + size_t ip = 0; + + while (is < s1.length() && ip < sp.length()) { + if (s1[is] == sp[ip]) { + is++; + ip++; + } else if (sp[ip] == ' ') { + ip++; + if (s1[is] == '_') { // Valid matches to spaces in sp. + is++; + } + } else { + break; + } + } + return ip; +} + +/* + * Scan the available fonts to find the font name that best match. + * + * If nothing can be matched, returns an empty string. + */ +std::string FontData::getSubstitute() const +{ + if (found) + return ""; + + double bestMatch = 0; + std::string bestFontname = ""; + + for (auto fontname : FontFactory::get().GetAllFontNames()) { + // At least the first word of the font name should match. + size_t minMatch = fontname.find(" "); + if (minMatch == std::string::npos) { + minMatch = fontname.length(); + } + + size_t Match = MatchingChars(family, fontname); + if (Match >= minMatch) { + double relMatch = (float)Match / (fontname.length() + family.length()); + if (relMatch > bestMatch) { + bestMatch = relMatch; + bestFontname = fontname; + } + } + } + return bestFontname.empty() ? "Arial" : bestFontname; +} + +std::string FontData::getSpecification() const +{ + return family + (style.empty() ? "" : "-" + style); +} + +//------------------------------------------------------------------------ +// scanFonts from FontInfo.cc +//------------------------------------------------------------------------ + +void _getFontsRecursive(std::shared_ptr<PDFDoc> pdf_doc, Dict *resources, const FontList &fontsList, + std::set<int> &visitedObjects, int page) +{ + auto xref = pdf_doc->getXRef(); + + InkFontDict *fontDict = nullptr; + const Object &obj1 = resources->lookupNF("Font"); + if (obj1.isRef()) { + Object obj2 = obj1.fetch(xref); + if (obj2.isDict()) { + auto r = obj1.getRef(); + fontDict = new InkFontDict(xref, &r, obj2.getDict()); + } + } else if (obj1.isDict()) { + fontDict = new InkFontDict(xref, nullptr, obj1.getDict()); + } + + if (fontDict) { + for (int i = 0; i < fontDict->getNumFonts(); ++i) { + auto font = fontDict->getFont(i); + if (fontsList->find(font) == fontsList->end()) { + // Create new font data + fontsList->emplace(font, FontData(font)); + } + fontsList->at(font).pages.insert(page); + } + } + + // recursively scan any resource dictionaries in objects in this resource dictionary + const char *resTypes[] = {"XObject", "Pattern"}; + for (const char *resType : resTypes) { + Object objDict = resources->lookup(resType); + if (!objDict.isDict()) + continue; + + for (int i = 0; i < objDict.dictGetLength(); ++i) { + Ref obj2Ref; + const Object obj2 = objDict.getDict()->getVal(i, &obj2Ref); + if (obj2Ref != Ref::INVALID() && !visitedObjects.insert(obj2Ref.num).second) + continue; + + if (!obj2.isStream()) + continue; + + Ref resourcesRef; + const Object resObj = obj2.streamGetDict()->lookup("Resources", &resourcesRef); + if (resourcesRef != Ref::INVALID() && !visitedObjects.insert(resourcesRef.num).second) + continue; + + if (resObj.isDict() && resObj.getDict() != resources) { + _getFontsRecursive(pdf_doc, resObj.getDict(), fontsList, visitedObjects, page); + } + } + } +} + +FontList getPdfFonts(std::shared_ptr<PDFDoc> pdf_doc) +{ + auto fontsList = std::make_shared<std::map<FontPtr, FontData>>(); + auto count = pdf_doc->getCatalog()->getNumPages(); + std::set<int> visitedObjects; + + for (auto page_num = 1; page_num <= count; page_num++) { + auto page = pdf_doc->getCatalog()->getPage(page_num); + auto resources = page->getResourceDict(); + _getFontsRecursive(pdf_doc, resources, fontsList, visitedObjects, page_num); + } + return fontsList; +} + + +std::string getDictString(Dict *dict, const char *key) +{ + Object obj = dict->lookup(key); + + if (!obj.isString()) { + return ""; + } + + const GooString *value = obj.getString(); + if (value->hasUnicodeMarker()) { + return g_convert(value->getCString () + 2, value->getLength () - 2, + "UTF-8", "UTF-16BE", NULL, NULL, NULL); + } else if (value->hasUnicodeMarkerLE()) { + return g_convert(value->getCString () + 2, value->getLength () - 2, + "UTF-8", "UTF-16LE", NULL, NULL, NULL); + } + return value->toStr(); +} + + diff --git a/src/extension/internal/pdfinput/poppler-utils.h b/src/extension/internal/pdfinput/poppler-utils.h new file mode 100644 index 0000000..3cdec07 --- /dev/null +++ b/src/extension/internal/pdfinput/poppler-utils.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF parsing utilities for libpoppler. + *//* + * Authors: + * Martin Owens + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef POPPLER_UTILS_H +#define POPPLER_UTILS_H + +#include <map> +#include <memory> +#include <string> +#include <unordered_set> +#include <vector> + +#include "poppler-transition-api.h" + +namespace Geom { +class Affine; +} +class Dict; +class FNVHash; +class GfxFont; +class GfxState; +class Object; +class PDFDoc; +class Ref; +class XRef; + +Geom::Affine stateToAffine(GfxState *state); +Geom::Affine ctmToAffine(const double *ctm); + +void ctmout(const char *label, const double *ctm); +void affout(const char *label, Geom::Affine affine); + +#if POPPLER_CHECK_VERSION(22, 4, 0) +typedef std::shared_ptr<GfxFont> FontPtr; +#else +typedef GfxFont *FontPtr; +#endif + +class FontData +{ +public: + FontData(FontPtr font); + std::string getSubstitute() const; + std::string getSpecification() const; + + bool found = false; + + std::unordered_set<int> pages; + std::string name; + std::string family; + + std::string style; + std::string weight; + std::string stretch; + std::string variation; + +private: + void _parseStyle(); +}; + +typedef std::shared_ptr<std::map<FontPtr, FontData>> FontList; + +FontList getPdfFonts(std::shared_ptr<PDFDoc> pdf_doc); +std::string getDictString(Dict *dict, const char *key); + +// Replacate poppler FontDict +class InkFontDict +{ +public: + // Build the font dictionary, given the PDF font dictionary. + InkFontDict(XRef *xref, Ref *fontDictRef, Dict *fontDict); + + // Iterative access. + int getNumFonts() const { return fonts.size(); } + + // Get the specified font. + FontPtr lookup(const char *tag) const; + FontPtr getFont(int i) const { return fonts[i]; } + std::vector<FontPtr> fonts; + +private: + int hashFontObject(Object *obj); + void hashFontObject1(const Object *obj, FNVHash *h); +}; + +#endif /* POPPLER_UTILS_H */ diff --git a/src/extension/internal/pdfinput/svg-builder.cpp b/src/extension/internal/pdfinput/svg-builder.cpp new file mode 100644 index 0000000..fbab2ff --- /dev/null +++ b/src/extension/internal/pdfinput/svg-builder.cpp @@ -0,0 +1,2311 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Native PDF import using libpoppler. + * + * Authors: + * miklos erdelyi + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <string> +#include <locale> +#include <codecvt> + +#ifdef HAVE_POPPLER +#define USE_CMS + +#include "Function.h" +#include "GfxFont.h" +#include "GfxState.h" +#include "GlobalParams.h" +#include "Page.h" +#include "Stream.h" +#include "UnicodeMap.h" +#include "color.h" +#include "display/cairo-utils.h" +#include "display/nr-filter-utils.h" +#include "document.h" +#include "extract-uri.h" +#include "libnrtype/font-factory.h" +#include "libnrtype/font-instance.h" +#include "object/color-profile.h" +#include "object/sp-defs.h" +#include "object/sp-item-group.h" +#include "object/sp-namedview.h" +#include "pdf-parser.h" +#include "pdf-utils.h" +#include "png.h" +#include "poppler-cairo-font-engine.h" +#include "profile-manager.h" +#include "svg-builder.h" +#include "svg/css-ostringstream.h" +#include "svg/path-string.h" +#include "svg/svg-color.h" +#include "svg/svg.h" +#include "util/units.h" +#include "xml/document.h" +#include "xml/node.h" +#include "xml/repr.h" +#include "xml/sp-css-attr.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +//#define IFTRACE(_code) _code +#define IFTRACE(_code) + +#define TRACE(_args) IFTRACE(g_print _args) + + +/** + * \class SvgBuilder + * + */ + +SvgBuilder::SvgBuilder(SPDocument *document, gchar *docname, XRef *xref) +{ + _is_top_level = true; + _doc = document; + _docname = docname; + _xref = xref; + _xml_doc = _doc->getReprDoc(); + _container = _root = _doc->getReprRoot(); + _init(); + + // Set default preference settings + _preferences = _xml_doc->createElement("svgbuilder:prefs"); + _preferences->setAttribute("embedImages", "1"); +} + +SvgBuilder::SvgBuilder(SvgBuilder *parent, Inkscape::XML::Node *root) { + _is_top_level = false; + _doc = parent->_doc; + _docname = parent->_docname; + _xref = parent->_xref; + _xml_doc = parent->_xml_doc; + _preferences = parent->_preferences; + _container = this->_root = root; + _init(); +} + +SvgBuilder::~SvgBuilder() +{ + if (_clip_history) { + delete _clip_history; + _clip_history = nullptr; + } +} + +void SvgBuilder::_init() { + _clip_history = new ClipHistoryEntry(); + _css_font = nullptr; + _font_specification = nullptr; + _in_text_object = false; + _invalidated_style = true; + _width = 0; + _height = 0; + + _node_stack.push_back(_container); +} + +/** + * We're creating a multi-page document, push page number. + */ +void SvgBuilder::pushPage(const std::string &label, GfxState *state) +{ + // Move page over by the last page width + if (_page && this->_width) { + int gap = 20; + _page_left += this->_width + gap; + // TODO: A more interesting page layout could be implemented here. + } + _page_num += 1; + _page_offset = true; + + if (_page) { + Inkscape::GC::release(_page); + } + _page = _xml_doc->createElement("inkscape:page"); + _page->setAttributeSvgDouble("x", _page_left); + _page->setAttributeSvgDouble("y", _page_top); + + // Page translation is somehow lost in the way we're using poppler and the state management + // Applying the state directly doesn't work as many of the flips/rotates are baked in already. + // The translation alone must be added back to the page position so items end up in the + // right places. If a better method is found, please replace this code. + auto st = stateToAffine(state); + auto tr = st.translation(); + if (st[0] < 0 || st[2] < 0) { // Flip or rotate in X + tr[Geom::X] = -tr[Geom::X] + state->getPageWidth(); + } + if (st[1] < 0 || st[3] < 0) { // Flip or rotate in Y + tr[Geom::Y] = -tr[Geom::Y] + state->getPageHeight(); + } + // Note: This translation is very rare in pdf files, most of the time their initial state doesn't contain + // any real translations, just a flip and the because of our GfxState constructor, the pt/px scale. + // Please use an example pdf which produces a non-zero translation in order to change this code! + _page_affine = Geom::Translate(tr).inverse() * Geom::Translate(_page_left, _page_top); + + if (!label.empty()) { + _page->setAttribute("inkscape:label", label); + } + auto _nv = _doc->getNamedView()->getRepr(); + _nv->appendChild(_page); + + // No OptionalContentGroups means no layers, so make a default layer for this page. + if (_ocgs.empty()) { + // Reset to root + while (_container != _root) { + _popGroup(); + } + _pushGroup(); + setAsLayer(label.c_str(), true); + } +} + +void SvgBuilder::setDocumentSize(double width, double height) { + this->_width = width; + this->_height = height; + + if (_page_num < 2) { + _root->setAttributeSvgDouble("width", width); + _root->setAttributeSvgDouble("height", height); + } + if (_page) { + _page->setAttributeSvgDouble("width", width); + _page->setAttributeSvgDouble("height", height); + } +} + +/** + * Crop to this bounding box, do this before setMargins() but after setDocumentSize + */ +void SvgBuilder::cropPage(const Geom::Rect &bbox) +{ + if (_container == _root) { + // We're not going to crop when there's PDF Layers + return; + } + auto box = bbox * _page_affine; + Inkscape::CSSOStringStream val; + val << "M" << box.left() << " " << box.top() + << "H" << box.right() << "V" << box.bottom() + << "H" << box.left() << "Z"; + auto clip_path = _createClip(val.str(), Geom::identity(), false); + gchar *urltext = g_strdup_printf("url(#%s)", clip_path->attribute("id")); + _container->setAttribute("clip-path", urltext); + g_free(urltext); +} + +/** + * Calculate the page margin size based on the pdf settings. + */ +void SvgBuilder::setMargins(const Geom::Rect &page, const Geom::Rect &margins, const Geom::Rect &bleed) +{ + if (page.width() != _width || page.height() != _height) { + // We need to re-set the page size and change the page_affine. + _page_affine *= Geom::Translate(-page.left(), -page.top()); + setDocumentSize(page.width(), page.height()); + } + if (page != margins) { + if (!_page) { + g_warning("Can not store PDF margins in bare document."); + return; + } + // Calculate the margins from the pdf art box. + Inkscape::CSSOStringStream val; + val << margins.top() - page.top() << " " + << page.right() - margins.right() << " " + << page.bottom() - margins.bottom() << " " + << margins.left() - page.left(); + _page->setAttribute("margin", val.str()); + } + if (page != bleed) { + if (!_page) { + g_warning("Can not store PDF bleed in bare document."); + return; + } + Inkscape::CSSOStringStream val; + val << page.top() - bleed.top() << " " + << bleed.right() - page.right() << " " + << bleed.bottom() - page.bottom() << " " + << page.left() - bleed.left(); + _page->setAttribute("bleed", val.str()); + } +} + +/** + * \brief Sets groupmode of the current container to 'layer' and sets its label if given + */ +void SvgBuilder::setAsLayer(const char *layer_name, bool visible) +{ + _container->setAttribute("inkscape:groupmode", "layer"); + if (layer_name) { + _container->setAttribute("inkscape:label", layer_name); + } + if (!visible) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "display", "none"); + sp_repr_css_change(_container, css, "style"); + } +} + +/** + * \brief Sets the current container's opacity + */ +void SvgBuilder::setGroupOpacity(double opacity) { + _container->setAttributeSvgDouble("opacity", CLAMP(opacity, 0.0, 1.0)); +} + +void SvgBuilder::saveState(GfxState *state) +{ + _clip_history = _clip_history->save(); +} + +void SvgBuilder::restoreState(GfxState *state) { + _clip_history = _clip_history->restore(); + + if (!_mask_groups.empty()) { + GfxState *mask_state = _mask_groups.back(); + if (state == mask_state) { + popGroup(state); + _mask_groups.pop_back(); + } + } + while (_clip_groups > 0) { + popGroup(nullptr); + _clip_groups--; + } +} + +Inkscape::XML::Node *SvgBuilder::_pushContainer(const char *name) +{ + return _pushContainer(_xml_doc->createElement(name)); +} + +Inkscape::XML::Node *SvgBuilder::_pushContainer(Inkscape::XML::Node *node) +{ + _node_stack.push_back(node); + _container = node; + // Clear the clip history + _clip_history = _clip_history->save(true); + return node; +} + +Inkscape::XML::Node *SvgBuilder::_popContainer() +{ + Inkscape::XML::Node *node = nullptr; + if ( _node_stack.size() > 1 ) { + node = _node_stack.back(); + _node_stack.pop_back(); + _container = _node_stack.back(); // Re-set container + _clip_history = _clip_history->restore(); + } else { + TRACE(("_popContainer() called when stack is empty\n")); + node = _root; + } + return node; +} + +/** + * Create an svg element and append it to the current container object. + */ +Inkscape::XML::Node *SvgBuilder::_addToContainer(const char *name) +{ + Inkscape::XML::Node *node = _xml_doc->createElement(name); + _addToContainer(node); + return node; +} + +/** + * Append the given xml element to the current container object, clipping and masking as needed. + * + * if release is true (default), the XML node will be GC released too. + */ +void SvgBuilder::_addToContainer(Inkscape::XML::Node *node, bool release) +{ + if (!node->parent()) { + _container->appendChild(node); + } + if (release) { + Inkscape::GC::release(node); + } +} + +void SvgBuilder::_setClipPath(Inkscape::XML::Node *node) +{ + if (_clip_history->hasClipPath() || _clip_text) { + auto tr = Geom::identity(); + if (auto attr = node->attribute("transform")) { + sp_svg_transform_read(attr, &tr); + } + if (auto clip_path = _getClip(tr)) { + gchar *urltext = g_strdup_printf("url(#%s)", clip_path->attribute("id")); + node->setAttribute("clip-path", urltext); + g_free(urltext); + } + } +} + +Inkscape::XML::Node *SvgBuilder::_pushGroup() +{ + Inkscape::XML::Node *saved_container = _container; + Inkscape::XML::Node *node = _pushContainer("svg:g"); + saved_container->appendChild(node); + Inkscape::GC::release(node); + return _container; +} + +Inkscape::XML::Node *SvgBuilder::_popGroup() +{ + if (_container != _root) { // Pop if the current container isn't root + _popContainer(); + } + return _container; +} + +static gchar *svgConvertRGBToText(double r, double g, double b) { + using Inkscape::Filters::clamp; + static gchar tmp[1023] = {0}; + snprintf(tmp, 1023, + "#%02x%02x%02x", + clamp(SP_COLOR_F_TO_U(r)), + clamp(SP_COLOR_F_TO_U(g)), + clamp(SP_COLOR_F_TO_U(b))); + return (gchar *)&tmp; +} + +static std::string svgConvertGfxRGB(GfxRGB *color) +{ + double r = (double)color->r / 65535.0; + double g = (double)color->g / 65535.0; + double b = (double)color->b / 65535.0; + return svgConvertRGBToText(r, g, b); +} + +std::string SvgBuilder::convertGfxColor(const GfxColor *color, GfxColorSpace *space) +{ + std::string icc = ""; + switch (space->getMode()) { + case csDeviceGray: + case csDeviceRGB: + case csDeviceCMYK: + icc = _icc_profile; + break; + case csICCBased: +#if POPPLER_CHECK_VERSION(0, 90, 0) + auto icc_space = dynamic_cast<GfxICCBasedColorSpace *>(space); + icc = _getColorProfile(icc_space->getProfile().get()); +#else + g_warning("ICC profile ignored; libpoppler >= 0.90.0 required."); +#endif + break; + } + + GfxRGB rgb; + space->getRGB(color, &rgb); + auto rgb_color = svgConvertGfxRGB(&rgb); + + if (!icc.empty()) { + Inkscape::CSSOStringStream icc_color; + icc_color << rgb_color << " icc-color(" << icc; + for (int i = 0; i < space->getNComps(); ++i) { + icc_color << ", " << colToDbl((*color).c[i]); + } + icc_color << ");"; + return icc_color.str(); + } + + return rgb_color; +} + +static void svgSetTransform(Inkscape::XML::Node *node, Geom::Affine matrix) { + if (node->attribute("clip-path")) { + g_error("Adding transform AFTER clipping path."); + } + node->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(matrix)); +} + +/** + * \brief Generates a SVG path string from poppler's data structure + */ +static gchar *svgInterpretPath(_POPPLER_CONST_83 GfxPath *path) { + Inkscape::SVG::PathString pathString; + for (int i = 0 ; i < path->getNumSubpaths() ; ++i ) { + _POPPLER_CONST_83 GfxSubpath *subpath = path->getSubpath(i); + if (subpath->getNumPoints() > 0) { + pathString.moveTo(subpath->getX(0), subpath->getY(0)); + int j = 1; + while (j < subpath->getNumPoints()) { + if (subpath->getCurve(j)) { + pathString.curveTo(subpath->getX(j), subpath->getY(j), + subpath->getX(j+1), subpath->getY(j+1), + subpath->getX(j+2), subpath->getY(j+2)); + + j += 3; + } else { + pathString.lineTo(subpath->getX(j), subpath->getY(j)); + ++j; + } + } + if (subpath->isClosed()) { + pathString.closePath(); + } + } + } + + return g_strdup(pathString.c_str()); +} + +/** + * \brief Sets stroke style from poppler's GfxState data structure + * Uses the given SPCSSAttr for storing the style properties + */ +void SvgBuilder::_setStrokeStyle(SPCSSAttr *css, GfxState *state) { + // Stroke color/pattern + auto space = state->getStrokeColorSpace(); + if (space->getMode() == csPattern) { + gchar *urltext = _createPattern(state->getStrokePattern(), state, true); + sp_repr_css_set_property(css, "stroke", urltext); + if (urltext) { + g_free(urltext); + } + } else { + sp_repr_css_set_property(css, "stroke", convertGfxColor(state->getStrokeColor(), space).c_str()); + } + + // Opacity + Inkscape::CSSOStringStream os_opacity; + os_opacity << state->getStrokeOpacity(); + sp_repr_css_set_property(css, "stroke-opacity", os_opacity.str().c_str()); + + // Line width + Inkscape::CSSOStringStream os_width; + double lw = state->getLineWidth(); + // emit a stroke which is 1px in toplevel user units + os_width << (lw > 0.0 ? lw : 1.0); + sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str()); + + // Line cap + switch (state->getLineCap()) { + case 0: + sp_repr_css_set_property(css, "stroke-linecap", "butt"); + break; + case 1: + sp_repr_css_set_property(css, "stroke-linecap", "round"); + break; + case 2: + sp_repr_css_set_property(css, "stroke-linecap", "square"); + break; + } + + // Line join + switch (state->getLineJoin()) { + case 0: + sp_repr_css_set_property(css, "stroke-linejoin", "miter"); + break; + case 1: + sp_repr_css_set_property(css, "stroke-linejoin", "round"); + break; + case 2: + sp_repr_css_set_property(css, "stroke-linejoin", "bevel"); + break; + } + + // Miterlimit + Inkscape::CSSOStringStream os_ml; + os_ml << state->getMiterLimit(); + sp_repr_css_set_property(css, "stroke-miterlimit", os_ml.str().c_str()); + + // Line dash + int dash_length; + double dash_start; +#if POPPLER_CHECK_VERSION(22, 9, 0) + const double *dash_pattern; + const std::vector<double> &dash = state->getLineDash(&dash_start); + dash_pattern = dash.data(); + dash_length = dash.size(); +#else + double *dash_pattern; + state->getLineDash(&dash_pattern, &dash_length, &dash_start); +#endif + if ( dash_length > 0 ) { + Inkscape::CSSOStringStream os_array; + for ( int i = 0 ; i < dash_length ; i++ ) { + os_array << dash_pattern[i]; + if (i < (dash_length - 1)) { + os_array << ","; + } + } + sp_repr_css_set_property(css, "stroke-dasharray", os_array.str().c_str()); + + Inkscape::CSSOStringStream os_offset; + os_offset << dash_start; + sp_repr_css_set_property(css, "stroke-dashoffset", os_offset.str().c_str()); + } else { + sp_repr_css_set_property(css, "stroke-dasharray", "none"); + sp_repr_css_set_property(css, "stroke-dashoffset", nullptr); + } +} + +/** + * \brief Sets fill style from poppler's GfxState data structure + * Uses the given SPCSSAttr for storing the style properties. + */ +void SvgBuilder::_setFillStyle(SPCSSAttr *css, GfxState *state, bool even_odd) { + + // Fill color/pattern + auto space = state->getFillColorSpace(); + if (space->getMode() == csPattern) { + gchar *urltext = _createPattern(state->getFillPattern(), state); + sp_repr_css_set_property(css, "fill", urltext); + if (urltext) { + g_free(urltext); + } + } else { + sp_repr_css_set_property(css, "fill", convertGfxColor(state->getFillColor(), space).c_str()); + } + + // Opacity + Inkscape::CSSOStringStream os_opacity; + os_opacity << state->getFillOpacity(); + sp_repr_css_set_property(css, "fill-opacity", os_opacity.str().c_str()); + + // Fill rule + sp_repr_css_set_property(css, "fill-rule", even_odd ? "evenodd" : "nonzero"); +} + +/** + * \brief Sets blend style properties from poppler's GfxState data structure + * \update a SPCSSAttr with all mix-blend-mode set + */ +void SvgBuilder::_setBlendMode(Inkscape::XML::Node *node, GfxState *state) +{ + SPCSSAttr *css = sp_repr_css_attr(node, "style"); + GfxBlendMode blendmode = state->getBlendMode(); + if (blendmode) { + sp_repr_css_set_property(css, "mix-blend-mode", enum_blend_mode[blendmode].key); + } + Glib::ustring value; + sp_repr_css_write_string(css, value); + node->setAttributeOrRemoveIfEmpty("style", value); + sp_repr_css_attr_unref(css); +} + +void SvgBuilder::_setTransform(Inkscape::XML::Node *node, GfxState *state, Geom::Affine extra) +{ + svgSetTransform(node, extra * stateToAffine(state) * _page_affine); +} + +/** + * \brief Sets style properties from poppler's GfxState data structure + * \return SPCSSAttr with all the relevant properties set + */ +SPCSSAttr *SvgBuilder::_setStyle(GfxState *state, bool fill, bool stroke, bool even_odd) { + SPCSSAttr *css = sp_repr_css_attr_new(); + if (fill) { + _setFillStyle(css, state, even_odd); + } else { + sp_repr_css_set_property(css, "fill", "none"); + } + + if (stroke) { + _setStrokeStyle(css, state); + } else { + sp_repr_css_set_property(css, "stroke", "none"); + } + + return css; +} + +/** + * Returns the CSSAttr of the previously added path if it's exactly + * the same path AND is missing the fill or stroke that is now being painted. + */ +bool SvgBuilder::shouldMergePath(bool is_fill, const std::string &path) +{ + auto prev = _container->lastChild(); + if (!prev || prev->attribute("mask")) + return false; + + auto prev_d = prev->attribute("d"); + if (!prev_d) + return false; + + if (path != prev_d && path != std::string(prev_d) + " Z") + return false; + + auto prev_css = sp_repr_css_attr(prev, "style"); + std::string prev_val = sp_repr_css_property(prev_css, is_fill ? "fill" : "stroke", ""); + // Very specific check excludes paths created elsewhere who's fill/stroke was unset. + return prev_val == "none"; +} + +/** + * Set the fill XOR stroke of the previously added path, if that path + * is missing the given attribute AND the path is exactly the same. + * + * This effectively merges the two objects and is an 'interpretation' step. + */ +bool SvgBuilder::mergePath(GfxState *state, bool is_fill, const std::string &path, bool even_odd) +{ + if (shouldMergePath(is_fill, path)) { + auto prev = _container->lastChild(); + SPCSSAttr *css = sp_repr_css_attr_new(); + if (is_fill) { + _setFillStyle(css, state, even_odd); + // Fill after stroke indicates a different paint order. + sp_repr_css_set_property(css, "paint-order", "stroke fill markers"); + } else { + _setStrokeStyle(css, state); + } + sp_repr_css_change(prev, css, "style"); + sp_repr_css_attr_unref(css); + return true; + } + return false; +} + +/** + * \brief Emits the current path in poppler's GfxState data structure + * Can be used to do filling and stroking at once. + * + * \param fill whether the path should be filled + * \param stroke whether the path should be stroked + * \param even_odd whether the even-odd rule should be used when filling the path + */ +void SvgBuilder::addPath(GfxState *state, bool fill, bool stroke, bool even_odd) { + gchar *pathtext = svgInterpretPath(state->getPath()); + + if (!pathtext) + return; + + if (!strlen(pathtext) || (fill != stroke && mergePath(state, fill, pathtext, even_odd))) { + g_free(pathtext); + return; + } + + Inkscape::XML::Node *path = _addToContainer("svg:path"); + path->setAttribute("d", pathtext); + g_free(pathtext); + + // Set style + SPCSSAttr *css = _setStyle(state, fill, stroke, even_odd); + sp_repr_css_change(path, css, "style"); + sp_repr_css_attr_unref(css); + _setBlendMode(path, state); + _setTransform(path, state); + _setClipPath(path); +} + +void SvgBuilder::addClippedFill(GfxShading *shading, const Geom::Affine shading_tr) +{ + if (_clip_history->getClipPath()) { + addShadedFill(shading, shading_tr, _clip_history->getClipPath(), _clip_history->getAffine(), + _clip_history->getClipType() == clipEO); + } +} + +/** + * \brief Emits the current path in poppler's GfxState data structure + * The path is set to be filled with the given shading. + */ +void SvgBuilder::addShadedFill(GfxShading *shading, const Geom::Affine shading_tr, GfxPath *path, const Geom::Affine tr, + bool even_odd) +{ + auto prev = _container->lastChild(); + gchar *pathtext = svgInterpretPath(path); + + // Create a new gradient object before comitting to creating a path for it + // And package it into a css bundle which can be applied + SPCSSAttr *css = sp_repr_css_attr_new(); + // We remove the shape's affine to adjust the gradient back into place + gchar *id = _createGradient(shading, shading_tr * tr.inverse(), true); + if (id) { + gchar *urltext = g_strdup_printf ("url(#%s)", id); + sp_repr_css_set_property(css, "fill", urltext); + g_free(urltext); + g_free(id); + } else { + sp_repr_css_attr_unref(css); + return; + } + if (even_odd) { + sp_repr_css_set_property(css, "fill-rule", "evenodd"); + } + // Merge the style with the previous shape + if (shouldMergePath(true, pathtext)) { + // POSSIBLE: The gradientTransform might now incorrect if the + // state of the transformation was different between the two paths. + sp_repr_css_change(prev, css, "style"); + g_free(pathtext); + return; + } + + Inkscape::XML::Node *path_node = _addToContainer("svg:path"); + path_node->setAttribute("d", pathtext); + g_free(pathtext); + + // Don't add transforms to mask children. + if (std::string("svg:mask") != _container->name()) { + svgSetTransform(path_node, tr * _page_affine); + } + + // Set the gradient into this new path. + sp_repr_css_set_property(css, "stroke", "none"); + sp_repr_css_change(path_node, css, "style"); + sp_repr_css_attr_unref(css); +} + +/** + * \brief Clips to the current path set in GfxState + * \param state poppler's data structure + * \param even_odd whether the even-odd rule should be applied + */ +void SvgBuilder::setClip(GfxState *state, GfxClipType clip, bool is_bbox) +{ + // When there's already a clip path, we add clipping groups to handle them. + if (!is_bbox && _clip_history->hasClipPath() && !_clip_history->isCopied()) { + _pushContainer("svg:g"); + _clip_groups++; + } + if (clip == clipNormal) { + _clip_history->setClip(state, clipNormal, is_bbox); + } else { + _clip_history->setClip(state, clipEO); + } +} + +/** + * Return the active clip as a new xml node. + */ +Inkscape::XML::Node *SvgBuilder::_getClip(const Geom::Affine &node_tr) +{ + // In SVG the path-clip transforms are compounded, so we have to do extra work to + // pull transforms back out of the clipping object and set them. Otherwise this + // would all be a lot simpler. + if (_clip_text) { + auto node = _clip_text; + + auto text_tr = Geom::identity(); + if (auto attr = node->attribute("transform")) { + sp_svg_transform_read(attr, &text_tr); + node->removeAttribute("transform"); + } + + for (auto child = node->firstChild(); child; child = child->next()) { + Geom::Affine child_tr = text_tr * _page_affine * node_tr.inverse(); + svgSetTransform(child, child_tr); + } + + _clip_text = nullptr; + return node; + } + if (_clip_history->hasClipPath()) { + std::string clip_d = svgInterpretPath(_clip_history->getClipPath()); + Geom::Affine tr = _clip_history->getAffine() * _page_affine * node_tr.inverse(); + return _createClip(clip_d, tr, _clip_history->evenOdd()); + } + return nullptr; +} + +Inkscape::XML::Node *SvgBuilder::_createClip(const std::string &d, const Geom::Affine tr, bool even_odd) +{ + Inkscape::XML::Node *clip_path = _xml_doc->createElement("svg:clipPath"); + clip_path->setAttribute("clipPathUnits", "userSpaceOnUse"); + + // Create the path + Inkscape::XML::Node *path = _xml_doc->createElement("svg:path"); + path->setAttribute("d", d); + svgSetTransform(path, tr); + + if (even_odd) { + path->setAttribute("clip-rule", "evenodd"); + } + clip_path->appendChild(path); + Inkscape::GC::release(path); + + // Append clipPath to defs and get id + _doc->getDefs()->getRepr()->appendChild(clip_path); + Inkscape::GC::release(clip_path); + return clip_path; +} + +void SvgBuilder::beginMarkedContent(const char *name, const char *group) +{ + if (name && group && std::string(name) == "OC") { + auto layer_id = std::string("layer-") + group; + if (auto existing = _doc->getObjectById(layer_id)) { + if (existing->getRepr()->parent() == _container) { + _container = existing->getRepr(); + _node_stack.push_back(_container); + } else { + g_warning("Unexpected marked content group in PDF!"); + _pushGroup(); + } + } else { + auto node = _pushGroup(); + node->setAttribute("id", layer_id); + if (_ocgs.find(group) != _ocgs.end()) { + auto pair = _ocgs[group]; + setAsLayer(pair.first.c_str(), pair.second); + } + } + } else { + auto node = _pushGroup(); + if (group) { + node->setAttribute("id", std::string("group-") + group); + } + } +} + +void SvgBuilder::addOptionalGroup(const std::string &oc, const std::string &label, bool visible) +{ + _ocgs[oc] = {label, visible}; +} + +void SvgBuilder::endMarkedContent() +{ + _popGroup(); +} + +void SvgBuilder::addColorProfile(unsigned char *profBuf, int length) +{ + cmsHPROFILE hp = cmsOpenProfileFromMem(profBuf, length); + if (!hp) { + g_warning("Failed to read ICCBased color space profile from PDF file."); + return; + } + _icc_profile = _getColorProfile(hp); +} + +/** + * Return the color profile name if it's already been added + */ +std::string SvgBuilder::_getColorProfile(cmsHPROFILE hp) +{ + if (!hp) + return ""; + + // Cached name of this profile by reference + if (_icc_profiles.find(hp) != _icc_profiles.end()) + return _icc_profiles[hp]; + + std::string name = Inkscape::ColorProfile::getNameFromProfile(hp); + Inkscape::ColorProfile::sanitizeName(name); + + // Find the named profile in the document (if already added) + if (_doc->getProfileManager().find(name.c_str())) + return name; + + // Add the profile, we've never seen it before. + cmsUInt32Number len = 0; + cmsSaveProfileToMem(hp, nullptr, &len); + auto buf = (unsigned char *)malloc(len * sizeof(unsigned char)); + cmsSaveProfileToMem(hp, buf, &len); + + Inkscape::XML::Node *icc_node = _xml_doc->createElement("svg:color-profile"); + std::string label = Inkscape::ColorProfile::getNameFromProfile(hp); + icc_node->setAttribute("inkscape:label", label); + icc_node->setAttribute("name", name); + + auto *base64String = g_base64_encode(buf, len); + auto icc_data = std::string("data:application/vnd.iccprofile;base64,") + base64String; + g_free(base64String); + icc_node->setAttributeOrRemoveIfEmpty("xlink:href", icc_data); + _doc->getDefs()->getRepr()->appendChild(icc_node); + Inkscape::GC::release(icc_node); + + free(buf); + _icc_profiles[hp] = name; + return name; +} + +/** + * \brief Checks whether the given pattern type can be represented in SVG + * Used by PdfParser to decide when to do fallback operations. + */ +bool SvgBuilder::isPatternTypeSupported(GfxPattern *pattern) { + if ( pattern != nullptr ) { + if ( pattern->getType() == 2 ) { // shading pattern + GfxShading *shading = (static_cast<GfxShadingPattern *>(pattern))->getShading(); + int shadingType = shading->getType(); + if ( shadingType == 2 || // axial shading + shadingType == 3 ) { // radial shading + return true; + } + return false; + } else if ( pattern->getType() == 1 ) { // tiling pattern + return true; + } + } + + return false; +} + +/** + * \brief Creates a pattern from poppler's data structure + * Handles linear and radial gradients. Creates a new PdfParser and uses it to + * build a tiling pattern. + * \return a url pointing to the created pattern + */ +gchar *SvgBuilder::_createPattern(GfxPattern *pattern, GfxState *state, bool is_stroke) { + gchar *id = nullptr; + if ( pattern != nullptr ) { + if ( pattern->getType() == 2 ) { // Shading pattern + GfxShadingPattern *shading_pattern = static_cast<GfxShadingPattern *>(pattern); + // construct a (pattern space) -> (current space) transform matrix + auto flip = Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, _height); + auto pt = Geom::Scale(Inkscape::Util::Quantity::convert(1.0, "pt", "px")); + auto grad_affine = ctmToAffine(shading_pattern->getMatrix()); + auto obj_affine = stateToAffine(state); + // SVG applies the object's affine on top of the gradient's affine, + // So we must remove the object affine to move it back into place. + auto affine = (grad_affine * pt * flip) * obj_affine.inverse(); + id = _createGradient(shading_pattern->getShading(), affine, !is_stroke); + } else if ( pattern->getType() == 1 ) { // Tiling pattern + id = _createTilingPattern(static_cast<GfxTilingPattern*>(pattern), state, is_stroke); + } + } else { + return nullptr; + } + gchar *urltext = g_strdup_printf ("url(#%s)", id); + g_free(id); + return urltext; +} + +/** + * \brief Creates a tiling pattern from poppler's data structure + * Creates a sub-page PdfParser and uses it to parse the pattern's content stream. + * \return id of the created pattern + */ +gchar *SvgBuilder::_createTilingPattern(GfxTilingPattern *tiling_pattern, + GfxState *state, bool is_stroke) { + + Inkscape::XML::Node *pattern_node = _xml_doc->createElement("svg:pattern"); + // Set pattern transform matrix + auto pat_matrix = ctmToAffine(tiling_pattern->getMatrix()); + pattern_node->setAttributeOrRemoveIfEmpty("patternTransform", sp_svg_transform_write(pat_matrix)); + pattern_node->setAttribute("patternUnits", "userSpaceOnUse"); + // Set pattern tiling + // FIXME: don't ignore XStep and YStep + const double *bbox = tiling_pattern->getBBox(); + pattern_node->setAttributeSvgDouble("x", 0.0); + pattern_node->setAttributeSvgDouble("y", 0.0); + pattern_node->setAttributeSvgDouble("width", bbox[2] - bbox[0]); + pattern_node->setAttributeSvgDouble("height", bbox[3] - bbox[1]); + + // Convert BBox for PdfParser + PDFRectangle box; + box.x1 = bbox[0]; + box.y1 = bbox[1]; + box.x2 = bbox[2]; + box.y2 = bbox[3]; + // Create new SvgBuilder and sub-page PdfParser + SvgBuilder *pattern_builder = new SvgBuilder(this, pattern_node); + PdfParser *pdf_parser = new PdfParser(_xref, pattern_builder, tiling_pattern->getResDict(), + &box); + // Get pattern color space + GfxPatternColorSpace *pat_cs = (GfxPatternColorSpace *)( is_stroke ? state->getStrokeColorSpace() + : state->getFillColorSpace() ); + // Set fill/stroke colors if this is an uncolored tiling pattern + GfxColorSpace *cs = nullptr; + if ( tiling_pattern->getPaintType() == 2 && ( cs = pat_cs->getUnder() ) ) { + GfxState *pattern_state = pdf_parser->getState(); + pattern_state->setFillColorSpace(cs->copy()); + pattern_state->setFillColor(state->getFillColor()); + pattern_state->setStrokeColorSpace(cs->copy()); + pattern_state->setStrokeColor(state->getFillColor()); + } + + // Generate the SVG pattern + pdf_parser->parse(tiling_pattern->getContentStream()); + + // Cleanup + delete pdf_parser; + delete pattern_builder; + + // Append the pattern to defs + _doc->getDefs()->getRepr()->appendChild(pattern_node); + gchar *id = g_strdup(pattern_node->attribute("id")); + Inkscape::GC::release(pattern_node); + + return id; +} + +/** + * \brief Creates a linear or radial gradient from poppler's data structure + * \param shading poppler's data structure for the shading + * \param matrix gradient transformation, can be null + * \param for_shading true if we're creating this for a shading operator; false otherwise + * \return id of the created object + */ +gchar *SvgBuilder::_createGradient(GfxShading *shading, const Geom::Affine pat_matrix, bool for_shading) +{ + Inkscape::XML::Node *gradient; + _POPPLER_CONST Function *func; + int num_funcs; + bool extend0, extend1; + + if ( shading->getType() == 2 ) { // Axial shading + gradient = _xml_doc->createElement("svg:linearGradient"); + GfxAxialShading *axial_shading = static_cast<GfxAxialShading*>(shading); + double x1, y1, x2, y2; + axial_shading->getCoords(&x1, &y1, &x2, &y2); + gradient->setAttributeSvgDouble("x1", x1); + gradient->setAttributeSvgDouble("y1", y1); + gradient->setAttributeSvgDouble("x2", x2); + gradient->setAttributeSvgDouble("y2", y2); + extend0 = axial_shading->getExtend0(); + extend1 = axial_shading->getExtend1(); + num_funcs = axial_shading->getNFuncs(); + func = axial_shading->getFunc(0); + } else if (shading->getType() == 3) { // Radial shading + gradient = _xml_doc->createElement("svg:radialGradient"); + GfxRadialShading *radial_shading = static_cast<GfxRadialShading*>(shading); + double x1, y1, r1, x2, y2, r2; + radial_shading->getCoords(&x1, &y1, &r1, &x2, &y2, &r2); + // FIXME: the inner circle's radius is ignored here + gradient->setAttributeSvgDouble("fx", x1); + gradient->setAttributeSvgDouble("fy", y1); + gradient->setAttributeSvgDouble("cx", x2); + gradient->setAttributeSvgDouble("cy", y2); + gradient->setAttributeSvgDouble("r", r2); + extend0 = radial_shading->getExtend0(); + extend1 = radial_shading->getExtend1(); + num_funcs = radial_shading->getNFuncs(); + func = radial_shading->getFunc(0); + } else { // Unsupported shading type + return nullptr; + } + gradient->setAttribute("gradientUnits", "userSpaceOnUse"); + // If needed, flip the gradient transform around the y axis + if (pat_matrix != Geom::identity()) { + gradient->setAttributeOrRemoveIfEmpty("gradientTransform", sp_svg_transform_write(pat_matrix)); + } + + if ( extend0 && extend1 ) { + gradient->setAttribute("spreadMethod", "pad"); + } + + if ( num_funcs > 1 || !_addGradientStops(gradient, shading, func) ) { + Inkscape::GC::release(gradient); + return nullptr; + } + + _doc->getDefs()->getRepr()->appendChild(gradient); + gchar *id = g_strdup(gradient->attribute("id")); + Inkscape::GC::release(gradient); + + return id; +} + +#define EPSILON 0.0001 +/** + * \brief Adds a stop with the given properties to the gradient's representation + */ +void SvgBuilder::_addStopToGradient(Inkscape::XML::Node *gradient, double offset, GfxColor *color, GfxColorSpace *space, + double opacity) +{ + Inkscape::XML::Node *stop = _xml_doc->createElement("svg:stop"); + SPCSSAttr *css = sp_repr_css_attr_new(); + Inkscape::CSSOStringStream os_opacity; + std::string color_text = "#ffffff"; + if (space->getMode() == csDeviceGray) { + // This is a transparency mask. + GfxRGB rgb; + space->getRGB(color, &rgb); + double gray = (double)rgb.r / 65535.0; + gray = CLAMP(gray, 0.0, 1.0); + os_opacity << gray; + } else { + os_opacity << opacity; + color_text = convertGfxColor(color, space); + } + sp_repr_css_set_property(css, "stop-opacity", os_opacity.str().c_str()); + sp_repr_css_set_property(css, "stop-color", color_text.c_str()); + + sp_repr_css_change(stop, css, "style"); + sp_repr_css_attr_unref(css); + stop->setAttributeCssDouble("offset", offset); + + gradient->appendChild(stop); + Inkscape::GC::release(stop); +} + +static bool svgGetShadingColor(GfxShading *shading, double offset, GfxColor *result) +{ + if ( shading->getType() == 2 ) { // Axial shading + (static_cast<GfxAxialShading *>(shading))->getColor(offset, result); + } else if ( shading->getType() == 3 ) { // Radial shading + (static_cast<GfxRadialShading *>(shading))->getColor(offset, result); + } else { + return false; + } + return true; +} + +#define INT_EPSILON 8 +bool SvgBuilder::_addGradientStops(Inkscape::XML::Node *gradient, GfxShading *shading, + _POPPLER_CONST Function *func) { + int type = func->getType(); + auto space = shading->getColorSpace(); + if ( type == 0 || type == 2 ) { // Sampled or exponential function + GfxColor stop1, stop2; + if (!svgGetShadingColor(shading, 0.0, &stop1) || !svgGetShadingColor(shading, 1.0, &stop2)) { + return false; + } else { + _addStopToGradient(gradient, 0.0, &stop1, space, 1.0); + _addStopToGradient(gradient, 1.0, &stop2, space, 1.0); + } + } else if ( type == 3 ) { // Stitching + auto stitchingFunc = static_cast<_POPPLER_CONST StitchingFunction*>(func); + const double *bounds = stitchingFunc->getBounds(); + const double *encode = stitchingFunc->getEncode(); + int num_funcs = stitchingFunc->getNumFuncs(); + // Adjust gradient so it's always between 0.0 - 1.0 + double max_bound = std::max({1.0, bounds[num_funcs]}); + + // Add stops from all the stitched functions + GfxColor prev_color, color; + svgGetShadingColor(shading, bounds[0], &prev_color); + _addStopToGradient(gradient, bounds[0], &prev_color, space, 1.0); + for ( int i = 0 ; i < num_funcs ; i++ ) { + svgGetShadingColor(shading, bounds[i + 1], &color); + // Add stops + if (stitchingFunc->getFunc(i)->getType() == 2) { // process exponential fxn + double expE = (static_cast<_POPPLER_CONST ExponentialFunction*>(stitchingFunc->getFunc(i)))->getE(); + if (expE > 1.0) { + expE = (bounds[i + 1] - bounds[i])/expE; // approximate exponential as a single straight line at x=1 + if (encode[2*i] == 0) { // normal sequence + auto offset = (bounds[i + 1] - expE) / max_bound; + _addStopToGradient(gradient, offset, &prev_color, space, 1.0); + } else { // reflected sequence + auto offset = (bounds[i] + expE) / max_bound; + _addStopToGradient(gradient, offset, &color, space, 1.0); + } + } + } + _addStopToGradient(gradient, bounds[i + 1] / max_bound, &color, space, 1.0); + prev_color = color; + } + } else { // Unsupported function type + return false; + } + + return true; +} + +/** + * \brief Sets _invalidated_style to true to indicate that styles have to be updated + * Used for text output when glyphs are buffered till a font change + */ +void SvgBuilder::updateStyle(GfxState *state) { + if (_in_text_object) { + _invalidated_style = true; + } +} + +/** + * \brief Updates _css_font according to the font set in parameter state + */ +void SvgBuilder::updateFont(GfxState *state, std::shared_ptr<CairoFont> cairo_font, bool flip) +{ + TRACE(("updateFont()\n")); + updateTextMatrix(state, flip); // Ensure that we have a text matrix built + + auto font = state->getFont(); + auto font_id = font->getID()->num; + + auto new_font_size = state->getFontSize(); + if (font->getType() == fontType3) { + const double *font_matrix = font->getFontMatrix(); + if (font_matrix[0] != 0.0) { + new_font_size *= font_matrix[3] / font_matrix[0]; + } + } + if (new_font_size != _css_font_size) { + _css_font_size = new_font_size; + _invalidated_style = true; + } + bool was_css_font = (bool)_css_font; + // Clean up any previous css font + if (_css_font) { + sp_repr_css_attr_unref(_css_font); + _css_font = nullptr; + } + + auto font_strategy = FontFallback::AS_TEXT; + if (_font_strategies.find(font_id) != _font_strategies.end()) { + font_strategy = _font_strategies[font_id]; + } + + if (font_strategy == FontFallback::DELETE_TEXT) { + _invalidated_strategy = true; + _cairo_font = nullptr; + return; + } + if (font_strategy == FontFallback::AS_SHAPES) { + _invalidated_strategy = _invalidated_strategy || was_css_font; + _invalidated_style = (_cairo_font != cairo_font); + _cairo_font = cairo_font; + return; + } + + auto font_data = FontData(font); + _font_specification = font_data.getSpecification().c_str(); + _invalidated_strategy = (bool)_cairo_font; + _invalidated_style = true; + + // Font family + _cairo_font = nullptr; + _css_font = sp_repr_css_attr_new(); + if (font->getFamily()) { // if font family is explicitly given use it. + sp_repr_css_set_property(_css_font, "font-family", font->getFamily()->getCString()); + } else if (font_strategy == FontFallback::AS_SUB && !font_data.found) { + sp_repr_css_set_property(_css_font, "font-family", font_data.getSubstitute().c_str()); + } else { + sp_repr_css_set_property(_css_font, "font-family", font_data.family.c_str()); + } + + // Set the font data + sp_repr_css_set_property(_css_font, "font-style", font_data.style.c_str()); + sp_repr_css_set_property(_css_font, "font-weight", font_data.weight.c_str()); + sp_repr_css_set_property(_css_font, "font-stretch", font_data.stretch.c_str()); + sp_repr_css_set_property(_css_font, "font-variant", "normal"); + + // Writing mode + if ( font->getWMode() == 0 ) { + sp_repr_css_set_property(_css_font, "writing-mode", "lr"); + } else { + sp_repr_css_set_property(_css_font, "writing-mode", "tb"); + } +} + +/** + * \brief Shifts the current text position by the given amount (specified in text space) + */ +void SvgBuilder::updateTextShift(GfxState *state, double shift) { + double shift_value = -shift * 0.001 * fabs(state->getFontSize()); + if (state->getFont()->getWMode()) { + _text_position[1] += shift_value; + } else { + _text_position[0] += shift_value; + } +} + +/** + * \brief Updates current text position + */ +void SvgBuilder::updateTextPosition(double tx, double ty) { + _text_position = Geom::Point(tx, ty); +} + +/** + * \brief Flushes the buffered characters + */ +void SvgBuilder::updateTextMatrix(GfxState *state, bool flip) { + // Update text matrix, it contains an extra flip which we must undo. + auto new_matrix = Geom::Scale(1, flip ? -1 : 1) * ctmToAffine(state->getTextMat()); + // TODO: Detect if the text matrix is actually just a rotational kern + // this can help stich back together texts where letters are rotated + if (new_matrix != _text_matrix) { + _flushText(state); + _text_matrix = new_matrix; + } +} + +/** + * \brief Writes the buffered characters to the SVG document + * + * This is a dual path function that can produce either a text element + * or a group of path elements depending on the font handling mode. + */ +void SvgBuilder::_flushText(GfxState *state) +{ + // Set up a clipPath group + if (state->getRender() & 4 && !_clip_text_group) { + auto defs = _doc->getDefs()->getRepr(); + _clip_text_group = _pushContainer("svg:clipPath"); + _clip_text_group->setAttribute("clipPathUnits", "userSpaceOnUse"); + defs->appendChild(_clip_text_group); + Inkscape::GC::release(_clip_text_group); + } + + // Ignore empty strings + if (_glyphs.empty()) { + _glyphs.clear(); + return; + } + std::vector<SvgGlyph>::iterator i = _glyphs.begin(); + const SvgGlyph& first_glyph = (*i); + + // Ignore invisible characters + if (first_glyph.state->getRender() == 3) { + _glyphs.clear(); + return; + } + + // If cairo, then no text node is needed. + Inkscape::XML::Node *text_group = nullptr; + Inkscape::XML::Node *text_node = nullptr; + cairo_glyph_t *cairo_glyphs = nullptr; + unsigned int cairo_glyph_count = 0; + + if (!first_glyph.cairo_font) { + // we preserve spaces in the text objects we create, this applies to any descendant + text_node = _addToContainer("svg:text"); + text_node->setAttribute("xml:space", "preserve"); + } + + // Strip out text size from text_matrix and remove from text_transform + double text_scale = _text_matrix.expansionX(); + Geom::Affine tr = stateToAffine(state); + Geom::Affine text_transform = _text_matrix * tr * Geom::Scale(text_scale).inverse(); + // The glyph position must be moved by the document scale without flipping + // the text object itself. This is why the text affine is applied to the + // translation point and not simply used in the text element directly. + auto pos = first_glyph.position * tr; + text_transform.setTranslation(pos); + // Cache the text transform when clipping + if (_clip_text_group) { + svgSetTransform(_clip_text_group, text_transform); + } + + bool new_tspan = true; + bool same_coords[2] = {true, true}; + Geom::Point last_delta_pos; + unsigned int glyphs_in_a_row = 0; + Inkscape::XML::Node *tspan_node = nullptr; + Glib::ustring x_coords; + Glib::ustring y_coords; + Glib::ustring text_buffer; + + // Output all buffered glyphs + while (true) { + const SvgGlyph& glyph = (*i); + auto prev_iterator = (i == _glyphs.begin()) ? _glyphs.end() : (i-1); + // Check if we need to make a new tspan + if (glyph.style_changed) { + new_tspan = true; + } else if ( i != _glyphs.begin() ) { + const SvgGlyph& prev_glyph = (*prev_iterator); + if (!((glyph.delta[Geom::Y] == 0.0 && prev_glyph.delta[Geom::Y] == 0.0 && + glyph.text_position[1] == prev_glyph.text_position[1]) || + (glyph.delta[Geom::X] == 0.0 && prev_glyph.delta[Geom::X] == 0.0 && + glyph.text_position[0] == prev_glyph.text_position[0]))) { + new_tspan = true; + } + } + + // Create tspan node if needed + if (!first_glyph.cairo_font && text_node && (new_tspan || i == _glyphs.end())) { + if (tspan_node) { + // Set the x and y coordinate arrays + if (same_coords[0]) { + tspan_node->setAttributeSvgDouble("x", last_delta_pos[0]); + } else { + tspan_node->setAttributeOrRemoveIfEmpty("x", x_coords); + } + if (same_coords[1]) { + tspan_node->setAttributeSvgDouble("y", last_delta_pos[1]); + } else { + tspan_node->setAttributeOrRemoveIfEmpty("y", y_coords); + } + TRACE(("tspan content: %s\n", text_buffer.c_str())); + if ( glyphs_in_a_row > 1 ) { + tspan_node->setAttribute("sodipodi:role", "line"); + } + // Add text content node to tspan + Inkscape::XML::Node *text_content = _xml_doc->createTextNode(text_buffer.c_str()); + tspan_node->appendChild(text_content); + Inkscape::GC::release(text_content); + text_node->appendChild(tspan_node); + // Clear temporary buffers + x_coords.clear(); + y_coords.clear(); + text_buffer.clear(); + Inkscape::GC::release(tspan_node); + glyphs_in_a_row = 0; + } + if ( i == _glyphs.end() ) { + sp_repr_css_attr_unref((*prev_iterator).css_font); + break; + } else { + tspan_node = _xml_doc->createElement("svg:tspan"); + + // Set style and unref SPCSSAttr if it won't be needed anymore + // assume all <tspan> nodes in a <text> node share the same style + double text_size = text_scale * glyph.text_size; + sp_repr_css_set_property_double(glyph.css_font, "font-size", text_size); + _setTextStyle(tspan_node, glyph.state, glyph.css_font, text_transform); + if ( glyph.style_changed && i != _glyphs.begin() ) { // Free previous style + sp_repr_css_attr_unref((*prev_iterator).css_font); + } + } + new_tspan = false; + } + if ( glyphs_in_a_row > 0 && i != _glyphs.begin() ) { + x_coords.append(" "); + y_coords.append(" "); + // Check if we have the same coordinates + const SvgGlyph& prev_glyph = (*prev_iterator); + for ( int p = 0 ; p < 2 ; p++ ) { + if ( glyph.text_position[p] != prev_glyph.text_position[p] ) { + same_coords[p] = false; + } + } + } + // Append the coordinates to their respective strings + Geom::Point delta_pos(glyph.text_position - first_glyph.text_position); + delta_pos[1] += glyph.rise; + delta_pos[1] *= -1.0; // flip it + delta_pos *= Geom::Scale(text_scale); + Inkscape::CSSOStringStream os_x; + os_x << delta_pos[0]; + x_coords.append(os_x.str()); + Inkscape::CSSOStringStream os_y; + os_y << delta_pos[1]; + y_coords.append(os_y.str()); + last_delta_pos = delta_pos; + + if (first_glyph.cairo_font) { + if (!cairo_glyphs) { + cairo_glyphs = (cairo_glyph_t *)gmallocn(_glyphs.size(), sizeof(cairo_glyph_t)); + } + bool is_last_glyph = i + 1 == _glyphs.end(); + + // Push the data into the cairo glyph list for later rendering. + cairo_glyphs[cairo_glyph_count].index = glyph.cairo_index; + cairo_glyphs[cairo_glyph_count].x = delta_pos[Geom::X]; + cairo_glyphs[cairo_glyph_count].y = delta_pos[Geom::Y]; + cairo_glyph_count++; + + bool style_will_change = is_last_glyph ? true : (i+1)->style_changed; + if (style_will_change) { + if (style_will_change && !is_last_glyph && !text_group) { + // We create a group, so each style can be contained within the resulting path. + text_group = _pushGroup(); + } + + // Render and set the style for this drawn text. + double text_size = text_scale * glyph.text_size; + + // Set to 'text_node' because if the style does NOT change, we won't have a group + // but still need to set this text's position and blend modes. + text_node = _renderText(glyph.cairo_font, text_size, text_transform, cairo_glyphs, cairo_glyph_count); + if (text_node) { + _setTextStyle(text_node, glyph.state, nullptr, text_transform); + } + + // Free up the used glyph stack. + gfree(cairo_glyphs); + cairo_glyphs = nullptr; + cairo_glyph_count = 0; + + if (is_last_glyph) { + // Stop drawing text now, we have cleaned up. + break; + } + } + } else { + // Append the character to the text buffer + if (!glyph.code.empty()) { + text_buffer.append(1, glyph.code[0]); + } + + /* Append any utf8 conversion doublets and request a new tspan. + * + * This is a fix for the unusual situation in some PDF files that use + * certain fonts where two ascii letters have been bolted together into + * one Unicode position and our conversion to UTF8 produces extra glyphs + * which if we don't add will be missing and if we add without ending the + * tspan will cause the rest of the glyph-positions to be off by one. + */ + for (int j = 1; j < glyph.code.size(); j++) { + text_buffer.append(1, glyph.code[j]); + new_tspan = true; + } + } + + glyphs_in_a_row++; + ++i; + } + if (text_group) { + // Pop the group so the clip and transform can be applied to it. + text_node = text_group; + _popGroup(); + } + + if (text_node) { + if (first_glyph.cairo_font) { + // Save aria-label for any rendered text blocks + text_node->setAttribute("aria-label", _aria_label); + } + + // Set the text matrix which sits under the page's position + _setBlendMode(text_node, state); + svgSetTransform(text_node, text_transform * _page_affine); + _setClipPath(text_node); + } + + _aria_label = ""; + _glyphs.clear(); +} + +/** + * Sets the style for the text, rendered or un-rendered, preserving the text_transform for any + * gradients or other patterns. These values were promised to us when the font was updated. + */ +void SvgBuilder::_setTextStyle(Inkscape::XML::Node *node, GfxState *state, SPCSSAttr *font_style, Geom::Affine ta) +{ + int render_mode = state->getRender(); + bool has_fill = !(render_mode & 1); + bool has_stroke = ( render_mode & 3 ) == 1 || ( render_mode & 3 ) == 2; + + state = state->save(); + state->setCTM(ta[0], ta[1], ta[2], ta[3], ta[4], ta[5]); + auto style = _setStyle(state, has_fill, has_stroke); + state = state->restore(); + if (font_style) { + sp_repr_css_merge(style, font_style); + } + sp_repr_css_change(node, style, "style"); + sp_repr_css_attr_unref(style); +} + +/** + * Renders the text as a path object using cairo and returns the node object. + * + * cairo_font - The font that cairo can use to convert text to path. + * font_size - The size of the text when drawing the path. + * transform - The matrix which will place the text on the page, this is critical + * to allow cairo to render all the required parts of the text. + * cairo_glyphs - A pointer to a list of glyphs to render. + * count - A count of the number of glyphs to render. + */ +Inkscape::XML::Node *SvgBuilder::_renderText(std::shared_ptr<CairoFont> cairo_font, double font_size, + const Geom::Affine &transform, + cairo_glyph_t *cairo_glyphs, unsigned int count) +{ + if (!cairo_glyphs || !cairo_font || _aria_label.empty()) + return nullptr; + + // The surface isn't actually used, no rendering in cairo takes place. + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, _width, _height); + cairo_t *cairo = cairo_create(surface); + cairo_set_font_face(cairo, cairo_font->getFontFace()); + cairo_set_font_size(cairo, font_size); + ink_cairo_transform(cairo, transform); + cairo_glyph_path(cairo, cairo_glyphs, count); + auto pathv = extract_pathvector_from_cairo(cairo); + cairo_destroy(cairo); + cairo_surface_destroy(surface); + + // Failing to render text. + if (!pathv) { + g_warning("Failed to render PDF text!"); + return nullptr; + } + + auto textpath = sp_svg_write_path(*pathv); + if (textpath.empty()) + return nullptr; + + Inkscape::XML::Node *path = _addToContainer("svg:path"); + path->setAttribute("d", textpath); + return path; +} + +/** + * Begin and end string is the inner most text processing step + * which tells us we're about to have a certain number of chars. + */ +void SvgBuilder::beginString(GfxState *state, int len) +{ + if (!_glyphs.empty()) { + // What to do about unflushed text in the buffer. + if (_invalidated_strategy) { + _flushText(state); + _invalidated_strategy = false; + } else { + // Add seperator for aria text. + _aria_space = true; + } + } + IFTRACE(double *m = state->getTextMat()); + TRACE(("tm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5])); + IFTRACE(m = state->getCTM()); + TRACE(("ctm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5])); +} +void SvgBuilder::endString(GfxState *state) +{ +} + +/** + * \brief Adds the specified character to the text buffer + * Takes care of converting it to UTF-8 and generates a new style repr if style + * has changed since the last call. + */ +void SvgBuilder::addChar(GfxState *state, double x, double y, double dx, double dy, double originX, double originY, + CharCode code, int /*nBytes*/, Unicode const *u, int uLen) +{ + if (_aria_space) { + const SvgGlyph& prev_glyph = _glyphs.back(); + // This helps reconstruct the aria text, though it could be made better + if (prev_glyph.position[Geom::Y] != (y - originY)) { + _aria_label += "\n"; + } + _aria_space = false; + } + static std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> conv1; + if (u) { + _aria_label += conv1.to_bytes(*u); + } + + // Skip control characters, found in LaTeX generated PDFs + // https://gitlab.com/inkscape/inkscape/-/issues/1369 + if (uLen > 0 && u[0] < 0x80 && g_ascii_iscntrl(u[0]) && !g_ascii_isspace(u[0])) { + g_warning("Skipping ASCII control character %u", u[0]); + _text_position += Geom::Point(dx, dy); + return; + } + + if (!_css_font && !_cairo_font) { + // Deleted text. + return; + } + + bool is_space = ( uLen == 1 && u[0] == 32 ); + // Skip beginning space + if ( is_space && _glyphs.empty()) { + Geom::Point delta(dx, dy); + _text_position += delta; + return; + } + // Allow only one space in a row + if ( is_space && (_glyphs[_glyphs.size() - 1].code.size() == 1) && + (_glyphs[_glyphs.size() - 1].code[0] == 32) ) { + Geom::Point delta(dx, dy); + _text_position += delta; + return; + } + + SvgGlyph new_glyph; + new_glyph.is_space = is_space; + new_glyph.delta = Geom::Point(dx, dy); + new_glyph.position = Geom::Point( x - originX, y - originY ); + new_glyph.text_position = _text_position; + new_glyph.text_size = _css_font_size; + new_glyph.state = state; + if (_cairo_font) { + new_glyph.cairo_font = _cairo_font; + new_glyph.cairo_index = _cairo_font->getGlyph(code, u, uLen); + } + _text_position += new_glyph.delta; + + // Convert the character to UTF-8 since that's our SVG document's encoding + { + gunichar2 uu[8] = {0}; + + for (int i = 0; i < uLen; i++) { + uu[i] = u[i]; + } + + gchar *tmp = g_utf16_to_utf8(uu, uLen, nullptr, nullptr, nullptr); + if ( tmp && *tmp ) { + new_glyph.code = tmp; + } else { + new_glyph.code.clear(); + } + g_free(tmp); + } + + // Copy current style if it has changed since the previous glyph + if (_invalidated_style || _glyphs.empty()) { + _invalidated_style = false; + new_glyph.style_changed = true; + if (_css_font) { + new_glyph.css_font = sp_repr_css_attr_new(); + sp_repr_css_merge(new_glyph.css_font, _css_font); + } + } else { + new_glyph.style_changed = false; + // Point to previous glyph's style information + const SvgGlyph& prev_glyph = _glyphs.back(); + new_glyph.css_font = prev_glyph.css_font; + } + new_glyph.font_specification = _font_specification; + new_glyph.rise = state->getRise(); + + _glyphs.push_back(new_glyph); +} + +/** + * These text object functions are the outer most calls for begining and + * ending text. No text functions should be called outside of these two calls + */ +void SvgBuilder::beginTextObject(GfxState *state) { + _in_text_object = true; + _invalidated_style = true; // Force copying of current state +} + +void SvgBuilder::endTextObject(GfxState *state) +{ + _in_text_object = false; + _flushText(state); + + if (_clip_text_group) { + // Use the clip as a real clip path + _clip_text = _popContainer(); + _clip_text_group = nullptr; + } +} + +/** + * Helper functions for supporting direct PNG output into a base64 encoded stream + */ +void png_write_vector(png_structp png_ptr, png_bytep data, png_size_t length) +{ + auto *v_ptr = reinterpret_cast<std::vector<guchar> *>(png_get_io_ptr(png_ptr)); // Get pointer to stream + for ( unsigned i = 0 ; i < length ; i++ ) { + v_ptr->push_back(data[i]); + } +} + +/** + * \brief Creates an <image> element containing the given ImageStream as a PNG + * + */ +Inkscape::XML::Node *SvgBuilder::_createImage(Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + int *mask_colors, bool alpha_only, + bool invert_alpha) { + + // Create PNG write struct + png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if ( png_ptr == nullptr ) { + return nullptr; + } + // Create PNG info struct + png_infop info_ptr = png_create_info_struct(png_ptr); + if ( info_ptr == nullptr ) { + png_destroy_write_struct(&png_ptr, nullptr); + return nullptr; + } + // Set error handler + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_write_struct(&png_ptr, &info_ptr); + return nullptr; + } + // Decide whether we should embed this image + bool embed_image = _preferences->getAttributeBoolean("embedImages", true); + + // Set read/write functions + std::vector<guchar> png_buffer; + FILE *fp = nullptr; + gchar *file_name = nullptr; + if (embed_image) { + png_set_write_fn(png_ptr, &png_buffer, png_write_vector, nullptr); + } else { + static int counter = 0; + file_name = g_strdup_printf("%s_img%d.png", _docname, counter++); + fp = fopen(file_name, "wb"); + if ( fp == nullptr ) { + png_destroy_write_struct(&png_ptr, &info_ptr); + g_free(file_name); + return nullptr; + } + png_init_io(png_ptr, fp); + } + + // Set header data + if ( !invert_alpha && !alpha_only ) { + png_set_invert_alpha(png_ptr); + } + png_color_8 sig_bit; + if (alpha_only) { + png_set_IHDR(png_ptr, info_ptr, + width, + height, + 8, /* bit_depth */ + PNG_COLOR_TYPE_GRAY, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, + PNG_FILTER_TYPE_BASE); + sig_bit.red = 0; + sig_bit.green = 0; + sig_bit.blue = 0; + sig_bit.gray = 8; + sig_bit.alpha = 0; + } else { + png_set_IHDR(png_ptr, info_ptr, + width, + height, + 8, /* bit_depth */ + PNG_COLOR_TYPE_RGB_ALPHA, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, + PNG_FILTER_TYPE_BASE); + sig_bit.red = 8; + sig_bit.green = 8; + sig_bit.blue = 8; + sig_bit.alpha = 8; + } + png_set_sBIT(png_ptr, info_ptr, &sig_bit); + png_set_bgr(png_ptr); + // Write the file header + png_write_info(png_ptr, info_ptr); + + // Convert pixels + ImageStream *image_stream; + if (alpha_only) { + if (color_map) { + image_stream = new ImageStream(str, width, color_map->getNumPixelComps(), + color_map->getBits()); + } else { + image_stream = new ImageStream(str, width, 1, 1); + } + image_stream->reset(); + + // Convert grayscale values + unsigned char *buffer = new unsigned char[width]; + int invert_bit = invert_alpha ? 1 : 0; + for ( int y = 0 ; y < height ; y++ ) { + unsigned char *row = image_stream->getLine(); + if (color_map) { + color_map->getGrayLine(row, buffer, width); + } else { + unsigned char *buf_ptr = buffer; + for ( int x = 0 ; x < width ; x++ ) { + if ( row[x] ^ invert_bit ) { + *buf_ptr++ = 0; + } else { + *buf_ptr++ = 255; + } + } + } + png_write_row(png_ptr, (png_bytep)buffer); + } + delete [] buffer; + } else if (color_map) { + image_stream = new ImageStream(str, width, + color_map->getNumPixelComps(), + color_map->getBits()); + image_stream->reset(); + + // Convert RGB values + unsigned int *buffer = new unsigned int[width]; + if (mask_colors) { + for ( int y = 0 ; y < height ; y++ ) { + unsigned char *row = image_stream->getLine(); + color_map->getRGBLine(row, buffer, width); + + unsigned int *dest = buffer; + for ( int x = 0 ; x < width ; x++ ) { + // Check each color component against the mask + for ( int i = 0; i < color_map->getNumPixelComps() ; i++) { + if ( row[i] < mask_colors[2*i] * 255 || + row[i] > mask_colors[2*i + 1] * 255 ) { + *dest = *dest | 0xff000000; + break; + } + } + // Advance to the next pixel + row += color_map->getNumPixelComps(); + dest++; + } + // Write it to the PNG + png_write_row(png_ptr, (png_bytep)buffer); + } + } else { + for ( int i = 0 ; i < height ; i++ ) { + unsigned char *row = image_stream->getLine(); + memset((void*)buffer, 0xff, sizeof(int) * width); + color_map->getRGBLine(row, buffer, width); + png_write_row(png_ptr, (png_bytep)buffer); + } + } + delete [] buffer; + + } else { // A colormap must be provided, so quit + png_destroy_write_struct(&png_ptr, &info_ptr); + if (!embed_image) { + fclose(fp); + g_free(file_name); + } + return nullptr; + } + delete image_stream; + str->close(); + // Close PNG + png_write_end(png_ptr, info_ptr); + png_destroy_write_struct(&png_ptr, &info_ptr); + + // Create repr + Inkscape::XML::Node *image_node = _xml_doc->createElement("svg:image"); + image_node->setAttributeSvgDouble("width", 1); + image_node->setAttributeSvgDouble("height", 1); + if( !interpolate ) { + SPCSSAttr *css = sp_repr_css_attr_new(); + // This should be changed after CSS4 Images widely supported. + sp_repr_css_set_property(css, "image-rendering", "optimizeSpeed"); + sp_repr_css_change(image_node, css, "style"); + sp_repr_css_attr_unref(css); + } + + // PS/PDF images are placed via a transformation matrix, no preserveAspectRatio used + image_node->setAttribute("preserveAspectRatio", "none"); + + // Create href + if (embed_image) { + // Append format specification to the URI + auto *base64String = g_base64_encode(png_buffer.data(), png_buffer.size()); + auto png_data = std::string("data:image/png;base64,") + base64String; + g_free(base64String); + image_node->setAttributeOrRemoveIfEmpty("xlink:href", png_data); + } else { + fclose(fp); + image_node->setAttribute("xlink:href", file_name); + g_free(file_name); + } + + return image_node; +} + +/** + * \brief Creates a <mask> with the specified width and height and adds to <defs> + * If we're not the top-level SvgBuilder, creates a <defs> too and adds the mask to it. + * \return the created XML node + */ +Inkscape::XML::Node *SvgBuilder::_createMask(double width, double height) { + Inkscape::XML::Node *mask_node = _xml_doc->createElement("svg:mask"); + mask_node->setAttribute("maskUnits", "userSpaceOnUse"); + mask_node->setAttributeSvgDouble("x", 0.0); + mask_node->setAttributeSvgDouble("y", 0.0); + mask_node->setAttributeSvgDouble("width", width); + mask_node->setAttributeSvgDouble("height", height); + // Append mask to defs + if (_is_top_level) { + _doc->getDefs()->getRepr()->appendChild(mask_node); + Inkscape::GC::release(mask_node); + return _doc->getDefs()->getRepr()->lastChild(); + } else { // Work around for renderer bug when mask isn't defined in pattern + static int mask_count = 0; + gchar *mask_id = g_strdup_printf("_mask%d", mask_count++); + mask_node->setAttribute("id", mask_id); + g_free(mask_id); + _doc->getDefs()->getRepr()->appendChild(mask_node); + Inkscape::GC::release(mask_node); + return mask_node; + } +} + +void SvgBuilder::addImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map, + bool interpolate, int *mask_colors) +{ + Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, mask_colors); + if (image_node) { + _setBlendMode(image_node, state); + _setTransform(image_node, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0)); + _addToContainer(image_node); + _setClipPath(image_node); + } +} + +void SvgBuilder::addImageMask(GfxState *state, Stream *str, int width, int height, + bool invert, bool interpolate) { + + // Create a rectangle + Inkscape::XML::Node *rect = _addToContainer("svg:rect"); + rect->setAttributeSvgDouble("x", 0.0); + rect->setAttributeSvgDouble("y", 0.0); + rect->setAttributeSvgDouble("width", 1.0); + rect->setAttributeSvgDouble("height", 1.0); + + // Get current fill style and set it on the rectangle + SPCSSAttr *css = sp_repr_css_attr_new(); + _setFillStyle(css, state, false); + sp_repr_css_change(rect, css, "style"); + sp_repr_css_attr_unref(css); + _setBlendMode(rect, state); + _setTransform(rect, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0)); + _setClipPath(rect); + + // Scaling 1x1 surfaces might not work so skip setting a mask with this size + if ( width > 1 || height > 1 ) { + Inkscape::XML::Node *mask_image_node = + _createImage(str, width, height, nullptr, interpolate, nullptr, true, invert); + if (mask_image_node) { + // Create the mask + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Remove unnecessary transformation from the mask image + mask_image_node->removeAttribute("transform"); + mask_node->appendChild(mask_image_node); + Inkscape::GC::release(mask_image_node); + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + rect->setAttribute("mask", mask_url); + g_free(mask_url); + } + } +} + +void SvgBuilder::addMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map, + bool interpolate, Stream *mask_str, int mask_width, int mask_height, bool invert_mask, + bool mask_interpolate) +{ + Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height, + nullptr, mask_interpolate, nullptr, true, invert_mask); + Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr); + if ( mask_image_node && image_node ) { + // Create mask for the image + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Remove unnecessary transformation from the mask image + mask_image_node->removeAttribute("transform"); + mask_node->appendChild(mask_image_node); + // Scale the mask to the size of the image + Geom::Affine mask_transform((double)width, 0.0, 0.0, (double)height, 0.0, 0.0); + mask_node->setAttributeOrRemoveIfEmpty("maskTransform", sp_svg_transform_write(mask_transform)); + // Set mask and add image + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + image_node->setAttribute("mask", mask_url); + g_free(mask_url); + _setBlendMode(image_node, state); + _setTransform(image_node, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0)); + _addToContainer(image_node); + _setClipPath(image_node); + } else if (image_node) { + Inkscape::GC::release(image_node); + } + if (mask_image_node) { + Inkscape::GC::release(mask_image_node); + } +} + +void SvgBuilder::addSoftMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map, + bool interpolate, Stream *mask_str, int mask_width, int mask_height, + GfxImageColorMap *mask_color_map, bool mask_interpolate) +{ + Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height, + mask_color_map, mask_interpolate, nullptr, true); + Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr); + if ( mask_image_node && image_node ) { + // Create mask for the image + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Remove unnecessary transformation from the mask image + mask_image_node->removeAttribute("transform"); + mask_node->appendChild(mask_image_node); + // Set mask and add image + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + image_node->setAttribute("mask", mask_url); + g_free(mask_url); + _addToContainer(image_node); + _setBlendMode(image_node, state); + _setTransform(image_node, state, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0)); + _setClipPath(image_node); + } else if (image_node) { + Inkscape::GC::release(image_node); + } + if (mask_image_node) { + Inkscape::GC::release(mask_image_node); + } +} + +/** + * Find the fill or stroke gradient we previously set on this node. + */ +Inkscape::XML::Node *SvgBuilder::_getGradientNode(Inkscape::XML::Node *node, bool is_fill) +{ + auto css = sp_repr_css_attr(node, "style"); + if (auto id = try_extract_uri_id(css->attribute(is_fill ? "fill" : "stroke"))) { + if (auto obj = _doc->getObjectById(*id)) { + return obj->getRepr(); + } + } + return nullptr; +} + +bool SvgBuilder::_attrEqual(Inkscape::XML::Node *a, Inkscape::XML::Node *b, char const *attr) +{ + return (!a->attribute(attr) && !b->attribute(attr)) || std::string(a->attribute(attr)) == b->attribute(attr); +} + +/** + * Take a constructed mask and decide how to apply it to the target. + */ +void SvgBuilder::applyOptionalMask(Inkscape::XML::Node *mask, Inkscape::XML::Node *target) +{ + // Merge transparency gradient back into real gradient if possible + if (mask->childCount() == 1) { + auto source = mask->firstChild(); + auto source_gr = _getGradientNode(source, true); + auto target_gr = _getGradientNode(target, true); + // Both objects have a gradient, try and merge them + if (source_gr && target_gr && source_gr->childCount() == target_gr->childCount()) { + bool same_pos = _attrEqual(source_gr, target_gr, "x1") && _attrEqual(source_gr, target_gr, "x2") + && _attrEqual(source_gr, target_gr, "y1") && _attrEqual(source_gr, target_gr, "y2"); + + bool white_mask = false; + for (auto source_st = source_gr->firstChild(); source_st != nullptr; source_st = source_st->next()) { + auto source_css = sp_repr_css_attr(source_st, "style"); + white_mask = white_mask or source_css->getAttributeDouble("stop-opacity") != 1.0; + if (std::string(source_css->attribute("stop-color")) != "#ffffff") { + white_mask = false; + break; + } + } + + if (same_pos && white_mask) { + // We move the stop-opacity from the source to the target + auto target_st = target_gr->firstChild(); + for (auto source_st = source_gr->firstChild(); source_st != nullptr; source_st = source_st->next()) { + auto target_css = sp_repr_css_attr(target_st, "style"); + auto source_css = sp_repr_css_attr(source_st, "style"); + sp_repr_css_set_property(target_css, "stop-opacity", source_css->attribute("stop-opacity")); + sp_repr_css_change(target_st, target_css, "style"); + target_st = target_st->next(); + } + // Remove mask and gradient xml objects + mask->parent()->removeChild(mask); + source_gr->parent()->removeChild(source_gr); + return; + } + } + } + gchar *mask_url = g_strdup_printf("url(#%s)", mask->attribute("id")); + target->setAttribute("mask", mask_url); + g_free(mask_url); +} + + +/** + * \brief Starts building a new transparency group + */ +void SvgBuilder::startGroup(GfxState *state, double *bbox, GfxColorSpace * /*blending_color_space*/, bool isolated, + bool knockout, bool for_softmask) +{ + // Push group node, but don't attach to previous container yet + _pushContainer("svg:g"); + + if (for_softmask) { + _mask_groups.push_back(state); + // Create a container for the mask + _pushContainer(_createMask(1.0, 1.0)); + } + + // TODO: In the future we could use state to insert transforms + // and then remove the inverse from the items added into the children + // to reduce the transformational duplication. +} + +void SvgBuilder::finishGroup(GfxState *state, bool for_softmask) +{ + if (for_softmask) { + // Create mask + auto mask_node = _popContainer(); + applyOptionalMask(mask_node, _container); + } else { + popGroup(state); + } +} + +void SvgBuilder::popGroup(GfxState *state) +{ + // Restore node stack + auto parent = _popContainer(); + bool will_clip = _clip_history->hasClipPath() && !_clip_history->isBoundingBox(); + + if (parent->childCount() == 1 && !parent->attribute("transform")) { + // Merge this opacity and remove unnecessary group + auto child = parent->firstChild(); + + if (will_clip && child->attribute("d")) { + // Note to future: this means the group contains a single path, this path is likely + // a fake bounding box path and the real path is contained within the clipping region + // Moving the clipping region out into the path object and deleting the group would + // improve output here. + } + + // Do not merge masked or clipped groups, to avoid clobering + if (!will_clip && !child->attribute("mask") && !child->attribute("clip-path")) { + auto orig = child->getAttributeDouble("opacity", 1.0); + auto grp = parent->getAttributeDouble("opacity", 1.0); + child->setAttributeSvgDouble("opacity", orig * grp); + + if (auto mask_id = try_extract_uri_id(parent->attribute("mask"))) { + if (auto obj = _doc->getObjectById(*mask_id)) { + applyOptionalMask(obj->getRepr(), child); + } + } + if (auto clip = parent->attribute("clip-path")) { + child->setAttribute("clip-path", clip); + } + + // This duplicate child will get applied in the place of the group + parent->removeChild(child); + Inkscape::GC::anchor(child); + parent = child; + } + } + + // Add the parent to the last container + _addToContainer(parent); + _setClipPath(parent); +} + +/** + * Decide what to do for each font in the font list, with the given strategy. + */ +FontStrategies SvgBuilder::autoFontStrategies(FontStrategy s, FontList fonts) +{ + FontStrategies ret; + for (auto font : *fonts.get()) { + int id = font.first->getID()->num; + bool found = font.second.found; + switch (s) { + case FontStrategy::RENDER_ALL: + ret[id] = FontFallback::AS_SHAPES; + break; + case FontStrategy::DELETE_ALL: + ret[id] = FontFallback::DELETE_TEXT; + break; + case FontStrategy::RENDER_MISSING: + ret[id] = found ? FontFallback::AS_TEXT : FontFallback::AS_SHAPES; + break; + case FontStrategy::SUBSTITUTE_MISSING: + ret[id] = found ? FontFallback::AS_TEXT : FontFallback::AS_SUB; + break; + case FontStrategy::KEEP_MISSING: + ret[id] = FontFallback::AS_TEXT; + break; + case FontStrategy::DELETE_MISSING: + ret[id] = found ? FontFallback::AS_TEXT : FontFallback::DELETE_TEXT; + break; + } + } + return ret; +} +} } } /* namespace Inkscape, Extension, Internal */ + +#endif /* HAVE_POPPLER */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/pdfinput/svg-builder.h b/src/extension/internal/pdfinput/svg-builder.h new file mode 100644 index 0000000..6d43095 --- /dev/null +++ b/src/extension/internal/pdfinput/svg-builder.h @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_EXTENSION_INTERNAL_PDFINPUT_SVGBUILDER_H +#define SEEN_EXTENSION_INTERNAL_PDFINPUT_SVGBUILDER_H + +/* + * Authors: + * miklos erdelyi + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +#include "poppler-transition-api.h" + +class SPDocument; +namespace Inkscape { + namespace XML { + struct Document; + class Node; + } +} + +#define Operator Operator_Gfx +#include <Gfx.h> +#undef Operator + +#include <2geom/affine.h> +#include <2geom/point.h> +#include <cairo-ft.h> +#include <glibmm/ustring.h> +#include <lcms2.h> + +#include "CharTypes.h" +#include "enums.h" +#include "poppler-utils.h" +class Function; +class GfxState; +struct GfxColor; +class GfxColorSpace; +enum GfxClipType; +struct GfxRGB; +class GfxPath; +class GfxPattern; +class GfxTilingPattern; +class GfxShading; +class GfxFont; +class GfxImageColorMap; +class Stream; +class XRef; + +class CairoFont; +class SPCSSAttr; +class ClipHistoryEntry; + +#include <glib.h> +#include <map> +#include <memory> +#include <vector> + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + * Holds information about glyphs added by PdfParser which haven't been added + * to the document yet. + */ +struct SvgGlyph { + Geom::Point position; // Absolute glyph coords + Geom::Point text_position; // Absolute glyph coords in text space + Geom::Point delta; // X, Y advance values + double rise; // Text rise parameter + Glib::ustring code; // UTF-8 coded character + bool is_space; + + bool style_changed; // Set to true if style has to be reset + GfxState *state; // A promise of the future text style + double text_size; // Text size + + const char *font_specification; // Pointer to current font specification + SPCSSAttr *css_font; // The font style as a css style + unsigned int cairo_index; // The index into the selected cairo font + std::shared_ptr<CairoFont> cairo_font; // A pointer to the selected cairo font +}; + +/** + * Builds the inner SVG representation using libpoppler from the calls of PdfParser. + */ +class SvgBuilder { +public: + SvgBuilder(SPDocument *document, gchar *docname, XRef *xref); + SvgBuilder(SvgBuilder *parent, Inkscape::XML::Node *root); + virtual ~SvgBuilder(); + + // Property setting + void setDocumentSize(double width, double height); // Document size in px + void setMargins(const Geom::Rect &page, const Geom::Rect &margins, const Geom::Rect &bleed); + void cropPage(const Geom::Rect &bbox); + void setAsLayer(const char *layer_name = nullptr, bool visible = true); + void setGroupOpacity(double opacity); + Inkscape::XML::Node *getPreferences() { + return _preferences; + } + void pushPage(const std::string &label, GfxState *state); + + // Path adding + bool shouldMergePath(bool is_fill, const std::string &path); + bool mergePath(GfxState *state, bool is_fill, const std::string &path, bool even_odd = false); + void addPath(GfxState *state, bool fill, bool stroke, bool even_odd=false); + void addClippedFill(GfxShading *shading, const Geom::Affine shading_tr); + void addShadedFill(GfxShading *shading, const Geom::Affine shading_tr, GfxPath *path, const Geom::Affine tr, + bool even_odd = false); + + // Image handling + void addImage(GfxState *state, Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, int *mask_colors); + void addImageMask(GfxState *state, Stream *str, int width, int height, + bool invert, bool interpolate); + void addMaskedImage(GfxState *state, Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + Stream *mask_str, int mask_width, int mask_height, + bool invert_mask, bool mask_interpolate); + void addSoftMaskedImage(GfxState *state, Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + Stream *mask_str, int mask_width, int mask_height, + GfxImageColorMap *mask_color_map, bool mask_interpolate); + void applyOptionalMask(Inkscape::XML::Node *mask, Inkscape::XML::Node *target); + + // Groups, Transparency group and soft mask handling + void startGroup(GfxState *state, double *bbox, GfxColorSpace *blending_color_space, bool isolated, bool knockout, + bool for_softmask); + void finishGroup(GfxState *state, bool for_softmask); + void popGroup(GfxState *state); + + // Text handling + void beginString(GfxState *state, int len); + void endString(GfxState *state); + void addChar(GfxState *state, double x, double y, + double dx, double dy, + double originX, double originY, + CharCode code, int nBytes, Unicode const *u, int uLen); + void beginTextObject(GfxState *state); + void endTextObject(GfxState *state); + + bool isPatternTypeSupported(GfxPattern *pattern); + void setFontStrategies(FontStrategies fs) { _font_strategies = fs; } + static FontStrategies autoFontStrategies(FontStrategy s, FontList fonts); + + // State manipulation + void saveState(GfxState *state); + void restoreState(GfxState *state); + void updateStyle(GfxState *state); + void updateFont(GfxState *state, std::shared_ptr<CairoFont> cairo_font, bool flip); + void updateTextPosition(double tx, double ty); + void updateTextShift(GfxState *state, double shift); + void updateTextMatrix(GfxState *state, bool flip); + + // Clipping + void setClip(GfxState *state, GfxClipType clip, bool is_bbox = false); + + // Layers i.e Optional Groups + void addOptionalGroup(const std::string &oc, const std::string &label, bool visible = true); + void beginMarkedContent(const char *name = nullptr, const char *group = nullptr); + void endMarkedContent(); + + void addColorProfile(unsigned char *profBuf, int length); + +private: + void _init(); + + // Pattern creation + gchar *_createPattern(GfxPattern *pattern, GfxState *state, bool is_stroke=false); + gchar *_createGradient(GfxShading *shading, const Geom::Affine pat_matrix, bool for_shading = false); + void _addStopToGradient(Inkscape::XML::Node *gradient, double offset, GfxColor *color, GfxColorSpace *space, + double opacity); + bool _addGradientStops(Inkscape::XML::Node *gradient, GfxShading *shading, + _POPPLER_CONST Function *func); + gchar *_createTilingPattern(GfxTilingPattern *tiling_pattern, GfxState *state, + bool is_stroke=false); + // Image/mask creation + Inkscape::XML::Node *_createImage(Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + int *mask_colors, bool alpha_only=false, + bool invert_alpha=false); + Inkscape::XML::Node *_createMask(double width, double height); + Inkscape::XML::Node *_createClip(const std::string &d, const Geom::Affine tr, bool even_odd); + + // Style setting + SPCSSAttr *_setStyle(GfxState *state, bool fill, bool stroke, bool even_odd=false); + void _setStrokeStyle(SPCSSAttr *css, GfxState *state); + void _setFillStyle(SPCSSAttr *css, GfxState *state, bool even_odd); + void _setTextStyle(Inkscape::XML::Node *node, GfxState *state, SPCSSAttr *font_style, Geom::Affine text_affine); + void _setBlendMode(Inkscape::XML::Node *node, GfxState *state); + void _setTransform(Inkscape::XML::Node *node, GfxState *state, Geom::Affine extra = Geom::identity()); + // Write buffered text into doc + void _flushText(GfxState *state); + std::string _aria_label; + bool _aria_space = false; + + // Handling of node stack + Inkscape::XML::Node *_pushGroup(); + Inkscape::XML::Node *_popGroup(); + Inkscape::XML::Node *_pushContainer(const char *name); + Inkscape::XML::Node *_pushContainer(Inkscape::XML::Node *node); + Inkscape::XML::Node *_popContainer(); + std::vector<Inkscape::XML::Node *> _node_stack; + std::vector<GfxState *> _mask_groups; + int _clip_groups = 0; + + Inkscape::XML::Node *_getClip(const Geom::Affine &node_tr); + Inkscape::XML::Node *_addToContainer(const char *name); + Inkscape::XML::Node *_renderText(std::shared_ptr<CairoFont> cairo_font, double font_size, + const Geom::Affine &transform, + cairo_glyph_t *cairo_glyphs, unsigned int count); + + void _setClipPath(Inkscape::XML::Node *node); + void _addToContainer(Inkscape::XML::Node *node, bool release = true); + + Inkscape::XML::Node *_getGradientNode(Inkscape::XML::Node *node, bool is_fill); + static bool _attrEqual(Inkscape::XML::Node *a, Inkscape::XML::Node *b, char const *attr); + + // Colors + std::string convertGfxColor(const GfxColor *color, GfxColorSpace *space); + std::string _getColorProfile(cmsHPROFILE hp); + + // The calculated font style, if not set, the text must be rendered with cairo instead. + FontStrategies _font_strategies; + double _css_font_size = 1.0; + SPCSSAttr *_css_font; + const char *_font_specification; + double _text_size; + Geom::Affine _text_matrix; + Geom::Point _text_position; + std::vector<SvgGlyph> _glyphs; // Added characters + + // The font when drawing the text into vector glyphs instead of text elements. + std::shared_ptr<CairoFont> _cairo_font; + + bool _in_text_object; // Whether we are inside a text object + bool _invalidated_style; + bool _invalidated_strategy = false; + bool _for_softmask = false; + + bool _is_top_level; // Whether this SvgBuilder is the top-level one + SPDocument *_doc; + gchar *_docname; // Basename of the URI from which this document is created + XRef *_xref; // Cross-reference table from the PDF doc we're converting from + Inkscape::XML::Document *_xml_doc; + Inkscape::XML::Node *_root; // Root node from the point of view of this SvgBuilder + Inkscape::XML::Node *_container; // Current container (group/pattern/mask) + Inkscape::XML::Node *_preferences; // Preferences container node + double _width; // Document size in px + double _height; // Document size in px + + Inkscape::XML::Node *_page = nullptr; // XML Page definition + int _page_num = 0; // Are we on a page + double _page_left = 0 ; // Move to the left for more pages + double _page_top = 0 ; // Move to the top (maybe) + bool _page_offset = false; + Geom::Affine _page_affine = Geom::identity(); + + std::map<std::string, std::pair<std::string, bool>> _ocgs; + + std::string _icc_profile; + std::map<cmsHPROFILE, std::string> _icc_profiles; + + ClipHistoryEntry *_clip_history; // clip path stack + Inkscape::XML::Node *_clip_text = nullptr; + Inkscape::XML::Node *_clip_text_group = nullptr; +}; + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +#endif // HAVE_POPPLER + +#endif // SEEN_EXTENSION_INTERNAL_PDFINPUT_SVGBUILDER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : |