Swash is a pure Rust, cross-platform crate that provides font introspection, complex text shaping and glyph rendering.

Overview

swash

Swash is a pure Rust, cross-platform crate that provides font introspection, complex text shaping and glyph rendering.

Crates.io Docs.rs MIT licensed Apache licensed

Goals

This crate aims to lay the foundation for a cross-platform, high performance set of components for beautiful typography. In particular, this library focuses on fonts and operations that are directly applicable to them. For the most part, the desire is to be unopinionated with respect to resource management, higher level layout and lower level rendering.

Non goals

Due to the intention of being generally useful and easy to integrate, the following areas of related interest are specifically avoided:

  • Text layout. This is highly application specific and the requirements for both features and performance differ greatly among web browsers, word processors, text editors, game engines, etc. There is a sibling crate in development that does provide general purpose text layout based on this library.

  • Composition. Like layout, this is also application specific in addition to being hardware dependent. Glyph caching, geometry batching and rendering all belong here and should integrate well with the application and the hardware environment.

General features

  • Simple borrowed font representation that imposes no requirements on resource management leading to...
  • Thread friendly architecture. Acceleration structures are completely separate from font data and can be retained per thread, thrown away and rematerialized at any time
  • Zero transient heap allocations. All scratch buffers and caches are maintained by contexts. Resources belonging to evicted cache entries are immediately reused

Introspection

  • Enumerating font collections (ttc/otc)
  • Localized strings including names and other metadata
  • Font variation axes and named instances
  • Comprehensive font and per-glyph metrics (with synthesized vertical metrics if not provided)
  • Primary attributes (stretch, weight, style) with support for synthesis suggestions based on available variations and scaler transforms (faux bold and oblique)
  • Color palettes
  • Embedded color and alpha bitmap strikes
  • Character to nominal glyph identifier mapping with support for enumerating all pairs
  • Writing systems: provides a list of all supported script/language pairs and their associated typographic features
  • All introspection is zero allocation and zero copy

Complex text shaping

  • Full support for OpenType advanced typography (GSUB/GPOS)
  • Partial support for Apple advanced typography: glyph metamorphosis (morx) is fully supported while the current extended kerning support (kerx) covers most common cases (kerning and mark positioning)
  • Full support for variable fonts including positioning and feature substitutions
  • Implementation of the Universal Shaping Engine for complex scripts such as Devanagari, Malayalam, etc.
  • Arabic joining including Urdu style climbing runs
  • Basic shaping support: ligatures, marks, kerning, etc.
  • Enable/disable individual features with argument support for activating alternates such as swashes...
  • Pre-shaping cluster parsing with an iterative mapping technique (including normalization) allowing for sophisticated font fallback mechanisms without the expense of heuristically shaping runs
  • Shaper output is structured by cluster including original source ranges and provides simple identification of ligatures and complex multi-glyph clusters
  • Pass-through per character user data (a single u32) for accurate association of style properties with glyphs
  • Pass-through per cluster information for retaining text analysis results such as word and line boundaries, whitespace identification and emoji presentation modes

Scaling

  • Scalable outlines with full variation support (TrueType and Postscript)
  • Asymmetric vertical hinting (TrueType and Postscript)
  • Horizontal subpixel rendering and fractional positioning
  • Full emoji support for Apple (sbix), Google (CBLC/CBDT) and Microsoft (COLR/CPAL) formats
  • Path effects (stroking and dashing)
  • Transforms including synthetic emboldening and affine transformations
  • Customizable glyph source prioritization (best fit color bitmap -> exact size alpha bitmap -> outline)

Text analysis

  • Unicode character properties related to layout and shaping
  • Character composition and decomposition (canonical and compatible)
  • Complex, script aware cluster segmentation
  • Single pass, iterator based analysis determines word and line boundaries and detects whether bidi resolution is necessary

Performance

Performance is a primary goal for this crate and preliminary microbenchmarks show a general improvement over FreeType and Harfbuzz by about 10-20% on average and some cases show substantial wins, particularly when scaling Postscript outlines or shaping text with complex features. Specifically, shaping runs with fonts like Calibri and Noto Sans Myanmar is almost twice as fast. Simple fonts and pure ASCII runs tend to show the smallest gains as those simply measure shaper initialization and glyph iteration. A comprehensive set of benchmarks (and test cases!) are needed here to gain more insight and track regressions.

Contributing

Contributions are welcome and appreciated! All contributions must be granted under the licenses under which this project is released.

