-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrender.rs
253 lines (216 loc) · 8.23 KB
/
render.rs
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
use bmfont_rs::{Char, Common, Font, Packing};
use image::{self, GrayImage, ImageFormat};
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::result::Result;
const FONT_DIR: &str = "data/examples";
const FONT: &str = "anton_latin.fnt";
const SURFACE_WIDTH: i32 = 600;
const SURFACE_HEIGHT: i32 = 300;
/// Basic rectangle.
#[derive(Clone, Copy, Debug, Default)]
pub struct Rec2 {
top_left: Vec2,
bottom_right: Vec2,
}
impl Rec2 {
pub fn new(top_left: Vec2, bottom_right: Vec2) -> Self {
Self { top_left, bottom_right }
}
pub fn with_size(top_left: Vec2, size: Vec2) -> Self {
Self::new(top_left, Vec2::new(top_left.x + size.x, top_left.y + size.y))
}
}
/// Basic length 2 vector
#[derive(Clone, Copy, Debug, Default)]
pub struct Vec2 {
x: i32,
y: i32,
}
impl Vec2 {
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
/// A basic surface to render our font to.
pub struct RenderSurface {
/// Render target
dst: GrayImage,
/// New font position
pos: Vec2,
/// Last character
last: Option<char>,
}
impl RenderSurface {
pub fn new(res: Vec2) -> Self {
Self { dst: GrayImage::new(res.x as u32, res.y as u32), pos: Vec2::default(), last: None }
}
/// Save our font. Selects formats according to the path extension (png, jpg only).
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
self.dst.save(path)?;
Ok(())
}
/// Print and newline. Text wrapping not implemented.
pub fn println(&mut self, render_font: &RenderFont, str: &str) {
self.print(render_font, str);
// Newline.
self.pos.x = 0;
self.pos.y += render_font.common.line_height as i32;
self.last = None;
}
/// Print. Text wrapping not implemented.
pub fn print(&mut self, render_font: &RenderFont, str: &str) {
str.chars().for_each(|character| self.print_character(render_font, character))
}
/// Print character. Text wrapping not implemented.
pub fn print_character(&mut self, render_font: &RenderFont, character: char) {
if let Some(char) = render_font.chars.get(&(character as u32)) {
// Calculate the source image coordinates.
let src_rect = Rec2::with_size(
Vec2::new(char.x as i32, char.y as i32),
Vec2::new(char.width as i32, char.height as i32),
);
// Calculate the destination image coordinates.
// We aren't implementing text wrapping, but here would be the place to do it.
let dst_pos =
Vec2::new(self.pos.x + char.xoffset as i32, self.pos.y + char.yoffset as i32);
// Advance our pos.
self.pos.x += char.xadvance as i32;
// Kerning pair adjustment for pos.
if let Some(last) = self.last {
let kerning_pair = (last as u32, character as u32);
if let Some(amount) = render_font.kernings.get(&kerning_pair) {
self.pos.x += *amount as i32;
}
}
self.last = Some(character);
// Grab the correct bitmap page.
let src = &render_font.bitmaps[char.page as usize];
// Render.
render(src, src_rect, &mut self.dst, dst_pos);
} else {
// Implement our missing character strategy.
eprintln!("cannot render character: {:08X}", character as u32);
}
}
}
/// The Font data we need in an accessible format.
/// Chars and Kernings have been restructured as maps.
/// Unused items have been discarded.
pub struct RenderFont {
/// Common field
common: Common,
/// Bitmaps
bitmaps: Vec<GrayImage>,
/// Characters keyed to u32 character
chars: HashMap<u32, Char>,
/// Kerning amount keyed to (u32 first character, u32 second character)
kernings: HashMap<(u32, u32), i16>,
}
impl RenderFont {
pub fn new(font: Font, bitmaps: Vec<GrayImage>) -> Result<Self, Box<dyn Error>> {
// Check we don't have references to things that don't exist.
font.validate_references()?;
// Take what we need.
let Font { common, mut chars, mut kernings, .. } = font;
// Restructure Chars and Kernings into maps for efficiency.
let chars = chars.drain(..).map(|u| (u.id, u)).collect();
let kernings = kernings.drain(..).map(|u| ((u.first, u.second), u.amount)).collect();
Ok(Self { common, bitmaps, chars, kernings })
}
}
/// Load the bitmap font.
fn load_bitmap_font(
folder: impl AsRef<Path>,
font: impl AsRef<Path>,
) -> Result<(Font, Vec<GrayImage>), Box<dyn Error>> {
let folder: &Path = folder.as_ref();
let font: &Path = font.as_ref();
// Load the font descriptor.
let rdr = File::open(folder.join(font))?;
let font = bmfont_rs::text::from_reader(rdr)?;
// Manage info and common attributes.
//
// If you trust that the font descriptor file has been generated with the correct parameters,
// you could skip this step.
//
// We are only supporting Unicode and 8-bit gray scale:
// info: unicode=1
// common: packed=0 alphaChnl=1
if !font.info.unicode || font.common.packed || font.common.alpha_chnl != Packing::Outline {
return Err(
format!("unsupported font descriptor: {:?}, {:?}", font.info, font.common).into()
);
}
// Load the textures
let mut bitmaps = Vec::with_capacity(font.pages.len());
for page in &font.pages {
let rdr = BufReader::new(File::open(folder.join(page))?);
let bitmap = image::load(rdr, ImageFormat::Png).map(|u| u.into_luma8())?;
bitmaps.push(bitmap);
}
// Done! We have what we need to render the font.
Ok((font, bitmaps))
}
/// Render from src to dst using the supplied dimensions. This function is inefficient.
/// In practice you likely want to render using a graphics capable API such as SDL, OpenGL or
/// similar.
fn render(src: &GrayImage, src_rect: Rec2, dst: &mut GrayImage, dst_pos: Vec2) {
// Clamp height/ width to available src/ dst image dimensions.
let src_pos = src_rect.top_left;
let src_width = src_rect.bottom_right.x - src_pos.x;
let src_height = src_rect.bottom_right.y - src_pos.y;
let dst_width = dst.width() as i32 - dst_pos.x;
let dst_height = dst.height() as i32 - dst_pos.y;
let width = src_width.min(dst_width);
let height = src_height.min(dst_height);
// Copy over our pixels, one by one, slowly...
for x in 0..width {
for y in 0..height {
let pixel = src.get_pixel((src_pos.x + x) as u32, (src_pos.y + y) as u32);
dst.put_pixel((dst_pos.x + x) as u32, (dst_pos.y + y) as u32, *pixel);
}
}
}
/// Render basic text to an image file
///
/// Execute from the project root with:
/// ```
/// cargo run --example render FILE
/// ```
///
/// Where FILE is the output image destination with either a .png or .jpg extension.
///
/// Example:
/// ```
/// cargo run --example render ~/Desktop/lorem.png
/// ```
fn main() -> Result<(), Box<dyn Error>> {
// Get output file from command line arguments.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("missing image output filename: png or jpg");
println!("try: cargo run --example render lorem_ipsum.jpg");
return Ok(());
}
let file = &args[1];
// Load our font and bitmaps.
let (font, bitmaps) = load_bitmap_font(FONT_DIR, FONT)?;
// We'll opt for an immutable RenderFont object that we can inject into render calls.
let render_font = RenderFont::new(font, bitmaps)?;
// Basic render surface.
let mut render_surface = RenderSurface::new(Vec2::new(SURFACE_WIDTH, SURFACE_HEIGHT));
// Ok! Let's print something.
render_surface.println(&render_font, " Lorem ipsum dolor sit amet,");
render_surface.println(&render_font, " consectetur adipiscing elit,");
render_surface.println(&render_font, " sed do eiusmod tempor incididunt");
render_surface.println(&render_font, " ut labore et dolore magna aliqua.");
// Let's save and make a run for it.
render_surface.save(file)?;
// All done! Time for coffee and cookies...
Ok(())
}