Skip to main content

text/
colr.rs

1// colr -- colour glyphs: COLR v0 / CPAL layers and CBDT / CBLC PNG bitmaps  [src/text/colr.s]
2//
3//   open(ctx)                         validate COLR + CPAL (palette 0) and CBDT + CBLC; sets
4//                                     TFF_COLR / TFF_CBDT
5//   layers(ctx, glyph, cap) -> {glyph | LAYER_FG, argb (straight)}   (empty = none)
6//   bitmap(ctx, glyph, size_fx, png, scale) -> Bitmap   the strike's PNG decoded and scaled
7//        to size_fx (area average when shrinking, bilinear when enlarging)
8//
9// COLR v1 paint graphs are not supported: v1 fonts that keep v0 base glyph records use
10// those, otherwise the monochrome glyph is drawn.  Palette 0 only; palette index 0xFFFF =
11// the current text colour.  CBDT: index subtable formats 1..5, glyph image formats 17, 18,
12// 19 (PNG); strike = the smallest ppem >= the requested size, else the largest.  The PNG
13// decoder and the enlarging scaler come from the image crates through hooks (Text).
14
15use alloc::vec;
16use alloc::vec::Vec;
17
18use gfx::Surface;
19use rt::fx::{cvt, cvtt, maxss, minss};
20
21use crate::ttf::{Tb, TtFont, TFF_CBDT, TFF_COLR, TT_CBDT, TT_CBLC, TT_COLR, TT_CPAL};
22
23pub const LAYER_FG: u32 = 0x8000_0000;
24const PNG_MAX: u64 = 16 * 1024 * 1024;
25const BMP_MAXSIDE: i32 = 2048;
26
27/// decode a PNG into a PARGB surface
28pub type PngDecode = fn(&[u8]) -> Option<Surface>;
29/// bilinear resize of a PARGB surface to w x h (img_scale mode 1)
30pub type ImgScale = fn(&Surface, i32, i32) -> Option<Surface>;
31
32/// a colour glyph bitmap: placement relative to pen x / baseline, PARGB pixels (stride 4 w)
33pub struct Bitmap {
34    pub left: i32,
35    pub top: i32,
36    pub w: i32,
37    pub h: i32,
38    pub px: Vec<u8>,
39}
40
41/// validate COLR + CPAL and CBDT + CBLC  [colr_open]
42pub fn open(f: &mut TtFont) {
43    // ---- COLR + CPAL
44    'colr: {
45        let t = f.tb(TT_COLR);
46        if t.len() < 14 || t.u16(0) as u32 > 1 {
47            break 'colr;
48        }
49        let nb = t.u16(2);
50        if nb <= 0 {
51            break 'colr; // no v0 base glyph records
52        }
53        let bo = t.u32(4);
54        if bo < 0 || (nb as u32).wrapping_mul(6) as u64 + bo as u64 > t.len() as u64 {
55            break 'colr;
56        }
57        let nl = t.u16(12);
58        if nl <= 0 {
59            break 'colr;
60        }
61        let lo = t.u32(8);
62        if lo < 0 || lo as u64 + nl as u64 * 4 > t.len() as u64 {
63            break 'colr;
64        }
65        // CPAL: palette 0
66        let p = f.tb(TT_CPAL);
67        if p.len() < 14 {
68            break 'colr;
69        }
70        let ne = p.u16(2);
71        if ne <= 0 || p.u16(4) <= 0 {
72            break 'colr;
73        }
74        let cro = p.u32(8);
75        if cro < 0 {
76            break 'colr;
77        }
78        let ci = p.u16(12);
79        if ci < 0 {
80            break 'colr;
81        }
82        let first = cro as u64 + ci as u64 * 4; // palette 0's first record
83        if p.len() as u64 <= first {
84            break 'colr;
85        }
86        let avail = (p.len() as u64 - first) >> 2;
87        let n = (ne as u64).min(avail) as u32;
88        if n == 0 {
89            break 'colr;
90        }
91        f.cpal = (first as u32).wrapping_add(f.tab[TT_CPAL].0);
92        f.cpaln = n;
93        f.flags |= TFF_COLR;
94    }
95    // ---- CBDT + CBLC: at least one bitmap size record
96    if f.tab[TT_CBDT].1 < 8 {
97        return;
98    }
99    let t = f.tb(TT_CBLC);
100    if t.len() < 8 + 48 {
101        return;
102    }
103    let v = t.u16(0);
104    if !(2..=3).contains(&v) || t.u32(4) <= 0 {
105        return;
106    }
107    f.flags |= TFF_CBDT;
108}
109
110/// layers of a COLR base glyph (empty = none), at most cap  [colr_layers]
111pub fn layers(f: &TtFont, glyph: u32, cap: usize, out: &mut Vec<(u32, u32)>) {
112    out.clear();
113    if f.flags & TFF_COLR == 0 {
114        return;
115    }
116    let t = f.tb(TT_COLR);
117    let d = t.0;
118    let nb = t.u16(2) as u32; // base glyph records (checked by open)
119    let bo = t.u32(4) as u32;
120    let (mut lo, mut hi) = (0u32, nb);
121    let mut found = None;
122    while lo < hi {
123        let mid = (lo + hi) >> 1;
124        let r = (mid * 6 + bo) as usize;
125        let g = u16::from_be_bytes([d[r], d[r + 1]]) as u32;
126        if glyph < g {
127            hi = mid;
128        } else if glyph > g {
129            lo = mid + 1;
130        } else {
131            let first = u16::from_be_bytes([d[r + 2], d[r + 3]]) as u32;
132            let n = u16::from_be_bytes([d[r + 4], d[r + 5]]) as u32;
133            found = Some((first, n));
134            break;
135        }
136    }
137    let Some((first, n)) = found else { return };
138    let n = n.min(cap as u32);
139    let nl = t.u16(12) as u32; // numLayerRecords (checked by open)
140    let lo = t.u32(8) as u32;
141    for k in 0..n {
142        let li = first + k;
143        if li >= nl {
144            break;
145        }
146        let r = (lo + li * 4) as usize;
147        let lg = u16::from_be_bytes([d[r], d[r + 1]]) as u32;
148        let pi = u16::from_be_bytes([d[r + 2], d[r + 3]]) as u32;
149        if pi >= f.cpaln {
150            out.push((lg | LAYER_FG, 0)); // 0xFFFF (and anything invalid): text colour
151        } else {
152            // bytes B G R A = 0xAARRGGBB
153            let o = (f.cpal + pi * 4) as usize;
154            let c = u32::from_le_bytes([f.data[o], f.data[o + 1], f.data[o + 2], f.data[o + 3]]);
155            out.push((lg, c));
156        }
157    }
158}
159
160// the image of glyph in the best strike  [cbdt_find]
161struct Found {
162    off: u32, // image data offset in CBDT
163    len: u32,
164    fmt: u32,
165    ppem: u32,
166    metrics: [u8; 5], // height, width, bearingX, bearingY, advance (from the index, formats 2 / 5)
167}
168
169// BigGlyphMetrics at a CBLC offset (bytes past the end read as 0)  [cb_bigmetrics]
170fn bigmetrics(t: Tb, off: u32, m: &mut [u8; 5]) {
171    for (i, v) in m.iter_mut().enumerate() {
172        let b = t.u8(off.wrapping_add(i as u32));
173        *v = if b < 0 { 0 } else { b as u8 };
174    }
175}
176
177fn find(f: &TtFont, glyph: u32, want: u32, metrics: [u8; 5]) -> Option<Found> {
178    let t = f.tb(TT_CBLC);
179    let ns = t.u32(4);
180    if ns <= 0 {
181        return None;
182    }
183    let ns = (ns as u32).min(256);
184    // strike: smallest ppemY >= wanted containing the glyph, else the largest
185    let (mut best, mut bppem) = (u32::MAX, 0u32);
186    for i in 0..ns {
187        let r = i * 48 + 8; // BitmapSize record
188        let s = t.u16(r + 40);
189        if s < 0 || glyph < s as u32 {
190            continue;
191        }
192        if glyph > t.u16(r + 42) as u32 {
193            continue;
194        }
195        let pp = t.u8(r + 45);
196        if pp <= 0 {
197            continue;
198        }
199        let pp = pp as u32;
200        let take = if best == u32::MAX {
201            true
202        } else if bppem >= want {
203            // prefer: >= wanted and smaller
204            pp >= want && pp < bppem
205        } else {
206            // both below: larger
207            pp >= want || pp > bppem
208        };
209        if take {
210            best = r;
211            bppem = pp;
212        }
213    }
214    if best == u32::MAX {
215        return None;
216    }
217    let r = best;
218    let mut m = metrics;
219    let sa = t.u32(r);
220    if sa < 0 {
221        return None;
222    }
223    let sa = sa as u32;
224    let nsub = t.u32(r + 8);
225    if nsub <= 0 {
226        return None;
227    }
228    let nsub = (nsub as u32).min(4096);
229    // index subtable containing the glyph
230    let mut sub = None;
231    for i in 0..nsub {
232        let e = (i << 3).wrapping_add(sa);
233        let first = t.u16(e);
234        if first < 0 {
235            return None;
236        }
237        if glyph < first as u32 {
238            continue;
239        }
240        if glyph <= t.u16(e.wrapping_add(2)) as u32 {
241            sub = Some((e, first as u32));
242            break;
243        }
244    }
245    let (e, first) = sub?;
246    let ao = t.u32(e.wrapping_add(4));
247    if ao < 0 {
248        return None;
249    }
250    let st = (ao as u32).wrapping_add(sa); // index subtable
251    let ifmt = t.u16(st);
252    let fmt = t.u16(st.wrapping_add(2)) as u32;
253    let ido = t.u32(st.wrapping_add(4));
254    if ido < 0 {
255        return None;
256    }
257    let mut ido = ido as u32; // image data offset (CBDT)
258    let k = glyph - first;
259    // offsets of the glyph's data in CBDT: (start, end) relative to ido
260    let span = |a: i64, b: i64| -> Option<(u32, u32)> {
261        if b <= a {
262            return None; // no data for this glyph
263        }
264        Some(((a as u32).wrapping_add(ido), (b - a) as u32))
265    };
266    let (off, len) = match ifmt {
267        1 => {
268            let a = t.u32(st.wrapping_add(k.wrapping_mul(4)).wrapping_add(8));
269            if a < 0 {
270                return None;
271            }
272            let b = t.u32(st.wrapping_add(k.wrapping_mul(4)).wrapping_add(12));
273            if b < 0 {
274                return None;
275            }
276            span(a, b)?
277        }
278        3 => {
279            let a = t.u16(st.wrapping_add(k.wrapping_mul(2)).wrapping_add(8));
280            if a < 0 {
281                return None;
282            }
283            let b = t.u16(st.wrapping_add(k.wrapping_mul(2)).wrapping_add(10));
284            if b < 0 {
285                return None;
286            }
287            span(a as i64, b as i64)?
288        }
289        2 => {
290            let size = t.u32(st.wrapping_add(8)); // imageSize
291            if size <= 0 {
292                return None;
293            }
294            let size = size as u32;
295            ido = size.wrapping_mul(k).wrapping_add(ido);
296            bigmetrics(t, st.wrapping_add(12), &mut m);
297            (ido, size)
298        }
299        5 => {
300            let n = t.u32(st.wrapping_add(20)); // numGlyphs
301            if n <= 0 {
302                return None;
303            }
304            let mut idx = None;
305            for i in 0..n as u32 {
306                let g = t.u16(st.wrapping_add(i * 2).wrapping_add(24));
307                if g < 0 {
308                    return None;
309                }
310                if g as u32 == glyph {
311                    idx = Some(i);
312                    break;
313                }
314            }
315            let i = idx?;
316            let size = t.u32(st.wrapping_add(8));
317            if size <= 0 {
318                return None;
319            }
320            let size = size as u32;
321            ido = size.wrapping_mul(i).wrapping_add(ido);
322            bigmetrics(t, st.wrapping_add(12), &mut m);
323            (ido, size)
324        }
325        4 => {
326            let n = t.u32(st.wrapping_add(8)); // numGlyphs
327            if n <= 0 {
328                return None;
329            }
330            let mut idx = None;
331            for i in 0..n as u32 {
332                let g = t.u16(st.wrapping_add(i * 4).wrapping_add(12));
333                if g < 0 {
334                    return None;
335                }
336                if g as u32 == glyph {
337                    idx = Some(i);
338                    break;
339                }
340            }
341            let i = idx?;
342            let a = t.u16(st.wrapping_add(i * 4).wrapping_add(14));
343            let b = t.u16(st.wrapping_add(i * 4).wrapping_add(18));
344            if b < 0 {
345                return None;
346            }
347            span(a as u32 as i64, b as i64)?
348        }
349        _ => return None,
350    };
351    if off == 0 {
352        return None;
353    }
354    Some(Found { off, len, fmt, ppem: bppem, metrics: m })
355}
356
357/// the colour bitmap of glyph at size_fx (24.8 px)  [cbdt_bitmap]
358pub fn bitmap(f: &TtFont, glyph: u32, size_fx: i32, png: Option<PngDecode>, scale: Option<ImgScale>,
359              metrics: &mut [u8; 5]) -> Option<Bitmap> {
360    if f.flags & TFF_CBDT == 0 {
361        return None;
362    }
363    let want = ((size_fx as u32).wrapping_add(255)) >> 8; // wanted ppem (rounded up)
364    let fd = find(f, glyph, want, *metrics)?;
365    *metrics = fd.metrics;
366    let t = f.tb(TT_CBDT);
367    let s = fd.off;
368    let (plen, poff): (i64, u32) = match fd.fmt {
369        19 => {
370            // metrics from the index, u32 length, PNG
371            let l = t.u32(s);
372            if l < 0 {
373                return None;
374            }
375            (l, s.wrapping_add(4))
376        }
377        17 => {
378            for i in 0..5u32 {
379                let b = t.u8(s.wrapping_add(i));
380                if b < 0 {
381                    return None;
382                }
383                metrics[i as usize] = b as u8;
384            }
385            let l = t.u32(s.wrapping_add(5));
386            if l < 0 {
387                return None;
388            }
389            (l, s.wrapping_add(9))
390        }
391        18 => {
392            bigmetrics(t, s, metrics);
393            let l = t.u32(s.wrapping_add(8));
394            if l < 0 {
395                return None;
396            }
397            (l, s.wrapping_add(12))
398        }
399        _ => return None,
400    };
401    let _ = fd.len;
402    if poff as u64 + plen as u64 > t.len() as u64 || plen as u64 > PNG_MAX {
403        return None;
404    }
405    let src = (png?)(&t.0[poff as usize..poff as usize + plen as usize])?;
406    // target size: bitmap * size / ppem
407    let sc = size_fx as f32 * (1.0 / 256.0) / fd.ppem as i32 as f32;
408    let w = cvt(src.w as f32 * sc).max(1);
409    if w > BMP_MAXSIDE {
410        return None;
411    }
412    let h = cvt(src.h as f32 * sc).max(1);
413    if h > BMP_MAXSIDE {
414        return None;
415    }
416    // pixels: shrink by area averaging, enlarge bilinearly
417    let px = if w > src.w || h > src.h {
418        let up = (scale?)(&src, w, h)?;
419        area_scale(&up, w, h) // same size: a packed copy
420    } else {
421        area_scale(&src, w, h)
422    };
423    // placement: bearingX, -bearingY (scaled)
424    let left = cvt(metrics[2] as i8 as i32 as f32 * sc);
425    let top = cvt(metrics[3] as i8 as i32 as f32 * sc).wrapping_neg();
426    Some(Bitmap { left, top, w, h, px })
427}
428
429// packed PARGB w x h: the area average of the source (w <= src.w, h <= src.h)  [area_scale]
430fn area_scale(src: &Surface, w: i32, h: i32) -> Vec<u8> {
431    let mut out = vec![0u8; (w * h * 4) as usize];
432    // source pixels per target pixel
433    let fx = src.w as f32 / w as f32;
434    let fy = src.h as f32 / h as f32;
435    let stride = src.stride as usize;
436    let mut o = 0usize;
437    for y in 0..h {
438        for x in 0..w {
439            // source rectangle [x0, x1) x [y0, y1)
440            let x0 = x as f32 * fx;
441            let x1 = x0 + fx;
442            let y0 = y as f32 * fy;
443            let y1 = y0 + fy;
444            let mut acc = [0f32; 4];
445            let mut wsum = 0f32;
446            let mut sy = cvtt(y0);
447            loop {
448                let syf = sy as f32;
449                if !(syf < y1) || sy as u32 >= src.h as u32 {
450                    break;
451                }
452                // vertical weight = min(sy+1, y1) - max(sy, y0)
453                let wy = minss(syf + 1.0, y1) - maxss(syf, y0);
454                let row = sy as usize * stride;
455                let mut sx = cvtt(x0);
456                loop {
457                    let sxf = sx as f32;
458                    if !(sxf < x1) || sx as u32 >= src.w as u32 {
459                        break;
460                    }
461                    let wgt = (minss(sxf + 1.0, x1) - maxss(sxf, x0)) * wy;
462                    wsum = wsum + wgt;
463                    let p = row + sx as usize * 4;
464                    for c in 0..4 {
465                        acc[c] = acc[c] + src.px[p + c] as f32 * wgt;
466                    }
467                    sx += 1;
468                }
469                sy += 1;
470            }
471            if wsum > 0.0 {
472                for c in 0..4 {
473                    // cvtps2dq (nearest), packssdw, packuswb
474                    let v = cvt(acc[c] / wsum).clamp(-32768, 32767);
475                    out[o + c] = v.clamp(0, 255) as u8;
476                }
477            }
478            o += 4;
479        }
480    }
481    out
482}