| /* |
| * Copyright (C) 2020-2022 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "DrawGlyphsRecorder.h" |
| |
| #include "BitmapImage.h" |
| #include "Color.h" |
| #include "FloatPoint.h" |
| #include "Font.h" |
| #include "FontCascade.h" |
| #include "FontPlatformData.h" |
| #include "GlyphBuffer.h" |
| #include "GraphicsContextCG.h" |
| #include "ImageBuffer.h" |
| |
| #include <CoreText/CoreText.h> |
| #include <wtf/Vector.h> |
| |
| #if PLATFORM(WIN) |
| #include <pal/spi/win/CoreTextSPIWin.h> |
| #endif |
| |
| namespace WebCore { |
| |
| static CGContextDelegateRef beginLayer(CGContextDelegateRef delegate, CGRenderingStateRef rstate, CGGStateRef gstate, CGRect rect, CFDictionaryRef, CGContextDelegateRef) |
| { |
| DrawGlyphsRecorder& recorder = *static_cast<DrawGlyphsRecorder*>(CGContextDelegateGetInfo(delegate)); |
| recorder.recordBeginLayer(rstate, gstate, rect); |
| return delegate; |
| } |
| |
| static CGContextDelegateRef endLayer(CGContextDelegateRef delegate, CGRenderingStateRef rstate, CGGStateRef gstate) |
| { |
| DrawGlyphsRecorder& recorder = *static_cast<DrawGlyphsRecorder*>(CGContextDelegateGetInfo(delegate)); |
| recorder.recordEndLayer(rstate, gstate); |
| return delegate; |
| } |
| |
| static CGError drawGlyphs(CGContextDelegateRef delegate, CGRenderingStateRef rstate, CGGStateRef gstate, const CGAffineTransform* tm, const CGGlyph glyphs[], const CGPoint positions[], size_t count) |
| { |
| if (CGGStateGetAlpha(gstate) > 0) { |
| DrawGlyphsRecorder& recorder = *static_cast<DrawGlyphsRecorder*>(CGContextDelegateGetInfo(delegate)); |
| recorder.recordDrawGlyphs(rstate, gstate, tm, glyphs, positions, count); |
| } |
| return kCGErrorSuccess; |
| } |
| |
| static CGError drawImage(CGContextDelegateRef delegate, CGRenderingStateRef rstate, CGGStateRef gstate, CGRect rect, CGImageRef image) |
| { |
| DrawGlyphsRecorder& recorder = *static_cast<DrawGlyphsRecorder*>(CGContextDelegateGetInfo(delegate)); |
| recorder.recordDrawImage(rstate, gstate, rect, image); |
| return kCGErrorSuccess; |
| } |
| |
| UniqueRef<GraphicsContext> DrawGlyphsRecorder::createInternalContext() |
| { |
| auto contextDelegate = adoptCF(CGContextDelegateCreate(this)); |
| CGContextDelegateSetCallback(contextDelegate.get(), deBeginLayer, reinterpret_cast<CGContextDelegateCallback>(&beginLayer)); |
| CGContextDelegateSetCallback(contextDelegate.get(), deEndLayer, reinterpret_cast<CGContextDelegateCallback>(&endLayer)); |
| CGContextDelegateSetCallback(contextDelegate.get(), deDrawGlyphs, reinterpret_cast<CGContextDelegateCallback>(&WebCore::drawGlyphs)); |
| CGContextDelegateSetCallback(contextDelegate.get(), deDrawImage, reinterpret_cast<CGContextDelegateCallback>(&drawImage)); |
| #if HAVE(CORE_TEXT_FIX_FOR_RADAR_93925620) |
| auto contextType = kCGContextTypeUnknown; |
| #else |
| auto contextType = kCGContextTypeWindow; |
| #endif |
| auto context = adoptCF(CGContextCreateWithDelegate(contextDelegate.get(), contextType, nullptr, nullptr)); |
| return makeUniqueRef<GraphicsContextCG>(context.get()); |
| } |
| |
| DrawGlyphsRecorder::DrawGlyphsRecorder(GraphicsContext& owner, float scaleFactor, DeriveFontFromContext deriveFontFromContext) |
| : m_owner(owner) |
| , m_internalContext(createInternalContext()) |
| , m_deriveFontFromContext(deriveFontFromContext) |
| { |
| m_internalContext->applyDeviceScaleFactor(scaleFactor); |
| } |
| |
| void DrawGlyphsRecorder::populateInternalState(const GraphicsContextState& contextState) |
| { |
| m_originalState.fillBrush = contextState.fillBrush(); |
| m_originalState.strokeBrush = contextState.strokeBrush(); |
| |
| m_originalState.ctm = m_owner.getCTM(); |
| |
| m_originalState.dropShadow = contextState.dropShadow(); |
| m_originalState.ignoreTransforms = contextState.shadowsIgnoreTransforms(); |
| } |
| |
| void DrawGlyphsRecorder::populateInternalContext(const GraphicsContextState& contextState) |
| { |
| m_internalContext->setCTM(m_originalState.ctm); |
| |
| m_internalContext->setFillBrush(m_originalState.fillBrush); |
| m_internalContext->applyFillPattern(); |
| |
| m_internalContext->setStrokeBrush(m_originalState.strokeBrush); |
| m_internalContext->applyStrokePattern(); |
| |
| m_internalContext->setShadowsIgnoreTransforms(m_originalState.ignoreTransforms); |
| m_internalContext->setDropShadow(m_originalState.dropShadow); |
| |
| m_internalContext->setTextDrawingMode(contextState.textDrawingMode()); |
| } |
| |
| void DrawGlyphsRecorder::recordInitialColors() |
| { |
| CGContextRef cgContext = m_internalContext->platformContext(); |
| m_initialFillColor = CGContextGetFillColorAsColor(cgContext); |
| m_initialStrokeColor = CGContextGetStrokeColorAsColor(cgContext); |
| } |
| |
| void DrawGlyphsRecorder::prepareInternalContext(const Font& font, FontSmoothingMode smoothingMode) |
| { |
| ASSERT(CGAffineTransformIsIdentity(CTFontGetMatrix(font.platformData().ctFont()))); |
| |
| m_originalFont = &font; |
| m_smoothingMode = smoothingMode; |
| |
| m_originalTextMatrix = computeOverallTextMatrix(font); |
| if (font.platformData().orientation() == FontOrientation::Vertical) |
| m_originalTextMatrix = computeVerticalTextMatrix(font, m_originalTextMatrix); |
| |
| auto& contextState = m_owner.state(); |
| populateInternalState(contextState); |
| populateInternalContext(contextState); |
| recordInitialColors(); |
| } |
| |
| void DrawGlyphsRecorder::concludeInternalContext() |
| { |
| updateCTM(m_originalState.ctm); |
| updateFillBrush(m_originalState.fillBrush); |
| updateStrokeBrush(m_originalState.strokeBrush); |
| updateShadow(m_originalState.dropShadow, m_originalState.ignoreTransforms ? ShadowsIgnoreTransforms::Yes : ShadowsIgnoreTransforms::No); |
| } |
| |
| void DrawGlyphsRecorder::updateFillColor(CGColorRef fillColor) |
| { |
| if (CGColorGetPattern(fillColor)) { |
| ASSERT(m_originalState.fillBrush.pattern()); |
| return; |
| } |
| if (fillColor == m_initialFillColor) |
| m_owner.setFillBrush(m_originalState.fillBrush); |
| else |
| m_owner.setFillBrush(Color::createAndPreserveColorSpace(fillColor)); |
| } |
| |
| void DrawGlyphsRecorder::updateFillBrush(const SourceBrush& newBrush) |
| { |
| m_owner.setFillBrush(newBrush); |
| } |
| |
| void DrawGlyphsRecorder::updateStrokeColor(CGColorRef strokeColor) |
| { |
| if (CGColorGetPattern(strokeColor)) { |
| ASSERT(m_originalState.strokeBrush.pattern()); |
| return; |
| } |
| if (strokeColor == m_initialStrokeColor) |
| m_owner.setStrokeBrush(m_originalState.strokeBrush); |
| else |
| m_owner.setStrokeBrush(Color::createAndPreserveColorSpace(strokeColor)); |
| } |
| |
| void DrawGlyphsRecorder::updateStrokeBrush(const SourceBrush& newBrush) |
| { |
| m_owner.setStrokeBrush(newBrush); |
| } |
| |
| void DrawGlyphsRecorder::updateCTM(const AffineTransform& ctm) |
| { |
| if (m_owner.getCTM() == ctm) |
| return; |
| // Instead of recording a SetCTM command, we compute the transform needed |
| // to change the current CTM to `ctm`. This allows the recorded comamnds |
| // to be re-used by elements drawing the same text in different locations. |
| if (auto inverseOfCurrentCTM = m_owner.getCTM().inverse()) |
| m_owner.concatCTM(*inverseOfCurrentCTM * ctm); |
| } |
| |
| void DrawGlyphsRecorder::updateShadow(const DropShadow& dropShadow, ShadowsIgnoreTransforms shadowsIgnoreTransforms) |
| { |
| m_owner.setDropShadow(dropShadow); |
| m_owner.setShadowsIgnoreTransforms(shadowsIgnoreTransforms == ShadowsIgnoreTransforms::Yes); |
| } |
| |
| void DrawGlyphsRecorder::updateShadow(CGStyleRef style) |
| { |
| if (CGStyleGetType(style) != kCGStyleShadow) { |
| // FIXME: Support more kinds of CGStyles. |
| updateShadow({ }, ShadowsIgnoreTransforms::Unspecified); |
| return; |
| } |
| |
| const auto& shadowStyle = *static_cast<const CGShadowStyle*>(CGStyleGetData(style)); |
| auto rad = deg2rad(shadowStyle.azimuth - 180); |
| auto shadowOffset = FloatSize(std::cos(rad), std::sin(rad)) * shadowStyle.height; |
| auto shadowColor = CGStyleGetColor(style); |
| updateShadow({ shadowOffset, static_cast<float>(shadowStyle.radius), Color::createAndPreserveColorSpace(shadowColor) }, ShadowsIgnoreTransforms::Yes); |
| } |
| |
| void DrawGlyphsRecorder::recordBeginLayer(CGRenderingStateRef, CGGStateRef gstate, CGRect) |
| { |
| updateCTM(*CGGStateGetCTM(gstate)); |
| auto alpha = CGGStateGetAlpha(gstate); |
| m_owner.beginTransparencyLayer(alpha); |
| } |
| |
| void DrawGlyphsRecorder::recordEndLayer(CGRenderingStateRef, CGGStateRef gstate) |
| { |
| updateCTM(*CGGStateGetCTM(gstate)); |
| m_owner.endTransparencyLayer(); |
| } |
| |
| static Vector<CGSize> computeAdvancesFromPositions(const CGPoint positions[], size_t count, const CGAffineTransform& textMatrix) |
| { |
| Vector<CGSize> result; |
| for (size_t i = 0; i < count - 1; ++i) { |
| auto nextPosition = positions[i + 1]; |
| auto currentPosition = positions[i]; |
| auto advance = CGSizeMake(nextPosition.x - currentPosition.x, nextPosition.y - currentPosition.y); |
| result.append(CGSizeApplyAffineTransform(advance, textMatrix)); |
| } |
| result.constructAndAppend(CGSizeMake(0, 0)); |
| return result; |
| } |
| |
| void DrawGlyphsRecorder::recordDrawGlyphs(CGRenderingStateRef, CGGStateRef gstate, const CGAffineTransform*, const CGGlyph glyphs[], const CGPoint positions[], size_t count) |
| { |
| ASSERT_IMPLIES(m_deriveFontFromContext == DeriveFontFromContext::No, m_originalFont); |
| |
| if (!count) |
| return; |
| |
| CGFontRef usedFont = CGGStateGetFont(gstate); |
| if (m_deriveFontFromContext == DeriveFontFromContext::No && usedFont != adoptCF(CTFontCopyGraphicsFont(m_originalFont->platformData().ctFont(), nullptr)).get()) |
| return; |
| |
| updateCTM(*CGGStateGetCTM(gstate)); |
| |
| // We want the replayer's CTM and text matrix to match the current CTM and text matrix. |
| // The current text matrix is a concatenation of whatever WebKit sets it to and whatever |
| // Core Text appends to it. So, we have |
| // CTM * m_originalTextMatrix * Core Text's text matrix. |
| // However, CGContextGetTextMatrix() just tells us what the whole text matrix is, so |
| // m_originalTextMatrix * Core Text's text matrix = currentTextMatrix. |
| // The only way we can emulate Core Text's text matrix is by modifying the CTM here. |
| // So, if we do that, the GPU process will have |
| // CTM * X * m_originalTextMatrix |
| // If you set these two equal to each other, and solve for X, you get |
| // CTM * currentTextMatrix = CTM * X * m_originalTextMatrix |
| // currentTextMatrix * inverse(m_originalTextMatrix) = X |
| AffineTransform currentTextMatrix = CGContextGetTextMatrix(m_internalContext->platformContext()); |
| AffineTransform ctmFixup; |
| if (auto invertedOriginalTextMatrix = m_originalTextMatrix.inverse()) |
| ctmFixup = currentTextMatrix * invertedOriginalTextMatrix.value(); |
| AffineTransform inverseCTMFixup; |
| if (auto inverse = ctmFixup.inverse()) |
| inverseCTMFixup = inverse.value(); |
| else |
| ctmFixup = AffineTransform(); |
| m_owner.concatCTM(ctmFixup); |
| |
| updateFillColor(CGGStateGetFillColor(gstate)); |
| updateStrokeColor(CGGStateGetStrokeColor(gstate)); |
| updateShadow(CGGStateGetStyle(gstate)); |
| |
| auto fontSize = CGGStateGetFontSize(gstate); |
| Ref font = m_deriveFontFromContext == DeriveFontFromContext::No ? *m_originalFont : Font::create(FontPlatformData(adoptCF(CTFontCreateWithGraphicsFont(usedFont, fontSize, nullptr, nullptr)), fontSize)); |
| |
| // The above does the work of ensuring the right CTM (which is the combination of CG's CTM and |
| // CG's text matrix) is set for the replayer, but in order to provide the right values to |
| // `FontCascade::drawGlyphs` we need to recalculate the original advances from the resulting |
| // positions by inverting the operations applied to the original advances. |
| auto textMatrix = m_originalTextMatrix; |
| auto initialPenPosition = textMatrix.mapPoint(positions[0]); |
| |
| if (font->platformData().orientation() == FontOrientation::Vertical) { |
| // Keep this in sync as the inverse of `fillVectorWithVerticalGlyphPositions`. |
| // FIXME: Use rotateLeftTransform(), as fillVectorWithVerticalGlyphPositions() does, instead of using transposedSize(). |
| CGSize translation; |
| CTFontGetVerticalTranslationsForGlyphs(font->platformData().ctFont(), glyphs, &translation, 1); |
| |
| initialPenPosition += FloatSize(translation).transposedSize(); |
| |
| auto ascentDelta = font->fontMetrics().floatAscent(IdeographicBaseline) - font->fontMetrics().floatAscent(); |
| initialPenPosition.move(0, -ascentDelta); |
| } |
| |
| m_owner.drawGlyphsAndCacheResources(font, glyphs, computeAdvancesFromPositions(positions, count, textMatrix).data(), count, initialPenPosition, m_smoothingMode); |
| |
| m_owner.concatCTM(inverseCTMFixup); |
| } |
| |
| void DrawGlyphsRecorder::recordDrawImage(CGRenderingStateRef, CGGStateRef gstate, CGRect rect, CGImageRef cgImage) |
| { |
| updateCTM(*CGGStateGetCTM(gstate)); |
| updateShadow(CGGStateGetStyle(gstate)); |
| |
| // Core Graphics assumes a "y up" coordinate system, but in WebKit, we use a "y-down" coordinate system. |
| // This means that WebKit's drawing routines (GraphicsContext::drawImage()) intentionally draw images upside-down from Core Graphics's point of view. |
| // (There's a y-flip inside the implementation of GraphicsContext::drawImage().) |
| // The rect has the right bounds, but we need to transform from CG's coordinate system to WebKit's by performing our own y-flip so images are drawn the right-side-up. |
| // We do this at the boundary between the two APIs, which is right here. |
| m_owner.translate(0, rect.size.height + 2 * rect.origin.y); |
| m_owner.scale(FloatSize(1, -1)); |
| |
| auto image = NativeImage::create(cgImage); |
| m_owner.drawNativeImage(*image, image->size(), FloatRect(rect), FloatRect {{ }, image->size()}, ImagePaintingOptions { ImageOrientation::OriginTopLeft }); |
| |
| // Undo the above y-flip to restore the context. |
| m_owner.scale(FloatSize(1, -1)); |
| m_owner.translate(0, -(rect.size.height + 2 * rect.origin.y)); |
| } |
| |
| void DrawGlyphsRecorder::drawOTSVGRun(const Font& font, const GlyphBufferGlyph* glyphs, const GlyphBufferAdvance* advances, unsigned numGlyphs, const FloatPoint& startPoint, FontSmoothingMode smoothingMode) |
| { |
| FloatPoint penPosition = startPoint; |
| |
| for (unsigned i = 0; i < numGlyphs; ++i) { |
| auto bounds = font.boundsForGlyph(glyphs[i]); |
| |
| // Create a local ImageBuffer because decoding the SVG fonts has to happen in WebProcess. |
| if (auto imageBuffer = m_owner.createAlignedImageBuffer(bounds, DestinationColorSpace::SRGB(), RenderingMethod::Local)) { |
| FontCascade::drawGlyphs(imageBuffer->context(), font, glyphs + i, advances + i, 1, FloatPoint(), smoothingMode); |
| |
| FloatRect destinationRect = enclosingIntRect(bounds); |
| destinationRect.moveBy(penPosition); |
| m_owner.drawImageBuffer(*imageBuffer, destinationRect); |
| } |
| |
| penPosition.move(size(advances[i])); |
| } |
| } |
| |
| void DrawGlyphsRecorder::drawNonOTSVGRun(const Font& font, const GlyphBufferGlyph* glyphs, const GlyphBufferAdvance* advances, unsigned numGlyphs, const FloatPoint& startPoint, FontSmoothingMode smoothingMode) |
| { |
| prepareInternalContext(font, smoothingMode); |
| FontCascade::drawGlyphs(m_internalContext, font, glyphs, advances, numGlyphs, startPoint, smoothingMode); |
| concludeInternalContext(); |
| } |
| |
| void DrawGlyphsRecorder::drawBySplittingIntoOTSVGAndNonOTSVGRuns(const Font& font, const GlyphBufferGlyph* glyphs, const GlyphBufferAdvance* advances, unsigned numGlyphs, const FloatPoint& startPoint, FontSmoothingMode smoothingMode) |
| { |
| auto otsvgGlyphs = font.findOTSVGGlyphs(glyphs, numGlyphs); |
| if (!otsvgGlyphs) { |
| drawNonOTSVGRun(font, glyphs, advances, numGlyphs, startPoint, smoothingMode); |
| return; |
| } |
| |
| ASSERT(otsvgGlyphs->size() >= numGlyphs); |
| |
| // We can't just partition the glyphs into OT-SVG glyphs and non-OT-SVG glyphs because glyphs are allowed to draw outside of their layout boxes. |
| // This means that glyphs can overlap, which means we have to get the z-order correct. We can't have an earlier run be drawn on top of a later run. |
| FloatPoint runOrigin = startPoint; |
| FloatPoint penPosition = startPoint; |
| size_t glyphCountInRun = 0; |
| bool isOTSVGRun = false; |
| unsigned i; |
| auto draw = [&] () { |
| if (!glyphCountInRun) |
| return; |
| if (isOTSVGRun) |
| drawOTSVGRun(font, glyphs + i - glyphCountInRun, advances + i - glyphCountInRun, glyphCountInRun, runOrigin, smoothingMode); |
| else |
| drawNonOTSVGRun(font, glyphs + i - glyphCountInRun, advances + i - glyphCountInRun, glyphCountInRun, runOrigin, smoothingMode); |
| }; |
| for (i = 0; i < numGlyphs; ++i) { |
| bool isOTSVGGlyph = otsvgGlyphs->quickGet(i); |
| if (isOTSVGGlyph != isOTSVGRun) { |
| draw(); |
| isOTSVGRun = isOTSVGGlyph; |
| glyphCountInRun = 0; |
| runOrigin = penPosition; |
| } |
| ++glyphCountInRun; |
| penPosition.move(size(advances[i])); |
| } |
| draw(); |
| } |
| |
| void DrawGlyphsRecorder::drawGlyphs(const Font& font, const GlyphBufferGlyph* glyphs, const GlyphBufferAdvance* advances, unsigned numGlyphs, const FloatPoint& startPoint, FontSmoothingMode smoothingMode) |
| { |
| drawBySplittingIntoOTSVGAndNonOTSVGRuns(font, glyphs, advances, numGlyphs, startPoint, smoothingMode); |
| } |
| |
| void DrawGlyphsRecorder::drawNativeText(CTFontRef font, CGFloat fontSize, CTLineRef line, CGRect lineRect) |
| { |
| GraphicsContextStateSaver ownerSaver(m_owner); |
| GraphicsContextStateSaver internalContextSaver(m_internalContext.get()); |
| |
| m_owner.translate(lineRect.origin.x, lineRect.origin.y + lineRect.size.height); |
| m_owner.scale(FloatSize(1, -1)); |
| |
| prepareInternalContext(Font::create(FontPlatformData(font, fontSize)), FontSmoothingMode::SubpixelAntialiased); |
| CGContextSetTextPosition(m_internalContext->platformContext(), 0, 0); |
| CTLineDraw(line, m_internalContext->platformContext()); |
| concludeInternalContext(); |
| } |
| |
| } // namespace WebCore |