Comments
  • Tests

    Tests

    cc @dfrg

    I'm having some trouble with a few tests, so could you possibly take a look at what I need to do? I'll also take a look myself tomorrow. The results should be in the CI. I would guess around 200-300 of the fails are because of this: [uni1A45|[email protected],0|[email protected],0|[email protected],0] versus [uni1A45|[email protected],0|[email protected],-734] but I'm not sure how to do this.

    (The code is very terrible right now, it'll be much better by merging time).

    Generator:

    ```rs use std::{ fmt::{self, Display}, fs::File, io::{BufWriter, Write}, mem, path::PathBuf, };

    #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ParseError;

    impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "missing closing quote") } }

    impl std::error::Error for ParseError {}

    enum State { /// Within a delimiter. Delimiter, /// After backslash, but before starting word. Backslash, /// Within an unquoted word. Unquoted, /// After backslash in an unquoted word. UnquotedBackslash, /// Within a single quoted word. SingleQuoted, /// Within a double quoted word. DoubleQuoted, /// After backslash inside a double quoted word. DoubleQuotedBackslash, /// Inside a comment. Comment, InPath, }

    pub fn split(s: &str) -> Result<Vec, ParseError> { use State::*;

    let mut words = Vec::new();
    let mut word = String::new();
    let mut chars = s.chars();
    let mut state = Delimiter;
    
    loop {
        let c = chars.next();
        state = match state {
            Delimiter => match c {
                None => break,
                Some('\'') => SingleQuoted,
                Some(c @ '[') => {
                    word.push(c);
                    SingleQuoted
                }
                Some('\"') => DoubleQuoted,
                Some('\\') => Backslash,
                Some('\t') | Some(' ') | Some('\n') | Some(':') | Some(';') => Delimiter,
                Some('#') => Comment,
                Some(c) => {
                    word.push(c);
                    Unquoted
                }
            },
            Backslash => match c {
                None => {
                    word.push('\\');
                    words.push(mem::replace(&mut word, String::new()));
                    break;
                }
                Some('\n') => Delimiter,
                Some(c) => {
                    word.push(c);
                    Unquoted
                }
            },
            Unquoted => match c {
                None => {
                    words.push(mem::replace(&mut word, String::new()));
                    break;
                }
                Some('\'') => SingleQuoted,
                Some(c @ '[') => {
                    word.push(c);
                    SingleQuoted
                }
                Some('\"') => DoubleQuoted,
                Some('\\') => UnquotedBackslash,
                Some('\t') | Some(' ') | Some('\n') | Some(':') | Some(';') => {
                    words.push(mem::replace(&mut word, String::new()));
                    Delimiter
                }
                Some(c @ '/') => {
                    word.push(c);
                    InPath
                }
                Some(c) => {
                    word.push(c);
                    Unquoted
                }
            },
            UnquotedBackslash => match c {
                None => {
                    word.push('\\');
                    words.push(mem::replace(&mut word, String::new()));
                    break;
                }
                Some('\n') => Unquoted,
                Some(c) => {
                    word.push(c);
                    Unquoted
                }
            },
            SingleQuoted => match c {
                None => return Err(ParseError),
                Some(c @ ']') => {
                    word.push(c);
                    Unquoted
                }
                Some('\'') => Unquoted,
                Some(c) => {
                    word.push(c);
                    SingleQuoted
                }
            },
            DoubleQuoted => match c {
                None => return Err(ParseError),
                Some('\"') => Unquoted,
                Some('\\') => DoubleQuotedBackslash,
                Some(c) => {
                    word.push(c);
                    DoubleQuoted
                }
            },
            DoubleQuotedBackslash => match c {
                None => return Err(ParseError),
                Some('\n') => DoubleQuoted,
                Some(c @ '$') | Some(c @ '`') | Some(c @ '"') | Some(c @ '\\') => {
                    word.push(c);
                    DoubleQuoted
                }
                Some(c) => {
                    word.push('\\');
                    word.push(c);
                    DoubleQuoted
                }
            },
            Comment => match c {
                None => break,
                Some('\n') => Delimiter,
                Some(_) => Comment,
            },
            InPath => match c {
                None => break,
                Some(';') => {
                    words.push(mem::replace(&mut word, String::new()));
                    Delimiter
                }
                Some(c) => {
                    word.push(c);
                    InPath
                }
            },
        }
    }
    
    Ok(words)
    

    }

    #[derive(Default, Debug)] struct TestCase { name: String, path: PathBuf, font_size: usize, features: Vec<(String, u16)>, variations: Vec<(String, f32)>, input: Vec, output: Vec, }

    impl Display for TestCase { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.output.contains(&"*".to_string()) { write!( f, "create_test!({}, {:?}, {}, &{:?}, &{:?}, &{:?});", self.name, self.path.to_str().unwrap(), self.font_size, self.features, self.variations, self.input, ) } else { write!( f, "create_test!({}, {:?}, {}, &{:?}, &{:?}, &{:?}, &{:?});", self.name, self.path.to_str().unwrap(), self.font_size, self.features, self.variations, self.input, self.output, ) } } }

    impl TestCase { pub fn parse(arguments: Vec) -> Self { let mut case = Self::default(); case.font_size = 75;

        for arg in arguments.iter() {
            if arg.starts_with("../") {
                // Path
                case.path = arg.into();
            } else if arg.starts_with("--") {
                // Features/Variations
                let arg = arg.strip_prefix("--").unwrap();
                if arg.contains("=") {
                    // Variation
                    let split: Vec<_> = arg.split("=").collect();
                    let variation: (String, f32) = (
                        split.first().unwrap().to_string(),
                        split.last().unwrap().parse().unwrap_or(0.0),
                    );
                    case.variations.push(variation);
                } else {
                    // Feature
                    case.features.push((arg.to_string(), 1));
                }
            } else if arg.starts_with("[") || arg == "*" {
                // Output
                let output = arg.trim_matches(|chr| chr == '[' || chr == ']');
                for chr in output.split('|') {
                    // if chr.contains('=') {
                        // Failed attempt at parsing runs/positions.
                        // let mut split = chr.split('=');
                        // let unicode = split.next().unwrap().to_string();
                        // let mut split_for_offsets = split.next().unwrap().split('+');
                        // let x = split_for_offsets.next().unwrap().parse().expect(chr);
                        // let y = split_for_offsets.next().unwrap().parse().unwrap();
                        // case.output.push((unicode, x, y));
                    // } else {
                        case.output.push(chr.to_string());
                    // }
                };
            }
        }
    
        let input = arguments
            .get(
                arguments
                    .len()
                    .checked_sub(2)
                    .unwrap_or_else(|| panic!("{:#?}", arguments)),
            )
            .unwrap();
        for chr in input.split(',') {
            let chr = chr.trim_start_matches("U+");
            let parsed_chr: char =
                char::from_u32(u32::from_str_radix(chr, 16).expect(chr)).expect(chr);
            case.input.push(parsed_chr);
        }
    
        case
    }
    

    }

    fn main() -> std::io::Result<()> { let input = PathBuf::from("data"); let output = PathBuf::from("output"); let rendering_tests = input.join("text-rendering-tests"); let aot_tests = input.join("aots"); let inhouse_tests = input.join("in-house"); std::fs::create_dir_all(output.clone())?;

    if inhouse_tests.exists() {
        let directory = std::fs::read_dir(inhouse_tests.join("tests"))?
            .flatten()
            .filter(|file| file.file_type().unwrap().is_file());
        let mut file = BufWriter::new(File::create(output.join("text-rendering-tests.txt"))?);
    
        // Iterate over each file in the directory and generate tests.
        for test in directory {
            let content = std::fs::read_to_string(test.path())?;
            for line in content.lines() {
                // file.write(format!("{:#?}\n", split(line).unwrap()).as_bytes())?;
                let val = split(line).unwrap();
                if val.is_empty() {
                    continue;
                }
                let formatted_string = format!("{}\n", TestCase::parse(val));
                file.write(formatted_string.as_bytes())?;
            }
        }
    } else {
        panic!()
    }
    
    Ok(())
    

    }

    </p>
    </details>
    opened by kirawi 36
  • ๐Ÿค–๐Ÿชต Fix emoji placement by calculating full bounds

    ๐Ÿค–๐Ÿชต Fix emoji placement by calculating full bounds

    Addresses incorrect emoji rendering as reported in #26

    Previously the placement of the very first colored outline was used to calculate image dimensions, however this is incorrect. One should use the full bounds of all outlines for calculating this size.

    Notice that this also needs a tiny PR on zeno https://github.com/dfrg/zeno/pull/4

    Attached are correctly rendered ๐Ÿค– and ๐Ÿชต emoji's as per the issue file.

    stuff stuff

    opened by Jasper-Bekkers 8
  • ๐Ÿค– and ๐Ÿชต are rendered incorrectly it would seem

    ๐Ÿค– and ๐Ÿชต are rendered incorrectly it would seem

    Repro case

    It looks like both of these render partially (I haven't tried many more, however ๐Ÿ”ฅ from the documentation seems to render correctly). The robot-man seems to have his mouth cut off, and the log seems to have the top cut off.

    Since this is my very first time diving into this library, I may well be doing something wrong. I've attached the repro of what I'm trying to do.

    I've also attached the rendered image below;

    use swash::scale::ScaleContext;
    use swash::scale::StrikeWith;
    use swash::scale::*;
    use zeno::Vector;
    
    fn main() {
        use swash::FontRef;
    
        let font_path = "C:/Windows/Fonts/seguiemj.ttf";
    
        let font_data = std::fs::read(font_path).ok().unwrap();
        let font = FontRef::from_index(&font_data, 0).unwrap();
    
        let mut context = ScaleContext::new();
        let mut scaler = context.builder(font).hint(true).build();
        let glyph_id = font.charmap().map('๐Ÿค–');
        let image = Render::new(&[
            Source::ColorBitmap(StrikeWith::ExactSize),
            Source::ColorOutline(1),
            Source::Outline,
        ])
        .format(zeno::Format::Subpixel)
        .offset(Vector::new(0.0, 0.0))
        .render(&mut scaler, glyph_id)
        .unwrap();
    
        dbg!(image.placement);
    
        let img =
            ::image::RgbaImage::from_raw(image.placement.width, image.placement.height, image.data)
                .unwrap();
        img.save_with_format("stuff.png", ::image::ImageFormat::Png);
    }
    

    stuff

    opened by Jasper-Bekkers 7
  • โžก๏ธ Need to take offset into account when calculating draw area

    โžก๏ธ Need to take offset into account when calculating draw area

    There was a small artifact noticeable when offset wasn't taken into account when calculating the bounds of the rectangle, leading to 1px clipping artifacts.

    This would break in cosmic-text where the offset would be used to offset the glyph with a tiny fractional amount, causing incorrect rounding in this routine as a result.

    https://github.com/pop-os/cosmic-text/blob/2342bf0eaebe6d41661b1fb49ed30d4021a9807d/src/swash.rs#L33-L35

    Current main branch;

    image

    With this fix applied;

    image

    Notice some instances of the ๐Ÿ’ฉ , ๐Ÿญand more subtly the top of the ๐Ÿค– emoji.

    opened by Jasper-Bekkers 2
  • Tests

    Tests

    Resolves #6

    All tests are present.

    Note: ~All AOTs, and all but one in-house tests fail. AOTs are failing because the output format is different than expected. This might be resolvable by simply cutting the gid prefixes from the output and reference output entirely.~ (done)

    ~Previously disabled tests are also back, and so will need to be disabled again. This should probably be done through a master list of disabled tests in the generator.~ (done)

    Correctly parsing variations and settings is also a possible TODO task, depending on whether they're necessary for the tests.

    • [ ] Get all tests passing.
    • [ ] Rebase and re-commit tests as --author="nobody <>".
    Generator Tool
    use std::{
      fmt::{self, Display},
      fs::File,
      io::{BufWriter, Write},
      mem,
      path::PathBuf,
    };
    
    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    pub struct ParseError;
    
    impl fmt::Display for ParseError {
      fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
          write!(f, "missing closing quote")
      }
    }
    
    impl std::error::Error for ParseError {}
    
    enum State {
      /// Within a delimiter.
      Delimiter,
      /// After backslash, but before starting word.
      Backslash,
      /// Within an unquoted word.
      Unquoted,
      /// After backslash in an unquoted word.
      UnquotedBackslash,
      /// Within a single quoted word.
      SingleQuoted,
      /// Within a double quoted word.
      DoubleQuoted,
      /// After backslash inside a double quoted word.
      DoubleQuotedBackslash,
      /// Inside a comment.
      Comment,
      InPath,
    }
    
    pub fn split(s: &str) -> Result<Vec<String>, ParseError> {
      use State::*;
    
      let mut words = Vec::new();
      let mut word = String::new();
      let mut chars = s.chars();
      let mut state = Delimiter;
    
      loop {
          let c = chars.next();
          state = match state {
              Delimiter => match c {
                  None => break,
                  Some('\'') => SingleQuoted,
                  Some(c @ '[') => {
                      word.push(c);
                      SingleQuoted
                  }
                  Some('\"') => DoubleQuoted,
                  Some('\\') => Backslash,
                  Some('\t') | Some(' ') | Some('\n') | Some(':') | Some(';') => Delimiter,
                  Some('#') => Comment,
                  Some(c) => {
                      word.push(c);
                      Unquoted
                  }
              },
              Backslash => match c {
                  None => {
                      word.push('\\');
                      words.push(mem::replace(&mut word, String::new()));
                      break;
                  }
                  Some('\n') => Delimiter,
                  Some(c) => {
                      word.push(c);
                      Unquoted
                  }
              },
              Unquoted => match c {
                  None => {
                      words.push(mem::replace(&mut word, String::new()));
                      break;
                  }
                  Some('\'') => SingleQuoted,
                  Some(c @ '[') => {
                      word.push(c);
                      SingleQuoted
                  }
                  Some('\"') => DoubleQuoted,
                  Some('\\') => UnquotedBackslash,
                  Some('\t') | Some(' ') | Some('\n') | Some(':') | Some(';') => {
                      words.push(mem::replace(&mut word, String::new()));
                      Delimiter
                  }
                  Some(c @ '/') => {
                      word.push(c);
                      InPath
                  }
                  Some(c) => {
                      word.push(c);
                      Unquoted
                  }
              },
              UnquotedBackslash => match c {
                  None => {
                      word.push('\\');
                      words.push(mem::replace(&mut word, String::new()));
                      break;
                  }
                  Some('\n') => Unquoted,
                  Some(c) => {
                      word.push(c);
                      Unquoted
                  }
              },
              SingleQuoted => match c {
                  None => return Err(ParseError),
                  Some(c @ ']') => {
                      word.push(c);
                      Unquoted
                  }
                  Some('\'') => Unquoted,
                  Some(c) => {
                      word.push(c);
                      SingleQuoted
                  }
              },
              DoubleQuoted => match c {
                  None => return Err(ParseError),
                  Some('\"') => Unquoted,
                  Some('\\') => DoubleQuotedBackslash,
                  Some(c) => {
                      word.push(c);
                      DoubleQuoted
                  }
              },
              DoubleQuotedBackslash => match c {
                  None => return Err(ParseError),
                  Some('\n') => DoubleQuoted,
                  Some(c @ '$') | Some(c @ '`') | Some(c @ '"') | Some(c @ '\\') => {
                      word.push(c);
                      DoubleQuoted
                  }
                  Some(c) => {
                      word.push('\\');
                      word.push(c);
                      DoubleQuoted
                  }
              },
              Comment => match c {
                  None => break,
                  Some('\n') => Delimiter,
                  Some(_) => Comment,
              },
              InPath => match c {
                  None => break,
                  Some(';') => {
                      words.push(mem::replace(&mut word, String::new()));
                      Delimiter
                  }
                  Some(c) => {
                      word.push(c);
                      InPath
                  }
              },
          }
      }
    
      Ok(words)
    }
    
    #[derive(Default, Debug)]
    struct TestCase {
      name: String,
      path: PathBuf,
      font_size: usize,
      features: Vec<(String, u16)>,
      variations: Vec<(String, f32)>,
      input: String,
      output: Vec<String>,
      show_advance: bool,
      show_name: bool,
    }
    
    impl Display for TestCase {
      fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
          if self.path.starts_with("/") {
              write!(f, "#[cfg(target_os = \"macos\")]\n")?;
          }
    
          if self.output.contains(&"*".to_string()) {
              write!(
                  f,
                  "shaping_test!({}, {:?}, {}, &{:?}, &{:?}, {:?});",
                  self.name,
                  self.path.to_str().unwrap(),
                  self.font_size,
                  self.features,
                  self.variations,
                  self.input,
              )
          } else {
              write!(
                  f,
                  "shaping_test!({}, {:?}, {}, &{:?}, &{:?}, {:?}, &{:?}, {:?}, {:?});",
                  self.name,
                  self.path.to_str().unwrap(),
                  self.font_size,
                  self.features,
                  self.variations,
                  self.input,
                  self.output,
                  self.show_advance,
                  self.show_name,
              )
          }
      }
    }
    
    impl TestCase {
      pub fn parse(
          arguments: Vec<String>,
          file_path: PathBuf,
          base_path: PathBuf,
          idx: usize,
      ) -> Self {
          let name = format!(
              "{}_{}",
              file_path
                  .file_stem()
                  .unwrap()
                  .to_str()
                  .unwrap()
                  .replace("-", "_")
                  .replace(" ", "_")
                  .to_lowercase(),
              idx
          );
    
          let mut case = Self {
              name,
              show_advance: true,
              show_name: true,
              ..Self::default()
          };
          case.font_size = 75;
    
          for arg in arguments.iter() {
              if arg.starts_with("../") {
                  // Path
                  let arg = arg.strip_prefix("../fonts/").unwrap();
                  case.path = base_path.join(arg);
              } else if arg.starts_with("/") {
                  case.path = arg.into();
              } else if arg.starts_with("--") {
                  // Features/Variations
                  let arg = arg.strip_prefix("--").unwrap();
                  if arg.starts_with("font-size=") {
                      let split: Vec<_> = arg.split("=").collect();
                      case.font_size = split.last().unwrap().parse().unwrap();
                  } else if arg.contains("=") {
                      // Variation
                      let split: Vec<_> = arg.split("=").collect();
                      let variation: (String, f32) = (
                          split.first().unwrap().to_string(),
                          split.last().unwrap().parse().unwrap_or(0.0),
                      );
                      case.variations.push(variation);
                  } else {
                      // Feature
                      match arg {
                          "no-positions" => case.show_advance = false,
                          "no-glyph-names" => case.show_name = false,
                          _ => case.features.push((arg.to_string(), 1)),
                      }
                  }
              } else if arg.starts_with("[") || arg == "*" {
                  // Output
                  let output = arg.trim_matches(|chr| chr == '[' || chr == ']');
                  for chr in output.split('|') {
                      case.output.push(chr.trim_start_matches("gid").to_string());
                  }
              }
          }
    
          let input = arguments
              .get(
                  arguments
                      .len()
                      .checked_sub(2)
                      .unwrap_or_else(|| panic!("{:#?}", arguments)),
              )
              .unwrap();
          for chr in input.split(',') {
              let chr = chr.trim_start_matches("U+");
              let parsed_chr: char =
                  char::from_u32(u32::from_str_radix(chr, 16).expect(chr)).expect(chr);
              case.input.push(parsed_chr);
          }
    
          case
      }
    }
    
    fn main() -> std::io::Result<()> {
      let input = PathBuf::from("data");
      let output = PathBuf::from("output");
      std::fs::create_dir_all(output.clone())?;
    
      let shaping_tests = ["text-rendering-tests", "aots", "in-house"];
      let output_shaping_tests = ["text-rendering", "aot", "in-house"];
      let ignored_tests = vec![vec!["shlana", "cmap_3"], vec![], vec![]];
      for ((test, output_test), ignored_tests) in shaping_tests
          .iter()
          .zip(output_shaping_tests)
          .zip(ignored_tests)
      {
          let dir = input.join(test).join("tests");
          if dir.exists() {
              let entries = std::fs::read_dir(dir)?
                  .flatten()
                  .filter(|file| file.file_type().unwrap().is_file());
              let mut writer =
                  BufWriter::new(File::create(output.join(format!("{}.rs", output_test)))?);
              writer.write("mod shaping;\n".as_bytes())?;
    
              // Iterate over each file in the directory and generate tests.
              for file in entries {
                  if ignored_tests.iter().any(|x| {
                      file.file_name()
                          .to_string_lossy()
                          .to_lowercase()
                          .replace("-", "_")
                          .contains(x)
                  }) {
                      continue;
                  }
                  let content = std::fs::read_to_string(file.path())?;
                  for (idx, test) in content
                      .lines()
                      .map(|line| split(line).unwrap())
                      .filter(|line| !line.is_empty())
                      .enumerate()
                  {
                      let formatted_string = format!(
                          "{}\n",
                          TestCase::parse(
                              test,
                              file.path(),
                              PathBuf::from("tests").join("fonts").join(output_test),
                              idx + 1
                          )
                      );
                      writer.write(formatted_string.as_bytes())?;
                  }
              }
          }
      }
    
      Ok(())
    }
    

    opened by kirawi 2
  • Subpixel Format Rendering?

    Subpixel Format Rendering?

    The subpixel format is described as "32-bit RGBA subpixel mask with 1/3 pixel offsets for the red and blue channels."

    Could you elaborate on what 1/3 pixel offset means in this case? I'm not quite sure how to actually apply the subpixel mask to render text in a given color from looking at the docs (though I may have missed something obvious).

    opened by wooosh 2
  • How is it zero transient heap allocation?

    How is it zero transient heap allocation?

    This is largely because I am unfamiliar with shaping, but I'm also not that skilled in programming yet either. So, I was wondering how you achieved zero transient heap allocations. At least to me, it seems like it would cause a stack overflow if you had to shape a large amount of text. You mention a cache, but I'm still pretty confused. How does the cache work without transiently allocating to the heap?

    opened by kirawi 2
  • Add CI

    Add CI

    If fmt and clippy are wanted in the future:

    fmt:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/[email protected]
        - uses: actions-rs/[email protected]
          with:
            toolchain: stable
            components: rustfmt
            override: true
          
        - run: cargo fmt -- --check
    
    clippy:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/[email protected]
        - uses: dtolnay/[email protected]
        - run: cargo clippy
    
    opened by kirawi 2
  • Cannot render `U+1FB87 : RIGHT ONE QUARTER BLOCK`

    Cannot render `U+1FB87 : RIGHT ONE QUARTER BLOCK`

    The JetBrains Mono font does have that glyph present (which I inspected using the BirdFont font inspector), I suspect that some glyph resolution code is might be wrong somewhere, but I don't know which (trust me I tried).

    I built the swash_demo from source using the following code for build_document

    fn build_document() -> doc::Document {
        use layout::*;
        let mut db = doc::Document::builder();
    
        use SpanStyle as S;
    
        let underline = &[
            S::Underline(true),
            S::UnderlineOffset(Some(-1.)),
            S::UnderlineSize(Some(1.)),
        ];
    
        db.enter_span(&[S::family_list("JetBrains Mono"), S::Size(22.)]);
        db.add_text("๐Ÿฎ‡");
        db.leave_span();
        db.build()
    }
    

    Would be grateful if you could provide some pointers on where should I be looking for what, thanks!

    opened by ad-chaos 1
  • Fix Nko tag.

    Fix Nko tag.

    according to https://docs.microsoft.com/en-us/typography/opentype/spec/scripttags "nko " is the correct script tag not "nkoo". with this change I can run the fount itemize example on linux

    opened by dvc94ch 1
  • Extract text analysis code into separate crate

    Extract text analysis code into separate crate

    The text analysis code including segmentation, cluster parsing and Unicode properties is not font dependent and should be moved to a new crate. The cluster model also needs to be updated to accommodate additional Indic scripts (specifically those with multiple pre-base forms).

    opened by dfrg 1
  • i is not rendered properly when using JetBrains Mono

    i is not rendered properly when using JetBrains Mono

    Hi!

    I tried learning how swash works and I instantly jumped to the swash_demo crate. There changed the font from "times" to "JetBrains Mono" and it indeed worked (even ligatures), but for some weird reason the i was rendered incorrectly...

    Example: Screenshot 2022-11-13 at 19 05 18

    Zoomed: Screenshot 2022-11-13 at 19 05 46

    opened by rice7th 0
  • Some fonts are rasterised very small

    Some fonts are rasterised very small

    I'm trying to use swash for font rasterisation in Lapce. The subpixel works great. But I do have a weird issue that some fonts are rasterised very small. The font file I've got issue with is this https://github.com/lapce/piet-wgpu/blob/master/fonts/CascadiaCode-Regular.otf

    opened by dzhou121 0
  • Testing swash

    Testing swash

    Hello,

    I am playing with swash (and Harfbuzz too), sometimes I try to experience with algorithms and source code. However, I cannot sure whether it works for all font/script except for my limited testcases.

    Does swash have testcases, test data already?

    Thanks,

    opened by oooanoonymoous 0
  • AAT ligatures

    AAT ligatures

    Hi I'm looking at how ligatures work in the AAT morx table and I note that in fontkit it pushes the ligature glyphs back onto the stack after generating them:

    https://github.com/foliojs/fontkit/blob/417af0c79c5664271a07a783574ec7fac7ebad0c/src/aat/AATMorxProcessor.js#L209

    This is necessary for the Zapfino font, where the "ffi" ligature is implemented by first generating an "ff" ligature and then using that followed by the "i" to generate the "ffi" ligature, however the second ligature action won't work unless the "ff" is on the stack.

    It doesn't seem that swash does this at the moment, although I haven't been able to test it with the Zapfino font yet, is there a simple shaping test program I could try?

    opened by mikeday 1
  • Spacing has changed

    Spacing has changed

    In Neovide we determine the grid size by looking at the average glyph width and assuming that for monospace fonts, that will give us a reasonable size for a character grid.

    However in your recent change, that average width has increased. Plus I think thats actually a bad metric to use because some fonts have a few very wide glyphs, so it would be incorrect to use the average glyph width.

    I'm wondering if you have a better recommendation for what metric to look at. I've seen some systems pick an arbitrary character such as capital X and assume that it is representative. I've also attempted in the past to actually shape a representative piece of text and measure the spacing used there are the ground truth for the entire grid.

    To be honest I don't know what the "Correct" solution is. Does swash have any metric that would work well for this use case?

    opened by Kethku 6
