From 06dc2f5fe07231eaa736a12bb5959e6862a44771 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Thu, 28 May 2020 23:38:19 -0400 Subject: [PATCH] Implement the `leading` attribute as defined by fonts. `EditText` supports two different forms of leading: 1. Font-provided leading, specified relative to the EM square and scaled with font size 2. User-specificed leading, specified in pixels Notably, the former appears to apply to the first line in the text and pushes it down. This showed up in the `edittext_font_size` test, and according to that test result the leading is rounded *up* to the nearest pixel, plus one. That last bit seems possibly wrong and is subject to further change, but it matches the tests at multiple scales. --- core/src/font.rs | 40 ++++++++++++++-- core/src/html/layout.rs | 16 +++++-- core/tests/regression_tests.rs | 1 + .../swfs/avm1/edittext_font_size/output.txt | 45 ++++++++++++++++++ .../swfs/avm1/edittext_font_size/test.fla | Bin 0 -> 53760 bytes .../swfs/avm1/edittext_font_size/test.swf | Bin 0 -> 12162 bytes 6 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 core/tests/swfs/avm1/edittext_font_size/output.txt create mode 100644 core/tests/swfs/avm1/edittext_font_size/test.fla create mode 100644 core/tests/swfs/avm1/edittext_font_size/test.swf diff --git a/core/src/font.rs b/core/src/font.rs index d6179210c..6264a8b26 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -3,11 +3,16 @@ use crate::prelude::*; use crate::transform::Transform; use gc_arena::{Collect, Gc, MutationContext}; -/// Certain Flash routines measure text up to the nearest whole pixel. -fn round_to_pixel(t: Twips) -> Twips { +/// Certain Flash routines measure text by rounding down to the nearest whole pixel. +fn round_down_to_pixel(t: Twips) -> Twips { Twips::from_pixels(t.to_pixels().floor()) } +/// Certain Flash routines measure text by rounding up to the nearest whole pixel. +pub fn round_up_to_pixel(t: Twips) -> Twips { + Twips::from_pixels(t.to_pixels().ceil()) +} + type Error = Box; #[derive(Debug, Clone, Collect, Copy)] @@ -33,6 +38,18 @@ struct FontData { /// Maps from a pair of unicode code points to horizontal offset value. kerning_pairs: fnv::FnvHashMap<(u16, u16), Twips>, + /// The distance from the top of each glyph to the baseline of the font, in + /// EM-square coordinates. + ascent: u16, + + /// The distance from the baseline of the font to the bottom of each glyph, + /// in EM-square coordinates. + descent: u16, + + /// The distance between the bottom of any one glyph and the top of + /// another, in EM-square coordinates. + leading: i16, + /// The identity of the font. descriptor: FontDescriptor, } @@ -65,6 +82,11 @@ impl<'gc> Font<'gc> { }; let descriptor = FontDescriptor::from_swf_tag(tag); + let (ascent, descent, leading) = if let Some(layout) = &tag.layout { + (layout.ascent, layout.descent, layout.leading) + } else { + (0, 0, 0) + }; Ok(Font(Gc::allocate( gc_context, @@ -76,6 +98,9 @@ impl<'gc> Font<'gc> { /// (SWF19 p.164) scale: if tag.version >= 3 { 20480.0 } else { 1024.0 }, kerning_pairs, + ascent, + descent, + leading, descriptor, }, ))) @@ -119,6 +144,13 @@ impl<'gc> Font<'gc> { .unwrap_or_default() } + /// Return the leading for this font at a given height. + pub fn get_leading_for_height(self, height: Twips) -> Twips { + let scale = height.get() as f32 / self.scale(); + + Twips::new((self.0.leading as f32 * scale) as i32) + } + /// Returns whether this font contains kerning information. pub fn has_kerning_info(self) -> bool { !self.0.kerning_pairs.is_empty() @@ -179,8 +211,8 @@ impl<'gc> Font<'gc> { |transform, _glyph, advance| { let tx = transform.matrix.tx; let ty = transform.matrix.ty; - size.0 = std::cmp::max(size.0, round_to_pixel(tx + advance)); - size.1 = std::cmp::max(size.1, round_to_pixel(ty)); + size.0 = std::cmp::max(size.0, round_down_to_pixel(tx + advance)); + size.1 = std::cmp::max(size.1, round_down_to_pixel(ty)); }, ); diff --git a/core/src/html/layout.rs b/core/src/html/layout.rs index beee838bc..9b1a5b3dd 100644 --- a/core/src/html/layout.rs +++ b/core/src/html/layout.rs @@ -1,7 +1,7 @@ //! Layout box structure use crate::context::UpdateContext; -use crate::font::Font; +use crate::font::{round_up_to_pixel, Font}; use crate::html::dimensions::{BoxBounds, Position, Size}; use crate::html::text_format::{FormatSpans, TextFormat, TextSpan}; use crate::tag_utils::SwfMovie; @@ -117,6 +117,14 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> { Twips::from_pixels(0.0), ); + // Flash appears to round up the font's leading to the nearest pixel + // and adds one. I'm not sure why. + let font_leading_adjustment = round_up_to_pixel( + self.font + .map(|f| f.get_leading_for_height(self.max_font_size)) + .unwrap_or_else(|| Twips::new(0)), + ) + Twips::from_pixels(1.0); + line = self.current_line; while let Some(linebox) = line { let mut write = linebox.write(mc); @@ -126,8 +134,10 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> { // which is information we don't have yet. let font_size_adjustment = self.max_font_size - write.bounds.height(); - write.bounds += - Position::from((left_adjustment + align_adjustment, font_size_adjustment)); + write.bounds += Position::from(( + left_adjustment + align_adjustment, + font_size_adjustment + font_leading_adjustment, + )); line = write.next_sibling(); } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 35d075b6b..f186a220b 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -197,6 +197,7 @@ swf_tests! { (as1_constructor_v6, "avm1/as1_constructor_v6", 1), (as1_constructor_v7, "avm1/as1_constructor_v7", 1), (issue_710, "avm1/issue_710", 1), + (edittext_font_size, "avm1/edittext_font_size", 1), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/edittext_font_size/output.txt b/core/tests/swfs/avm1/edittext_font_size/output.txt new file mode 100644 index 000000000..9fe0891e4 --- /dev/null +++ b/core/tests/swfs/avm1/edittext_font_size/output.txt @@ -0,0 +1,45 @@ +//Creating left aligned text @ 10px +W: 7 +H: 15 +//Creating left aligned text @ 12px +W: 9 +H: 18 +//Creating left aligned text @ 20px +W: 15 +H: 29 +//Creating left aligned text @ 30px +W: 23 +H: 42 +//Creating left aligned text @ 100px +W: 78 +H: 138 +//Creating left aligned text 'Qe' @ 10px +W: 13 +H: 15 +//Creating left aligned text 'Qe' @ 12px +W: 16 +H: 18 +//Creating left aligned text 'Qe' @ 20px +W: 26 +H: 29 +//Creating left aligned text 'Qe' @ 30px +W: 40 +H: 42 +//Creating left aligned text 'Qe' @ 100px +W: 134 +H: 138 +//Creating left aligned text 'Q e' @ 10px +W: 15 +H: 15 +//Creating left aligned text 'Q e' @ 12px +W: 19 +H: 18 +//Creating left aligned text 'Q e' @ 20px +W: 32 +H: 29 +//Creating left aligned text 'Q e' @ 30px +W: 48 +H: 42 +//Creating left aligned text 'Q e' @ 100px +W: 160 +H: 138 diff --git a/core/tests/swfs/avm1/edittext_font_size/test.fla b/core/tests/swfs/avm1/edittext_font_size/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..2b8a90e7e5654a6cc53d893f42fba2f90779a83c GIT binary patch literal 53760 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