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