Owner
Chad Brokaw
Chad Brokaw
This is a Rust implementation of a boid flocking simulation using the ggez graphics crate.

Boidflock This is a Rust implementation of a boid flocking simulation using the ggez graphics crate. The CLI for this program is built using the struc

Andrew Lee 11 May 20, 2021
Rust crate for creating beautiful interactive Chord Diagrams

Chord PRO Released Chord PRO is the full-featured chord visualization API, producing beautiful interactive visualizations, e.g. those featured on the

Dr. Shahin Rostami 25 Sep 10, 2022
A rust drawing library for high quality data plotting for both WASM and native, statically and realtimely ๐Ÿฆ€ ๐Ÿ“ˆ๐Ÿš€

Plotters - A Rust drawing library focus on data plotting for both WASM and native applications ?? ?? ?? Plotters is drawing library designed for rende

Hao Hou 2.6k Nov 24, 2022
A small charting/visualization tool and partial vega implementation for Rust

Gust A charting library for rust! Disclaimer This is still very much a work in progress! APIs are very unstable and subject to change. Contributions a

Samuel Resendez 128 Oct 14, 2022
KDash - A fast and simple dashboard for Kubernetes

KDash - A fast and simple dashboard for Kubernetes

null 881 Dec 2, 2022
๐ŸŒฑ Easy to use, blazing-fast, and reliable Chia plot manager for your farm

