summaryrefslogtreecommitdiffstats
path: root/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs')
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs899
1 files changed, 899 insertions, 0 deletions
diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs
new file mode 100644
index 0000000000..804dbc93e8
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs
@@ -0,0 +1,899 @@
+/* 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/. */
+
+use api::{ColorF, ColorU, FontKey, FontRenderMode, FontSize, GlyphDimensions};
+use api::{FontInstanceFlags, FontVariation, NativeFontHandle};
+use core_foundation::data::CFData;
+use core_foundation::base::TCFType;
+use core_foundation::dictionary::CFDictionary;
+use core_foundation::number::{CFNumber};
+use core_foundation::string::CFString;
+use core_graphics::base::{kCGImageAlphaNoneSkipFirst, kCGImageAlphaPremultipliedFirst};
+use core_graphics::base::{kCGBitmapByteOrder32Little};
+use core_graphics::color_space::CGColorSpace;
+use core_graphics::context::CGContext;
+use core_graphics::context::{CGBlendMode, CGTextDrawingMode};
+use core_graphics::font::{CGFont, CGGlyph};
+use core_graphics::geometry::{CGAffineTransform, CGPoint, CGSize};
+use core_graphics::geometry::{CG_AFFINE_TRANSFORM_IDENTITY, CGRect};
+use core_text;
+use core_text::font::CTFont;
+use core_text::font_descriptor::{CTFontDescriptor, kCTFontDefaultOrientation, kCTFontVariationAttribute};
+use core_text::font_manager;
+use euclid::default::Size2D;
+use crate::gamma_lut::{ColorLut, GammaLut};
+use crate::rasterizer::{FontInstance, FontTransform, GlyphKey};
+use crate::rasterizer::{GlyphFormat, GlyphRasterError, GlyphRasterResult, RasterizedGlyph};
+use crate::types::FastHashMap;
+use std::collections::hash_map::Entry;
+use std::sync::Arc;
+
+const INITIAL_CG_CONTEXT_SIDE_LENGTH: u32 = 32;
+
+pub struct FontContext {
+ ct_font_descs: FastHashMap<FontKey, CTFontDescriptor>,
+ // Table mapping a sized font key with variations to its instantiated CoreText font.
+ ct_fonts: FastHashMap<(FontKey, FontSize, Vec<FontVariation>), CTFont>,
+ #[allow(dead_code)]
+ graphics_context: GraphicsContext,
+ #[allow(dead_code)]
+ gamma_lut: GammaLut,
+}
+
+// core text is safe to use on multiple threads and non-shareable resources are
+// all hidden inside their font context.
+unsafe impl Send for FontContext {}
+
+struct GlyphMetrics {
+ rasterized_left: i32,
+ #[allow(dead_code)]
+ rasterized_descent: i32,
+ rasterized_ascent: i32,
+ rasterized_width: i32,
+ rasterized_height: i32,
+ advance: f32,
+}
+
+// There are a number of different OS prefs that control whether or not
+// requesting font smoothing actually results in subpixel AA. This gets even
+// murkier in newer macOS versions that deprecate subpixel AA, with the prefs
+// potentially interacting and overriding each other. In an attempt to future-
+// proof things against any new prefs or interpretation of those prefs in
+// future macOS versions, we do a check here to request font smoothing and see
+// what result it actually gives us much like Skia does. We need to check for
+// each of three potential results and process them in the font backend in
+// distinct ways:
+// 1) subpixel AA (differing RGB channels) with dilation
+// 2) grayscale AA (matching RGB channels) with dilation, a compatibility mode
+// 3) grayscale AA without dilation as if font smoothing was not requested
+// We can discern between case 1 and the rest by checking if the subpixels differ.
+// We can discern between cases 2 and 3 by rendering with and without smoothing
+// and comparing the two to determine if there was some dilation.
+// This returns the actual FontRenderMode needed to support each case, if any.
+fn determine_font_smoothing_mode() -> Option<FontRenderMode> {
+ let mut smooth_context = CGContext::create_bitmap_context(
+ None,
+ 12,
+ 12,
+ 8,
+ 12 * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little,
+ );
+ smooth_context.set_should_smooth_fonts(true);
+ smooth_context.set_should_antialias(true);
+ smooth_context.set_rgb_fill_color(1.0, 1.0, 1.0, 1.0);
+ let mut gray_context = CGContext::create_bitmap_context(
+ None,
+ 12,
+ 12,
+ 8,
+ 12 * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little,
+ );
+ gray_context.set_should_smooth_fonts(false);
+ gray_context.set_should_antialias(true);
+ gray_context.set_rgb_fill_color(1.0, 1.0, 1.0, 1.0);
+
+ // Autorelease pool for CTFont
+ objc::rc::autoreleasepool(|| {
+ // Lucida Grande 12 is the default fallback font in Firefox
+ let ct_font = core_text::font::new_from_name("Lucida Grande", 12.).unwrap();
+ let point = CGPoint { x: 0., y: 0. };
+ let glyph = 'X' as CGGlyph;
+ ct_font.draw_glyphs(&[glyph], &[point], smooth_context.clone());
+ ct_font.draw_glyphs(&[glyph], &[point], gray_context.clone());
+ });
+
+ let mut mode = None;
+ for (smooth, gray) in smooth_context.data().chunks(4).zip(gray_context.data().chunks(4)) {
+ if smooth[0] != smooth[1] || smooth[1] != smooth[2] {
+ return Some(FontRenderMode::Subpixel);
+ }
+ if smooth[0] != gray[0] || smooth[1] != gray[1] || smooth[2] != gray[2] {
+ mode = Some(FontRenderMode::Alpha);
+ }
+ }
+ return mode;
+}
+
+// We cache the font smoothing mode globally, rather than storing it in each FontContext,
+// to avoid having to determine this redundantly in each context and to avoid needing to
+// lock them to access this setting in prepare_font.
+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>,
+ glyph: CGGlyph,
+ x_offset: f64,
+ y_offset: f64,
+ extra_width: f64,
+) -> GlyphMetrics {
+ let mut bounds = ct_font.get_bounding_rects_for_glyphs(kCTFontDefaultOrientation, &[glyph]);
+
+ if bounds.origin.x.is_nan() || bounds.origin.y.is_nan() || bounds.size.width.is_nan() ||
+ bounds.size.height.is_nan()
+ {
+ // If an unexpected glyph index is requested, core text will return NaN values
+ // which causes us to do bad thing as the value is cast into an integer and
+ // overflow when expanding the bounds a few lines below.
+ // Instead we are better off returning zero-sized metrics because this special
+ // case is handled by the callers of this method.
+ return GlyphMetrics {
+ rasterized_left: 0,
+ rasterized_width: 0,
+ rasterized_height: 0,
+ rasterized_ascent: 0,
+ rasterized_descent: 0,
+ advance: 0.0,
+ };
+ }
+
+ let mut advance = CGSize { width: 0.0, height: 0.0 };
+ unsafe {
+ ct_font.get_advances_for_glyphs(kCTFontDefaultOrientation, &glyph, &mut advance, 1);
+ }
+
+ if bounds.size.width > 0.0 {
+ bounds.size.width += extra_width;
+ }
+ if advance.width > 0.0 {
+ advance.width += extra_width;
+ }
+
+ if let Some(transform) = transform {
+ bounds = bounds.apply_transform(transform);
+ }
+
+ // First round out to pixel boundaries
+ // CG Origin is bottom left
+ let mut left = bounds.origin.x.floor() as i32;
+ let mut bottom = bounds.origin.y.floor() as i32;
+ let mut right = (bounds.origin.x + bounds.size.width + x_offset).ceil() as i32;
+ let mut top = (bounds.origin.y + bounds.size.height + y_offset).ceil() as i32;
+
+ // Expand the bounds by 1 pixel, to give CG room for anti-aliasing.
+ // Note that this outset is to allow room for LCD smoothed glyphs. However, the correct outset
+ // is not currently known, as CG dilates the outlines by some percentage.
+ // This is taken from Skia.
+ left -= 1;
+ bottom -= 1;
+ right += 1;
+ top += 1;
+
+ let width = right - left;
+ let height = top - bottom;
+
+ GlyphMetrics {
+ rasterized_left: left,
+ rasterized_width: width,
+ rasterized_height: height,
+ rasterized_ascent: top,
+ rasterized_descent: -bottom,
+ advance: advance.width as f32,
+ }
+}
+
+fn new_ct_font_with_variations(ct_font_desc: &CTFontDescriptor, size: f64, variations: &[FontVariation]) -> CTFont {
+ let ct_font = core_text::font::new_from_descriptor(ct_font_desc, size);
+ if variations.is_empty() {
+ return ct_font;
+ }
+ let mut vals: Vec<(CFNumber, CFNumber)> = Vec::with_capacity(variations.len() as usize);
+ for variation in variations {
+ vals.push((CFNumber::from(variation.tag as i64), CFNumber::from(variation.value as f64)));
+ }
+ if vals.is_empty() {
+ return ct_font;
+ }
+ let vals_dict = CFDictionary::from_CFType_pairs(&vals);
+ let variation_attribute = unsafe { CFString::wrap_under_get_rule(kCTFontVariationAttribute) };
+ let attrs_dict = CFDictionary::from_CFType_pairs(&[(variation_attribute, vals_dict)]);
+ let ct_var_font_desc = ct_font.copy_descriptor().create_copy_with_attributes(attrs_dict.to_untyped()).unwrap();
+ core_text::font::new_from_descriptor(&ct_var_font_desc, size)
+
+}
+
+// We rely on Gecko to determine whether the font may have color glyphs to avoid
+// needing to load the font ahead of time to query its symbolic traits.
+fn is_bitmap_font(font: &FontInstance) -> bool {
+ font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS)
+}
+
+impl FontContext {
+ pub fn distribute_across_threads() -> bool {
+ true
+ }
+
+ pub fn new() -> FontContext {
+ debug!("Test for subpixel AA support: {:?}", *FONT_SMOOTHING_MODE);
+
+ // Force CG to use sRGB color space to gamma correct.
+ let contrast = 0.0;
+ let gamma = 0.0;
+
+ FontContext {
+ ct_font_descs: FastHashMap::default(),
+ ct_fonts: FastHashMap::default(),
+ graphics_context: GraphicsContext::new(),
+ gamma_lut: GammaLut::new(contrast, gamma, gamma),
+ }
+ }
+
+ pub fn add_raw_font(&mut self, font_key: &FontKey, bytes: Arc<Vec<u8>>, index: u32) {
+ if self.ct_font_descs.contains_key(font_key) {
+ return;
+ }
+
+ assert_eq!(index, 0);
+ let data = CFData::from_arc(bytes);
+ let ct_font_desc = match font_manager::create_font_descriptor_with_data(data) {
+ Err(_) => return,
+ Ok(cg_font) => cg_font,
+ };
+ self.ct_font_descs.insert(*font_key, ct_font_desc);
+ }
+
+ pub fn add_native_font(&mut self, font_key: &FontKey, native_font_handle: NativeFontHandle) {
+ if self.ct_font_descs.contains_key(font_key) {
+ return;
+ }
+
+ // There's no way great way to go from a CGFont to a CTFontDescriptor
+ // We could use the postscript name but that doesn't work for the
+ // system UI fonts on newer macOS versions. Instead we create a CTFont
+ // and use the descriptor for that. Normally we'd try to avoid new_from_CGFont
+ // because that adds the CGFont to the descriptor cache which can keep the CGFont
+ // around for a long time, but that should be ok for non-web (native) fonts.
+ let cf_name = CFString::new(&native_font_handle.name);
+
+ // For "hidden" system fonts, whose names start with a period,
+ // we can't instantiate CTFonts via a descriptor. We're really
+ // supposed to use CTFontCreateUIFontForLanguage, but for now
+ // we just use the CGFont.
+ let desc = if native_font_handle.name.starts_with('.') {
+ let cg_font = match CGFont::from_name(&cf_name) {
+ Ok(cg_font) => cg_font,
+ Err(_) => {
+ // If for some reason we failed to load a font descriptor, then our
+ // only options are to either abort or substitute a fallback font.
+ // It is preferable to use a fallback font instead so that rendering
+ // can at least still proceed in some fashion without erroring.
+ // Lucida Grande is the fallback font in Gecko, so use that here.
+ CGFont::from_name(&CFString::from_static_string("Lucida Grande"))
+ .expect("couldn't find font with postscript name and couldn't load fallback font")
+ }
+ };
+ core_text::font::new_from_CGFont(&cg_font, 0.).copy_descriptor()
+ } else {
+ core_text::font_descriptor::new_from_postscript_name(&cf_name)
+ };
+
+ self.ct_font_descs
+ .insert(*font_key, desc);
+ }
+
+ pub fn delete_font(&mut self, font_key: &FontKey) {
+ if let Some(_) = self.ct_font_descs.remove(font_key) {
+ self.ct_fonts.retain(|k, _| k.0 != *font_key);
+ }
+ }
+
+ pub fn delete_font_instance(&mut self, instance: &FontInstance) {
+ // Remove the CoreText font corresponding to this instance.
+ let size = FontSize::from_f64_px(instance.get_transformed_size());
+ self.ct_fonts.remove(&(instance.font_key, size, instance.variations.clone()));
+ }
+
+ fn get_ct_font(
+ &mut self,
+ font_key: FontKey,
+ size: f64,
+ variations: &[FontVariation],
+ ) -> Option<CTFont> {
+ // Interacting with CoreText can create autorelease garbage.
+ objc::rc::autoreleasepool(|| {
+ match self.ct_fonts.entry((font_key, FontSize::from_f64_px(size), variations.to_vec())) {
+ Entry::Occupied(entry) => Some((*entry.get()).clone()),
+ Entry::Vacant(entry) => {
+ let ct_font_desc = self.ct_font_descs.get(&font_key)?;
+ let ct_font = new_ct_font_with_variations(ct_font_desc, size, variations);
+ entry.insert(ct_font.clone());
+ Some(ct_font)
+ }
+ }
+ })
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ let character = ch as u16;
+ let mut glyph = 0;
+
+ self.get_ct_font(font_key, 16.0, &[])
+ .and_then(|ct_font| {
+ unsafe {
+ let result = ct_font.get_glyphs_for_characters(&character, &mut glyph, 1);
+
+ if result {
+ Some(glyph as u32)
+ } else {
+ None
+ }
+ }
+ })
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ key: &GlyphKey,
+ ) -> Option<GlyphDimensions> {
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let size = font.size.to_f64_px() * y_scale;
+ self.get_ct_font(font.font_key, size, &font.variations)
+ .and_then(|ct_font| {
+ let glyph = key.index() as CGGlyph;
+ let bitmap = is_bitmap_font(font);
+ let (mut shape, (x_offset, y_offset)) = if bitmap {
+ (FontTransform::identity(), (0.0, 0.0))
+ } else {
+ (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key))
+ };
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ }
+ let transform = if !shape.is_identity() || (tx, ty) != (0.0, 0.0) {
+ Some(CGAffineTransform {
+ a: shape.scale_x as f64,
+ b: -shape.skew_y as f64,
+ c: -shape.skew_x as f64,
+ d: shape.scale_y as f64,
+ tx: tx,
+ ty: -ty,
+ })
+ } else {
+ None
+ };
+ let (strike_scale, pixel_step) = if bitmap {
+ (y_scale, 1.0)
+ } else {
+ (x_scale, y_scale / x_scale)
+ };
+ let extra_strikes = font.get_extra_strikes(
+ FontInstanceFlags::SYNTHETIC_BOLD | FontInstanceFlags::MULTISTRIKE_BOLD,
+ strike_scale,
+ );
+ let metrics = get_glyph_metrics(
+ &ct_font,
+ transform.as_ref(),
+ glyph,
+ x_offset,
+ y_offset,
+ extra_strikes as f64 * pixel_step,
+ );
+ if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 {
+ None
+ } else {
+ Some(GlyphDimensions {
+ left: metrics.rasterized_left,
+ top: metrics.rasterized_ascent,
+ width: metrics.rasterized_width,
+ height: metrics.rasterized_height,
+ advance: metrics.advance,
+ })
+ }
+ })
+ }
+
+ // Assumes the pixels here are linear values from CG
+ fn gamma_correct_pixels(
+ &self,
+ pixels: &mut Vec<u8>,
+ render_mode: FontRenderMode,
+ color: ColorU,
+ ) {
+ // Then convert back to gamma corrected values.
+ match render_mode {
+ FontRenderMode::Alpha => {
+ self.gamma_lut.preblend_grayscale(pixels, color);
+ }
+ FontRenderMode::Subpixel => {
+ self.gamma_lut.preblend(pixels, color);
+ }
+ _ => {} // Again, give mono untouched since only the alpha matters.
+ }
+ }
+
+ #[allow(dead_code)]
+ fn print_glyph_data(&mut self, data: &[u8], width: usize, height: usize) {
+ // Rust doesn't have step_by support on stable :(
+ debug!("Width is: {:?} height: {:?}", width, height);
+ for i in 0 .. height {
+ let current_height = i * width * 4;
+
+ for pixel in data[current_height .. current_height + (width * 4)].chunks(4) {
+ let b = pixel[0];
+ let g = pixel[1];
+ let r = pixel[2];
+ let a = pixel[3];
+ debug!("({}, {}, {}, {}) ", r, g, b, a);
+ }
+ }
+ }
+
+ pub fn prepare_font(font: &mut FontInstance) {
+ if is_bitmap_font(font) {
+ // Render mode is ignored for bitmap fonts. Also, avoid normalizing the color
+ // in case CoreText needs the current color for rendering glyph color layers.
+ font.render_mode = FontRenderMode::Mono;
+ font.disable_subpixel_position();
+ return;
+ }
+ // Sanitize the render mode for font smoothing. If font smoothing is supported,
+ // then we just need to ensure the render mode is limited to what is supported.
+ // If font smoothing is actually disabled, then we need to fall back to grayscale.
+ if font.flags.contains(FontInstanceFlags::FONT_SMOOTHING) ||
+ font.render_mode == FontRenderMode::Subpixel {
+ match *FONT_SMOOTHING_MODE {
+ Some(mode) => {
+ font.render_mode = font.render_mode.limit_by(mode);
+ font.flags.insert(FontInstanceFlags::FONT_SMOOTHING);
+ }
+ None => {
+ font.render_mode = font.render_mode.limit_by(FontRenderMode::Alpha);
+ font.flags.remove(FontInstanceFlags::FONT_SMOOTHING);
+ }
+ }
+ }
+ match font.render_mode {
+ FontRenderMode::Mono => {
+ // In mono mode the color of the font is irrelevant.
+ font.color = ColorU::new(255, 255, 255, 255);
+ // Subpixel positioning is disabled in mono mode.
+ font.disable_subpixel_position();
+ }
+ 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)
+ } 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()
+ };
+ }
+ }
+ }
+
+ pub fn begin_rasterize(_font: &FontInstance) {
+ }
+
+ pub fn end_rasterize(_font: &FontInstance) {
+ }
+
+ pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult {
+ objc::rc::autoreleasepool(|| {
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let size = font.size.to_f64_px() * y_scale;
+ let ct_font =
+ self.get_ct_font(font.font_key, size, &font.variations).ok_or(GlyphRasterError::LoadFailed)?;
+ let glyph_type = if is_bitmap_font(font) {
+ GlyphType::Bitmap
+ } else {
+ GlyphType::Vector
+ };
+
+ let (mut shape, (x_offset, y_offset)) = match glyph_type {
+ GlyphType::Bitmap => (FontTransform::identity(), (0.0, 0.0)),
+ GlyphType::Vector => {
+ (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key))
+ }
+ };
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ }
+ let transform = if !shape.is_identity() || (tx, ty) != (0.0, 0.0) {
+ Some(CGAffineTransform {
+ a: shape.scale_x as f64,
+ b: -shape.skew_y as f64,
+ c: -shape.skew_x as f64,
+ d: shape.scale_y as f64,
+ tx: tx,
+ ty: -ty,
+ })
+ } else {
+ None
+ };
+
+ let glyph = key.index() as CGGlyph;
+ let (strike_scale, pixel_step) = if glyph_type == GlyphType::Bitmap {
+ (y_scale, 1.0)
+ } else {
+ (x_scale, y_scale / x_scale)
+ };
+ let extra_strikes = font.get_extra_strikes(
+ FontInstanceFlags::SYNTHETIC_BOLD | FontInstanceFlags::MULTISTRIKE_BOLD,
+ strike_scale,
+ );
+ let metrics = get_glyph_metrics(
+ &ct_font,
+ transform.as_ref(),
+ glyph,
+ x_offset,
+ y_offset,
+ extra_strikes as f64 * pixel_step,
+ );
+ if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 {
+ return Err(GlyphRasterError::LoadFailed);
+ }
+
+ let raster_size = Size2D::new(
+ metrics.rasterized_width as u32,
+ metrics.rasterized_height as u32
+ );
+
+ // If the font render mode is Alpha, we support two different ways to
+ // compute the grayscale mask, depending on the value of the platform
+ // options' font_smoothing flag:
+ // - Alpha + smoothing:
+ // We will recover a grayscale mask from a subpixel rasterization, in
+ // such a way that the result looks as close to subpixel text
+ // blending as we can make it. This involves gamma correction,
+ // luminance computations and preblending based on the text color,
+ // just like with the Subpixel render mode.
+ // - Alpha without smoothing:
+ // We will ask CoreGraphics to rasterize the text with font_smoothing
+ // off. This will cause it to use grayscale anti-aliasing with
+ // comparatively thin text. This method of text rendering is not
+ // gamma-aware.
+ //
+ // For subpixel rasterization, starting with macOS 10.11, CoreGraphics
+ // uses different glyph dilation based on the text color. Bright text
+ // uses less font dilation (looks thinner) than dark text.
+ // As a consequence, when we ask CG to rasterize with subpixel AA, we
+ // will render white-on-black text as opposed to black-on-white text if
+ // the text color brightness exceeds a certain threshold. This applies
+ // to both the Subpixel and the "Alpha + smoothing" modes, but not to
+ // the "Alpha without smoothing" and Mono modes.
+ //
+ // Fonts with color glyphs may, depending on the state within per-glyph
+ // 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),
+ 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),
+ }
+ }
+ };
+
+ {
+ let cg_context = self.graphics_context.get_context(&raster_size, glyph_type);
+
+ // These are always true in Gecko, even for non-AA fonts
+ cg_context.set_allows_font_subpixel_positioning(true);
+ cg_context.set_should_subpixel_position_fonts(true);
+
+ // Don't quantize because we're doing it already.
+ cg_context.set_allows_font_subpixel_quantization(false);
+ cg_context.set_should_subpixel_quantize_fonts(false);
+
+ cg_context.set_should_smooth_fonts(smooth);
+ cg_context.set_should_antialias(antialias);
+
+ // Fill the background. This could be opaque white, opaque black, or
+ // transparency.
+ cg_context.set_rgb_fill_color(
+ bg_color.r.into(),
+ bg_color.g.into(),
+ bg_color.b.into(),
+ bg_color.a.into(),
+ );
+ let rect = CGRect {
+ origin: CGPoint { x: 0.0, y: 0.0 },
+ size: CGSize {
+ width: metrics.rasterized_width as f64,
+ height: metrics.rasterized_height as f64,
+ },
+ };
+
+ // Make sure we use the Copy blend mode, or else we'll get the Porter-Duff OVER
+ // operator, which can't clear to the transparent color!
+ cg_context.set_blend_mode(CGBlendMode::Copy);
+ cg_context.fill_rect(rect);
+ cg_context.set_blend_mode(CGBlendMode::Normal);
+
+ // Set the text color and draw the glyphs.
+ cg_context.set_rgb_fill_color(
+ text_color.r.into(),
+ text_color.g.into(),
+ text_color.b.into(),
+ 1.0,
+ );
+ cg_context.set_text_drawing_mode(CGTextDrawingMode::CGTextFill);
+
+ // CG Origin is bottom left, WR is top left. Need -y offset
+ let mut draw_origin = CGPoint {
+ x: -metrics.rasterized_left as f64 + x_offset + tx,
+ y: metrics.rasterized_descent as f64 - y_offset - ty,
+ };
+
+ if let Some(transform) = transform {
+ cg_context.set_text_matrix(&transform);
+
+ draw_origin = draw_origin.apply_transform(&transform.invert());
+ } else {
+ // Make sure to reset this because some previous glyph rasterization might have
+ // changed it.
+ cg_context.set_text_matrix(&CG_AFFINE_TRANSFORM_IDENTITY);
+ }
+
+ ct_font.draw_glyphs(&[glyph], &[draw_origin], cg_context.clone());
+
+ // We'd like to render all the strikes in a single ct_font.draw_glyphs call,
+ // passing an array of glyph IDs and an array of origins, but unfortunately
+ // with some fonts, Core Text may inappropriately pixel-snap the rasterization,
+ // such that the strikes overprint instead of being offset. Rendering the
+ // strikes with individual draw_glyphs calls avoids this.
+ // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1633397 for details.)
+ for i in 1 ..= extra_strikes {
+ let origin = CGPoint {
+ x: draw_origin.x + i as f64 * pixel_step,
+ y: draw_origin.y,
+ };
+ ct_font.draw_glyphs(&[glyph], &[origin], cg_context.clone());
+ }
+ }
+
+ let mut rasterized_pixels = self.graphics_context
+ .get_rasterized_pixels(&raster_size, glyph_type);
+
+ if glyph_type == GlyphType::Vector {
+ // We rendered text into an opaque surface. The code below needs to
+ // ignore the current value of each pixel's alpha channel. But it's
+ // allowed to write to the alpha channel, because we're done calling
+ // CG functions now.
+
+ if smooth {
+ // Convert to linear space for subpixel AA.
+ // We explicitly do not do this for grayscale AA ("Alpha without
+ // smoothing" or Mono) because those rendering modes are not
+ // gamma-aware in CoreGraphics.
+ self.gamma_lut.coregraphics_convert_to_linear(
+ &mut rasterized_pixels,
+ );
+ }
+
+ 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];
+ }
+
+ // Set alpha to the value of the green channel. For grayscale
+ // text, all three channels have the same value anyway.
+ // For subpixel text, the mask's alpha only makes a difference
+ // when computing the destination alpha on destination pixels
+ // that are not completely opaque. Picking an alpha value
+ // that's somehow based on the mask at least ensures that text
+ // blending doesn't modify the destination alpha on pixels where
+ // the mask is entirely zero.
+ pixel[3] = pixel[1];
+ }
+
+ if smooth {
+ // Convert back from linear space into device space, and perform
+ // some "preblending" based on the text color.
+ // In Alpha + smoothing mode, this will also convert subpixel AA
+ // into grayscale AA.
+ self.gamma_correct_pixels(
+ &mut rasterized_pixels,
+ font.render_mode,
+ font.color,
+ );
+ }
+ }
+
+ Ok(RasterizedGlyph {
+ left: metrics.rasterized_left as f32,
+ top: metrics.rasterized_ascent as f32,
+ width: metrics.rasterized_width,
+ height: metrics.rasterized_height,
+ scale: match glyph_type {
+ GlyphType::Bitmap => y_scale.recip() as f32,
+ GlyphType::Vector => 1.0,
+ },
+ format: match glyph_type {
+ GlyphType::Bitmap => GlyphFormat::ColorBitmap,
+ GlyphType::Vector => font.get_glyph_format(),
+ },
+ bytes: rasterized_pixels,
+ })})
+ }
+}
+
+// Avoids taking locks by recycling Core Graphics contexts.
+#[allow(dead_code)]
+struct GraphicsContext {
+ vector_context: CGContext,
+ vector_context_size: Size2D<u32>,
+ bitmap_context: CGContext,
+ bitmap_context_size: Size2D<u32>,
+}
+
+impl GraphicsContext {
+ fn new() -> GraphicsContext {
+ let size = Size2D::new(INITIAL_CG_CONTEXT_SIDE_LENGTH, INITIAL_CG_CONTEXT_SIDE_LENGTH);
+ GraphicsContext {
+ vector_context: GraphicsContext::create_cg_context(&size, GlyphType::Vector),
+ vector_context_size: size,
+ bitmap_context: GraphicsContext::create_cg_context(&size, GlyphType::Bitmap),
+ bitmap_context_size: size,
+ }
+ }
+
+ #[allow(dead_code)]
+ fn get_context(&mut self, size: &Size2D<u32>, glyph_type: GlyphType)
+ -> &mut CGContext {
+ let (cached_context, cached_size) = match glyph_type {
+ GlyphType::Vector => {
+ (&mut self.vector_context, &mut self.vector_context_size)
+ }
+ GlyphType::Bitmap => {
+ (&mut self.bitmap_context, &mut self.bitmap_context_size)
+ }
+ };
+ let rounded_size = Size2D::new(size.width.next_power_of_two(),
+ size.height.next_power_of_two());
+ if rounded_size.width > cached_size.width || rounded_size.height > cached_size.height {
+ *cached_size = Size2D::new(u32::max(cached_size.width, rounded_size.width),
+ u32::max(cached_size.height, rounded_size.height));
+ *cached_context = GraphicsContext::create_cg_context(cached_size, glyph_type);
+ }
+ cached_context
+ }
+
+ #[allow(dead_code)]
+ fn get_rasterized_pixels(&mut self, size: &Size2D<u32>, glyph_type: GlyphType)
+ -> Vec<u8> {
+ let (cached_context, cached_size) = match glyph_type {
+ GlyphType::Vector => (&mut self.vector_context, &self.vector_context_size),
+ GlyphType::Bitmap => (&mut self.bitmap_context, &self.bitmap_context_size),
+ };
+ let cached_data = cached_context.data();
+ let cached_stride = cached_size.width as usize * 4;
+
+ let result_len = size.width as usize * size.height as usize * 4;
+ let mut result = Vec::with_capacity(result_len);
+ for y in (cached_size.height - size.height)..cached_size.height {
+ let cached_start = y as usize * cached_stride;
+ let cached_end = cached_start + size.width as usize * 4;
+ result.extend_from_slice(&cached_data[cached_start..cached_end]);
+ }
+ debug_assert_eq!(result.len(), result_len);
+ result
+ }
+
+ fn create_cg_context(size: &Size2D<u32>, glyph_type: GlyphType) -> CGContext {
+ // The result of rasterization, in all render modes, is going to be a
+ // BGRA surface with white text on transparency using premultiplied
+ // alpha. For subpixel text, the RGB values will be the mask value for
+ // the individual components. For bitmap glyphs, the RGB values will be
+ // the (premultiplied) color of the pixel. For Alpha and Mono, each
+ // pixel will have R==G==B==A at the end of this function.
+ // We access the color channels in little-endian order.
+ // The CGContext will create and own our pixel buffer.
+ // In the non-Bitmap cases, we will ask CoreGraphics to draw text onto
+ // an opaque background. In order to hit the most efficient path in CG
+ // for this, we will tell CG that the CGContext is opaque, by passing
+ // an "[...]AlphaNone[...]" context flag. This creates a slight
+ // contradiction to the way we use the buffer after CG is done with it,
+ // because we will convert it into text-on-transparency. But that's ok;
+ // we still get four bytes per pixel and CG won't mess with the alpha
+ // channel after we've stopped calling CG functions. We just need to
+ // make sure that we don't look at the alpha values of the pixels that
+ // we get from CG, and compute our own alpha value only from RGB.
+ // Note that CG requires kCGBitmapByteOrder32Little in order to do
+ // subpixel AA at all (which we need it to do in both Subpixel and
+ // Alpha+smoothing mode). But little-endian is what we want anyway, so
+ // this works out nicely.
+ let color_type = match glyph_type {
+ GlyphType::Vector => kCGImageAlphaNoneSkipFirst,
+ GlyphType::Bitmap => kCGImageAlphaPremultipliedFirst,
+ };
+
+ CGContext::create_bitmap_context(None,
+ size.width as usize,
+ size.height as usize,
+ 8,
+ size.width as usize * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGBitmapByteOrder32Little | color_type)
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+enum GlyphType {
+ Vector,
+ Bitmap,
+}
+