Skip to content

Commit

Permalink
Merge pull request #500 from CryZe/resolve-fonts-with-gdi
Browse files Browse the repository at this point in the history
Use GDI to resolve fonts of the original LiveSplit
  • Loading branch information
CryZe authored Jan 1, 2022
2 parents 96e47a3 + f37343c commit 46d51ec
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 46d51ec

Please sign in to comment.