1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # Parse Angel Code BMFont format and draw text
17 # The BMFont format supports packed textures, varying advance per character and
18 # even kernings. It can be generated with a number of tools, inluding:
19 # * BMFont, free software Windows app, http://www.angelcode.com/products/bmfont/
20 # * Littera, a web app, http://kvazars.com/littera/
22 # Format reference: http://www.angelcode.com/products/bmfont/doc/file_format.html
29 # BMFont description, parsed with `Text::parse_bmfont` or loaded as a `BMFontAsset`
31 # This class flattens all the `info` and `common` data.
37 # How the font was generated.
39 # Name of the source true type font
42 # Size of the source true type font
51 # Does the font uses the Unicode charset?
54 # Padding for each character
56 # In the format `up,right,down,left`
59 # Spacing for each character
61 # In the format `horizontal,vertical`.
67 # Information common to all characters
69 # Distance in pixels between each line of text
72 # Pixels from the top of the line to the base of the characters
75 # Width of the texture
78 # Height of the texture
82 var pages
= new Map[String, TextureAsset]
84 # Characters in the font
85 var chars
= new Map[Char, BMFontChar]
87 # Distance between certain characters
88 var kernings
= new HashMap2[Char, Char, Int]
90 redef fun to_s
do return "<{class_name} {face} at {size} pt, "+
91 "{pages.length} pages, {chars.length} chars>"
110 # Description of a character in a `BMFont`
113 # Subtexture left coordinate
116 # Subtexture top coordinate
125 # Drawing offset on X
128 # Drawing offset on Y
131 # Cursor advance after drawing this character
134 # Full texture contaning this character and others
135 var page
: TextureAsset
137 # TODO Channel where the image is found
140 # Subtexture with this character image only
141 var subtexture
: Texture = page
.subtexture
(x
, y
, width
, height
) is lazy
146 # Parse `self` as an XML BMFont description file
148 # Reports only basic XML format errors, other errors may be ignored or
154 # <info face="arial" size="72" bold="0" italic="0" charset=""
155 # unicode="1" stretchH="100" smooth="1" aa="1" padding="2,2,2,2"
156 # spacing="0,0" outline="0"/>
157 # <common lineHeight="80" base="65" scaleW="4030" scaleH="231"
158 # pages="1" packed="0"/>
160 # <page id="0" file="arial.png"/>
163 # <char id="65" x="2519" y="10" width="55" height="59" xoffset="0"
164 # yoffset="13" xadvance="48" page="0" chnl="15"/>
165 # <char id="66" x="2600" y="10" width="46" height="58" xoffset="5"
166 # yoffset="13" xadvance="48" page="0" chnl="15"/>
167 # <char id="67" x="2673" y="9" width="52" height="60" xoffset="4"
168 # yoffset="12" xadvance="52" page="0" chnl="15"/>
170 # <kernings count="1">
171 # <kerning first="65" second="67" amount="-1"/>
176 # var fnt = desc.parse_bmfont("dir_in_assets").value
177 # assert fnt.to_s == "<BMFont arial at 72 pt, 1 pages, 3 chars>"
178 # assert fnt.line_height == 80
179 # assert fnt.kernings['A', 'C'] == -1
180 # assert fnt.chars['A'].page.path == "dir_in_assets/arial.png"
182 fun parse_bmfont
(dir
: String): MaybeError[BMFont, Error]
186 if xml
isa XMLError then
187 var msg
= "XML Parse Error: {xml.message}:{xml.location or else 0}"
188 return new MaybeError[BMFont, Error](maybe_error
=new Error(msg
))
192 var roots
= xml
["font"]
193 if roots
.is_empty
then
194 var msg
= "Error: the XML document doesn't declare the expected `font` root"
195 return new MaybeError[BMFont, Error](maybe_error
=new Error(msg
))
198 # Expect the rest of the document to be well formatted
199 var root
= roots
.first
201 var info
= root
["info"].first
202 assert info
isa XMLAttrTag
203 var info_map
= info
.attributes_to_map
205 var common
= root
["common"].first
206 assert common
isa XMLAttrTag
207 var common_map
= common
.attributes_to_map
209 var fnt
= new BMFont(
211 info_map
["size"].to_i
,
212 info_map
["bold"] == "1",
213 info_map
["italic"] == "1",
214 info_map
["unicode"] == "1",
217 common_map
["lineHeight"].to_i
,
218 common_map
["base"].to_i
,
219 common_map
["scaleW"].to_i
,
220 common_map
["scaleH"].to_i
223 # Pages / pixel data files
224 var xml_pages
= root
["pages"].first
225 for page
in xml_pages
["page"] do
226 if not page
isa XMLAttrTag then continue
228 var attributes
= page
.attributes_to_map
229 var file
= dir
/ attributes
["file"]
230 fnt
.pages
[attributes
["id"]] = new TextureAsset(file
)
234 for item
in root
["chars"].first
["char"] do
235 if not item
isa XMLAttrTag then continue
237 var attributes
= item
.attributes_to_map
238 var id
= attributes
["id"].to_i
.code_point
240 var c
= new BMFontChar(
241 attributes
["x"].to_i
, attributes
["y"].to_i
,
242 attributes
["width"].to_i
, attributes
["height"].to_i
,
243 attributes
["xoffset"].to_i
, attributes
["yoffset"].to_i
,
244 attributes
["xadvance"].to_i
,
245 fnt
.pages
[attributes
["page"]])
250 # Kerning between two characters
251 var kernings
= root
["kernings"]
252 if kernings
.not_empty
then
253 for item
in kernings
.first
["kerning"] do
254 if not item
isa XMLAttrTag then continue
256 var attributes
= item
.attributes_to_map
257 var first
= attributes
["first"].to_i
.code_point
258 var second
= attributes
["second"].to_i
.code_point
259 var amount
= attributes
["amount"].to_i
260 fnt
.kernings
[first
, second
] = amount
264 return new MaybeError[BMFont, Error](fnt
)
268 # BMFont from the assets folder
272 # var font = new BMFontAsset("arial.fnt")
273 # var ui_text = new TextSprites(font, ui_camera.top_left)
275 # redef fun on_create
280 # assert font.error == null
282 # ui_text.text = "Hello world!"
292 # Require: `error == null`
296 var cache
= desc_cache
297 if cache
!= null then return cache
299 assert error
== null else print_error error
301 # Load on first access
304 assert error
== null else print_error error
306 return desc_cache
.as(not null)
309 private var desc_cache
: nullable BMFont = null
312 var error
: nullable Error = null
314 # XML description in the assets folder
315 private var text_asset
= new TextAsset(path
) is lateinit
317 # Load font description and textures from the assets folder
319 # Sets `error` if an error occurred, otherwise
320 # the font description can be accessed via `desc`.
323 var text_asset
= text_asset
325 var error
= text_asset
.error
326 if error
!= null then
331 var desc
= text_asset
.to_s
332 var fnt_or_error
= desc
.parse_bmfont
(path
.dirname
)
333 if fnt_or_error
.is_error
then
334 self.error
= fnt_or_error
.error
338 var fnt
= fnt_or_error
.value
339 self.desc_cache
= fnt
342 for page_name
, texture
in fnt
.pages
do
345 # Move up any texture loading error.
346 # This algo keeps only the latest error,
347 # but this isn't a problem on single page fonts.
348 error
= texture
.error
349 if error
!= null then self.error
= error
353 redef fun write_into
(text_sprites
, text
)
358 var line_height
= desc
.line_height
.to_f
363 var partial_line_mod
= 0.4
367 dy
-= line_height
.to_f
368 dx
= 0.0 #advance/2.0
371 else if c
== pld
then
372 dy
-= line_height
* partial_line_mod
.to_f
375 else if c
== plu
then
376 dy
+= line_height
* partial_line_mod
.to_f
379 else if c
.is_whitespace
then
380 var advance
= if desc
.chars
.keys
.has
(' ') then
381 desc
.chars
[' '].xadvance
.to_f
382 else if desc
.chars
.keys
.has
('f') then
383 desc
.chars
['f'].xadvance
.to_f
390 # Replace or skip unknown characters
391 if not desc
.chars
.keys
.has
(c
) then
392 var rc
= replacement_char
393 if rc
== null then continue
397 var char_info
= desc
.chars
[c
]
398 var advance
= char_info
.xadvance
.to_f
401 if prev_char
!= null then
402 kerning
= (desc
.kernings
[prev_char
, c
] or else 0).to_f
405 var x
= dx
+ char_info
.width
.to_f
/2.0 + char_info
.xoffset
.to_f
+ kerning
406 var y
= dy
- char_info
.height
.to_f
/2.0 - char_info
.yoffset
.to_f
407 var pos
= text_sprites
.anchor
.offset
(x
, y
, 0.0)
408 text_sprites
.sprites
.add
new Sprite(char_info
.subtexture
, pos
)
410 dx
+= advance
+ kerning
415 # Character replacing other charactesr missing from the font
416 private var replacement_char
: nullable Char is lazy
do
417 for c
in "�?".chars
do
418 if desc
.chars
.keys
.has
(c
) then return c