Skip to main content

fontcore/
fontfile.rs

1//! File and collection entry points for the public API.
2
3use crate::fontface::FontFace;
4use crate::fontreader;
5#[cfg(not(target_arch = "wasm32"))]
6use std::io::Read;
7#[cfg(not(target_arch = "wasm32"))]
8use std::io::Write;
9use std::io::{Error, ErrorKind};
10#[cfg(not(target_arch = "wasm32"))]
11use std::net::TcpStream;
12use std::path::Path;
13
14/// Source used by [`open_font`] and [`load_font`].
15pub enum FontSource<'a> {
16    /// Load from a filesystem path.
17    File(&'a Path),
18    /// Load from an in-memory byte slice.
19    Buffer(&'a [u8]),
20}
21
22/// Owns a font file or collection and lets callers choose faces from it.
23#[derive(Debug, Clone)]
24pub struct FontFile {
25    font: fontreader::Font,
26}
27
28impl FontFile {
29    /// Opens a font file from disk.
30    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
31        let font = fontreader::Font::get_font_from_file(&path.as_ref().to_path_buf())?;
32        Ok(Self { font })
33    }
34
35    /// Opens a font from bytes already loaded in memory.
36    pub fn from_buffer(buffer: &[u8]) -> Result<Self, Error> {
37        let font = fontreader::Font::get_font_from_buffer(buffer)?;
38        Ok(Self { font })
39    }
40
41    /// Opens a font from a generic [`FontSource`].
42    pub fn from_source(source: FontSource<'_>) -> Result<Self, Error> {
43        match source {
44            FontSource::File(path) => Self::from_file(path),
45            FontSource::Buffer(buffer) => Self::from_buffer(buffer),
46        }
47    }
48
49    /// Opens a font over plain `http://`.
50    ///
51    /// This is not available on `wasm32`.
52    pub fn from_net(url: &str) -> Result<Self, Error> {
53        #[cfg(target_arch = "wasm32")]
54        {
55            let _ = url;
56            return Err(Error::new(
57                ErrorKind::Unsupported,
58                "network font loading is not supported on wasm32",
59            ));
60        }
61
62        #[cfg(not(target_arch = "wasm32"))]
63        {
64            let bytes = fetch_http_font(url)?;
65            Self::from_buffer(&bytes)
66        }
67    }
68
69    /// Returns the number of faces in the file or collection.
70    pub fn face_count(&self) -> usize {
71        self.font.get_font_count()
72    }
73
74    /// Returns one face by index.
75    pub fn face(&self, index: usize) -> Result<FontFace, Error> {
76        let mut font = self.font.clone();
77        font.set_font(index)
78            .map_err(|message| Error::new(ErrorKind::InvalidInput, message))?;
79        Ok(FontFace::from_font(font))
80    }
81
82    /// Returns the currently selected face.
83    pub fn current_face(&self) -> Result<FontFace, Error> {
84        self.face(self.font.get_font_number())
85    }
86
87    /// Returns all faces in the file or collection.
88    pub fn faces(&self) -> Result<Vec<FontFace>, Error> {
89        let mut faces = Vec::with_capacity(self.face_count());
90        for index in 0..self.face_count() {
91            faces.push(self.face(index)?);
92        }
93        Ok(faces)
94    }
95
96    /// Dumps a small human-readable summary of the loaded file.
97    pub fn dump(&self) -> String {
98        format!(
99            "FontFile\nface_count: {}\ncurrent_face: {}\nformat: {}",
100            self.face_count(),
101            self.font.get_font_number(),
102            self.font.font_type.to_string()
103        )
104    }
105
106    #[cfg(feature = "raw")]
107    /// Returns the low-level parsed font when `raw` is enabled.
108    pub fn raw_font(&self) -> &crate::fontreader::Font {
109        &self.font
110    }
111}
112
113/// Opens a [`FontFile`] from disk.
114pub fn open_font_from_file(path: impl AsRef<Path>) -> Result<FontFile, Error> {
115    FontFile::from_file(path)
116}
117
118/// Opens a [`FontFile`] from memory.
119pub fn open_font_from_buffer(buffer: &[u8]) -> Result<FontFile, Error> {
120    FontFile::from_buffer(buffer)
121}
122
123/// Opens a [`FontFile`] from plain `http://`.
124pub fn open_font_from_net(url: &str) -> Result<FontFile, Error> {
125    FontFile::from_net(url)
126}
127
128/// Opens a [`FontFile`] from a generic [`FontSource`].
129pub fn open_font(source: FontSource<'_>) -> Result<FontFile, Error> {
130    FontFile::from_source(source)
131}
132
133/// Loads the current face from a file.
134pub fn load_font_from_file(path: impl AsRef<Path>) -> Result<FontFace, Error> {
135    FontFile::from_file(path)?.current_face()
136}
137
138/// Loads the current face from an in-memory buffer.
139pub fn load_font_from_buffer(buffer: &[u8]) -> Result<FontFace, Error> {
140    FontFile::from_buffer(buffer)?.current_face()
141}
142
143/// Loads the current face from plain `http://`.
144pub fn load_font_from_net(url: &str) -> Result<FontFace, Error> {
145    FontFile::from_net(url)?.current_face()
146}
147
148/// Loads the current face from a generic [`FontSource`].
149pub fn load_font(source: FontSource<'_>) -> Result<FontFace, Error> {
150    FontFile::from_source(source)?.current_face()
151}
152
153#[cfg(feature = "raw")]
154#[deprecated(note = "use `load_font_from_file()` instead")]
155pub fn fontload_file(path: impl AsRef<Path>) -> Result<FontFace, Error> {
156    load_font_from_file(path)
157}
158
159#[cfg(feature = "raw")]
160#[deprecated(note = "use `load_font_from_buffer()` instead")]
161pub fn fontload_buffer(buffer: &[u8]) -> Result<FontFace, Error> {
162    load_font_from_buffer(buffer)
163}
164
165#[cfg(feature = "raw")]
166#[deprecated(note = "use `load_font_from_net()` instead")]
167pub fn fontload_net(url: &str) -> Result<FontFace, Error> {
168    load_font_from_net(url)
169}
170
171#[cfg(feature = "raw")]
172#[deprecated(note = "use `load_font()` instead")]
173pub fn fontload(source: FontSource<'_>) -> Result<FontFace, Error> {
174    load_font(source)
175}
176
177/// Collects a font file incrementally before decoding it.
178pub struct ChunkedFontBuffer {
179    total_size: usize,
180    data: Vec<u8>,
181    filled: Vec<bool>,
182    filled_len: usize,
183}
184
185impl ChunkedFontBuffer {
186    /// Creates an empty buffer for a known final font size.
187    pub fn new(total_size: usize) -> Result<Self, Error> {
188        if total_size == 0 {
189            return Err(Error::new(
190                ErrorKind::InvalidInput,
191                "chunked font buffer size must be greater than zero",
192            ));
193        }
194
195        Ok(Self {
196            total_size,
197            data: vec![0; total_size],
198            filled: vec![false; total_size],
199            filled_len: 0,
200        })
201    }
202
203    /// Returns the target total size in bytes.
204    pub fn total_size(&self) -> usize {
205        self.total_size
206    }
207
208    /// Returns how many bytes are already filled.
209    pub fn filled_len(&self) -> usize {
210        self.filled_len
211    }
212
213    /// Returns `true` when every byte has been supplied.
214    pub fn is_complete(&self) -> bool {
215        self.filled_len == self.total_size
216    }
217
218    /// Appends one chunk at the given byte offset.
219    pub fn append(&mut self, offset: usize, bytes: &[u8]) -> Result<(), Error> {
220        if bytes.is_empty() {
221            return Ok(());
222        }
223
224        let end = offset
225            .checked_add(bytes.len())
226            .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "chunk offset overflow"))?;
227        if end > self.total_size {
228            return Err(Error::new(
229                ErrorKind::InvalidInput,
230                "chunk is out of range for the target font buffer",
231            ));
232        }
233
234        for (index, byte) in bytes.iter().copied().enumerate() {
235            let position = offset + index;
236            if self.filled[position] {
237                if self.data[position] != byte {
238                    return Err(Error::new(
239                        ErrorKind::InvalidData,
240                        "conflicting chunk data for the same byte range",
241                    ));
242                }
243                continue;
244            }
245
246            self.data[position] = byte;
247            self.filled[position] = true;
248            self.filled_len += 1;
249        }
250
251        Ok(())
252    }
253
254    /// Returns missing byte ranges as half-open intervals.
255    pub fn missing_ranges(&self) -> Vec<(usize, usize)> {
256        let mut ranges = Vec::new();
257        let mut start = None;
258
259        for (index, filled) in self.filled.iter().copied().enumerate() {
260            match (start, filled) {
261                (None, false) => start = Some(index),
262                (Some(range_start), true) => {
263                    ranges.push((range_start, index));
264                    start = None;
265                }
266                _ => {}
267            }
268        }
269
270        if let Some(range_start) = start {
271            ranges.push((range_start, self.total_size));
272        }
273
274        ranges
275    }
276
277    /// Clones the complete font bytes.
278    pub fn to_vec(&self) -> Result<Vec<u8>, Error> {
279        if !self.is_complete() {
280            return Err(Error::new(
281                ErrorKind::WouldBlock,
282                "font buffer is incomplete; append all chunks before decoding",
283            ));
284        }
285
286        Ok(self.data.clone())
287    }
288
289    /// Decodes the collected bytes as a [`FontFile`].
290    pub fn load_font_file(&self) -> Result<FontFile, Error> {
291        let bytes = self.to_vec()?;
292        open_font_from_buffer(&bytes)
293    }
294
295    /// Decodes the collected bytes and returns the current face.
296    pub fn load_font_face(&self) -> Result<FontFace, Error> {
297        self.load_font_file()?.current_face()
298    }
299
300    #[cfg(feature = "raw")]
301    pub fn load_font(&self) -> Result<FontFace, Error> {
302        self.load_font_face()
303    }
304
305    /// Consumes the buffer and decodes it as a [`FontFile`].
306    pub fn into_font_file(self) -> Result<FontFile, Error> {
307        if !self.is_complete() {
308            return Err(Error::new(
309                ErrorKind::WouldBlock,
310                "font buffer is incomplete; append all chunks before decoding",
311            ));
312        }
313
314        open_font_from_buffer(&self.data)
315    }
316
317    /// Consumes the buffer and returns the current face.
318    pub fn into_font_face(self) -> Result<FontFace, Error> {
319        self.into_font_file()?.current_face()
320    }
321
322    #[cfg(feature = "raw")]
323    pub fn into_loaded_font(self) -> Result<FontFace, Error> {
324        self.into_font_face()
325    }
326}
327
328#[cfg(not(target_arch = "wasm32"))]
329fn fetch_http_font(url: &str) -> Result<Vec<u8>, Error> {
330    let url = url.strip_prefix("http://").ok_or_else(|| {
331        Error::new(
332            ErrorKind::InvalidInput,
333            "only http:// URLs are supported for font loading",
334        )
335    })?;
336
337    let (authority, path) = match url.split_once('/') {
338        Some((authority, path)) => (authority, format!("/{}", path)),
339        None => (url, "/".to_string()),
340    };
341
342    let (host, port) = match authority.rsplit_once(':') {
343        Some((host, port)) if !host.is_empty() && !port.is_empty() => {
344            let port = port
345                .parse::<u16>()
346                .map_err(|_| Error::new(ErrorKind::InvalidInput, "invalid port in http URL"))?;
347            (host.to_string(), port)
348        }
349        _ => (authority.to_string(), 80),
350    };
351
352    let mut stream = TcpStream::connect((host.as_str(), port))?;
353    let host_header = if port == 80 {
354        host.clone()
355    } else {
356        format!("{}:{}", host, port)
357    };
358    let request = format!(
359        "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nAccept: */*\r\n\r\n",
360        path, host_header
361    );
362    stream.write_all(request.as_bytes())?;
363
364    let mut response = Vec::new();
365    stream.read_to_end(&mut response)?;
366
367    let header_end = response
368        .windows(4)
369        .position(|window| window == b"\r\n\r\n")
370        .ok_or_else(|| Error::new(ErrorKind::InvalidData, "invalid http response"))?
371        + 4;
372
373    let header = std::str::from_utf8(&response[..header_end])
374        .map_err(|_| Error::new(ErrorKind::InvalidData, "invalid http header"))?;
375    if !(header.starts_with("HTTP/1.1 200") || header.starts_with("HTTP/1.0 200")) {
376        return Err(Error::new(
377            ErrorKind::InvalidData,
378            format!(
379                "unexpected http status: {}",
380                header.lines().next().unwrap_or("")
381            ),
382        ));
383    }
384
385    Ok(response[header_end..].to_vec())
386}