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