-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use GDI to resolve fonts of the original LiveSplit
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
Showing
7 changed files
with
316 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>())?)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.