//======================================================================== // // 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 // Copyright (C) 2005-2009, 2011, 2012, 2014, 2015, 2018, 2019, 2021, 2022, 2024 Albert Astals Cid // Copyright (C) 2008, 2010 Pino Toscano // Copyright (C) 2009, 2011 Carlos Garcia Campos // Copyright (C) 2009 Petr Gajdos // Copyright (C) 2010 Matthias Fauconneau // Copyright (C) 2011 Andreas Hartmetz // Copyright (C) 2013 Thomas Freitag // Copyright (C) 2013 Dominik Haumann // Copyright (C) 2013 Mihai Niculescu // Copyright (C) 2017, 2018, 2020-2022 Oliver Sander // Copyright (C) 2017, 2022 Adrian Johnson // Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, . Work sponsored by the LiMux project of the city of Munich // Copyright (C) 2018 Adam Reichold // // 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 #include #include #include #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 #include #include "QPainterOutputDev.h" #include "Page.h" #include "Gfx.h" #include "PDFDoc.h" #include #include #include #include #include class QPainterOutputDevType3Font { public: QPainterOutputDevType3Font(PDFDoc *doc, const std::shared_ptr &font); const QPicture &getGlyph(int gid) const; private: PDFDoc *m_doc; std::shared_ptr m_font; mutable std::vector> glyphs; public: std::vector codeToGID; }; QPainterOutputDevType3Font::QPainterOutputDevType3Font(PDFDoc *doc, const std::shared_ptr &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(); glyphPainter.begin(glyphs[gid].get()); auto output_dev = std::make_unique(&glyphPainter); auto gfx = std::make_unique(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(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(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 &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 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::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 = 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(gfxFont)); m_type3FontCache.insert(std::make_pair(fontID, std::unique_ptr(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 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> 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(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(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> fontBuffer; std::optional 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 FT_Face freeTypeFace; if (fontLoc->locType != gfxFontLocEmbedded) { if (ft_new_face_from_file(m_ftLibrary, fontLoc->path.c_str(), fontLoc->fontNum, &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(), fontLoc->fontNum, &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(), fontLoc->fontNum) : FoFiTrueType::make(fontBuffer->data(), fontBuffer->size(), fontLoc->fontNum); 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::load(fontLoc->path.c_str())) : std::unique_ptr(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(), fontLoc->fontNum) : FoFiTrueType::make(fontBuffer->data(), fontBuffer->size(), fontLoc->fontNum); 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(), fontLoc->fontNum) : FoFiTrueType::make(fontBuffer->data(), fontBuffer->size(), fontLoc->fontNum); 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 ta; ta[0] = tMin; std::array 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 = 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(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(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(str, width, colorMap->getNumPixelComps(), colorMap->getBits()); imgStr->reset(); image = QImage(width, height, QImage::Format_ARGB32); data = reinterpret_cast(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(str, width, colorMap->getNumPixelComps(), colorMap->getBits()); imgStr->reset(); auto maskImageStr = std::make_unique(maskStr, maskWidth, maskColorMap->getNumPixelComps(), maskColorMap->getBits()); maskImageStr->reset(); QImage image(width, height, QImage::Format_ARGB32); unsigned int *data = reinterpret_cast(image.bits()); int stride = image.bytesPerLine() / 4; std::vector 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; }