?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? UNDER DEVELOPME

Daniel De Lucca 8 May 31, 2022
below is an interactive tool to view and record historical system data.

A time traveling resource monitor for modern Linux systems

Facebook Incubator 800 Nov 26, 2022
This is an implementation of an Iced backend for Plotters, for both native and wasm applications.

plotters-iced This is an implementation of an Iced backend for Plotters, for both native and wasm applications. This backend has been optimized as for

joylei 52 Dec 4, 2022
๐Ÿ“ Web-based, reactive Datalog notebooks for data analysis and visualization

?? Web-based, reactive Datalog notebooks for data analysis and visualization

Eric Zhang 478 Dec 4, 2022
Data plotting library for Rust

plotlib plotlib is a generic data visualisation and plotting library for Rust. It is currently in the very early stages of development. It can current

Matt Williams 411 Nov 26, 2022
Plotly for Rust

Plotly.rs Plotly for Rust Getting Started | Recipes | API Docs | Changelog | | | A plotting library for Rust powered by Plotly.js. Usage Add this to y

Ioannis Giagkiozis 638 Nov 24, 2022
A Rust library for drawing plots, powered by Gnuplot.

RustGnuplot A Gnuplot controller written in Rust. Documentation See here Examples A simple example: let mut fg = Figure::new(); fg.axes2d() .set_titl

