| //======================================================================== |
| // |
| // QPainterOutputDev.cc |
| // |
| // Copyright 2003 Glyph & Cog, LLC |
| // |
| //======================================================================== |
| |
| //======================================================================== |
| // |
| // 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 Brad Hards <bradh@frogmouth.net> |
| // Copyright (C) 2005-2009, 2011, 2012, 2014, 2015, 2018, 2019, 2021, 2022 Albert Astals Cid <aacid@kde.org> |
| // Copyright (C) 2008, 2010 Pino Toscano <pino@kde.org> |
| // Copyright (C) 2009, 2011 Carlos Garcia Campos <carlosgc@gnome.org> |
| // Copyright (C) 2009 Petr Gajdos <pgajdos@novell.com> |
| // Copyright (C) 2010 Matthias Fauconneau <matthias.fauconneau@gmail.com> |
| // Copyright (C) 2011 Andreas Hartmetz <ahartmetz@gmail.com> |
| // Copyright (C) 2013 Thomas Freitag <Thomas.Freitag@alfa.de> |
| // Copyright (C) 2013 Dominik Haumann <dhaumann@kde.org> |
| // Copyright (C) 2013 Mihai Niculescu <q.quark@gmail.com> |
| // Copyright (C) 2017, 2018, 2020-2022 Oliver Sander <oliver.sander@tu-dresden.de> |
| // Copyright (C) 2017, 2022 Adrian Johnson <ajohnson@redneon.com> |
| // Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, <info@kdab.com>. Work sponsored by the LiMux project of the city of Munich |
| // Copyright (C) 2018 Adam Reichold <adam.reichold@t-online.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 |
| // |
| //======================================================================== |
| |
| #include <config.h> |
| |
| #include <cstring> |
| #include <cmath> |
| |
| #include <array> |
| |
| #include "goo/ft_utils.h" |
| #include "goo/gfile.h" |
| #include "GlobalParams.h" |
| #include "Error.h" |
| #include "Object.h" |
| #include "GfxState.h" |
| #include "GfxFont.h" |
| #include "Link.h" |
| #include "FontEncodingTables.h" |
| #include <fofi/FoFiTrueType.h> |
| #include <fofi/FoFiType1C.h> |
| #include "QPainterOutputDev.h" |
| #include "Page.h" |
| #include "Gfx.h" |
| #include "PDFDoc.h" |
| |
| #include <QtCore/QtDebug> |
| #include <QRawFont> |
| #include <QGlyphRun> |
| #include <QtGui/QPainterPath> |
| #include <QPicture> |
| |
| class QPainterOutputDevType3Font |
| { |
| public: |
| QPainterOutputDevType3Font(PDFDoc *doc, const std::shared_ptr<Gfx8BitFont> &font); |
| |
| const QPicture &getGlyph(int gid) const; |
| |
| private: |
| PDFDoc *m_doc; |
| std::shared_ptr<Gfx8BitFont> m_font; |
| |
| mutable std::vector<std::unique_ptr<QPicture>> glyphs; |
| |
| public: |
| std::vector<int> codeToGID; |
| }; |
| |
| QPainterOutputDevType3Font::QPainterOutputDevType3Font(PDFDoc *doc, const std::shared_ptr<Gfx8BitFont> &font) : m_doc(doc), m_font(font) |
| { |
| char *name; |
| const Dict *charProcs = font->getCharProcs(); |
| |
| // Storage for the rendered glyphs |
| glyphs.resize(charProcs->getLength()); |
| |
| // Compute the code-to-GID map |
| char **enc = font->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; |
| } |
| } |
| } |
| } |
| } |
| |
| const QPicture &QPainterOutputDevType3Font::getGlyph(int gid) const |
| { |
| if (!glyphs[gid]) { |
| |
| // Glyph has not been rendered before: render it now |
| |
| // Smallest box that contains all the glyphs from this font |
| const double *fontBBox = m_font->getFontBBox(); |
| PDFRectangle box(fontBBox[0], fontBBox[1], fontBBox[2], fontBBox[3]); |
| |
| Dict *resDict = m_font->getResources(); |
| |
| QPainter glyphPainter; |
| glyphs[gid] = std::make_unique<QPicture>(); |
| glyphPainter.begin(glyphs[gid].get()); |
| auto output_dev = std::make_unique<QPainterOutputDev>(&glyphPainter); |
| |
| auto gfx = std::make_unique<Gfx>(m_doc, output_dev.get(), resDict, |
| &box, // pagebox |
| nullptr // cropBox |
| ); |
| |
| output_dev->startDoc(m_doc); |
| |
| output_dev->startPage(1, gfx->getState(), gfx->getXRef()); |
| |
| const Dict *charProcs = m_font->getCharProcs(); |
| Object charProc = charProcs->getVal(gid); |
| gfx->display(&charProc); |
| |
| glyphPainter.end(); |
| } |
| |
| return *glyphs[gid]; |
| } |
| |
| //------------------------------------------------------------------------ |
| // QPainterOutputDev |
| //------------------------------------------------------------------------ |
| |
| QPainterOutputDev::QPainterOutputDev(QPainter *painter) : m_lastTransparencyGroupPicture(nullptr), m_hintingPreference(QFont::PreferDefaultHinting) |
| { |
| m_painter.push(painter); |
| m_currentBrush = QBrush(Qt::SolidPattern); |
| |
| auto error = FT_Init_FreeType(&m_ftLibrary); |
| if (error) { |
| qCritical() << "An error occurred will initializing the FreeType library"; |
| } |
| |
| // as of FT 2.1.8, CID fonts are indexed by CID instead of GID |
| FT_Int major, minor, patch; |
| FT_Library_Version(m_ftLibrary, &major, &minor, &patch); |
| m_useCIDs = major > 2 || (major == 2 && (minor > 1 || (minor == 1 && patch > 7))); |
| } |
| |
| QPainterOutputDev::~QPainterOutputDev() |
| { |
| for (auto &codeToGID : m_codeToGIDCache) { |
| gfree(const_cast<int *>(codeToGID.second)); |
| } |
| |
| FT_Done_FreeType(m_ftLibrary); |
| } |
| |
| void QPainterOutputDev::startDoc(PDFDoc *doc) |
| { |
| xref = doc->getXRef(); |
| m_doc = doc; |
| |
| for (auto &codeToGID : m_codeToGIDCache) { |
| gfree(const_cast<int *>(codeToGID.second)); |
| } |
| m_codeToGIDCache.clear(); |
| } |
| |
| void QPainterOutputDev::startPage(int pageNum, GfxState *state, XRef *) { } |
| |
| void QPainterOutputDev::endPage() { } |
| |
| void QPainterOutputDev::saveState(GfxState *state) |
| { |
| m_currentPenStack.push(m_currentPen); |
| m_currentBrushStack.push(m_currentBrush); |
| m_rawFontStack.push(m_rawFont); |
| m_type3FontStack.push(m_currentType3Font); |
| m_codeToGIDStack.push(m_codeToGID); |
| |
| m_painter.top()->save(); |
| } |
| |
| void QPainterOutputDev::restoreState(GfxState *state) |
| { |
| m_painter.top()->restore(); |
| |
| m_codeToGID = m_codeToGIDStack.top(); |
| m_codeToGIDStack.pop(); |
| m_rawFont = m_rawFontStack.top(); |
| m_rawFontStack.pop(); |
| m_currentType3Font = m_type3FontStack.top(); |
| m_type3FontStack.pop(); |
| m_currentBrush = m_currentBrushStack.top(); |
| m_currentBrushStack.pop(); |
| m_currentPen = m_currentPenStack.top(); |
| m_currentPenStack.pop(); |
| } |
| |
| void QPainterOutputDev::updateAll(GfxState *state) |
| { |
| OutputDev::updateAll(state); |
| m_needFontUpdate = true; |
| } |
| |
| // Set CTM (Current Transformation Matrix) to a fixed matrix |
| void QPainterOutputDev::setDefaultCTM(const double *ctm) |
| { |
| m_painter.top()->setTransform(QTransform(ctm[0], ctm[1], ctm[2], ctm[3], ctm[4], ctm[5])); |
| } |
| |
| // Update the CTM (Current Transformation Matrix), i.e., compose the old |
| // CTM with a new matrix. |
| void QPainterOutputDev::updateCTM(GfxState *state, double m11, double m12, double m21, double m22, double m31, double m32) |
| { |
| updateLineDash(state); |
| updateLineJoin(state); |
| updateLineCap(state); |
| updateLineWidth(state); |
| |
| QTransform update(m11, m12, m21, m22, m31, m32); |
| |
| // We could also set (rather than update) the painter transformation to state->getCMT(); |
| m_painter.top()->setTransform(update, true); |
| } |
| |
| void QPainterOutputDev::updateLineDash(GfxState *state) |
| { |
| double dashStart; |
| const std::vector<double> &dashPattern = state->getLineDash(&dashStart); |
| |
| // Special handling for zero-length patterns, i.e., solid lines. |
| // Simply calling QPen::setDashPattern with an empty pattern does *not* |
| // result in a solid line. Rather, the current pattern is unchanged. |
| // See the implementation of the setDashPattern method in the file qpen.cpp. |
| if (dashPattern.empty()) { |
| m_currentPen.setStyle(Qt::SolidLine); |
| m_painter.top()->setPen(m_currentPen); |
| return; |
| } |
| |
| QVector<qreal> pattern(dashPattern.size()); |
| double scaling = state->getLineWidth(); |
| |
| // Negative line widths are not allowed, width 0 counts as 'one pixel width'. |
| if (scaling <= 0) { |
| scaling = 1.0; |
| } |
| |
| for (std::vector<double>::size_type i = 0; i < dashPattern.size(); ++i) { |
| // pdf measures the dash pattern in dots, but Qt uses the |
| // line width as the unit. |
| pattern[i] = dashPattern[i] / scaling; |
| } |
| m_currentPen.setDashPattern(pattern); |
| m_currentPen.setDashOffset(dashStart); |
| m_painter.top()->setPen(m_currentPen); |
| } |
| |
| void QPainterOutputDev::updateFlatness(GfxState *state) |
| { |
| // qDebug() << "updateFlatness"; |
| } |
| |
| void QPainterOutputDev::updateLineJoin(GfxState *state) |
| { |
| switch (state->getLineJoin()) { |
| case 0: |
| // The correct style here is Qt::SvgMiterJoin, *not* Qt::MiterJoin. |
| // The two differ in what to do if the miter limit is exceeded. |
| // See https://bugs.freedesktop.org/show_bug.cgi?id=102356 |
| m_currentPen.setJoinStyle(Qt::SvgMiterJoin); |
| break; |
| case 1: |
| m_currentPen.setJoinStyle(Qt::RoundJoin); |
| break; |
| case 2: |
| m_currentPen.setJoinStyle(Qt::BevelJoin); |
| break; |
| } |
| m_painter.top()->setPen(m_currentPen); |
| } |
| |
| void QPainterOutputDev::updateLineCap(GfxState *state) |
| { |
| switch (state->getLineCap()) { |
| case 0: |
| m_currentPen.setCapStyle(Qt::FlatCap); |
| break; |
| case 1: |
| m_currentPen.setCapStyle(Qt::RoundCap); |
| break; |
| case 2: |
| m_currentPen.setCapStyle(Qt::SquareCap); |
| break; |
| } |
| m_painter.top()->setPen(m_currentPen); |
| } |
| |
| void QPainterOutputDev::updateMiterLimit(GfxState *state) |
| { |
| m_currentPen.setMiterLimit(state->getMiterLimit()); |
| m_painter.top()->setPen(m_currentPen); |
| } |
| |
| void QPainterOutputDev::updateLineWidth(GfxState *state) |
| { |
| m_currentPen.setWidthF(state->getLineWidth()); |
| m_painter.top()->setPen(m_currentPen); |
| // The updateLineDash method needs to know the line width, but it is sometimes |
| // called before the updateLineWidth method. To make sure that the last call |
| // to updateLineDash before a drawing operation is always with the correct line |
| // width, we call it here, right after a change to the line width. |
| updateLineDash(state); |
| } |
| |
| void QPainterOutputDev::updateFillColor(GfxState *state) |
| { |
| GfxRGB rgb; |
| QColor brushColour = m_currentBrush.color(); |
| state->getFillRGB(&rgb); |
| brushColour.setRgbF(colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b), brushColour.alphaF()); |
| m_currentBrush.setColor(brushColour); |
| } |
| |
| void QPainterOutputDev::updateStrokeColor(GfxState *state) |
| { |
| GfxRGB rgb; |
| QColor penColour = m_currentPen.color(); |
| state->getStrokeRGB(&rgb); |
| penColour.setRgbF(colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b), penColour.alphaF()); |
| m_currentPen.setColor(penColour); |
| m_painter.top()->setPen(m_currentPen); |
| } |
| |
| void QPainterOutputDev::updateBlendMode(GfxState *state) |
| { |
| GfxBlendMode blendMode = state->getBlendMode(); |
| |
| // missing composition modes in QPainter: |
| // - CompositionMode_Hue |
| // - CompositionMode_Color |
| // - CompositionMode_Luminosity |
| // - CompositionMode_Saturation |
| |
| switch (blendMode) { |
| case gfxBlendMultiply: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_Multiply); |
| break; |
| case gfxBlendScreen: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_Screen); |
| break; |
| case gfxBlendDarken: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_Darken); |
| break; |
| case gfxBlendLighten: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_Lighten); |
| break; |
| case gfxBlendColorDodge: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_ColorDodge); |
| break; |
| case gfxBlendColorBurn: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_ColorBurn); |
| break; |
| case gfxBlendHardLight: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_HardLight); |
| break; |
| case gfxBlendSoftLight: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_SoftLight); |
| break; |
| case gfxBlendDifference: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_Difference); |
| break; |
| case gfxBlendExclusion: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_Exclusion); |
| break; |
| case gfxBlendColor: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_Plus); |
| break; |
| default: |
| qDebug() << "Unsupported blend mode, falling back to CompositionMode_SourceOver"; |
| [[fallthrough]]; |
| case gfxBlendNormal: |
| m_painter.top()->setCompositionMode(QPainter::CompositionMode_SourceOver); |
| break; |
| } |
| } |
| |
| void QPainterOutputDev::updateFillOpacity(GfxState *state) |
| { |
| QColor brushColour = m_currentBrush.color(); |
| brushColour.setAlphaF(state->getFillOpacity()); |
| m_currentBrush.setColor(brushColour); |
| } |
| |
| void QPainterOutputDev::updateStrokeOpacity(GfxState *state) |
| { |
| QColor penColour = m_currentPen.color(); |
| penColour.setAlphaF(state->getStrokeOpacity()); |
| m_currentPen.setColor(penColour); |
| m_painter.top()->setPen(m_currentPen); |
| } |
| |
| void QPainterOutputDev::updateFont(GfxState *state) |
| { |
| const std::shared_ptr<GfxFont> &gfxFont = state->getFont(); |
| if (!gfxFont) { |
| return; |
| } |
| |
| // The key to look in the font caches |
| QPainterFontID fontID = { *gfxFont->getID(), state->getFontSize() }; |
| |
| // Current font is a type3 font |
| if (gfxFont->getType() == fontType3) { |
| auto cacheEntry = m_type3FontCache.find(fontID); |
| |
| if (cacheEntry != m_type3FontCache.end()) { |
| |
| // Take the font from the cache |
| m_currentType3Font = cacheEntry->second.get(); |
| |
| } else { |
| |
| m_currentType3Font = new QPainterOutputDevType3Font(m_doc, std::static_pointer_cast<Gfx8BitFont>(gfxFont)); |
| m_type3FontCache.insert(std::make_pair(fontID, std::unique_ptr<QPainterOutputDevType3Font>(m_currentType3Font))); |
| } |
| |
| return; |
| } |
| |
| // Non-type3: is the font in the cache? |
| auto cacheEntry = m_rawFontCache.find(fontID); |
| |
| if (cacheEntry != m_rawFontCache.end()) { |
| |
| // Take the font from the cache |
| m_rawFont = cacheEntry->second.get(); |
| |
| } else { |
| |
| // New font: load it into the cache |
| float fontSize = state->getFontSize(); |
| |
| std::optional<GfxFontLoc> fontLoc = gfxFont->locateFont(xref, nullptr); |
| |
| if (fontLoc) { |
| // load the font from respective location |
| switch (fontLoc->locType) { |
| case gfxFontLocEmbedded: { // if there is an embedded font, read it to memory |
| const std::optional<std::vector<unsigned char>> fontData = gfxFont->readEmbFontFile(xref); |
| |
| // fontData gets copied in the QByteArray constructor |
| m_rawFont = new QRawFont(QByteArray(fontData ? (const char *)fontData->data() : nullptr, fontData ? fontData->size() : 0), fontSize, m_hintingPreference); |
| m_rawFontCache.insert(std::make_pair(fontID, std::unique_ptr<QRawFont>(m_rawFont))); |
| |
| break; |
| } |
| case gfxFontLocExternal: { // font is in an external font file |
| QString fontFile(fontLoc->path.c_str()); |
| m_rawFont = new QRawFont(fontFile, fontSize, m_hintingPreference); |
| m_rawFontCache.insert(std::make_pair(fontID, std::unique_ptr<QRawFont>(m_rawFont))); |
| break; |
| } |
| case gfxFontLocResident: { // font resides in a PS printer |
| qDebug() << "Resident Font Resident not implemented yet!"; |
| |
| break; |
| } |
| } // end switch |
| |
| } else { |
| qDebug() << "Font location not found!"; |
| return; |
| } |
| } |
| |
| if (!m_rawFont->isValid()) { |
| qDebug() << "RawFont is not valid"; |
| } |
| |
| // ***************************************************************************** |
| // We have now successfully loaded the font into a QRawFont object. This |
| // allows us to draw all the glyphs in the font. However, what is missing is |
| // the charcode-to-glyph-index mapping. Apparently, Qt does not provide this |
| // information at all. Therefore, we need to figure it ourselves, using |
| // FoFi and FreeType. |
| // ***************************************************************************** |
| |
| m_needFontUpdate = false; |
| |
| GfxFontType fontType = gfxFont->getType(); |
| |
| // Default: no codeToGID table |
| m_codeToGID = nullptr; |
| |
| // check the font file cache |
| Ref id = *gfxFont->getID(); |
| |
| auto codeToGIDIt = m_codeToGIDCache.find(id); |
| |
| if (codeToGIDIt != m_codeToGIDCache.end()) { |
| |
| m_codeToGID = codeToGIDIt->second; |
| |
| } else { |
| |
| std::optional<std::vector<unsigned char>> fontBuffer; |
| |
| std::optional<GfxFontLoc> fontLoc = gfxFont->locateFont(xref, nullptr); |
| if (!fontLoc) { |
| error(errSyntaxError, -1, "Couldn't find a font for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)"); |
| return; |
| } |
| |
| // embedded font |
| if (fontLoc->locType == gfxFontLocEmbedded) { |
| // if there is an embedded font, read it to memory |
| fontBuffer = gfxFont->readEmbFontFile(xref); |
| if (!fontBuffer) { |
| return; |
| } |
| |
| // external font |
| } else { // gfxFontLocExternal |
| // Hmm, fontType has already been set to gfxFont->getType() above. |
| // Can it really assume a different value here? |
| fontType = fontLoc->fontType; |
| } |
| |
| switch (fontType) { |
| case fontType1: |
| case fontType1C: |
| case fontType1COT: { |
| // Load the font face using FreeType |
| const int faceIndex = 0; // We always load the zero-th face from a font |
| FT_Face freeTypeFace; |
| |
| if (fontLoc->locType != gfxFontLocEmbedded) { |
| if (ft_new_face_from_file(m_ftLibrary, fontLoc->path.c_str(), faceIndex, &freeTypeFace)) { |
| error(errSyntaxError, -1, "Couldn't create a FreeType face for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)"); |
| return; |
| } |
| } else { |
| if (FT_New_Memory_Face(m_ftLibrary, (const FT_Byte *)fontBuffer->data(), fontBuffer->size(), faceIndex, &freeTypeFace)) { |
| error(errSyntaxError, -1, "Couldn't create a FreeType face for '{0:s}'", gfxFont->getName() ? gfxFont->getName()->c_str() : "(unnamed)"); |
| return; |
| } |
| } |
| |
| const char *name; |
| |
| int *codeToGID = (int *)gmallocn(256, sizeof(int)); |
| for (int i = 0; i < 256; ++i) { |
| codeToGID[i] = 0; |
| if ((name = ((const char **)((Gfx8BitFont *)gfxFont.get())->getEncoding())[i])) { |
| codeToGID[i] = (int)FT_Get_Name_Index(freeTypeFace, (char *)name); |
| if (codeToGID[i] == 0) { |
| name = GfxFont::getAlternateName(name); |
| if (name) { |
| codeToGID[i] = FT_Get_Name_Index(freeTypeFace, (char *)name); |
| } |
| } |
| } |
| } |
| |
| FT_Done_Face(freeTypeFace); |
| |
| m_codeToGIDCache[id] = codeToGID; |
| |
| break; |
| } |
| case fontTrueType: |
| case fontTrueTypeOT: { |
| auto ff = (fontLoc->locType != gfxFontLocEmbedded) ? FoFiTrueType::load(fontLoc->path.c_str()) : FoFiTrueType::make(fontBuffer->data(), fontBuffer->size()); |
| |
| m_codeToGIDCache[id] = (ff) ? ((Gfx8BitFont *)gfxFont.get())->getCodeToGIDMap(ff.get()) : nullptr; |
| |
| break; |
| } |
| case fontCIDType0: |
| case fontCIDType0C: { |
| int *cidToGIDMap = nullptr; |
| int nCIDs = 0; |
| |
| // check for a CFF font |
| if (!m_useCIDs) { |
| auto ff = (fontLoc->locType != gfxFontLocEmbedded) ? std::unique_ptr<FoFiType1C>(FoFiType1C::load(fontLoc->path.c_str())) : std::unique_ptr<FoFiType1C>(FoFiType1C::make(fontBuffer->data(), fontBuffer->size())); |
| |
| cidToGIDMap = (ff) ? ff->getCIDToGIDMap(&nCIDs) : nullptr; |
| } |
| |
| m_codeToGIDCache[id] = cidToGIDMap; |
| |
| break; |
| } |
| case fontCIDType0COT: { |
| int *codeToGID = nullptr; |
| |
| if (((GfxCIDFont *)gfxFont.get())->getCIDToGID()) { |
| int codeToGIDLen = ((GfxCIDFont *)gfxFont.get())->getCIDToGIDLen(); |
| codeToGID = (int *)gmallocn(codeToGIDLen, sizeof(int)); |
| memcpy(codeToGID, ((GfxCIDFont *)gfxFont.get())->getCIDToGID(), codeToGIDLen * sizeof(int)); |
| } |
| |
| int *cidToGIDMap = nullptr; |
| int nCIDs = 0; |
| |
| if (!codeToGID && !m_useCIDs) { |
| auto ff = (fontLoc->locType != gfxFontLocEmbedded) ? FoFiTrueType::load(fontLoc->path.c_str()) : FoFiTrueType::make(fontBuffer->data(), fontBuffer->size()); |
| |
| if (ff && ff->isOpenTypeCFF()) { |
| cidToGIDMap = ff->getCIDToGIDMap(&nCIDs); |
| } |
| } |
| |
| m_codeToGIDCache[id] = codeToGID ? codeToGID : cidToGIDMap; |
| |
| break; |
| } |
| case fontCIDType2: |
| case fontCIDType2OT: { |
| int *codeToGID = nullptr; |
| int codeToGIDLen = 0; |
| if (((GfxCIDFont *)gfxFont.get())->getCIDToGID()) { |
| codeToGIDLen = ((GfxCIDFont *)gfxFont.get())->getCIDToGIDLen(); |
| if (codeToGIDLen) { |
| codeToGID = (int *)gmallocn(codeToGIDLen, sizeof(int)); |
| memcpy(codeToGID, ((GfxCIDFont *)gfxFont.get())->getCIDToGID(), codeToGIDLen * sizeof(int)); |
| } |
| } else { |
| auto ff = (fontLoc->locType != gfxFontLocEmbedded) ? FoFiTrueType::load(fontLoc->path.c_str()) : FoFiTrueType::make(fontBuffer->data(), fontBuffer->size()); |
| if (!ff) { |
| return; |
| } |
| codeToGID = ((GfxCIDFont *)gfxFont.get())->getCodeToGIDMap(ff.get(), &codeToGIDLen); |
| } |
| |
| m_codeToGIDCache[id] = codeToGID; |
| |
| break; |
| } |
| default: |
| // this shouldn't happen |
| return; |
| } |
| |
| m_codeToGID = m_codeToGIDCache[id]; |
| } |
| } |
| |
| static QPainterPath convertPath(GfxState *state, const GfxPath *path, Qt::FillRule fillRule) |
| { |
| int i, j; |
| |
| QPainterPath qPath; |
| qPath.setFillRule(fillRule); |
| for (i = 0; i < path->getNumSubpaths(); ++i) { |
| const GfxSubpath *subpath = path->getSubpath(i); |
| if (subpath->getNumPoints() > 0) { |
| qPath.moveTo(subpath->getX(0), subpath->getY(0)); |
| j = 1; |
| while (j < subpath->getNumPoints()) { |
| if (subpath->getCurve(j)) { |
| qPath.cubicTo(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 { |
| qPath.lineTo(subpath->getX(j), subpath->getY(j)); |
| ++j; |
| } |
| } |
| if (subpath->isClosed()) { |
| qPath.closeSubpath(); |
| } |
| } |
| } |
| return qPath; |
| } |
| |
| void QPainterOutputDev::stroke(GfxState *state) |
| { |
| m_painter.top()->strokePath(convertPath(state, state->getPath(), Qt::OddEvenFill), m_currentPen); |
| } |
| |
| void QPainterOutputDev::fill(GfxState *state) |
| { |
| m_painter.top()->fillPath(convertPath(state, state->getPath(), Qt::WindingFill), m_currentBrush); |
| } |
| |
| void QPainterOutputDev::eoFill(GfxState *state) |
| { |
| m_painter.top()->fillPath(convertPath(state, state->getPath(), Qt::OddEvenFill), m_currentBrush); |
| } |
| |
| bool QPainterOutputDev::axialShadedFill(GfxState *state, GfxAxialShading *shading, double tMin, double tMax) |
| { |
| double x0, y0, x1, y1; |
| shading->getCoords(&x0, &y0, &x1, &y1); |
| |
| // get the clip region bbox |
| double xMin, yMin, xMax, yMax; |
| state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax); |
| |
| // get the function domain |
| double t0 = shading->getDomain0(); |
| double t1 = shading->getDomain1(); |
| |
| // Max number of splits along the t axis |
| constexpr int maxSplits = 256; |
| |
| // Max delta allowed in any color component |
| const double colorDelta = (dblToCol(1 / 256.0)); |
| |
| // Number of color space components |
| auto nComps = shading->getColorSpace()->getNComps(); |
| // If the clipping region is a stroke, then the current operation counts as a stroke |
| // rather than as a fill, and the opacity has to be set accordingly. |
| // See https://gitlab.freedesktop.org/poppler/poppler/-/issues/178 |
| auto opacity = (state->getStrokePattern()) ? state->getStrokeOpacity() : state->getFillOpacity(); |
| |
| // Helper function to test two color objects for 'almost-equality' |
| auto isSameGfxColor = [&nComps, &colorDelta](const GfxColor &colorA, const GfxColor &colorB) { |
| for (int k = 0; k < nComps; ++k) { |
| if (abs(colorA.c[k] - colorB.c[k]) > colorDelta) { |
| return false; |
| } |
| } |
| return true; |
| }; |
| |
| // Helper function: project a number into an interval |
| // With C++17 this is part of the standard library |
| auto clamp = [](double v, double lo, double hi) { return std::min(std::max(v, lo), hi); }; |
| |
| // ta stores all parameter values where we evaluate the input shading function. |
| // In between, QLinearGradient will interpolate linearly. |
| // We set up the array with three values. |
| std::array<double, maxSplits + 1> ta; |
| ta[0] = tMin; |
| std::array<int, maxSplits + 1> next; |
| next[0] = maxSplits / 2; |
| ta[maxSplits / 2] = 0.5 * (tMin + tMax); |
| next[maxSplits / 2] = maxSplits; |
| ta[maxSplits] = tMax; |
| |
| // compute the color at t = tMin |
| double tt = clamp(t0 + (t1 - t0) * tMin, t0, t1); |
| |
| GfxColor color0, color1; |
| shading->getColor(tt, &color0); |
| |
| // Construct a gradient object and set its color at one parameter end |
| QLinearGradient gradient(QPointF(x0 + tMin * (x1 - x0), y0 + tMin * (y1 - y0)), QPointF(x0 + tMax * (x1 - x0), y0 + tMax * (y1 - y0))); |
| |
| GfxRGB rgb; |
| shading->getColorSpace()->getRGB(&color0, &rgb); |
| QColor qColor(colToByte(rgb.r), colToByte(rgb.g), colToByte(rgb.b), dblToByte(opacity)); |
| gradient.setColorAt(0, qColor); |
| |
| // Look for more relevant parameter values by bisection |
| int i = 0; |
| while (i < maxSplits) { |
| |
| int j = next[i]; |
| while (j > i + 1) { |
| |
| // Next parameter value to try |
| tt = clamp(t0 + (t1 - t0) * ta[j], t0, t1); |
| shading->getColor(tt, &color1); |
| |
| // j is a good next color stop if the input shading can be approximated well |
| // on the interval (ta[i], ta[j]) by a linear interpolation. |
| // We test this by comparing the real color in the middle between ta[i] and ta[j] |
| // with the linear interpolant there. |
| auto midPoint = 0.5 * (ta[i] + ta[j]); |
| GfxColor colorAtMidPoint; |
| shading->getColor(midPoint, &colorAtMidPoint); |
| |
| GfxColor linearlyInterpolatedColor; |
| for (int ii = 0; ii < nComps; ii++) { |
| linearlyInterpolatedColor.c[ii] = 0.5 * (color0.c[ii] + color1.c[ii]); |
| } |
| |
| // If the two colors are equal, ta[j] is a good place for the next color stop; take it! |
| if (isSameGfxColor(colorAtMidPoint, linearlyInterpolatedColor)) { |
| break; |
| } |
| |
| // Otherwise: bisect further |
| int k = (i + j) / 2; |
| ta[k] = midPoint; |
| next[i] = k; |
| next[k] = j; |
| j = k; |
| } |
| |
| // set the color |
| shading->getColorSpace()->getRGB(&color1, &rgb); |
| qColor.setRgb(colToByte(rgb.r), colToByte(rgb.g), colToByte(rgb.b), dblToByte(opacity)); |
| gradient.setColorAt((ta[j] - tMin) / (tMax - tMin), qColor); |
| |
| // Move to the next parameter region |
| color0 = color1; |
| i = next[i]; |
| } |
| |
| state->moveTo(xMin, yMin); |
| state->lineTo(xMin, yMax); |
| state->lineTo(xMax, yMax); |
| state->lineTo(xMax, yMin); |
| state->closePath(); |
| |
| // Actually paint the shaded region |
| QBrush newBrush(gradient); |
| m_painter.top()->fillPath(convertPath(state, state->getPath(), Qt::WindingFill), newBrush); |
| |
| state->clearPath(); |
| |
| // True means: The shaded region has been painted |
| return true; |
| } |
| |
| void QPainterOutputDev::clip(GfxState *state) |
| { |
| m_painter.top()->setClipPath(convertPath(state, state->getPath(), Qt::WindingFill), Qt::IntersectClip); |
| } |
| |
| void QPainterOutputDev::eoClip(GfxState *state) |
| { |
| m_painter.top()->setClipPath(convertPath(state, state->getPath(), Qt::OddEvenFill), Qt::IntersectClip); |
| } |
| |
| void QPainterOutputDev::clipToStrokePath(GfxState *state) |
| { |
| QPainterPath clipPath = convertPath(state, state->getPath(), Qt::WindingFill); |
| |
| // Get the outline of 'clipPath' as a separate path |
| QPainterPathStroker stroker; |
| stroker.setWidth(state->getLineWidth()); |
| stroker.setCapStyle(m_currentPen.capStyle()); |
| stroker.setJoinStyle(m_currentPen.joinStyle()); |
| stroker.setMiterLimit(state->getMiterLimit()); |
| stroker.setDashPattern(m_currentPen.dashPattern()); |
| stroker.setDashOffset(m_currentPen.dashOffset()); |
| QPainterPath clipPathOutline = stroker.createStroke(clipPath); |
| |
| // The interior of the outline is the desired clipping region |
| m_painter.top()->setClipPath(clipPathOutline, Qt::IntersectClip); |
| } |
| |
| void QPainterOutputDev::drawChar(GfxState *state, double x, double y, double dx, double dy, double originX, double originY, CharCode code, int nBytes, const Unicode *u, int uLen) |
| { |
| |
| // First handle type3 fonts |
| const std::shared_ptr<GfxFont> &gfxFont = state->getFont(); |
| |
| GfxFontType fontType = gfxFont->getType(); |
| if (fontType == fontType3) { |
| |
| ///////////////////////////////////////////////////////////////////// |
| // Draw the QPicture that contains the glyph onto the page |
| ///////////////////////////////////////////////////////////////////// |
| |
| // Store the QPainter state; we need to modify it temporarily |
| m_painter.top()->save(); |
| |
| // Make the glyph position the coordinate origin -- that's our center of scaling |
| m_painter.top()->translate(QPointF(x - originX, y - originY)); |
| |
| const double *mat = gfxFont->getFontMatrix(); |
| QTransform fontMatrix(mat[0], mat[1], mat[2], mat[3], mat[4], mat[5]); |
| |
| // Scale with the font size |
| fontMatrix.scale(state->getFontSize(), state->getFontSize()); |
| m_painter.top()->setTransform(fontMatrix, true); |
| |
| // Apply the text matrix on top |
| const double *textMat = state->getTextMat(); |
| |
| QTransform textTransform(textMat[0] * state->getHorizScaling(), textMat[1] * state->getHorizScaling(), textMat[2], textMat[3], 0, 0); |
| |
| m_painter.top()->setTransform(textTransform, true); |
| |
| // Actually draw the glyph |
| int gid = m_currentType3Font->codeToGID[code]; |
| m_painter.top()->drawPicture(QPointF(0, 0), m_currentType3Font->getGlyph(gid)); |
| |
| // Restore transformation |
| m_painter.top()->restore(); |
| |
| return; |
| } |
| |
| // check for invisible text -- this is used by Acrobat Capture |
| int render = state->getRender(); |
| if (render == 3 || !m_rawFont) { |
| qDebug() << "Invisible text found!"; |
| return; |
| } |
| |
| if (!(render & 1)) { |
| quint32 glyphIndex = (m_codeToGID) ? m_codeToGID[code] : code; |
| QPointF glyphPosition = QPointF(x - originX, y - originY); |
| |
| // QGlyphRun objects can hold an entire sequence of glyphs, and it would possibly |
| // be more efficient to simply note the glyph and glyph position here and then |
| // draw several glyphs at once in the endString method. What keeps us from doing |
| // that is the transformation below: each glyph needs to be drawn upside down, |
| // i.e., reflected at its own baseline. Since we have no guarantee that this |
| // baseline is the same for all glyphs in a string we have to do it one by one. |
| QGlyphRun glyphRun; |
| glyphRun.setRawData(&glyphIndex, &glyphPosition, 1); |
| glyphRun.setRawFont(*m_rawFont); |
| |
| // Store the QPainter state; we need to modify it temporarily |
| m_painter.top()->save(); |
| |
| // Apply the text matrix to the glyph. The glyph is not scaled by the font size, |
| // because the font in m_rawFont already has the correct size. |
| // Additionally, the CTM is upside down, i.e., it contains a negative Y-scaling |
| // entry. Therefore, Qt will paint the glyphs upside down. We need to temporarily |
| // reflect the page at glyphPosition.y(). |
| |
| // Make the glyph position the coordinate origin -- that's our center of scaling |
| const double *textMat = state->getTextMat(); |
| |
| m_painter.top()->translate(QPointF(glyphPosition.x(), glyphPosition.y())); |
| |
| QTransform textTransform(textMat[0] * state->getHorizScaling(), textMat[1] * state->getHorizScaling(), |
| -textMat[2], // reflect at the horizontal axis, |
| -textMat[3], // because CTM is upside-down. |
| 0, 0); |
| |
| m_painter.top()->setTransform(textTransform, true); |
| |
| // We are painting a filled glyph here. But QPainter uses the pen to draw even filled text, |
| // not the brush. (see, e.g., http://doc.qt.io/qt-5/qpainter.html#setPen ) |
| // Therefore we have to temporarily overwrite the pen color. |
| |
| // Since we are drawing a filled glyph, one would really expect to have m_currentBrush |
| // have the correct color. However, somehow state->getFillRGB can change without |
| // updateFillColor getting called. Then m_currentBrush may not contain the correct color. |
| GfxRGB rgb; |
| state->getFillRGB(&rgb); |
| QColor fontColor; |
| fontColor.setRgbF(colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b), state->getFillOpacity()); |
| m_painter.top()->setPen(fontColor); |
| |
| // Actually draw the glyph |
| m_painter.top()->drawGlyphRun(QPointF(-glyphPosition.x(), -glyphPosition.y()), glyphRun); |
| |
| // Restore transformation and pen color |
| m_painter.top()->restore(); |
| } |
| } |
| |
| void QPainterOutputDev::type3D0(GfxState *state, double wx, double wy) { } |
| |
| void QPainterOutputDev::type3D1(GfxState *state, double wx, double wy, double llx, double lly, double urx, double ury) { } |
| |
| void QPainterOutputDev::endTextObject(GfxState *state) { } |
| |
| void QPainterOutputDev::drawImageMask(GfxState *state, Object *ref, Stream *str, int width, int height, bool invert, bool interpolate, bool inlineImg) |
| { |
| auto imgStr = std::make_unique<ImageStream>(str, width, |
| 1, // numPixelComps |
| 1 // getBits |
| ); |
| imgStr->reset(); |
| |
| // TODO: Would using QImage::Format_Mono be more efficient here? |
| QImage image(width, height, QImage::Format_ARGB32); |
| unsigned int *data = reinterpret_cast<unsigned int *>(image.bits()); |
| int stride = image.bytesPerLine() / 4; |
| |
| QRgb fillColor = m_currentBrush.color().rgb(); |
| |
| for (int y = 0; y < height; y++) { |
| |
| unsigned char *pix = imgStr->getLine(); |
| |
| // Invert the vertical coordinate: y is increasing from top to bottom |
| // on the page, but y is increasing bottom to top in the picture. |
| unsigned int *dest = data + (height - 1 - y) * stride; |
| |
| for (int x = 0; x < width; x++) { |
| |
| bool opaque = ((bool)pix[x]) == invert; |
| dest[x] = (opaque) ? fillColor : 0; |
| } |
| } |
| |
| // At this point, the QPainter coordinate transformation (CTM) is such |
| // that QRect(0,0,1,1) is exactly the area of the image. |
| m_painter.top()->drawImage(QRect(0, 0, 1, 1), image); |
| imgStr->close(); |
| } |
| |
| // TODO: lots more work here. |
| void QPainterOutputDev::drawImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, const int *maskColors, bool inlineImg) |
| { |
| unsigned int *data; |
| unsigned int *line; |
| int x, y; |
| unsigned char *pix; |
| int i; |
| QImage image; |
| int stride; |
| |
| /* TODO: Do we want to cache these? */ |
| auto imgStr = std::make_unique<ImageStream>(str, width, colorMap->getNumPixelComps(), colorMap->getBits()); |
| imgStr->reset(); |
| |
| image = QImage(width, height, QImage::Format_ARGB32); |
| data = reinterpret_cast<unsigned int *>(image.bits()); |
| stride = image.bytesPerLine() / 4; |
| for (y = 0; y < height; y++) { |
| pix = imgStr->getLine(); |
| // Invert the vertical coordinate: y is increasing from top to bottom |
| // on the page, but y is increasing bottom to top in the picture. |
| line = data + (height - 1 - y) * stride; |
| colorMap->getRGBLine(pix, line, width); |
| |
| if (maskColors) { |
| for (x = 0; x < width; x++) { |
| for (i = 0; i < colorMap->getNumPixelComps(); ++i) { |
| if (pix[i] < maskColors[2 * i] * 255 || pix[i] > maskColors[2 * i + 1] * 255) { |
| *line = *line | 0xff000000; |
| break; |
| } |
| } |
| pix += colorMap->getNumPixelComps(); |
| line++; |
| } |
| } else { |
| for (x = 0; x < width; x++) { |
| *line = *line | 0xff000000; |
| line++; |
| } |
| } |
| } |
| |
| // At this point, the QPainter coordinate transformation (CTM) is such |
| // that QRect(0,0,1,1) is exactly the area of the image. |
| m_painter.top()->drawImage(QRect(0, 0, 1, 1), image); |
| } |
| |
| void QPainterOutputDev::drawSoftMaskedImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, GfxImageColorMap *maskColorMap, |
| bool maskInterpolate) |
| { |
| // Bail out if the image size doesn't match the mask size. I don't know |
| // what to do in this case. |
| if (width != maskWidth || height != maskHeight) { |
| qDebug() << "Soft mask size does not match image size!"; |
| drawImage(state, ref, str, width, height, colorMap, interpolate, nullptr, false); |
| return; |
| } |
| |
| // Bail out if the mask isn't a single channel. I don't know |
| // what to do in this case. |
| if (maskColorMap->getColorSpace()->getNComps() != 1) { |
| qDebug() << "Soft mask is not a single 8-bit channel!"; |
| drawImage(state, ref, str, width, height, colorMap, interpolate, nullptr, false); |
| return; |
| } |
| |
| /* TODO: Do we want to cache these? */ |
| auto imgStr = std::make_unique<ImageStream>(str, width, colorMap->getNumPixelComps(), colorMap->getBits()); |
| imgStr->reset(); |
| |
| auto maskImageStr = std::make_unique<ImageStream>(maskStr, maskWidth, maskColorMap->getNumPixelComps(), maskColorMap->getBits()); |
| maskImageStr->reset(); |
| |
| QImage image(width, height, QImage::Format_ARGB32); |
| unsigned int *data = reinterpret_cast<unsigned int *>(image.bits()); |
| int stride = image.bytesPerLine() / 4; |
| |
| std::vector<unsigned char> maskLine(maskWidth); |
| |
| for (int y = 0; y < height; y++) { |
| |
| unsigned char *pix = imgStr->getLine(); |
| unsigned char *maskPix = maskImageStr->getLine(); |
| |
| // Invert the vertical coordinate: y is increasing from top to bottom |
| // on the page, but y is increasing bottom to top in the picture. |
| unsigned int *line = data + (height - 1 - y) * stride; |
| colorMap->getRGBLine(pix, line, width); |
| |
| // Apply the mask values to the image alpha channel |
| maskColorMap->getGrayLine(maskPix, maskLine.data(), width); |
| for (int x = 0; x < width; x++) { |
| *line = *line | (maskLine[x] << 24); |
| line++; |
| } |
| } |
| |
| // At this point, the QPainter coordinate transformation (CTM) is such |
| // that QRect(0,0,1,1) is exactly the area of the image. |
| m_painter.top()->drawImage(QRect(0, 0, 1, 1), image); |
| } |
| |
| void QPainterOutputDev::beginTransparencyGroup(GfxState * /*state*/, const double * /*bbox*/, GfxColorSpace * /*blendingColorSpace*/, bool /*isolated*/, bool /*knockout*/, bool /*forSoftMask*/) |
| { |
| // The entire transparency group will be painted into a |
| // freshly created QPicture object. Since an existing painter |
| // cannot change its paint device, we need to construct a |
| // new QPainter object as well. |
| m_qpictures.push(new QPicture); |
| m_painter.push(new QPainter(m_qpictures.top())); |
| } |
| |
| void QPainterOutputDev::endTransparencyGroup(GfxState * /*state*/) |
| { |
| // Stop painting into the group |
| m_painter.top()->end(); |
| |
| // Kill the painter that has been used for the transparency group |
| delete (m_painter.top()); |
| m_painter.pop(); |
| |
| // Store the QPicture object that holds the result of the transparency group |
| // painting. It will be painted and deleted in the method paintTransparencyGroup. |
| if (m_lastTransparencyGroupPicture) { |
| qDebug() << "Found a transparency group that has not been painted"; |
| delete (m_lastTransparencyGroupPicture); |
| } |
| m_lastTransparencyGroupPicture = m_qpictures.top(); |
| m_qpictures.pop(); |
| } |
| |
| void QPainterOutputDev::paintTransparencyGroup(GfxState * /*state*/, const double * /*bbox*/) |
| { |
| // Actually draw the transparency group |
| m_painter.top()->drawPicture(0, 0, *m_lastTransparencyGroupPicture); |
| |
| // And delete it |
| delete (m_lastTransparencyGroupPicture); |
| m_lastTransparencyGroupPicture = nullptr; |
| } |