blob: 73a6e6b350ce8fb395ed3ac292b4c9f3f9434a49 [file] [log] [blame]
/*
* Copyright 2011 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "SkPDFShader.h"
#include "SkData.h"
#include "SkPDFCanon.h"
#include "SkPDFDevice.h"
#include "SkPDFDocument.h"
#include "SkPDFFormXObject.h"
#include "SkPDFGradientShader.h"
#include "SkPDFGraphicState.h"
#include "SkPDFResourceDict.h"
#include "SkPDFUtils.h"
#include "SkScalar.h"
#include "SkStream.h"
#include "SkSurface.h"
#include "SkTemplates.h"
static void draw_image_matrix(SkCanvas* canvas, const SkImage* img,
const SkMatrix& matrix, const SkPaint& paint) {
SkAutoCanvasRestore acr(canvas, true);
canvas->concat(matrix);
canvas->drawImage(img, 0, 0, &paint);
}
static void draw_bitmap_matrix(SkCanvas* canvas, const SkBitmap& bm,
const SkMatrix& matrix, const SkPaint& paint) {
SkAutoCanvasRestore acr(canvas, true);
canvas->concat(matrix);
canvas->drawBitmap(bm, 0, 0, &paint);
}
static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
const SkPDFImageShaderKey& key,
SkImage* image) {
SkASSERT(image);
// The image shader pattern cell will be drawn into a separate device
// in pattern cell space (no scaling on the bitmap, though there may be
// translations so that all content is in the device, coordinates > 0).
// Map clip bounds to shader space to ensure the device is large enough
// to handle fake clamping.
SkMatrix finalMatrix = key.fCanvasTransform;
finalMatrix.preConcat(key.fShaderTransform);
SkRect deviceBounds = SkRect::Make(key.fBBox);
if (!SkPDFUtils::InverseTransformBBox(finalMatrix, &deviceBounds)) {
return nullptr;
}
SkRect bitmapBounds = SkRect::Make(image->bounds());
// For tiling modes, the bounds should be extended to include the bitmap,
// otherwise the bitmap gets clipped out and the shader is empty and awful.
// For clamp modes, we're only interested in the clip region, whether
// or not the main bitmap is in it.
SkShader::TileMode tileModes[2];
tileModes[0] = key.fImageTileModes[0];
tileModes[1] = key.fImageTileModes[1];
if (tileModes[0] != SkShader::kClamp_TileMode ||
tileModes[1] != SkShader::kClamp_TileMode) {
deviceBounds.join(bitmapBounds);
}
SkISize patternDeviceSize = {SkScalarCeilToInt(deviceBounds.width()),
SkScalarCeilToInt(deviceBounds.height())};
auto patternDevice = sk_make_sp<SkPDFDevice>(patternDeviceSize, doc);
SkCanvas canvas(patternDevice.get());
SkRect patternBBox = SkRect::Make(image->bounds());
// Translate the canvas so that the bitmap origin is at (0, 0).
canvas.translate(-deviceBounds.left(), -deviceBounds.top());
patternBBox.offset(-deviceBounds.left(), -deviceBounds.top());
// Undo the translation in the final matrix
finalMatrix.preTranslate(deviceBounds.left(), deviceBounds.top());
// If the bitmap is out of bounds (i.e. clamp mode where we only see the
// stretched sides), canvas will clip this out and the extraneous data
// won't be saved to the PDF.
canvas.drawImage(image, 0, 0);
SkScalar width = SkIntToScalar(image->width());
SkScalar height = SkIntToScalar(image->height());
SkPaint paint;
paint.setColor(key.fPaintColor);
// Tiling is implied. First we handle mirroring.
if (tileModes[0] == SkShader::kMirror_TileMode) {
SkMatrix xMirror;
xMirror.setScale(-1, 1);
xMirror.postTranslate(2 * width, 0);
draw_image_matrix(&canvas, image, xMirror, paint);
patternBBox.fRight += width;
}
if (tileModes[1] == SkShader::kMirror_TileMode) {
SkMatrix yMirror;
yMirror.setScale(SK_Scalar1, -SK_Scalar1);
yMirror.postTranslate(0, 2 * height);
draw_image_matrix(&canvas, image, yMirror, paint);
patternBBox.fBottom += height;
}
if (tileModes[0] == SkShader::kMirror_TileMode &&
tileModes[1] == SkShader::kMirror_TileMode) {
SkMatrix mirror;
mirror.setScale(-1, -1);
mirror.postTranslate(2 * width, 2 * height);
draw_image_matrix(&canvas, image, mirror, paint);
}
// Then handle Clamping, which requires expanding the pattern canvas to
// cover the entire surfaceBBox.
SkBitmap bitmap;
if (tileModes[0] == SkShader::kClamp_TileMode ||
tileModes[1] == SkShader::kClamp_TileMode) {
// For now, the easiest way to access the colors in the corners and sides is
// to just make a bitmap from the image.
if (!SkPDFUtils::ToBitmap(image, &bitmap)) {
bitmap.allocN32Pixels(image->width(), image->height());
bitmap.eraseColor(0x00000000);
}
}
// If both x and y are in clamp mode, we start by filling in the corners.
// (Which are just a rectangles of the corner colors.)
if (tileModes[0] == SkShader::kClamp_TileMode &&
tileModes[1] == SkShader::kClamp_TileMode) {
SkASSERT(!bitmap.drawsNothing());
SkPaint paint;
SkRect rect;
rect = SkRect::MakeLTRB(deviceBounds.left(), deviceBounds.top(), 0, 0);
if (!rect.isEmpty()) {
paint.setColor(bitmap.getColor(0, 0));
canvas.drawRect(rect, paint);
}
rect = SkRect::MakeLTRB(width, deviceBounds.top(),
deviceBounds.right(), 0);
if (!rect.isEmpty()) {
paint.setColor(bitmap.getColor(bitmap.width() - 1, 0));
canvas.drawRect(rect, paint);
}
rect = SkRect::MakeLTRB(width, height,
deviceBounds.right(), deviceBounds.bottom());
if (!rect.isEmpty()) {
paint.setColor(bitmap.getColor(bitmap.width() - 1,
bitmap.height() - 1));
canvas.drawRect(rect, paint);
}
rect = SkRect::MakeLTRB(deviceBounds.left(), height,
0, deviceBounds.bottom());
if (!rect.isEmpty()) {
paint.setColor(bitmap.getColor(0, bitmap.height() - 1));
canvas.drawRect(rect, paint);
}
}
// Then expand the left, right, top, then bottom.
if (tileModes[0] == SkShader::kClamp_TileMode) {
SkASSERT(!bitmap.drawsNothing());
SkIRect subset = SkIRect::MakeXYWH(0, 0, 1, bitmap.height());
if (deviceBounds.left() < 0) {
SkBitmap left;
SkAssertResult(bitmap.extractSubset(&left, subset));
SkMatrix leftMatrix;
leftMatrix.setScale(-deviceBounds.left(), 1);
leftMatrix.postTranslate(deviceBounds.left(), 0);
draw_bitmap_matrix(&canvas, left, leftMatrix, paint);
if (tileModes[1] == SkShader::kMirror_TileMode) {
leftMatrix.postScale(SK_Scalar1, -SK_Scalar1);
leftMatrix.postTranslate(0, 2 * height);
draw_bitmap_matrix(&canvas, left, leftMatrix, paint);
}
patternBBox.fLeft = 0;
}
if (deviceBounds.right() > width) {
SkBitmap right;
subset.offset(bitmap.width() - 1, 0);
SkAssertResult(bitmap.extractSubset(&right, subset));
SkMatrix rightMatrix;
rightMatrix.setScale(deviceBounds.right() - width, 1);
rightMatrix.postTranslate(width, 0);
draw_bitmap_matrix(&canvas, right, rightMatrix, paint);
if (tileModes[1] == SkShader::kMirror_TileMode) {
rightMatrix.postScale(SK_Scalar1, -SK_Scalar1);
rightMatrix.postTranslate(0, 2 * height);
draw_bitmap_matrix(&canvas, right, rightMatrix, paint);
}
patternBBox.fRight = deviceBounds.width();
}
}
if (tileModes[1] == SkShader::kClamp_TileMode) {
SkASSERT(!bitmap.drawsNothing());
SkIRect subset = SkIRect::MakeXYWH(0, 0, bitmap.width(), 1);
if (deviceBounds.top() < 0) {
SkBitmap top;
SkAssertResult(bitmap.extractSubset(&top, subset));
SkMatrix topMatrix;
topMatrix.setScale(SK_Scalar1, -deviceBounds.top());
topMatrix.postTranslate(0, deviceBounds.top());
draw_bitmap_matrix(&canvas, top, topMatrix, paint);
if (tileModes[0] == SkShader::kMirror_TileMode) {
topMatrix.postScale(-1, 1);
topMatrix.postTranslate(2 * width, 0);
draw_bitmap_matrix(&canvas, top, topMatrix, paint);
}
patternBBox.fTop = 0;
}
if (deviceBounds.bottom() > height) {
SkBitmap bottom;
subset.offset(0, bitmap.height() - 1);
SkAssertResult(bitmap.extractSubset(&bottom, subset));
SkMatrix bottomMatrix;
bottomMatrix.setScale(SK_Scalar1, deviceBounds.bottom() - height);
bottomMatrix.postTranslate(0, height);
draw_bitmap_matrix(&canvas, bottom, bottomMatrix, paint);
if (tileModes[0] == SkShader::kMirror_TileMode) {
bottomMatrix.postScale(-1, 1);
bottomMatrix.postTranslate(2 * width, 0);
draw_bitmap_matrix(&canvas, bottom, bottomMatrix, paint);
}
patternBBox.fBottom = deviceBounds.height();
}
}
auto imageShader = sk_make_sp<SkPDFStream>(patternDevice->content());
SkPDFUtils::PopulateTilingPatternDict(imageShader->dict(), patternBBox,
patternDevice->makeResourceDict(), finalMatrix);
return imageShader;
}
// Generic fallback for unsupported shaders:
// * allocate a surfaceBBox-sized bitmap
// * shade the whole area
// * use the result as a bitmap shader
static sk_sp<SkPDFObject> make_fallback_shader(SkPDFDocument* doc,
SkShader* shader,
const SkMatrix& canvasTransform,
const SkIRect& surfaceBBox,
SkColor paintColor) {
// TODO(vandebo) This drops SKComposeShader on the floor. We could
// handle compose shader by pulling things up to a layer, drawing with
// the first shader, applying the xfer mode and drawing again with the
// second shader, then applying the layer to the original drawing.
SkPDFImageShaderKey key = {
canvasTransform,
SkMatrix::I(),
surfaceBBox,
{{0, 0, 0, 0}, 0}, // don't need the key; won't de-dup.
{SkShader::kClamp_TileMode, SkShader::kClamp_TileMode},
paintColor};
key.fShaderTransform = shader->getLocalMatrix();
// surfaceBBox is in device space. While that's exactly what we
// want for sizing our bitmap, we need to map it into
// shader space for adjustments (to match
// MakeImageShader's behavior).
SkRect shaderRect = SkRect::Make(surfaceBBox);
if (!SkPDFUtils::InverseTransformBBox(canvasTransform, &shaderRect)) {
return nullptr;
}
// Clamp the bitmap size to about 1M pixels
static const SkScalar kMaxBitmapArea = 1024 * 1024;
SkScalar rasterScale = SkIntToScalar(doc->rasterDpi()) / SkPDFUtils::kDpiForRasterScaleOne;
SkScalar bitmapArea = rasterScale * surfaceBBox.width() * rasterScale * surfaceBBox.height();
if (bitmapArea > kMaxBitmapArea) {
rasterScale *= SkScalarSqrt(kMaxBitmapArea / bitmapArea);
}
SkISize size = {SkScalarRoundToInt(rasterScale * surfaceBBox.width()),
SkScalarRoundToInt(rasterScale * surfaceBBox.height())};
SkSize scale = {SkIntToScalar(size.width()) / shaderRect.width(),
SkIntToScalar(size.height()) / shaderRect.height()};
auto surface = SkSurface::MakeRasterN32Premul(size.width(), size.height());
SkCanvas* canvas = surface->getCanvas();
canvas->clear(SK_ColorTRANSPARENT);
SkPaint p;
p.setShader(sk_ref_sp(shader));
p.setColor(paintColor);
canvas->scale(scale.width(), scale.height());
canvas->translate(-shaderRect.x(), -shaderRect.y());
canvas->drawPaint(p);
key.fShaderTransform.setTranslate(shaderRect.x(), shaderRect.y());
key.fShaderTransform.preScale(1 / scale.width(), 1 / scale.height());
sk_sp<SkImage> image = surface->makeImageSnapshot();
return make_image_shader(doc, key, image.get());
}
static SkColor adjust_color(SkShader* shader, SkColor paintColor) {
if (SkImage* img = shader->isAImage(nullptr, nullptr)) {
if (img->isAlphaOnly()) {
return paintColor;
}
}
// only preserve the alpha.
return paintColor & SK_ColorBLACK;
}
sk_sp<SkPDFObject> SkPDFMakeShader(SkPDFDocument* doc,
SkShader* shader,
const SkMatrix& canvasTransform,
const SkIRect& surfaceBBox,
SkColor paintColor) {
SkASSERT(shader);
SkASSERT(doc);
if (SkShader::kNone_GradientType != shader->asAGradient(nullptr)) {
return SkPDFGradientShader::Make(doc, shader, canvasTransform, surfaceBBox);
}
if (surfaceBBox.isEmpty()) {
return nullptr;
}
SkBitmap image;
SkPDFImageShaderKey key = {
canvasTransform,
SkMatrix::I(),
surfaceBBox,
{{0, 0, 0, 0}, 0},
{SkShader::kClamp_TileMode, SkShader::kClamp_TileMode},
adjust_color(shader, paintColor)};
SkASSERT(shader->asAGradient(nullptr) == SkShader::kNone_GradientType) ;
if (SkImage* skimg = shader->isAImage(&key.fShaderTransform, key.fImageTileModes)) {
key.fBitmapKey = SkBitmapKeyFromImage(skimg);
SkPDFCanon* canon = doc->canon();
sk_sp<SkPDFObject>* shaderPtr = canon->fImageShaderMap.find(key);
if (shaderPtr) {
return *shaderPtr;
}
sk_sp<SkPDFObject> pdfShader = make_image_shader(doc, key, skimg);
canon->fImageShaderMap.set(std::move(key), pdfShader);
return pdfShader;
}
// Don't bother to de-dup fallback shader.
return make_fallback_shader(doc, shader, canvasTransform, surfaceBBox, key.fPaintColor);
}