Skip to content

Commit

Permalink
Use GDI to resolve fonts of the original LiveSplit
Browse files Browse the repository at this point in the history
The original LiveSplit uses Windows Forms which internally uses the
fairly old GDI API. GDI's font identifiers encode styling information,
don't use the proper family name and are limited to 32 characters. This
makes it very hard for us to properly parse that information when
importing fonts from the original LiveSplit's layouts. However on
Windows we can directly talk to GDI to let it resolve the font. We can
then query more accurate information from the font by querying for the
`name` table, which contains the proper family name. This of course is
only available on Windows and only if the font can actually be found.
Otherwise we still provide the fallback algorithm to parse the font
information.
  • Loading branch information
CryZe committed Jan 1, 2022
1 parent 96e47a3 commit f37343c
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 14 deletions.
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ web-sys = { version = "0.3.28", default-features = false, features = [
"Window",
], optional = true }

[target.'cfg(windows)'.dependencies]
# We need winapi to use GDI to resolve fonts on Windows.
winapi = { version = "0.3.9", features = ["wingdi"], optional = true }

[target.'cfg(any(target_os = "linux", target_os = "l4re", target_os = "android", target_os = "macos", target_os = "ios"))'.dependencies]
# We need libc for our own implementation of Instant
libc = { version = "0.2.101", optional = true }
Expand All @@ -105,6 +109,7 @@ criterion = "0.3.0"
default = ["image-shrinking", "std"]
doesnt-have-atomics = []
std = [
"bytemuck/derive",
"byteorder",
"image",
"indexmap",
Expand All @@ -120,6 +125,7 @@ std = [
"time/macros",
"time/parsing",
"utf-8",
"winapi",
]
more-image-formats = [
"image/bmp",
Expand Down
117 changes: 117 additions & 0 deletions src/layout/parser/font_resolving/gdi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use std::{ffi::OsStr, mem, ptr, str};

use mem::MaybeUninit;
use winapi::{
shared::windef::{HDC, HFONT},
um::wingdi::{
CreateCompatibleDC, CreateFontW, DeleteDC, DeleteObject, GetFontData, GetTextMetricsW,
SelectObject, DEFAULT_PITCH, DEFAULT_QUALITY, GDI_ERROR, HGDI_ERROR, TEXTMETRICW,
},
};

pub struct DeviceContext(HDC);

impl Drop for DeviceContext {
fn drop(&mut self) {
unsafe {
DeleteDC(self.0);
}
}
}

impl DeviceContext {
pub fn new() -> Option<Self> {
unsafe {
let res = CreateCompatibleDC(ptr::null_mut());
if res.is_null() {
return None;
}
Some(Self(res))
}
}

pub fn select_font(&mut self, font: &mut Font) -> Option<()> {
unsafe {
let res = SelectObject(self.0, font.0.cast());
if res.is_null() || res == HGDI_ERROR {
return None;
}
Some(())
}
}

pub fn get_font_table(&mut self, name: [u8; 4]) -> Option<Vec<u8>> {
unsafe {
let name = u32::from_le_bytes(name);
let len = GetFontData(self.0, name, 0, ptr::null_mut(), 0);
if len == GDI_ERROR {
return None;
}
let mut name_table = Vec::<u8>::with_capacity(len as usize);
GetFontData(self.0, name, 0, name_table.as_mut_ptr().cast(), len);
if len == GDI_ERROR {
return None;
}
name_table.set_len(len as usize);
Some(name_table)
}
}

pub fn get_font_metrics(&mut self) -> Option<TEXTMETRICW> {
unsafe {
let mut text_metric = MaybeUninit::uninit();
let res = GetTextMetricsW(self.0, text_metric.as_mut_ptr());
if res == 0 {
return None;
}
Some(text_metric.assume_init())
}
}
}

pub struct Font(HFONT);

impl Drop for Font {
fn drop(&mut self) {
unsafe {
DeleteObject(self.0.cast());
}
}
}

impl Font {
pub fn new(name: &str, bold: bool, italic: bool) -> Option<Self> {
use std::os::windows::ffi::OsStrExt;

let mut name_buf = [0; 32];
let min_len = name.len().min(32);
name_buf[..min_len].copy_from_slice(&name.as_bytes()[..min_len]);

let name = OsStr::new(str::from_utf8(&name_buf).ok()?)
.encode_wide()
.collect::<Vec<u16>>();

unsafe {
let res = CreateFontW(
0,
0,
0,
0,
if bold { 700 } else { 400 },
italic as _,
0,
0,
0,
0,
0,
DEFAULT_QUALITY,
DEFAULT_PITCH,
name.as_ptr(),
);
if res.is_null() {
return None;
}
Some(Self(res))
}
}
}
26 changes: 26 additions & 0 deletions src/layout/parser/font_resolving/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
mod gdi;
mod name;
mod parse_util;

pub struct FontInfo {
pub family: String,
pub italic: bool,
pub weight: i32,
}

impl FontInfo {
pub fn from_gdi(name: &str, bold: bool, italic: bool) -> Option<Self> {
let mut font = gdi::Font::new(name, bold, italic)?;
let mut dc = gdi::DeviceContext::new()?;
dc.select_font(&mut font)?;
let metrics = dc.get_font_metrics()?;
let name_table = dc.get_font_table(*b"name")?;
let family = name::look_up_family_name(&name_table)?;

Some(Self {
family,
italic: metrics.tmItalic != 0,
weight: metrics.tmWeight,
})
}
}
71 changes: 71 additions & 0 deletions src/layout/parser/font_resolving/name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::mem;

use super::parse_util::{pod, slice, U16};
use bytemuck::{Pod, Zeroable};

#[derive(Debug, Copy, Clone, Pod, Zeroable)]
#[repr(C)]
struct Header {
version: U16,
count: U16,
storage_offset: U16,
}

#[derive(Debug, Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct NameRecord {
platform_id: U16,
encoding_id: U16,
language_id: U16,
name_id: U16,
length: U16,
string_offset: U16,
}

impl NameRecord {
fn get_name(&self, storage: &[u8]) -> Option<String> {
let name = storage
.get(self.string_offset.usize()..)?
.get(..self.length.usize())?;

let mut buf = Vec::new();
let slice: &[[u8; 2]] = bytemuck::try_cast_slice(name).ok()?;
for &c in slice {
buf.push(u16::from_be_bytes(c));
}

String::from_utf16(&buf).ok()
}
}

const fn is_unicode_encoding(platform_id: u16, encoding_id: u16) -> bool {
match platform_id {
0 => true,
3 => matches!(encoding_id, 0 | 1),
_ => false,
}
}

pub fn look_up_family_name(table: &[u8]) -> Option<String> {
let header = pod::<Header>(table)?;
let records =
slice::<NameRecord>(table.get(mem::size_of::<Header>()..)?, header.count.usize())?;

let font_family = 1u16.to_be_bytes();
let typographic_family = 16u16.to_be_bytes();

let record = records
.iter()
.filter(|r| r.name_id.0 == font_family || r.name_id.0 == typographic_family)
.filter(|r| is_unicode_encoding(r.platform_id.get(), r.encoding_id.get()))
.filter(|r| match r.platform_id.get() {
0 => true,
1 => r.language_id.get() == 0,
3 => r.language_id.get() & 0xFF == 0x09,
_ => false,
})
.max_by_key(|r| (r.name_id.0, !r.platform_id.get()))?;

let storage = table.get(header.storage_offset.usize()..)?;
record.get_name(storage)
}
46 changes: 46 additions & 0 deletions src/layout/parser/font_resolving/parse_util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use bytemuck::{Pod, Zeroable};
use std::{fmt, mem};

#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(transparent)]
pub struct U16(pub [u8; 2]);

impl fmt::Debug for U16 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.get(), f)
}
}

