blob: c19e741557c308c941e4ef715e3db46594d185b6 [file] [log] [blame]
/*
* Copyright (C) 2006-2009, 2013, 2016 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 "FontCascade.h"
#if USE(CG)
#include "AffineTransform.h"
#include "FloatConversion.h"
#include "Font.h"
#include "GlyphBuffer.h"
#include "GraphicsContext.h"
#include "IntRect.h"
#include "WebCoreTextRenderer.h"
#include <pal/spi/cg/CoreGraphicsSPI.h>
#include <wtf/MathExtras.h>
namespace WebCore {
static inline CGFloat toCGFloat(FIXED f)
{
return f.value + f.fract / CGFloat(65536.0);
}
static CGPathRef createPathForGlyph(HDC hdc, Glyph glyph)
{
CGMutablePathRef path = CGPathCreateMutable();
static const MAT2 identity = { 0, 1, 0, 0, 0, 0, 0, 1 };
GLYPHMETRICS glyphMetrics;
// GGO_NATIVE matches the outline perfectly when Windows font smoothing is off.
// GGO_NATIVE | GGO_UNHINTED does not match perfectly either when Windows font smoothing is on or off.
DWORD outlineLength = GetGlyphOutline(hdc, glyph, GGO_GLYPH_INDEX | GGO_NATIVE, &glyphMetrics, 0, 0, &identity);
ASSERT(outlineLength >= 0);
if (outlineLength < 0)
return path;
Vector<UInt8> outline(outlineLength);
GetGlyphOutline(hdc, glyph, GGO_GLYPH_INDEX | GGO_NATIVE, &glyphMetrics, outlineLength, outline.data(), &identity);
unsigned offset = 0;
while (offset < outlineLength) {
LPTTPOLYGONHEADER subpath = reinterpret_cast<LPTTPOLYGONHEADER>(outline.data() + offset);
ASSERT(subpath->dwType == TT_POLYGON_TYPE);
if (subpath->dwType != TT_POLYGON_TYPE)
return path;
CGPathMoveToPoint(path, 0, toCGFloat(subpath->pfxStart.x), toCGFloat(subpath->pfxStart.y));
unsigned subpathOffset = sizeof(*subpath);
while (subpathOffset < subpath->cb) {
LPTTPOLYCURVE segment = reinterpret_cast<LPTTPOLYCURVE>(reinterpret_cast<UInt8*>(subpath) + subpathOffset);
switch (segment->wType) {
case TT_PRIM_LINE:
for (unsigned i = 0; i < segment->cpfx; i++)
CGPathAddLineToPoint(path, 0, toCGFloat(segment->apfx[i].x), toCGFloat(segment->apfx[i].y));
break;
case TT_PRIM_QSPLINE:
for (unsigned i = 0; i < segment->cpfx; i++) {
CGFloat x = toCGFloat(segment->apfx[i].x);
CGFloat y = toCGFloat(segment->apfx[i].y);
CGFloat cpx;
CGFloat cpy;
if (i == segment->cpfx - 2) {
cpx = toCGFloat(segment->apfx[i + 1].x);
cpy = toCGFloat(segment->apfx[i + 1].y);
i++;
} else {
cpx = (toCGFloat(segment->apfx[i].x) + toCGFloat(segment->apfx[i + 1].x)) / 2;
cpy = (toCGFloat(segment->apfx[i].y) + toCGFloat(segment->apfx[i + 1].y)) / 2;
}
CGPathAddQuadCurveToPoint(path, 0, x, y, cpx, cpy);
}
break;
case TT_PRIM_CSPLINE:
for (unsigned i = 0; i < segment->cpfx; i += 3) {
CGFloat cp1x = toCGFloat(segment->apfx[i].x);
CGFloat cp1y = toCGFloat(segment->apfx[i].y);
CGFloat cp2x = toCGFloat(segment->apfx[i + 1].x);
CGFloat cp2y = toCGFloat(segment->apfx[i + 1].y);
CGFloat x = toCGFloat(segment->apfx[i + 2].x);
CGFloat y = toCGFloat(segment->apfx[i + 2].y);
CGPathAddCurveToPoint(path, 0, cp1x, cp1y, cp2x, cp2y, x, y);
}
break;
default:
ASSERT_NOT_REACHED();
return path;
}
subpathOffset += sizeof(*segment) + (segment->cpfx - 1) * sizeof(segment->apfx[0]);
}
CGPathCloseSubpath(path);
offset += subpath->cb;
}
return path;
}
void FontCascade::drawGlyphs(GraphicsContext& graphicsContext, const Font& font, const GlyphBufferGlyph* glyphs,
const GlyphBufferAdvance* advances, unsigned numGlyphs, const FloatPoint& point, FontSmoothingMode smoothingMode)
{
CGContextRef cgContext = graphicsContext.platformContext();
bool shouldUseFontSmoothing = WebCoreShouldUseFontSmoothing();
switch (smoothingMode) {
case FontSmoothingMode::Antialiased: {
graphicsContext.setShouldAntialias(true);
shouldUseFontSmoothing = false;
break;
}
case FontSmoothingMode::SubpixelAntialiased: {
graphicsContext.setShouldAntialias(true);
shouldUseFontSmoothing = true;
break;
}
case FontSmoothingMode::NoSmoothing: {
graphicsContext.setShouldAntialias(false);
shouldUseFontSmoothing = false;
break;
}
case FontSmoothingMode::AutoSmoothing: {
// For the AutoSmooth case, don't do anything! Keep the default settings.
break;
}
default:
ASSERT_NOT_REACHED();
}
uint32_t oldFontSmoothingStyle = FontCascade::setFontSmoothingStyle(cgContext, shouldUseFontSmoothing);
const FontPlatformData& platformData = font.platformData();
CGContextSetFont(cgContext, platformData.cgFont());
CGAffineTransform matrix = CGAffineTransformIdentity;
matrix.b = -matrix.b;
matrix.d = -matrix.d;
if (platformData.syntheticOblique()) {
static float skew = -tanf(syntheticObliqueAngle() * piFloat / 180.0f);
matrix = CGAffineTransformConcat(matrix, CGAffineTransformMake(1, 0, skew, 1, 0, 0));
}
CGAffineTransform savedMatrix = CGContextGetTextMatrix(cgContext);
CGContextSetTextMatrix(cgContext, matrix);
CGContextSetFontSize(cgContext, platformData.size());
FontCascade::setCGContextFontRenderingStyle(cgContext, font.platformData().isSystemFont(), false, font.platformData().useGDI());
FloatSize shadowOffset;
float shadowBlur;
Color shadowColor;
graphicsContext.getShadow(shadowOffset, shadowBlur, shadowColor);
bool hasSimpleShadow = graphicsContext.textDrawingMode() == TextDrawingMode::Fill && shadowColor.isValid() && !shadowBlur && (!graphicsContext.shadowsIgnoreTransforms() || graphicsContext.getCTM().isIdentityOrTranslationOrFlipped());
if (hasSimpleShadow) {
// Paint simple shadows ourselves instead of relying on CG shadows, to avoid losing subpixel antialiasing.
graphicsContext.clearShadow();
Color fillColor = graphicsContext.fillColor();
Color shadowFillColor = shadowColor.colorWithAlphaMultipliedBy(fillColor.alphaAsFloat());
graphicsContext.setFillColor(shadowFillColor);
float shadowTextX = point.x() + shadowOffset.width();
// If shadows are ignoring transforms, then we haven't applied the Y coordinate flip yet, so down is negative.
float shadowTextY = point.y() + shadowOffset.height() * (graphicsContext.shadowsIgnoreTransforms() ? -1 : 1);
CGContextSetTextPosition(cgContext, shadowTextX, shadowTextY);
CGContextShowGlyphsWithAdvances(cgContext, glyphs, advances, numGlyphs);
if (font.syntheticBoldOffset()) {
CGContextSetTextPosition(cgContext, point.x() + shadowOffset.width() + font.syntheticBoldOffset(), point.y() + shadowOffset.height());
CGContextShowGlyphsWithAdvances(cgContext, glyphs, advances, numGlyphs);
}
graphicsContext.setFillColor(fillColor);
}
CGContextSetTextPosition(cgContext, point.x(), point.y());
CGContextShowGlyphsWithAdvances(cgContext, glyphs, advances, numGlyphs);
if (font.syntheticBoldOffset()) {
CGContextSetTextPosition(cgContext, point.x() + font.syntheticBoldOffset(), point.y());
CGContextShowGlyphsWithAdvances(cgContext, glyphs, advances, numGlyphs);
}
if (hasSimpleShadow)
graphicsContext.setShadow(shadowOffset, shadowBlur, shadowColor);
FontCascade::setFontSmoothingStyle(cgContext, oldFontSmoothingStyle);
CGContextSetTextMatrix(cgContext, savedMatrix);
}
constexpr uint32_t kCGFontSmoothingStyleMinimum = (1 << 4);
constexpr uint32_t kCGFontSmoothingStyleLight = (2 << 4);
constexpr uint32_t kCGFontSmoothingStyleMedium = (3 << 4);
constexpr uint32_t kCGFontSmoothingStyleHeavy = (4 << 4);
constexpr int fontSmoothingLevelMedium = 2;
constexpr CGFloat antialiasingGamma = 2.3;
double FontCascade::s_fontSmoothingContrast = 2;
uint32_t FontCascade::s_fontSmoothingType = kCGFontSmoothingStyleMedium;
int FontCascade::s_fontSmoothingLevel = fontSmoothingLevelMedium;
bool FontCascade::s_systemFontSmoothingEnabled;
uint32_t FontCascade::s_systemFontSmoothingType;
bool FontCascade::s_systemFontSmoothingSet;
void FontCascade::setFontSmoothingLevel(int level)
{
const uint32_t smoothingType[] = {
0, // FontSmoothingTypeStandard
kCGFontSmoothingStyleLight, // FontSmoothingTypeLight
kCGFontSmoothingStyleMedium, // FontSmoothingTypeMedium
kCGFontSmoothingStyleHeavy, // FontSmoothingTypeStrong
};
if (level < 0 || static_cast<size_t>(level) > ARRAYSIZE(smoothingType))
return;
s_fontSmoothingType = smoothingType[level];
s_fontSmoothingLevel = level;
}
static void setCGFontSmoothingStyle(CGContextRef cgContext, uint32_t smoothingType, bool fontAllowsSmoothing = true)
{
if (smoothingType) {
CGContextSetShouldSmoothFonts(cgContext, fontAllowsSmoothing);
CGContextSetFontSmoothingStyle(cgContext, smoothingType);
} else
CGContextSetShouldSmoothFonts(cgContext, false);
}
uint32_t FontCascade::setFontSmoothingStyle(CGContextRef cgContext, bool fontAllowsSmoothing)
{
uint32_t oldFontSmoothingStyle = 0;
if (CGContextGetShouldSmoothFonts(cgContext))
oldFontSmoothingStyle = CGContextGetFontSmoothingStyle(cgContext);
setCGFontSmoothingStyle(cgContext, s_fontSmoothingType, fontAllowsSmoothing);
return oldFontSmoothingStyle;
}
void FontCascade::setFontSmoothingContrast(CGFloat contrast)
{
s_fontSmoothingContrast = contrast;
}
static float clearTypeContrast()
{
const WCHAR referenceCharacter = '\\';
static UINT lastContrast = 2000;
static float gamma = 2;
UINT contrast;
if (!SystemParametersInfo(SPI_GETFONTSMOOTHINGCONTRAST, 0, &contrast, 0) || contrast == lastContrast)
return gamma;
lastContrast = contrast;
auto dc = adoptGDIObject(::CreateCompatibleDC(0));
HGDIOBJ oldHFONT = ::SelectObject(dc.get(), GetStockObject(DEFAULT_GUI_FONT));
GLYPHMETRICS glyphMetrics;
static const MAT2 identity = { 0, 1, 0, 0, 0, 0, 0, 1 };
if (::GetGlyphOutline(dc.get(), referenceCharacter, GGO_METRICS, &glyphMetrics, 0, 0, &identity) == GDI_ERROR)
return contrast / 1000.0f;
BITMAPINFO bitmapInfo;
bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biCompression = BI_RGB;
bitmapInfo.bmiHeader.biSizeImage = 0;
bitmapInfo.bmiHeader.biXPelsPerMeter = 0;
bitmapInfo.bmiHeader.biYPelsPerMeter = 0;
bitmapInfo.bmiHeader.biClrImportant = 0;
bitmapInfo.bmiHeader.biWidth = glyphMetrics.gmBlackBoxX;
bitmapInfo.bmiHeader.biHeight = -static_cast<int>(glyphMetrics.gmBlackBoxY);
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biClrUsed = 0;
uint8_t* pixels = nullptr;
auto bitmap = adoptGDIObject(::CreateDIBSection(0, &bitmapInfo, DIB_RGB_COLORS, reinterpret_cast<void**>(&pixels), 0, 0));
if (!bitmap)
return contrast / 1000.0f;
HGDIOBJ oldBitmap = ::SelectObject(dc.get(), bitmap.get());
BITMAP bmpInfo;
::GetObject(bitmap.get(), sizeof(bmpInfo), &bmpInfo);
memset(pixels, 0, glyphMetrics.gmBlackBoxY * bmpInfo.bmWidthBytes);
::SetBkMode(dc.get(), OPAQUE);
::SetTextAlign(dc.get(), TA_LEFT | TA_TOP);
::SetTextColor(dc.get(), RGB(255, 255, 255));
::SetBkColor(dc.get(), RGB(0, 0, 0));
::ExtTextOutW(dc.get(), 0, 0, 0, 0, &referenceCharacter, 1, 0);
uint8_t* referencePixel = nullptr;
uint8_t whiteReferenceValue = 0;
for (size_t i = 0; i < glyphMetrics.gmBlackBoxY && !referencePixel; ++i) {
for (size_t j = 0; j < 4 * glyphMetrics.gmBlackBoxX; ++j) {
whiteReferenceValue = pixels[i * bmpInfo.bmWidthBytes + j];
// Look for a pixel value in the range that allows us to estimate
// gamma within 0.1 without an error.
if (whiteReferenceValue > 32 && whiteReferenceValue < 240) {
referencePixel = pixels + i * bmpInfo.bmWidthBytes + j;
break;
}
}
}
if (referencePixel) {
::SetTextColor(dc.get(), RGB(0, 0, 0));
::SetBkColor(dc.get(), RGB(255, 255, 255));
::ExtTextOutW(dc.get(), 0, 0, 0, 0, &referenceCharacter, 1, 0);
uint8_t blackReferenceValue = *referencePixel;
float minDelta = 1;
for (float g = 1; g < 2.3f; g += 0.1f) {
float delta = fabs(powf((whiteReferenceValue / 255.0f), g) + powf((blackReferenceValue / 255.0f), g) - 1);
if (delta < minDelta) {
minDelta = delta;
gamma = g;
}
}
} else
gamma = contrast / 1000.0f;
::SelectObject(dc.get(), oldBitmap);
::SelectObject(dc.get(), oldHFONT);
return gamma;
}
void FontCascade::systemFontSmoothingChanged()
{
::SystemParametersInfo(SPI_GETFONTSMOOTHING, 0, &s_systemFontSmoothingEnabled, 0);
::SystemParametersInfo(SPI_GETFONTSMOOTHINGTYPE, 0, &s_systemFontSmoothingType, 0);
s_fontSmoothingContrast = clearTypeContrast();
s_systemFontSmoothingSet = true;
}
void FontCascade::setCGContextFontRenderingStyle(CGContextRef cgContext, bool isSystemFont, bool /*isPrinterFont*/, bool usePlatformNativeGlyphs)
{
bool shouldAntialias = true;
bool maySubpixelPosition = true;
CGFloat contrast = 2;
if (usePlatformNativeGlyphs) {
// <rdar://6564501> GDI can't subpixel-position, so don't bother asking.
maySubpixelPosition = false;
if (!s_systemFontSmoothingSet)
systemFontSmoothingChanged();
contrast = s_fontSmoothingContrast;
shouldAntialias = s_systemFontSmoothingEnabled;
if (s_systemFontSmoothingType == FE_FONTSMOOTHINGSTANDARD) {
CGContextSetFontSmoothingStyle(cgContext, kCGFontSmoothingStyleMinimum);
contrast = antialiasingGamma;
}
}
CGContextSetFontSmoothingContrast(cgContext, contrast);
CGContextSetShouldUsePlatformNativeGlyphs(cgContext, usePlatformNativeGlyphs);
CGContextSetShouldAntialiasFonts(cgContext, shouldAntialias);
CGAffineTransform contextTransform = CGContextGetCTM(cgContext);
bool isPureTranslation = contextTransform.a == 1 && (contextTransform.d == 1 || contextTransform.d == -1) && !contextTransform.b && !contextTransform.c;
CGContextSetShouldSubpixelPositionFonts(cgContext, maySubpixelPosition && (isSystemFont || !isPureTranslation));
CGContextSetShouldSubpixelQuantizeFonts(cgContext, isPureTranslation);
}
static inline CGFontRenderingStyle renderingStyleForFont(bool isSystemFont, bool isPrinterFont)
{
// FIXME: Need to support a minimum antialiased font size.
if (isSystemFont || isPrinterFont)
return kCGFontRenderingStyleAntialiasing | kCGFontRenderingStyleSubpixelPositioning | kCGFontRenderingStyleSubpixelQuantization;
return kCGFontRenderingStyleAntialiasing;
}
void FontCascade::getPlatformGlyphAdvances(CGFontRef font, const CGAffineTransform& m, bool isSystemFont, bool isPrinterFont, CGGlyph glyph, CGSize& advance)
{
CGFontRenderingStyle style = renderingStyleForFont(isSystemFont, isPrinterFont);
CGFontGetGlyphAdvancesForStyle(font, &m, style, &glyph, 1, &advance);
// <rdar://problem/7761165> The GDI back end in Core Graphics sometimes returns advances that
// differ from what the font's hmtx table specifies. The following code corrects that.
auto hmtxTable = adoptCF(CGFontCopyTableForTag(font, 'hmtx'));
if (!hmtxTable)
return;
auto hheaTable = adoptCF(CGFontCopyTableForTag(font, 'hhea'));
if (!hheaTable)
return;
const CFIndex hheaTableSize = 36;
const ptrdiff_t hheaTableNumberOfHMetricsOffset = 34;
if (CFDataGetLength(hheaTable.get()) < hheaTableSize)
return;
unsigned short numberOfHMetrics = *reinterpret_cast<const unsigned short*>(CFDataGetBytePtr(hheaTable.get()) + hheaTableNumberOfHMetricsOffset);
numberOfHMetrics = ((numberOfHMetrics & 0xFF) << 8) | (numberOfHMetrics >> 8);
if (!numberOfHMetrics)
return;
if (glyph >= numberOfHMetrics)
glyph = numberOfHMetrics - 1;
if (CFDataGetLength(hmtxTable.get()) < 4 * (glyph + 1))
return;
unsigned short advanceInDesignUnits = *reinterpret_cast<const unsigned short*>(CFDataGetBytePtr(hmtxTable.get()) + 4 * glyph);
advanceInDesignUnits = ((advanceInDesignUnits & 0xFF) << 8) | (advanceInDesignUnits >> 8);
CGSize horizontalAdvance = CGSizeMake(static_cast<CGFloat>(advanceInDesignUnits) / CGFontGetUnitsPerEm(font), 0);
horizontalAdvance = CGSizeApplyAffineTransform(horizontalAdvance, m);
advance.width = horizontalAdvance.width;
if (!(style & kCGFontRenderingStyleSubpixelPositioning))
advance.width = roundf(advance.width);
}
}
#endif