| /* |
| * 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 "SkPDFDocument.h" |
| #include "SkPDFDevice.h" |
| #include "SkPDFDocumentPriv.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 SkPDFIndirectReference 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 SkPDFIndirectReference(); |
| } |
| |
| 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); |
| |
| 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 = patternDevice->content(); |
| std::unique_ptr<SkPDFDict> resourceDict = patternDevice->makeResourceDict(); |
| std::unique_ptr<SkPDFDict> dict = SkPDFMakeDict(); |
| SkPDFUtils::PopulateTilingPatternDict(dict.get(), patternBBox, |
| std::move(resourceDict), finalMatrix); |
| return SkPDFStreamOut(std::move(dict), std::move(imageShader), doc); |
| } |
| |
| // Generic fallback for unsupported shaders: |
| // * allocate a surfaceBBox-sized bitmap |
| // * shade the whole area |
| // * use the result as a bitmap shader |
| static SkPDFIndirectReference 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 SkPDFIndirectReference(); |
| } |
| // Clamp the bitmap size to about 1M pixels |
| static const SkScalar kMaxBitmapArea = 1024 * 1024; |
| SkScalar bitmapArea = surfaceBBox.width() * surfaceBBox.height(); |
| SkScalar rasterScale = 1.0f; |
| 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; |
| } |
| |
| SkPDFIndirectReference 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 SkPDFIndirectReference(); |
| } |
| 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); |
| SkPDFIndirectReference* shaderPtr = doc->fImageShaderMap.find(key); |
| if (shaderPtr) { |
| return *shaderPtr; |
| } |
| SkPDFIndirectReference pdfShader = make_image_shader(doc, key, skimg); |
| doc->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); |
| } |