impl U16 {
pub const fn get(self) -> u16 {
u16::from_be_bytes(self.0)
}

pub const fn usize(self) -> usize {
self.get() as usize
}
}

#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(transparent)]
pub struct O32(pub [u8; 4]);

impl fmt::Debug for O32 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.get(), f)
}
}

impl O32 {
pub const fn get(self) -> u32 {
u32::from_be_bytes(self.0)
}
}

pub fn pod<P: Pod>(bytes: &[u8]) -> Option<&P> {
Some(bytemuck::from_bytes(bytes.get(..mem::size_of::<P>())?))
}

pub fn slice<P: Pod>(bytes: &[u8], n: usize) -> Option<&[P]> {
Some(bytemuck::cast_slice(bytes.get(..n * mem::size_of::<P>())?))
}
62 changes: 49 additions & 13 deletions src/layout/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ mod timer;
mod title;
mod total_playtime;

#[cfg(windows)]
mod font_resolving;

// One single row component is:
// 1.0 units high in component space.
// 24 pixels high in LiveSplit One's pixel coordinate space.
Expand Down Expand Up @@ -327,9 +330,10 @@ where
let rem = cursor.as_slice();

let font_name = rem.get(..len).ok_or(Error::ParseFont)?;
let mut family = str::from_utf8(font_name)
let original_family_name = str::from_utf8(font_name)
.map_err(|_| Error::ParseFont)?
.trim();
let mut family = original_family_name;

