static void draw_bbox(SkCanvas* canvas, const SkRect& r) { SkPaint paint; paint.setStyle(SkPaint::kStroke_Style); paint.setColor(0xFFFF7088); canvas->drawRect(r, paint); canvas->drawLine(r.left(), r.top(), r.right(), r.bottom(), paint); canvas->drawLine(r.left(), r.bottom(), r.right(), r.top(), paint); }
void draw(SkCanvas* canvas) { SkRect rect = SkRect::MakeLTRB(5, 35, 15, 25); SkDebugf("rect: %g, %g, %g, %g isEmpty: %s\n", rect.left(), rect.top(), rect.right(), rect.bottom(), rect.isEmpty() ? "true" : "false"); rect.sort(); SkDebugf("rect: %g, %g, %g, %g isEmpty: %s\n", rect.left(), rect.top(), rect.right(), rect.bottom(), rect.isEmpty() ? "true" : "false"); }
static void convolve_gaussian(GrDrawContext* drawContext, GrRenderTarget* rt, const GrClip& clip, const SkRect& srcRect, const SkRect& dstRect, GrTexture* texture, Gr1DKernelEffect::Direction direction, int radius, float sigma, bool cropToSrcRect) { float bounds[2] = { 0.0f, 1.0f }; if (!cropToSrcRect) { convolve_gaussian_1d(drawContext, rt, clip, srcRect, dstRect, texture, direction, radius, sigma, false, bounds); return; } SkRect lowerSrcRect = srcRect, lowerDstRect = dstRect; SkRect middleSrcRect = srcRect, middleDstRect = dstRect; SkRect upperSrcRect = srcRect, upperDstRect = dstRect; SkScalar size; SkScalar rad = SkIntToScalar(radius); if (direction == Gr1DKernelEffect::kX_Direction) { bounds[0] = SkScalarToFloat(srcRect.left()) / texture->width(); bounds[1] = SkScalarToFloat(srcRect.right()) / texture->width(); size = srcRect.width(); lowerSrcRect.fRight = srcRect.left() + rad; lowerDstRect.fRight = dstRect.left() + rad; upperSrcRect.fLeft = srcRect.right() - rad; upperDstRect.fLeft = dstRect.right() - rad; middleSrcRect.inset(rad, 0); middleDstRect.inset(rad, 0); } else { bounds[0] = SkScalarToFloat(srcRect.top()) / texture->height(); bounds[1] = SkScalarToFloat(srcRect.bottom()) / texture->height(); size = srcRect.height(); lowerSrcRect.fBottom = srcRect.top() + rad; lowerDstRect.fBottom = dstRect.top() + rad; upperSrcRect.fTop = srcRect.bottom() - rad; upperDstRect.fTop = dstRect.bottom() - rad; middleSrcRect.inset(0, rad); middleDstRect.inset(0, rad); } if (radius >= size * SK_ScalarHalf) { // Blur radius covers srcRect; use bounds over entire draw convolve_gaussian_1d(drawContext, rt, clip, srcRect, dstRect, texture, direction, radius, sigma, true, bounds); } else { // Draw upper and lower margins with bounds; middle without. convolve_gaussian_1d(drawContext, rt, clip, lowerSrcRect, lowerDstRect, texture, direction, radius, sigma, true, bounds); convolve_gaussian_1d(drawContext, rt, clip, upperSrcRect, upperDstRect, texture, direction, radius, sigma, true, bounds); convolve_gaussian_1d(drawContext, rt, clip, middleSrcRect, middleDstRect, texture, direction, radius, sigma, false, bounds); } }
static void draw_box_frame(SkCanvas* canvas, int width, int height) { SkPaint p; p.setStyle(SkPaint::kStroke_Style); p.setColor(SK_ColorRED); SkRect r = SkRect::MakeIWH(width, height); r.inset(0.5f, 0.5f); canvas->drawRect(r, p); canvas->drawLine(r.left(), r.top(), r.right(), r.bottom(), p); canvas->drawLine(r.left(), r.bottom(), r.right(), r.top(), p); }
virtual void onDraw(SkCanvas* canvas) { SkPaint paint; paint.setImageFilter(SkBlurImageFilter::Create(fSigmaX, fSigmaY))->unref(); const SkScalar tile_size = SkIntToScalar(128); SkRect bounds; if (!canvas->getClipBounds(&bounds)) { bounds.setEmpty(); } for (SkScalar y = bounds.top(); y < bounds.bottom(); y += tile_size) { for (SkScalar x = bounds.left(); x < bounds.right(); x += tile_size) { canvas->save(); canvas->clipRect(SkRect::MakeXYWH(x, y, tile_size, tile_size)); canvas->saveLayer(NULL, &paint); const char* str[] = { "The quick", "brown fox", "jumped over", "the lazy dog.", }; SkPaint textPaint; textPaint.setAntiAlias(true); textPaint.setTextSize(SkIntToScalar(100)); int posY = 0; for (unsigned i = 0; i < SK_ARRAY_COUNT(str); i++) { posY += 100; canvas->drawText(str[i], strlen(str[i]), SkIntToScalar(0), SkIntToScalar(posY), textPaint); } canvas->restore(); canvas->restore(); } } }
static bool quick_reject_in_y(const SkPoint pts[4], const SkRect& clip) { Sk4s ys(pts[0].fY, pts[1].fY, pts[2].fY, pts[3].fY); Sk4s t(clip.top()); Sk4s b(clip.bottom()); return (ys < t).allTrue() || (ys > b).allTrue(); }
void onDraw(SkCanvas* canvas) override { SkPaint blurPaint; SkAutoTUnref<SkImageFilter> blur(SkBlurImageFilter::Create(5.0f, 5.0f)); blurPaint.setImageFilter(blur); const SkScalar tile_size = SkIntToScalar(128); SkRect bounds; if (!canvas->getClipBounds(&bounds)) { bounds.setEmpty(); } int ts = SkScalarCeilToInt(tile_size); SkImageInfo info = SkImageInfo::MakeN32Premul(ts, ts); SkAutoTUnref<SkSurface> tileSurface(canvas->newSurface(info)); if (!tileSurface.get()) { tileSurface.reset(SkSurface::NewRaster(info)); } SkCanvas* tileCanvas = tileSurface->getCanvas(); for (SkScalar y = bounds.top(); y < bounds.bottom(); y += tile_size) { for (SkScalar x = bounds.left(); x < bounds.right(); x += tile_size) { tileCanvas->save(); tileCanvas->clear(0); tileCanvas->translate(-x, -y); SkRect rect = SkRect::MakeWH(WIDTH, HEIGHT); tileCanvas->saveLayer(&rect, &blurPaint); SkRRect rrect = SkRRect::MakeRectXY(rect.makeInset(20, 20), 25, 25); tileCanvas->clipRRect(rrect, SkRegion::kDifference_Op, true); SkPaint paint; tileCanvas->drawRect(rect, paint); tileCanvas->restore(); tileCanvas->restore(); canvas->drawImage(tileSurface->makeImageSnapshot().get(), x, y); } } }
Json::Value SkJSONCanvas::makeRect(const SkRect& rect) { Json::Value result(Json::arrayValue); result.append(Json::Value(rect.left())); result.append(Json::Value(rect.top())); result.append(Json::Value(rect.right())); result.append(Json::Value(rect.bottom())); return result; }
PassRefPtr<JSONObject> LoggingCanvas::objectForSkRect(const SkRect& rect) { RefPtr<JSONObject> rectItem = JSONObject::create(); rectItem->setNumber("left", rect.left()); rectItem->setNumber("top", rect.top()); rectItem->setNumber("right", rect.right()); rectItem->setNumber("bottom", rect.bottom()); return rectItem.release(); }
void drawBorders(SkCanvas* canvas) { SkPaint p; p.setStyle(SkPaint::kStroke_Style); p.setColor(SK_ColorBLUE); SkRect r = SkRect::MakeWH(fCell.width() * 2, fCell.height() * 2); r.inset(SK_ScalarHalf, SK_ScalarHalf); canvas->drawRect(r, p); canvas->drawLine(r.left(), r.centerY(), r.right(), r.centerY(), p); canvas->drawLine(r.centerX(), r.top(), r.centerX(), r.bottom(), p); }
// Return true if the rectangle is aligned to integer boundaries. // See comments for computeBitmapDrawRects() for how this is used. static bool areBoundariesIntegerAligned(const SkRect& rect) { // Value is 1.19209e-007. This is the tolerance threshold. const float epsilon = std::numeric_limits<float>::epsilon(); SkIRect roundedRect = roundedIntRect(rect); return fabs(rect.x() - roundedRect.x()) < epsilon && fabs(rect.y() - roundedRect.y()) < epsilon && fabs(rect.right() - roundedRect.right()) < epsilon && fabs(rect.bottom() - roundedRect.bottom()) < epsilon; }
static sk_sp<SkShader> make_shader(const SkRect& bounds) { const SkPoint pts[] = { { bounds.left(), bounds.top() }, { bounds.right(), bounds.bottom() }, }; const SkColor colors[] = { SK_ColorRED, SK_ColorGREEN, SK_ColorBLUE, SK_ColorBLACK, SK_ColorCYAN, SK_ColorMAGENTA, SK_ColorYELLOW, }; return SkGradientShader::MakeLinear(pts, colors, nullptr, SK_ARRAY_COUNT(colors), SkShader::kClamp_TileMode); }
void writePathVertices(GrDrawBatch::Target* target, GrBatchAtlas* atlas, intptr_t offset, GrColor color, size_t vertexStride, const SkMatrix& viewMatrix, const ShapeData* shapeData) const { GrTexture* texture = atlas->getTexture(); SkScalar dx = shapeData->fBounds.fLeft; SkScalar dy = shapeData->fBounds.fTop; SkScalar width = shapeData->fBounds.width(); SkScalar height = shapeData->fBounds.height(); SkScalar invScale = 1.0f / shapeData->fScale; dx *= invScale; dy *= invScale; width *= invScale; height *= invScale; SkPoint* positions = reinterpret_cast<SkPoint*>(offset); // vertex positions // TODO make the vertex attributes a struct SkRect r = SkRect::MakeXYWH(dx, dy, width, height); positions->setRectFan(r.left(), r.top(), r.right(), r.bottom(), vertexStride); // colors for (int i = 0; i < kVerticesPerQuad; i++) { GrColor* colorPtr = (GrColor*)(offset + sizeof(SkPoint) + i * vertexStride); *colorPtr = color; } const SkScalar tx = SkIntToScalar(shapeData->fAtlasLocation.fX); const SkScalar ty = SkIntToScalar(shapeData->fAtlasLocation.fY); // vertex texture coords SkPoint* textureCoords = (SkPoint*)(offset + sizeof(SkPoint) + sizeof(GrColor)); textureCoords->setRectFan(tx / texture->width(), ty / texture->height(), (tx + shapeData->fBounds.width()) / texture->width(), (ty + shapeData->fBounds.height()) / texture->height(), vertexStride); }
void draw_control(SkCanvas* canvas, const SkRect& bounds, SkScalar value, SkScalar min, SkScalar max, const char* name) { SkPaint paint; paint.setAntiAlias(true); paint.setStyle(SkPaint::kStroke_Style); canvas->drawRect(bounds, paint); SkScalar scale = max - min; SkScalar yPos = bounds.fTop + (value - min) * bounds.height() / scale; paint.setColor(0xFFFF0000); canvas->drawLine(bounds.fLeft - 5, yPos, bounds.fRight + 5, yPos, paint); SkString label; label.printf("%0.3g", value); paint.setColor(0xFF000000); paint.setTextSize(11.0f); paint.setStyle(SkPaint::kFill_Style); canvas->drawText(label.c_str(), label.size(), bounds.fLeft + 5, yPos - 5, paint); paint.setTextSize(13.0f); canvas->drawText(name, strlen(name), bounds.fLeft, bounds.bottom() + 11, paint); }
void advance(const SkRect& bounds) { fCenter += fVelocity; if (fCenter.fX > bounds.right()) { SkASSERT(fVelocity.fX > 0); fVelocity.fX = -fVelocity.fX; } else if (fCenter.fX < bounds.left()) { SkASSERT(fVelocity.fX < 0); fVelocity.fX = -fVelocity.fX; } if (fCenter.fY > bounds.bottom()) { if (fVelocity.fY > 0) { fVelocity.fY = -fVelocity.fY; } } else if (fCenter.fY < bounds.top()) { if (fVelocity.fY < 0) { fVelocity.fY = -fVelocity.fY; } } fScale += fDScale; if (fScale > 2 || fScale < SK_Scalar1/2) { fDScale = -fDScale; } fRadian += fDRadian; fRadian = SkScalarMod(fRadian, 2 * SK_ScalarPI); fAlpha += fDAlpha; if (fAlpha > 1) { fAlpha = 1; fDAlpha = -fDAlpha; } else if (fAlpha < 0) { fAlpha = 0; fDAlpha = -fDAlpha; } }
SkPDFImageShader* SkPDFImageShader::Create( SkPDFCanon* canon, SkScalar dpi, SkAutoTDelete<SkPDFShader::State>* autoState) { const SkPDFShader::State& state = **autoState; state.fImage.lockPixels(); // 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 = state.fCanvasTransform; finalMatrix.preConcat(state.fShaderTransform); SkRect deviceBounds; deviceBounds.set(state.fBBox); if (!inverse_transform_bbox(finalMatrix, &deviceBounds)) { return NULL; } const SkBitmap* image = &state.fImage; SkRect bitmapBounds; image->getBounds(&bitmapBounds); // 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] = state.fImageTileModes[0]; tileModes[1] = state.fImageTileModes[1]; if (tileModes[0] != SkShader::kClamp_TileMode || tileModes[1] != SkShader::kClamp_TileMode) { deviceBounds.join(bitmapBounds); } SkISize size = SkISize::Make(SkScalarRoundToInt(deviceBounds.width()), SkScalarRoundToInt(deviceBounds.height())); SkAutoTUnref<SkPDFDevice> patternDevice( SkPDFDevice::CreateUnflipped(size, dpi, canon)); SkCanvas canvas(patternDevice.get()); SkRect patternBBox; image->getBounds(&patternBBox); // 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.drawBitmap(*image, 0, 0); SkScalar width = SkIntToScalar(image->width()); SkScalar height = SkIntToScalar(image->height()); // Tiling is implied. First we handle mirroring. if (tileModes[0] == SkShader::kMirror_TileMode) { SkMatrix xMirror; xMirror.setScale(-1, 1); xMirror.postTranslate(2 * width, 0); drawBitmapMatrix(&canvas, *image, xMirror); patternBBox.fRight += width; } if (tileModes[1] == SkShader::kMirror_TileMode) { SkMatrix yMirror; yMirror.setScale(SK_Scalar1, -SK_Scalar1); yMirror.postTranslate(0, 2 * height); drawBitmapMatrix(&canvas, *image, yMirror); 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); drawBitmapMatrix(&canvas, *image, mirror); } // Then handle Clamping, which requires expanding the pattern canvas to // cover the entire surfaceBBox. // 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) { SkPaint paint; SkRect rect; rect = SkRect::MakeLTRB(deviceBounds.left(), deviceBounds.top(), 0, 0); if (!rect.isEmpty()) { paint.setColor(image->getColor(0, 0)); canvas.drawRect(rect, paint); } rect = SkRect::MakeLTRB(width, deviceBounds.top(), deviceBounds.right(), 0); if (!rect.isEmpty()) { paint.setColor(image->getColor(image->width() - 1, 0)); canvas.drawRect(rect, paint); } rect = SkRect::MakeLTRB(width, height, deviceBounds.right(), deviceBounds.bottom()); if (!rect.isEmpty()) { paint.setColor(image->getColor(image->width() - 1, image->height() - 1)); canvas.drawRect(rect, paint); } rect = SkRect::MakeLTRB(deviceBounds.left(), height, 0, deviceBounds.bottom()); if (!rect.isEmpty()) { paint.setColor(image->getColor(0, image->height() - 1)); canvas.drawRect(rect, paint); } } // Then expand the left, right, top, then bottom. if (tileModes[0] == SkShader::kClamp_TileMode) { SkIRect subset = SkIRect::MakeXYWH(0, 0, 1, image->height()); if (deviceBounds.left() < 0) { SkBitmap left; SkAssertResult(image->extractSubset(&left, subset)); SkMatrix leftMatrix; leftMatrix.setScale(-deviceBounds.left(), 1); leftMatrix.postTranslate(deviceBounds.left(), 0); drawBitmapMatrix(&canvas, left, leftMatrix); if (tileModes[1] == SkShader::kMirror_TileMode) { leftMatrix.postScale(SK_Scalar1, -SK_Scalar1); leftMatrix.postTranslate(0, 2 * height); drawBitmapMatrix(&canvas, left, leftMatrix); } patternBBox.fLeft = 0; } if (deviceBounds.right() > width) { SkBitmap right; subset.offset(image->width() - 1, 0); SkAssertResult(image->extractSubset(&right, subset)); SkMatrix rightMatrix; rightMatrix.setScale(deviceBounds.right() - width, 1); rightMatrix.postTranslate(width, 0); drawBitmapMatrix(&canvas, right, rightMatrix); if (tileModes[1] == SkShader::kMirror_TileMode) { rightMatrix.postScale(SK_Scalar1, -SK_Scalar1); rightMatrix.postTranslate(0, 2 * height); drawBitmapMatrix(&canvas, right, rightMatrix); } patternBBox.fRight = deviceBounds.width(); } } if (tileModes[1] == SkShader::kClamp_TileMode) { SkIRect subset = SkIRect::MakeXYWH(0, 0, image->width(), 1); if (deviceBounds.top() < 0) { SkBitmap top; SkAssertResult(image->extractSubset(&top, subset)); SkMatrix topMatrix; topMatrix.setScale(SK_Scalar1, -deviceBounds.top()); topMatrix.postTranslate(0, deviceBounds.top()); drawBitmapMatrix(&canvas, top, topMatrix); if (tileModes[0] == SkShader::kMirror_TileMode) { topMatrix.postScale(-1, 1); topMatrix.postTranslate(2 * width, 0); drawBitmapMatrix(&canvas, top, topMatrix); } patternBBox.fTop = 0; } if (deviceBounds.bottom() > height) { SkBitmap bottom; subset.offset(0, image->height() - 1); SkAssertResult(image->extractSubset(&bottom, subset)); SkMatrix bottomMatrix; bottomMatrix.setScale(SK_Scalar1, deviceBounds.bottom() - height); bottomMatrix.postTranslate(0, height); drawBitmapMatrix(&canvas, bottom, bottomMatrix); if (tileModes[0] == SkShader::kMirror_TileMode) { bottomMatrix.postScale(-1, 1); bottomMatrix.postTranslate(2 * width, 0); drawBitmapMatrix(&canvas, bottom, bottomMatrix); } patternBBox.fBottom = deviceBounds.height(); } } // Put the canvas into the pattern stream (fContent). SkAutoTDelete<SkStreamAsset> content(patternDevice->content()); SkPDFImageShader* imageShader = SkNEW_ARGS(SkPDFImageShader, (autoState->detach())); imageShader->setData(content.get()); SkAutoTUnref<SkPDFDict> resourceDict( patternDevice->createResourceDict()); populate_tiling_pattern_dict(imageShader, patternBBox, resourceDict.get(), finalMatrix); imageShader->fShaderState->fImage.unlockPixels(); canon->addImageShader(imageShader); return imageShader; }
static bool equal(const SkRect& a, const SkRect& b) { return SkScalarNearlyEqual(a.left(), b.left()) && SkScalarNearlyEqual(a.top(), b.top()) && SkScalarNearlyEqual(a.right(), b.right()) && SkScalarNearlyEqual(a.bottom(), b.bottom()); }
void onOnceBeforeDraw() override { const SkRect fieldBounds = kBounds.makeOutset(kBallSize / 2, kBallSize / 2); const SkRRect ball = SkRRect::MakeOval(SkRect::MakeWH(kBallSize, kBallSize)); const SkRRect paddle = SkRRect::MakeRectXY(SkRect::MakeWH(kPaddleSize.width(), kPaddleSize.height()), kPaddleSize.width() / 2, kPaddleSize.width() / 2); fBall.initialize(ball, SkPoint::Make(kBounds.centerX(), kBounds.centerY()), SkVector::Make(fRand.nextRangeScalar(kBallSpeedMin, kBallSpeedMax), fRand.nextRangeScalar(kBallSpeedMin, kBallSpeedMax))); fPaddle0.initialize(paddle, SkPoint::Make(fieldBounds.left() - kPaddleSize.width() / 2, fieldBounds.centerY()), SkVector::Make(0, 0)); fPaddle1.initialize(paddle, SkPoint::Make(fieldBounds.right() + kPaddleSize.width() / 2, fieldBounds.centerY()), SkVector::Make(0, 0)); // Background decoration. SkPath bgPath; bgPath.moveTo(kBounds.left() , fieldBounds.top()); bgPath.lineTo(kBounds.right(), fieldBounds.top()); bgPath.moveTo(kBounds.left() , fieldBounds.bottom()); bgPath.lineTo(kBounds.right(), fieldBounds.bottom()); // TODO: stroke-dash support would come in handy right about now. for (uint32_t i = 0; i < kBackgroundDashCount; ++i) { bgPath.moveTo(kBounds.centerX(), kBounds.top() + (i + 0.25f) * kBounds.height() / kBackgroundDashCount); bgPath.lineTo(kBounds.centerX(), kBounds.top() + (i + 0.75f) * kBounds.height() / kBackgroundDashCount); } auto bg_path = sksg::Path::Make(bgPath); auto bg_paint = sksg::Color::Make(SK_ColorBLACK); bg_paint->setStyle(SkPaint::kStroke_Style); bg_paint->setStrokeWidth(kBackgroundStroke); auto ball_paint = sksg::Color::Make(SK_ColorGREEN), paddle0_paint = sksg::Color::Make(SK_ColorBLUE), paddle1_paint = sksg::Color::Make(SK_ColorRED), shadow_paint = sksg::Color::Make(SK_ColorBLACK); ball_paint->setAntiAlias(true); paddle0_paint->setAntiAlias(true); paddle1_paint->setAntiAlias(true); shadow_paint->setAntiAlias(true); shadow_paint->setOpacity(kShadowOpacity); // Build the scene graph. auto group = sksg::Group::Make(); group->addChild(sksg::Draw::Make(std::move(bg_path), std::move(bg_paint))); group->addChild(sksg::Draw::Make(fPaddle0.shadowNode, shadow_paint)); group->addChild(sksg::Draw::Make(fPaddle1.shadowNode, shadow_paint)); group->addChild(sksg::Draw::Make(fBall.shadowNode, shadow_paint)); group->addChild(sksg::Draw::Make(fPaddle0.objectNode, paddle0_paint)); group->addChild(sksg::Draw::Make(fPaddle1.objectNode, paddle1_paint)); group->addChild(sksg::Draw::Make(fBall.objectNode, ball_paint)); // Handle everything in a normalized 1x1 space. fContentMatrix = sksg::Matrix<SkMatrix>::Make( SkMatrix::MakeRectToRect(SkRect::MakeWH(1, 1), SkRect::MakeIWH(this->width(), this->height()), SkMatrix::kFill_ScaleToFit)); auto root = sksg::TransformEffect::Make(std::move(group), fContentMatrix); fScene = sksg::Scene::Make(std::move(root), sksg::AnimatorList()); // Off we go. this->updatePaddleStrategy(); }
// Returns true if this method handled the glyph, false if needs to be passed to fallback // bool GrDistanceFieldTextContext::appendGlyph(GrGlyph::PackedID packed, SkScalar sx, SkScalar sy, GrFontScaler* scaler) { if (NULL == fDrawTarget) { return true; } if (NULL == fStrike) { fStrike = fContext->getFontCache()->getStrike(scaler, true); } GrGlyph* glyph = fStrike->getGlyph(packed, scaler); if (NULL == glyph || glyph->fBounds.isEmpty()) { return true; } // fallback to color glyph support if (kA8_GrMaskFormat != glyph->fMaskFormat) { return false; } SkScalar dx = SkIntToScalar(glyph->fBounds.fLeft + SK_DistanceFieldInset); SkScalar dy = SkIntToScalar(glyph->fBounds.fTop + SK_DistanceFieldInset); SkScalar width = SkIntToScalar(glyph->fBounds.width() - 2*SK_DistanceFieldInset); SkScalar height = SkIntToScalar(glyph->fBounds.height() - 2*SK_DistanceFieldInset); SkScalar scale = fTextRatio; dx *= scale; dy *= scale; sx += dx; sy += dy; width *= scale; height *= scale; SkRect glyphRect = SkRect::MakeXYWH(sx, sy, width, height); // check if we clipped out SkRect dstRect; const SkMatrix& ctm = fContext->getMatrix(); (void) ctm.mapRect(&dstRect, glyphRect); if (fClipRect.quickReject(SkScalarTruncToInt(dstRect.left()), SkScalarTruncToInt(dstRect.top()), SkScalarTruncToInt(dstRect.right()), SkScalarTruncToInt(dstRect.bottom()))) { // SkCLZ(3); // so we can set a break-point in the debugger return true; } if (NULL == glyph->fPlot) { if (!fStrike->glyphTooLargeForAtlas(glyph)) { if (fStrike->addGlyphToAtlas(glyph, scaler)) { goto HAS_ATLAS; } // try to clear out an unused plot before we flush if (fContext->getFontCache()->freeUnusedPlot(fStrike, glyph) && fStrike->addGlyphToAtlas(glyph, scaler)) { goto HAS_ATLAS; } if (c_DumpFontCache) { #ifdef SK_DEVELOPER fContext->getFontCache()->dump(); #endif } // before we purge the cache, we must flush any accumulated draws this->flush(); fContext->flush(); // we should have an unused plot now if (fContext->getFontCache()->freeUnusedPlot(fStrike, glyph) && fStrike->addGlyphToAtlas(glyph, scaler)) { goto HAS_ATLAS; } } if (NULL == glyph->fPath) { SkPath* path = SkNEW(SkPath); if (!scaler->getGlyphPath(glyph->glyphID(), path)) { // flag the glyph as being dead? delete path; return true; } glyph->fPath = path; } // flush any accumulated draws before drawing this glyph as a path. this->flush(); GrContext::AutoMatrix am; SkMatrix ctm; ctm.setScale(fTextRatio, fTextRatio); ctm.postTranslate(sx - dx, sy - dy); GrPaint tmpPaint(fPaint); am.setPreConcat(fContext, ctm, &tmpPaint); GrStrokeInfo strokeInfo(SkStrokeRec::kFill_InitStyle); fContext->drawPath(tmpPaint, *glyph->fPath, strokeInfo); // remove this glyph from the vertices we need to allocate fTotalVertexCount -= kVerticesPerGlyph; return true; } HAS_ATLAS: SkASSERT(glyph->fPlot); GrDrawTarget::DrawToken drawToken = fDrawTarget->getCurrentDrawToken(); glyph->fPlot->setDrawToken(drawToken); GrTexture* texture = glyph->fPlot->texture(); SkASSERT(texture); if (fCurrTexture != texture || fCurrVertex + kVerticesPerGlyph > fTotalVertexCount) { this->flush(); fCurrTexture = texture; fCurrTexture->ref(); } bool useColorVerts = !fUseLCDText; if (NULL == fVertices) { int maxQuadVertices = kVerticesPerGlyph * fContext->getQuadIndexBuffer()->maxQuads(); fAllocVertexCount = SkMin32(fTotalVertexCount, maxQuadVertices); fVertices = alloc_vertices(fDrawTarget, fAllocVertexCount, useColorVerts); } SkFixed tx = SkIntToFixed(glyph->fAtlasLocation.fX + SK_DistanceFieldInset); SkFixed ty = SkIntToFixed(glyph->fAtlasLocation.fY + SK_DistanceFieldInset); SkFixed tw = SkIntToFixed(glyph->fBounds.width() - 2*SK_DistanceFieldInset); SkFixed th = SkIntToFixed(glyph->fBounds.height() - 2*SK_DistanceFieldInset); fVertexBounds.joinNonEmptyArg(glyphRect); size_t vertSize = get_vertex_stride(useColorVerts); SkPoint* positions = reinterpret_cast<SkPoint*>( reinterpret_cast<intptr_t>(fVertices) + vertSize * fCurrVertex); positions->setRectFan(glyphRect.fLeft, glyphRect.fTop, glyphRect.fRight, glyphRect.fBottom, vertSize); // The texture coords are last in both the with and without color vertex layouts. SkPoint* textureCoords = reinterpret_cast<SkPoint*>( reinterpret_cast<intptr_t>(positions) + vertSize - sizeof(SkPoint)); textureCoords->setRectFan(SkFixedToFloat(texture->texturePriv().normalizeFixedX(tx)), SkFixedToFloat(texture->texturePriv().normalizeFixedY(ty)), SkFixedToFloat(texture->texturePriv().normalizeFixedX(tx + tw)), SkFixedToFloat(texture->texturePriv().normalizeFixedY(ty + th)), vertSize); if (useColorVerts) { // color comes after position. GrColor* colors = reinterpret_cast<GrColor*>(positions + 1); for (int i = 0; i < 4; ++i) { *colors = fPaint.getColor(); colors = reinterpret_cast<GrColor*>(reinterpret_cast<intptr_t>(colors) + vertSize); } } fCurrVertex += 4; return true; }
// Convert user-space bounds to grid tiles they cover (LT and RB both inclusive). void SkTileGrid::userToGrid(const SkRect& user, SkIRect* grid) const { grid->fLeft = SkPin32(user.left() * fInvWidth , 0, fXTiles - 1); grid->fTop = SkPin32(user.top() * fInvHeight, 0, fYTiles - 1); grid->fRight = SkPin32(user.right() * fInvWidth , 0, fXTiles - 1); grid->fBottom = SkPin32(user.bottom() * fInvHeight, 0, fYTiles - 1); }
static void convolve_gaussian(GrDrawContext* drawContext, const GrClip& clip, const SkRect& srcRect, GrTexture* texture, Gr1DKernelEffect::Direction direction, int radius, float sigma, const SkRect* srcBounds, const SkPoint& srcOffset) { float bounds[2] = { 0.0f, 1.0f }; SkRect dstRect = SkRect::MakeWH(srcRect.width(), srcRect.height()); if (!srcBounds) { convolve_gaussian_1d(drawContext, clip, dstRect, srcOffset, texture, direction, radius, sigma, false, bounds); return; } SkRect midRect = *srcBounds, leftRect, rightRect; midRect.offset(srcOffset); SkIRect topRect, bottomRect; SkScalar rad = SkIntToScalar(radius); if (direction == Gr1DKernelEffect::kX_Direction) { bounds[0] = SkScalarToFloat(srcBounds->left()) / texture->width(); bounds[1] = SkScalarToFloat(srcBounds->right()) / texture->width(); SkRect::MakeLTRB(0, 0, dstRect.right(), midRect.top()).roundOut(&topRect); SkRect::MakeLTRB(0, midRect.bottom(), dstRect.right(), dstRect.bottom()) .roundOut(&bottomRect); midRect.inset(rad, 0); leftRect = SkRect::MakeLTRB(0, midRect.top(), midRect.left(), midRect.bottom()); rightRect = SkRect::MakeLTRB(midRect.right(), midRect.top(), dstRect.width(), midRect.bottom()); dstRect.fTop = midRect.top(); dstRect.fBottom = midRect.bottom(); } else { bounds[0] = SkScalarToFloat(srcBounds->top()) / texture->height(); bounds[1] = SkScalarToFloat(srcBounds->bottom()) / texture->height(); SkRect::MakeLTRB(0, 0, midRect.left(), dstRect.bottom()).roundOut(&topRect); SkRect::MakeLTRB(midRect.right(), 0, dstRect.right(), dstRect.bottom()) .roundOut(&bottomRect);; midRect.inset(0, rad); leftRect = SkRect::MakeLTRB(midRect.left(), 0, midRect.right(), midRect.top()); rightRect = SkRect::MakeLTRB(midRect.left(), midRect.bottom(), midRect.right(), dstRect.height()); dstRect.fLeft = midRect.left(); dstRect.fRight = midRect.right(); } if (!topRect.isEmpty()) { drawContext->clear(&topRect, 0, false); } if (!bottomRect.isEmpty()) { drawContext->clear(&bottomRect, 0, false); } if (midRect.isEmpty()) { // Blur radius covers srcBounds; use bounds over entire draw convolve_gaussian_1d(drawContext, clip, dstRect, srcOffset, texture, direction, radius, sigma, true, bounds); } else { // Draw right and left margins with bounds; middle without. convolve_gaussian_1d(drawContext, clip, leftRect, srcOffset, texture, direction, radius, sigma, true, bounds); convolve_gaussian_1d(drawContext, clip, rightRect, srcOffset, texture, direction, radius, sigma, true, bounds); convolve_gaussian_1d(drawContext, clip, midRect, srcOffset, texture, direction, radius, sigma, false, bounds); } }
static void make_path_line(SkPath* path, const SkRect& bounds) { path->moveTo(bounds.left(), bounds.top()); path->lineTo(bounds.right(), bounds.bottom()); }
void onOnceBeforeDraw() override { const SkRect fieldBounds = kBounds.makeOutset(kBallSize / 2, kBallSize / 2); const SkRRect ball = SkRRect::MakeOval(SkRect::MakeWH(kBallSize, kBallSize)); const SkRRect paddle = SkRRect::MakeRectXY(SkRect::MakeWH(kPaddleSize.width(), kPaddleSize.height()), kPaddleSize.width() / 2, kPaddleSize.width() / 2); fBall.initialize(ball, SK_ColorGREEN, SkPoint::Make(kBounds.centerX(), kBounds.centerY()), SkVector::Make(fRand.nextRangeScalar(kBallSpeedMin, kBallSpeedMax), fRand.nextRangeScalar(kBallSpeedMin, kBallSpeedMax))); fPaddle0.initialize(paddle, SK_ColorBLUE, SkPoint::Make(fieldBounds.left() - kPaddleSize.width() / 2, fieldBounds.centerY()), SkVector::Make(0, 0)); fPaddle1.initialize(paddle, SK_ColorRED, SkPoint::Make(fieldBounds.right() + kPaddleSize.width() / 2, fieldBounds.centerY()), SkVector::Make(0, 0)); // Background decoration. SkPath bgPath; bgPath.moveTo(kBounds.left() , fieldBounds.top()); bgPath.lineTo(kBounds.right(), fieldBounds.top()); bgPath.moveTo(kBounds.left() , fieldBounds.bottom()); bgPath.lineTo(kBounds.right(), fieldBounds.bottom()); // TODO: stroke-dash support would come in handy right about now. for (uint32_t i = 0; i < kBackgroundDashCount; ++i) { bgPath.moveTo(kBounds.centerX(), kBounds.top() + (i + 0.25f) * kBounds.height() / kBackgroundDashCount); bgPath.lineTo(kBounds.centerX(), kBounds.top() + (i + 0.75f) * kBounds.height() / kBackgroundDashCount); } sk_sp<SkSVGPath> bg = SkSVGPath::Make(); bg->setPath(bgPath); bg->setFill(SkSVGPaint(SkSVGPaint::Type::kNone)); bg->setStroke(SkSVGPaint(SkSVGColorType(SK_ColorBLACK))); bg->setStrokeWidth(SkSVGLength(kBackgroundStroke)); // Build the SVG DOM tree. sk_sp<SkSVGSVG> root = SkSVGSVG::Make(); root->appendChild(std::move(bg)); root->appendChild(fPaddle0.shadowNode); root->appendChild(fPaddle1.shadowNode); root->appendChild(fBall.shadowNode); root->appendChild(fPaddle0.objectNode); root->appendChild(fPaddle1.objectNode); root->appendChild(fBall.objectNode); // Handle everything in a normalized 1x1 space. root->setViewBox(SkSVGViewBoxType(SkRect::MakeWH(1, 1))); fDom = sk_sp<SkSVGDOM>(new SkSVGDOM()); fDom->setContainerSize(SkSize::Make(this->width(), this->height())); fDom->setRoot(std::move(root)); // Off we go. this->updatePaddleStrategy(); }
// Returns true if this method handled the glyph, false if needs to be passed to fallback // bool GrDistanceFieldTextContext::appendGlyph(GrGlyph::PackedID packed, SkScalar sx, SkScalar sy, GrFontScaler* scaler) { if (NULL == fDrawTarget) { return true; } if (NULL == fStrike) { fStrike = fContext->getFontCache()->getStrike(scaler, true); } GrGlyph* glyph = fStrike->getGlyph(packed, scaler); if (NULL == glyph || glyph->fBounds.isEmpty()) { return true; } // fallback to color glyph support if (kA8_GrMaskFormat != glyph->fMaskFormat) { return false; } SkScalar dx = SkIntToScalar(glyph->fBounds.fLeft + SK_DistanceFieldInset); SkScalar dy = SkIntToScalar(glyph->fBounds.fTop + SK_DistanceFieldInset); SkScalar width = SkIntToScalar(glyph->fBounds.width() - 2*SK_DistanceFieldInset); SkScalar height = SkIntToScalar(glyph->fBounds.height() - 2*SK_DistanceFieldInset); SkScalar scale = fTextRatio; dx *= scale; dy *= scale; sx += dx; sy += dy; width *= scale; height *= scale; SkRect glyphRect = SkRect::MakeXYWH(sx, sy, width, height); // check if we clipped out SkRect dstRect; const SkMatrix& ctm = fViewMatrix; (void) ctm.mapRect(&dstRect, glyphRect); if (fClipRect.quickReject(SkScalarTruncToInt(dstRect.left()), SkScalarTruncToInt(dstRect.top()), SkScalarTruncToInt(dstRect.right()), SkScalarTruncToInt(dstRect.bottom()))) { return true; } if (NULL == glyph->fPlot) { // needs to be a separate conditional to avoid over-optimization // on Nexus 7 and Nexus 10 // If the glyph is too large we fall back to paths if (!uploadGlyph(glyph, scaler)) { if (NULL == glyph->fPath) { SkPath* path = SkNEW(SkPath); if (!scaler->getGlyphPath(glyph->glyphID(), path)) { // flag the glyph as being dead? delete path; return true; } glyph->fPath = path; } // flush any accumulated draws before drawing this glyph as a path. this->flush(); SkMatrix ctm; ctm.setScale(fTextRatio, fTextRatio); ctm.postTranslate(sx - dx, sy - dy); SkPath tmpPath(*glyph->fPath); tmpPath.transform(ctm); GrStrokeInfo strokeInfo(SkStrokeRec::kFill_InitStyle); fContext->drawPath(fRenderTarget, fClip, fPaint, fViewMatrix, tmpPath, strokeInfo); // remove this glyph from the vertices we need to allocate fTotalVertexCount -= kVerticesPerGlyph; return true; } } SkASSERT(glyph->fPlot); GrDrawTarget::DrawToken drawToken = fDrawTarget->getCurrentDrawToken(); glyph->fPlot->setDrawToken(drawToken); GrTexture* texture = glyph->fPlot->texture(); SkASSERT(texture); if (fCurrTexture != texture || fCurrVertex + kVerticesPerGlyph > fTotalVertexCount) { this->flush(); fCurrTexture = texture; fCurrTexture->ref(); } bool useColorVerts = !fUseLCDText; if (NULL == fVertices) { int maxQuadVertices = kVerticesPerGlyph * fContext->getQuadIndexBuffer()->maxQuads(); fAllocVertexCount = SkMin32(fTotalVertexCount, maxQuadVertices); fVertices = alloc_vertices(fDrawTarget, fAllocVertexCount, useColorVerts); } fVertexBounds.joinNonEmptyArg(glyphRect); int u0 = glyph->fAtlasLocation.fX + SK_DistanceFieldInset; int v0 = glyph->fAtlasLocation.fY + SK_DistanceFieldInset; int u1 = u0 + glyph->fBounds.width() - 2*SK_DistanceFieldInset; int v1 = v0 + glyph->fBounds.height() - 2*SK_DistanceFieldInset; size_t vertSize = get_vertex_stride(useColorVerts); intptr_t vertex = reinterpret_cast<intptr_t>(fVertices) + vertSize * fCurrVertex; // V0 SkPoint* position = reinterpret_cast<SkPoint*>(vertex); position->set(glyphRect.fLeft, glyphRect.fTop); if (useColorVerts) { SkColor* color = reinterpret_cast<SkColor*>(vertex + sizeof(SkPoint)); *color = fPaint.getColor(); } SkIPoint16* textureCoords = reinterpret_cast<SkIPoint16*>(vertex + vertSize - sizeof(SkIPoint16)); textureCoords->set(u0, v0); vertex += vertSize; // V1 position = reinterpret_cast<SkPoint*>(vertex); position->set(glyphRect.fLeft, glyphRect.fBottom); if (useColorVerts) { SkColor* color = reinterpret_cast<SkColor*>(vertex + sizeof(SkPoint)); *color = fPaint.getColor(); } textureCoords = reinterpret_cast<SkIPoint16*>(vertex + vertSize - sizeof(SkIPoint16)); textureCoords->set(u0, v1); vertex += vertSize; // V2 position = reinterpret_cast<SkPoint*>(vertex); position->set(glyphRect.fRight, glyphRect.fBottom); if (useColorVerts) { SkColor* color = reinterpret_cast<SkColor*>(vertex + sizeof(SkPoint)); *color = fPaint.getColor(); } textureCoords = reinterpret_cast<SkIPoint16*>(vertex + vertSize - sizeof(SkIPoint16)); textureCoords->set(u1, v1); vertex += vertSize; // V3 position = reinterpret_cast<SkPoint*>(vertex); position->set(glyphRect.fRight, glyphRect.fTop); if (useColorVerts) { SkColor* color = reinterpret_cast<SkColor*>(vertex + sizeof(SkPoint)); *color = fPaint.getColor(); } textureCoords = reinterpret_cast<SkIPoint16*>(vertex + vertSize - sizeof(SkIPoint16)); textureCoords->set(u1, v0); fCurrVertex += 4; return true; }
void draw(SkCanvas* canvas) { SkRect result; bool intersected = result.intersect({ 10, 40, 50, 80 }, { 30, 60, 70, 90 }); SkDebugf("%s intersection: %g, %g, %g, %g\n", intersected ? "" : "no ", result.left(), result.top(), result.right(), result.bottom()); }
static SkPDFIndirectReference make_function_shader(SkPDFDocument* doc, const SkPDFGradientShader::Key& state) { SkPoint transformPoints[2]; const SkShader::GradientInfo& info = state.fInfo; SkMatrix finalMatrix = state.fCanvasTransform; finalMatrix.preConcat(state.fShaderTransform); bool doStitchFunctions = (state.fType == SkShader::kLinear_GradientType || state.fType == SkShader::kRadial_GradientType || state.fType == SkShader::kConical_GradientType) && (SkTileMode)info.fTileMode == SkTileMode::kClamp && !finalMatrix.hasPerspective(); int32_t shadingType = 1; auto pdfShader = SkPDFMakeDict(); // The two point radial gradient further references // state.fInfo // in translating from x, y coordinates to the t parameter. So, we have // to transform the points and radii according to the calculated matrix. if (doStitchFunctions) { pdfShader->insertObject("Function", gradientStitchCode(info)); shadingType = (state.fType == SkShader::kLinear_GradientType) ? 2 : 3; auto extend = SkPDFMakeArray(); extend->reserve(2); extend->appendBool(true); extend->appendBool(true); pdfShader->insertObject("Extend", std::move(extend)); std::unique_ptr<SkPDFArray> coords; if (state.fType == SkShader::kConical_GradientType) { SkScalar r1 = info.fRadius[0]; SkScalar r2 = info.fRadius[1]; SkPoint pt1 = info.fPoint[0]; SkPoint pt2 = info.fPoint[1]; FixUpRadius(pt1, r1, pt2, r2); coords = SkPDFMakeArray(pt1.x(), pt1.y(), r1, pt2.x(), pt2.y(), r2); } else if (state.fType == SkShader::kRadial_GradientType) { const SkPoint& pt1 = info.fPoint[0]; coords = SkPDFMakeArray(pt1.x(), pt1.y(), 0, pt1.x(), pt1.y(), info.fRadius[0]); } else { const SkPoint& pt1 = info.fPoint[0]; const SkPoint& pt2 = info.fPoint[1]; coords = SkPDFMakeArray(pt1.x(), pt1.y(), pt2.x(), pt2.y()); } pdfShader->insertObject("Coords", std::move(coords)); } else { // Depending on the type of the gradient, we want to transform the // coordinate space in different ways. transformPoints[0] = info.fPoint[0]; transformPoints[1] = info.fPoint[1]; switch (state.fType) { case SkShader::kLinear_GradientType: break; case SkShader::kRadial_GradientType: transformPoints[1] = transformPoints[0]; transformPoints[1].fX += info.fRadius[0]; break; case SkShader::kConical_GradientType: { transformPoints[1] = transformPoints[0]; transformPoints[1].fX += SK_Scalar1; break; } case SkShader::kSweep_GradientType: transformPoints[1] = transformPoints[0]; transformPoints[1].fX += SK_Scalar1; break; case SkShader::kColor_GradientType: case SkShader::kNone_GradientType: default: return SkPDFIndirectReference(); } // Move any scaling (assuming a unit gradient) or translation // (and rotation for linear gradient), of the final gradient from // info.fPoints to the matrix (updating bbox appropriately). Now // the gradient can be drawn on on the unit segment. SkMatrix mapperMatrix; unit_to_points_matrix(transformPoints, &mapperMatrix); finalMatrix.preConcat(mapperMatrix); // Preserves as much as possible in the final matrix, and only removes // the perspective. The inverse of the perspective is stored in // perspectiveInverseOnly matrix and has 3 useful numbers // (p0, p1, p2), while everything else is either 0 or 1. // In this way the shader will handle it eficiently, with minimal code. SkMatrix perspectiveInverseOnly = SkMatrix::I(); if (finalMatrix.hasPerspective()) { if (!split_perspective(finalMatrix, &finalMatrix, &perspectiveInverseOnly)) { return SkPDFIndirectReference(); } } SkRect bbox; bbox.set(state.fBBox); if (!SkPDFUtils::InverseTransformBBox(finalMatrix, &bbox)) { return SkPDFIndirectReference(); } SkDynamicMemoryWStream functionCode; SkShader::GradientInfo infoCopy = info; if (state.fType == SkShader::kConical_GradientType) { SkMatrix inverseMapperMatrix; if (!mapperMatrix.invert(&inverseMapperMatrix)) { return SkPDFIndirectReference(); } inverseMapperMatrix.mapPoints(infoCopy.fPoint, 2); infoCopy.fRadius[0] = inverseMapperMatrix.mapRadius(info.fRadius[0]); infoCopy.fRadius[1] = inverseMapperMatrix.mapRadius(info.fRadius[1]); } switch (state.fType) { case SkShader::kLinear_GradientType: linearCode(infoCopy, perspectiveInverseOnly, &functionCode); break; case SkShader::kRadial_GradientType: radialCode(infoCopy, perspectiveInverseOnly, &functionCode); break; case SkShader::kConical_GradientType: twoPointConicalCode(infoCopy, perspectiveInverseOnly, &functionCode); break; case SkShader::kSweep_GradientType: sweepCode(infoCopy, perspectiveInverseOnly, &functionCode); break; default: SkASSERT(false); } pdfShader->insertObject( "Domain", SkPDFMakeArray(bbox.left(), bbox.right(), bbox.top(), bbox.bottom())); auto domain = SkPDFMakeArray(bbox.left(), bbox.right(), bbox.top(), bbox.bottom()); std::unique_ptr<SkPDFArray> rangeObject = SkPDFMakeArray(0, 1, 0, 1, 0, 1); pdfShader->insertRef("Function", make_ps_function(functionCode.detachAsStream(), std::move(domain), std::move(rangeObject), doc)); } pdfShader->insertInt("ShadingType", shadingType); pdfShader->insertName("ColorSpace", "DeviceRGB"); SkPDFDict pdfFunctionShader("Pattern"); pdfFunctionShader.insertInt("PatternType", 2); pdfFunctionShader.insertObject("Matrix", SkPDFUtils::MatrixToArray(finalMatrix)); pdfFunctionShader.insertObject("Shading", std::move(pdfShader)); return doc->emit(pdfFunctionShader); }