blob: 08f695e80a02509904edfbc855a276b7080c4ea3 [file] [log] [blame]
//========================================================================
//
// CairoOutputDev.cc
//
// Copyright 2003 Glyph & Cog, LLC
// Copyright 2004 Red Hat, Inc
//
//========================================================================
//========================================================================
//
// Modified under the Poppler project - http://poppler.freedesktop.org
//
// All changes made under the Poppler project to this file are licensed
// under GPL version 2 or later
//
// Copyright (C) 2005-2008 Jeff Muizelaar <jeff@infidigm.net>
// Copyright (C) 2005, 2006 Kristian Høgsberg <krh@redhat.com>
// Copyright (C) 2005, 2009, 2012, 2017-2021, 2023, 2024 Albert Astals Cid <aacid@kde.org>
// Copyright (C) 2005 Nickolay V. Shmyrev <nshmyrev@yandex.ru>
// Copyright (C) 2006-2011, 2013, 2014, 2017, 2018 Carlos Garcia Campos <carlosgc@gnome.org>
// Copyright (C) 2008 Carl Worth <cworth@cworth.org>
// Copyright (C) 2008-2018, 2021-2024 Adrian Johnson <ajohnson@redneon.com>
// Copyright (C) 2008 Michael Vrable <mvrable@cs.ucsd.edu>
// Copyright (C) 2008, 2009 Chris Wilson <chris@chris-wilson.co.uk>
// Copyright (C) 2008, 2012 Hib Eris <hib@hiberis.nl>
// Copyright (C) 2009, 2010 David Benjamin <davidben@mit.edu>
// Copyright (C) 2011-2014 Thomas Freitag <Thomas.Freitag@alfa.de>
// Copyright (C) 2012 Patrick Pfeifer <p2000@mailinator.com>
// Copyright (C) 2012, 2015, 2016 Jason Crain <jason@aquaticape.us>
// Copyright (C) 2015 Suzuki Toshiya <mpsuzuki@hiroshima-u.ac.jp>
// Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, <info@kdab.com>. Work sponsored by the LiMux project of the city of Munich
// Copyright (C) 2018, 2020 Adam Reichold <adam.reichold@t-online.de>
// Copyright (C) 2019, 2020, 2022 Marek Kasik <mkasik@redhat.com>
// Copyright (C) 2020 Michal <sudolskym@gmail.com>
// Copyright (C) 2020, 2022 Oliver Sander <oliver.sander@tu-dresden.de>
// Copyright (C) 2021 Uli Schlachter <psychon@znc.in>
// Copyright (C) 2021 Christian Persch <chpe@src.gnome.org>
// Copyright (C) 2022 Zachary Travis <ztravis@everlaw.com>
// Copyright (C) 2023 Artemy Gordon <artemy.gordon@gmail.com>
// Copyright (C) 2023 Anton Thomasson <antonthomasson@gmail.com>
// Copyright (C) 2024 Vincent Lefevre <vincent@vinc17.net>
// Copyright (C) 2024 Athul Raj Kollareth <krathul3152@gmail.com>
//
// 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 <cstdint>
#include <cstring>
#include <cmath>
#include <cassert>
#include <cairo.h>
#include "goo/gfile.h"
#include "GlobalParams.h"
#include "Error.h"
#include "Object.h"
#include "Gfx.h"
#include "GfxState.h"
#include "GfxFont.h"
#include "Page.h"
#include "Link.h"
#include "FontEncodingTables.h"
#include "PDFDocEncoding.h"
#include <fofi/FoFiTrueType.h>
#include <splash/SplashBitmap.h>
#include "CairoOutputDev.h"
#include "CairoFontEngine.h"
#include "CairoRescaleBox.h"
#include "UnicodeMap.h"
#include "UTF.h"
#include "JBIG2Stream.h"
//------------------------------------------------------------------------
// #define LOG_CAIRO
// To limit memory usage and improve performance when printing, limit
// cairo images to this size. 8192 is sufficient for an A2 sized
// 300ppi image.
#define MAX_PRINT_IMAGE_SIZE 8192
// Cairo has a max size for image surfaces due to their fixed-point
// coordinate handling, namely INT16_MAX, aka 32767.
#define MAX_CAIRO_IMAGE_SIZE 32767
#ifdef LOG_CAIRO
# define LOG(x) (x)
#else
# define LOG(x)
#endif
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
//------------------------------------------------------------------------
// CairoImage
//------------------------------------------------------------------------
CairoImage::CairoImage(double x1A, double y1A, double x2A, double y2A)
{
image = nullptr;
x1 = x1A;
y1 = y1A;
x2 = x2A;
y2 = y2A;
}
CairoImage::~CairoImage()
{
if (image) {
cairo_surface_destroy(image);
}
}
void CairoImage::setImage(cairo_surface_t *i)
{
if (image) {
cairo_surface_destroy(image);
}
image = cairo_surface_reference(i);
}
//------------------------------------------------------------------------
// CairoOutputDev
//------------------------------------------------------------------------
// We cannot tie the lifetime of an FT_Library object to that of
// CairoOutputDev, since any FT_Faces created with it may end up with a
// reference by Cairo which can be held long after the CairoOutputDev is
// deleted. The simplest way to avoid problems is to never tear down the
// FT_Library instance; to avoid leaks, just use a single global instance
// initialized the first time it is needed.
FT_Library CairoOutputDev::ft_lib;
std::once_flag CairoOutputDev::ft_lib_once_flag;
CairoOutputDev::CairoOutputDev()
{
doc = nullptr;
std::call_once(ft_lib_once_flag, FT_Init_FreeType, &ft_lib);
fontEngine = nullptr;
fontEngine_owner = false;
glyphs = nullptr;
fill_pattern = nullptr;
fill_color = {};
stroke_pattern = nullptr;
stroke_color = {};
stroke_opacity = 1.0;
fill_opacity = 1.0;
textClipPath = nullptr;
strokePathClip = nullptr;
cairo = nullptr;
currentFont = nullptr;
printing = true;
use_show_text_glyphs = false;
inUncoloredPattern = false;
t3_render_state = Type3RenderNone;
t3_glyph_has_bbox = false;
t3_glyph_has_color = false;
text_matrix_valid = true;
groupColorSpaceStack = nullptr;
group = nullptr;
mask = nullptr;
shape = nullptr;
cairo_shape = nullptr;
knockoutCount = 0;
textPage = nullptr;
actualText = nullptr;
logicalStruct = false;
pdfPageNum = 0;
cairoPageNum = 0;
// the SA parameter supposedly defaults to false, but Acrobat
// apparently hardwires it to true
stroke_adjust = true;
align_stroke_coords = false;
adjusted_stroke_width = false;
xref = nullptr;
currentStructParents = -1;
}
CairoOutputDev::~CairoOutputDev()
{
if (fontEngine_owner && fontEngine) {
delete fontEngine;
}
if (textClipPath) {
cairo_path_destroy(textClipPath);
textClipPath = nullptr;
}
if (cairo) {
cairo_destroy(cairo);
}
cairo_pattern_destroy(stroke_pattern);
cairo_pattern_destroy(fill_pattern);
if (group) {
cairo_pattern_destroy(group);
}
if (mask) {
cairo_pattern_destroy(mask);
}
if (shape) {
cairo_pattern_destroy(shape);
}
if (textPage) {
textPage->decRefCnt();
}
if (actualText) {
delete actualText;
}
}
void CairoOutputDev::setCairo(cairo_t *c)
{
if (cairo != nullptr) {
cairo_status_t status = cairo_status(cairo);
if (status) {
error(errInternal, -1, "cairo context error: {0:s}", cairo_status_to_string(status));
}
cairo_destroy(cairo);
assert(!cairo_shape);
}
if (c != nullptr) {
cairo = cairo_reference(c);
/* save the initial matrix so that we can use it for type3 fonts. */
// XXX: is this sufficient? could we miss changes to the matrix somehow?
cairo_get_matrix(cairo, &orig_matrix);
} else {
cairo = nullptr;
cairo_shape = nullptr;
}
}
bool CairoOutputDev::isPDF()
{
if (cairo) {
return cairo_surface_get_type(cairo_get_target(cairo)) == CAIRO_SURFACE_TYPE_PDF;
}
return false;
}
void CairoOutputDev::setTextPage(TextPage *text)
{
if (textPage) {
textPage->decRefCnt();
}
if (actualText) {
delete actualText;
}
if (text) {
textPage = text;
textPage->incRefCnt();
actualText = new ActualText(text);
} else {
textPage = nullptr;
actualText = nullptr;
}
}
void CairoOutputDev::copyAntialias(cairo_t *cr, cairo_t *source_cr)
{
cairo_set_antialias(cr, cairo_get_antialias(source_cr));
cairo_font_options_t *font_options = cairo_font_options_create();
cairo_get_font_options(source_cr, font_options);
cairo_set_font_options(cr, font_options);
cairo_font_options_destroy(font_options);
}
void CairoOutputDev::startDoc(PDFDoc *docA, CairoFontEngine *parentFontEngine)
{
doc = docA;
if (parentFontEngine) {
fontEngine = parentFontEngine;
} else {
if (fontEngine) {
delete fontEngine;
}
fontEngine = new CairoFontEngine(ft_lib);
fontEngine_owner = true;
}
xref = doc->getXRef();
mcidEmitted.clear();
destsMap.clear();
emittedDestinations.clear();
pdfPageToCairoPageMap.clear();
pdfPageRefToCairoPageNumMap.clear();
cairoPageNum = 0;
firstPage = true;
}
void CairoOutputDev::textStringToQuotedUtf8(const GooString *text, GooString *s)
{
std::string utf8 = TextStringToUtf8(text->toStr());
s->Set("'");
for (char c : utf8) {
if (c == '\\' || c == '\'') {
s->append("\\");
}
s->append(c);
}
s->append("'");
}
// Initialization that needs to be performed after setCairo() is called.
void CairoOutputDev::startFirstPage(int pageNum, GfxState *state, XRef *xrefA)
{
if (xrefA) {
xref = xrefA;
}
if (logicalStruct && isPDF()) {
int numDests = doc->getCatalog()->numDestNameTree();
for (int i = 0; i < numDests; i++) {
const GooString *name = doc->getCatalog()->getDestNameTreeName(i);
std::unique_ptr<LinkDest> dest = doc->getCatalog()->getDestNameTreeDest(i);
if (dest->isPageRef()) {
Ref ref = dest->getPageRef();
destsMap[ref].insert({ std::string(name->toStr()), std::move(dest) });
}
}
numDests = doc->getCatalog()->numDests();
for (int i = 0; i < numDests; i++) {
const char *name = doc->getCatalog()->getDestsName(i);
std::unique_ptr<LinkDest> dest = doc->getCatalog()->getDestsDest(i);
if (dest->isPageRef()) {
Ref ref = dest->getPageRef();
destsMap[ref].insert({ std::string(name), std::move(dest) });
}
}
}
}
void CairoOutputDev::startPage(int pageNum, GfxState *state, XRef *xrefA)
{
if (firstPage) {
startFirstPage(pageNum, state, xrefA);
firstPage = false;
}
/* set up some per page defaults */
cairo_pattern_destroy(fill_pattern);
cairo_pattern_destroy(stroke_pattern);
fill_pattern = cairo_pattern_create_rgb(0., 0., 0.);
fill_color = { 0, 0, 0 };
stroke_pattern = cairo_pattern_reference(fill_pattern);
stroke_color = { 0, 0, 0 };
if (textPage) {
textPage->startPage(state);
}
pdfPageNum = pageNum;
cairoPageNum++;
pdfPageToCairoPageMap[pdfPageNum] = cairoPageNum;
if (logicalStruct && isPDF()) {
Object obj = doc->getPage(pageNum)->getAnnotsObject(xref);
Annots *annots = new Annots(doc, pageNum, &obj);
for (Annot *annot : annots->getAnnots()) {
if (annot->getType() == Annot::typeLink) {
annot->incRefCnt();
annotations.push_back(annot);
}
}
delete annots;
// emit dests
Ref *ref = doc->getCatalog()->getPageRef(pageNum);
pdfPageRefToCairoPageNumMap[*ref] = cairoPageNum;
auto pageDests = destsMap.find(*ref);
if (pageDests != destsMap.end()) {
for (auto &it : pageDests->second) {
GooString quoted_name;
GooString name(it.first);
textStringToQuotedUtf8(&name, &quoted_name);
emittedDestinations.insert(quoted_name.toStr());
GooString attrib;
attrib.appendf("name={0:t} ", &quoted_name);
if (it.second->getChangeLeft()) {
attrib.appendf("x={0:g} ", it.second->getLeft());
}
if (it.second->getChangeTop()) {
attrib.appendf("y={0:g} ", state->getPageHeight() - it.second->getTop());
}
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 18, 0)
cairo_tag_begin(cairo, CAIRO_TAG_DEST, attrib.c_str());
cairo_tag_end(cairo, CAIRO_TAG_DEST);
#endif
}
}
currentStructParents = doc->getPage(pageNum)->getStructParents();
}
}
void CairoOutputDev::endPage()
{
if (textPage) {
textPage->endPage();
textPage->coalesce(true, 0, false);
}
}
void CairoOutputDev::beginForm(Object *obj, Ref id)
{
if (logicalStruct && isPDF()) {
structParentsStack.push_back(currentStructParents);
const Object tmp = obj->streamGetDict()->lookup("StructParents");
if (!(tmp.isInt() || tmp.isNull())) {
error(errSyntaxError, -1, "XObject StructParents object is wrong type ({0:s})", tmp.getTypeName());
} else if (tmp.isInt()) {
currentStructParents = tmp.getInt();
}
}
}
void CairoOutputDev::endForm(Object *obj, Ref id)
{
if (logicalStruct && isPDF()) {
currentStructParents = structParentsStack.back();
structParentsStack.pop_back();
}
}
void CairoOutputDev::quadToCairoRect(AnnotQuadrilaterals *quads, int idx, double pageHeight, cairo_rectangle_t *rect)
{
double x1, x2, y1, y2;
x1 = x2 = quads->getX1(idx);
y1 = y2 = quads->getX2(idx);
x1 = std::min(x1, quads->getX2(idx));
x1 = std::min(x1, quads->getX3(idx));
x1 = std::min(x1, quads->getX4(idx));
y1 = std::min(y1, quads->getY2(idx));
y1 = std::min(y1, quads->getY3(idx));
y1 = std::min(y1, quads->getY4(idx));
x2 = std::max(x2, quads->getX2(idx));
x2 = std::max(x2, quads->getX3(idx));
x2 = std::max(x2, quads->getX4(idx));
y2 = std::max(y2, quads->getY2(idx));
y2 = std::max(y2, quads->getY3(idx));
y2 = std::max(y2, quads->getY4(idx));
rect->x = x1;
rect->y = pageHeight - y2;
rect->width = x2 - x1;
rect->height = y2 - y1;
}
bool CairoOutputDev::appendLinkDestRef(GooString *s, const LinkDest *dest)
{
Ref ref = dest->getPageRef();
auto pageNum = pdfPageRefToCairoPageNumMap.find(ref);
if (pageNum != pdfPageRefToCairoPageNumMap.end()) {
auto cairoPage = pdfPageToCairoPageMap.find(pageNum->second);
if (cairoPage != pdfPageToCairoPageMap.end()) {
s->appendf("page={0:d} ", cairoPage->second);
double destPageHeight = doc->getPageMediaHeight(dest->getPageNum());
appendLinkDestXY(s, dest, destPageHeight);
return true;
}
}
return false;
}
void CairoOutputDev::appendLinkDestXY(GooString *s, const LinkDest *dest, double destPageHeight)
{
double x = 0;
double y = 0;
if (dest->getChangeLeft()) {
x = dest->getLeft();
}
if (dest->getChangeTop()) {
y = dest->getTop();
}
// if pageHeight is 0, dest is remote document, cairo uses PDF coords in this
// case. So don't flip coords when pageHeight is 0.
s->appendf("pos=[{0:g} {1:g}] ", x, destPageHeight ? destPageHeight - y : y);
}
bool CairoOutputDev::beginLinkTag(AnnotLink *annotLink)
{
int page_num = annotLink->getPageNum();
double height = doc->getPageMediaHeight(page_num);
GooString attrib;
attrib.appendf("link_page={0:d} ", page_num);
attrib.append("rect=[");
AnnotQuadrilaterals *quads = annotLink->getQuadrilaterals();
if (quads && quads->getQuadrilateralsLength() > 0) {
for (int i = 0; i < quads->getQuadrilateralsLength(); i++) {
cairo_rectangle_t rect;
quadToCairoRect(quads, i, height, &rect);
attrib.appendf("{0:g} {1:g} {2:g} {3:g} ", rect.x, rect.y, rect.width, rect.height);
}
} else {
double x1, x2, y1, y2;
annotLink->getRect(&x1, &y1, &x2, &y2);
attrib.appendf("{0:g} {1:g} {2:g} {3:g} ", x1, height - y2, x2 - x1, y2 - y1);
}
attrib.append("] ");
LinkAction *action = annotLink->getAction();
if (action->getKind() == actionGoTo) {
LinkGoTo *act = static_cast<LinkGoTo *>(action);
if (act->isOk()) {
const GooString *namedDest = act->getNamedDest();
const LinkDest *linkDest = act->getDest();
if (namedDest) {
GooString name;
textStringToQuotedUtf8(namedDest, &name);
if (emittedDestinations.count(name.toStr()) == 0) {
return false;
}
attrib.appendf("dest={0:t} ", &name);
} else if (linkDest && linkDest->isOk() && linkDest->isPageRef()) {
bool ok = appendLinkDestRef(&attrib, linkDest);
if (!ok) {
return false;
}
}
}
} else if (action->getKind() == actionGoToR) {
LinkGoToR *act = static_cast<LinkGoToR *>(action);
attrib.appendf("file='{0:t}' ", act->getFileName());
const GooString *namedDest = act->getNamedDest();
const LinkDest *linkDest = act->getDest();
if (namedDest) {
GooString name;
textStringToQuotedUtf8(namedDest, &name);
if (emittedDestinations.count(name.toStr()) == 0) {
return false;
}
attrib.appendf("dest={0:t} ", &name);
} else if (linkDest && linkDest->isOk() && !linkDest->isPageRef()) {
auto cairoPage = pdfPageToCairoPageMap.find(linkDest->getPageNum());
if (cairoPage != pdfPageToCairoPageMap.end()) {
attrib.appendf("page={0:d} ", cairoPage->second);
appendLinkDestXY(&attrib, linkDest, 0.0);
} else {
return false;
}
}
} else if (action->getKind() == actionURI) {
LinkURI *act = static_cast<LinkURI *>(action);
if (act->isOk()) {
attrib.appendf("uri='{0:s}'", act->getURI().c_str());
}
}
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 18, 0)
cairo_tag_begin(cairo, CAIRO_TAG_LINK, attrib.c_str());
#endif
return true;
}
AnnotLink *CairoOutputDev::findLinkObject(const StructElement *elem)
{
if (elem->isObjectRef()) {
Ref ref = elem->getObjectRef();
for (Annot *annot : annotations) {
if (annot->getType() == Annot::typeLink && annot->match(&ref)) {
return static_cast<AnnotLink *>(annot);
}
}
}
for (unsigned i = 0; i < elem->getNumChildren(); i++) {
AnnotLink *link = findLinkObject(elem->getChild(i));
if (link) {
return link;
}
}
return nullptr;
}
bool CairoOutputDev::beginLink(const StructElement *linkElem)
{
bool emitted = true;
AnnotLink *linkAnnot = findLinkObject(linkElem);
if (linkAnnot) {
emitted = beginLinkTag(linkAnnot);
} else {
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 18, 0)
cairo_tag_begin(cairo, linkElem->getTypeName(), nullptr);
#endif
}
return emitted;
}
void CairoOutputDev::getStructElemAttributeString(const StructElement *elem)
{
int mcid = 0;
GooString attribs;
Ref ref = elem->getObjectRef();
attribs.appendf("id='{0:d}_{1:d}_{2:d}'", ref.num, ref.gen, mcid);
attribs.appendf(" parent='{0:d}_{1:d}'", ref.num, ref.gen);
}
int CairoOutputDev::getContentElementStructParents(const StructElement *element)
{
int structParents = -1;
Ref ref;
if (element->hasStmRef()) {
element->getStmRef(ref);
Object xobjectObj = xref->fetch(ref);
const Object &spObj = xobjectObj.streamGetDict()->lookup("StructParents");
if (spObj.isInt()) {
structParents = spObj.getInt();
}
} else if (element->hasPageRef()) {
element->getPageRef(ref);
Object pageObj = xref->fetch(ref);
const Object &spObj = pageObj.dictLookup("StructParents");
if (spObj.isInt()) {
structParents = spObj.getInt();
}
}
if (structParents == -1) {
error(errSyntaxError, -1, "Unable to find StructParents object for StructElement");
}
return structParents;
}
bool CairoOutputDev::checkIfStructElementNeeded(const StructElement *element)
{
if (element->isContent() && !element->isObjectRef()) {
int structParents = getContentElementStructParents(element);
int mcid = element->getMCID();
if (mcidEmitted.count(std::pair(structParents, mcid)) > 0) {
structElementNeeded.insert(element);
return true;
}
} else if (!element->isContent()) {
bool needed = false;
for (unsigned i = 0; i < element->getNumChildren(); i++) {
if (checkIfStructElementNeeded(element->getChild(i))) {
needed = true;
}
}
if (needed) {
structElementNeeded.insert(element);
}
return needed;
}
return false;
}
void CairoOutputDev::emitStructElement(const StructElement *element)
{
if (structElementNeeded.count(element) == 0) {
return;
}
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 18, 0)
if (element->isContent() && !element->isObjectRef()) {
int structParents = getContentElementStructParents(element);
int mcid = element->getMCID();
GooString attribs;
attribs.appendf("ref='{0:d}_{1:d}'", structParents, mcid);
cairo_tag_begin(cairo, CAIRO_TAG_CONTENT_REF, attribs.c_str());
cairo_tag_end(cairo, CAIRO_TAG_CONTENT_REF);
} else if (!element->isContent()) {
if (element->getType() == StructElement::Link) {
bool ok = beginLink(element);
if (!ok) {
return;
}
} else {
cairo_tag_begin(cairo, element->getTypeName(), "");
}
for (unsigned i = 0; i < element->getNumChildren(); i++) {
emitStructElement(element->getChild(i));
}
cairo_tag_end(cairo, element->getTypeName());
}
#endif
}
void CairoOutputDev::emitStructTree()
{
if (logicalStruct && isPDF()) {
const StructTreeRoot *root = doc->getStructTreeRoot();
if (!root) {
return;
}
for (unsigned i = 0; i < root->getNumChildren(); i++) {
checkIfStructElementNeeded(root->getChild(i));
}
for (unsigned i = 0; i < root->getNumChildren(); i++) {
emitStructElement(root->getChild(i));
}
}
}
void CairoOutputDev::startType3Render(GfxState *state, XRef *xrefA)
{
/* When cairo calls a user font render function, the default
* source set on the provided cairo_t must be used, except in the
* case of a color user font explicitly setting a color.
*
* As startPage() resets the source to solid black, this function
* is used instead to initialise the CairoOutputDev when rendering
* a user font glyph.
*
* As noted in the Cairo documentation, the default source of a
* render callback contains an internal marker denoting the
* foreground color is to be used when the glyph is rendered, even
* though querying the default source will reveal solid black.
* For this reason, fill_color and stroke_color are set to nullopt
* to ensure updateFillColor()/updateStrokeColor() will update the
* color even if the new color is black.
*
* The saveState()/restoreState() functions also ensure the
* default source is saved and restored, and the fill_color and
* stroke_color is reset to nullopt for the same reason.
*/
/* Initialise fill and stroke pattern to the current source pattern */
fill_pattern = cairo_pattern_reference(cairo_get_source(cairo));
stroke_pattern = cairo_pattern_reference(cairo_get_source(cairo));
fill_color = {};
stroke_color = {};
t3_glyph_has_bbox = false;
t3_glyph_has_color = false;
if (xrefA != nullptr) {
xref = xrefA;
}
}
void CairoOutputDev::saveState(GfxState *state)
{
LOG(printf("save\n"));
cairo_save(cairo);
if (cairo_shape) {
cairo_save(cairo_shape);
}
/* To ensure the current source, potentially containing the hidden
* foreground color maker, is saved and restored as required by
* _render_type3_glyph, we avoid using the update color and
* opacity functions in restoreState() and instead be careful to
* save all the color related variables that have been set by the
* update functions on the stack. */
SaveStateElement elem;
elem.fill_pattern = cairo_pattern_reference(fill_pattern);
elem.fill_opacity = fill_opacity;
elem.stroke_pattern = cairo_pattern_reference(stroke_pattern);
elem.stroke_opacity = stroke_opacity;
elem.mask = mask ? cairo_pattern_reference(mask) : nullptr;
elem.mask_matrix = mask_matrix;
elem.fontRef = currentFont ? currentFont->getRef() : Ref::INVALID();
saveStateStack.push_back(elem);
if (strokePathClip) {
strokePathClip->ref_count++;
}
}
void CairoOutputDev::restoreState(GfxState *state)
{
LOG(printf("restore\n"));
cairo_restore(cairo);
if (cairo_shape) {
cairo_restore(cairo_shape);
}
text_matrix_valid = true;
cairo_pattern_destroy(fill_pattern);
fill_pattern = saveStateStack.back().fill_pattern;
fill_color = {};
fill_opacity = saveStateStack.back().fill_opacity;
cairo_pattern_destroy(stroke_pattern);
stroke_pattern = saveStateStack.back().stroke_pattern;
stroke_color = {};
stroke_opacity = saveStateStack.back().stroke_opacity;
if (saveStateStack.back().fontRef != (currentFont ? currentFont->getRef() : Ref::INVALID())) {
needFontUpdate = true;
}
/* This isn't restored by cairo_restore() since we keep it in the
* output device. */
updateBlendMode(state);
if (mask) {
cairo_pattern_destroy(mask);
}
mask = saveStateStack.back().mask;
mask_matrix = saveStateStack.back().mask_matrix;
saveStateStack.pop_back();
if (strokePathClip && --strokePathClip->ref_count == 0) {
delete strokePathClip->path;
if (strokePathClip->dashes) {
gfree(strokePathClip->dashes);
}
gfree(strokePathClip);
strokePathClip = nullptr;
}
}
void CairoOutputDev::updateAll(GfxState *state)
{
updateLineDash(state);
updateLineJoin(state);
updateLineCap(state);
updateLineWidth(state);
updateFlatness(state);
updateMiterLimit(state);
updateFillColor(state);
updateStrokeColor(state);
updateFillOpacity(state);
updateStrokeOpacity(state);
updateBlendMode(state);
needFontUpdate = true;
if (textPage) {
textPage->updateFont(state);
}
}
void CairoOutputDev::setDefaultCTM(const double *ctm)
{
cairo_matrix_t matrix;
matrix.xx = ctm[0];
matrix.yx = ctm[1];
matrix.xy = ctm[2];
matrix.yy = ctm[3];
matrix.x0 = ctm[4];
matrix.y0 = ctm[5];
cairo_transform(cairo, &matrix);
if (cairo_shape) {
cairo_transform(cairo_shape, &matrix);
}
OutputDev::setDefaultCTM(ctm);
}
void CairoOutputDev::updateCTM(GfxState *state, double m11, double m12, double m21, double m22, double m31, double m32)
{
cairo_matrix_t matrix, invert_matrix;
matrix.xx = m11;
matrix.yx = m12;
matrix.xy = m21;
matrix.yy = m22;
matrix.x0 = m31;
matrix.y0 = m32;
/* Make sure the matrix is invertible before setting it.
* cairo will blow up if we give it a matrix that's not
* invertible, so we need to check before passing it
* to cairo_transform. Ignoring it is likely to give better
* results than not rendering anything at all. See #14398
*
* Ideally, we could do the cairo_transform
* and then check if anything went wrong and fix it then
* instead of having to invert the matrix. */
invert_matrix = matrix;
if (cairo_matrix_invert(&invert_matrix)) {
error(errSyntaxWarning, -1, "matrix not invertible");
return;
}
cairo_transform(cairo, &matrix);
if (cairo_shape) {
cairo_transform(cairo_shape, &matrix);
}
updateLineDash(state);
updateLineJoin(state);
updateLineCap(state);
updateLineWidth(state);
}
void CairoOutputDev::updateLineDash(GfxState *state)
{
double dashStart;
const std::vector<double> &dashPattern = state->getLineDash(&dashStart);
cairo_set_dash(cairo, dashPattern.data(), dashPattern.size(), dashStart);
if (cairo_shape) {
cairo_set_dash(cairo_shape, dashPattern.data(), dashPattern.size(), dashStart);
}
}
void CairoOutputDev::updateFlatness(GfxState *state)
{
// cairo_set_tolerance (cairo, state->getFlatness());
}
void CairoOutputDev::updateLineJoin(GfxState *state)
{
switch (state->getLineJoin()) {
case GfxState::LineJoinMitre:
cairo_set_line_join(cairo, CAIRO_LINE_JOIN_MITER);
break;
case GfxState::LineJoinRound:
cairo_set_line_join(cairo, CAIRO_LINE_JOIN_ROUND);
break;
case GfxState::LineJoinBevel:
cairo_set_line_join(cairo, CAIRO_LINE_JOIN_BEVEL);
break;
}
if (cairo_shape) {
cairo_set_line_join(cairo_shape, cairo_get_line_join(cairo));
}
}
void CairoOutputDev::updateLineCap(GfxState *state)
{
switch (state->getLineCap()) {
case GfxState::LineCapButt:
cairo_set_line_cap(cairo, CAIRO_LINE_CAP_BUTT);
break;
case GfxState::LineCapRound:
cairo_set_line_cap(cairo, CAIRO_LINE_CAP_ROUND);
break;
case GfxState::LineCapProjecting:
cairo_set_line_cap(cairo, CAIRO_LINE_CAP_SQUARE);
break;
}
if (cairo_shape) {
cairo_set_line_cap(cairo_shape, cairo_get_line_cap(cairo));
}
}
void CairoOutputDev::updateMiterLimit(GfxState *state)
{
cairo_set_miter_limit(cairo, state->getMiterLimit());
if (cairo_shape) {
cairo_set_miter_limit(cairo_shape, state->getMiterLimit());
}
}
void CairoOutputDev::updateLineWidth(GfxState *state)
{
LOG(printf("line width: %f\n", state->getLineWidth()));
adjusted_stroke_width = false;
double width = state->getLineWidth();
if (stroke_adjust && !printing) {
double x, y;
x = y = width;
/* find out line width in device units */
cairo_user_to_device_distance(cairo, &x, &y);
if (fabs(x) <= 1.0 && fabs(y) <= 1.0) {
/* adjust width to at least one device pixel */
x = y = 1.0;
cairo_device_to_user_distance(cairo, &x, &y);
width = MIN(fabs(x), fabs(y));
adjusted_stroke_width = true;
}
} else if (width == 0.0) {
/* Cairo does not support 0 line width == 1 device pixel. Find out
* how big pixels (device unit) are in the x and y
* directions. Choose the smaller of the two as our line width.
*/
double x = 1.0, y = 1.0;
if (printing) {
// assume printer pixel size is 1/600 inch
x = 72.0 / 600;
y = 72.0 / 600;
}
cairo_device_to_user_distance(cairo, &x, &y);
width = MIN(fabs(x), fabs(y));
}
cairo_set_line_width(cairo, width);
if (cairo_shape) {
cairo_set_line_width(cairo_shape, cairo_get_line_width(cairo));
}
}
void CairoOutputDev::updateFillColor(GfxState *state)
{
if (inUncoloredPattern) {
return;
}
GfxRGB new_color;
state->getFillRGB(&new_color);
bool color_match = fill_color && *fill_color == new_color;
if (cairo_pattern_get_type(fill_pattern) != CAIRO_PATTERN_TYPE_SOLID || !color_match) {
cairo_pattern_destroy(fill_pattern);
fill_pattern = cairo_pattern_create_rgba(colToDbl(new_color.r), colToDbl(new_color.g), colToDbl(new_color.b), fill_opacity);
fill_color = new_color;
LOG(printf("fill color: %d %d %d\n", fill_color->r, fill_color->g, fill_color->b));
}
}
void CairoOutputDev::updateStrokeColor(GfxState *state)
{
if (inUncoloredPattern) {
return;
}
GfxRGB new_color;
state->getStrokeRGB(&new_color);
bool color_match = stroke_color && *stroke_color == new_color;
if (cairo_pattern_get_type(fill_pattern) != CAIRO_PATTERN_TYPE_SOLID || !color_match) {
cairo_pattern_destroy(stroke_pattern);
stroke_pattern = cairo_pattern_create_rgba(colToDbl(new_color.r), colToDbl(new_color.g), colToDbl(new_color.b), stroke_opacity);
stroke_color = new_color;
LOG(printf("stroke color: %d %d %d\n", stroke_color->r, stroke_color->g, stroke_color->b));
}
}
void CairoOutputDev::updateFillOpacity(GfxState *state)
{
double opacity = fill_opacity;
if (inUncoloredPattern) {
return;
}
fill_opacity = state->getFillOpacity();
if (opacity != fill_opacity) {
if (!fill_color) {
GfxRGB color;
state->getFillRGB(&color);
fill_color = color;
}
cairo_pattern_destroy(fill_pattern);
fill_pattern = cairo_pattern_create_rgba(colToDbl(fill_color->r), colToDbl(fill_color->g), colToDbl(fill_color->b), fill_opacity);
LOG(printf("fill opacity: %f\n", fill_opacity));
}
}
void CairoOutputDev::updateStrokeOpacity(GfxState *state)
{
double opacity = stroke_opacity;
if (inUncoloredPattern) {
return;
}
stroke_opacity = state->getStrokeOpacity();
if (opacity != stroke_opacity) {
if (!stroke_color) {
GfxRGB color;
state->getStrokeRGB(&color);
stroke_color = color;
}
cairo_pattern_destroy(stroke_pattern);
stroke_pattern = cairo_pattern_create_rgba(colToDbl(stroke_color->r), colToDbl(stroke_color->g), colToDbl(stroke_color->b), stroke_opacity);
LOG(printf("stroke opacity: %f\n", stroke_opacity));
}
}
void CairoOutputDev::updateFillColorStop(GfxState *state, double offset)
{
if (inUncoloredPattern) {
return;
}
GfxRGB color;
state->getFillRGB(&color);
// If stroke pattern is set then the current fill is clipped
// to a stroke path. In that case, the stroke opacity has to be used
// rather than the fill opacity.
// See https://gitlab.freedesktop.org/poppler/poppler/issues/178
auto opacity = (state->getStrokePattern()) ? state->getStrokeOpacity() : state->getFillOpacity();
cairo_pattern_add_color_stop_rgba(fill_pattern, offset, colToDbl(color.r), colToDbl(color.g), colToDbl(color.b), opacity);
LOG(printf("fill color stop: %f (%d, %d, %d, %d)\n", offset, color.r, color.g, color.b, dblToCol(opacity)));
}
void CairoOutputDev::updateBlendMode(GfxState *state)
{
switch (state->getBlendMode()) {
default:
case gfxBlendNormal:
cairo_set_operator(cairo, CAIRO_OPERATOR_OVER);
break;
case gfxBlendMultiply:
cairo_set_operator(cairo, CAIRO_OPERATOR_MULTIPLY);
break;
case gfxBlendScreen:
cairo_set_operator(cairo, CAIRO_OPERATOR_SCREEN);
break;
case gfxBlendOverlay:
cairo_set_operator(cairo, CAIRO_OPERATOR_OVERLAY);
break;
case gfxBlendDarken:
cairo_set_operator(cairo, CAIRO_OPERATOR_DARKEN);
break;
case gfxBlendLighten:
cairo_set_operator(cairo, CAIRO_OPERATOR_LIGHTEN);
break;
case gfxBlendColorDodge:
cairo_set_operator(cairo, CAIRO_OPERATOR_COLOR_DODGE);
break;
case gfxBlendColorBurn:
cairo_set_operator(cairo, CAIRO_OPERATOR_COLOR_BURN);
break;
case gfxBlendHardLight:
cairo_set_operator(cairo, CAIRO_OPERATOR_HARD_LIGHT);
break;
case gfxBlendSoftLight:
cairo_set_operator(cairo, CAIRO_OPERATOR_SOFT_LIGHT);
break;
case gfxBlendDifference:
cairo_set_operator(cairo, CAIRO_OPERATOR_DIFFERENCE);
break;
case gfxBlendExclusion:
cairo_set_operator(cairo, CAIRO_OPERATOR_EXCLUSION);
break;
case gfxBlendHue:
cairo_set_operator(cairo, CAIRO_OPERATOR_HSL_HUE);
break;
case gfxBlendSaturation:
cairo_set_operator(cairo, CAIRO_OPERATOR_HSL_SATURATION);
break;
case gfxBlendColor:
cairo_set_operator(cairo, CAIRO_OPERATOR_HSL_COLOR);
break;
case gfxBlendLuminosity:
cairo_set_operator(cairo, CAIRO_OPERATOR_HSL_LUMINOSITY);
break;
}
LOG(printf("blend mode: %d\n", (int)state->getBlendMode()));
}
void CairoOutputDev::updateFont(GfxState *state)
{
cairo_font_face_t *font_face;
cairo_matrix_t matrix, invert_matrix;
LOG(printf("updateFont() font=%s\n", state->getFont()->getName()->c_str()));
needFontUpdate = false;
// FIXME: use cairo font engine?
if (textPage) {
textPage->updateFont(state);
}
currentFont = fontEngine->getFont(state->getFont(), doc, printing, xref);
if (!currentFont) {
return;
}
font_face = currentFont->getFontFace();
cairo_set_font_face(cairo, font_face);
use_show_text_glyphs = state->getFont()->hasToUnicodeCMap() && cairo_surface_has_show_text_glyphs(cairo_get_target(cairo));
double fontSize = state->getFontSize();
const double *m = state->getTextMat();
/* NOTE: adjusting by a constant is hack. The correct solution
* is probably to use user-fonts and compute the scale on a per
* glyph basis instead of for the entire font */
double w = currentFont->getSubstitutionCorrection(state->getFont());
matrix.xx = m[0] * fontSize * state->getHorizScaling() * w;
matrix.yx = m[1] * fontSize * state->getHorizScaling() * w;
matrix.xy = -m[2] * fontSize;
matrix.yy = -m[3] * fontSize;
matrix.x0 = 0;
matrix.y0 = 0;
LOG(printf("font matrix: %f %f %f %f\n", matrix.xx, matrix.yx, matrix.xy, matrix.yy));
/* Make sure the font matrix is invertible before setting it. cairo
* will blow up if we give it a matrix that's not invertible, so we
* need to check before passing it to cairo_set_font_matrix. Ignoring it
* is likely to give better results than not rendering anything at
* all. See #18254.
*/
invert_matrix = matrix;
if (cairo_matrix_invert(&invert_matrix)) {
error(errSyntaxWarning, -1, "font matrix not invertible");
text_matrix_valid = false;
return;
}
cairo_set_font_matrix(cairo, &matrix);
text_matrix_valid = true;
}
/* Tolerance in pixels for checking if strokes are horizontal or vertical
* lines in device space */
#define STROKE_COORD_TOLERANCE 0.5
/* Align stroke coordinate i if the point is the start or end of a
* horizontal or vertical line */
void CairoOutputDev::alignStrokeCoords(const GfxSubpath *subpath, int i, double *x, double *y)
{
double x1, y1, x2, y2;
bool align = false;
x1 = subpath->getX(i);
y1 = subpath->getY(i);
cairo_user_to_device(cairo, &x1, &y1);
// Does the current coord and prev coord form a horiz or vert line?
if (i > 0 && !subpath->getCurve(i - 1)) {
x2 = subpath->getX(i - 1);
y2 = subpath->getY(i - 1);
cairo_user_to_device(cairo, &x2, &y2);
if (fabs(x2 - x1) < STROKE_COORD_TOLERANCE || fabs(y2 - y1) < STROKE_COORD_TOLERANCE) {
align = true;
}
}
// Does the current coord and next coord form a horiz or vert line?
if (i < subpath->getNumPoints() - 1 && !subpath->getCurve(i + 1)) {
x2 = subpath->getX(i + 1);
y2 = subpath->getY(i + 1);
cairo_user_to_device(cairo, &x2, &y2);
if (fabs(x2 - x1) < STROKE_COORD_TOLERANCE || fabs(y2 - y1) < STROKE_COORD_TOLERANCE) {
align = true;
}
}
*x = subpath->getX(i);
*y = subpath->getY(i);
if (align) {
/* see http://www.cairographics.org/FAQ/#sharp_lines */
cairo_user_to_device(cairo, x, y);
*x = floor(*x) + 0.5;
*y = floor(*y) + 0.5;
cairo_device_to_user(cairo, x, y);
}
}
#undef STROKE_COORD_TOLERANCE
void CairoOutputDev::doPath(cairo_t *c, GfxState *state, const GfxPath *path)
{
int i, j;
double x, y;
cairo_new_path(c);
for (i = 0; i < path->getNumSubpaths(); ++i) {
const GfxSubpath *subpath = path->getSubpath(i);
if (subpath->getNumPoints() > 0) {
if (align_stroke_coords) {
alignStrokeCoords(subpath, 0, &x, &y);
} else {
x = subpath->getX(0);
y = subpath->getY(0);
}
cairo_move_to(c, x, y);
j = 1;
while (j < subpath->getNumPoints()) {
if (subpath->getCurve(j)) {
if (align_stroke_coords) {
alignStrokeCoords(subpath, j + 2, &x, &y);
} else {
x = subpath->getX(j + 2);
y = subpath->getY(j + 2);
}
cairo_curve_to(c, subpath->getX(j), subpath->getY(j), subpath->getX(j + 1), subpath->getY(j + 1), x, y);
j += 3;
} else {
if (align_stroke_coords) {
alignStrokeCoords(subpath, j, &x, &y);
} else {
x = subpath->getX(j);
y = subpath->getY(j);
}
cairo_line_to(c, x, y);
++j;
}
}
if (subpath->isClosed()) {
LOG(printf("close\n"));
cairo_close_path(c);
}
}
}
}
void CairoOutputDev::stroke(GfxState *state)
{
if (t3_render_state == Type3RenderMask) {
GfxGray gray;
state->getFillGray(&gray);
if (colToDbl(gray) > 0.5) {
return;
}
}
if (adjusted_stroke_width) {
align_stroke_coords = true;
}
doPath(cairo, state, state->getPath());
align_stroke_coords = false;
cairo_set_source(cairo, stroke_pattern);
LOG(printf("stroke\n"));
if (strokePathClip) {
cairo_push_group(cairo);
cairo_stroke(cairo);
cairo_pop_group_to_source(cairo);
fillToStrokePathClip(state);
} else {
cairo_stroke(cairo);
}
if (cairo_shape) {
doPath(cairo_shape, state, state->getPath());
cairo_stroke(cairo_shape);
}
}
void CairoOutputDev::fill(GfxState *state)
{
if (t3_render_state == Type3RenderMask) {
GfxGray gray;
state->getFillGray(&gray);
if (colToDbl(gray) > 0.5) {
return;
}
}
doPath(cairo, state, state->getPath());
cairo_set_fill_rule(cairo, CAIRO_FILL_RULE_WINDING);
cairo_set_source(cairo, fill_pattern);
LOG(printf("fill\n"));
// XXX: how do we get the path
if (mask) {
cairo_save(cairo);
cairo_clip(cairo);
if (strokePathClip) {
cairo_push_group(cairo);
fillToStrokePathClip(state);
cairo_pop_group_to_source(cairo);
}
cairo_set_matrix(cairo, &mask_matrix);
cairo_mask(cairo, mask);
cairo_restore(cairo);
} else if (strokePathClip) {
fillToStrokePathClip(state);
} else {
cairo_fill(cairo);
}
if (cairo_shape) {
cairo_set_fill_rule(cairo_shape, CAIRO_FILL_RULE_WINDING);
doPath(cairo_shape, state, state->getPath());
cairo_fill(cairo_shape);
}
}
void CairoOutputDev::eoFill(GfxState *state)
{
doPath(cairo, state, state->getPath());
cairo_set_fill_rule(cairo, CAIRO_FILL_RULE_EVEN_ODD);
cairo_set_source(cairo, fill_pattern);
LOG(printf("fill-eo\n"));
if (mask) {
cairo_save(cairo);
cairo_clip(cairo);
cairo_set_matrix(cairo, &mask_matrix);
cairo_mask(cairo, mask);
cairo_restore(cairo);
} else {
cairo_fill(cairo);
}
if (cairo_shape) {
cairo_set_fill_rule(cairo_shape, CAIRO_FILL_RULE_EVEN_ODD);
doPath(cairo_shape, state, state->getPath());
cairo_fill(cairo_shape);
}
}
bool CairoOutputDev::tilingPatternFill(GfxState *state, Gfx *gfxA, Catalog *cat, GfxTilingPattern *tPat, const double *mat, int x0, int y0, int x1, int y1, double xStep, double yStep)
{
PDFRectangle box;
Gfx *gfx;
cairo_pattern_t *pattern;
cairo_surface_t *surface;
cairo_matrix_t matrix;
cairo_matrix_t pattern_matrix;
cairo_t *old_cairo;
double xMin, yMin, xMax, yMax;
double width, height;
double scaleX, scaleY;
int surface_width, surface_height;
StrokePathClip *strokePathTmp;
bool adjusted_stroke_width_tmp;
cairo_pattern_t *maskTmp;
const double *bbox = tPat->getBBox();
const double *pmat = tPat->getMatrix();
const int paintType = tPat->getPaintType();
Dict *resDict = tPat->getResDict();
Object *str = tPat->getContentStream();
width = bbox[2] - bbox[0];
height = bbox[3] - bbox[1];
if (xStep != width || yStep != height) {
return false;
}
/* TODO: implement the other cases here too */
// Find the width and height of the transformed pattern
cairo_get_matrix(cairo, &matrix);
cairo_matrix_init(&pattern_matrix, mat[0], mat[1], mat[2], mat[3], mat[4], mat[5]);
cairo_matrix_multiply(&matrix, &matrix, &pattern_matrix);
double widthX = width, widthY = 0;
cairo_matrix_transform_distance(&matrix, &widthX, &widthY);
surface_width = ceil(sqrt(widthX * widthX + widthY * widthY));
double heightX = 0, heightY = height;
cairo_matrix_transform_distance(&matrix, &heightX, &heightY);
surface_height = ceil(sqrt(heightX * heightX + heightY * heightY));
scaleX = surface_width / width;
scaleY = surface_height / height;
surface = cairo_surface_create_similar(cairo_get_target(cairo), CAIRO_CONTENT_COLOR_ALPHA, surface_width, surface_height);
if (cairo_surface_status(surface)) {
return false;
}
old_cairo = cairo;
cairo = cairo_create(surface);
cairo_surface_destroy(surface);
copyAntialias(cairo, old_cairo);
box.x1 = bbox[0];
box.y1 = bbox[1];
box.x2 = bbox[2];
box.y2 = bbox[3];
cairo_scale(cairo, scaleX, scaleY);
cairo_translate(cairo, -box.x1, -box.y1);
strokePathTmp = strokePathClip;
strokePathClip = nullptr;
adjusted_stroke_width_tmp = adjusted_stroke_width;
maskTmp = mask;
mask = nullptr;
gfx = new Gfx(doc, this, resDict, &box, nullptr, nullptr, nullptr, gfxA);
if (paintType == 2) {
inUncoloredPattern = true;
}
gfx->display(str);
if (paintType == 2) {
inUncoloredPattern = false;
}
delete gfx;
strokePathClip = strokePathTmp;
adjusted_stroke_width = adjusted_stroke_width_tmp;
mask = maskTmp;
pattern = cairo_pattern_create_for_surface(cairo_get_target(cairo));
cairo_destroy(cairo);
cairo = old_cairo;
if (cairo_pattern_status(pattern)) {
return false;
}
// Cairo can fail if the pattern translation is too large. Fix by making the
// translation smaller.
const double det = pmat[0] * pmat[3] - pmat[1] * pmat[2];
// Find the number of repetitions of pattern we need to shift by. Transform
// the translation component of pmat (pmat[4] and pmat[5]) into the pattern's
// coordinate system by multiplying by inverse of pmat, then divide by
// pattern size (xStep and yStep).
const double xoffset = round((pmat[3] * pmat[4] - pmat[2] * pmat[5]) / (xStep * det));
const double yoffset = -round((pmat[1] * pmat[4] - pmat[0] * pmat[5]) / (yStep * det));
if (!std::isfinite(xoffset) || !std::isfinite(yoffset)) {
error(errSyntaxWarning, -1, "CairoOutputDev: Singular matrix in tilingPatternFill");
return false;
}
// Shift pattern_matrix by multiples of the pattern size.
pattern_matrix.x0 -= xoffset * pattern_matrix.xx * xStep + yoffset * pattern_matrix.xy * yStep;
pattern_matrix.y0 -= xoffset * pattern_matrix.yx * xStep + yoffset * pattern_matrix.yy * yStep;
state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax);
cairo_rectangle(cairo, xMin, yMin, xMax - xMin, yMax - yMin);
cairo_matrix_init_scale(&matrix, scaleX, scaleY);
cairo_matrix_translate(&matrix, -box.x1, -box.y1);
cairo_pattern_set_matrix(pattern, &matrix);
cairo_transform(cairo, &pattern_matrix);
cairo_set_source(cairo, pattern);
cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REPEAT);
if (strokePathClip) {
fillToStrokePathClip(state);
} else {
cairo_fill(cairo);
}
cairo_pattern_destroy(pattern);
return true;
}
bool CairoOutputDev::functionShadedFill(GfxState *state, GfxFunctionShading *shading)
{
// Function shaded fills are subdivided to rectangles that are the
// following size in device space. Note when printing this size is
// in points.
const int subdivide_pixels = 10;
double x_begin, x_end, x1, x2;
double y_begin, y_end, y1, y2;
double x_step;
double y_step;
GfxColor color;
GfxRGB rgb;
cairo_matrix_t mat;
const double *matrix = shading->getMatrix();
mat.xx = matrix[0];
mat.yx = matrix[1];
mat.xy = matrix[2];
mat.yy = matrix[3];
mat.x0 = matrix[4];
mat.y0 = matrix[5];
if (cairo_matrix_invert(&mat)) {
error(errSyntaxWarning, -1, "matrix not invertible");
return false;
}
// get cell size in pattern space
x_step = y_step = subdivide_pixels;
cairo_matrix_transform_distance(&mat, &x_step, &y_step);
cairo_pattern_destroy(fill_pattern);
fill_pattern = cairo_pattern_create_mesh();
cairo_pattern_set_matrix(fill_pattern, &mat);
shading->getDomain(&x_begin, &y_begin, &x_end, &y_end);
for (x1 = x_begin; x1 < x_end; x1 += x_step) {
x2 = x1 + x_step;
if (x2 > x_end) {
x2 = x_end;
}
for (y1 = y_begin; y1 < y_end; y1 += y_step) {
y2 = y1 + y_step;
if (y2 > y_end) {
y2 = y_end;
}
cairo_mesh_pattern_begin_patch(fill_pattern);
cairo_mesh_pattern_move_to(fill_pattern, x1, y1);
cairo_mesh_pattern_line_to(fill_pattern, x2, y1);
cairo_mesh_pattern_line_to(fill_pattern, x2, y2);
cairo_mesh_pattern_line_to(fill_pattern, x1, y2);
shading->getColor(x1, y1, &color);
shading->getColorSpace()->getRGB(&color, &rgb);
cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, 0, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));
shading->getColor(x2, y1, &color);
shading->getColorSpace()->getRGB(&color, &rgb);
cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, 1, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));
shading->getColor(x2, y2, &color);
shading->getColorSpace()->getRGB(&color, &rgb);
cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, 2, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));
shading->getColor(x1, y2, &color);
shading->getColorSpace()->getRGB(&color, &rgb);
cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, 3, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));
cairo_mesh_pattern_end_patch(fill_pattern);
}
}
double xMin, yMin, xMax, yMax;
// get the clip region bbox
state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax);
state->moveTo(xMin, yMin);
state->lineTo(xMin, yMax);
state->lineTo(xMax, yMax);
state->lineTo(xMax, yMin);
state->closePath();
fill(state);
state->clearPath();
return true;
}
bool CairoOutputDev::axialShadedFill(GfxState *state, GfxAxialShading *shading, double tMin, double tMax)
{
double x0, y0, x1, y1;
double dx, dy;
shading->getCoords(&x0, &y0, &x1, &y1);
dx = x1 - x0;
dy = y1 - y0;
cairo_pattern_destroy(fill_pattern);
fill_pattern = cairo_pattern_create_linear(x0 + tMin * dx, y0 + tMin * dy, x0 + tMax * dx, y0 + tMax * dy);
if (!shading->getExtend0() && !shading->getExtend1()) {
cairo_pattern_set_extend(fill_pattern, CAIRO_EXTEND_NONE);
} else {
cairo_pattern_set_extend(fill_pattern, CAIRO_EXTEND_PAD);
}
LOG(printf("axial-sh\n"));
// TODO: use the actual stops in the shading in the case
// of linear interpolation (Type 2 Exponential functions with N=1)
return false;
}
bool CairoOutputDev::axialShadedSupportExtend(GfxState *state, GfxAxialShading *shading)
{
return (shading->getExtend0() == shading->getExtend1());
}
bool CairoOutputDev::radialShadedFill(GfxState *state, GfxRadialShading *shading, double sMin, double sMax)
{
double x0, y0, r0, x1, y1, r1;
double dx, dy, dr;
cairo_matrix_t matrix;
double scale;
shading->getCoords(&x0, &y0, &r0, &x1, &y1, &r1);
dx = x1 - x0;
dy = y1 - y0;
dr = r1 - r0;
// Cairo/pixman do not work well with a very large or small scaled
// matrix. See cairo bug #81657.
//
// As a workaround, scale the pattern by the average of the vertical
// and horizontal scaling of the current transformation matrix.
cairo_get_matrix(cairo, &matrix);
scale = (sqrt(matrix.xx * matrix.xx + matrix.yx * matrix.yx) + sqrt(matrix.xy * matrix.xy + matrix.yy * matrix.yy)) / 2;
cairo_matrix_init_scale(&matrix, scale, scale);
cairo_pattern_destroy(fill_pattern);
fill_pattern = cairo_pattern_create_radial((x0 + sMin * dx) * scale, (y0 + sMin * dy) * scale, (r0 + sMin * dr) * scale, (x0 + sMax * dx) * scale, (y0 + sMax * dy) * scale, (r0 + sMax * dr) * scale);
cairo_pattern_set_matrix(fill_pattern, &matrix);
if (shading->getExtend0() && shading->getExtend1()) {
cairo_pattern_set_extend(fill_pattern, CAIRO_EXTEND_PAD);
} else {
cairo_pattern_set_extend(fill_pattern, CAIRO_EXTEND_NONE);
}
LOG(printf("radial-sh\n"));
return false;
}
bool CairoOutputDev::radialShadedSupportExtend(GfxState *state, GfxRadialShading *shading)
{
return (shading->getExtend0() == shading->getExtend1());
}
bool CairoOutputDev::gouraudTriangleShadedFill(GfxState *state, GfxGouraudTriangleShading *shading)
{
double x0, y0, x1, y1, x2, y2;
GfxColor color[3];
int i, j;
GfxRGB rgb;
cairo_pattern_destroy(fill_pattern);
fill_pattern = cairo_pattern_create_mesh();
for (i = 0; i < shading->getNTriangles(); i++) {
if (shading->isParameterized()) {
double color0, color1, color2;
shading->getTriangle(i, &x0, &y0, &color0, &x1, &y1, &color1, &x2, &y2, &color2);
shading->getParameterizedColor(color0, &color[0]);
shading->getParameterizedColor(color1, &color[1]);
shading->getParameterizedColor(color2, &color[2]);
} else {
shading->getTriangle(i, &x0, &y0, &color[0], &x1, &y1, &color[1], &x2, &y2, &color[2]);
}
cairo_mesh_pattern_begin_patch(fill_pattern);
cairo_mesh_pattern_move_to(fill_pattern, x0, y0);
cairo_mesh_pattern_line_to(fill_pattern, x1, y1);
cairo_mesh_pattern_line_to(fill_pattern, x2, y2);
for (j = 0; j < 3; j++) {
shading->getColorSpace()->getRGB(&color[j], &rgb);
cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, j, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));
}
cairo_mesh_pattern_end_patch(fill_pattern);
}
double xMin, yMin, xMax, yMax;
// get the clip region bbox
state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax);
state->moveTo(xMin, yMin);
state->lineTo(xMin, yMax);
state->lineTo(xMax, yMax);
state->lineTo(xMax, yMin);
state->closePath();
fill(state);
state->clearPath();
return true;
}
bool CairoOutputDev::patchMeshShadedFill(GfxState *state, GfxPatchMeshShading *shading)
{
int i, j, k;
cairo_pattern_destroy(fill_pattern);
fill_pattern = cairo_pattern_create_mesh();
for (i = 0; i < shading->getNPatches(); i++) {
const GfxPatch *patch = shading->getPatch(i);
GfxColor color;
GfxRGB rgb;
cairo_mesh_pattern_begin_patch(fill_pattern);
cairo_mesh_pattern_move_to(fill_pattern, patch->x[0][0], patch->y[0][0]);
cairo_mesh_pattern_curve_to(fill_pattern, patch->x[0][1], patch->y[0][1], patch->x[0][2], patch->y[0][2], patch->x[0][3], patch->y[0][3]);
cairo_mesh_pattern_curve_to(fill_pattern, patch->x[1][3], patch->y[1][3], patch->x[2][3], patch->y[2][3], patch->x[3][3], patch->y[3][3]);
cairo_mesh_pattern_curve_to(fill_pattern, patch->x[3][2], patch->y[3][2], patch->x[3][1], patch->y[3][1], patch->x[3][0], patch->y[3][0]);
cairo_mesh_pattern_curve_to(fill_pattern, patch->x[2][0], patch->y[2][0], patch->x[1][0], patch->y[1][0], patch->x[0][0], patch->y[0][0]);
cairo_mesh_pattern_set_control_point(fill_pattern, 0, patch->x[1][1], patch->y[1][1]);
cairo_mesh_pattern_set_control_point(fill_pattern, 1, patch->x[1][2], patch->y[1][2]);
cairo_mesh_pattern_set_control_point(fill_pattern, 2, patch->x[2][2], patch->y[2][2]);
cairo_mesh_pattern_set_control_point(fill_pattern, 3, patch->x[2][1], patch->y[2][1]);
for (j = 0; j < 4; j++) {
int u, v;
switch (j) {
case 0:
u = 0;
v = 0;
break;
case 1:
u = 0;
v = 1;
break;
case 2:
u = 1;
v = 1;
break;
case 3:
u = 1;
v = 0;
break;
}
if (shading->isParameterized()) {
shading->getParameterizedColor(patch->color[u][v].c[0], &color);
} else {
for (k = 0; k < shading->getColorSpace()->getNComps(); k++) {
// simply cast to the desired type; that's all what is needed.
color.c[k] = GfxColorComp(patch->color[u][v].c[k]);
}
}
shading->getColorSpace()->getRGB(&color, &rgb);
cairo_mesh_pattern_set_corner_color_rgb(fill_pattern, j, colToDbl(rgb.r), colToDbl(rgb.g), colToDbl(rgb.b));
}
cairo_mesh_pattern_end_patch(fill_pattern);
}
double xMin, yMin, xMax, yMax;
// get the clip region bbox
state->getUserClipBBox(&xMin, &yMin, &xMax, &yMax);
state->moveTo(xMin, yMin);
state->lineTo(xMin, yMax);
state->lineTo(xMax, yMax);
state->lineTo(xMax, yMin);
state->closePath();
fill(state);
state->clearPath();
return true;
}
void CairoOutputDev::clip(GfxState *state)
{
doPath(cairo, state, state->getPath());
cairo_set_fill_rule(cairo, CAIRO_FILL_RULE_WINDING);
cairo_clip(cairo);
LOG(printf("clip\n"));
if (cairo_shape) {
doPath(cairo_shape, state, state->getPath());
cairo_set_fill_rule(cairo_shape, CAIRO_FILL_RULE_WINDING);
cairo_clip(cairo_shape);
}
}
void CairoOutputDev::eoClip(GfxState *state)
{
doPath(cairo, state, state->getPath());
cairo_set_fill_rule(cairo, CAIRO_FILL_RULE_EVEN_ODD);
cairo_clip(cairo);
LOG(printf("clip-eo\n"));
if (cairo_shape) {
doPath(cairo_shape, state, state->getPath());
cairo_set_fill_rule(cairo_shape, CAIRO_FILL_RULE_EVEN_ODD);
cairo_clip(cairo_shape);
}
}
void CairoOutputDev::clipToStrokePath(GfxState *state)
{
LOG(printf("clip-to-stroke-path\n"));
strokePathClip = (StrokePathClip *)gmalloc(sizeof(*strokePathClip));
strokePathClip->path = state->getPath()->copy();
cairo_get_matrix(cairo, &strokePathClip->ctm);
strokePathClip->line_width = cairo_get_line_width(cairo);
strokePathClip->dash_count = cairo_get_dash_count(cairo);
if (strokePathClip->dash_count) {
strokePathClip->dashes = (double *)gmallocn(sizeof(double), strokePathClip->dash_count);
cairo_get_dash(cairo, strokePathClip->dashes, &strokePathClip->dash_offset);
} else {
strokePathClip->dashes = nullptr;
}
strokePathClip->cap = cairo_get_line_cap(cairo);
strokePathClip->join = cairo_get_line_join(cairo);
strokePathClip->miter = cairo_get_miter_limit(cairo);
strokePathClip->ref_count = 1;
}
void CairoOutputDev::fillToStrokePathClip(GfxState *state)
{
cairo_save(cairo);
cairo_set_matrix(cairo, &strokePathClip->ctm);
cairo_set_line_width(cairo, strokePathClip->line_width);
cairo_set_dash(cairo, strokePathClip->dashes, strokePathClip->dash_count, strokePathClip->dash_offset);
cairo_set_line_cap(cairo, strokePathClip->cap);
cairo_set_line_join(cairo, strokePathClip->join);
cairo_set_miter_limit(cairo, strokePathClip->miter);
doPath(cairo, state, strokePathClip->path);
cairo_stroke(cairo);
cairo_restore(cairo);
}
void CairoOutputDev::beginString(GfxState *state, const GooString *s)
{
int len = s->getLength();
if (needFontUpdate) {
updateFont(state);
}
if (!currentFont) {
return;
}
glyphs = (cairo_glyph_t *)gmallocn(len, sizeof(cairo_glyph_t));
glyphCount = 0;
if (use_show_text_glyphs) {
clusters = (cairo_text_cluster_t *)gmallocn(len, sizeof(cairo_text_cluster_t));
clusterCount = 0;
utf8Max = len * 2; // start with twice the number of glyphs. we will realloc if we need more.
utf8 = (char *)gmalloc(utf8Max);
utf8Count = 0;
}
}
void CairoOutputDev::drawChar(GfxState *state, double x, double y, double dx, double dy, double originX, double originY, CharCode code, int nBytes, const Unicode *u, int uLen)
{
if (currentFont) {
glyphs[glyphCount].index = currentFont->getGlyph(code, u, uLen);
glyphs[glyphCount].x = x - originX;
glyphs[glyphCount].y = y - originY;
glyphCount++;
if (use_show_text_glyphs) {
const UnicodeMap *utf8Map = globalParams->getUtf8Map();
if (utf8Max - utf8Count < uLen * 6) {
// utf8 encoded characters can be up to 6 bytes
if (utf8Max > uLen * 6) {
utf8Max *= 2;
} else {
utf8Max += 2 * uLen * 6;
}
utf8 = (char *)grealloc(utf8, utf8Max);
}
clusters[clusterCount].num_bytes = 0;
for (int i = 0; i < uLen; i++) {
int size = utf8Map->mapUnicode(u[i], utf8 + utf8Count, utf8Max - utf8Count);
utf8Count += size;
clusters[clusterCount].num_bytes += size;
}
clusters[clusterCount].num_glyphs = 1;
clusterCount++;
}
}
if (!textPage) {
return;
}
actualText->addChar(state, x, y, dx, dy, code, nBytes, u, uLen);
}
void CairoOutputDev::endString(GfxState *state)
{
int render;
if (!currentFont) {
return;
}
// endString can be called without a corresponding beginString. If this
// happens glyphs will be null so don't draw anything, just return.
// XXX: OutputDevs should probably not have to deal with this...
if (!glyphs) {
return;
}
// ignore empty strings and invisible text -- this is used by
// Acrobat Capture
render = state->getRender();
if (render == 3 || glyphCount == 0 || !text_matrix_valid) {
goto finish;
}
if (state->getFont()->getType() == fontType3 && render != 7) {
// If the current font is a type 3 font, we should ignore the text rendering mode
// (and use the default of 0) as long as we are going to either fill or stroke.
render = 0;
}
if (!(render & 1)) {
LOG(printf("fill string\n"));
cairo_set_source(cairo, fill_pattern);
if (use_show_text_glyphs) {
cairo_show_text_glyphs(cairo, utf8, utf8Count, glyphs, glyphCount, clusters, clusterCount, (cairo_text_cluster_flags_t)0);
} else {
cairo_show_glyphs(cairo, glyphs, glyphCount);
}
if (cairo_shape) {
cairo_show_glyphs(cairo_shape, glyphs, glyphCount);
}
}
// stroke
if ((render & 3) == 1 || (render & 3) == 2) {
LOG(printf("stroke string\n"));
cairo_set_source(cairo, stroke_pattern);
cairo_glyph_path(cairo, glyphs, glyphCount);
cairo_stroke(cairo);
if (cairo_shape) {
cairo_glyph_path(cairo_shape, glyphs, glyphCount);
cairo_stroke(cairo_shape);
}
}
// clip
if ((render & 4)) {
LOG(printf("clip string\n"));
// append the glyph path to textClipPath.
// set textClipPath as the currentPath
if (textClipPath) {
cairo_append_path(cairo, textClipPath);
if (cairo_shape) {
cairo_append_path(cairo_shape, textClipPath);
}
cairo_path_destroy(textClipPath);
}
// append the glyph path
cairo_glyph_path(cairo, glyphs, glyphCount);
// move the path back into textClipPath
// and clear the current path
textClipPath = cairo_copy_path(cairo);
cairo_new_path(cairo);
if (cairo_shape) {
cairo_new_path(cairo_shape);
}
}
finish:
gfree(glyphs);
glyphs = nullptr;
if (use_show_text_glyphs) {
gfree(clusters);
clusters = nullptr;
gfree(utf8);
utf8 = nullptr;
}
}
bool CairoOutputDev::beginType3Char(GfxState *state, double x, double y, double dx, double dy, CharCode code, const Unicode *u, int uLen)
{
cairo_save(cairo);
cairo_matrix_t matrix;
const double *ctm = state->getCTM();
matrix.xx = ctm[0];
matrix.yx = ctm[1];
matrix.xy = ctm[2];
matrix.yy = ctm[3];
matrix.x0 = ctm[4];
matrix.y0 = ctm[5];
/* Restore the original matrix and then transform to matrix needed for the
* type3 font. This is ugly but seems to work. Perhaps there is a better way to do it?*/
cairo_set_matrix(cairo, &orig_matrix);
cairo_transform(cairo, &matrix);
if (cairo_shape) {
cairo_save(cairo_shape);
cairo_set_matrix(cairo_shape, &orig_matrix);
cairo_transform(cairo_shape, &matrix);
}
cairo_pattern_destroy(stroke_pattern);
cairo_pattern_reference(fill_pattern);
stroke_pattern = fill_pattern;
return false;
}
void CairoOutputDev::endType3Char(GfxState *state)
{
cairo_restore(cairo);
if (cairo_shape) {
cairo_restore(cairo_shape);
}
}
void CairoOutputDev::type3D0(GfxState *state, double wx, double wy)
{
t3_glyph_wx = wx;
t3_glyph_wy = wy;
t3_glyph_has_color = true;
}
void CairoOutputDev::type3D1(GfxState *state, double wx, double wy, double llx, double lly, double urx, double ury)
{
t3_glyph_wx = wx;
t3_glyph_wy = wy;
t3_glyph_bbox[0] = llx;
t3_glyph_bbox[1] = lly;
t3_glyph_bbox[2] = urx;
t3_glyph_bbox[3] = ury;
t3_glyph_has_bbox = true;
t3_glyph_has_color = false;
}
void CairoOutputDev::beginTextObject(GfxState *state) { }
void CairoOutputDev::endTextObject(GfxState *state)
{
if (textClipPath) {
// clip the accumulated text path
cairo_append_path(cairo, textClipPath);
cairo_clip(cairo);
if (cairo_shape) {
cairo_append_path(cairo_shape, textClipPath);
cairo_clip(cairo_shape);
}
cairo_path_destroy(textClipPath);
textClipPath = nullptr;
}
}
void CairoOutputDev::beginActualText(GfxState *state, const GooString *text)
{
if (textPage) {
actualText->begin(state, text);
}
}
void CairoOutputDev::endActualText(GfxState *state)
{
if (textPage) {
actualText->end(state);
}
}
static inline int splashRound(SplashCoord x)
{
return (int)floor(x + 0.5);
}
static inline int splashCeil(SplashCoord x)
{
return (int)ceil(x);
}
static inline int splashFloor(SplashCoord x)
{
return (int)floor(x);
}
static cairo_surface_t *cairo_surface_create_similar_clip(cairo_t *cairo, cairo_content_t content)
{
cairo_pattern_t *pattern;
cairo_surface_t *surface = nullptr;
cairo_push_group_with_content(cairo, content);
pattern = cairo_pop_group(cairo);
cairo_pattern_get_surface(pattern, &surface);
cairo_surface_reference(surface);
cairo_pattern_destroy(pattern);
return surface;
}
void CairoOutputDev::beginTransparencyGroup(GfxState * /*state*/, const double * /*bbox*/, GfxColorSpace *blendingColorSpace, bool /*isolated*/, bool knockout, bool forSoftMask)
{
/* push color space */
ColorSpaceStack *css = new ColorSpaceStack;
css->cs = blendingColorSpace;
css->knockout = knockout;
cairo_get_matrix(cairo, &css->group_matrix);
css->next = groupColorSpaceStack;
groupColorSpaceStack = css;
LOG(printf("begin transparency group. knockout: %s\n", knockout ? "yes" : "no"));
if (knockout) {
knockoutCount++;
if (!cairo_shape) {
/* create a surface for tracking the shape */
cairo_surface_t *cairo_shape_surface = cairo_surface_create_similar_clip(cairo, CAIRO_CONTENT_ALPHA);
cairo_shape = cairo_create(cairo_shape_surface);
cairo_surface_destroy(cairo_shape_surface);
copyAntialias(cairo_shape, cairo);
/* the color doesn't matter as long as it is opaque */
cairo_set_source_rgb(cairo_shape, 0, 0, 0);
cairo_matrix_t matrix;
cairo_get_matrix(cairo, &matrix);
cairo_set_matrix(cairo_shape, &matrix);
}
}
if (groupColorSpaceStack->next && groupColorSpaceStack->next->knockout) {
/* we need to track the shape */
cairo_push_group(cairo_shape);
}
if (false && forSoftMask) {
cairo_push_group_with_content(cairo, CAIRO_CONTENT_ALPHA);
} else {
cairo_push_group(cairo);
}
/* push_group has an implicit cairo_save() */
if (knockout) {
/*XXX: let's hope this matches the semantics needed */
cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE);
} else {
cairo_set_operator(cairo, CAIRO_OPERATOR_OVER);
}
}
void CairoOutputDev::endTransparencyGroup(GfxState * /*state*/)
{
if (group) {
cairo_pattern_destroy(group);
}
group = cairo_pop_group(cairo);
LOG(printf("end transparency group\n"));
if (groupColorSpaceStack->next && groupColorSpaceStack->next->knockout) {
if (shape) {
cairo_pattern_destroy(shape);
}
shape = cairo_pop_group(cairo_shape);
}
}
void CairoOutputDev::paintTransparencyGroup(GfxState * /*state*/, const double * /*bbox*/)
{
LOG(printf("paint transparency group\n"));
cairo_save(cairo);
cairo_set_matrix(cairo, &groupColorSpaceStack->group_matrix);
if (shape) {
/* OPERATOR_SOURCE w/ a mask is defined as (src IN mask) ADD (dest OUT mask)
* however our source has already been clipped to mask so we only need to
* do ADD and OUT */
/* clear the shape mask */
cairo_set_source(cairo, shape);
cairo_set_operator(cairo, CAIRO_OPERATOR_DEST_OUT);
cairo_paint(cairo);
cairo_set_operator(cairo, CAIRO_OPERATOR_ADD);
}
cairo_set_source(cairo, group);
if (!mask) {
cairo_paint_with_alpha(cairo, fill_opacity);
cairo_status_t status = cairo_status(cairo);
if (status) {
printf("BAD status: %s\n", cairo_status_to_string(status));
}
} else {
if (fill_opacity < 1.0) {
cairo_push_group(cairo);
}
cairo_save(cairo);
cairo_set_matrix(cairo, &mask_matrix);
cairo_mask(cairo, mask);
cairo_restore(cairo);
if (fill_opacity < 1.0) {
cairo_pop_group_to_source(cairo);
cairo_paint_with_alpha(cairo, fill_opacity);
}
cairo_pattern_destroy(mask);
mask = nullptr;
}
if (shape) {
if (cairo_shape) {
cairo_set_source(cairo_shape, shape);
cairo_paint(cairo_shape);
cairo_set_source_rgb(cairo_shape, 0, 0, 0);
}
cairo_pattern_destroy(shape);
shape = nullptr;
}
popTransparencyGroup();
cairo_restore(cairo);
}
static int luminocity(uint32_t x)
{
int r = (x >> 16) & 0xff;
int g = (x >> 8) & 0xff;
int b = (x >> 0) & 0xff;
// an arbitrary integer approximation of .3*r + .59*g + .11*b
int y = (r * 19661 + g * 38666 + b * 7209 + 32829) >> 16;
return y;
}
/* XXX: do we need to deal with shape here? */
void CairoOutputDev::setSoftMask(GfxState *state, const double *bbox, bool alpha, Function *transferFunc, GfxColor *backdropColor)
{
cairo_pattern_destroy(mask);
LOG(printf("set softMask\n"));
if (!alpha || transferFunc) {
/* We need to mask according to the luminocity of the group.
* So we paint the group to an image surface convert it to a luminocity map
* and then use that as the mask. */
/* Get clip extents in device space */
double x1, y1, x2, y2, x_min, y_min, x_max, y_max;
cairo_clip_extents(cairo, &x1, &y1, &x2, &y2);
cairo_user_to_device(cairo, &x1, &y1);
cairo_user_to_device(cairo, &x2, &y2);
x_min = MIN(x1, x2);
y_min = MIN(y1, y2);
x_max = MAX(x1, x2);
y_max = MAX(y1, y2);
cairo_clip_extents(cairo, &x1, &y1, &x2, &y2);
cairo_user_to_device(cairo, &x1, &y2);
cairo_user_to_device(cairo, &x2, &y1);
x_min = MIN(x_min, MIN(x1, x2));
y_min = MIN(y_min, MIN(y1, y2));
x_max = MAX(x_max, MAX(x1, x2));
y_max = MAX(y_max, MAX(y1, y2));
int width = (int)(ceil(x_max) - floor(x_min));
int height = (int)(ceil(y_max) - floor(y_min));
/* Get group device offset */
double x_offset, y_offset;
if (cairo_get_group_target(cairo) == cairo_get_target(cairo)) {
cairo_surface_get_device_offset(cairo_get_group_target(cairo), &x_offset, &y_offset);
} else {
cairo_surface_t *pats;
cairo_pattern_get_surface(group, &pats);
cairo_surface_get_device_offset(pats, &x_offset, &y_offset);
}
/* Adjust extents by group offset */
x_min += x_offset;
y_min += y_offset;
cairo_surface_t *source = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
cairo_t *maskCtx = cairo_create(source);
copyAntialias(maskCtx, cairo);
// XXX: hopefully this uses the correct color space */
if (!alpha && groupColorSpaceStack->cs) {
GfxRGB backdropColorRGB;
groupColorSpaceStack->cs->getRGB(backdropColor, &backdropColorRGB);
/* paint the backdrop */
cairo_set_source_rgb(maskCtx, colToDbl(backdropColorRGB.r), colToDbl(backdropColorRGB.g), colToDbl(backdropColorRGB.b));
}
cairo_paint(maskCtx);
/* Copy source ctm to mask ctm and translate origin so that the
* mask appears it the same location on the source surface. */
cairo_matrix_t mat, tmat;
cairo_matrix_init_translate(&tmat, -x_min, -y_min);
cairo_get_matrix(cairo, &mat);
cairo_matrix_multiply(&mat, &mat, &tmat);
cairo_set_matrix(maskCtx, &mat);
/* make the device offset of the new mask match that of the group */
cairo_surface_set_device_offset(source, x_offset, y_offset);
/* paint the group */
cairo_set_source(maskCtx, group);
cairo_paint(maskCtx);
/* XXX status = cairo_status(maskCtx); */
cairo_destroy(maskCtx);
/* convert to a luminocity map */
uint32_t *source_data = reinterpret_cast<uint32_t *>(cairo_image_surface_get_data(source));
if (source_data) {
/* get stride in units of 32 bits */
ptrdiff_t stride = cairo_image_surface_get_stride(source) / 4;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int lum = alpha ? fill_opacity : luminocity(source_data[y * stride + x]);
if (transferFunc) {
double lum_in, lum_out;
lum_in = lum / 256.0;
transferFunc->transform(&lum_in, &lum_out);
lum = (int)(lum_out * 255.0 + 0.5);
}
source_data[y * stride + x] = lum << 24;
}
}
cairo_surface_mark_dirty(source);
}
/* setup the new mask pattern */
mask = cairo_pattern_create_for_surface(source);
cairo_get_matrix(cairo, &mask_matrix);
if (cairo_get_group_target(cairo) == cairo_get_target(cairo)) {
cairo_pattern_set_matrix(mask, &mat);
} else {
cairo_matrix_t patMatrix;
cairo_pattern_get_matrix(group, &patMatrix);
/* Apply x_min, y_min offset to it appears in the same location as source. */
cairo_matrix_multiply(&patMatrix, &patMatrix, &tmat);
cairo_pattern_set_matrix(mask, &patMatrix);
}
cairo_surface_destroy(source);
} else if (alpha) {
mask = cairo_pattern_reference(group);
cairo_get_matrix(cairo, &mask_matrix);
}
popTransparencyGroup();
}
void CairoOutputDev::popTransparencyGroup()
{
/* pop color space */
ColorSpaceStack *css = groupColorSpaceStack;
if (css->knockout) {
knockoutCount--;
if (!knockoutCount) {
/* we don't need to track the shape anymore because
* we are not above any knockout groups */
cairo_destroy(cairo_shape);
cairo_shape = nullptr;
}
}
groupColorSpaceStack = css->next;
delete css;
}
void CairoOutputDev::clearSoftMask(GfxState * /*state*/)
{
if (mask) {
cairo_pattern_destroy(mask);
}
mask = nullptr;
}
/* Taken from cairo/doc/tutorial/src/singular.c */
static void get_singular_values(const cairo_matrix_t *matrix, double *major, double *minor)
{
double xx = matrix->xx, xy = matrix->xy;
double yx = matrix->yx, yy = matrix->yy;
double a = xx * xx + yx * yx;
double b = xy * xy + yy * yy;
double k = xx * xy + yx * yy;
double f = (a + b) * .5;
double g = (a - b) * .5;
double delta = sqrt(g * g + k * k);
if (major) {
*major = sqrt(f + delta);
}
if (minor) {
*minor = sqrt(f - delta);
}
}
void CairoOutputDev::getScaledSize(const cairo_matrix_t *matrix, int orig_width, int orig_height, int *scaledWidth, int *scaledHeight)
{
double xScale;
double yScale;
if (orig_width > orig_height) {
get_singular_values(matrix, &xScale, &yScale);
} else {
get_singular_values(matrix, &yScale, &xScale);
}
int tx, tx2, ty, ty2; /* the integer co-ordinates of the resulting image */
if (xScale >= 0) {
tx = splashRound(matrix->x0 - 0.01);
tx2 = splashRound(matrix->x0 + xScale + 0.01) - 1;
} else {
tx = splashRound(matrix->x0 + 0.01) - 1;
tx2 = splashRound(matrix->x0 + xScale - 0.01);
}
*scaledWidth = abs(tx2 - tx) + 1;
// scaledWidth = splashRound(fabs(xScale));
if (*scaledWidth == 0) {
// technically, this should draw nothing, but it generally seems
// better to draw a one-pixel-wide stripe rather than throwing it
// away
*scaledWidth = 1;
}
if (yScale >= 0) {
ty = splashFloor(matrix->y0 + 0.01);
ty2 = splashCeil(matrix->y0 + yScale - 0.01);
} else {
ty = splashCeil(matrix->y0 - 0.01);
ty2 = splashFloor(matrix->y0 + yScale + 0.01);
}
*scaledHeight = abs(ty2 - ty);
if (*scaledHeight == 0) {
*scaledHeight = 1;
}
}
cairo_filter_t CairoOutputDev::getFilterForSurface(cairo_surface_t *image, bool interpolate)
{
if (interpolate) {
return CAIRO_FILTER_GOOD;
}
int orig_width = cairo_image_surface_get_width(image);
int orig_height = cairo_image_surface_get_height(image);
if (orig_width == 0 || orig_height == 0) {
return CAIRO_FILTER_NEAREST;
}
/* When printing, don't change the interpolation. */
if (printing) {
return CAIRO_FILTER_NEAREST;
}
cairo_matrix_t matrix;
cairo_get_matrix(cairo, &matrix);
int scaled_width, scaled_height;
getScaledSize(&matrix, orig_width, orig_height, &scaled_width, &scaled_height);
/* When scale factor is >= 400% we don't interpolate. See bugs #25268, #9860 */
if (scaled_width / orig_width >= 4 || scaled_height / orig_height >= 4) {
return CAIRO_FILTER_NEAREST;
}
return CAIRO_FILTER_GOOD;
}
void CairoOutputDev::drawImageMask(GfxState *state, Object *ref, Stream *str, int width, int height, bool invert, bool interpolate, bool inlineImg)
{
/* FIXME: Doesn't the image mask support any colorspace? */
cairo_set_source(cairo, fill_pattern);
/* work around a cairo bug when scaling 1x1 surfaces */
if (width == 1 && height == 1) {
ImageStream *imgStr;
unsigned char pix;
int invert_bit;
imgStr = new ImageStream(str, width, 1, 1);
imgStr->reset();
imgStr->getPixel(&pix);
imgStr->close();
delete imgStr;
invert_bit = invert ? 1 : 0;
if (pix ^ invert_bit) {
return;
}
cairo_save(cairo);
cairo_rectangle(cairo, 0., 0., width, height);
cairo_fill(cairo);
cairo_restore(cairo);
if (cairo_shape) {
cairo_save(cairo_shape);
cairo_rectangle(cairo_shape, 0., 0., width, height);
cairo_fill(cairo_shape);
cairo_restore(cairo_shape);
}
return;
}
/* shape is 1.0 for painted areas, 0.0 for unpainted ones */
cairo_matrix_t matrix;
cairo_get_matrix(cairo, &matrix);
// XXX: it is possible that we should only do sub pixel positioning if
// we are rendering fonts */
drawImageMaskRegular(state, ref, str, width, height, invert, interpolate, inlineImg);
}
void CairoOutputDev::setSoftMaskFromImageMask(GfxState *state, Object *ref, Stream *str, int width, int height, bool invert, bool inlineImg, double *baseMatrix)
{
/* FIXME: Doesn't the image mask support any colorspace? */
cairo_set_source(cairo, fill_pattern);
/* work around a cairo bug when scaling 1x1 surfaces */
if (width == 1 && height == 1) {
ImageStream *imgStr;
unsigned char pix;
int invert_bit;
imgStr = new ImageStream(str, width, 1, 1);
imgStr->reset();
imgStr->getPixel(&pix);
imgStr->close();
delete imgStr;
invert_bit = invert ? 1 : 0;
if (!(pix ^ invert_bit)) {
cairo_save(cairo);
cairo_rectangle(cairo, 0., 0., width, height);
cairo_fill(cairo);
cairo_restore(cairo);
if (cairo_shape) {
cairo_save(cairo_shape);
cairo_rectangle(cairo_shape, 0., 0., width, height);
cairo_fill(cairo_shape);
cairo_restore(cairo_shape);
}
}
} else {
cairo_push_group_with_content(cairo, CAIRO_CONTENT_ALPHA);
/* shape is 1.0 for painted areas, 0.0 for unpainted ones */
cairo_matrix_t matrix;
cairo_get_matrix(cairo, &matrix);
// XXX: it is possible that we should only do sub pixel positioning if
// we are rendering fonts */
drawImageMaskRegular(state, ref, str, width, height, invert, false, inlineImg);
if (state->getFillColorSpace()->getMode() == csPattern) {
cairo_set_source_rgb(cairo, 1, 1, 1);
cairo_set_matrix(cairo, &mask_matrix);
cairo_mask(cairo, mask);
}
if (mask) {
cairo_pattern_destroy(mask);
}
mask = cairo_pop_group(cairo);
}
saveState(state);
double bbox[4] = { 0, 0, 1, 1 }; // dummy
beginTransparencyGroup(state, bbox, state->getFillColorSpace(), true, false, false);
}
void CairoOutputDev::unsetSoftMaskFromImageMask(GfxState *state, double *baseMatrix)
{
double bbox[4] = { 0, 0, 1, 1 }; // dummy
endTransparencyGroup(state);
restoreState(state);
paintTransparencyGroup(state, bbox);
clearSoftMask(state);
}
void CairoOutputDev::drawImageMaskRegular(GfxState *state, Object *ref, Stream *str, int width, int height, bool invert, bool interpolate, bool inlineImg)
{
unsigned char *buffer;
unsigned char *dest;
cairo_surface_t *image;
cairo_pattern_t *pattern;
int x, y, i, bit;
ImageStream *imgStr;
unsigned char *pix;
cairo_matrix_t matrix;
int invert_bit;
ptrdiff_t row_stride;
cairo_filter_t filter;
/* TODO: Do we want to cache these? */
imgStr = new ImageStream(str, width, 1, 1);
imgStr->reset();
image = cairo_image_surface_create(CAIRO_FORMAT_A1, width, height);
if (cairo_surface_status(image)) {
goto cleanup;
}
buffer = cairo_image_surface_get_data(image);
row_stride = cairo_image_surface_get_stride(image);
invert_bit = invert ? 1 : 0;
for (y = 0; y < height; y++) {
pix = imgStr->getLine();
dest = buffer + y * row_stride;
i = 0;
bit = 0;
for (x = 0; x < width; x++) {
if (bit == 0) {
dest[i] = 0;
}
if (!(pix[x] ^ invert_bit)) {
#ifdef WORDS_BIGENDIAN
dest[i] |= (1 << (7 - bit));
#else
dest[i] |= (1 << bit);
#endif
}
bit++;
if (bit > 7) {
bit = 0;
i++;
}
}
}
filter = getFilterForSurface(image, interpolate);
cairo_surface_mark_dirty(image);
pattern = cairo_pattern_create_for_surface(image);
cairo_surface_destroy(image);
if (cairo_pattern_status(pattern)) {
goto cleanup;
}
LOG(printf("drawImageMask %dx%d\n", width, height));
cairo_pattern_set_filter(pattern, filter);
cairo_matrix_init_translate(&matrix, 0, height);
cairo_matrix_scale(&matrix, width, -height);
cairo_pattern_set_matrix(pattern, &matrix);
if (cairo_pattern_status(pattern)) {
cairo_pattern_destroy(pattern);
goto cleanup;
}
if (state->getFillColorSpace()->getMode() == csPattern) {
mask = cairo_pattern_reference(pattern);
cairo_get_matrix(cairo, &mask_matrix);
} else if (!printing) {
cairo_save(cairo);
cairo_rectangle(cairo, 0., 0., 1., 1.);
cairo_clip(cairo);
if (strokePathClip) {
cairo_push_group(cairo);
fillToStrokePathClip(state);
cairo_pop_group_to_source(cairo);
}
cairo_mask(cairo, pattern);
cairo_restore(cairo);
} else {
cairo_mask(cairo, pattern);
}
if (cairo_shape) {
cairo_save(cairo_shape);
cairo_set_source(cairo_shape, pattern);
if (!printing) {
cairo_rectangle(cairo_shape, 0., 0., 1., 1.);
cairo_fill(cairo_shape);
} else {
cairo_mask(cairo_shape, pattern);
}
cairo_restore(cairo_shape);
}
cairo_pattern_destroy(pattern);
cleanup:
imgStr->close();
delete imgStr;
}
void CairoOutputDev::drawMaskedImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, bool interpolate, Stream *maskStr, int maskWidth, int maskHeight, bool maskInvert, bool maskInterpolate)
{
ImageStream *maskImgStr, *imgStr;
ptrdiff_t row_stride;
unsigned char *maskBuffer, *buffer;
unsigned char *maskDest;
unsigned int *dest;
cairo_surface_t *maskImage, *image;
cairo_pattern_t *maskPattern, *pattern;
cairo_matrix_t matrix;
cairo_matrix_t maskMatrix;
unsigned char *pix;
int x, y;
int invert_bit;
cairo_filter_t filter;
cairo_filter_t maskFilter;
maskImgStr = new ImageStream(maskStr, maskWidth, 1, 1);
maskImgStr->reset();
maskImage = cairo_image_surface_create(CAIRO_FORMAT_A8, maskWidth, maskHeight);
if (cairo_surface_status(maskImage)) {
maskImgStr->close();
delete maskImgStr;
return;
}
maskBuffer = cairo_image_surface_get_data(maskImage);
row_stride = cairo_image_surface_get_stride(maskImage);
invert_bit = maskInvert ? 1 : 0;
for (y = 0; y < maskHeight; y++) {
pix = maskImgStr->getLine();
maskDest = maskBuffer + y * row_stride;
for (x = 0; x < maskWidth; x++) {
if (pix[x] ^ invert_bit) {
*maskDest++ = 0;
} else {
*maskDest++ = 255;
}
}
}
maskImgStr->close();
delete maskImgStr;
maskFilter = getFilterForSurface(maskImage, maskInterpolate);
cairo_surface_mark_dirty(maskImage);
maskPattern = cairo_pattern_create_for_surface(maskImage);
cairo_surface_destroy(maskImage);
if (cairo_pattern_status(maskPattern)) {
return;
}
#if 0
/* ICCBased color space doesn't do any color correction
* so check its underlying color space as well */
int is_identity_transform;
is_identity_transform = colorMap->getColorSpace()->getMode() == csDeviceRGB ||
(colorMap->getColorSpace()->getMode() == csICCBased &&
((GfxICCBasedColorSpace*)colorMap->getColorSpace())->getAlt()->getMode() == csDeviceRGB);
#endif
/* TODO: Do we want to cache these? */
imgStr = new ImageStream(str, width, colorMap->getNumPixelComps(), colorMap->getBits());
imgStr->reset();
image = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
if (cairo_surface_status(image)) {
goto cleanup;
}
buffer = cairo_image_surface_get_data(image);
row_stride = cairo_image_surface_get_stride(image);
for (y = 0; y < height; y++) {
dest = reinterpret_cast<unsigned int *>(buffer + y * row_stride);
pix = imgStr->getLine();
colorMap->getRGBLine(pix, dest, width);
}
filter = getFilterForSurface(image, interpolate);
cairo_surface_mark_dirty(image);
pattern = cairo_pattern_create_for_surface(image);
cairo_surface_destroy(image);
if (cairo_pattern_status(pattern)) {
goto cleanup;
}