• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*******************************************************************************
2  * Copyright 2011 See AUTHORS file.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  ******************************************************************************/
16 
17 package com.badlogic.gdx.tools.bmfont;
18 
19 import com.badlogic.gdx.files.FileHandle;
20 import com.badlogic.gdx.graphics.Pixmap;
21 import com.badlogic.gdx.graphics.PixmapIO;
22 import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
23 import com.badlogic.gdx.graphics.g2d.BitmapFont.Glyph;
24 import com.badlogic.gdx.graphics.g2d.PixmapPacker.Page;
25 import com.badlogic.gdx.tools.hiero.Hiero;
26 import com.badlogic.gdx.utils.Array;
27 
28 /** A utility to output BitmapFontData to a FNT file. This can be useful for caching the result from TrueTypeFont, for faster load
29  * times.
30  * <p>
31  * The font file format is from the AngelCodeFont BMFont tool.
32  * <p>
33  * Output is nearly identical to the FreeType settting in the {@link Hiero} tool. BitmapFontWriter gives more flexibility, eg
34  * borders and shadows can be used. Hiero is able to avoid outputting the same glyph image more than once if multiple character
35  * codes have the exact same glyph.
36  * @author mattdesl AKA davedes */
37 public class BitmapFontWriter {
38 
39 	/** The output format. */
40 	public static enum OutputFormat {
41 
42 		/** AngelCodeFont text format */
43 		Text,
44 		/** AngelCodeFont XML format */
45 		XML;
46 	}
47 
48 	/** The output format */
49 	private static OutputFormat format = OutputFormat.Text;
50 
51 	/** Sets the AngelCodeFont output format for subsequent writes; can be text (for LibGDX) or XML (for other engines, like
52 	 * Pixi.js).
53 	 *
54 	 * @param fmt the output format to use */
setOutputFormat(OutputFormat fmt)55 	public static void setOutputFormat (OutputFormat fmt) {
56 		if (fmt == null) throw new NullPointerException("format cannot be null");
57 		format = fmt;
58 	}
59 
60 	/** Returns the currently used output format.
61 	 * @return the output format */
getOutputFormat()62 	public static OutputFormat getOutputFormat () {
63 		return format;
64 	}
65 
66 	/** The Padding parameter for FontInfo. */
67 	public static class Padding {
68 		public int up, down, left, right;
69 
Padding()70 		public Padding () {
71 		}
72 
Padding(int up, int down, int left, int right)73 		public Padding (int up, int down, int left, int right) {
74 			this.up = up;
75 			this.down = down;
76 			this.left = left;
77 			this.right = right;
78 		}
79 	}
80 
81 	/** The spacing parameter for FontInfo. */
82 	public static class Spacing {
83 		public int horizontal, vertical;
84 	}
85 
86 	/** The font "info" line; everything except padding is ignored by LibGDX's BitmapFont reader, it is otherwise just useful for
87 	 * clean and organized output. */
88 	public static class FontInfo {
89 		/** Face name */
90 		public String face;
91 		/** Font size (pt) */
92 		public int size = 12;
93 		/** Whether the font is bold */
94 		public boolean bold;
95 		/** Whether the font is italic */
96 		public boolean italic;
97 		/** The charset; or null/empty for default */
98 		public String charset;
99 		/** Whether the font uses unicode glyphs */
100 		public boolean unicode = true;
101 		/** Stretch for height; default to 100% */
102 		public int stretchH = 100;
103 		/** Whether smoothing is applied */
104 		public boolean smooth = true;
105 		/** Amount of anti-aliasing that was applied to the font */
106 		public int aa = 2;
107 		/** Padding that was applied to the font */
108 		public Padding padding = new Padding();
109 		/** Horizontal/vertical spacing that was applied to font */
110 		public Spacing spacing = new Spacing();
111 		public int outline = 0;
112 
FontInfo()113 		public FontInfo () {
114 		}
115 
FontInfo(String face, int size)116 		public FontInfo (String face, int size) {
117 			this.face = face;
118 			this.size = size;
119 		}
120 	}
121 
quote(Object params)122 	private static String quote (Object params) {
123 		return quote(params, false);
124 	}
125 
quote(Object params, boolean spaceAfter)126 	private static String quote (Object params, boolean spaceAfter) {
127 		if (BitmapFontWriter.getOutputFormat() == OutputFormat.XML)
128 			return "\"" + params.toString().trim() + "\"" + (spaceAfter ? " " : "");
129 		else
130 			return params.toString();
131 	}
132 
133 	/** Writes the given BitmapFontData to a file, using the specified <tt>pageRefs</tt> strings as the image paths for each
134 	 * texture page. The glyphs in BitmapFontData have a "page" id, which references the index of the pageRef you specify here.
135 	 *
136 	 * The FontInfo parameter is useful for cleaner output; such as including a size and font face name hint. However, it can be
137 	 * null to use default values. LibGDX ignores most of the "info" line when reading back fonts, only padding is used. Padding
138 	 * also affects the size, location, and offset of the glyphs that are output.
139 	 *
140 	 * Likewise, the scaleW and scaleH are only for cleaner output. They are currently ignored by LibGDX's reader. For maximum
141 	 * compatibility with other BMFont tools, you should use the width and height of your texture pages (each page should be the
142 	 * same size).
143 	 *
144 	 * @param fontData the bitmap font
145 	 * @param pageRefs the references to each texture page image file, generally in the same folder as outFntFile
146 	 * @param outFntFile the font file to save to (typically ends with '.fnt')
147 	 * @param info the optional info for the file header; can be null
148 	 * @param scaleW the width of your texture pages
149 	 * @param scaleH the height of your texture pages */
writeFont(BitmapFontData fontData, String[] pageRefs, FileHandle outFntFile, FontInfo info, int scaleW, int scaleH)150 	public static void writeFont (BitmapFontData fontData, String[] pageRefs, FileHandle outFntFile, FontInfo info, int scaleW,
151 		int scaleH) {
152 		if (info == null) {
153 			info = new FontInfo();
154 			info.face = outFntFile.nameWithoutExtension();
155 		}
156 
157 		int lineHeight = (int)fontData.lineHeight;
158 		int pages = pageRefs.length;
159 		int packed = 0;
160 		int base = (int)((fontData.capHeight) + (fontData.flipped ? -fontData.ascent : fontData.ascent));
161 		OutputFormat fmt = BitmapFontWriter.getOutputFormat();
162 		boolean xml = fmt == OutputFormat.XML;
163 
164 		StringBuilder buf = new StringBuilder();
165 
166 		if (xml) {
167 			buf.append("<font>\n");
168 		}
169 		String xmlOpen = xml ? "\t<" : "";
170 		String xmlCloseSelf = xml ? "/>" : "";
171 		String xmlTab = xml ? "\t" : "";
172 		String xmlClose = xml ? ">" : "";
173 
174 		String xmlQuote = xml ? "\"" : "";
175 		String alphaChnlParams = xml ? " alphaChnl=\"0\" redChnl=\"0\" greenChnl=\"0\" blueChnl=\"0\""
176 			: " alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0";
177 
178 		// INFO LINE
179 		buf.append(xmlOpen).append("info face=\"").append(info.face == null ? "" : info.face.replaceAll("\"", "'"))
180 			.append("\" size=").append(quote(info.size)).append(" bold=").append(quote(info.bold ? 1 : 0)).append(" italic=")
181 			.append(quote(info.italic ? 1 : 0)).append(" charset=\"").append(info.charset == null ? "" : info.charset)
182 			.append("\" unicode=").append(quote(info.unicode ? 1 : 0)).append(" stretchH=").append(quote(info.stretchH))
183 			.append(" smooth=").append(quote(info.smooth ? 1 : 0)).append(" aa=").append(quote(info.aa)).append(" padding=")
184 			.append(xmlQuote).append(info.padding.up).append(",").append(info.padding.down).append(",").append(info.padding.left)
185 			.append(",").append(info.padding.right).append(xmlQuote).append(" spacing=").append(xmlQuote)
186 			.append(info.spacing.horizontal).append(",").append(info.spacing.vertical).append(xmlQuote).append(xmlCloseSelf)
187 			.append("\n");
188 
189 		// COMMON line
190 		buf.append(xmlOpen).append("common lineHeight=").append(quote(lineHeight)).append(" base=").append(quote(base))
191 			.append(" scaleW=").append(quote(scaleW)).append(" scaleH=").append(quote(scaleH)).append(" pages=").append(quote(pages))
192 			.append(" packed=").append(quote(packed)).append(alphaChnlParams).append(xmlCloseSelf).append("\n");
193 
194 		if (xml) buf.append("\t<pages>\n");
195 
196 		// PAGES
197 		for (int i = 0; i < pageRefs.length; i++) {
198 			buf.append(xmlTab).append(xmlOpen).append("page id=").append(quote(i)).append(" file=\"").append(pageRefs[i])
199 				.append("\"").append(xmlCloseSelf).append("\n");
200 		}
201 
202 		if (xml) buf.append("\t</pages>\n");
203 
204 		// CHARS
205 		Array<Glyph> glyphs = new Array<Glyph>(256);
206 		for (int i = 0; i < fontData.glyphs.length; i++) {
207 			if (fontData.glyphs[i] == null) continue;
208 
209 			for (int j = 0; j < fontData.glyphs[i].length; j++) {
210 				if (fontData.glyphs[i][j] != null) {
211 					glyphs.add(fontData.glyphs[i][j]);
212 				}
213 			}
214 		}
215 
216 		buf.append(xmlOpen).append("chars count=").append(quote(glyphs.size)).append(xmlClose).append("\n");
217 
218 		int padLeft = 0, padRight = 0, padTop = 0, padX = 0, padY = 0;
219 		if (info != null) {
220 			padTop = info.padding.up;
221 			padLeft = info.padding.left;
222 			padRight = info.padding.right;
223 			padX = padLeft + padRight;
224 			padY = info.padding.up + info.padding.down;
225 		}
226 
227 		// CHAR definitions
228 		for (int i = 0; i < glyphs.size; i++) {
229 			Glyph g = glyphs.get(i);
230 			boolean empty = g.width == 0 || g.height == 0;
231 			buf.append(xmlTab).append(xmlOpen).append("char id=").append(quote(String.format("%-6s", g.id), true)).append("x=")
232 				.append(quote(String.format("%-5s", empty ? 0 : g.srcX - padLeft), true)).append("y=")
233 				.append(quote(String.format("%-5s", empty ? 0 : g.srcY - padRight), true)).append("width=")
234 				.append(quote(String.format("%-5s", empty ? 0 : g.width + padX), true)).append("height=")
235 				.append(quote(String.format("%-5s", empty ? 0 : g.height + padY), true)).append("xoffset=")
236 				.append(quote(String.format("%-5s", g.xoffset - padLeft), true)).append("yoffset=")
237 				.append(
238 					quote(String.format("%-5s", fontData.flipped ? g.yoffset + padTop : -(g.height + (g.yoffset + padTop))), true))
239 				.append("xadvance=").append(quote(String.format("%-5s", g.xadvance), true)).append("page=")
240 				.append(quote(String.format("%-5s", g.page), true)).append("chnl=").append(quote(0, true)).append(xmlCloseSelf)
241 				.append("\n");
242 		}
243 
244 		if (xml) buf.append("\t</chars>\n");
245 
246 		// KERNINGS
247 		int kernCount = 0;
248 		StringBuilder kernBuf = new StringBuilder();
249 		for (int i = 0; i < glyphs.size; i++) {
250 			for (int j = 0; j < glyphs.size; j++) {
251 				Glyph first = glyphs.get(i);
252 				Glyph second = glyphs.get(j);
253 				int kern = first.getKerning((char)second.id);
254 				if (kern != 0) {
255 					kernCount++;
256 					kernBuf.append(xmlTab).append(xmlOpen).append("kerning first=").append(quote(first.id)).append(" second=")
257 						.append(quote(second.id)).append(" amount=").append(quote(kern, true)).append(xmlCloseSelf).append("\n");
258 				}
259 			}
260 		}
261 
262 		// KERN info
263 		buf.append(xmlOpen).append("kernings count=").append(quote(kernCount)).append(xmlClose).append("\n");
264 		buf.append(kernBuf);
265 
266 		if (xml) {
267 			buf.append("\t</kernings>\n");
268 			buf.append("</font>");
269 		}
270 
271 		String charset = info.charset;
272 		if (charset != null && charset.length() == 0) charset = null;
273 
274 		outFntFile.writeString(buf.toString(), false, charset);
275 	}
276 
277 	/** A utility method which writes the given font data to a file.
278 	 *
279 	 * The specified pixmaps are written to the parent directory of <tt>outFntFile</tt>, using that file's name without an
280 	 * extension for the PNG file name(s).
281 	 *
282 	 * The specified FontInfo is optional, and can be null.
283 	 *
284 	 * Typical usage looks like this:
285 	 *
286 	 * <pre>
287 	 * BitmapFontWriter.writeFont(myFontData, myFontPixmaps, Gdx.files.external(&quot;fonts/output.fnt&quot;), new FontInfo(&quot;Arial&quot;, 16));
288 	 * </pre>
289 	 *
290 	 * @param fontData the font data
291 	 * @param pages the pixmaps to write as PNGs
292 	 * @param outFntFile the output file for the font definition
293 	 * @param info the optional font info for the header file, can be null */
writeFont(BitmapFontData fontData, Pixmap[] pages, FileHandle outFntFile, FontInfo info)294 	public static void writeFont (BitmapFontData fontData, Pixmap[] pages, FileHandle outFntFile, FontInfo info) {
295 		String[] pageRefs = writePixmaps(pages, outFntFile.parent(), outFntFile.nameWithoutExtension());
296 
297 		// write the font data
298 		writeFont(fontData, pageRefs, outFntFile, info, pages[0].getWidth(), pages[0].getHeight());
299 	}
300 
301 	/** A utility method to write the given array of pixmaps to the given output directory, with the specified file name. If the
302 	 * pages array is of length 1, then the resulting file ref will look like: "fileName.png".
303 	 *
304 	 * If the pages array is greater than length 1, the resulting file refs will be appended with "_N", such as "fileName_0.png",
305 	 * "fileName_1.png", "fileName_2.png" etc.
306 	 *
307 	 * The returned string array can then be passed to the <tt>writeFont</tt> method.
308 	 *
309 	 * Note: None of the pixmaps will be disposed.
310 	 *
311 	 * @param pages the pages of pixmap data to write
312 	 * @param outputDir the output directory
313 	 * @param fileName the file names for the output images
314 	 * @return the array of string references to be used with <tt>writeFont</tt> */
writePixmaps(Pixmap[] pages, FileHandle outputDir, String fileName)315 	public static String[] writePixmaps (Pixmap[] pages, FileHandle outputDir, String fileName) {
316 		if (pages == null || pages.length == 0) throw new IllegalArgumentException("no pixmaps supplied to BitmapFontWriter.write");
317 
318 		String[] pageRefs = new String[pages.length];
319 
320 		for (int i = 0; i < pages.length; i++) {
321 			String ref = pages.length == 1 ? (fileName + ".png") : (fileName + "_" + i + ".png");
322 
323 			// the ref for this image
324 			pageRefs[i] = ref;
325 
326 			// write the PNG in that directory
327 			PixmapIO.writePNG(outputDir.child(ref), pages[i]);
328 		}
329 		return pageRefs;
330 	}
331 
332 	/** A convenience method to write pixmaps by page; typically returned from a PixmapPacker when used alongside
333 	 * FreeTypeFontGenerator.
334 	 *
335 	 * @param pages the pages containing the Pixmaps
336 	 * @param outputDir the output directory
337 	 * @param fileName the file name
338 	 * @return the file refs */
writePixmaps(Array<Page> pages, FileHandle outputDir, String fileName)339 	public static String[] writePixmaps (Array<Page> pages, FileHandle outputDir, String fileName) {
340 		Pixmap[] pix = new Pixmap[pages.size];
341 		for (int i = 0; i < pages.size; i++) {
342 			pix[i] = pages.get(i).getPixmap();
343 		}
344 		return writePixmaps(pix, outputDir, fileName);
345 	}
346 }
347