summaryrefslogtreecommitdiff
path: root/src/env.rs
blob: 3db71e087e7b1386a0cac98dd28045b27b288ba1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
//! Environment interactions.

use std::any::Any;
use std::collections::{hash_map::Entry, HashMap};
use std::fmt::{self, Debug, Formatter};
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};

use fontdock::{ContainsChar, FaceFromVec, FaceId, FontSource};
use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImageView, ImageFormat};
use ttf_parser::Face;

#[cfg(feature = "fs")]
use fontdock::fs::{FsIndex, FsSource};

/// Encapsulates all environment dependencies (fonts, resources).
#[derive(Debug)]
pub struct Env {
    /// Loads fonts from a dynamic font source.
    pub fonts: FontLoader,
    /// Loads resource from the file system.
    pub resources: ResourceLoader,
}

impl Env {
    /// Create an empty environment for testing purposes.
    pub fn blank() -> Self {
        struct BlankSource;

        impl FontSource for BlankSource {
            type Face = FaceBuf;

            fn load(&self, _: FaceId) -> Option<Self::Face> {
                None
            }
        }

        Self {
            fonts: FontLoader::new(Box::new(BlankSource), vec![]),
            resources: ResourceLoader::new(),
        }
    }
}

/// A font loader that is backed by a dynamic source.
pub type FontLoader = fontdock::FontLoader<Box<dyn FontSource<Face = FaceBuf>>>;

/// An owned font face.
pub struct FaceBuf {
    data: Box<[u8]>,
    face: Face<'static>,
}

impl FaceBuf {
    /// Get a reference to the underlying face.
    pub fn get(&self) -> &Face<'_> {
        // We can't implement Deref because that would leak the internal 'static
        // lifetime.
        &self.face
    }

    /// The raw face data.
    pub fn data(&self) -> &[u8] {
        &self.data
    }
}

impl FaceFromVec for FaceBuf {
    fn from_vec(vec: Vec<u8>, i: u32) -> Option<Self> {
        let data = vec.into_boxed_slice();

        // SAFETY: The slices's location is stable in memory since we don't
        //         touch it and it can't be touched from outside this type.
        let slice: &'static [u8] =
            unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };

        Some(Self {
            data,
            face: Face::from_slice(slice, i).ok()?,
        })
    }
}

impl ContainsChar for FaceBuf {
    fn contains_char(&self, c: char) -> bool {
        self.get().glyph_index(c).is_some()
    }
}

/// Simplify font loader construction from an [`FsIndex`].
#[cfg(feature = "fs")]
pub trait FsIndexExt {
    /// Create a font loader backed by a boxed [`FsSource`] which serves all
    /// indexed font faces.
    fn into_dynamic_loader(self) -> FontLoader;
}

#[cfg(feature = "fs")]
impl FsIndexExt for FsIndex {
    fn into_dynamic_loader(self) -> FontLoader {
        let (files, descriptors) = self.into_vecs();
        FontLoader::new(Box::new(FsSource::new(files)), descriptors)
    }
}

/// Loads resource from the file system.
pub struct ResourceLoader {
    paths: HashMap<PathBuf, ResourceId>,
    entries: Vec<Box<dyn Any>>,
}

/// A unique identifier for a resource.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ResourceId(usize);

impl ResourceLoader {
    /// Create a new resource loader.
    pub fn new() -> Self {
        Self { paths: HashMap::new(), entries: vec![] }
    }

    /// Load a resource from a path and parse it.
    pub fn load<P, F, R>(&mut self, path: P, parse: F) -> Option<(ResourceId, &R)>
    where
        P: AsRef<Path>,
        F: FnOnce(Vec<u8>) -> Option<R>,
        R: 'static,
    {
        let path = path.as_ref();
        let id = match self.paths.entry(path.to_owned()) {
            Entry::Occupied(entry) => *entry.get(),
            Entry::Vacant(entry) => {
                let data = fs::read(path).ok()?;
                let resource = parse(data)?;
                let len = self.entries.len();
                self.entries.push(Box::new(resource));
                *entry.insert(ResourceId(len))
            }
        };

        Some((id, self.loaded(id)))
    }

    /// Retrieve a previously loaded resource by its id.
    ///
    /// # Panics
    /// This panics if no resource with this id was loaded.
    #[track_caller]
    pub fn loaded<R: 'static>(&self, id: ResourceId) -> &R {
        self.entries[id.0].downcast_ref().expect("bad resource type")
    }
}

impl Debug for ResourceLoader {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        f.debug_set().entries(self.paths.keys()).finish()
    }
}

/// A loaded image resource.
pub struct ImageResource {
    /// The original format the image was encoded in.
    pub format: ImageFormat,
    /// The decoded image.
    pub buf: DynamicImage,
}

impl ImageResource {
    /// Parse an image resource from raw data in a supported format.
    ///
    /// The image format is determined automatically.
    pub fn parse(data: Vec<u8>) -> Option<Self> {
        let reader = ImageReader::new(Cursor::new(data)).with_guessed_format().ok()?;
        let format = reader.format()?;
        let buf = reader.decode().ok()?;
        Some(Self { format, buf })
    }
}

impl Debug for ImageResource {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        let (width, height) = self.buf.dimensions();
        f.debug_struct("ImageResource")
            .field("format", &self.format)
            .field("color", &self.buf.color())
            .field("width", &width)
            .field("height", &height)
            .finish()
    }
}