Bug 1625606 - Use Skia's mac font smoothing handling. r=aosmond

Currently we try to alternate between light-on-dark and dark-on-light
text rendering masks on macOS depending on text colors. Historically,
this matched earlier versions of macOS reasonably well. In successive
versions of macOS, however, this approximation appears to have broken
down and no longer gives correct results.

Given that recent macOS versions no longer promotes font-smoothing at
all, it seems more reasonable to match Skia's font smoothing handling
because it appears to better work with these more recent versions while
requiring us to have less special code to do so.

Differential Revision: https://phabricator.services.mozilla.com/D229444
This commit is contained in:
Lee Salzman 2024-11-19 07:37:51 +00:00
parent 1415e9c6a1
commit ccba550402
15 changed files with 45 additions and 97 deletions

View File

@ -4336,27 +4336,17 @@ static DeviceColor QuantizePreblendColor(const DeviceColor& aColor,
int32_t r = int32_t(aColor.r * 255.0f + 0.5f);
int32_t g = int32_t(aColor.g * 255.0f + 0.5f);
int32_t b = int32_t(aColor.b * 255.0f + 0.5f);
// Ensure that even if two values would normally quantize to the same bucket,
// that the reference value within the bucket still allows for accurate
// determination of whether light-on-dark or dark-on-light rasterization will
// be used (as on macOS).
bool lightOnDark = r >= 85 && g >= 85 && b >= 85 && r + g + b >= 2 * 255;
// Skia only uses the high 3 bits of each color component to cache preblend
// ramp tables.
constexpr int32_t lumBits = 3;
constexpr int32_t ceilMask = (1 << (8 - lumBits)) - 1;
constexpr int32_t floorMask = ((1 << lumBits) - 1) << (8 - lumBits);
if (!aUseSubpixelAA) {
// If not using subpixel AA, then quantize only the luminance, stored in the
// G channel.
g = (r * 54 + g * 183 + b * 19) >> 8;
g |= ceilMask;
// Still distinguish between light and dark in the key.
r = b = lightOnDark ? 255 : 0;
} else if (lightOnDark) {
r |= ceilMask;
g |= ceilMask;
b |= ceilMask;
g &= floorMask;
r = g;
b = g;
} else {
r &= floorMask;
g &= floorMask;
@ -4415,8 +4405,7 @@ bool SharedContextWebgl::DrawGlyphsAccel(ScaledFont* aFont,
#endif
// If the font has bitmaps, use the color directly. Otherwise, the texture
// will hold a grayscale mask, so encode the key's subpixel and light-or-dark
// state in the color.
// holds a grayscale mask, so encode the key's subpixel state in the color.
const Matrix& currentTransform = mCurrentTarget->GetTransform();
IntPoint quantizeScale = QuantizeScale(aFont, currentTransform);
Matrix quantizeTransform = currentTransform;

View File

@ -141,8 +141,6 @@
#define SK_USE_FREETYPE_EMBOLDEN
#define SK_IGNORE_MAC_BLENDING_MATCH_FIX
#ifndef MOZ_IMPLICIT
# ifdef MOZ_CLANG_PLUGIN
# define MOZ_IMPLICIT __attribute__((annotate("moz_implicit")))

View File

@ -280,8 +280,6 @@ public:
kBaselineSnap_Flag = 0x2000,
kNeedsForegroundColor_Flag = 0x4000,
kLightOnDark_Flag = 0x8000, // Moz + Mac only, used to distinguish different mask dilations
};
// computed values

View File

@ -177,8 +177,7 @@ SkScalerContext_Mac::Offscreen::Offscreen(SkColor foregroundColor)
CGRGBPixel* SkScalerContext_Mac::Offscreen::getCG(const SkScalerContext_Mac& context,
const SkGlyph& glyph, CGGlyph glyphID,
size_t* rowBytesPtr,
bool generateA8FromLCD,
bool lightOnDark) {
bool generateA8FromLCD) {
if (!fRGBSpace) {
//It doesn't appear to matter what color space is specified.
//Regular blends and antialiased text are always (s*a + d*(1-a))
@ -242,8 +241,7 @@ CGRGBPixel* SkScalerContext_Mac::Offscreen::getCG(const SkScalerContext_Mac& con
if (SkMask::kARGB32_Format != glyph.maskFormat()) {
// Draw black on white to create mask. (Special path exists to speed this up in CG.)
// If light-on-dark is requested, draw white on black.
CGContextSetGrayFillColor(fCG.get(), lightOnDark ? 1.0f : 0.0f, 1.0f);
CGContextSetGrayFillColor(fCG.get(), 0.0f, 1.0f);
} else {
CGContextSetFillColorWithColor(fCG.get(), fCGForegroundColor.get());
}
@ -270,7 +268,7 @@ CGRGBPixel* SkScalerContext_Mac::Offscreen::getCG(const SkScalerContext_Mac& con
// Erase to white (or transparent black if it's a color glyph, to not composite against white).
// For light-on-dark, instead erase to black.
uint32_t bgColor = (!glyph.isColor()) ? (lightOnDark ? 0xFF000000 : 0xFFFFFFFF) : 0x00000000;
uint32_t bgColor = (!glyph.isColor()) ? 0xFFFFFFFF : 0x00000000;
sk_memset_rect32(image, bgColor, glyph.width(), glyph.height(), rowBytes);
float subX = 0;
@ -473,11 +471,10 @@ void SkScalerContext_Mac::generateImage(const SkGlyph& glyph, void* imageBuffer)
// FIXME: lcd smoothed un-hinted rasterization unsupported.
bool requestSmooth = fRec.getHinting() != SkFontHinting::kNone;
bool lightOnDark = (fRec.fFlags & SkScalerContext::kLightOnDark_Flag) != 0;
// Draw the glyph
size_t cgRowBytes;
CGRGBPixel* cgPixels = fOffscreen.getCG(*this, glyph, cgGlyph, &cgRowBytes, requestSmooth, lightOnDark);
CGRGBPixel* cgPixels = fOffscreen.getCG(*this, glyph, cgGlyph, &cgRowBytes, requestSmooth);
if (cgPixels == nullptr) {
return;
}
@ -498,16 +495,10 @@ void SkScalerContext_Mac::generateImage(const SkGlyph& glyph, void* imageBuffer)
CGRGBPixel* addr = cgPixels;
for (int y = 0; y < glyph.height(); ++y) {
for (int x = 0; x < glyph.width(); ++x) {
int r = linear[(addr[x] >> 16) & 0xFF];
int g = linear[(addr[x] >> 8) & 0xFF];
int b = linear[(addr[x] >> 0) & 0xFF];
// If light-on-dark was requested, the mask is drawn inverted.
if (lightOnDark) {
r = 255 - r;
g = 255 - g;
b = 255 - b;
}
addr[x] = (r << 16) | (g << 8) | b;
int r = (addr[x] >> 16) & 0xFF;
int g = (addr[x] >> 8) & 0xFF;
int b = (addr[x] >> 0) & 0xFF;
addr[x] = (linear[r] << 16) | (linear[g] << 8) | linear[b];
}
addr = SkTAddOffset<CGRGBPixel>(addr, cgRowBytes);
}

View File

@ -55,8 +55,7 @@ private:
Offscreen(SkColor foregroundColor);
CGRGBPixel* getCG(const SkScalerContext_Mac& context, const SkGlyph& glyph,
CGGlyph glyphID, size_t* rowBytesPtr, bool generateA8FromLCD,
bool lightOnDark);
CGGlyph glyphID, size_t* rowBytesPtr, bool generateA8FromLCD);
private:
enum {

View File

@ -980,21 +980,6 @@ void SkTypeface_Mac::onFilterRec(SkScalerContextRec* rec) const {
rec->fMaskFormat = SkMask::kARGB32_Format;
}
// Smoothing will be used if the format is either LCD or if there is hinting.
// In those cases, we need to choose the proper dilation mask based on the color.
if (rec->fMaskFormat == SkMask::kLCD16_Format ||
(rec->fMaskFormat == SkMask::kA8_Format && rec->getHinting() != SkFontHinting::kNone)) {
SkColor color = rec->getLuminanceColor();
int r = SkColorGetR(color);
int g = SkColorGetG(color);
int b = SkColorGetB(color);
// Choose whether to draw using a light-on-dark mask based on observed
// color/luminance thresholds that CoreText uses.
if (r >= 85 && g >= 85 && b >= 85 && r + g + b >= 2 * 255) {
rec->fFlags |= SkScalerContext::kLightOnDark_Flag;
}
}
// Unhinted A8 masks (those not derived from LCD masks) must respect SK_GAMMA_APPLY_TO_A8.
// All other masks can use regular gamma.
if (SkMask::kA8_Format == rec->fMaskFormat && SkFontHinting::kNone == rec->getHinting()) {
@ -1003,7 +988,6 @@ void SkTypeface_Mac::onFilterRec(SkScalerContextRec* rec) const {
rec->ignorePreBlend();
#endif
} else {
#ifndef SK_IGNORE_MAC_BLENDING_MATCH_FIX
SkColor color = rec->getLuminanceColor();
if (smoothBehavior == SkCTFontSmoothBehavior::some) {
// CoreGraphics smoothed text without subpixel coverage blitting goes from a gamma of
@ -1021,7 +1005,6 @@ void SkTypeface_Mac::onFilterRec(SkScalerContextRec* rec) const {
SkColorGetB(color) * 3/4);
}
rec->setLuminanceColor(color);
#endif
// CoreGraphics dialates smoothed text to provide contrast.
rec->setContrast(0);

View File

@ -24,7 +24,7 @@ fuzzy-if(!useDrawSnapshot,14-14,44-95) == 1524353.html 1524353-ref.html
== bug1523410-translate-scale-snap.html bug1523410-translate-scale-snap-ref.html
== 1523080.html 1523080-ref.html
== 1616444-same-color-different-paths.html 1616444-same-color-different-paths-ref.html
skip-if(useDrawSnapshot||Android) fuzzy-if(winWidget,54-94,2713-3419) fuzzy-if(cocoaWidget,24-24,1190-1200) pref(apz.allow_zooming,true) == picture-caching-on-async-zoom.html picture-caching-on-async-zoom.html?ref
skip-if(useDrawSnapshot||Android) fuzzy-if(winWidget,54-94,2713-3419) fuzzy-if(cocoaWidget,22-24,1100-1200) pref(apz.allow_zooming,true) == picture-caching-on-async-zoom.html picture-caching-on-async-zoom.html?ref
pref(apz.allow_zooming,true) fails-if(useDrawSnapshot) fuzzy-if(!useDrawSnapshot,0-244,0-4285) == 1662062-1-no-blurry.html 1662062-1-ref.html
# Bug 1715676: nsBulletFrame has been removed and the new rendering does not use PushRoundedRect that this test is for:
# == 1681610.html 1681610-ref.html

View File

@ -127,12 +127,6 @@ lazy_static! {
static ref FONT_SMOOTHING_MODE: Option<FontRenderMode> = determine_font_smoothing_mode();
}
fn should_use_white_on_black(color: ColorU) -> bool {
let (r, g, b) = (color.r as u32, color.g as u32, color.b as u32);
// These thresholds were determined on 10.12 by observing what CG does.
r >= 85 && g >= 85 && b >= 85 && r + g + b >= 2 * 255
}
fn get_glyph_metrics(
ct_font: &CTFont,
transform: Option<&CGAffineTransform>,
@ -450,13 +444,22 @@ impl FontContext {
render_mode: FontRenderMode,
color: ColorU,
) {
let ColorU {r, g, b, a} = color;
let smooth_color = match *FONT_SMOOTHING_MODE {
// Use Skia's gamma approximation for subpixel smoothing of 3/4.
Some(FontRenderMode::Subpixel) => ColorU::new(r - r / 4, g - g / 4, b - b / 4, a),
// Use Skia's gamma approximation for grayscale smoothing of 1/2.
Some(FontRenderMode::Alpha) => ColorU::new(r / 2, g / 2, b / 2, a),
_ => color,
};
// Then convert back to gamma corrected values.
match render_mode {
FontRenderMode::Alpha => {
self.gamma_lut.preblend_grayscale(pixels, color);
self.gamma_lut.preblend_grayscale(pixels, smooth_color);
}
FontRenderMode::Subpixel => {
self.gamma_lut.preblend(pixels, color);
self.gamma_lut.preblend(pixels, smooth_color);
}
_ => {} // Again, give mono untouched since only the alpha matters.
}
@ -512,23 +515,13 @@ impl FontContext {
}
FontRenderMode::Alpha => {
font.color = if font.flags.contains(FontInstanceFlags::FONT_SMOOTHING) {
// Only the G channel is used to index grayscale tables,
// so use R and B to preserve light/dark determination.
let ColorU { g, a, .. } = font.color.luminance_color().quantized_ceil();
let rb = if should_use_white_on_black(font.color) { 255 } else { 0 };
ColorU::new(rb, g, rb, a)
font.color.luminance_color().quantize()
} else {
ColorU::new(255, 255, 255, 255)
};
}
FontRenderMode::Subpixel => {
// Quantization may change the light/dark determination, so quantize in the
// direction necessary to respect the threshold.
font.color = if should_use_white_on_black(font.color) {
font.color.quantized_ceil()
} else {
font.color.quantized_floor()
};
font.color = font.color.quantize();
}
}
}
@ -641,20 +634,15 @@ impl FontContext {
// table data, require the current font color to determine the output
// color. For such fonts we must thus supply the current font color just
// in case it is necessary.
let use_white_on_black = should_use_white_on_black(font.color);
let use_font_smoothing = font.flags.contains(FontInstanceFlags::FONT_SMOOTHING);
let (antialias, smooth, text_color, bg_color, invert) = match glyph_type {
GlyphType::Bitmap => (true, false, ColorF::from(font.color), ColorF::TRANSPARENT, false),
let (antialias, smooth, text_color, bg_color) = match glyph_type {
GlyphType::Bitmap => (true, false, ColorF::from(font.color), ColorF::TRANSPARENT),
GlyphType::Vector => {
match (font.render_mode, use_font_smoothing) {
(FontRenderMode::Subpixel, _) |
(FontRenderMode::Alpha, true) => if use_white_on_black {
(true, true, ColorF::WHITE, ColorF::BLACK, false)
} else {
(true, true, ColorF::BLACK, ColorF::WHITE, true)
},
(FontRenderMode::Alpha, false) => (true, false, ColorF::BLACK, ColorF::WHITE, true),
(FontRenderMode::Mono, _) => (false, false, ColorF::BLACK, ColorF::WHITE, true),
(FontRenderMode::Alpha, true) => (true, true, ColorF::BLACK, ColorF::WHITE),
(FontRenderMode::Alpha, false) => (true, false, ColorF::BLACK, ColorF::WHITE),
(FontRenderMode::Mono, _) => (false, false, ColorF::BLACK, ColorF::WHITE),
}
}
};
@ -757,11 +745,9 @@ impl FontContext {
}
for pixel in rasterized_pixels.chunks_mut(4) {
if invert {
pixel[0] = 255 - pixel[0];
pixel[1] = 255 - pixel[1];
pixel[2] = 255 - pixel[2];
}
pixel[0] = 255 - pixel[0];
pixel[1] = 255 - pixel[1];
pixel[2] = 255 - pixel[2];
// Set alpha to the value of the green channel. For grayscale
// text, all three channels have the same value anyway.

View File

@ -20,7 +20,7 @@ fuzzy(2,405) fuzzy-if(platform(swgl),2,1510) == split-batch.yaml split-batch-ref
# Next 3 tests affected by bug 1548099 on Android
skip_on(android) == shadow-red.yaml shadow-red-ref.yaml
skip_on(android) fuzzy(1,999) fuzzy-if(platform(swgl),2,1324) == shadow-grey.yaml shadow-grey-ref.yaml
skip_on(android) fuzzy(1,828) fuzzy-if(platform(swgl),2,1538) == shadow-grey-transparent.yaml shadow-grey-ref.yaml
skip_on(android) fuzzy(1,834) fuzzy-if(platform(swgl),2,1538) == shadow-grey-transparent.yaml shadow-grey-ref.yaml
== subtle-shadow.yaml subtle-shadow-ref.yaml
fuzzy(1,64) == shadow-atomic.yaml shadow-atomic-ref.yaml
fuzzy(1,64) == shadow-clip-rect.yaml shadow-atomic-ref.yaml

View File

@ -31,7 +31,7 @@ random-if(cocoaWidget) == subpixel-1.html about:blank # see bug 1192616, re-enab
== text-rtl-alignment-test.html text-rtl-alignment-ref.html
fuzzy-if(winWidget,0-1,0-256) == text-horzline-with-bottom.html text-horzline.html
fuzzy-if(winWidget,0-1,0-256) fails-if(cocoaWidget) == text-horzline-with-top.html text-horzline.html
fuzzy-if(winWidget,0-1,0-256) == text-horzline-with-top.html text-horzline.html
!= text-big-stroke.html text-blank.html
!= text-big-stroke.html text-big-fill.html

View File

@ -154,9 +154,9 @@ HTTP(..) == font-redirect.html order-1-ref.html
== dynamic-duplicate-rule-1c.html dynamic-duplicate-rule-1-ref.html
# Test for COLR and CPAL support
fuzzy-if(cocoaWidget,198-198,172-172) == color-1a.html color-1-ref.html
fuzzy-if(cocoaWidget,154-198,172-172) == color-1a.html color-1-ref.html
!= color-1a.html color-1-notref.html
fuzzy-if(cocoaWidget,198-198,172-172) == color-1b.html color-1-ref.html
fuzzy-if(cocoaWidget,154-198,172-172) == color-1b.html color-1-ref.html
== color-2a.html color-2-ref.html
!= color-2a.html color-2-notref.html

View File

@ -25,7 +25,7 @@ fuzzy-if(winWidget,47-138,260-319) == simple-bidi.svg simple-bidi-ref.html
== simple-dx-rtl-2.svg simple-dx-rtl-2-ref.svg
== simple-fill-color-dynamic.svg simple-fill-color-dynamic-ref.svg
fuzzy-if(winWidget,47-129,221-254) fuzzy-if(cocoaWidget,23-65,195-196) == simple-fill-color.svg simple-fill-color-ref.html
fuzzy-if(winWidget,47-129,221-254) fuzzy-if(cocoaWidget,0-65,0-196) == simple-fill-color.svg simple-fill-color-ref.html
== simple-fill-gradient.svg simple-fill-gradient-ref.svg
== simple-fill-none.svg simple.svg
== simple-pointer-events.svg simple.svg

View File

@ -1,4 +1,4 @@
[first-line-bidi-002.html]
fuzzy:
if (os == "mac"): maxDifference=99-120;totalPixels=0-18
if (os == "mac"): maxDifference=82-120;totalPixels=0-18
if (os == "win"): maxDifference=13-143;totalPixels=0-19

View File

@ -4,3 +4,4 @@
if win11_2009 and bits == 32: PASS
if (os == "win") and (processor == "x86_64") and debug and not swgl: [FAIL, PASS]
if (os == "win") and (processor == "x86_64") and not debug: FAIL
if (os == "mac"): FAIL

View File

@ -0,0 +1,3 @@
[hyphen-as-minus-sign.html]
fuzzy:
if (os == "mac"): maxDifference=0-1;totalPixels=0-47