fix: DroidSans reemplaza AdwaitaSans (variable) + Y-flip TTF

- AdwaitaSans era fuente variable (fvar/gvar) incompatible con parser
- DroidSans.ttf: fuente estática 187KB (Apache 2.0)
- Y-flip en rasterización: TTF Y crece arriba, bitmap Y crece abajo
- Pendiente: UTF-8 multibyte en drawText

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-16 13:19:40 +01:00
parent c0107de978
commit 222c4e1542
5 changed files with 43 additions and 14 deletions

View file

@ -79,12 +79,13 @@ Resumen breve (1-2 frases). Resultado principal.
## TAREA COMPLETADA: Fuentes TTF con Antialiasing ✅
### Estado actual (v0.16.0)
### Estado actual (v0.16.2)
- ✅ Parsing TTF completo (glyf, cmap format 4 y 12)
- ✅ Rasterización con bezier cuadráticas
- ✅ Antialiasing 2x supersampling
- ✅ **Fuente embebida** (AdwaitaSans-Regular, ~860KB)
- ✅ **Fuente embebida** (DroidSans, 187KB) - Fuente estática clásica
- ✅ Integración con SoftwareRenderer
- ⚠️ **Pendiente:** UTF-8 multibyte (á,é,ñ) - drawText itera bytes, no codepoints
### Uso
```zig
@ -98,7 +99,7 @@ renderer.setTtfFont(&ttf);
### Archivos clave
- `src/render/ttf.zig` - Parsing y rasterización TTF
- `src/render/embedded_font.zig` - Fuente embebida
- `src/render/fonts/AdwaitaSans-Regular.ttf` - Datos de fuente
- `src/render/fonts/DroidSans.ttf` - Datos de fuente (Apache 2.0)
---
@ -107,7 +108,7 @@ renderer.setTtfFont(&ttf);
| Campo | Valor |
|-------|-------|
| **Nombre** | zcatgui |
| **Versión** | v0.16.1 |
| **Versión** | v0.16.2 |
| **Fecha inicio** | 2025-12-09 |
| **Estado** | ✅ COMPLETO - 37 widgets, ~35K LOC, 4 backends |
| **Lenguaje** | Zig 0.15.2 |
@ -665,14 +666,15 @@ const stdout = std.fs.File.stdout(); // NO std.io.getStdOut()
| 2025-12-09 | v0.15.0 | Documentación: REFERENCE.md completo (1370 líneas) |
| 2025-12-11 | v0.15.1 | FocusSystem rediseñado: registration_group/active_group, focus implícito |
| 2025-12-11 | v0.15.2 | Widgets adaptados a FocusSystem: numberentry, textarea, select, radio, slider, tabs |
| 2025-12-16 | v0.16.0 | TTF rasterization con antialiasing (supersampling 2x), tests con AdwaitaSans |
| 2025-12-16 | v0.16.1 | Fuente embebida: AdwaitaSans-Regular.ttf (~860KB), TtfFont.initEmbedded() |
| 2025-12-16 | v0.16.0 | TTF rasterization con antialiasing (supersampling 2x) |
| 2025-12-16 | v0.16.1 | Fuente embebida: TtfFont.initEmbedded() |
| 2025-12-16 | v0.16.2 | Fix TTF: DroidSans (187KB) reemplaza AdwaitaSans (variable). Y-flip rasterización. **UTF-8 pendiente** |
---
## ESTADO ACTUAL
**✅ PROYECTO COMPLETADO - v0.16.1**
**✅ PROYECTO COMPLETADO - v0.16.2**
> **Para detalles técnicos completos, ver `REFERENCE.md`** (1370 líneas de documentación)

View file

@ -1,18 +1,25 @@
//! Fuente TTF embebida para uso sin dependencias externas
//!
//! Incluye AdwaitaSans-Regular para renderizado de texto con antialiasing.
//! Licencia: SIL Open Font License (permite redistribución embebida).
//! Incluye DroidSans para renderizado de texto con antialiasing.
//! Licencia: Apache 2.0 (permite redistribución embebida).
//!
//! Nota: Se usa DroidSans (fuente estática) en lugar de AdwaitaSans
//! porque AdwaitaSans es una fuente variable (OpenType 1.8+) que requiere
//! parsing de tablas fvar/gvar que no soportamos.
const std = @import("std");
const TtfFont = @import("ttf.zig").TtfFont;
/// Datos binarios de la fuente embebida (cargados en tiempo de compilación)
pub const adwaita_sans_data: []const u8 = @embedFile("fonts/AdwaitaSans-Regular.ttf");
pub const font_data: []const u8 = @embedFile("fonts/DroidSans.ttf");
/// Alias para compatibilidad (deprecated, usar font_data)
pub const adwaita_sans_data = font_data;
/// Tamaño de la fuente embebida en bytes
pub const adwaita_sans_size: usize = adwaita_sans_data.len;
pub const font_size: usize = font_data.len;
/// Inicializa TtfFont desde la fuente embebida AdwaitaSans-Regular
/// Inicializa TtfFont desde la fuente embebida DroidSans
///
/// Ejemplo:
/// ```zig
@ -22,7 +29,7 @@ pub const adwaita_sans_size: usize = adwaita_sans_data.len;
/// renderer.setTtfFont(&ttf);
/// ```
pub fn initEmbeddedFont(allocator: std.mem.Allocator) !TtfFont {
return TtfFont.initFromMemory(allocator, adwaita_sans_data);
return TtfFont.initFromMemory(allocator, font_data);
}
test "embedded font data is valid" {

Binary file not shown.

View file

@ -112,8 +112,10 @@ pub fn rasterizeGlyph(
const ss = @as(f32, @floatFromInt(supersample));
const ss_sq = @as(f32, @floatFromInt(@as(u32, supersample) * @as(u32, supersample)));
const height_f = @as(f32, @floatFromInt(height));
// Scanline fill with supersampling
// Y-flip: TTF Y goes up, bitmap Y goes down
for (0..height) |py| {
for (0..width) |px| {
var coverage: u32 = 0;
@ -122,7 +124,8 @@ pub fn rasterizeGlyph(
for (0..supersample) |sy| {
for (0..supersample) |sx| {
const sample_x = @as(f32, @floatFromInt(px)) + (@as(f32, @floatFromInt(sx)) + 0.5) / ss;
const sample_y = @as(f32, @floatFromInt(py)) + (@as(f32, @floatFromInt(sy)) + 0.5) / ss;
// Flip Y: bitmap row 0 should sample at top of glyph (high y)
const sample_y = (height_f - 1.0 - @as(f32, @floatFromInt(py))) + (@as(f32, @floatFromInt(sy)) + 0.5) / ss;
// Count winding number
var winding: i32 = 0;
@ -1158,3 +1161,20 @@ test "TTF rasterize multiple characters" {
}
}
}
test "embedded font glyph indices" {
const allocator = std.testing.allocator;
const embedded = @import("embedded_font.zig");
var font = try TtfFont.initFromMemory(allocator, embedded.font_data);
defer font.deinit();
// Verify basic font properties
try std.testing.expect(font.num_glyphs > 0);
try std.testing.expect(font.cmap_offset > 0);
// Verify ASCII character mapping
try std.testing.expect(font.getGlyphIndex('A') > 0);
try std.testing.expect(font.getGlyphIndex('a') > 0);
try std.testing.expect(font.getGlyphIndex('0') > 0);
}