.
1 /*
2 Copyright (C) 2014 2015 Johan Mattsson
3
4 This library is free software; you can redistribute it and/or modify
5 it under the terms of the GNU Lesser General Public License as
6 published by the Free Software Foundation; either version 3 of the
7 License, or (at your option) any later version.
8
9 This library is distributed in the hope that it will be useful, but
10 WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Lesser General Public License for more details.
13 */
14
15 using Cairo;
16 using Math;
17
18 namespace BirdFont {
19
20 /** Test implementation of a birdfont rendering engine. */
21 public class Text : Widget {
22 FontCache font_cache;
23 public CachedFont cached_font;
24
25 Surface? cache = null;
26
27 public string text;
28
29 GlyphSequence glyph_sequence {
30 get {
31 if (gs == null) {
32 gs = generate_glyphs ();
33 }
34
35 return (!) gs;
36 }
37 }
38
39 Gee.ArrayList<string> glyph_names;
40 GlyphSequence? gs = null;
41
42 public delegate void Iterator (Glyph glyph, double kerning, bool last);
43 public double font_size;
44 double sidebearing_extent = 0;
45
46 public double r = 0;
47 public double g = 0;
48 public double b = 0;
49 public double a = 1;
50 double truncated_width = -1;
51
52 public Text (string text = "", double size = 17, double margin_bottom = 0) {
53 this.margin_bottom = margin_bottom;
54 font_cache = FontCache.get_default_cache ();
55 cached_font = font_cache.get_fallback ();
56
57 set_text (text);
58 set_font_size (size);
59 }
60
61 public string get_text () {
62 return text;
63 }
64
65 /** Set font for this text area.
66 * @param font_absolute path to the font file or a file name for one of the font files in search paths.
67 * @return true if the font was found
68 */
69 public bool load_font (string font_file) {
70 File path;
71 File f;
72 FontCache fc;
73
74 f = File.new_for_path (font_file);
75 path = (f.query_exists ()) ? f : SearchPaths.find_file (null, font_file);
76
77 fc = FontCache.get_default_cache ();
78 cached_font = fc.get_font ((!) path.get_path ());
79 gs = generate_glyphs ();
80
81 return cached_font.font != null;
82 }
83
84 public void set_font_size (double height_in_pixels) {
85 font_size = height_in_pixels;
86 sidebearing_extent = 0;
87
88 if (gs == null) { // ensure height is loaded for the font
89 gs = generate_glyphs ();
90 }
91 }
92
93 public void set_font_cache (FontCache font_cache) {
94 this.font_cache = font_cache;
95 }
96
97 public void set_text (string text) {
98 this.text = text;
99 gs = null;
100 sidebearing_extent = 0;
101 }
102
103 private GlyphSequence generate_glyphs () {
104 int index;
105 unichar c;
106 string name;
107 Glyph? g;
108 GlyphSequence gs;
109
110 gs = new GlyphSequence ();
111
112 glyph_names = new Gee.ArrayList<string> ();
113 index = 0;
114 while (text.get_next_char (ref index, out c)) {
115 name = (!) c.to_string ();
116 g = cached_font.get_glyph_by_name (name);
117
118 gs.glyph.add (g);
119 glyph_names.add (name);
120 }
121
122 return gs;
123 }
124
125 public void iterate (Iterator iter) {
126 Glyph glyph;
127 double w, kern;
128 int wi;
129 Glyph? prev;
130 Glyph? g;
131 GlyphSequence word_with_ligatures;
132 GlyphRange? gr_left, gr_right;
133 GlyphSequence word;
134 KerningClasses kc;
135 Font empty = Font.empty;
136
137 glyph = new Glyph.no_lines ("", '\0');
138
139 w = 0;
140 prev = null;
141 kern = 0;
142
143 word = glyph_sequence;
144 wi = 0;
145
146 if (cached_font.font != null) {
147 word_with_ligatures = word.process_ligatures ((!) cached_font.font);
148 } else {
149 word_with_ligatures = word.process_ligatures (new Font ());
150 }
151
152 gr_left = null;
153 gr_right = null;
154
155 if (cached_font.font != null) {
156 kc = ((!) cached_font.font).get_kerning_classes ();
157 } else {
158 kc = new KerningClasses (empty);
159 }
160
161 for (int i = 0; i < word_with_ligatures.glyph.size; i++) {
162 g = word_with_ligatures.glyph.get (i);
163
164 if (g == null || prev == null || wi == 0) {
165 kern = 0;
166 } else {
167 return_if_fail (wi < word_with_ligatures.ranges.size);
168 return_if_fail (wi - 1 >= 0);
169
170 gr_left = word_with_ligatures.ranges.get (wi - 1);
171 gr_right = word_with_ligatures.ranges.get (wi);
172
173 kern = kc.get_kerning_for_pair (((!) prev).get_name (), ((!) g).get_name (), gr_left, gr_right);
174 }
175
176 // process glyph
177 if (g == null && (0 <= i < glyph_names.size)) {
178 g = cached_font.get_glyph_by_name (glyph_names.get (i));
179 }
180
181 glyph = (g == null) ? new Glyph ("") : (!) g;
182 iter (glyph, kern, i + 1 == word_with_ligatures.glyph.size);
183 prev = g;
184 wi++;
185 }
186 }
187
188 // FIXME: some fonts doesn't have on curve extrema
189 public double get_extent () {
190 double x = 0;
191
192 iterate ((glyph, kerning, last) => {
193 double x1, y1, x2, y2;
194 double lsb;
195
196 lsb = glyph.left_limit;
197
198 if (!last) {
199 x += (glyph.get_width () + kerning) * get_scale (glyph);
200 } else {
201 glyph.boundaries (out x1, out y1, out x2, out y2);
202 x += (x2 - lsb) * get_scale (glyph);
203 }
204 });
205
206 return x;
207 }
208
209 public double get_sidebearing_extent () {
210 double x ;
211
212 if (likely (sidebearing_extent > 0)) {
213 return sidebearing_extent;
214 }
215
216 x = 0;
217
218 iterate ((glyph, kerning, last) => {
219 double lsb;
220 lsb = glyph.left_limit;
221 x += (glyph.get_width () + kerning) * get_scale (glyph);
222 });
223
224 sidebearing_extent = x;
225 return x;
226 }
227
228 public override double get_height () {
229 return font_size;
230 }
231
232 public double get_acender () {
233 double max_height = 0;
234
235 iterate ((glyph, kerning, last) => {
236 double x1, y1, x2, y2;
237 double h;
238 glyph.boundaries (out x1, out y1, out x2, out y2);
239 h = Math.fmax (y1, y2) - Math.fmin (y1, y2);
240 h *= get_scale(glyph) - glyph.baseline * get_scale(glyph);
241 if (h > max_height) {
242 max_height = h;
243 }
244 });
245
246 return max_height;
247 }
248
249 public override double get_width () {
250 double x = 0;
251 bool first = true;
252
253 iterate ((glyph, kerning, last) => {
254 double x1, y1, x2, y2;
255 double lsb;
256
257 lsb = glyph.left_limit;
258
259 if (first) {
260 glyph.boundaries (out x1, out y1, out x2, out y2);
261 x += (glyph.get_width () + kerning - Math.fmin (x1, x2)) * get_scale (glyph);
262 first = false;
263 } else if (!last) {
264 x += (glyph.get_width () + kerning) * get_scale (glyph);
265 } else {
266 glyph.boundaries (out x1, out y1, out x2, out y2);
267 x += (x2 - lsb) * get_scale (glyph);
268 }
269 });
270
271 return x;
272 }
273
274 public double get_decender () {
275 double decender_max = get_max_decender ();
276 return decender_max > 0 ? decender_max : 0;
277 }
278
279 private double get_max_decender () {
280 double decender = 0;
281 double decender_max = 0;
282
283 iterate ((glyph, kerning, last) => {
284 double x1, y1, x2, y2;
285 double y;
286 glyph.boundaries (out x1, out y1, out x2, out y2);
287 y = Math.fmin (y1, y2);
288 decender = (glyph.baseline - y) * get_scale (glyph);
289 if (decender > decender_max) {
290 decender_max = decender;
291 }
292 });
293
294 return decender_max;
295 }
296
297 public override void draw (Context cr) {
298 double descender = cached_font.bottom_limit + cached_font.base_line;
299 double y = widget_y + get_height () + get_font_scale () * descender; // FIXME:
300 draw_at_baseline (cr, widget_x, y);
301 }
302
303 public void draw_at_top (Context cr, double px, double py, string cacheid = "") {
304 double s = get_font_scale ();
305 double y = py + s * (cached_font.top_limit - cached_font.base_line);
306 draw_at_baseline (cr, px, y, cacheid);
307 }
308
309 public void set_source_rgba (double r, double g, double b, double a) {
310 this.r = r;
311 this.g = g;
312 this.b = b;
313 this.a = a;
314 }
315
316 public string get_cache_id (int offset_x, int offset_y) {
317 string key;
318 int64 c;
319
320 c = (((int64) (r * 255)) << 24)
321 | (((int64) (g * 255)) << 16)
322 | (((int64) (b * 255)) << 8)
323 | (((int64) (a * 255)) << 0);
324
325 // FIXME: use binary key
326 key = @"$font_size $c $offset_x $offset_y";
327
328 return key;
329 }
330
331 public void draw_at_baseline (Context cr, double px, double py, string cacheid = "") {
332 double x, y;
333 double ratio;
334 double cc_y;
335
336 if (cache == null) {
337 cache = draw_on_cache_surface (cacheid);
338 }
339
340 double s = get_font_scale ();
341 double cache_y = py - s * (cached_font.top_limit - cached_font.base_line);
342 cr.set_source_surface ((!) cache, (int) rint (px), (int) rint (cache_y));
343 cr.paint ();
344 }
345
346 Surface draw_on_cache_surface (string cacheid) {
347 double x, y;
348 double ratio;
349 double cc_y;
350 Context cr;
351 Surface cache_surface;
352 double screen_scale = Screen.get_scale();
353 double h = font_size * screen_scale + 1;
354 double w = get_sidebearing_extent () * screen_scale + 1;
355
356 cache_surface = Screen.create_background_surface ((int) w, (int) h);
357 cr = new Context (cache_surface);
358 cr.scale(screen_scale, screen_scale);
359
360 ratio = get_font_scale ();
361 cc_y = (cached_font.top_limit - cached_font.base_line) * ratio;
362
363 double px = 0;
364 double py = cc_y;
365
366 y = cc_y;
367 x = 0;
368
369 if (unlikely (cached_font.base_line != 0)) {
370 warning ("Base line not zero.");
371 }
372
373 iterate ((glyph, kerning, last) => {
374 double end;
375
376 x += kerning * ratio;
377 end = x + glyph.get_width () * ratio;
378
379 // truncation
380 if (truncated_width > 0 && end - px > truncated_width) {
381 return;
382 }
383
384 draw_chached (cr, glyph, kerning, last, x, y, cc_y,
385 ratio, cacheid);
386
387 x = end;
388 });
389
390 return cache_surface;
391 }
392
393 void draw_without_cache (Context cr, Glyph glyph, double kerning, bool last,
394 double x, double y, double cc_y, double ratio) {
395
396 double lsb;
397
398 cr.save ();
399 cr.set_source_rgba (r, g, b, a);
400 cr.new_path ();
401
402 lsb = glyph.left_limit;
403
404 foreach (Path path in glyph.get_visible_paths ()) {
405 draw_path (cr, glyph, path, lsb, x, y, ratio);
406 }
407
408 cr.fill ();
409 cr.restore ();
410
411 }
412
413 void draw_chached (Context cr, Glyph glyph, double kerning, bool last,
414 double x, double y, double cc_y, double ratio,
415 string cacheid = "") {
416
417 double lsb;
418 Surface cache;
419 Context cc;
420 string cache_id;
421 double xp = x;
422 double yp = y - cc_y;
423 int offset_x, offset_y;
424
425 offset_x = (int) (10 * (xp - (int) xp));
426 offset_y = (int) (10 * (yp - (int) yp));
427
428 cache_id = (cacheid == "") ? get_cache_id (offset_x, offset_y) : cacheid;
429
430 if (unlikely (!glyph.has_cache (cache_id))) {
431 int w = (int) (glyph.get_width () * ratio) + 2;
432 int h = (int) font_size + 2;
433 cache = Screen.create_background_surface (w, h);
434 cc = new Context (cache);
435
436 cc.scale(Screen.get_scale (), Screen.get_scale ());
437
438 lsb = glyph.left_limit;
439
440 cc.save ();
441 cc.set_source_rgba (r, g, b, a);
442 cc.new_path ();
443
444 foreach (Path path in glyph.get_visible_paths ()) {
445 draw_path (cc, glyph, path, lsb, offset_x / 10.0, cc_y + offset_y / 10.0, ratio);
446 }
447
448 cc.fill ();
449 cc.restore ();
450
451 glyph.set_cache (cache_id, cache);
452 }
453
454 cr.save ();
455 cr.set_antialias (Cairo.Antialias.NONE);
456 cr.scale(1 / Screen.get_scale (), 1 / Screen.get_scale ());
457 cr.set_source_surface (glyph.get_cache (cache_id),
458 (int) (xp * Screen.get_scale ()),
459 (int) (yp * Screen.get_scale ()));
460 cr.paint ();
461 cr.restore ();
462 }
463
464 void draw_path (Context cr, Glyph glyph, Path path,
465 double lsb, double x, double y, double scale) {
466
467 EditPoint e, prev;
468 double xa, ya, xb, yb, xc, yc, xd, yd;
469 double by;
470 double s = get_scale (glyph);
471
472 if (path.points.size > 0) {
473 if (unlikely (path.is_open ())) {
474 warning (@"Path is open in $(glyph.get_name ()).");
475 }
476
477 //path.add_hidden_double_points (); // FIXME: this distorts shapes
478
479 prev = path.points.get (path.points.size - 1);
480 xa = (prev.x - lsb) * s + x;
481 ya = y - prev.y * s;
482 cr.move_to (xa, ya);
483
484 by = (y - cached_font.base_line * s);
485 for (int i = 0; i < path.points.size; i++) {
486 e = path.points.get (i).copy ();
487
488 PenTool.convert_point_segment_type (prev, e, PointType.CUBIC);
489
490 xb = (prev.get_right_handle ().x - lsb) * s + x;
491 yb = by - prev.get_right_handle ().y * s;
492
493 xc = (e.get_left_handle ().x - lsb) * s + x;
494 yc = by - e.get_left_handle ().y * s;
495
496 xd = (e.x - lsb) * s + x;
497 yd = by - e.y * s;
498
499 cr.curve_to (xb, yb, xc, yc, xd, yd);
500 cr.line_to (xd, yd);
501
502 prev = e;
503 }
504 }
505 }
506
507 public double get_baseline_to_bottom (Glyph g) {
508 return get_scale (g) * (-g.baseline - g.bottom_limit);
509 }
510
511 public double get_scale (Glyph g) {
512 double s = g.top_limit - g.bottom_limit;
513
514 if (s == 0) {
515 s = cached_font.top_limit - cached_font.bottom_limit;
516 }
517
518 return font_size / s;
519 }
520
521 public double get_font_scale () {
522 return font_size / (cached_font.top_limit - cached_font.bottom_limit);
523 }
524
525 public double get_baseline_to_bottom_for_font () {
526 return get_font_scale () * (-cached_font.base_line - cached_font.bottom_limit);
527 }
528
529 public void truncate (double max_width) {
530 truncated_width = max_width;
531 }
532 }
533
534 }
535