//! **¿Un emoji se pinta a color?** Hasta este test, no — en ninguna app. //! //! El hallazgo, medido antes de tocar nada (§8): con `TextBlock::simple` a //! 32 px, `😀` resolvía a **DejaVu Sans del sistema** y `❤️` a **Inter**, y las //! cuatro cadenas probadas daban **cero píxeles cromáticos**. No era el //! rasterizador —vello 0.9 dibuja COLR y CBDT solo, y la máquina tiene //! `Noto Color Emoji` que fontconfig resuelve con el alias `emoji`— sino el //! **orden de la pila de fuentes**: parley agrega la familia genérica `Emoji` //! detrás de la pila normal, así que cualquier fuente de texto que cubra el //! codepoint gana. DejaVu tiene contornos monocromos del bloque Emoticons e //! Inter tiene `❤` como dingbat. //! //! Se certifica por texto (§8): conteo de píxeles cromáticos, no una imagen. //! Un píxel es cromático si sus canales se separan más de 24/255 — un gris, //! sea cual sea su brillo, no lo es. //! //! **Los dos controles negativos son la mitad del test**, y valen más que el //! positivo: si el gate estuviera puesto de más, el corazón con selector de //! TEXTO (`U+FE0E`) también saldría rojo, y los símbolos del chrome de la UI //! (`⚠ ▶ ✔ ©`) se convertirían en caritas de colores. Los dos casos están acá. use llimphi_test::{with_harness, Harness, Rendered}; use llimphi_text::peniko::Color; use llimphi_text::{layout_block, TextBlock, Typesetter}; const ANCHO: u32 = 320; const ALTO: u32 = 80; const ORIGEN: (f64, f64) = (10.0, 10.0); const TAM: f32 = 32.0; /// Cuánto se pueden separar los canales de un píxel antes de que deje de ser /// un gris. Bien por encima del ruido del antialiasing (medido: un render sin /// emoji da `max_croma = 0` exacto sobre fondo y tinta neutros). const CROMA_MIN: u32 = 24; fn fondo() -> Color { Color::from_rgba8(0, 0, 0, 255) } fn tinta() -> Color { Color::from_rgba8(255, 255, 255, 255) } struct Medida { /// Píxeles con algo pintado encima del fondo. tinta: u32, /// De ésos, los que no son grises. cromaticos: u32, /// La separación de canales más grande que haya en el cuadro. max_croma: u32, /// Píxeles claramente rojos (R por encima de G y de B). rojos: u32, } fn medir(r: &Rendered) -> Medida { let mut m = Medida { tinta: 0, cromaticos: 0, max_croma: 0, rojos: 0 }; for i in (0..r.rgba.len()).step_by(4) { let (cr, cg, cb) = (r.rgba[i] as i32, r.rgba[i + 1] as i32, r.rgba[i + 2] as i32); if cr + cg + cb > 24 { m.tinta += 1; } let croma = (cr - cg).abs().max((cg - cb).abs()).max((cr - cb).abs()) as u32; if croma > CROMA_MIN { m.cromaticos += 1; } m.max_croma = m.max_croma.max(croma); if cr - cg > 60 && cr - cb > 60 { m.rojos += 1; } } m } fn pintar(h: &mut Harness, texto: &str) -> Rendered { let mut ts = Typesetter::new(); let bloque = TextBlock::simple(texto, TAM, tinta(), ORIGEN); let layout = layout_block(&mut ts, &bloque); h.render_con( llimphi_test::llimphi_compositor::View::<()>::new(Default::default()), ANCHO, ALTO, fondo(), |escena, _m, _c, _ts| { llimphi_text::draw_layout(escena, &layout, tinta(), ORIGEN); }, ) } /// Sin fuente de emoji en la máquina no hay nada que preguntar: el emoji cae /// al fallback y sale monocromo, que es el comportamiento correcto en esa /// máquina. El test se saltea **diciéndolo**, en vez de fallar por el entorno. fn hay_emoji() -> bool { let hay = Typesetter::new().hay_fuente_de_emoji(); if !hay { eprintln!( "SALTEADO: esta máquina no tiene fuente de emoji (la familia genérica \ `emoji` de fontique no resuelve a nada). Instalá una —p. ej. \ Noto Color Emoji— para que este test tenga algo que medir." ); } hay } #[test] fn una_carita_se_pinta_a_color() { if !hay_emoji() { return; } with_harness(|h| { let m = medir(&pintar(h, "😀")); // Medido con el arreglo puesto: tinta 924, cromáticos 892, croma 235. // Sin el arreglo: tinta 451, cromáticos 0, croma 0 — o sea que el // umbral no es un número prudente, es un abismo. Se pide la mitad de // lo medido para dejar lugar a que cambie la fuente del sistema. assert!( m.cromaticos > 400, "la carita salió monocroma: tinta={} cromáticos={} croma_máx={} \ (sin el arreglo daba cromáticos=0)", m.tinta, m.cromaticos, m.max_croma ); }); } #[test] fn el_corazon_sale_rojo_y_no_de_cualquier_color() { if !hay_emoji() { return; } with_harness(|h| { // `❤` + VS16: el caso donde el selector pide presentación de color. let m = medir(&pintar(h, "\u{2764}\u{FE0F}")); assert!( m.rojos > 200, "el corazón no salió rojo: tinta={} cromáticos={} rojos={} — \ «hay color» no alcanza como oráculo, un corazón verde también \ tendría cromáticos>0", m.tinta, m.cromaticos, m.rojos ); }); } #[test] fn el_selector_de_texto_lo_deja_monocromo() { if !hay_emoji() { return; } with_harness(|h| { // `❤` + VS15 (`U+FE0E`) es el pedido EXPLÍCITO de la forma de símbolo. let m = medir(&pintar(h, "\u{2764}\u{FE0E}")); assert!(m.tinta > 0, "no se pintó nada; el test no probaría nada"); assert_eq!( m.cromaticos, 0, "el selector de texto se ignoró y el corazón salió de colores \ (tinta={} croma_máx={})", m.tinta, m.max_croma ); }); } #[test] fn los_simbolos_del_chrome_siguen_monocromos() { if !hay_emoji() { return; } with_harness(|h| { // Los cuatro son Extended_Pictographic —o sea que el clasificador de // swash los marca como emoji— y ninguno es presentación emoji. Si el // gate se aflojara a «extended pictographic», el aviso de la UI // saldría como una carita amarilla. let m = medir(&pintar(h, "⚠ ▶ ✔ ©")); assert!(m.tinta > 0, "no se pintó nada; el test no probaría nada"); assert_eq!( m.cromaticos, 0, "un símbolo del chrome se fue a la fuente de emoji \ (tinta={} croma_máx={})", m.tinta, m.max_croma ); }); } #[test] fn el_texto_normal_no_se_tocó() { with_harness(|h| { let m = medir(&pintar(h, "Hamburgefonstiv 0123")); assert!(m.tinta > 0, "no se pintó nada"); assert_eq!( m.cromaticos, 0, "el texto sin emoji salió con color: croma_máx={}", m.max_croma ); }); }