From d12a67626da4f5919b48c513fee80974f603473e Mon Sep 17 00:00:00 2001 From: Hal Canary Date: Fri, 26 May 2017 17:01:16 -0400 Subject: [PATCH] SkPDF: Draw paths with mask filters; color filter. Also: - drawPaint, drawPath w/ perspective shaders - text with mask filters, stroking, path effect. - SkPDFUtils::GetShaderLocalMatrix BUG=skia:237 BUG=skia:238 BUG=skia:5607 Change-Id: Iffeaf2d7abbde13fd2577ce9feaa178657f48364 Reviewed-on: https://skia-review.googlesource.com/18200 Commit-Queue: Hal Canary Reviewed-by: Ben Wagner --- src/pdf/SkPDFDevice.cpp | 241 +++++++++++++++++++++++++++++++++++----- src/pdf/SkPDFDevice.h | 6 + src/pdf/SkPDFShader.cpp | 2 +- src/pdf/SkPDFUtils.h | 9 ++ 4 files changed, 230 insertions(+), 28 deletions(-) diff --git a/src/pdf/SkPDFDevice.cpp b/src/pdf/SkPDFDevice.cpp index 4014101b25..f4cdd864c8 100644 --- a/src/pdf/SkPDFDevice.cpp +++ b/src/pdf/SkPDFDevice.cpp @@ -11,18 +11,19 @@ #include "SkAnnotationKeys.h" #include "SkBitmapDevice.h" #include "SkBitmapKey.h" +#include "SkClipOpPriv.h" #include "SkColor.h" #include "SkColorFilter.h" #include "SkDraw.h" #include "SkDrawFilter.h" #include "SkGlyphCache.h" #include "SkImageFilterCache.h" +#include "SkJpegEncoder.h" #include "SkMakeUnique.h" -#include "SkPath.h" -#include "SkPathEffect.h" -#include "SkPathOps.h" +#include "SkMaskFilter.h" #include "SkPDFBitmap.h" #include "SkPDFCanon.h" +#include "SkPDFCanvas.h" #include "SkPDFDocument.h" #include "SkPDFFont.h" #include "SkPDFFormXObject.h" @@ -31,9 +32,12 @@ #include "SkPDFShader.h" #include "SkPDFTypes.h" #include "SkPDFUtils.h" +#include "SkPath.h" +#include "SkPathEffect.h" +#include "SkPathOps.h" #include "SkPixelRef.h" -#include "SkRasterClip.h" #include "SkRRect.h" +#include "SkRasterClip.h" #include "SkScopeExit.h" #include "SkString.h" #include "SkSurface.h" @@ -42,7 +46,17 @@ #include "SkTextFormatParams.h" #include "SkUtils.h" #include "SkXfermodeInterpretation.h" -#include "SkClipOpPriv.h" + +#ifndef SK_PDF_MASK_QUALITY + // If MASK_QUALITY is in [0,100], will be used for JpegEncoder. + // Otherwise, just encode masks losslessly. + #define SK_PDF_MASK_QUALITY 50 + // Since these masks are used for blurry shadows, we shouldn't need + // high quality. Raise this value if your shadows have visible JPEG + // artifacts. + // If SkJpegEncoder::Encode fails, we will fall back to the lossless + // encoding. +#endif #define DPI_FOR_RASTER_SCALE_ONE 72 @@ -73,6 +87,23 @@ static void replace_srcmode_on_opaque_paint(SkPaint* paint) { } } +// A shader's matrix is: CTMM x LocalMatrix x WrappingLocalMatrix. We want to +// switch to device space, where CTM = I, while keeping the original behavior. +// +// I * LocalMatrix * NewWrappingMatrix = CTM * LocalMatrix +// LocalMatrix * NewWrappingMatrix = CTM * LocalMatrix +// InvLocalMatrix * LocalMatrix * NewWrappingMatrix = InvLocalMatrix * CTM * LocalMatrix +// NewWrappingMatrix = InvLocalMatrix * CTM * LocalMatrix +// +static void transform_shader(SkPaint* paint, const SkMatrix& ctm) { + SkMatrix lm = SkPDFUtils::GetShaderLocalMatrix(paint->getShader()); + SkMatrix lmInv; + if (lm.invert(&lmInv)) { + SkMatrix m = SkMatrix::Concat(SkMatrix::Concat(lmInv, ctm), lm); + paint->setShader(paint->getShader()->makeWithLocalMatrix(m)); + } +} + static void emit_pdf_color(SkColor color, SkWStream* result) { SkASSERT(SkColorGetA(color) == 0xFF); // We handle alpha elsewhere. SkPDFUtils::AppendColorComponent(SkColorGetR(color), result); @@ -122,6 +153,19 @@ static SkImageSubset make_image_subset(const SkBitmap& bitmap) { return imageSubset; } +// If the paint has a color filter, apply the color filter to the shader or the +// paint color. Remove the color filter. +void remove_color_filter(SkPaint* paint) { + if (SkColorFilter* cf = paint->getColorFilter()) { + if (SkShader* shader = paint->getShader()) { + paint->setShader(shader->makeWithColorFilter(paint->refColorFilter())); + } else { + paint->setColor(cf->filterColor(paint->getColor())); + } + paint->setColorFilter(nullptr); + } +} + SkPDFDevice::GraphicStateEntry::GraphicStateEntry() : fColor(SK_ColorBLACK) , fTextScaleX(SK_Scalar1) @@ -544,12 +588,20 @@ void SkPDFDevice::drawAnnotation(const SkRect& rect, const char key[], SkData* v } } -void SkPDFDevice::drawPaint(const SkPaint& paint) { - SkPaint newPaint = paint; +void SkPDFDevice::drawPaint(const SkPaint& srcPaint) { + SkPaint newPaint = srcPaint; + remove_color_filter(&newPaint); replace_srcmode_on_opaque_paint(&newPaint); - newPaint.setStyle(SkPaint::kFill_Style); - ScopedContentEntry content(this, newPaint); + + SkMatrix ctm = this->ctm(); + if (ctm.getType() & SkMatrix::kPerspective_Mask) { + if (newPaint.getShader()) { + transform_shader(&newPaint, ctm); + } + ctm = SkMatrix::I(); + } + ScopedContentEntry content(this, this->cs(), ctm, newPaint); this->internalDrawPaint(newPaint, content.entry()); } @@ -576,6 +628,7 @@ void SkPDFDevice::drawPoints(SkCanvas::PointMode mode, const SkPoint* points, const SkPaint& srcPaint) { SkPaint passedPaint = srcPaint; + remove_color_filter(&passedPaint); replace_srcmode_on_opaque_paint(&passedPaint); if (count == 0) { @@ -703,11 +756,12 @@ static sk_sp create_link_named_dest(const SkData* nameData, void SkPDFDevice::drawRect(const SkRect& rect, const SkPaint& srcPaint) { SkPaint paint = srcPaint; + remove_color_filter(&paint); replace_srcmode_on_opaque_paint(&paint); SkRect r = rect; r.sort(); - if (paint.getPathEffect()) { + if (paint.getPathEffect() || paint.getMaskFilter()) { if (this->cs().isEmpty(size(*this))) { return; } @@ -729,6 +783,7 @@ void SkPDFDevice::drawRect(const SkRect& rect, void SkPDFDevice::drawRRect(const SkRRect& rrect, const SkPaint& srcPaint) { SkPaint paint = srcPaint; + remove_color_filter(&paint); replace_srcmode_on_opaque_paint(&paint); SkPath path; path.addRRect(rrect); @@ -738,6 +793,7 @@ void SkPDFDevice::drawRRect(const SkRRect& rrect, void SkPDFDevice::drawOval(const SkRect& oval, const SkPaint& srcPaint) { SkPaint paint = srcPaint; + remove_color_filter(&paint); replace_srcmode_on_opaque_paint(&paint); SkPath path; path.addOval(oval); @@ -752,6 +808,91 @@ void SkPDFDevice::drawPath(const SkPath& origPath, this->cs(), this->ctm(), origPath, srcPaint, prePathMatrix, pathIsMutable); } +void SkPDFDevice::internalDrawPathWithFilter(const SkClipStack& clipStack, + const SkMatrix& ctm, + const SkPath& origPath, + const SkPaint& origPaint, + const SkMatrix* prePathMatrix) { + SkASSERT(origPaint.getMaskFilter()); + SkPath path(origPath); + SkTCopyOnFirstWrite paint(origPaint); + if (prePathMatrix) { + path.transform(*prePathMatrix, &path); + } + SkStrokeRec::InitStyle initStyle = paint->getFillPath(path, &path) + ? SkStrokeRec::kFill_InitStyle + : SkStrokeRec::kHairline_InitStyle; + path.transform(ctm, &path); + + // TODO(halcanary): respect fRasterDpi. + // SkScalar rasterScale = (float)fRasterDpi / DPI_FOR_RASTER_SCALE_ONE; + // Would it be easier to just change the device size (and pre-scale the canvas)? + SkIRect bounds = clipStack.bounds(size(*this)).roundOut(); + SkMask sourceMask; + if (!SkDraw::DrawToMask(path, &bounds, paint->getMaskFilter(), &SkMatrix::I(), + &sourceMask, SkMask::kComputeBoundsAndRenderImage_CreateMode, + initStyle)) { + return; + } + SkAutoMaskFreeImage srcAutoMaskFreeImage(sourceMask.fImage); + SkMask dstMask; + SkIPoint margin; + if (!paint->getMaskFilter()->filterMask(&dstMask, sourceMask, ctm, &margin)) { + return; + } + SkPixmap pm(SkImageInfo::Make(dstMask.fBounds.width(), dstMask.fBounds.height(), + kGray_8_SkColorType, kOpaque_SkAlphaType), + dstMask.fImage, dstMask.fRowBytes); + sk_sp mask; + const int maskQuality = SK_PDF_MASK_QUALITY; + if (maskQuality <= 100 && maskQuality >= 0) { + SkDynamicMemoryWStream buffer; + SkJpegEncoder::Options jpegOptions; + jpegOptions.fQuality = maskQuality; + if (SkJpegEncoder::Encode(&buffer, pm, jpegOptions)) { + mask = SkImage::MakeFromEncoded(buffer.detachAsData()); + SkASSERT(mask); + if (mask) { + SkMask::FreeImage(dstMask.fImage); + } + } + } + if (!mask) { + mask = SkImage::MakeFromRaster( + pm, [](const void* p, void*) { SkMask::FreeImage((void*)p); }, nullptr); + } + // PDF doesn't seem to allow masking vector graphics with an Image XObject. + // Must mask with a Form XObject. + sk_sp maskDevice(SkPDFDevice::CreateUnflipped(fPageSize, fRasterDpi, fDocument)); + { + SkPDFCanvas canvas(maskDevice); + canvas.drawImage(mask, dstMask.fBounds.x(), dstMask.fBounds.y()); + } + sk_sp sMaskGS = SkPDFGraphicState::GetSMaskGraphicState( + maskDevice->makeFormXObjectFromDevice(), false, + SkPDFGraphicState::kLuminosity_SMaskMode, fDocument->canon()); + maskDevice = nullptr; + if (!ctm.isIdentity() && paint->getShader()) { + transform_shader(paint.writable(), ctm); // Since we are using identity matrix. + } + ScopedContentEntry content(this, clipStack, SkMatrix::I(), *paint); + if (!content.entry()) { + return; + } + SkPDFUtils::ApplyGraphicState(this->addGraphicStateResource(sMaskGS.get()), + &content.entry()->fContent); + + SkRect dstBounds = SkRect::Make(dstMask.fBounds); + SkPDFUtils::AppendRectangle(dstBounds, &content.entry()->fContent); + SkPDFUtils::PaintPath(SkPaint::kFill_Style, path.getFillType(), &content.entry()->fContent); + + // The no-softmask graphic state is used to "turn off" the mask for later draw calls. + auto noSMaskGS = SkPDFUtils::GetCachedT(&fDocument->canon()->fNoSmaskGraphicState, + &SkPDFGraphicState::MakeNoSmaskGraphicState); + SkPDFUtils::ApplyGraphicState(this->addGraphicStateResource(noSMaskGS.get()), + &content.entry()->fContent); +} + void SkPDFDevice::internalDrawPath(const SkClipStack& clipStack, const SkMatrix& ctm, const SkPath& origPath, @@ -759,10 +900,16 @@ void SkPDFDevice::internalDrawPath(const SkClipStack& clipStack, const SkMatrix* prePathMatrix, bool pathIsMutable) { SkPaint paint = srcPaint; + remove_color_filter(&paint); replace_srcmode_on_opaque_paint(&paint); SkPath modifiedPath; SkPath* pathPtr = const_cast(&origPath); + if (paint.getMaskFilter()) { + this->internalDrawPathWithFilter(clipStack, ctm, origPath, paint, prePathMatrix); + return; + } + SkMatrix matrix = ctm; if (prePathMatrix) { if (paint.getPathEffect() || paint.getStyle() != SkPaint::kFill_Style) { @@ -781,26 +928,34 @@ void SkPDFDevice::internalDrawPath(const SkClipStack& clipStack, return; } if (!pathIsMutable) { + modifiedPath = origPath; pathPtr = &modifiedPath; pathIsMutable = true; } - bool fill = paint.getFillPath(origPath, pathPtr); - - SkPaint noEffectPaint(paint); - noEffectPaint.setPathEffect(nullptr); - if (fill) { - noEffectPaint.setStyle(SkPaint::kFill_Style); + if (paint.getFillPath(*pathPtr, pathPtr)) { + paint.setStyle(SkPaint::kFill_Style); } else { - noEffectPaint.setStyle(SkPaint::kStroke_Style); - noEffectPaint.setStrokeWidth(0); + paint.setStyle(SkPaint::kStroke_Style); + paint.setStrokeWidth(0); } - this->internalDrawPath(clipStack, ctm, *pathPtr, noEffectPaint, nullptr, true); - return; + paint.setPathEffect(nullptr); } - if (this->handleInversePath(origPath, paint, pathIsMutable, prePathMatrix)) { + if (this->handleInversePath(*pathPtr, paint, pathIsMutable, prePathMatrix)) { return; } + if (matrix.getType() & SkMatrix::kPerspective_Mask) { + if (!pathIsMutable) { + modifiedPath = origPath; + pathPtr = &modifiedPath; + pathIsMutable = true; + } + pathPtr->transform(matrix); + if (paint.getShader()) { + transform_shader(&paint, matrix); + } + matrix = SkMatrix::I(); + } ScopedContentEntry content(this, clipStack, matrix, paint); if (!content.entry()) { @@ -1237,15 +1392,41 @@ static void update_font(SkWStream* wStream, int fontIndex, SkScalar textSize) { wStream->writeText(" Tf\n"); } +static SkPath draw_text_as_path(const void* sourceText, size_t sourceByteCount, + const SkScalar pos[], SkTextBlob::GlyphPositioning positioning, + SkPoint offset, const SkPaint& srcPaint) { + SkPath path; + int glyphCount; + SkAutoTMalloc tmpPoints; + switch (positioning) { + case SkTextBlob::kDefault_Positioning: + srcPaint.getTextPath(sourceText, sourceByteCount, offset.x(), offset.y(), &path); + break; + case SkTextBlob::kHorizontal_Positioning: + glyphCount = srcPaint.countText(sourceText, sourceByteCount); + tmpPoints.realloc(glyphCount); + for (int i = 0; i < glyphCount; ++i) { + tmpPoints[i] = {pos[i] + offset.x(), offset.y()}; + } + srcPaint.getPosTextPath(sourceText, sourceByteCount, tmpPoints.get(), &path); + break; + case SkTextBlob::kFull_Positioning: + srcPaint.getPosTextPath(sourceText, sourceByteCount, (const SkPoint*)pos, &path); + path.offset(offset.x(), offset.y()); + break; + } + return path; +} + void SkPDFDevice::internalDrawText( const void* sourceText, size_t sourceByteCount, const SkScalar pos[], SkTextBlob::GlyphPositioning positioning, SkPoint offset, const SkPaint& srcPaint, const uint32_t* clusters, uint32_t textByteLength, const char* utf8Text) { - NOT_IMPLEMENTED(srcPaint.getMaskFilter() != nullptr, false); - if (srcPaint.getMaskFilter() != nullptr) { - // Don't pretend we support drawing MaskFilters, it makes for artifacts - // making text unreadable (e.g. same text twice when using CSS shadows). + if (0 == sourceByteCount || !sourceText) { + return; + } + if (this->cs().isEmpty(size(*this))) { return; } NOT_IMPLEMENTED(srcPaint.isVerticalText(), false); @@ -1254,12 +1435,18 @@ void SkPDFDevice::internalDrawText( // clear to me how to switch to "vertical writing" mode in PDF. // Currently neither Chromium or Android set this flag. // https://bug.skia.org/5665 - return; } - if (0 == sourceByteCount || !sourceText) { + if (srcPaint.getPathEffect() + || srcPaint.getMaskFilter() + || SkPaint::kFill_Style != srcPaint.getStyle()) { + // Stroked Text doesn't work well with Type3 fonts. + SkPath path = draw_text_as_path(sourceText, sourceByteCount, pos, + positioning, offset, srcPaint); + this->drawPath(path, srcPaint, nullptr, true); return; } SkPaint paint = calculate_text_paint(srcPaint); + remove_color_filter(&paint); replace_srcmode_on_opaque_paint(&paint); if (!paint.getTypeface()) { paint.setTypeface(SkTypeface::MakeDefault()); diff --git a/src/pdf/SkPDFDevice.h b/src/pdf/SkPDFDevice.h index 1906bb37a7..61c08c397d 100644 --- a/src/pdf/SkPDFDevice.h +++ b/src/pdf/SkPDFDevice.h @@ -274,6 +274,12 @@ private: const SkMatrix* prePathMatrix, bool pathIsMutable); + void internalDrawPathWithFilter(const SkClipStack& clipStack, + const SkMatrix& ctm, + const SkPath& origPath, + const SkPaint& paint, + const SkMatrix* prePathMatrix); + bool handleInversePath(const SkPath& origPath, const SkPaint& paint, bool pathIsMutable, const SkMatrix* prePathMatrix = nullptr); diff --git a/src/pdf/SkPDFShader.cpp b/src/pdf/SkPDFShader.cpp index b6b096d22c..49af685d7a 100644 --- a/src/pdf/SkPDFShader.cpp +++ b/src/pdf/SkPDFShader.cpp @@ -1230,7 +1230,7 @@ SkPDFShader::State::State(SkShader* shader, const SkMatrix& canvasTransform, if (fType != SkShader::kNone_GradientType) { fBitmapKey = SkBitmapKey{{0, 0, 0, 0}, 0}; - fShaderTransform = shader->getLocalMatrix(); + fShaderTransform = SkPDFUtils::GetShaderLocalMatrix(shader); this->allocateGradientInfoStorage(); shader->asAGradient(&fInfo); return; diff --git a/src/pdf/SkPDFUtils.h b/src/pdf/SkPDFUtils.h index 00c4b72b7b..c1d9a72250 100644 --- a/src/pdf/SkPDFUtils.h +++ b/src/pdf/SkPDFUtils.h @@ -11,6 +11,7 @@ #include "SkPaint.h" #include "SkPath.h" +#include "SkShader.h" #include "SkStream.h" #include "SkUtils.h" @@ -111,6 +112,14 @@ static sk_sp GetCachedT(sk_sp* cachedT, sk_sp (*makeNewT)()) { *cachedT = (*makeNewT)(); return *cachedT; } + +inline SkMatrix GetShaderLocalMatrix(const SkShader* shader) { + SkMatrix localMatrix; + if (sk_sp s = shader->makeAsALocalMatrixShader(&localMatrix)) { + return SkMatrix::Concat(s->getLocalMatrix(), localMatrix); + } + return shader->getLocalMatrix(); +} } // namespace SkPDFUtils #endif