null 352 Nov 27, 2022
Render farm simulator & plotting for optimisation written in Rust.

Farm Usage Simulator Given a few basic charasteristics of a render farm and render jobs this app runs a few randomized farm usage scenarios and plots

ford 2 Jul 17, 2022
A Rust API for Vega-Lite V4 to build chart with a rusty API.

Vega-Lite V4 for Rust A Rust API for Vega-Lite V4 to build chart with a rusty API. Similar to the Altair project in python, this crate build upon Vega

Procyon 17 Sep 21, 2022
Externalize easily the plotting process from Rust to gnuplot.

preexplorer Easy plotter and saver of simple data. Handy tool for development stage or small computational projects. Save data, have a quick view and

Raimundo Saona 4 Jan 7, 2022
A library to generate syntax diagrams for Rust macros.

Live demo (code) A browser add-on for Firefox, Chrome and Edge A library to generate syntax ("railroad") diagrams for Rust's macro_rules!(). Diagrams

null 464 Nov 25, 2022
Graphical Rust program that uses a fractal algorithm to draw a tree of sorts

rusty-vegetation Graphical Rust program that uses a fractal algorithm to draw a "tree" of sorts. To Build and Run On Linux: Install build-essentials o

Stephen G Tuggy 3 Nov 5, 2022
A Rust program for visualizing how sections get packed into your Game Boy ROM

GB Packing Visualizer A Rust program for visualizing how sections get packed into your Game Boy ROM. rhythm_land.mp4 Each column represents a ROM bank

Eldred Habert 6 Jan 31, 2022