let mut style = FontStyle::Normal;
let mut weight = FontWeight::Normal;
Expand All @@ -354,14 +358,6 @@ where
// to not recognize them. An example of this is "Bahnschrift SemiLight
// SemiConde" where the end should say "SemiCondensed" but doesn't due
// to the character limit.
//
// A more sophisticated approach where on Windows we may talk directly
// to GDI to resolve the name has not been implemented so far. The
// problem is that GDI does not give you access to either the path of
// the font or its data. You can receive the byte representation of
// individual tables you query for, but ttf-parser, the crate we use for
// parsing fonts, doesn't expose the ability to parse individual tables
// in its public API.

for token in family.split_whitespace().rev() {
// FontWeight and FontStretch both have the variant "normal"
Expand Down Expand Up @@ -421,23 +417,63 @@ where
}
}

// Later on we find the style as bitflags of System.Drawing.FontStyle.
// Later on we find the style and weight as bitflags of System.Drawing.FontStyle.
// 1 -> bold
// 2 -> italic
// 4 -> underline
// 8 -> strikeout
let flags = *rem.get(len + 52).ok_or(Error::ParseFont)?;
let (bold_flag, italic_flag) = (flags & 1 != 0, flags & 2 != 0);

// If we are on Windows, we can however directly use GDI to get the
// proper family name out of the font. The problem is that GDI does not
// give us access to either the path of the font or its data. However we can
// receive the byte representation of individual tables we query for, so
// we can get the family name from the `name` table.

#[cfg(windows)]
let family = if let Some(info) =
font_resolving::FontInfo::from_gdi(original_family_name, bold_flag, italic_flag)
{
weight = match info.weight {
i32::MIN..=149 => FontWeight::Thin,
150..=249 => FontWeight::ExtraLight,
250..=324 => FontWeight::Light,
325..=374 => FontWeight::SemiLight,
375..=449 => FontWeight::Normal,
450..=549 => FontWeight::Medium,
550..=649 => FontWeight::SemiBold,
650..=749 => FontWeight::Bold,
750..=849 => FontWeight::ExtraBold,
850..=924 => FontWeight::Black,
925.. => FontWeight::ExtraBlack,
};
style = if info.italic {
FontStyle::Italic
} else {
FontStyle::Normal
};
info.family
} else {
family.to_owned()
};

#[cfg(not(windows))]
let family = family.to_owned();

// The font might not exist on the user's system, so we still prefer to
// apply these flags.

if flags & 1 != 0 {
if bold_flag && weight < FontWeight::Bold {
weight = FontWeight::Bold;
}

if flags & 2 != 0 {
if italic_flag {
style = FontStyle::Italic;
}

f(Font {
family: family.to_owned(),
family,
style,
weight,
stretch,
Expand Down
Loading

0 comments on commit f37343c

Please sign in to comment.