| //======================================================================== |
| // |
| // Gfx.cc |
| // |
| // Copyright 1996-2013 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 Jonathan Blandford <jrb@redhat.com> |
| // Copyright (C) 2005-2013, 2015-2019 Albert Astals Cid <aacid@kde.org> |
| // Copyright (C) 2006 Thorkild Stray <thorkild@ifi.uio.no> |
| // Copyright (C) 2006 Kristian Høgsberg <krh@redhat.com> |
| // Copyright (C) 2006-2011 Carlos Garcia Campos <carlosgc@gnome.org> |
| // Copyright (C) 2006, 2007 Jeff Muizelaar <jeff@infidigm.net> |
| // Copyright (C) 2007, 2008 Brad Hards <bradh@kde.org> |
| // Copyright (C) 2007, 2011, 2017 Adrian Johnson <ajohnson@redneon.com> |
| // Copyright (C) 2007, 2008 Iñigo Martínez <inigomartinez@gmail.com> |
| // Copyright (C) 2007 Koji Otani <sho@bbr.jp> |
| // Copyright (C) 2007 Krzysztof Kowalczyk <kkowalczyk@gmail.com> |
| // Copyright (C) 2008 Pino Toscano <pino@kde.org> |
| // Copyright (C) 2008 Michael Vrable <mvrable@cs.ucsd.edu> |
| // Copyright (C) 2008 Hib Eris <hib@hiberis.nl> |
| // Copyright (C) 2009 M Joonas Pihlaja <jpihlaja@cc.helsinki.fi> |
| // Copyright (C) 2009-2016 Thomas Freitag <Thomas.Freitag@alfa.de> |
| // Copyright (C) 2009 William Bader <williambader@hotmail.com> |
| // Copyright (C) 2009, 2010 David Benjamin <davidben@mit.edu> |
| // Copyright (C) 2010 Nils Höglund <nils.hoglund@gmail.com> |
| // Copyright (C) 2010 Christian Feuersänger <cfeuersaenger@googlemail.com> |
| // Copyright (C) 2011 Axel Strübing <axel.struebing@freenet.de> |
| // Copyright (C) 2012 Even Rouault <even.rouault@mines-paris.org> |
| // Copyright (C) 2012, 2013 Fabio D'Urso <fabiodurso@hotmail.it> |
| // Copyright (C) 2012 Lu Wang <coolwanglu@gmail.com> |
| // Copyright (C) 2014 Jason Crain <jason@aquaticape.us> |
| // Copyright (C) 2017, 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, 2019 Adam Reichold <adam.reichold@t-online.de> |
| // Copyright (C) 2018 Denis Onishchenko <denis.onischenko@gmail.com> |
| // Copyright (C) 2019 LE GARREC Vincent <legarrec.vincent@gmail.com> |
| // Copyright (C) 2019 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 |
| // |
| //======================================================================== |
| |
| #include <config.h> |
| |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <stddef.h> |
| #include <string.h> |
| #include <math.h> |
| #include <memory> |
| #include "goo/gmem.h" |
| #include "goo/GooTimer.h" |
| #include "GlobalParams.h" |
| #include "CharTypes.h" |
| #include "Object.h" |
| #include "PDFDoc.h" |
| #include "Array.h" |
| #include "Annot.h" |
| #include "Dict.h" |
| #include "Stream.h" |
| #include "Lexer.h" |
| #include "Parser.h" |
| #include "GfxFont.h" |
| #include "GfxState.h" |
| #include "OutputDev.h" |
| #include "Page.h" |
| #include "Annot.h" |
| #include "Error.h" |
| #include "Gfx.h" |
| #include "ProfileData.h" |
| #include "Catalog.h" |
| #include "OptionalContent.h" |
| |
| // the MSVC math.h doesn't define this |
| #ifndef M_PI |
| #define M_PI 3.14159265358979323846 |
| #endif |
| |
| //------------------------------------------------------------------------ |
| // constants |
| //------------------------------------------------------------------------ |
| |
| // Max recursive depth for a function shading fill. |
| #define functionMaxDepth 6 |
| |
| // Max delta allowed in any color component for a function shading fill. |
| #define functionColorDelta (dblToCol(1 / 256.0)) |
| |
| // Max number of splits along the t axis for an axial shading fill. |
| #define axialMaxSplits 256 |
| |
| // Max delta allowed in any color component for an axial shading fill. |
| #define axialColorDelta (dblToCol(1 / 256.0)) |
| |
| // Max number of splits along the t axis for a radial shading fill. |
| #define radialMaxSplits 256 |
| |
| // Max delta allowed in any color component for a radial shading fill. |
| #define radialColorDelta (dblToCol(1 / 256.0)) |
| |
| // Max recursive depth for a Gouraud triangle shading fill. |
| // |
| // Triangles will be split at most gouraudMaxDepth times (each time into 4 |
| // smaller ones). That makes pow(4,gouraudMaxDepth) many triangles for |
| // every triangle. |
| #define gouraudMaxDepth 6 |
| |
| // Max delta allowed in any color component for a Gouraud triangle |
| // shading fill. |
| #define gouraudColorDelta (dblToCol(3. / 256.0)) |
| |
| // Gouraud triangle: if the three color parameters differ by at more than this percend of |
| // the total color parameter range, the triangle will be refined |
| #define gouraudParameterizedColorDelta 5e-3 |
| |
| // Max recursive depth for a patch mesh shading fill. |
| #define patchMaxDepth 6 |
| |
| // Max delta allowed in any color component for a patch mesh shading |
| // fill. |
| #define patchColorDelta (dblToCol((3. / 256.0))) |
| |
| //------------------------------------------------------------------------ |
| // Operator table |
| //------------------------------------------------------------------------ |
| |
| Operator Gfx::opTab[] = { |
| {"\"", 3, {tchkNum, tchkNum, tchkString}, |
| &Gfx::opMoveSetShowText}, |
| {"'", 1, {tchkString}, |
| &Gfx::opMoveShowText}, |
| {"B", 0, {tchkNone}, |
| &Gfx::opFillStroke}, |
| {"B*", 0, {tchkNone}, |
| &Gfx::opEOFillStroke}, |
| {"BDC", 2, {tchkName, tchkProps}, |
| &Gfx::opBeginMarkedContent}, |
| {"BI", 0, {tchkNone}, |
| &Gfx::opBeginImage}, |
| {"BMC", 1, {tchkName}, |
| &Gfx::opBeginMarkedContent}, |
| {"BT", 0, {tchkNone}, |
| &Gfx::opBeginText}, |
| {"BX", 0, {tchkNone}, |
| &Gfx::opBeginIgnoreUndef}, |
| {"CS", 1, {tchkName}, |
| &Gfx::opSetStrokeColorSpace}, |
| {"DP", 2, {tchkName, tchkProps}, |
| &Gfx::opMarkPoint}, |
| {"Do", 1, {tchkName}, |
| &Gfx::opXObject}, |
| {"EI", 0, {tchkNone}, |
| &Gfx::opEndImage}, |
| {"EMC", 0, {tchkNone}, |
| &Gfx::opEndMarkedContent}, |
| {"ET", 0, {tchkNone}, |
| &Gfx::opEndText}, |
| {"EX", 0, {tchkNone}, |
| &Gfx::opEndIgnoreUndef}, |
| {"F", 0, {tchkNone}, |
| &Gfx::opFill}, |
| {"G", 1, {tchkNum}, |
| &Gfx::opSetStrokeGray}, |
| {"ID", 0, {tchkNone}, |
| &Gfx::opImageData}, |
| {"J", 1, {tchkInt}, |
| &Gfx::opSetLineCap}, |
| {"K", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, |
| &Gfx::opSetStrokeCMYKColor}, |
| {"M", 1, {tchkNum}, |
| &Gfx::opSetMiterLimit}, |
| {"MP", 1, {tchkName}, |
| &Gfx::opMarkPoint}, |
| {"Q", 0, {tchkNone}, |
| &Gfx::opRestore}, |
| {"RG", 3, {tchkNum, tchkNum, tchkNum}, |
| &Gfx::opSetStrokeRGBColor}, |
| {"S", 0, {tchkNone}, |
| &Gfx::opStroke}, |
| {"SC", -4, {tchkNum, tchkNum, tchkNum, tchkNum}, |
| &Gfx::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}, |
| &Gfx::opSetStrokeColorN}, |
| {"T*", 0, {tchkNone}, |
| &Gfx::opTextNextLine}, |
| {"TD", 2, {tchkNum, tchkNum}, |
| &Gfx::opTextMoveSet}, |
| {"TJ", 1, {tchkArray}, |
| &Gfx::opShowSpaceText}, |
| {"TL", 1, {tchkNum}, |
| &Gfx::opSetTextLeading}, |
| {"Tc", 1, {tchkNum}, |
| &Gfx::opSetCharSpacing}, |
| {"Td", 2, {tchkNum, tchkNum}, |
| &Gfx::opTextMove}, |
| {"Tf", 2, {tchkName, tchkNum}, |
| &Gfx::opSetFont}, |
| {"Tj", 1, {tchkString}, |
| &Gfx::opShowText}, |
| {"Tm", 6, {tchkNum, tchkNum, tchkNum, tchkNum, |
| tchkNum, tchkNum}, |
| &Gfx::opSetTextMatrix}, |
| {"Tr", 1, {tchkInt}, |
| &Gfx::opSetTextRender}, |
| {"Ts", 1, {tchkNum}, |
| &Gfx::opSetTextRise}, |
| {"Tw", 1, {tchkNum}, |
| &Gfx::opSetWordSpacing}, |
| {"Tz", 1, {tchkNum}, |
| &Gfx::opSetHorizScaling}, |
| {"W", 0, {tchkNone}, |
| &Gfx::opClip}, |
| {"W*", 0, {tchkNone}, |
| &Gfx::opEOClip}, |
| {"b", 0, {tchkNone}, |
| &Gfx::opCloseFillStroke}, |
| {"b*", 0, {tchkNone}, |
| &Gfx::opCloseEOFillStroke}, |
| {"c", 6, {tchkNum, tchkNum, tchkNum, tchkNum, |
| tchkNum, tchkNum}, |
| &Gfx::opCurveTo}, |
| {"cm", 6, {tchkNum, tchkNum, tchkNum, tchkNum, |
| tchkNum, tchkNum}, |
| &Gfx::opConcat}, |
| {"cs", 1, {tchkName}, |
| &Gfx::opSetFillColorSpace}, |
| {"d", 2, {tchkArray, tchkNum}, |
| &Gfx::opSetDash}, |
| {"d0", 2, {tchkNum, tchkNum}, |
| &Gfx::opSetCharWidth}, |
| {"d1", 6, {tchkNum, tchkNum, tchkNum, tchkNum, |
| tchkNum, tchkNum}, |
| &Gfx::opSetCacheDevice}, |
| {"f", 0, {tchkNone}, |
| &Gfx::opFill}, |
| {"f*", 0, {tchkNone}, |
| &Gfx::opEOFill}, |
| {"g", 1, {tchkNum}, |
| &Gfx::opSetFillGray}, |
| {"gs", 1, {tchkName}, |
| &Gfx::opSetExtGState}, |
| {"h", 0, {tchkNone}, |
| &Gfx::opClosePath}, |
| {"i", 1, {tchkNum}, |
| &Gfx::opSetFlat}, |
| {"j", 1, {tchkInt}, |
| &Gfx::opSetLineJoin}, |
| {"k", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, |
| &Gfx::opSetFillCMYKColor}, |
| {"l", 2, {tchkNum, tchkNum}, |
| &Gfx::opLineTo}, |
| {"m", 2, {tchkNum, tchkNum}, |
| &Gfx::opMoveTo}, |
| {"n", 0, {tchkNone}, |
| &Gfx::opEndPath}, |
| {"q", 0, {tchkNone}, |
| &Gfx::opSave}, |
| {"re", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, |
| &Gfx::opRectangle}, |
| {"rg", 3, {tchkNum, tchkNum, tchkNum}, |
| &Gfx::opSetFillRGBColor}, |
| {"ri", 1, {tchkName}, |
| &Gfx::opSetRenderingIntent}, |
| {"s", 0, {tchkNone}, |
| &Gfx::opCloseStroke}, |
| {"sc", -4, {tchkNum, tchkNum, tchkNum, tchkNum}, |
| &Gfx::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}, |
| &Gfx::opSetFillColorN}, |
| {"sh", 1, {tchkName}, |
| &Gfx::opShFill}, |
| {"v", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, |
| &Gfx::opCurveTo1}, |
| {"w", 1, {tchkNum}, |
| &Gfx::opSetLineWidth}, |
| {"y", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, |
| &Gfx::opCurveTo2}, |
| }; |
| |
| #define numOps (sizeof(opTab) / sizeof(Operator)) |
| |
| static inline bool isSameGfxColor(const GfxColor &colorA, const GfxColor &colorB, unsigned int nComps, double delta) { |
| for (unsigned int k = 0; k < nComps; ++k) { |
| if (abs(colorA.c[k] - colorB.c[k]) > delta) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| //------------------------------------------------------------------------ |
| // GfxResources |
| //------------------------------------------------------------------------ |
| |
| GfxResources::GfxResources(XRef *xrefA, Dict *resDictA, GfxResources *nextA) : |
| gStateCache(2), xref(xrefA) { |
| Ref r; |
| |
| if (resDictA) { |
| |
| // build font dictionary |
| Dict *resDict = resDictA->copy(xref); |
| fonts = nullptr; |
| const Object &obj1 = resDict->lookupNF("Font"); |
| if (obj1.isRef()) { |
| Object obj2 = obj1.fetch(xref); |
| if (obj2.isDict()) { |
| r = obj1.getRef(); |
| fonts = new GfxFontDict(xref, &r, obj2.getDict()); |
| } |
| } else if (obj1.isDict()) { |
| fonts = new GfxFontDict(xref, nullptr, obj1.getDict()); |
| } |
| |
| // get XObject dictionary |
| xObjDict = resDict->lookup("XObject"); |
| |
| // get color space dictionary |
| colorSpaceDict = resDict->lookup("ColorSpace"); |
| |
| // get pattern dictionary |
| patternDict = resDict->lookup("Pattern"); |
| |
| // get shading dictionary |
| shadingDict = resDict->lookup("Shading"); |
| |
| // get graphics state parameter dictionary |
| gStateDict = resDict->lookup("ExtGState"); |
| |
| // get properties dictionary |
| propertiesDict = resDict->lookup("Properties"); |
| |
| delete resDict; |
| } else { |
| fonts = nullptr; |
| xObjDict.setToNull(); |
| colorSpaceDict.setToNull(); |
| patternDict.setToNull(); |
| shadingDict.setToNull(); |
| gStateDict.setToNull(); |
| propertiesDict.setToNull(); |
| } |
| |
| next = nextA; |
| } |
| |
| GfxResources::~GfxResources() { |
| delete fonts; |
| } |
| |
| GfxFont *GfxResources::doLookupFont(const char *name) const |
| { |
| GfxFont *font; |
| const GfxResources *resPtr; |
| |
| for (resPtr = this; resPtr; resPtr = resPtr->next) { |
| if (resPtr->fonts) { |
| if ((font = resPtr->fonts->lookup(name))) |
| return font; |
| } |
| } |
| error(errSyntaxError, -1, "Unknown font tag '{0:s}'", name); |
| return nullptr; |
| } |
| |
| GfxFont *GfxResources::lookupFont(const char *name) { |
| return doLookupFont(name); |
| } |
| |
| const GfxFont *GfxResources::lookupFont(const char *name) const { |
| return doLookupFont(name); |
| } |
| |
| Object GfxResources::lookupXObject(const char *name) { |
| GfxResources *resPtr; |
| |
| for (resPtr = this; resPtr; resPtr = resPtr->next) { |
| if (resPtr->xObjDict.isDict()) { |
| Object obj = resPtr->xObjDict.dictLookup(name); |
| if (!obj.isNull()) |
| return obj; |
| } |
| } |
| error(errSyntaxError, -1, "XObject '{0:s}' is unknown", name); |
| return Object(objNull); |
| } |
| |
| Object GfxResources::lookupXObjectNF(const char *name) { |
| GfxResources *resPtr; |
| |
| for (resPtr = this; resPtr; resPtr = resPtr->next) { |
| if (resPtr->xObjDict.isDict()) { |
| Object obj = resPtr->xObjDict.dictLookupNF(name).copy(); |
| if (!obj.isNull()) |
| return obj; |
| } |
| } |
| error(errSyntaxError, -1, "XObject '{0:s}' is unknown", name); |
| return Object(objNull); |
| } |
| |
| Object GfxResources::lookupMarkedContentNF(const char *name) { |
| GfxResources *resPtr; |
| |
| for (resPtr = this; resPtr; resPtr = resPtr->next) { |
| if (resPtr->propertiesDict.isDict()) { |
| Object obj = resPtr->propertiesDict.dictLookupNF(name).copy(); |
| if (!obj.isNull()) |
| return obj; |
| } |
| } |
| error(errSyntaxError, -1, "Marked Content '{0:s}' is unknown", name); |
| return Object(objNull); |
| } |
| |
| Object GfxResources::lookupColorSpace(const char *name) { |
| GfxResources *resPtr; |
| |
| for (resPtr = this; resPtr; resPtr = resPtr->next) { |
| if (resPtr->colorSpaceDict.isDict()) { |
| Object obj = resPtr->colorSpaceDict.dictLookup(name); |
| if (!obj.isNull()) { |
| return obj; |
| } |
| } |
| } |
| return Object(objNull); |
| } |
| |
| GfxPattern *GfxResources::lookupPattern(const char *name, OutputDev *out, GfxState *state) { |
| GfxResources *resPtr; |
| |
| for (resPtr = this; resPtr; resPtr = resPtr->next) { |
| if (resPtr->patternDict.isDict()) { |
| Ref patternRef = Ref::INVALID(); |
| Object obj = resPtr->patternDict.getDict()->lookup(name, &patternRef); |
| if (!obj.isNull()) { |
| return GfxPattern::parse(resPtr, &obj, out, state, patternRef.num); |
| } |
| } |
| } |
| error(errSyntaxError, -1, "Unknown pattern '{0:s}'", name); |
| return nullptr; |
| } |
| |
| GfxShading *GfxResources::lookupShading(const char *name, OutputDev *out, GfxState *state) { |
| GfxResources *resPtr; |
| GfxShading *shading; |
| |
| for (resPtr = this; resPtr; resPtr = resPtr->next) { |
| if (resPtr->shadingDict.isDict()) { |
| Object obj = resPtr->shadingDict.dictLookup(name); |
| if (!obj.isNull()) { |
| shading = GfxShading::parse(resPtr, &obj, out, state); |
| return shading; |
| } |
| } |
| } |
| error(errSyntaxError, -1, "ExtGState '{0:s}' is unknown", name); |
| return nullptr; |
| } |
| |
| Object GfxResources::lookupGState(const char *name) { |
| Object obj = lookupGStateNF(name); |
| if (obj.isNull()) |
| return Object(objNull); |
| |
| if (!obj.isRef()) |
| return obj; |
| |
| const Ref ref = obj.getRef(); |
| |
| if (auto *item = gStateCache.lookup(ref)) { |
| return item->copy(); |
| } |
| |
| auto *item = new Object{xref->fetch(ref)}; |
| gStateCache.put(ref, item); |
| return item->copy(); |
| } |
| |
| Object GfxResources::lookupGStateNF(const char *name) { |
| GfxResources *resPtr; |
| |
| for (resPtr = this; resPtr; resPtr = resPtr->next) { |
| if (resPtr->gStateDict.isDict()) { |
| Object obj = resPtr->gStateDict.dictLookupNF(name).copy(); |
| if (!obj.isNull()) { |
| return obj; |
| } |
| } |
| } |
| error(errSyntaxError, -1, "ExtGState '{0:s}' is unknown", name); |
| return Object(objNull); |
| } |
| |
| //------------------------------------------------------------------------ |
| // Gfx |
| //------------------------------------------------------------------------ |
| |
| Gfx::Gfx(PDFDoc *docA, OutputDev *outA, int pageNum, Dict *resDict, |
| double hDPI, double vDPI, const PDFRectangle *box, |
| const PDFRectangle *cropBox, int rotate, |
| bool (*abortCheckCbkA)(void *data), |
| void *abortCheckCbkDataA, XRef *xrefA) |
| { |
| int i; |
| |
| doc = docA; |
| xref = (xrefA == nullptr) ? doc->getXRef() : xrefA; |
| catalog = doc->getCatalog(); |
| subPage = false; |
| printCommands = globalParams->getPrintCommands(); |
| profileCommands = globalParams->getProfileCommands(); |
| mcStack = nullptr; |
| parser = nullptr; |
| |
| // start the resource stack |
| res = new GfxResources(xref, resDict, nullptr); |
| |
| // initialize |
| out = outA; |
| state = new GfxState(hDPI, vDPI, box, rotate, out->upsideDown()); |
| stackHeight = 1; |
| pushStateGuard(); |
| fontChanged = false; |
| clip = clipNone; |
| ignoreUndef = 0; |
| out->startPage(pageNum, state, xref); |
| out->setDefaultCTM(state->getCTM()); |
| out->updateAll(state); |
| for (i = 0; i < 6; ++i) { |
| baseMatrix[i] = state->getCTM()[i]; |
| } |
| formDepth = 0; |
| ocState = true; |
| parser = nullptr; |
| abortCheckCbk = abortCheckCbkA; |
| abortCheckCbkData = abortCheckCbkDataA; |
| |
| // set crop box |
| if (cropBox) { |
| state->moveTo(cropBox->x1, cropBox->y1); |
| state->lineTo(cropBox->x2, cropBox->y1); |
| state->lineTo(cropBox->x2, cropBox->y2); |
| state->lineTo(cropBox->x1, cropBox->y2); |
| state->closePath(); |
| state->clip(); |
| out->clip(state); |
| state->clearPath(); |
| } |
| #ifdef USE_CMS |
| initDisplayProfile(); |
| #endif |
| } |
| |
| Gfx::Gfx(PDFDoc *docA, OutputDev *outA, Dict *resDict, |
| const PDFRectangle *box, const PDFRectangle *cropBox, |
| bool (*abortCheckCbkA)(void *data), |
| void *abortCheckCbkDataA, Gfx *gfxA) |
| { |
| int i; |
| |
| doc = docA; |
| if (gfxA) { |
| xref = gfxA->getXRef(); |
| formsDrawing = gfxA->formsDrawing; |
| charProcDrawing = gfxA->charProcDrawing; |
| } else { |
| xref = doc->getXRef(); |
| } |
| catalog = doc->getCatalog(); |
| subPage = true; |
| printCommands = globalParams->getPrintCommands(); |
| profileCommands = globalParams->getProfileCommands(); |
| mcStack = nullptr; |
| parser = nullptr; |
| |
| // start the resource stack |
| res = new GfxResources(xref, resDict, nullptr); |
| |
| // initialize |
| out = outA; |
| state = new GfxState(72, 72, box, 0, false); |
| stackHeight = 1; |
| pushStateGuard(); |
| fontChanged = false; |
| clip = clipNone; |
| ignoreUndef = 0; |
| for (i = 0; i < 6; ++i) { |
| baseMatrix[i] = state->getCTM()[i]; |
| } |
| formDepth = 0; |
| ocState = true; |
| parser = nullptr; |
| abortCheckCbk = abortCheckCbkA; |
| abortCheckCbkData = abortCheckCbkDataA; |
| |
| // set crop box |
| if (cropBox) { |
| state->moveTo(cropBox->x1, cropBox->y1); |
| state->lineTo(cropBox->x2, cropBox->y1); |
| state->lineTo(cropBox->x2, cropBox->y2); |
| state->lineTo(cropBox->x1, cropBox->y2); |
| state->closePath(); |
| state->clip(); |
| out->clip(state); |
| state->clearPath(); |
| } |
| #ifdef USE_CMS |
| initDisplayProfile(); |
| #endif |
| } |
| |
| #ifdef USE_CMS |
| |
| #include <lcms2.h> |
| |
| void Gfx::initDisplayProfile() { |
| Object catDict = xref->getCatalog(); |
| if (catDict.isDict()) { |
| Object outputIntents = catDict.dictLookup("OutputIntents"); |
| if (outputIntents.isArray() && outputIntents.arrayGetLength() == 1) { |
| Object firstElement = outputIntents.arrayGet(0); |
| if (firstElement.isDict()) { |
| Object profile = firstElement.dictLookup("DestOutputProfile"); |
| if (profile.isStream()) { |
| Stream *iccStream = profile.getStream(); |
| int length = 0; |
| unsigned char *profBuf = iccStream->toUnsignedChars(&length, 65536, 65536); |
| cmsHPROFILE hp = cmsOpenProfileFromMem(profBuf,length); |
| if (hp == nullptr) { |
| error(errSyntaxWarning, -1, "read ICCBased color space profile error"); |
| } else { |
| state->setDisplayProfile(hp); |
| } |
| gfree(profBuf); |
| } |
| } |
| } |
| } |
| } |
| |
| #endif |
| |
| Gfx::~Gfx() { |
| while (stateGuards.size()) { |
| popStateGuard(); |
| } |
| if (!subPage) { |
| out->endPage(); |
| } |
| // There shouldn't be more saves, but pop them if there were any |
| while (state->hasSaves()) { |
| error(errSyntaxError, -1, "Found state under last state guard. Popping."); |
| restoreState(); |
| } |
| delete state; |
| while (res) { |
| popResources(); |
| } |
| while (mcStack) { |
| popMarkedContent(); |
| } |
| } |
| |
| void Gfx::display(Object *obj, bool topLevel) { |
| int i; |
| |
| if (obj->isArray()) { |
| for (i = 0; i < obj->arrayGetLength(); ++i) { |
| Object obj2 = obj->arrayGet(i); |
| if (!obj2.isStream()) { |
| error(errSyntaxError, -1, "Weird page contents"); |
| return; |
| } |
| } |
| } else if (!obj->isStream()) { |
| error(errSyntaxError, -1, "Weird page contents"); |
| return; |
| } |
| parser = new Parser(xref, obj, false); |
| go(topLevel); |
| delete parser; |
| parser = nullptr; |
| } |
| |
| void Gfx::go(bool topLevel) { |
| Object obj; |
| Object args[maxArgs]; |
| int numArgs, i; |
| int lastAbortCheck; |
| |
| // scan a sequence of objects |
| pushStateGuard(); |
| updateLevel = 1; // make sure even empty pages trigger a call to dump() |
| lastAbortCheck = 0; |
| numArgs = 0; |
| obj = parser->getObj(); |
| while (!obj.isEOF()) { |
| commandAborted = false; |
| |
| // got a command - execute it |
| if (obj.isCmd()) { |
| if (printCommands) { |
| obj.print(stdout); |
| for (i = 0; i < numArgs; ++i) { |
| printf(" "); |
| args[i].print(stdout); |
| } |
| printf("\n"); |
| fflush(stdout); |
| } |
| GooTimer *timer = nullptr; |
| |
| if (unlikely(profileCommands)) { |
| timer = new GooTimer(); |
| } |
| |
| // Run the operation |
| execOp(&obj, args, numArgs); |
| |
| // Update the profile information |
| if (unlikely(profileCommands)) { |
| if (auto* const hash = out->getProfileHash()) { |
| auto& data = (*hash)[obj.getCmd()]; |
| data.addElement(timer->getElapsed()); |
| } |
| delete timer; |
| } |
| for (i = 0; i < numArgs; ++i) |
| args[i].setToNull(); // Free memory early |
| numArgs = 0; |
| |
| // periodically update display |
| if (++updateLevel >= 20000) { |
| out->dump(); |
| updateLevel = 0; |
| lastAbortCheck = 0; |
| } |
| |
| // did the command throw an exception |
| if (commandAborted) { |
| // don't propogate; recursive drawing comes from Form XObjects which |
| // should probably be drawn in a separate context anyway for caching |
| commandAborted = false; |
| break; |
| } |
| |
| // check for an abort |
| if (abortCheckCbk) { |
| if (updateLevel - lastAbortCheck > 10) { |
| if ((*abortCheckCbk)(abortCheckCbkData)) { |
| break; |
| } |
| lastAbortCheck = updateLevel; |
| } |
| } |
| |
| // 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); |
| } |
| } |
| |
| // grab the next object |
| obj = parser->getObj(); |
| } |
| |
| // args at end with no command |
| if (numArgs > 0) { |
| error(errSyntaxError, getPos(), "Leftover args in content stream"); |
| if (printCommands) { |
| printf("%d leftovers:", numArgs); |
| for (i = 0; i < numArgs; ++i) { |
| printf(" "); |
| args[i].print(stdout); |
| } |
| printf("\n"); |
| fflush(stdout); |
| } |
| } |
| |
| popStateGuard(); |
| |
| // update display |
| if (topLevel && updateLevel > 0) { |
| out->dump(); |
| } |
| } |
| |
| void Gfx::execOp(Object *cmd, Object args[], int numArgs) { |
| Operator *op; |
| Object *argPtr; |
| int i; |
| |
| // find operator |
| const char *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:s}' operator", numArgs, name); |
| commandAborted = true; |
| return; |
| } |
| if (numArgs > op->numArgs) { |
| #if 0 |
| error(errSyntaxWarning, 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; |
| } |
| } |
| |
| // do it |
| (this->*op->func)(argPtr, numArgs); |
| } |
| |
| Operator *Gfx::findOp(const char *name) { |
| int a, b, m, cmp; |
| |
| a = -1; |
| b = numOps; |
| cmp = 0; // make gcc happy |
| // invariant: opTab[a] < name < opTab[b] |
| while (b - a > 1) { |
| 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]; |
| } |
| |
| bool Gfx::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 false; |
| } |
| return false; |
| } |
| |
| Goffset Gfx::getPos() { |
| return parser ? parser->getPos() : -1; |
| } |
| |
| //------------------------------------------------------------------------ |
| // graphics state operators |
| //------------------------------------------------------------------------ |
| |
| void Gfx::opSave(Object args[], int numArgs) { |
| saveState(); |
| } |
| |
| void Gfx::opRestore(Object args[], int numArgs) { |
| restoreState(); |
| } |
| |
| void Gfx::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()); |
| out->updateCTM(state, args[0].getNum(), args[1].getNum(), |
| args[2].getNum(), args[3].getNum(), |
| args[4].getNum(), args[5].getNum()); |
| fontChanged = true; |
| } |
| |
| void Gfx::opSetDash(Object args[], int numArgs) { |
| Array *a; |
| int length; |
| double *dash; |
| int i; |
| |
| a = args[0].getArray(); |
| length = a->getLength(); |
| if (length == 0) { |
| dash = nullptr; |
| } else { |
| dash = (double *)gmallocn(length, sizeof(double)); |
| bool dummyOk; |
| for (i = 0; i < length; ++i) { |
| const Object obj = a->get(i); |
| dash[i] = obj.getNum(&dummyOk); |
| } |
| } |
| state->setLineDash(dash, length, args[1].getNum()); |
| out->updateLineDash(state); |
| } |
| |
| void Gfx::opSetFlat(Object args[], int numArgs) { |
| state->setFlatness((int)args[0].getNum()); |
| out->updateFlatness(state); |
| } |
| |
| void Gfx::opSetLineJoin(Object args[], int numArgs) { |
| state->setLineJoin(args[0].getInt()); |
| out->updateLineJoin(state); |
| } |
| |
| void Gfx::opSetLineCap(Object args[], int numArgs) { |
| state->setLineCap(args[0].getInt()); |
| out->updateLineCap(state); |
| } |
| |
| void Gfx::opSetMiterLimit(Object args[], int numArgs) { |
| state->setMiterLimit(args[0].getNum()); |
| out->updateMiterLimit(state); |
| } |
| |
| void Gfx::opSetLineWidth(Object args[], int numArgs) { |
| state->setLineWidth(args[0].getNum()); |
| out->updateLineWidth(state); |
| } |
| |
| void Gfx::opSetExtGState(Object args[], int numArgs) { |
| Object obj1, obj2; |
| GfxBlendMode mode; |
| bool haveFillOP; |
| Function *funcs[4]; |
| GfxColor backdropColor; |
| bool haveBackdropColor; |
| bool alpha, isolated, knockout; |
| double opac; |
| int i; |
| |
| 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()); |
| return; |
| } |
| if (printCommands) { |
| printf(" gfx state dict: "); |
| obj1.print(); |
| printf("\n"); |
| } |
| |
| // parameters that are also set by individual PDF operators |
| obj2 = obj1.dictLookup("LW"); |
| if (obj2.isNum()) { |
| opSetLineWidth(&obj2, 1); |
| } |
| obj2 = obj1.dictLookup("LC"); |
| if (obj2.isInt()) { |
| opSetLineCap(&obj2, 1); |
| } |
| obj2 = obj1.dictLookup("LJ"); |
| if (obj2.isInt()) { |
| opSetLineJoin(&obj2, 1); |
| } |
| obj2 = obj1.dictLookup("ML"); |
| if (obj2.isNum()) { |
| opSetMiterLimit(&obj2, 1); |
| } |
| obj2 = obj1.dictLookup("D"); |
| if (obj2.isArray() && obj2.arrayGetLength() == 2) { |
| Object args2[2]; |
| args2[0] = obj2.arrayGet(0); |
| args2[1] = obj2.arrayGet(1); |
| if (args2[0].isArray() && args2[1].isNum()) { |
| opSetDash(args2, 2); |
| } |
| } |
| #if 0 //~ need to add a new version of GfxResources::lookupFont() that |
| //~ takes an indirect ref instead of a name |
| if (obj1.dictLookup("Font", &obj2)->isArray() && |
| obj2.arrayGetLength() == 2) { |
| obj2.arrayGet(0, &args2[0]); |
| obj2.arrayGet(1, &args2[1]); |
| if (args2[0].isDict() && args2[1].isNum()) { |
| opSetFont(args2, 2); |
| } |
| args2[0].free(); |
| args2[1].free(); |
| } |
| obj2.free(); |
| #endif |
| obj2 = obj1.dictLookup("FL"); |
| if (obj2.isNum()) { |
| opSetFlat(&obj2, 1); |
| } |
| |
| // transparency support: blend mode, fill/stroke opacity |
| obj2 = obj1.dictLookup("BM"); |
| if (!obj2.isNull()) { |
| if (state->parseBlendMode(&obj2, &mode)) { |
| state->setBlendMode(mode); |
| out->updateBlendMode(state); |
| } else { |
| error(errSyntaxError, getPos(), "Invalid blend mode in ExtGState"); |
| } |
| } |
| obj2 = obj1.dictLookup("ca"); |
| if (obj2.isNum()) { |
| opac = obj2.getNum(); |
| state->setFillOpacity(opac < 0 ? 0 : opac > 1 ? 1 : opac); |
| out->updateFillOpacity(state); |
| } |
| obj2 = obj1.dictLookup("CA"); |
| if (obj2.isNum()) { |
| opac = obj2.getNum(); |
| state->setStrokeOpacity(opac < 0 ? 0 : opac > 1 ? 1 : opac); |
| out->updateStrokeOpacity(state); |
| } |
| |
| // fill/stroke overprint, overprint mode |
| obj2 = obj1.dictLookup("op"); |
| if ((haveFillOP = obj2.isBool())) { |
| state->setFillOverprint(obj2.getBool()); |
| out->updateFillOverprint(state); |
| } |
| obj2 = obj1.dictLookup("OP"); |
| if (obj2.isBool()) { |
| state->setStrokeOverprint(obj2.getBool()); |
| out->updateStrokeOverprint(state); |
| if (!haveFillOP) { |
| state->setFillOverprint(obj2.getBool()); |
| out->updateFillOverprint(state); |
| } |
| } |
| obj2 = obj1.dictLookup("OPM"); |
| if (obj2.isInt()) { |
| state->setOverprintMode(obj2.getInt()); |
| out->updateOverprintMode(state); |
| } |
| |
| // stroke adjust |
| obj2 = obj1.dictLookup("SA"); |
| if (obj2.isBool()) { |
| state->setStrokeAdjust(obj2.getBool()); |
| out->updateStrokeAdjust(state); |
| } |
| |
| // transfer function |
| obj2 = obj1.dictLookup("TR2"); |
| if (obj2.isNull()) { |
| obj2 = obj1.dictLookup("TR"); |
| } |
| if (obj2.isName("Default") || |
| obj2.isName("Identity")) { |
| funcs[0] = funcs[1] = funcs[2] = funcs[3] = nullptr; |
| state->setTransfer(funcs); |
| out->updateTransfer(state); |
| } else if (obj2.isArray() && obj2.arrayGetLength() == 4) { |
| for (i = 0; i < 4; ++i) { |
| Object obj3 = obj2.arrayGet(i); |
| funcs[i] = Function::parse(&obj3); |
| if (!funcs[i]) { |
| break; |
| } |
| } |
| if (i == 4) { |
| state->setTransfer(funcs); |
| out->updateTransfer(state); |
| } |
| } else if (obj2.isName() || obj2.isDict() || obj2.isStream()) { |
| if ((funcs[0] = Function::parse(&obj2))) { |
| funcs[1] = funcs[2] = funcs[3] = nullptr; |
| state->setTransfer(funcs); |
| out->updateTransfer(state); |
| } |
| } else if (!obj2.isNull()) { |
| error(errSyntaxError, getPos(), "Invalid transfer function in ExtGState"); |
| } |
| |
| // alpha is shape |
| obj2 = obj1.dictLookup("AIS"); |
| if (obj2.isBool()) { |
| state->setAlphaIsShape(obj2.getBool()); |
| out->updateAlphaIsShape(state); |
| } |
| |
| // text knockout |
| obj2 = obj1.dictLookup("TK"); |
| if (obj2.isBool()) { |
| state->setTextKnockout(obj2.getBool()); |
| out->updateTextKnockout(state); |
| } |
| |
| // soft mask |
| obj2 = obj1.dictLookup("SMask"); |
| if (!obj2.isNull()) { |
| if (obj2.isName("None")) { |
| out->clearSoftMask(state); |
| } else if (obj2.isDict()) { |
| Object obj3 = obj2.dictLookup("S"); |
| if (obj3.isName("Alpha")) { |
| alpha = true; |
| } else { // "Luminosity" |
| alpha = false; |
| } |
| funcs[0] = nullptr; |
| obj3 = obj2.dictLookup("TR"); |
| if (!obj3.isNull()) { |
| if (obj3.isName("Default") || |
| obj3.isName("Identity")) { |
| funcs[0] = nullptr; |
| } else { |
| funcs[0] = Function::parse(&obj3); |
| if (funcs[0] == nullptr || |
| 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; |
| } |
| } |
| } |
| obj3 = obj2.dictLookup("BC"); |
| if ((haveBackdropColor = obj3.isArray())) { |
| for (i = 0; i < gfxColorMaxComps; ++i) { |
| backdropColor.c[i] = 0; |
| } |
| for (i = 0; i < obj3.arrayGetLength() && i < gfxColorMaxComps; ++i) { |
| Object obj4 = obj3.arrayGet(i); |
| if (obj4.isNum()) { |
| backdropColor.c[i] = dblToCol(obj4.getNum()); |
| } |
| } |
| } |
| obj3 = obj2.dictLookup("G"); |
| if (obj3.isStream()) { |
| Object obj4 = obj3.streamGetDict()->lookup("Group"); |
| if (obj4.isDict()) { |
| GfxColorSpace *blendingColorSpace = nullptr; |
| isolated = knockout = false; |
| Object obj5 = obj4.dictLookup("CS"); |
| if (!obj5.isNull()) { |
| blendingColorSpace = GfxColorSpace::parse(res, &obj5, out, state); |
| } |
| obj5 = obj4.dictLookup("I"); |
| if (obj5.isBool()) { |
| isolated = obj5.getBool(); |
| } |
| obj5 = obj4.dictLookup("K"); |
| if (obj5.isBool()) { |
| knockout = obj5.getBool(); |
| } |
| if (!haveBackdropColor) { |
| if (blendingColorSpace) { |
| blendingColorSpace->getDefaultColor(&backdropColor); |
| } else { |
| //~ need to get the parent or default color space (?) |
| for (i = 0; i < gfxColorMaxComps; ++i) { |
| backdropColor.c[i] = 0; |
| } |
| } |
| } |
| doSoftMask(&obj3, alpha, blendingColorSpace, |
| isolated, knockout, funcs[0], &backdropColor); |
| delete blendingColorSpace; |
| } else { |
| error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState - missing group"); |
| } |
| } else { |
| error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState - missing group"); |
| } |
| delete funcs[0]; |
| } else if (!obj2.isNull()) { |
| error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState"); |
| } |
| } |
| obj2 = obj1.dictLookup("Font"); |
| if (obj2.isArray()) { |
| GfxFont *font; |
| if (obj2.arrayGetLength() == 2) { |
| const Object &fargs0 = obj2.arrayGetNF(0); |
| Object fargs1 = obj2.arrayGet(1); |
| if (fargs0.isRef() && fargs1.isNum()) { |
| Object fobj = fargs0.fetch(xref); |
| if (fobj.isDict()) { |
| Ref r = fargs0.getRef(); |
| font = GfxFont::makeFont(xref,args[0].getName(),r,fobj.getDict()); |
| state->setFont(font,fargs1.getNum()); |
| fontChanged = true; |
| } |
| } |
| } else { |
| error(errSyntaxError, getPos(), "Number of args mismatch for /Font in ExtGState"); |
| } |
| } |
| obj2 = obj1.dictLookup("LW"); |
| if (obj2.isNum()) { |
| opSetLineWidth(&obj2,1); |
| } |
| obj2 = obj1.dictLookup("LC"); |
| if (obj2.isInt()) { |
| opSetLineCap(&obj2,1); |
| } |
| obj2 = obj1.dictLookup("LJ"); |
| if (obj2.isInt()) { |
| opSetLineJoin(&obj2,1); |
| } |
| obj2 = obj1.dictLookup("ML"); |
| if (obj2.isNum()) { |
| opSetMiterLimit(&obj2,1); |
| } |
| obj2 = obj1.dictLookup("D"); |
| if (obj2.isArray()) { |
| if (obj2.arrayGetLength() == 2) { |
| Object dargs[2]; |
| |
| dargs[0] = obj2.arrayGetNF(0).copy(); |
| dargs[1] = obj2.arrayGet(1); |
| if (dargs[0].isArray() && dargs[1].isInt()) { |
| opSetDash(dargs,2); |
| } |
| } else { |
| error(errSyntaxError, getPos(), "Number of args mismatch for /D in ExtGState"); |
| } |
| } |
| obj2 = obj1.dictLookup("RI"); |
| if (obj2.isName()) { |
| opSetRenderingIntent(&obj2,1); |
| } |
| obj2 = obj1.dictLookup("FL"); |
| if (obj2.isNum()) { |
| opSetFlat(&obj2,1); |
| } |
| } |
| |
| void Gfx::doSoftMask(Object *str, bool alpha, |
| GfxColorSpace *blendingColorSpace, |
| bool isolated, bool knockout, |
| Function *transferFunc, GfxColor *backdropColor) { |
| Dict *dict, *resDict; |
| double m[6], bbox[4]; |
| Object obj1; |
| int i; |
| |
| // check for excessive recursion |
| if (formDepth > 20) { |
| return; |
| } |
| |
| // get stream dict |
| dict = str->streamGetDict(); |
| |
| // check form type |
| obj1 = dict->lookup("FormType"); |
| if (!(obj1.isNull() || (obj1.isInt() && obj1.getInt() == 1))) { |
| error(errSyntaxError, getPos(), "Unknown form type"); |
| } |
| |
| // get bounding box |
| obj1 = dict->lookup("BBox"); |
| if (!obj1.isArray()) { |
| error(errSyntaxError, getPos(), "Bad form bounding box"); |
| return; |
| } |
| for (i = 0; i < 4; ++i) { |
| Object obj2 = obj1.arrayGet(i); |
| if (likely(obj2.isNum())) bbox[i] = obj2.getNum(); |
| else { |
| error(errSyntaxError, getPos(), "Bad form bounding box (non number)"); |
| return; |
| } |
| } |
| |
| // get matrix |
| obj1 = dict->lookup("Matrix"); |
| if (obj1.isArray()) { |
| for (i = 0; i < 6; ++i) { |
| Object obj2 = obj1.arrayGet(i); |
| if (likely(obj2.isNum())) m[i] = obj2.getNum(); |
| else m[i] = 0; |
| } |
| } else { |
| m[0] = 1; m[1] = 0; |
| m[2] = 0; m[3] = 1; |
| m[4] = 0; m[5] = 0; |
| } |
| |
| // get resources |
| obj1 = dict->lookup("Resources"); |
| resDict = obj1.isDict() ? obj1.getDict() : nullptr; |
| |
| // draw it |
| ++formDepth; |
| drawForm(str, resDict, m, bbox, true, true, |
| blendingColorSpace, isolated, knockout, |
| alpha, transferFunc, backdropColor); |
| --formDepth; |
| } |
| |
| void Gfx::opSetRenderingIntent(Object args[], int numArgs) { |
| state->setRenderingIntent(args[0].getName()); |
| } |
| |
| //------------------------------------------------------------------------ |
| // color operators |
| //------------------------------------------------------------------------ |
| |
| void Gfx::opSetFillGray(Object args[], int numArgs) { |
| GfxColor color; |
| GfxColorSpace *colorSpace = nullptr; |
| |
| state->setFillPattern(nullptr); |
| Object obj = res->lookupColorSpace("DefaultGray"); |
| if (!obj.isNull()) { |
| colorSpace = GfxColorSpace::parse(res, &obj, out, state); |
| } |
| if (colorSpace == nullptr) { |
| colorSpace = new GfxDeviceGrayColorSpace(); |
| } |
| state->setFillColorSpace(colorSpace); |
| out->updateFillColorSpace(state); |
| color.c[0] = dblToCol(args[0].getNum()); |
| state->setFillColor(&color); |
| out->updateFillColor(state); |
| } |
| |
| void Gfx::opSetStrokeGray(Object args[], int numArgs) { |
| GfxColor color; |
| GfxColorSpace *colorSpace = nullptr; |
| |
| state->setStrokePattern(nullptr); |
| Object obj = res->lookupColorSpace("DefaultGray"); |
| if (!obj.isNull()) { |
| colorSpace = GfxColorSpace::parse(res, &obj, out, state); |
| } |
| if (colorSpace == nullptr) { |
| colorSpace = new GfxDeviceGrayColorSpace(); |
| } |
| state->setStrokeColorSpace(colorSpace); |
| out->updateStrokeColorSpace(state); |
| color.c[0] = dblToCol(args[0].getNum()); |
| state->setStrokeColor(&color); |
| out->updateStrokeColor(state); |
| } |
| |
| void Gfx::opSetFillCMYKColor(Object args[], int numArgs) { |
| GfxColor color; |
| GfxColorSpace *colorSpace = nullptr; |
| int i; |
| |
| Object obj = res->lookupColorSpace("DefaultCMYK"); |
| if (!obj.isNull()) { |
| colorSpace = GfxColorSpace::parse(res, &obj, out, state); |
| } |
| if (colorSpace == nullptr) { |
| colorSpace = new GfxDeviceCMYKColorSpace(); |
| } |
| state->setFillPattern(nullptr); |
| state->setFillColorSpace(colorSpace); |
| out->updateFillColorSpace(state); |
| for (i = 0; i < 4; ++i) { |
| color.c[i] = dblToCol(args[i].getNum()); |
| } |
| state->setFillColor(&color); |
| out->updateFillColor(state); |
| } |
| |
| void Gfx::opSetStrokeCMYKColor(Object args[], int numArgs) { |
| GfxColor color; |
| GfxColorSpace *colorSpace = nullptr; |
| int i; |
| |
| state->setStrokePattern(nullptr); |
| Object obj = res->lookupColorSpace("DefaultCMYK"); |
| if (!obj.isNull()) { |
| colorSpace = GfxColorSpace::parse(res, &obj, out, state); |
| } |
| if (colorSpace == nullptr) { |
| colorSpace = new GfxDeviceCMYKColorSpace(); |
| } |
| state->setStrokeColorSpace(colorSpace); |
| out->updateStrokeColorSpace(state); |
| for (i = 0; i < 4; ++i) { |
| color.c[i] = dblToCol(args[i].getNum()); |
| } |
| state->setStrokeColor(&color); |
| out->updateStrokeColor(state); |
| } |
| |
| void Gfx::opSetFillRGBColor(Object args[], int numArgs) { |
| GfxColorSpace *colorSpace = nullptr; |
| GfxColor color; |
| int i; |
| |
| state->setFillPattern(nullptr); |
| Object obj = res->lookupColorSpace("DefaultRGB"); |
| if (!obj.isNull()) { |
| colorSpace = GfxColorSpace::parse(res, &obj, out, state); |
| } |
| if (colorSpace == nullptr) { |
| colorSpace = new GfxDeviceRGBColorSpace(); |
| } |
| state->setFillColorSpace(colorSpace); |
| out->updateFillColorSpace(state); |
| for (i = 0; i < 3; ++i) { |
| color.c[i] = dblToCol(args[i].getNum()); |
| } |
| state->setFillColor(&color); |
| out->updateFillColor(state); |
| } |
| |
| void Gfx::opSetStrokeRGBColor(Object args[], int numArgs) { |
| GfxColorSpace *colorSpace = nullptr; |
| GfxColor color; |
| int i; |
| |
| state->setStrokePattern(nullptr); |
| Object obj = res->lookupColorSpace("DefaultRGB"); |
| if (!obj.isNull()) { |
| colorSpace = GfxColorSpace::parse(res, &obj, out, state); |
| } |
| if (colorSpace == nullptr) { |
| colorSpace = new GfxDeviceRGBColorSpace(); |
| } |
| state->setStrokeColorSpace(colorSpace); |
| out->updateStrokeColorSpace(state); |
| for (i = 0; i < 3; ++i) { |
| color.c[i] = dblToCol(args[i].getNum()); |
| } |
| state->setStrokeColor(&color); |
| out->updateStrokeColor(state); |
| } |
| |
| void Gfx::opSetFillColorSpace(Object args[], int numArgs) { |
| GfxColorSpace *colorSpace; |
| GfxColor color; |
| |
| Object obj = res->lookupColorSpace(args[0].getName()); |
| if (obj.isNull()) { |
| colorSpace = GfxColorSpace::parse(res, &args[0], out, state); |
| } else { |
| colorSpace = GfxColorSpace::parse(res, &obj, out, state); |
| } |
| if (colorSpace) { |
| state->setFillPattern(nullptr); |
| state->setFillColorSpace(colorSpace); |
| out->updateFillColorSpace(state); |
| colorSpace->getDefaultColor(&color); |
| state->setFillColor(&color); |
| out->updateFillColor(state); |
| } else { |
| error(errSyntaxError, getPos(), "Bad color space (fill)"); |
| } |
| } |
| |
| void Gfx::opSetStrokeColorSpace(Object args[], int numArgs) { |
| GfxColorSpace *colorSpace; |
| GfxColor color; |
| |
| state->setStrokePattern(nullptr); |
| Object obj = res->lookupColorSpace(args[0].getName()); |
| if (obj.isNull()) { |
| colorSpace = GfxColorSpace::parse(res, &args[0], out, state); |
| } else { |
| colorSpace = GfxColorSpace::parse(res, &obj, out, state); |
| } |
| if (colorSpace) { |
| state->setStrokeColorSpace(colorSpace); |
| out->updateStrokeColorSpace(state); |
| colorSpace->getDefaultColor(&color); |
| state->setStrokeColor(&color); |
| out->updateStrokeColor(state); |
| } else { |
| error(errSyntaxError, getPos(), "Bad color space (stroke)"); |
| } |
| } |
| |
| void Gfx::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); |
| out->updateFillColor(state); |
| } |
| |
| void Gfx::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); |
| out->updateStrokeColor(state); |
| } |
| |
| void Gfx::opSetFillColorN(Object args[], int numArgs) { |
| GfxColor color; |
| GfxPattern *pattern; |
| 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()); |
| } else { |
| color.c[i] = 0; // TODO Investigate if this is what Adobe does |
| } |
| } |
| state->setFillColor(&color); |
| out->updateFillColor(state); |
| } |
| if (numArgs > 0) { |
| if (args[numArgs-1].isName() && |
| (pattern = res->lookupPattern(args[numArgs-1].getName(), out, state))) { |
| state->setFillPattern(pattern); |
| } |
| } |
| |
| } 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()); |
| } else { |
| color.c[i] = 0; // TODO Investigate if this is what Adobe does |
| } |
| } |
| state->setFillColor(&color); |
| out->updateFillColor(state); |
| } |
| } |
| |
| void Gfx::opSetStrokeColorN(Object args[], int numArgs) { |
| GfxColor color; |
| GfxPattern *pattern; |
| 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()); |
| } else { |
| color.c[i] = 0; // TODO Investigate if this is what Adobe does |
| } |
| } |
| state->setStrokeColor(&color); |
| out->updateStrokeColor(state); |
| } |
| if (unlikely(numArgs <= 0)) { |
| error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SCN' command"); |
| return; |
| } |
| if (args[numArgs-1].isName() && |
| (pattern = res->lookupPattern(args[numArgs-1].getName(), out, state))) { |
| state->setStrokePattern(pattern); |
| } |
| |
| } 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()); |
| } else { |
| color.c[i] = 0; // TODO Investigate if this is what Adobe does |
| } |
| } |
| state->setStrokeColor(&color); |
| out->updateStrokeColor(state); |
| } |
| } |
| |
| //------------------------------------------------------------------------ |
| // path segment operators |
| //------------------------------------------------------------------------ |
| |
| void Gfx::opMoveTo(Object args[], int numArgs) { |
| state->moveTo(args[0].getNum(), args[1].getNum()); |
| } |
| |
| void Gfx::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()); |
| } |
| |
| void Gfx::opCurveTo(Object args[], int numArgs) { |
| double x1, y1, x2, y2, x3, y3; |
| |
| if (!state->isCurPt()) { |
| error(errSyntaxError, getPos(), "No current point in curveto"); |
| return; |
| } |
| x1 = args[0].getNum(); |
| y1 = args[1].getNum(); |
| x2 = args[2].getNum(); |
| y2 = args[3].getNum(); |
| x3 = args[4].getNum(); |
| y3 = args[5].getNum(); |
| state->curveTo(x1, y1, x2, y2, x3, y3); |
| } |
| |
| void Gfx::opCurveTo1(Object args[], int numArgs) { |
| double x1, y1, x2, y2, x3, y3; |
| |
| if (!state->isCurPt()) { |
| error(errSyntaxError, getPos(), "No current point in curveto1"); |
| return; |
| } |
| x1 = state->getCurX(); |
| y1 = state->getCurY(); |
| x2 = args[0].getNum(); |
| y2 = args[1].getNum(); |
| x3 = args[2].getNum(); |
| y3 = args[3].getNum(); |
| state->curveTo(x1, y1, x2, y2, x3, y3); |
| } |
| |
| void Gfx::opCurveTo2(Object args[], int numArgs) { |
| double x1, y1, x2, y2, x3, y3; |
| |
| if (!state->isCurPt()) { |
| error(errSyntaxError, getPos(), "No current point in curveto2"); |
| return; |
| } |
| x1 = args[0].getNum(); |
| y1 = args[1].getNum(); |
| x2 = args[2].getNum(); |
| y2 = args[3].getNum(); |
| x3 = x2; |
| y3 = y2; |
| state->curveTo(x1, y1, x2, y2, x3, y3); |
| } |
| |
| void Gfx::opRectangle(Object args[], int numArgs) { |
| double x, y, w, h; |
| |
| x = args[0].getNum(); |
| y = args[1].getNum(); |
| w = args[2].getNum(); |
| 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 Gfx::opClosePath(Object args[], int numArgs) { |
| if (!state->isCurPt()) { |
| error(errSyntaxError, getPos(), "No current point in closepath"); |
| return; |
| } |
| state->closePath(); |
| } |
| |
| //------------------------------------------------------------------------ |
| // path painting operators |
| //------------------------------------------------------------------------ |
| |
| void Gfx::opEndPath(Object args[], int numArgs) { |
| doEndPath(); |
| } |
| |
| void Gfx::opStroke(Object args[], int numArgs) { |
| if (!state->isCurPt()) { |
| //error(errSyntaxError, getPos(), "No path in stroke"); |
| return; |
| } |
| if (state->isPath()) { |
| if (ocState) { |
| if (state->getStrokeColorSpace()->getMode() == csPattern) { |
| doPatternStroke(); |
| } else { |
| out->stroke(state); |
| } |
| } |
| } |
| doEndPath(); |
| } |
| |
| void Gfx::opCloseStroke(Object * /*args[]*/, int /*numArgs*/) { |
| if (!state->isCurPt()) { |
| //error(errSyntaxError, getPos(), "No path in closepath/stroke"); |
| return; |
| } |
| if (state->isPath()) { |
| state->closePath(); |
| if (ocState) { |
| if (state->getStrokeColorSpace()->getMode() == csPattern) { |
| doPatternStroke(); |
| } else { |
| out->stroke(state); |
| } |
| } |
| } |
| doEndPath(); |
| } |
| |
| void Gfx::opFill(Object args[], int numArgs) { |
| if (!state->isCurPt()) { |
| //error(errSyntaxError, getPos(), "No path in fill"); |
| return; |
| } |
| if (state->isPath()) { |
| if (ocState) { |
| if (state->getFillColorSpace()->getMode() == csPattern) { |
| doPatternFill(false); |
| } else { |
| out->fill(state); |
| } |
| } |
| } |
| doEndPath(); |
| } |
| |
| void Gfx::opEOFill(Object args[], int numArgs) { |
| if (!state->isCurPt()) { |
| //error(errSyntaxError, getPos(), "No path in eofill"); |
| return; |
| } |
| if (state->isPath()) { |
| if (ocState) { |
| if (state->getFillColorSpace()->getMode() == csPattern) { |
| doPatternFill(true); |
| } else { |
| out->eoFill(state); |
| } |
| } |
| } |
| doEndPath(); |
| } |
| |
| void Gfx::opFillStroke(Object args[], int numArgs) { |
| if (!state->isCurPt()) { |
| //error(errSyntaxError, getPos(), "No path in fill/stroke"); |
| return; |
| } |
| if (state->isPath()) { |
| if (ocState) { |
| if (state->getFillColorSpace()->getMode() == csPattern) { |
| doPatternFill(false); |
| } else { |
| out->fill(state); |
| } |
| if (state->getStrokeColorSpace()->getMode() == csPattern) { |
| doPatternStroke(); |
| } else { |
| out->stroke(state); |
| } |
| } |
| } |
| doEndPath(); |
| } |
| |
| void Gfx::opCloseFillStroke(Object args[], int numArgs) { |
| if (!state->isCurPt()) { |
| //error(errSyntaxError, getPos(), "No path in closepath/fill/stroke"); |
| return; |
| } |
| if (state->isPath()) { |
| state->closePath(); |
| if (ocState) { |
| if (state->getFillColorSpace()->getMode() == csPattern) { |
| doPatternFill(false); |
| } else { |
| out->fill(state); |
| } |
| if (state->getStrokeColorSpace()->getMode() == csPattern) { |
| doPatternStroke(); |
| } else { |
| out->stroke(state); |
| } |
| } |
| } |
| doEndPath(); |
| } |
| |
| void Gfx::opEOFillStroke(Object args[], int numArgs) { |
| if (!state->isCurPt()) { |
| //error(errSyntaxError, getPos(), "No path in eofill/stroke"); |
| return; |
| } |
| if (state->isPath()) { |
| if (ocState) { |
| if (state->getFillColorSpace()->getMode() == csPattern) { |
| doPatternFill(true); |
| } else { |
| out->eoFill(state); |
| } |
| if (state->getStrokeColorSpace()->getMode() == csPattern) { |
| doPatternStroke(); |
| } else { |
| out->stroke(state); |
| } |
| } |
| } |
| doEndPath(); |
| } |
| |
| void Gfx::opCloseEOFillStroke(Object args[], int numArgs) { |
| if (!state->isCurPt()) { |
| //error(errSyntaxError, getPos(), "No path in closepath/eofill/stroke"); |
| return; |
| } |
| if (state->isPath()) { |
| state->closePath(); |
| if (ocState) { |
| if (state->getFillColorSpace()->getMode() == csPattern) { |
| doPatternFill(true); |
| } else { |
| out->eoFill(state); |
| } |
| if (state->getStrokeColorSpace()->getMode() == csPattern) { |
| doPatternStroke(); |
| } else { |
| out->stroke(state); |
| } |
| } |
| } |
| doEndPath(); |
| } |
| |
| void Gfx::doPatternFill(bool eoFill) { |
| GfxPattern *pattern; |
| |
| // this is a bit of a kludge -- patterns can be really slow, so we |
| // skip them if we're only doing text extraction, since they almost |
| // certainly don't contain any text |
| if (!out->needNonText()) { |
| return; |
| } |
| |
| if (!(pattern = state->getFillPattern())) { |
| return; |
| } |
| switch (pattern->getType()) { |
| case 1: |
| doTilingPatternFill((GfxTilingPattern *)pattern, false, eoFill, false); |
| break; |
| case 2: |
| doShadingPatternFill((GfxShadingPattern *)pattern, false, eoFill, false); |
| break; |
| default: |
| error(errSyntaxError, getPos(), "Unknown pattern type ({0:d}) in fill", |
| pattern->getType()); |
| break; |
| } |
| } |
| |
| void Gfx::doPatternStroke() { |
| GfxPattern *pattern; |
| |
| // this is a bit of a kludge -- patterns can be really slow, so we |
| // skip them if we're only doing text extraction, since they almost |
| // certainly don't contain any text |
| if (!out->needNonText()) { |
| return; |
| } |
| |
| if (!(pattern = state->getStrokePattern())) { |
| return; |
| } |
| switch (pattern->getType()) { |
| case 1: |
| doTilingPatternFill((GfxTilingPattern *)pattern, true, false, false); |
| break; |
| case 2: |
| doShadingPatternFill((GfxShadingPattern *)pattern, true, false, false); |
| break; |
| default: |
| error(errSyntaxError, getPos(), "Unknown pattern type ({0:d}) in stroke", |
| pattern->getType()); |
| break; |
| } |
| } |
| |
| void Gfx::doPatternText() { |
| GfxPattern *pattern; |
| |
| // this is a bit of a kludge -- patterns can be really slow, so we |
| // skip them if we're only doing text extraction, since they almost |
| // certainly don't contain any text |
| if (!out->needNonText()) { |
| return; |
| } |
| |
| if (!(pattern = state->getFillPattern())) { |
| return; |
| } |
| switch (pattern->getType()) { |
| case 1: |
| doTilingPatternFill((GfxTilingPattern *)pattern, false, false, true); |
| break; |
| case 2: |
| doShadingPatternFill((GfxShadingPattern *)pattern, false, false, true); |
| break; |
| default: |
| error(errSyntaxError, getPos(), "Unknown pattern type ({0:d}) in fill", |
| pattern->getType()); |
| break; |
| } |
| } |
| |
| void Gfx::doPatternImageMask(Object *ref, Stream *str, int width, int height, |
| bool invert, bool inlineImg) { |
| saveState(); |
| |
| out->setSoftMaskFromImageMask(state, ref, str, |
| width, height, invert, inlineImg, baseMatrix); |
| |
| state->clearPath(); |
| state->moveTo(0, 0); |
| state->lineTo(1, 0); |
| state->lineTo(1, 1); |
| state->lineTo(0, 1); |
| state->closePath(); |
| doPatternText(); |
| |
| out->unsetSoftMaskFromImageMask(state, baseMatrix); |
| restoreState(); |
| } |
| |
| void Gfx::doTilingPatternFill(GfxTilingPattern *tPat, |
| bool stroke, bool eoFill, bool text) { |
| GfxPatternColorSpace *patCS; |
| GfxColorSpace *cs; |
| GfxColor color; |
| GfxState *savedState; |
| double xMin, yMin, xMax, yMax, x, y, x1, y1; |
| double cxMin, cyMin, cxMax, cyMax; |
| int xi0, yi0, xi1, yi1, xi, yi; |
| const double *ctm, *btm, *ptm; |
| double m[6], ictm[6], m1[6], imb[6]; |
| double det; |
| double xstep, ystep; |
| int i; |
| |
| // get color space |
| patCS = (GfxPatternColorSpace *)(stroke ? state->getStrokeColorSpace() |
| : state->getFillColorSpace()); |
| |
| // construct a (pattern space) -> (current space) transform matrix |
| ctm = state->getCTM(); |
| btm = baseMatrix; |
| ptm = tPat->getMatrix(); |
| // iCTM = invert CTM |
| det = ctm[0] * ctm[3] - ctm[1] * ctm[2]; |
| if (fabs(det) < 0.000001) { |
| error(errSyntaxError, getPos(), "Singular matrix in tiling pattern fill"); |
| return; |
| } |
| det = 1 / det; |
| ictm[0] = ctm[3] * det; |
| ictm[1] = -ctm[1] * det; |
| ictm[2] = -ctm[2] * det; |
| ictm[3] = ctm[0] * det; |
| ictm[4] = (ctm[2] * ctm[5] - ctm[3] * ctm[4]) * det; |
| ictm[5] = (ctm[1] * ctm[4] - ctm[0] * ctm[5]) * det; |
| // m1 = PTM * BTM = PTM * base transform matrix |
| m1[0] = ptm[0] * btm[0] + ptm[1] * btm[2]; |
| m1[1] = ptm[0] * btm[1] + ptm[1] * btm[3]; |
| m1[2] = ptm[2] * btm[0] + ptm[3] * btm[2]; |
| m1[3] = ptm[2] * btm[1] + ptm[3] * btm[3]; |
| m1[4] = ptm[4] * btm[0] + ptm[5] * btm[2] + btm[4]; |
| m1[5] = ptm[4] * btm[1] + ptm[5] * btm[3] + btm[5]; |
| // m = m1 * iCTM = (PTM * BTM) * (iCTM) |
| m[0] = m1[0] * ictm[0] + m1[1] * ictm[2]; |
| m[1] = m1[0] * ictm[1] + m1[1] * ictm[3]; |
| m[2] = m1[2] * ictm[0] + m1[3] * ictm[2]; |
| m[3] = m1[2] * ictm[1] + m1[3] * ictm[3]; |
| m[4] = m1[4] * ictm[0] + m1[5] * ictm[2] + ictm[4]; |
| m[5] = m1[4] * ictm[1] + m1[5] * ictm[3] + ictm[5]; |
| |
| // construct a (device space) -> (pattern space) transform matrix |
| det = m1[0] * m1[3] - m1[1] * m1[2]; |
| if (fabs(det) < 0.000001) { |
| error(errSyntaxError, getPos(), "Singular matrix in tiling pattern fill"); |
| return; |
| } |
| det = 1 / det; |
| imb[0] = m1[3] * det; |
| imb[1] = -m1[1] * det; |
| imb[2] = -m1[2] * det; |
| imb[3] = m1[0] * det; |
| imb[4] = (m1[2] * m1[5] - m1[3] * m1[4]) * det; |
| imb[5] = (m1[1] * m1[4] - m1[0] * m1[5]) * det; |
| |
| // save current graphics state |
| savedState = saveStateStack(); |
| |
| // set underlying color space (for uncolored tiling patterns); set |
| // various other parameters (stroke color, line width) to match |
| // Adobe's behavior |
| state->setFillPattern(nullptr); |
| state->setStrokePattern(nullptr); |
| if (tPat->getPaintType() == 2 && (cs = patCS->getUnder())) { |
| state->setFillColorSpace(cs->copy()); |
| out->updateFillColorSpace(state); |
| state->setStrokeColorSpace(cs->copy()); |
| out->updateStrokeColorSpace(state); |
| if (stroke) { |
| state->setFillColor(state->getStrokeColor()); |
| } else { |
| state->setStrokeColor(state->getFillColor()); |
| } |
| out->updateFillColor(state); |
| out->updateStrokeColor(state); |
| } else { |
| cs = new GfxDeviceGrayColorSpace(); |
| state->setFillColorSpace(cs); |
| cs->getDefaultColor(&color); |
| state->setFillColor(&color); |
| out->updateFillColorSpace(state); |
| state->setStrokeColorSpace(new GfxDeviceGrayColorSpace()); |
| state->setStrokeColor(&color); |
| out->updateStrokeColorSpace(state); |
| } |
| if (!stroke) { |
| state->setLineWidth(0); |
| out->updateLineWidth(state); |
| } |
| |
| // clip to current path |
| if (stroke) { |
| state->clipToStrokePath(); |
| out->clipToStrokePath(state); |
| } else if (!text) { |
| state->clip(); |
| if (eoFill) { |
| out->eoClip(state); |
| } else { |
| out->clip(state); |
| } |
| } |
| state->clearPath(); |
| |
| // get the clip region, check for empty |
| state->getClipBBox(&cxMin, &cyMin, &cxMax, &cyMax); |
| if (cxMin > cxMax || cyMin > cyMax) { |
| goto restore; |
| } |
| |
| // transform clip region bbox to pattern space |
| xMin = xMax = cxMin * imb[0] + cyMin * imb[2] + imb[4]; |
| yMin = yMax = cxMin * imb[1] + cyMin * imb[3] + imb[5]; |
| x1 = cxMin * imb[0] + cyMax * imb[2] + imb[4]; |
| y1 = cxMin * imb[1] + cyMax * imb[3] + imb[5]; |
| if (x1 < xMin) { |
| xMin = x1; |
| } else if (x1 > xMax) { |
| xMax = x1; |
| } |
| if (y1 < yMin) { |
| yMin = y1; |
| } else if (y1 > yMax) { |
| yMax = y1; |
| } |
| x1 = cxMax * imb[0] + cyMin * imb[2] + imb[4]; |
| y1 = cxMax * imb[1] + cyMin * imb[3] + imb[5]; |
| if (x1 < xMin) { |
| xMin = x1; |
| } else if (x1 > xMax) { |
| xMax = x1; |
| } |
| if (y1 < yMin) { |
| yMin = y1; |
| } else if (y1 > yMax) { |
| yMax = y1; |
| } |
| x1 = cxMax * imb[0] + cyMax * imb[2] + imb[4]; |
| y1 = cxMax * imb[1] + cyMax * imb[3] + imb[5]; |
| if (x1 < xMin) { |
| xMin = x1; |
| } else if (x1 > xMax) { |
| xMax = x1; |
| } |
| if (y1 < yMin) { |
| yMin = y1; |
| } else if (y1 > yMax) { |
| yMax = y1; |
| } |
| |
| // draw the pattern |
| //~ this should treat negative steps differently -- start at right/top |
| //~ edge instead of left/bottom (?) |
| xstep = fabs(tPat->getXStep()); |
| ystep = fabs(tPat->getYStep()); |
| if (unlikely(xstep == 0 || ystep == 0)) { |
| goto restore; |
| } |
| if (tPat->getBBox()[0] < tPat->getBBox()[2]) { |
| xi0 = (int)ceil((xMin - tPat->getBBox()[2]) / xstep); |
| xi1 = (int)floor((xMax - tPat->getBBox()[0]) / xstep) + 1; |
| } else { |
| xi0 = (int)ceil((xMin - tPat->getBBox()[0]) / xstep); |
| xi1 = (int)floor((xMax - tPat->getBBox()[2]) / xstep) + 1; |
| } |
| if (tPat->getBBox()[1] < tPat->getBBox()[3]) { |
| yi0 = (int)ceil((yMin - tPat->getBBox()[3]) / ystep); |
| yi1 = (int)floor((yMax - tPat->getBBox()[1]) / ystep) + 1; |
| } else { |
| yi0 = (int)ceil((yMin - tPat->getBBox()[1]) / ystep); |
| yi1 = (int)floor((yMax - tPat->getBBox()[3]) / ystep) + 1; |
| } |
| for (i = 0; i < 4; ++i) { |
| m1[i] = m[i]; |
| } |
| m1[4] = m[4]; |
| m1[5] = m[5]; |
| { |
| bool shouldDrawPattern = true; |
| std::set<int>::iterator patternRefIt; |
| const int patternRefNum = tPat->getPatternRefNum(); |
| if (patternRefNum != -1) { |
| if (formsDrawing.find(patternRefNum) == formsDrawing.end()) { |
| patternRefIt = formsDrawing.insert(patternRefNum).first; |
| } else { |
| shouldDrawPattern = false; |
| } |
| } |
| if (shouldDrawPattern) { |
| if (out->useTilingPatternFill() && |
| out->tilingPatternFill(state, this, catalog, tPat->getContentStream(), |
| tPat->getMatrix(), tPat->getPaintType(), tPat->getTilingType(), |
| tPat->getResDict(), m1, tPat->getBBox(), |
| xi0, yi0, xi1, yi1, xstep, ystep)) { |
| // do nothing |
| } else { |
| out->updatePatternOpacity(state); |
| for (yi = yi0; yi < yi1; ++yi) { |
| for (xi = xi0; xi < xi1; ++xi) { |
| x = xi * xstep; |
| y = yi * ystep; |
| m1[4] = x * m[0] + y * m[2] + m[4]; |
| m1[5] = x * m[1] + y * m[3] + m[5]; |
| drawForm(tPat->getContentStream(), tPat->getResDict(), |
| m1, tPat->getBBox()); |
| } |
| } |
| out->clearPatternOpacity(state); |
| } |
| if (patternRefNum != -1) { |
| formsDrawing.erase(patternRefIt); |
| } |
| } |
| } |
| |
| // restore graphics state |
| restore: |
| restoreStateStack(savedState); |
| } |
| |
| void Gfx::doShadingPatternFill(GfxShadingPattern *sPat, |
| bool stroke, bool eoFill, bool text) { |
| GfxShading *shading; |
| GfxState *savedState; |
| const double *ctm, *btm, *ptm; |
| double m[6], ictm[6], m1[6]; |
| double xMin, yMin, xMax, yMax; |
| double det; |
| |
| shading = sPat->getShading(); |
| |
| // save current graphics state |
| savedState = saveStateStack(); |
| |
| // clip to current path |
| if (stroke) { |
| state->clipToStrokePath(); |
| out->clipToStrokePath(state); |
| } else if (!text) { |
| state->clip(); |
| if (eoFill) { |
| out->eoClip(state); |
| } else { |
| out->clip(state); |
| } |
| } |
| state->clearPath(); |
| |
| // construct a (pattern space) -> (current space) transform matrix |
| ctm = state->getCTM(); |
| btm = baseMatrix; |
| ptm = sPat->getMatrix(); |
| // iCTM = invert CTM |
| det = ctm[0] * ctm[3] - ctm[1] * ctm[2]; |
| if (fabs(det) < 0.000001) { |
| error(errSyntaxError, getPos(), "Singular matrix in shading pattern fill"); |
| restoreStateStack(savedState); |
| return; |
| } |
| det = 1 / det; |
| ictm[0] = ctm[3] * det; |
| ictm[1] = -ctm[1] * det; |
| ictm[2] = -ctm[2] * det; |
| ictm[3] = ctm[0] * det; |
| ictm[4] = (ctm[2] * ctm[5] - ctm[3] * ctm[4]) * det; |
| ictm[5] = (ctm[1] * ctm[4] - ctm[0] * ctm[5]) * det; |
| // m1 = PTM * BTM = PTM * base transform matrix |
| m1[0] = ptm[0] * btm[0] + ptm[1] * btm[2]; |
| m1[1] = ptm[0] * btm[1] + ptm[1] * btm[3]; |
| m1[2] = ptm[2] * btm[0] + ptm[3] * btm[2]; |
| m1[3] = ptm[2] * btm[1] + ptm[3] * btm[3]; |
| m1[4] = ptm[4] * btm[0] + ptm[5] * btm[2] + btm[4]; |
| m1[5] = ptm[4] * btm[1] + ptm[5] * btm[3] + btm[5]; |
| // m = m1 * iCTM = (PTM * BTM) * (iCTM) |
| m[0] = m1[0] * ictm[0] + m1[1] * ictm[2]; |
| m[1] = m1[0] * ictm[1] + m1[1] * ictm[3]; |
| m[2] = m1[2] * ictm[0] + m1[3] * ictm[2]; |
| m[3] = m1[2] * ictm[1] + m1[3] * ictm[3]; |
| m[4] = m1[4] * ictm[0] + m1[5] * ictm[2] + ictm[4]; |
| m[5] = m1[4] * ictm[1] + m1[5] * ictm[3] + ictm[5]; |
| |
| // set the new matrix |
| state->concatCTM(m[0], m[1], m[2], m[3], m[4], m[5]); |
| out->updateCTM(state, m[0], m[1], m[2], m[3], m[4], m[5]); |
| |
| // clip to bbox |
| if (shading->getHasBBox()) { |
| 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(); |
| out->clip(state); |
| state->clearPath(); |
| } |
| |
| // set the color space |
| state->setFillColorSpace(shading->getColorSpace()->copy()); |
| out->updateFillColorSpace(state); |
| |
| // background color fill |
| if (shading->getHasBackground()) { |
| state->setFillColor(shading->getBackground()); |
| out->updateFillColor(state); |
| state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax); |
| state->moveTo(xMin, yMin); |
| state->lineTo(xMax, yMin); |
| state->lineTo(xMax, yMax); |
| state->lineTo(xMin, yMax); |
| state->closePath(); |
| out->fill(state); |
| state->clearPath(); |
| } |
| |
| #if 1 //~tmp: turn off anti-aliasing temporarily |
| bool vaa = out->getVectorAntialias(); |
| if (vaa) { |
| out->setVectorAntialias(false); |
| } |
| #endif |
| |
| // do shading type-specific operations |
| switch (shading->getType()) { |
| case 1: |
| doFunctionShFill((GfxFunctionShading *)shading); |
| break; |
| case 2: |
| doAxialShFill((GfxAxialShading *)shading); |
| break; |
| case 3: |
| doRadialShFill((GfxRadialShading *)shading); |
| break; |
| case 4: |
| case 5: |
| doGouraudTriangleShFill((GfxGouraudTriangleShading *)shading); |
| break; |
| case 6: |
| case 7: |
| doPatchMeshShFill((GfxPatchMeshShading *)shading); |
| break; |
| } |
| |
| #if 1 //~tmp: turn off anti-aliasing temporarily |
| if (vaa) { |
| out->setVectorAntialias(true); |
| } |
| #endif |
| |
| // restore graphics state |
| restoreStateStack(savedState); |
| } |
| |
| void Gfx::opShFill(Object args[], int numArgs) { |
| GfxShading *shading; |
| GfxState *savedState; |
| double xMin, yMin, xMax, yMax; |
| |
| if (!ocState) { |
| return; |
| } |
| |
| if (!(shading = res->lookupShading(args[0].getName(), out, state))) { |
| return; |
| } |
| |
| // save current graphics state |
| savedState = saveStateStack(); |
| |
| // clip to bbox |
| if (shading->getHasBBox()) { |
| 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(); |
| out->clip(state); |
| state->clearPath(); |
| } |
| |
| // set the color space |
| state->setFillColorSpace(shading->getColorSpace()->copy()); |
| out->updateFillColorSpace(state); |
| |
| #if 1 //~tmp: turn off anti-aliasing temporarily |
| bool vaa = out->getVectorAntialias(); |
| if (vaa) { |
| out->setVectorAntialias(false); |
| } |
| #endif |
| |
| // do shading type-specific operations |
| switch (shading->getType()) { |
| case 1: |
| doFunctionShFill((GfxFunctionShading *)shading); |
| break; |
| case 2: |
| doAxialShFill((GfxAxialShading *)shading); |
| break; |
| case 3: |
| doRadialShFill((GfxRadialShading *)shading); |
| break; |
| case 4: |
| case 5: |
| doGouraudTriangleShFill((GfxGouraudTriangleShading *)shading); |
| break; |
| case 6: |
| case 7: |
| doPatchMeshShFill((GfxPatchMeshShading *)shading); |
| break; |
| } |
| |
| #if 1 //~tmp: turn off anti-aliasing temporarily |
| if (vaa) { |
| out->setVectorAntialias(true); |
| } |
| #endif |
| |
| // restore graphics state |
| restoreStateStack(savedState); |
| |
| delete shading; |
| } |
| |
| void Gfx::doFunctionShFill(GfxFunctionShading *shading) { |
| double x0, y0, x1, y1; |
| GfxColor colors[4]; |
| |
| if (out->useShadedFills( shading->getType() ) && |
| out->functionShadedFill(state, shading)) { |
| return; |
| } |
| |
| 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 Gfx::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 xM, yM; |
| int nComps, i, j; |
| |
| nComps = shading->getColorSpace()->getNComps(); |
| const double *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 == functionMaxDepth) { |
| |
| // use the center color |
| shading->getColor(xM, yM, &fillColor); |
| state->setFillColor(&fillColor); |
| out->updateFillColor(state); |
| |
| // 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(); |
| out->fill(state); |
| 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); |
| } |
| } |
| |
| static void bubbleSort(double array[]) |
| { |
| for (int j = 0; j < 3; ++j) { |
| int kk = j; |
| for (int k = j + 1; k < 4; ++k) { |
| if (array[k] < array[kk]) { |
| kk = k; |
| } |
| } |
| double tmp = array[j]; |
| array[j] = array[kk]; |
| array[kk] = tmp; |
| } |
| } |
| |
| void Gfx::doAxialShFill(GfxAxialShading *shading) { |
| double xMin, yMin, xMax, yMax; |
| double x0, y0, x1, y1; |
| double dx, dy, mul; |
| bool dxZero, dyZero; |
| double bboxIntersections[4]; |
| double tMin, tMax, tx, ty; |
| double s[4], sMin, sMax, tmp; |
| double ux0, uy0, ux1, uy1, vx0, vy0, vx1, vy1; |
| double t0, t1, tt; |
| double ta[axialMaxSplits + 1]; |
| int next[axialMaxSplits + 1]; |
| GfxColor color0 = {}, color1 = {}; |
| int nComps; |
| int i, j, k; |
| bool needExtend = true; |
| |
| // get the clip region bbox |
| state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax); |
| |
| // compute min and max t values, based on the four corners of the |
| // clip region bbox |
| shading->getCoords(&x0, &y0, &x1, &y1); |
| dx = x1 - x0; |
| dy = y1 - y0; |
| dxZero = fabs(dx) < 0.01; |
| dyZero = fabs(dy) < 0.01; |
| if (dxZero && dyZero) { |
| tMin = tMax = 0; |
| } else { |
| mul = 1 / (dx * dx + dy * dy); |
| bboxIntersections[0] = ((xMin - x0) * dx + (yMin - y0) * dy) * mul; |
| bboxIntersections[1] = ((xMin - x0) * dx + (yMax - y0) * dy) * mul; |
| bboxIntersections[2] = ((xMax - x0) * dx + (yMin - y0) * dy) * mul; |
| bboxIntersections[3] = ((xMax - x0) * dx + (yMax - y0) * dy) * mul; |
| bubbleSort(bboxIntersections); |
| tMin = bboxIntersections[0]; |
| tMax = bboxIntersections[3]; |
| if (tMin < 0 && !shading->getExtend0()) { |
| tMin = 0; |
| } |
| if (tMax > 1 && !shading->getExtend1()) { |
| tMax = 1; |
| } |
| } |
| |
| if (out->useShadedFills( shading->getType() ) && |
| out->axialShadedFill(state, shading, tMin, tMax)) { |
| return; |
| } |
| |
| // get the function domain |
| t0 = shading->getDomain0(); |
| t1 = shading->getDomain1(); |
| |
| // Traverse the t axis and do the shading. |
| // |
| // For each point (tx, ty) on the t axis, consider a line through |
| // that point perpendicular to the t axis: |
| // |
| // x(s) = tx + s * -dy --> s = (x - tx) / -dy |
| // y(s) = ty + s * dx --> s = (y - ty) / dx |
| // |
| // Then look at the intersection of this line with the bounding box |
| // (xMin, yMin, xMax, yMax). In the general case, there are four |
| // intersection points: |
| // |
| // s0 = (xMin - tx) / -dy |
| // s1 = (xMax - tx) / -dy |
| // s2 = (yMin - ty) / dx |
| // s3 = (yMax - ty) / dx |
| // |
| // and we want the middle two s values. |
| // |
| // In the case where dx = 0, take s0 and s1; in the case where dy = |
| // 0, take s2 and s3. |
| // |
| // Each filled polygon is bounded by two of these line segments |
| // perpdendicular to the t axis. |
| // |
| // The t axis is bisected into smaller regions until the color |
| // difference across a region is small enough, and then the region |
| // is painted with a single color. |
| |
| // set up: require at least one split to avoid problems when the two |
| // ends of the t axis have the same color |
| nComps = shading->getColorSpace()->getNComps(); |
| ta[0] = tMin; |
| next[0] = axialMaxSplits / 2; |
| ta[axialMaxSplits / 2] = 0.5 * (tMin + tMax); |
| next[axialMaxSplits / 2] = axialMaxSplits; |
| ta[axialMaxSplits] = tMax; |
| |
| // compute the color at t = tMin |
| if (tMin < 0) { |
| tt = t0; |
| } else if (tMin > 1) { |
| tt = t1; |
| } else { |
| tt = t0 + (t1 - t0) * tMin; |
| } |
| shading->getColor(tt, &color0); |
|