/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // A document-style SkDevice which turns Skia drawing into GDI calls on a (printer) HDC. // // GDI can only express a subset of the Skia drawing model: aliased solid fills and strokes, // opaque bitmaps, and text. Every page is therefore drawn multiple times, first twice into an // SkGDIDevice: // // 1. An analysis pass, which classifies every draw and accumulates the region of the page whose // contents GDI can't reproduce (blending with existing content, isolated layers, filters...). // 2. An emission pass, which emits the supported draws as GDI calls. // // Finally the unsupported region is rasterized (including any native draw that happens to overlap // it) and blitted on top. Translucent draws that don't overlap anything else // are flattened against the (white) page and emitted natively, like cairo does. #include "gdi/SkGDIDocument.h" #include "include/core/SkBitmap.h" #include "include/core/SkCPURecorder.h" #include "include/core/SkCanvas.h" #include "include/core/SkColorFilter.h" #include "include/core/SkFont.h" #include "include/core/SkImage.h" #include "include/core/SkMesh.h" #include "include/core/SkPaint.h" #include "include/core/SkPath.h" #include "include/core/SkPathBuilder.h" #include "include/core/SkPathUtils.h" #include "include/core/SkRRect.h" #include "include/core/SkRegion.h" #include "include/core/SkShader.h" #include "include/core/SkVertices.h" #include "include/pathops/SkPathOps.h" #include "include/private/base/SkFloatingPoint.h" #include "include/private/base/SkTPin.h" #include "src/core/SkClipStack.h" #include "src/core/SkClipStackDevice.h" #include "src/core/SkFontDescriptor.h" #include "src/core/SkGeometry.h" #include "src/core/SkGlyph.h" #include "src/core/SkImageInfoPriv.h" #include "src/core/SkMatrixPriv.h" #include "src/core/SkPathPriv.h" #include "src/core/SkSpecialImage.h" #include "src/core/SkStrikeSpec.h" #include "src/core/SkTHash.h" #include "src/ports/SkTypeface_win_dw.h" #include "src/text/GlyphRun.h" #include "src/utils/win/SkTScopedComPtr.h" #include #include #include #include #include namespace { // GDI coordinates are integers; keep them well within the range every driver can deal with. constexpr SkScalar kMaxCoord = 1 << 26; // Tolerance, in device pixels, when converting conics to quads. constexpr SkScalar kConicTolerance = 0.25f; // Maximum number of pixels of a single raster bitmap. constexpr int64_t kMaxRasterPixels = 16 * 1024 * 1024; // ExtTextOut can choke on very long glyph arrays on some drivers. constexpr size_t kMaxGlyphsPerCall = 4096; // Text is emitted at this many GDI units per device pixel, for sub-pixel font sizes and glyph // positions. Same as cairo's WIN32_FONT_LOGICAL_SCALE. constexpr SkScalar kTextLogicalScale = 32; LONG to_gdi(SkScalar v) { return (LONG)SkScalarRoundToInt(SkTPin(v, -kMaxCoord, kMaxCoord)); } int fill_mode(const SkPath& path) { return path.getFillType() == SkPathFillType::kEvenOdd ? ALTERNATE : WINDING; } XFORM to_xform(const SkMatrix& m) { XFORM x; x.eM11 = m.getScaleX(); x.eM12 = m.getSkewY(); x.eM21 = m.getSkewX(); x.eM22 = m.getScaleY(); x.eDx = m.getTranslateX(); x.eDy = m.getTranslateY(); return x; } // Emits the given device-space path into the current GDI path bracket. void emit_path(HDC dc, const SkPath& path) { std::vector pts; std::vector types; pts.reserve(path.countPoints() * 2); types.reserve(path.countPoints() * 2); auto add = [&](SkPoint p, BYTE type) { pts.push_back({to_gdi(p.fX), to_gdi(p.fY)}); types.push_back(type); }; auto addQuad = [&](const SkPoint q[3]) { add(q[0] + (q[1] - q[0]) * (2.0f / 3), PT_BEZIERTO); add(q[2] + (q[1] - q[2]) * (2.0f / 3), PT_BEZIERTO); add(q[2], PT_BEZIERTO); }; for (auto [verb, p, w] : SkPathPriv::Iterate(path)) { switch (verb) { case SkPathVerb::kMove: add(p[0], PT_MOVETO); break; case SkPathVerb::kLine: add(p[1], PT_LINETO); break; case SkPathVerb::kQuad: addQuad(p); break; case SkPathVerb::kConic: { SkAutoConicToQuads quadder; const SkPoint* quads = quadder.computeQuads(p, *w, kConicTolerance); for (int i = 0; i < quadder.countQuads(); ++i) { addQuad(&quads[i * 2]); } break; } case SkPathVerb::kCubic: add(p[1], PT_BEZIERTO); add(p[2], PT_BEZIERTO); add(p[3], PT_BEZIERTO); break; case SkPathVerb::kClose: if (!types.empty() && types.back() != PT_MOVETO) { types.back() |= PT_CLOSEFIGURE; } break; } } if (!pts.empty()) { PolyDraw(dc, pts.data(), types.data(), (int)pts.size()); } } // Returns a non-inverse path covering the same area as |path| within |bounds|. std::optional uninvert_path(const SkPath& path, const SkIRect& bounds) { if (!path.isInverseFillType()) { return path; } return Op(SkPath::Rect(SkRect::Make(bounds)), path, kIntersect_SkPathOp); } void fill_bitmap_info(BITMAPINFO* bmi, int width, int height) { memset(bmi, 0, sizeof(*bmi)); bmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi->bmiHeader.biWidth = width; bmi->bmiHeader.biHeight = -height; // Top-down. bmi->bmiHeader.biPlanes = 1; bmi->bmiHeader.biBitCount = 32; bmi->bmiHeader.biCompression = BI_RGB; } // Draws an (opaque) BGRA bitmap stretched to the given rect in the current logical space. void blit_bitmap(HDC dc, const SkBitmap& bm, const SkIRect& dst, bool smooth = true) { BITMAPINFO bmi; fill_bitmap_info(&bmi, bm.width(), bm.height()); int oldMode = SetStretchBltMode(dc, smooth ? HALFTONE : COLORONCOLOR); SetBrushOrgEx(dc, 0, 0, nullptr); StretchDIBits(dc, dst.x(), dst.y(), dst.width(), dst.height(), 0, 0, bm.width(), bm.height(), bm.getPixels(), &bmi, DIB_RGB_COLORS, SRCCOPY); SetStretchBltMode(dc, oldMode); } // Rasterizes |draw| (in device space) for the |area| of the page on top of white, at |scale| // times the device resolution. bool rasterize(const SkIRect& area, SkScalar scale, const std::function& draw, SkBitmap* bm) { int64_t w = SkScalarCeilToInt(area.width() * scale); int64_t h = SkScalarCeilToInt(area.height() * scale); if (w <= 0 || h <= 0) { return false; } if (w * h > kMaxRasterPixels) { SkScalar shrink = SkScalarSqrt(SkScalar(kMaxRasterPixels) / SkScalar(w * h)); w = std::max(1, SkScalarFloorToInt(w * shrink)); h = std::max(1, SkScalarFloorToInt(h * shrink)); } if (!bm->tryAllocPixels(SkImageInfo::Make(w, h, kBGRA_8888_SkColorType, kPremul_SkAlphaType))) { return false; } SkCanvas canvas(*bm); canvas.clear(SK_ColorWHITE); canvas.scale(SkScalar(w) / area.width(), SkScalar(h) / area.height()); canvas.translate(-area.x(), -area.y()); canvas.clipRect(SkRect::Make(area)); draw(&canvas); // Make sure that nothing is left translucent (e.g. due to kClear or kSrc), since GDI ignores // the alpha channel. canvas.drawColor(SK_ColorWHITE, SkBlendMode::kDstOver); return true; } COLORREF to_colorref(SkColor4f c, bool flattenOnWhite) { if (flattenOnWhite) { c = {c.fR * c.fA + 1 - c.fA, c.fG * c.fA + 1 - c.fA, c.fB * c.fA + 1 - c.fA, 1}; } SkColor sk = c.toSkColor(); return RGB(SkColorGetR(sk), SkColorGetG(sk), SkColorGetB(sk)); } class AutoGDIObject { public: AutoGDIObject(HDC dc, HGDIOBJ obj) : fDC(dc), fObj(obj), fOld(SelectObject(dc, obj)) {} ~AutoGDIObject() { SelectObject(fDC, fOld); DeleteObject(fObj); } private: HDC fDC; HGDIOBJ fObj; HGDIOBJ fOld; }; // Whether GDI renders |lf| using the exact same font file as |face|, so that glyph ids match. bool gdi_font_matches(const LOGFONTW& lf, IDWriteFontFace* face) { HDC dc = CreateCompatibleDC(nullptr); if (!dc) { return false; } bool matches = true; { AutoGDIObject font(dc, CreateFontIndirectW(&lf)); const UINT32 kTables[] = {DWRITE_MAKE_OPENTYPE_TAG('h', 'e', 'a', 'd'), DWRITE_MAKE_OPENTYPE_TAG('m', 'a', 'x', 'p'), DWRITE_MAKE_OPENTYPE_TAG('n', 'a', 'm', 'e')}; for (UINT32 tag : kTables) { const void* data; UINT32 size; void* context; BOOL exists; if (FAILED(face->TryGetFontTable(tag, &data, &size, &context, &exists)) || !exists) { matches = false; break; } std::vector gdiData(size); matches = GetFontData(dc, tag, 0, nullptr, 0) == size && GetFontData(dc, tag, 0, gdiData.data(), size) == size && !memcmp(gdiData.data(), data, size); face->ReleaseFontTable(context); if (!matches) { break; } } } DeleteDC(dc); return matches; } const DWriteFontTypeface* as_dwrite_typeface(const SkTypeface& typeface) { SkFontDescriptor desc; bool serialize = false; typeface.getFontDescriptor(&desc, &serialize); if (desc.getFactoryId() != DWriteFontTypeface::FactoryId) { return nullptr; } // A LOGFONT can't express a particular instance of a variable font. if (typeface.getVariationDesignPosition({}) > 0) { return nullptr; } return static_cast(&typeface); } std::optional compute_logfont(const DWriteFontTypeface& dw) { SkTScopedComPtr interop; LOGFONTW lf; if (FAILED(dw.fFactory->GetGdiInterop(&interop)) || FAILED(interop->ConvertFontFaceToLOGFONT(dw.fDWriteFontFace.get(), &lf)) || !gdi_font_matches(lf, dw.fDWriteFontFace.get())) { return {}; } return lf; } bool is_image_opaque(const SkImage* image) { if (image->isOpaque()) { return true; } SkPixmap pm; if (image->peekPixels(&pm)) { return pm.computeIsOpaque(); } SkBitmap tmp; return tmp.tryAllocPixels(image->imageInfo().makeColorType(kBGRA_8888_SkColorType) .makeAlphaType(kPremul_SkAlphaType)) && image->readPixels(nullptr, tmp.pixmap(), 0, 0) && tmp.pixmap().computeIsOpaque(); } // Caches that persist across pages. class DocumentCaches { public: const LOGFONTW* logFontFor(const SkTypeface& typeface) { IDWriteFontFace** face = fTypefaceFaces.find(typeface.uniqueID()); if (!face) { const DWriteFontTypeface* dw = as_dwrite_typeface(typeface); face = fTypefaceFaces.set(typeface.uniqueID(), dw ? dw->fDWriteFontFace.get() : nullptr); // Typefaces get re-created every time a page is drawn, but font faces are generally // shared. if (dw && !fLogFonts.find(*face)) { fLogFonts.set(*face, {SkTScopedComPtr(SkRefComPtr(*face)), compute_logfont(*dw)}); } } if (!*face) { return nullptr; } const LogFontEntry* entry = fLogFonts.find(*face); return entry->fLogFont.has_value() ? &entry->fLogFont.value() : nullptr; } private: struct LogFontEntry { // Keeps the key alive. SkTScopedComPtr fFace; std::optional fLogFont; }; skia_private::THashMap fTypefaceFaces; skia_private::THashMap fLogFonts; }; // Conservative coarse coverage of what has been drawn on the page so far. class CoverageGrid { public: explicit CoverageGrid(const SkIRect& bounds) : fBounds(bounds) , fCols((bounds.width() + kCell - 1) / kCell) , fRows((bounds.height() + kCell - 1) / kCell) , fCells(size_t(fCols) * fRows, false) {} bool intersects(const SkIRect& r) const { bool found = false; this->forEachCell(r, [&](size_t i) { found = found || fCells[i]; }); return found; } void mark(const SkIRect& r) { this->forEachCell(r, [&](size_t i) { fCells[i] = true; }); } private: static constexpr int kCell = 32; template void forEachCell(SkIRect r, Fn&& fn) const { if (!r.intersect(fBounds)) { return; } int x0 = (r.fLeft - fBounds.fLeft) / kCell, x1 = (r.fRight - 1 - fBounds.fLeft) / kCell; int y0 = (r.fTop - fBounds.fTop) / kCell, y1 = (r.fBottom - 1 - fBounds.fTop) / kCell; for (int y = y0; y <= y1; ++y) { for (int x = x0; x <= x1; ++x) { fn(size_t(y) * fCols + x); } } } SkIRect fBounds; int fCols; int fRows; std::vector fCells; }; enum class Support { kNative, // Supported if nothing is drawn below, by flattening the draw against the white page. kNeedsFlattening, kUnsupported, }; enum class Decision { kSkip, kEmit, kEmitFlattened, }; // The effective clip of a device, as a list of device-space paths to intersect. struct ClipInfo { std::vector fPaths; bool fHasShader = false; bool fEmpty = false; uint32_t fID = 0; }; struct PageState { PageState(const SkIRect& bounds, SkScalar rasterScale, DocumentCaches* caches) : fBounds(bounds), fRasterScale(rasterScale), fCaches(caches), fDrawn(bounds) {} // Null during the analysis pass. HDC fDC = nullptr; const SkIRect fBounds; const SkScalar fRasterScale; DocumentCaches* const fCaches; // During the emission pass, the region that will be rasterized, computed by the analysis. const SkRegion* fFallbackFromAnalysis = nullptr; CoverageGrid fDrawn; SkRegion fFallback; uint32_t fNextClipID = 1; uint32_t fAppliedClipID = 0; Decision decide(const SkIRect& bounds, Support support) { Decision decision = Decision::kSkip; if (support == Support::kNative) { decision = Decision::kEmit; } else if (support == Support::kNeedsFlattening && !fDrawn.intersects(bounds)) { decision = Decision::kEmitFlattened; } else { fFallback.op(bounds, SkRegion::kUnion_Op); } fDrawn.mark(bounds); if (!fDC || (fFallbackFromAnalysis && fFallbackFromAnalysis->contains(bounds))) { return Decision::kSkip; } return decision; } void applyClip(const ClipInfo& clip) { if (clip.fID == fAppliedClipID) { return; } // The page setup did a SaveDC() right before any clipping happened. RestoreDC(fDC, -1); SaveDC(fDC); for (const SkPath& path : clip.fPaths) { SkRect r; if (path.isEmpty()) { IntersectClipRect(fDC, 0, 0, 0, 0); } else if (path.isRect(&r)) { SkIRect ir = r.round(); IntersectClipRect(fDC, ir.fLeft, ir.fTop, ir.fRight, ir.fBottom); } else { BeginPath(fDC); emit_path(fDC, path); EndPath(fDC); SetPolyFillMode(fDC, fill_mode(path)); SelectClipPath(fDC, RGN_AND); } } fAppliedClipID = clip.fID; } }; struct Geometry { SkPath fPath; // In device space. bool fHairline = false; }; class SkGDIDevice final : public SkClipStackDevice { public: enum class Kind { // Draws go straight to the page (the root device, and pass-through layers). kDirect, // A layer which needs compositing that GDI can't do: its contents are rasterized. kIsolated, }; SkGDIDevice(PageState* page, SkISize size, Kind kind, SkGDIDevice* parent) : SkClipStackDevice(SkImageInfo::MakeUnknown(size.width(), size.height()), SkSurfaceProps()) , fPage(page) , fKind(kind) , fHasParent(parent) { if (parent) { fParentClip = parent->clipInfo(); fParentClipBounds = parent->globalClipBounds(); } else { fParentClipBounds = page->fBounds; } } SkRecorder* baseRecorder() const override { return skcpu::Recorder::TODO(); } sk_sp createDevice(const CreateInfo& info, const SkPaint* paint) override { bool passThrough = fKind == Kind::kDirect && (!paint || (paint->isSrcOver() && paint->getAlpha() == 0xFF && !paint->getColorFilter() && !paint->getImageFilter() && !paint->getMaskFilter() && !paint->getShader())); if (!passThrough && paint && (paint->getImageFilter() || paint->getColorFilter() || !paint->asBlendMode())) { // The output of the layer is not bounded by its contents. this->markUnsupported(this->globalClipBounds()); } return sk_make_sp(fPage, info.fInfo.dimensions(), passThrough ? Kind::kDirect : Kind::kIsolated, this); } void drawDevice(SkDevice* device, const SkSamplingOptions&, const SkPaint&) override { // createDevice() only ever creates SkGDIDevices. auto* layer = static_cast(device); if (layer->fKind == Kind::kIsolated) { this->markUnsupported(layer->fIsolatedBounds); } } void drawSpecial(SkSpecialImage* image, const SkMatrix& localToDevice, const SkSamplingOptions&, const SkPaint&, SkCanvas::SrcRectConstraint) override { this->markUnsupported(this->toGlobalBounds( localToDevice.mapRect(SkRect::Make(image->dimensions())))); } void drawCoverageMask(const SkSpecialImage* mask, const SkMatrix& maskToDevice, const SkSamplingOptions&, const SkPaint&) override { this->markUnsupported(this->toGlobalBounds( maskToDevice.mapRect(SkRect::Make(mask->dimensions())))); } void drawPaint(const SkPaint& paint) override { SkIRect bounds = this->globalClipBounds(); Decision decision = this->decide(bounds, this->classifyPaint(paint)); if (decision != Decision::kSkip) { this->emitGeometry({SkPath::Rect(SkRect::Make(bounds)), false}, paint, decision == Decision::kEmitFlattened, bounds); } } void drawPoints(SkCanvas::PointMode mode, SkSpan pts, const SkPaint& paint) override { SkPathBuilder builder; switch (mode) { case SkCanvas::kPolygon_PointMode: builder.addPolygon(pts, false); break; case SkCanvas::kLines_PointMode: for (size_t i = 0; i + 1 < pts.size(); i += 2) { builder.moveTo(pts[i]); builder.lineTo(pts[i + 1]); } break; case SkCanvas::kPoints_PointMode: for (SkPoint p : pts) { builder.moveTo(p); builder.lineTo(p); } break; } SkPaint strokePaint(paint); strokePaint.setStyle(SkPaint::kStroke_Style); if (mode == SkCanvas::kPoints_PointMode && strokePaint.getStrokeCap() == SkPaint::kButt_Cap) { strokePaint.setStrokeCap(SkPaint::kSquare_Cap); } this->drawPath(builder.detach(), strokePaint); } void drawRect(const SkRect& r, const SkPaint& paint) override { this->drawPath(SkPath::Rect(r), paint); } void drawOval(const SkRect& oval, const SkPaint& paint) override { this->drawPath(SkPath::Oval(oval), paint); } void drawRRect(const SkRRect& rr, const SkPaint& paint) override { this->drawPath(SkPath::RRect(rr), paint); } void drawPath(const SkPath& path, const SkPaint& paint) override { std::optional geometry = this->makeGeometry(path, paint); if (!geometry) { this->markUnsupported(this->globalClipBounds()); return; } SkRect devBounds = geometry->fPath.getBounds(); if (geometry->fHairline) { devBounds.outset(1, 1); } SkIRect bounds = devBounds.roundOut().makeOutset(1, 1); Support support = this->classifyPaint(paint); if (geometry->fHairline && paint.getShader()) { support = Support::kUnsupported; } Decision decision = this->decide(bounds, support); if (decision != Decision::kSkip) { this->emitGeometry(*geometry, paint, decision == Decision::kEmitFlattened, bounds); } } void drawImageRect(const SkImage* image, const SkRect* src, const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint& paint, SkCanvas::SrcRectConstraint) override { SkRect srcRect = src ? *src : SkRect::Make(image->bounds()); SkMatrix srcToDst = SkMatrix::RectToRect(srcRect, dst); if (!srcRect.intersect(SkRect::Make(image->bounds()))) { return; } SkRect dstRect = srcToDst.mapRect(srcRect); SkMatrix ctm = this->globalCTM(); SkIRect bounds = ctm.mapRect(dstRect).roundOut().makeOutset(1, 1); Decision decision = this->decide(bounds, this->classifyImage(image, paint, ctm)); if (decision != Decision::kSkip) { this->emitImage(image, srcRect, dstRect, sampling, paint, decision == Decision::kEmitFlattened); } } void drawVertices(const SkVertices* vertices, sk_sp, const SkPaint&, bool) override { this->markUnsupported(this->toGlobalBounds(vertices->bounds())); } void drawMesh(const SkMesh& mesh, sk_sp, const SkPaint&) override { this->markUnsupported(this->toGlobalBounds(mesh.bounds())); } private: void onDrawGlyphRunList(SkCanvas*, const sktext::GlyphRunList& list, const SkPaint& paint) override { const SkMatrix ctm = this->globalCTM(); const Support paintSupport = this->classifyPaint(paint); for (const sktext::GlyphRun& run : list) { if (run.glyphsIDs().empty()) { continue; } SkBulkGlyphMetrics metrics(SkStrikeSpec::MakeWithNoDevice(run.font(), &paint)); SkSpan glyphs = metrics.glyphs(run.glyphsIDs()); bool hasColorGlyphs = false; SkRect localBounds = SkRect::MakeEmpty(); for (size_t i = 0; i < glyphs.size(); ++i) { hasColorGlyphs |= glyphs[i]->isColor(); if (!glyphs[i]->isEmpty()) { localBounds.join(glyphs[i]->rect().makeOffset(run.positions()[i] + list.origin())); } } if (localBounds.isEmpty()) { continue; } SkRect storage; if (paint.canComputeFastBounds()) { localBounds = paint.computeFastBounds(localBounds, &storage); } SkIRect bounds = ctm.mapRect(localBounds).roundOut().makeOutset(1, 1); Decision decision = this->decide(bounds, hasColorGlyphs ? Support::kUnsupported : paintSupport); if (decision == Decision::kSkip) { continue; } bool flatten = decision == Decision::kEmitFlattened; if (this->emitGlyphRunAsText(run, list.origin(), paint, flatten)) { continue; } struct Rec { SkPathBuilder fBuilder; SkPoint fOrigin; const SkPoint* fPos; } rec = {{}, list.origin(), run.positions().data()}; run.font().getPaths( run.glyphsIDs(), [](const SkPath* path, const SkMatrix& mx, void* ctx) { auto* rec = static_cast(ctx); if (path) { SkMatrix m = mx; m.postTranslate(rec->fPos->fX + rec->fOrigin.fX, rec->fPos->fY + rec->fOrigin.fY); rec->fBuilder.addPath(*path, m); } rec->fPos++; }, &rec); if (std::optional geometry = this->makeGeometry(rec.fBuilder.detach(), paint)) { this->emitGeometry(*geometry, paint, flatten, bounds); } } } // -- Coordinate spaces. Emission always happens in global (page) device space. SkMatrix globalCTM() const { return SkMatrix::Concat(this->deviceToGlobal().asM33(), this->localToDevice()); } SkIRect toGlobalBounds(const SkRect& localBounds) const { return this->globalCTM().mapRect(localBounds).roundOut().makeOutset(1, 1); } SkIRect globalClipBounds() const { SkIRect bounds = SkMatrixPriv::MapRect(this->deviceToGlobal(), SkRect::Make(this->devClipBounds())) .roundOut(); if (!bounds.intersect(fParentClipBounds)) { return SkIRect::MakeEmpty(); } return bounds; } const ClipInfo& clipInfo() { uint32_t genID = this->cs().getTopmostGenID(); if (fClipInfo.fID && genID == fClipInfoGenID) { return fClipInfo; } fClipInfo = fParentClip; if (fHasParent) { fClipInfo.fPaths.push_back(SkPath::Rect(SkRect::Make(this->getGlobalBounds()))); } const size_t ownStart = fClipInfo.fPaths.size(); const SkMatrix toGlobal = this->deviceToGlobal().asM33(); SkClipStack::B2TIter iter(this->cs()); while (const SkClipStack::Element* element = iter.next()) { using Type = SkClipStack::Element::DeviceSpaceType; if (element->isReplaceOp()) { fClipInfo.fPaths.resize(ownStart); } if (element->getDeviceSpaceType() == Type::kEmpty) { fClipInfo.fEmpty = true; continue; } if (element->getDeviceSpaceType() == Type::kShader) { fClipInfo.fHasShader = true; continue; } SkPath path = element->asDeviceSpacePath().makeTransform(toGlobal); if (element->getOp() == SkClipOp::kDifference) { path.toggleInverseFillType(); } std::optional uninverted = uninvert_path(path, fPage->fBounds); if (!uninverted) { fClipInfo.fHasShader = true; // Treat as unsupported. continue; } fClipInfo.fPaths.push_back(std::move(*uninverted)); } fClipInfo.fID = fPage->fNextClipID++; fClipInfoGenID = genID; return fClipInfo; } // -- Classification. This must give the same result in the analysis and emission passes. Decision decide(SkIRect bounds, Support support) { if (!bounds.intersect(this->globalClipBounds())) { return Decision::kSkip; } if (fKind == Kind::kIsolated) { fIsolatedBounds.join(bounds); return Decision::kSkip; } const ClipInfo& clip = this->clipInfo(); if (clip.fEmpty) { return Decision::kSkip; } if (clip.fHasShader) { support = Support::kUnsupported; } return fPage->decide(bounds, support); } void markUnsupported(const SkIRect& bounds) { (void)this->decide(bounds, Support::kUnsupported); } static SkColor4f PaintColor(const SkPaint& paint) { if (paint.asBlendMode() == SkBlendMode::kClear) { return SkColors::kTransparent; } SkColor4f color = paint.getColor4f(); if (SkColorFilter* filter = paint.getColorFilter()) { color = filter->filterColor4f(color, nullptr, nullptr); } return color; } Support classifyPaint(const SkPaint& paint) const { if (paint.getMaskFilter() || paint.getImageFilter()) { return Support::kUnsupported; } std::optional mode = paint.asBlendMode(); if (!mode) { return Support::kUnsupported; } switch (*mode) { case SkBlendMode::kSrcOver: case SkBlendMode::kSrc: break; case SkBlendMode::kClear: return Support::kNeedsFlattening; default: return Support::kUnsupported; } if (const SkShader* shader = paint.getShader()) { // Non-solid paints are rasterized locally and clipped to the geometry. const SkColorFilter* filter = paint.getColorFilter(); bool opaque = shader->isOpaque() && paint.getAlpha() == 0xFF && (!filter || filter->isAlphaUnchanged()); return opaque ? Support::kNative : Support::kNeedsFlattening; } return PaintColor(paint).isOpaque() ? Support::kNative : Support::kNeedsFlattening; } Support classifyImage(const SkImage* image, const SkPaint& paint, const SkMatrix& ctm) const { if (paint.getMaskFilter() || paint.getImageFilter() || paint.getColorFilter() || ctm.hasPerspective() || SkColorTypeIsAlphaOnly(image->colorType())) { return Support::kUnsupported; } std::optional mode = paint.asBlendMode(); if (mode != SkBlendMode::kSrcOver && mode != SkBlendMode::kSrc) { return Support::kUnsupported; } if (paint.getAlpha() == 0xFF && is_image_opaque(image)) { return Support::kNative; } return Support::kNeedsFlattening; } // Converts a local-space path into the device-space area that it covers. std::optional makeGeometry(const SkPath& path, const SkPaint& paint) const { const SkMatrix ctm = this->globalCTM(); Geometry geometry; if (paint.getPathEffect() || paint.getStyle() != SkPaint::kFill_Style) { SkPathBuilder builder; geometry.fHairline = !skpathutils::FillPathWithPaint(path, paint, &builder, nullptr, ctm); geometry.fPath = builder.detach(&ctm); } else { geometry.fPath = path.makeTransform(ctm); } std::optional uninverted = uninvert_path(geometry.fPath, this->globalClipBounds()); if (!uninverted) { return {}; } geometry.fPath = std::move(*uninverted); return geometry; } // -- Emission. void emitGeometry(const Geometry& geometry, const SkPaint& paint, bool flatten, const SkIRect& bounds) { HDC dc = fPage->fDC; fPage->applyClip(this->clipInfo()); if (geometry.fHairline) { LOGBRUSH brush = {BS_SOLID, to_colorref(PaintColor(paint), flatten), 0}; AutoGDIObject pen(dc, ExtCreatePen(PS_COSMETIC | PS_SOLID, 1, &brush, 0, nullptr)); BeginPath(dc); emit_path(dc, geometry.fPath); EndPath(dc); StrokePath(dc); return; } if (!paint.getShader()) { HBRUSH brush = CreateSolidBrush(to_colorref(PaintColor(paint), flatten)); SkRect rect; if (geometry.fPath.isRect(&rect)) { SkIRect ir = rect.round(); RECT r = {ir.fLeft, ir.fTop, ir.fRight, ir.fBottom}; FillRect(dc, &r, brush); DeleteObject(brush); return; } AutoGDIObject selected(dc, brush); BeginPath(dc); emit_path(dc, geometry.fPath); EndPath(dc); SetPolyFillMode(dc, fill_mode(geometry.fPath)); FillPath(dc); return; } // Rasterize the shader over the bounds, and clip that to the geometry. SkIRect area = bounds; if (!area.intersect(this->globalClipBounds())) { return; } const SkMatrix ctm = this->globalCTM(); SkBitmap bm; bool ok = rasterize(area, fPage->fRasterScale, [&](SkCanvas* canvas) { canvas->concat(ctm); SkPaint fill(paint); fill.setStyle(SkPaint::kFill_Style); fill.setPathEffect(nullptr); canvas->drawPaint(fill); }, &bm); if (!ok) { return; } SaveDC(dc); BeginPath(dc); emit_path(dc, geometry.fPath); EndPath(dc); SetPolyFillMode(dc, fill_mode(geometry.fPath)); SelectClipPath(dc, RGN_AND); blit_bitmap(dc, bm, area); RestoreDC(dc, -1); } void emitImage(const SkImage* image, const SkRect& src, const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint& paint, bool flatten) { SkIRect isrc = src.roundOut(); if (!isrc.intersect(image->bounds())) { return; } SkBitmap bm; if (!bm.tryAllocPixels(SkImageInfo::Make(isrc.width(), isrc.height(), kBGRA_8888_SkColorType, kPremul_SkAlphaType))) { return; } if (flatten) { SkCanvas canvas(bm); canvas.clear(SK_ColorWHITE); SkPaint alpha; alpha.setAlphaf(paint.getAlphaf()); canvas.drawImage(image, -isrc.x(), -isrc.y(), SkSamplingOptions(), &alpha); } else if (!image->readPixels(nullptr, bm.pixmap(), isrc.x(), isrc.y())) { return; } HDC dc = fPage->fDC; const SkMatrix ctm = this->globalCTM(); fPage->applyClip(this->clipInfo()); SaveDC(dc); if (SkRect::Make(isrc) != src) { // We had to round out the source rect, clip to the actual destination. BeginPath(dc); emit_path(dc, SkPath::Rect(dst).makeTransform(ctm)); EndPath(dc); SelectClipPath(dc, RGN_AND); } SkMatrix bitmapToDevice = SkMatrix::Concat(ctm, SkMatrix::RectToRect(src, dst)); bitmapToDevice.preTranslate(isrc.x(), isrc.y()); XFORM xform = to_xform(bitmapToDevice); if (SetWorldTransform(dc, &xform)) { bool smooth = sampling.useCubic || sampling.filter != SkFilterMode::kNearest; blit_bitmap(dc, bm, SkIRect::MakeWH(bm.width(), bm.height()), smooth); } RestoreDC(dc, -1); } // Emits the glyph run with ExtTextOut, if the font and paint allow it. Returns false if // nothing was emitted and the run needs to be drawn some other way. bool emitGlyphRunAsText(const sktext::GlyphRun& run, SkPoint origin, const SkPaint& paint, bool flatten) { const SkFont& font = run.font(); if (paint.getShader() || paint.getPathEffect() || font.isEmbolden() || font.getScaleX() == 0 || (paint.getStyle() != SkPaint::kFill_Style && paint.getStrokeWidth() > 0)) { return false; } const SkMatrix ctm = this->globalCTM(); if (ctm.hasPerspective()) { return false; } const LOGFONTW* baseLogFont = fPage->fCaches->logFontFor(*font.getTypeface()); if (!baseLogFont) { return false; } // Glyph space is local space without the font's horizontal scale and skew. Emit the // text in glyph space scaled up by |scale|, so that integer GDI coordinates and font // heights are roughly in units of 1/kTextLogicalScale device pixels, and let the world // transform do the rest. const SkMatrix fontMatrix = SkMatrix::MakeAll(font.getScaleX(), font.getSkewX(), 0, 0, 1, 0, 0, 0, 1); const SkMatrix glyphToDevice = SkMatrix::Concat(ctm, fontMatrix); const SkScalar scale = SkScalarSqrt(SkScalarAbs( glyphToDevice.getScaleX() * glyphToDevice.getScaleY() - glyphToDevice.getSkewX() * glyphToDevice.getSkewY())) * kTextLogicalScale; SkMatrix localToGdi; if (!SkIsFinite(scale) || scale <= 0 || !fontMatrix.invert(&localToGdi)) { return false; } localToGdi.postScale(scale, scale); const LONG height = SkScalarRoundToInt(font.getSize() * scale); if (height <= 0) { return false; } SkMatrix gdiToDevice = glyphToDevice; gdiToDevice.preScale(1 / scale, 1 / scale); LOGFONTW lf = *baseLogFont; lf.lfHeight = -height; lf.lfWidth = 0; lf.lfEscapement = 0; lf.lfOrientation = 0; HDC dc = fPage->fDC; fPage->applyClip(this->clipInfo()); SaveDC(dc); XFORM xform = to_xform(gdiToDevice); if (!SetWorldTransform(dc, &xform)) { RestoreDC(dc, -1); return false; } { AutoGDIObject selected(dc, CreateFontIndirectW(&lf)); SetTextAlign(dc, TA_BASELINE | TA_LEFT | TA_NOUPDATECP); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, to_colorref(PaintColor(paint), flatten)); SkSpan ids = run.glyphsIDs(); SkSpan positions = run.positions(); std::vector gdiPos(ids.size()); for (size_t i = 0; i < ids.size(); ++i) { SkPoint p = localToGdi.mapPoint(positions[i] + origin); gdiPos[i] = {to_gdi(p.fX), to_gdi(p.fY)}; } std::vector dxy; for (size_t start = 0; start < ids.size(); start += kMaxGlyphsPerCall) { size_t count = std::min(kMaxGlyphsPerCall, ids.size() - start); dxy.assign(count * 2, 0); for (size_t i = 0; i + 1 < count; ++i) { dxy[i * 2] = gdiPos[start + i + 1].x - gdiPos[start + i].x; // ETO_PDY offsets go up. dxy[i * 2 + 1] = gdiPos[start + i].y - gdiPos[start + i + 1].y; } static_assert(sizeof(SkGlyphID) == sizeof(WORD)); ExtTextOutW(dc, gdiPos[start].x, gdiPos[start].y, ETO_GLYPH_INDEX | ETO_PDY, nullptr, reinterpret_cast(ids.data() + start), (UINT)count, dxy.data()); } } RestoreDC(dc, -1); return true; } PageState* const fPage; const Kind fKind; const bool fHasParent; ClipInfo fParentClip; SkIRect fParentClipBounds; ClipInfo fClipInfo; uint32_t fClipInfoGenID = 0; // For isolated layers, the global bounds of their contents. SkIRect fIsolatedBounds = SkIRect::MakeEmpty(); }; // When printing to an EMF-backed DC, ExtTextOut with ETO_GLYPH_INDEX doesn't work unless this // has been called (see cairo's _cairo_win32_printing_surface_init_language_pack). bool clip_to_region(HDC dc, const SkRegion& region) { std::vector rects; for (SkRegion::Iterator it(region); !it.done(); it.next()) { const SkIRect& r = it.rect(); rects.push_back({r.fLeft, r.fTop, r.fRight, r.fBottom}); } const DWORD rectsSize = DWORD(rects.size() * sizeof(RECT)); std::vector storage(sizeof(RGNDATAHEADER) + rectsSize); auto* data = reinterpret_cast(storage.data()); const SkIRect& bounds = region.getBounds(); data->rdh = {sizeof(RGNDATAHEADER), RDH_RECTANGLES, DWORD(rects.size()), rectsSize, {bounds.fLeft, bounds.fTop, bounds.fRight, bounds.fBottom}}; memcpy(data->Buffer, rects.data(), rectsSize); HRGN rgn = ExtCreateRegion(nullptr, DWORD(storage.size()), data); if (!rgn) { return false; } int result = ExtSelectClipRgn(dc, rgn, RGN_AND); DeleteObject(rgn); return result != ERROR; } void init_language_pack() { if (GetModuleHandleW(L"LPK.DLL")) { return; } if (HMODULE gdi = GetModuleHandleW(L"GDI32.DLL")) { using InitLanguagePack = BOOL(WINAPI*)(int); if (auto init = reinterpret_cast( reinterpret_cast(GetProcAddress(gdi, "GdiInitializeLanguagePack")))) { init(0); } } } class GDIDocument final : public SkGDI::Document { public: GDIDocument(HDC dc, const SkGDI::Options& options) : fDC(dc), fOptions(options) {} void drawPage(SkISize size, const std::function& draw) override { if (size.isEmpty()) { return; } const SkIRect pageBounds = SkIRect::MakeSize(size); const SkScalar rasterScale = std::min(fOptions.fRasterScale, 1); PageState analysis(pageBounds, rasterScale, &fCaches); Playback(draw, &analysis); const int savedDC = SaveDC(fDC); SetGraphicsMode(fDC, GM_ADVANCED); ModifyWorldTransform(fDC, nullptr, MWT_IDENTITY); SetMapMode(fDC, MM_TEXT); // Restored by PageState::applyClip() every time the clip changes. SaveDC(fDC); { PageState emission(pageBounds, rasterScale, &fCaches); emission.fDC = fDC; emission.fFallbackFromAnalysis = &analysis.fFallback; Playback(draw, &emission); } RestoreDC(fDC, -1); this->rasterizeFallback(draw, analysis.fFallback, rasterScale); RestoreDC(fDC, savedDC); } private: static void Playback(const std::function& draw, PageState* state) { SkCanvas canvas(sk_make_sp(state, state->fBounds.size(), SkGDIDevice::Kind::kDirect, nullptr)); draw(&canvas); } // Every rasterization needs to draw the whole page, so rasterize the bounds of the fallback // region in as few strips as possible, and clip the result to the region. void rasterizeFallback(const std::function& draw, const SkRegion& fallback, SkScalar scale) { if (fallback.isEmpty()) { return; } if (!fallback.isRect() && !clip_to_region(fDC, fallback)) { return; } const SkIRect& area = fallback.getBounds(); // Split tall areas into strips to bound memory usage. int64_t rowPixels = std::max(1, SkScalarCeilToInt(area.width() * scale)); int stripHeight = std::max( 1, SkScalarFloorToInt((kMaxRasterPixels / 4 / rowPixels) / scale)); for (int y = area.fTop; y < area.fBottom; y += stripHeight) { SkRegion strip(SkIRect::MakeLTRB(area.fLeft, y, area.fRight, std::min(y + stripHeight, area.fBottom))); if (!strip.op(fallback, SkRegion::kIntersect_Op)) { continue; } SkBitmap bm; if (rasterize(strip.getBounds(), scale, draw, &bm)) { blit_bitmap(fDC, bm, strip.getBounds()); } } } HDC fDC; SkGDI::Options fOptions; DocumentCaches fCaches; }; } // namespace namespace SkGDI { std::unique_ptr MakeDocument(HDC dc, const Options& options) { if (!dc) { return nullptr; } init_language_pack(); return std::make_unique(dc, options); } } // namespace SkGDI