1 /*
2  * Copyright (C) 2015-2017, by Laszlo Szeremi under the Boost license.
3  *
4  * Pixel Perfect Engine, graphics.fontsets module
5  */
6 
7 module pixelperfectengine.graphics.fontsets;
8 public import pixelperfectengine.graphics.bitmap;
9 public import pixelperfectengine.system.exc;
10 import bmfont;
11 import dimage;
12 //public import pixelperfectengine.system.binarySearchTree;
13 import collections.treemap;
14 import collections.sortedlist;
15 static import std.stdio;
16 /**
17  * Stores the letters and all the data associated with the font, also has functions related to text lenght and line formatting. Supports variable letter width.
18  * TODO: Add ability to load from dpk files through the use of vfiles.
19  */
20 public class Fontset(T)
21 		if(T.stringof == Bitmap8Bit.stringof || T.stringof == Bitmap16Bit.stringof || T.stringof == Bitmap32Bit.stringof) {
22 	//If the kerning map will cost too much memory etc, this will be used instead.
23 	protected struct KerningInfo {
24 	align(2):
25 		dchar		a;
26 		dchar		b;
27 		short		amount;
28 	}
29 	//public Font 						fontinfo;	///BMFont information on drawing the letters (might be removed later on)
30 	//alias CharMap = TreeMap!(dchar, Font.Char, true);
31 	alias CharMap = Font.Char[];
32 	/* alias KerningMapB = TreeMap!(dchar, short, true);
33 	alias KerningMap = TreeMap!(dchar, KerningMapB, true); */
34 	alias KerningMap = KerningInfo[];
35 	protected CharMap					_chars;		///Contains character information in a fast lookup form
36 	protected KerningMap				_kerning;	///Contains kerning information
37 	public T[] 							pages;		///Character pages
38 	private Fontset!T[]					fallbacks;	///Provides fallbacks to other fontsets in case of missing characters
39 	private string 						name;		///Name of the font
40 	private int							_size;		///Height of the font
41 	///Empty constructor, primarily for testing purposes
42 	public this () {}
43 	/**
44 	 * Loads a fontset from disk.
45 	 */
46 	public this (std.stdio.File file, string basepath = "") {
47 		import std.path : extension;
48 		import bitleveld.reinterpret;
49 		
50 		ubyte[] buffer;
51 		buffer.length = cast(size_t)file.size;
52 		file.rawRead(buffer);
53 		Font fontinfo = parseFnt(buffer);
54 		/* foreach(ch; fontinfo.chars){
55 			_chars[ch.id] = ch;
56 		} */
57 		_chars = fontinfo.chars;
58 		foreach(krn; fontinfo.kernings){
59 			_kerning ~= KerningInfo(krn.first, krn.second, krn.amount);
60 			/* KerningMapB* mapB = _kerning.ptrOf(krn.first);
61 			if (mapB !is null) {
62 				(*mapB)[krn.second] = krn.amount;
63 			} else {
64 				KerningMapB newMap;
65 				newMap[krn.second] = krn.amount;
66 				_kerning[krn.first] = newMap;
67 			} */
68 			//_kerning[krn.first][krn.second] = krn.amount;
69 		}
70 		_size = fontinfo.info.fontSize;
71 		name = fontinfo.info.fontName;
72 		foreach (path; fontinfo.pages) {
73 			std.stdio.File pageload = std.stdio.File(basepath ~ path);
74 			switch (extension(path)) {
75 				case ".tga", ".TGA":
76 					TGA fontPage = TGA.load(pageload);
77 					if(!fontPage.getHeader.topOrigin){
78 						fontPage.flipVertical;
79 					}
80 					static if (T.stringof == Bitmap8Bit.stringof) {
81 						if(fontPage.getBitdepth != 8)
82 							throw new BitmapFormatException("Bitdepth mismatch exception!");
83 						pages ~= new Bitmap8Bit(fontPage.imageData.raw, fontPage.width, fontPage.height);
84 					} else static if (T.stringof == Bitmap16Bit.stringof) {
85 						if(fontPage.getBitdepth != 16)
86 							throw new BitmapFormatException("Bitdepth mismatch exception!");
87 						pages ~= new Bitmap16Bit(reinterpretCast!ushort(fontPage.imageData.raw), fontPage.width, fontPage.height);
88 					} else static if (T.stringof == Bitmap32Bit.stringof) {
89 						if(fontPage.getBitdepth != 32)
90 							throw new BitmapFormatException("Bitdepth mismatch exception!");
91 						pages ~= new Bitmap32Bit(reinterpretCast!Color(fontPage.imageData.raw), fontPage.width, fontPage.height);
92 					}
93 					break;
94 				case ".png", ".PNG":
95 					PNG fontPage = PNG.load(pageload);
96 					static if(T.stringof == Bitmap8Bit.stringof) {
97 						if(fontPage.getBitdepth != 8)
98 							throw new BitmapFormatException("Bitdepth mismatch exception!");
99 						pages ~= new Bitmap8Bit(fontPage.imageData.raw, fontPage.width, fontPage.height);
100 					} else static if(T.stringof == Bitmap32Bit.stringof) {
101 						if(fontPage.getBitdepth != 32)
102 							throw new BitmapFormatException("Bitdepth mismatch exception!");
103 						pages ~= new Bitmap32Bit(reinterpretCast!Color(fontPage.imageData.raw), fontPage.width, fontPage.height);
104 					}
105 					break;
106 				case ".bmp", ".BMP":
107 					BMP fontPage = BMP.load(pageload);
108 					static if(T.stringof == Bitmap8Bit.stringof) {
109 						if(fontPage.getBitdepth != 8)
110 							throw new BitmapFormatException("Bitdepth mismatch exception!");
111 						pages ~= new Bitmap8Bit(fontPage.imageData.raw, fontPage.width, fontPage.height);
112 					} else static if(T.stringof == Bitmap32Bit.stringof) {
113 						if(fontPage.getBitdepth != 32)
114 							throw new BitmapFormatException("Bitdepth mismatch exception!");
115 						pages ~= new Bitmap32Bit(fontPage.imageData.raw, fontPage.width, fontPage.height);
116 					}
117 					break;
118 				default:
119 					throw new Exception("Unsupported file format!");
120 			}
121 		}
122 	}
123 	///Returns the name of the font
124 	public string getName() @nogc @safe nothrow pure @property const {
125 		return name;
126 	}
127 	/* ///Returns the height of the font.
128 	///WILL BE DEPRECATED!
129 	public deprecated int getSize() @nogc @safe nothrow pure const {
130 		return _size;
131 	} */
132 	///returns the height of the font.
133 	public int size() @nogc @safe nothrow pure @property const {
134 		return _size;
135 	}
136 	///Returns the width of the text in pixels.
137 	public int getTextLength(dstring text) @nogc @safe nothrow pure {
138 		int length;
139 		foreach(c; text){
140 			length += chars(c).xadvance;
141 		}
142 		return length;
143 	}
144 	/**
145 	 * Returns the character info if present, or a substitute from either a fallback font if it found in them or 
146 	 * the default substitute character (0xFFFD)
147 	 */
148 	public Font.Char chars(dchar c) @nogc @trusted nothrow pure {
149 		foreach (key; _chars) {
150 			if (key.id == c)
151 				return key;
152 		}
153 		assert(c != 0xFFFD);
154 		foreach(fntSt ; fallbacks) {
155 			Font.Char result = fntSt.chars(c);
156 			if(result.id != dchar.init) return result;
157 		}
158 		
159 		return chars(0xFFFD);
160 	}
161 	/**
162 	 * Returns the kerning for the given character pair if there's any, or 0.
163 	 * Should be called through CharacterFormattingInfo, which can bypass it if formatting flag is enabled.
164 	 */
165 	public final short getKerning(const dchar first, const dchar second) @nogc @safe pure nothrow {
166 		foreach (KerningInfo key ; _kerning) {
167 			if (key.a == first && key.b == second)
168 				return key.amount;
169 		}
170 		return 0;
171 	}
172 	/**
173 	 * Breaks the input text into multiple lines according to the parameters.
174 	 */
175 	public dstring[] breakTextIntoMultipleLines(dstring input, int maxWidth, bool ignoreNewLineChars = false){
176 		dstring[] output;
177 		dstring currentWord, currentLine;
178 		int currentWordLength, currentLineLength;
179 
180 		foreach(character; input){
181 			currentWordLength += chars(character).xadvance;
182 			if(!ignoreNewLineChars && currentWordLength && (character == FormattingCharacters.newLine || 
183 						character == FormattingCharacters.carriageReturn || character == FormattingCharacters.newParagraph)) {
184 				//initialize new line on symbols indicating new lines
185 				if(currentWordLength + currentLineLength <= maxWidth){
186 					currentLine ~= currentWord;
187 					output ~= currentLine;
188 				}else{
189 					output ~= currentLine;
190 					currentLine = currentWord;
191 				}
192 				currentLine.length = 0;
193 				currentLineLength = 0;
194 				currentWord.length = 0;
195 				currentWordLength = 0;
196 			}else if(character == FormattingCharacters.space){
197 				//add new word to the current line if it has enough space, otherwise break the line and initialize next one
198 				if(currentWordLength + currentLineLength <= maxWidth){
199 					currentLineLength += currentWordLength;
200 					currentLine ~= currentWord ~ ' ';
201 				}else{
202 					output ~= currentLine;
203 				}
204 			}else{
205 				if(currentWordLength > maxWidth){	//Flush current word if it will be too long for a single line
206 					output ~= currentWord;
207 					currentLine.length = 0;
208 					currentWordLength = 0;
209 				}
210 				currentWord ~= character;
211 
212 			}
213 		}
214 
215 		return output;
216 	}
217 }
218 /**
219  * Specifies formatting flags.
220  * They usually can be mixed with others, see documentation for further info.
221  */
222 public enum FormattingFlags : uint {
223 	reset				=	0b0000_0000_0000_0000_0000_0000_0000_0000,
224 	//Mask used for detecting complex formatting flags
225 	justifyMask			=	0b0000_0000_0000_0000_0000_0000_0000_0111,
226 	ulMask				=	0b0000_0000_0000_0000_0000_0000_0111_0000,
227 	ulLineMultiplier	=	0b0000_0000_0000_0000_0000_0001_1000_0000,
228 	ulLineStyle			=	0b0000_0000_0000_0000_0000_1110_0000_0000,
229 	forceItalicsMask	=	0b0000_0000_0000_0000_1100_0000_0000_0000,
230 
231 	leftJustify			=	0b0000_0000_0000_0000_0000_0000_0000_0000,
232 	rightJustify		=	0b0000_0000_0000_0000_0000_0000_0000_0001,
233 	centerJustify		=	0b0000_0000_0000_0000_0000_0000_0000_0010,
234 	fillEntireLine		=	0b0000_0000_0000_0000_0000_0000_0000_0100,
235 	slHorizCenter		=	0b0000_0000_0000_0000_0000_0000_0000_1000,
236 
237 	underline			=	0b0000_0000_0000_0000_0000_0000_0001_0000,
238 	underlinePerWord	=	0b0000_0000_0000_0000_0000_0000_0010_0000,
239 
240 	underlineDouble		=	0b0000_0000_0000_0000_0000_0000_1000_0000,
241 	underlineTriple		=	0b0000_0000_0000_0000_0000_0001_0000_0000,
242 	underlineQuadruple	=	0b0000_0000_0000_0000_0000_0001_1000_0000,
243 
244 	underlineDotted		=	0b0000_0000_0000_0000_0000_0010_0000_0000,
245 	underlineWavy		=	0b0000_0000_0000_0000_0000_0100_0000_0000,
246 	underlineWavySoft	=	0b0000_0000_0000_0000_0000_0110_0000_0000,
247 	underlineStripes	=	0b0000_0000_0000_0000_0000_1000_0000_0000,
248 
249 	overline			=	0b0000_0000_0000_0000_0001_0000_0000_0000,
250 	strikeThrough		=	0b0000_0000_0000_0000_0010_0000_0000_0000,
251 	//Forced italic fonts. The upper portions of the letters get shifted to the right by a certain amount set by the flags.
252 	//If all clear, then the text will be displayed normally from its bitmap form.
253 	forceItalics1per4	=	0b0000_0000_0000_0000_0100_0000_0000_0000,
254 	forceItalics1per3	=	0b0000_0000_0000_0000_1000_0000_0000_0000,
255 	forceItalics1per2	=	0b0000_0000_0000_0000_1100_0000_0000_0000,
256 
257 	disableKerning		=	0b0000_0000_0000_0001_0000_0000_0000_0000,
258 }
259 /**
260  * Stores character formatting info.
261  */
262 public class CharacterFormattingInfo(T) {
263 	public Fontset!T		font;		///The type of the font
264 	static if(T.stringof == Bitmap8Bit.stringof)
265 		public ubyte			color;		///The displayed color
266 	else static if(T.stringof == Bitmap16Bit.stringof)
267 		public ushort			color;		///The displayed color
268 	else static if(T.stringof == Bitmap32Bit.stringof)
269 		public Color			color;		///The displayed color
270 	public uint				formatFlags;	///Styleflags to be set for different purposes (eg, orientation, understrike style)
271 	public ushort			paragraphSpace;	///The space between paragraphs in pixels
272 	public short			rowHeight;		///Modifies the space between rows of text within a single formatting unit
273 	public ushort			offsetV;		///Upper-part offseting. The amount of lines which should be skipped (single line), or moved upwards (multi line)
274 	static if(T.stringof == Bitmap8Bit.stringof) {
275 		/**
276 		 * Creates character formatting from the supplied data.
277 		 * Params:
278 		 *   font = The fontset to be used for the formatting.
279 		 *   color = The color of the text.
280 		 *   formatFlags = Formatting flags, some combination of FormattingFlags ORed together.
281 		 *   paragraphSpace = Spaces between new paragraphs.
282 		 *   rowHeight = Total height of a row (fontsize + space between lines).
283 		 *   offsetV = Vertical offset on singleline texts.
284 		 */
285 		public this(Fontset!T font, ubyte color, uint formatFlags, ushort paragraphSpace, short rowHeight, ushort offsetV) 
286 				@safe {
287 			this.font = font;
288 			this.color = color;
289 			this.formatFlags = formatFlags;
290 			this.paragraphSpace = paragraphSpace;
291 			this.rowHeight = rowHeight;
292 			this.offsetV = offsetV;
293 		}
294 	}
295 	///Copy constructor
296 	public this(CharacterFormattingInfo!T source) @safe {
297 		this.font = source.font;
298 		this.color = source.color;
299 		this.formatFlags = source.formatFlags;
300 		this.paragraphSpace = source.paragraphSpace;
301 		this.rowHeight = source.rowHeight;
302 		this.offsetV = source.offsetV;
303 	}
304 	/**
305 	 * Returns the kerning amount, or zero if disabled by formatting.
306 	 */
307 	public short getKerning(const dchar first, const dchar second) @nogc @safe pure nothrow {
308 		if (!(formatFlags & FormattingFlags.disableKerning)) 
309 			return font.getKerning(first, second);
310 		else 
311 			return short.init;
312 	}
313 	/**
314 	 * Checks if two instances hold the same character formatting information.
315 	 */
316 	public bool opEquals(CharacterFormattingInfo!T rhs) {
317 		return this.color == rhs.color && this.font == rhs.font && this.formatFlags == rhs.formatFlags &&
318 				this.paragraphSpace == rhs.paragraphSpace && this.rowHeight == rhs.rowHeight;
319 	}
320 	public int getItalicsAm() @nogc @safe pure nothrow {
321 		switch (formatFlags & FormattingFlags.forceItalicsMask) {
322 			case FormattingFlags.forceItalics1per2:
323 				return 2;
324 			case FormattingFlags.forceItalics1per3:
325 				return 3;
326 			case FormattingFlags.forceItalics1per4:
327 				return 4;
328 			default:
329 				return 1;
330 		}
331 	}
332 	alias opEquals = Object.opEquals;
333 }
334 ///Defines formatting characters.
335 ///DC1 is repurposed to initialize binary embedded CharacterFormattingInfo.
336 ///DC2 is repurposed to initialize length of formatted text blocks.
337 public enum FormattingCharacters : dchar{
338 	horizontalTab	=	0x09,
339 	newLine			=	0x0A,
340 	newParagraph	=	0x0B,
341 	carriageReturn	=	0x0D,
342 	binaryDLE		=	0x10,		///
343 	binaryCFI		=	0x11,		///Use DC1 to initialize binary embedded CFI
344 	binaryLI		=	0x12,		///Use DC2 to initialize binary length indicator of text blocks in bytes
345 	space			=	0x20,
346 }
347 ///Defines what kind of encoding the text use
348 public enum TextType : ubyte {
349 	ASCII		=	1,
350 	UTF8		=	2,
351 	UTF16		=	3,
352 	UTF32		=	4,
353 }