diff options
| author | eunhhu <[email protected]> | 2026-09-28 13:03:50 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-28 13:03:50 +0100 |
| commit | 202e6bf4677565c0cfbef5340119d0f9674bf17d (patch) | |
| tree | de23a6974e74784813556e51b52c0719b699b220 /text | |
| parent | 0d2e04f1c9d05a598deb443ba067ef61e0b9cab3 (diff) | |
| download | vega-202e6bf4677565c0cfbef5340119d0f9674bf17d.tar.gz vega-202e6bf4677565c0cfbef5340119d0f9674bf17d.zip | |
Set paragraphs into lines: bidi, script runs, shaping, line breaking, visual order
text/layout.c puts the checked parts together: a paragraph's levels,
scripts resolved for Common and Inherited characters, break
opportunities, one shaping to measure, lines broken at the last
opportunity that fits (trailing spaces hang, and a line that shapes
wider than measured backs off), then per line L1, runs of one level and
one script shaped again, the runs in L2's order and their glyphs placed.
test-layout checks 30,000 random mixed-script paragraphs: lines cover
the text, mandatory breaks end lines, lines fit when they can, and
glyphs come in the order UAX #9 gives the characters. vega-symbols
paragraph draws a layout through the symbol file's glyphs.
Diffstat (limited to 'text')
| -rw-r--r-- | text/layout.c | 239 | ||||
| -rw-r--r-- | text/layout.h | 38 | ||||
| -rw-r--r-- | text/test-layout.c | 188 |
3 files changed, 465 insertions, 0 deletions
diff --git a/text/layout.c b/text/layout.c new file mode 100644 index 0000000..dd4bd4d --- /dev/null +++ b/text/layout.c @@ -0,0 +1,239 @@ +/* Paragraphs set into lines (text/layout.h). The parts are the ones Unicode + * and HarfBuzz check (bidi.c, linebreak.c, shape.c); this puts them + * together as UAX #9 section 3.4 and UAX #14 describe: + * 1. levels for the paragraph (P2 to I2), each removed character at the + * level of the one before it; + * 2. a script for each character, Common and Inherited ones taking the + * one before them (at the start, the first after); + * 3. break opportunities, and one shaping of the paragraph to measure it; + * 4. lines: the last opportunity where what shows still fits (spaces at a + * line's end hang past it), or the first when none does; + * 5. each line: L1 at its end, runs of one level and one script shaped + * again (so joining and kerning end with the line), the runs in L2's + * order, the glyphs placed left to right. */ +#include "layout.h" +#include "text.h" +#include <stdlib.h> +#include <string.h> + +typedef struct { size_t start, end; uint8_t level; VegaScript script; } Run; + +static bool grow(void **array, size_t *allocated, size_t need, size_t size) { + if (need <= *allocated) return true; + size_t n = *allocated ? *allocated : 16; + while (n < need) n *= 2; + void *grown = realloc(*array, n * size); + if (!grown) return false; + *array = grown; + *allocated = n; + return true; +} + +static bool isolate_control(VegaBidiClass c) { + return c == VEGA_BIDI_LRI || c == VEGA_BIDI_RLI || c == VEGA_BIDI_FSI || c == VEGA_BIDI_PDI; +} +static bool removed(VegaBidiClass c) { + return c == VEGA_BIDI_BN || c == VEGA_BIDI_LRE || c == VEGA_BIDI_RLE || c == VEGA_BIDI_LRO || c == VEGA_BIDI_RLO || + c == VEGA_BIDI_PDF; +} + +/* Shape [r->start, r->end) and add its glyphs at *pen. */ +static bool shape_run(VegaLayout *l, VegaShaper *shaper, const VegaShaping *s, const uint32_t *codes, const Run *r, + int32_t *pen) { + const VegaShaped *g; + long n = vega_shape(shaper, s, codes + r->start, r->end - r->start, vega_script_code(r->script), + r->level & 1 ? VEGA_SHAPE_RTL : VEGA_SHAPE_LTR, &g); + if (n < 0 || !grow((void **)&l->glyphs, &l->glyphs_allocated, l->glyph_count + (size_t)n, sizeof(VegaPlaced))) + return false; + for (long k = 0; k < n; ++k) { + l->glyphs[l->glyph_count++] = (VegaPlaced){g[k].glyph, (uint32_t)(r->start + g[k].cluster), *pen + g[k].x_offset, + g[k].y_offset, g[k].x_advance}; + *pen += g[k].x_advance; + } + return true; +} + +/* Runs of one level and one script in [start, end). */ +static size_t itemize(const uint8_t *levels, const VegaScript *scripts, size_t start, size_t end, Run *out) { + size_t n = 0; + for (size_t i = start; i < end; ++i) { + if (n && out[n - 1].level == levels[i] && out[n - 1].script == scripts[i]) out[n - 1].end = i + 1; + else out[n++] = (Run){i, i + 1, levels[i], scripts[i]}; + } + return n; +} + +static bool set_line(VegaLayout *l, VegaShaper *shaper, const VegaShaping *s, const uint32_t *codes, + const VegaBidiClass *classes, const uint8_t *levels, uint8_t *line_levels, const VegaScript *scripts, + Run *runs, size_t *order, int paragraph, size_t start, size_t end, int32_t *pen_out, + int32_t *hang_out); + +bool vega_layout(VegaLayout *l, VegaShaper *shaper, const VegaShaping *s, const uint32_t *codes, size_t count, + int direction, int32_t width) { + l->line_count = l->glyph_count = 0; + l->width = 0; + l->level = direction == VEGA_BIDI_RTL; + if (!count) return true; + /* The work, in one block: classes, levels (the paragraph's, and a + * line's), scripts, breaks, each character's advance, runs and their + * order, and each line's end widths. */ + size_t need = count * (sizeof(VegaBidiClass) + 2 + sizeof(VegaScript) + 1 + sizeof(int32_t) + sizeof(Run) + + sizeof(size_t) + 2 * sizeof(int32_t)) + 64; + if (!grow(&l->work, &l->work_size, need, 1)) return false; + VegaBidiClass *classes = l->work; + int32_t *advance = (int32_t *)(classes + count), *totals = advance + count, *hanging = totals + count; + Run *runs = (Run *)(hanging + count); + size_t *order = (size_t *)(runs + count); + uint8_t *levels = (uint8_t *)(order + count), *line_levels = levels + count, *breaks = line_levels + count; + VegaScript *scripts = breaks + count + 1; + + /* 1. Levels. */ + for (size_t i = 0; i < count; ++i) classes[i] = vega_bidi_class(codes[i]); + int paragraph = vega_bidi_levels(classes, codes, count, direction, levels); + if (paragraph < 0) return false; + l->level = paragraph; + for (size_t i = 0; i < count; ++i) + if (levels[i] == VEGA_BIDI_REMOVED) levels[i] = (uint8_t)(i ? levels[i - 1] : paragraph); + /* 2. Scripts. */ + VegaScript first = VEGA_SCRIPT_COMMON, last = 0; + for (size_t i = 0; i < count; ++i) { + VegaScript script = vega_script(codes[i]); + if (script > VEGA_SCRIPT_INHERITED) { + first = script; + break; + } + } + for (size_t i = 0; i < count; ++i) { + VegaScript script = vega_script(codes[i]); + if (script > VEGA_SCRIPT_INHERITED) last = script; + scripts[i] = script > VEGA_SCRIPT_INHERITED ? script : last ? last : first; + } + /* 3. Opportunities, and what each character takes. */ + if (!vega_line_breaks(codes, count, breaks)) return false; + memset(advance, 0, count * sizeof(*advance)); + size_t run_count = itemize(levels, scripts, 0, count, runs); + for (size_t r = 0; r < run_count; ++r) { + size_t before = l->glyph_count; + int32_t pen = 0; + if (!shape_run(l, shaper, s, codes, &runs[r], &pen)) return false; + for (size_t k = before; k < l->glyph_count; ++k) advance[l->glyphs[k].cluster] += l->glyphs[k].advance; + l->glyph_count = before; + } + /* 4. Lines, and 5. what each holds. */ + int32_t widest = 0; + for (size_t start = 0; start < count;) { + int32_t sum = 0, trail = 0; + size_t candidate = 0, end = count; + for (size_t i = start; i < count; ++i) { + sum += advance[i]; + trail = classes[i] == VEGA_BIDI_WS ? trail + advance[i] : 0; + uint8_t b = breaks[i + 1]; + if (b == VEGA_BREAK_NONE) continue; + bool fits = !width || sum - trail <= width; + if (b == VEGA_BREAK_MANDATORY) { + end = fits || !candidate ? i + 1 : candidate; + break; + } + if (fits) candidate = i + 1; + else { + end = candidate ? candidate : i + 1; + break; + } + } + size_t first = l->glyph_count; + int32_t pen, hang; + for (;;) { + if (!set_line(l, shaper, s, codes, classes, levels, line_levels, scripts, runs, order, paragraph, start, end, + &pen, &hang)) + return false; + /* Shaped as a line, it may be wider than measured (a letter + * at its end takes its final form): then end at the opportunity + * before, if there is one. */ + if (!width || pen - hang <= width) break; + size_t earlier = end - 1; + while (earlier > start && breaks[earlier] == VEGA_BREAK_NONE) --earlier; + if (earlier == start) break; + end = earlier; + l->glyph_count = first; + } + if (!grow((void **)&l->lines, &l->lines_allocated, l->line_count + 1, sizeof(VegaLine))) return false; + totals[l->line_count] = pen; + hanging[l->line_count] = hang; + l->lines[l->line_count++] = (VegaLine){first, l->glyph_count - first, start, end, 0, pen - hang}; + if (pen - hang > widest) widest = pen - hang; + start = end; + } + /* Right to left, lines end at the paragraph's right edge. */ + l->width = width ? width : widest; + if (paragraph & 1) + for (size_t i = 0; i < l->line_count; ++i) { + VegaLine *line = &l->lines[i]; + line->left = l->width - totals[i]; + for (size_t k = line->first; k < line->first + line->count; ++k) l->glyphs[k].x += line->left; + line->left += hanging[i]; + } + return true; +} + +/* Lay out [start, end) as one line: its glyphs added to the layout, how + * far they go (pen) and how much of that is spaces hanging at its end. */ +static bool set_line(VegaLayout *l, VegaShaper *shaper, const VegaShaping *s, const uint32_t *codes, + const VegaBidiClass *classes, const uint8_t *levels, uint8_t *line_levels, const VegaScript *scripts, + Run *runs, size_t *order, int paragraph, size_t start, size_t end, int32_t *pen_out, + int32_t *hang_out) { + /* L1 at the line's end: separators, and the whitespace and isolate + * controls before them or at the end, go to the paragraph's level. */ + memcpy(line_levels + start, levels + start, end - start); + for (size_t i = end, trailing = 1; i-- > start;) { + VegaBidiClass c = classes[i]; + if (c == VEGA_BIDI_S || c == VEGA_BIDI_B) line_levels[i] = (uint8_t)paragraph, trailing = 1; + else if (c == VEGA_BIDI_WS || isolate_control(c) || removed(c)) { + if (trailing) line_levels[i] = (uint8_t)paragraph; + } else + trailing = 0; + } + size_t n = itemize(line_levels, scripts, start, end, runs); + /* L2 over the runs: from the highest level to the lowest odd one, + * reverse each stretch of runs at that level or higher. */ + uint8_t highest = 0, lowest_odd = 0xff; + for (size_t r = 0; r < n; ++r) { + order[r] = r; + if (runs[r].level > highest) highest = runs[r].level; + if ((runs[r].level & 1) && runs[r].level < lowest_odd) lowest_odd = runs[r].level; + } + for (int level = highest; lowest_odd != 0xff && level >= lowest_odd; --level) + for (size_t k = 0; k < n;) { + if (runs[order[k]].level < level) { + ++k; + continue; + } + size_t stop = k; + while (stop < n && runs[order[stop]].level >= level) ++stop; + for (size_t a = k, z = stop - 1; a < z; ++a, --z) { + size_t swap = order[a]; + order[a] = order[z]; + order[z] = swap; + } + k = stop; + } + size_t first = l->glyph_count; + int32_t pen = 0; + for (size_t r = 0; r < n; ++r) + if (!shape_run(l, shaper, s, codes, &runs[order[r]], &pen)) return false; + /* The spaces at its end hang: they are not in its width. */ + size_t shown = end; + while (shown > start && classes[shown - 1] == VEGA_BIDI_WS) --shown; + int32_t hang = 0; + for (size_t k = first; k < l->glyph_count; ++k) + if (l->glyphs[k].cluster >= shown) hang += l->glyphs[k].advance; + *pen_out = pen; + *hang_out = hang; + return true; +} + +void vega_layout_free(VegaLayout *l) { + free(l->lines); + free(l->glyphs); + free(l->work); + *l = (VegaLayout){0}; +} diff --git a/text/layout.h b/text/layout.h new file mode 100644 index 0000000..9282536 --- /dev/null +++ b/text/layout.h @@ -0,0 +1,38 @@ +#ifndef VEGA_LAYOUT_H +#define VEGA_LAYOUT_H +#include "shape.h" + +/* A paragraph set into lines (docs/design/004-text.md): its bidi levels + * (UAX #9), lines broken where UAX #14 allows and they fit (spaces at a + * line's end hang past it), and each line's runs (one level, one script) + * shaped and put in visual order. Positions are in the font's units: x + * from the paragraph's left edge, y up from the line's baseline. */ +typedef struct { + uint32_t glyph; /* the font's: its symbol is VEGA_GLYPH_SYMBOLS + glyph */ + uint32_t cluster; /* the paragraph index of the first character it came from */ + int32_t x, y, advance; +} VegaPlaced; +typedef struct { + size_t first, count; /* its glyphs, left to right */ + size_t start, end; /* the characters it holds, [start, end) */ + int32_t left, width; /* where it starts and how far what shows goes */ +} VegaLine; +typedef struct { + VegaLine *lines; + size_t line_count; + VegaPlaced *glyphs; + size_t glyph_count; + int level; /* the paragraph's: odd is right to left, and its lines align right */ + int32_t width; /* the widest line's */ + /* what setting the paragraph works in, kept for the next */ + void *work; + size_t work_size, lines_allocated, glyphs_allocated; +} VegaLayout; + +/* Set count characters in lines of at most width (0: no limit) with the + * font's shaping; direction is VEGA_BIDI_AUTO, _LTR or _RTL. The layout + * holds the result until it is set again; false without memory. */ +bool vega_layout(VegaLayout *, VegaShaper *, const VegaShaping *, const uint32_t *codes, size_t count, int direction, + int32_t width); +void vega_layout_free(VegaLayout *); +#endif diff --git a/text/test-layout.c b/text/test-layout.c new file mode 100644 index 0000000..5282572 --- /dev/null +++ b/text/test-layout.c @@ -0,0 +1,188 @@ +/* Paragraph layout against what its parts promise (text/layout.h): + * test-layout SHAPED.vsym [PARAGRAPHS] + * Random paragraphs of Latin, Arabic and Hebrew words (with marks, digits, + * brackets, directional marks and line feeds between), set at random + * widths. Every layout must: cover the paragraph with lines in order; end + * a line at every mandatory break; keep each line within the width unless + * it has no break opportunity inside; keep each glyph's cluster in its + * line; and put glyphs in the order UAX #9 puts the line's characters + * (vega_bidi_order, with L1 at the line's end). Prints one JSON line; + * exits 1 on a failure (the first few shown). */ +#define _POSIX_C_SOURCE 200809L +#include "layout.h" +#include "symbols.h" +#include "text.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +static unsigned failures; +static void fail(size_t paragraph, const char *what) { + if (failures++ < 10) fprintf(stderr, "paragraph %zu: %s\n", paragraph, what); +} + +static uint64_t state = 0x9E3779B97F4A7C15ULL; +static uint32_t below(uint32_t n) { + state ^= state >> 12; + state ^= state << 25; + state ^= state >> 27; + return n ? (uint32_t)((state * 0x2545F4914F6CDD1DULL) >> 33) % n : 0; +} + +typedef struct { uint32_t items[4096]; size_t count; } Pool; + +static bool whitespace(VegaBidiClass c) { return c == VEGA_BIDI_WS; } +static bool trailing_kind(VegaBidiClass c) { + return c == VEGA_BIDI_WS || c == VEGA_BIDI_LRI || c == VEGA_BIDI_RLI || c == VEGA_BIDI_FSI || c == VEGA_BIDI_PDI || + c == VEGA_BIDI_BN || c == VEGA_BIDI_LRE || c == VEGA_BIDI_RLE || c == VEGA_BIDI_LRO || c == VEGA_BIDI_RLO || + c == VEGA_BIDI_PDF; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "usage: test-layout SHAPED.vsym [PARAGRAPHS]\n"); + return 2; + } + size_t paragraphs = argc > 2 ? strtoul(argv[2], NULL, 10) : 3000; + FILE *f = fopen(argv[1], "rb"); + if (!f) { perror(argv[1]); return 2; } + fseek(f, 0, SEEK_END); + size_t size = (size_t)ftell(f); + fseek(f, 0, SEEK_SET); + unsigned char *bytes = malloc(size); + if (!bytes || fread(bytes, 1, size, f) != size) return 2; + fclose(f); + char error[128]; + VegaSymbols symbols; + VegaShaping shaping; + if (!vega_symbols_open(&symbols, bytes, size, error, sizeof(error)) || !symbols.shaping || + !vega_shaping_open(&shaping, symbols.shaping, symbols.shaping_size, error, sizeof(error))) { + fprintf(stderr, "%s: %s\n", argv[1], symbols.shaping ? error : "no shaping"); + return 2; + } + /* Words from each script's letters (marks after some), and what goes + * between them. */ + static Pool latin, arabic, hebrew, marks_latin, marks_arabic, marks_hebrew; + for (uint32_t i = 0; i < symbols.symbols; ++i) { + uint32_t code = vega_symbol_at(&symbols, i).code; + if (code >= VEGA_GLYPH_SYMBOLS) continue; + VegaScript script = vega_script(code); + VegaCategory c = vega_category(code); + bool mark = c == VEGA_MN || c == VEGA_MC; + bool letter = c >= VEGA_LU && c <= VEGA_LO; + Pool *p = NULL; + if (script == VEGA_SCRIPT_LATIN && letter) p = &latin; + else if (script == VEGA_SCRIPT_ARABIC && letter && code != 0x0640) p = &arabic; + else if (script == VEGA_SCRIPT_HEBREW && letter) p = &hebrew; + else if (mark && code >= 0x0300 && code <= 0x036F) p = &marks_latin; + else if (mark && code >= 0x064B && code <= 0x0652) p = &marks_arabic; + else if (mark && code >= 0x05B0 && code <= 0x05C2) p = &marks_hebrew; + if (p && p->count < 4096) p->items[p->count++] = code; + } + if (!latin.count || !arabic.count || !hebrew.count) { + fprintf(stderr, "%s: needs Latin, Arabic and Hebrew letters\n", argv[1]); + return 2; + } + static const uint32_t between[] = {' ', ' ', ' ', ' ', ',', '.', '(', ')', '[', ']', '-', '!', '?', 0x060C, 0x061F, + '1', '2', '3', 0x0661, 0x0662, 0x200E, 0x200F, 0x2028, 0x2014, 0x00A0}; + VegaShaper *shaper = vega_shaper_new(); + VegaLayout layout = {0}; + size_t lines = 0, glyphs = 0, characters = 0; + double seconds = 0; + static uint32_t codes[2048]; + static uint8_t breaks[2049], levels[2048], line_levels[2048]; + static VegaBidiClass classes[2048]; + static uint32_t order[2048], rank[2048]; + for (size_t k = 0; k < paragraphs; ++k) { + size_t n = 0; + unsigned words = 1 + below(40); + for (unsigned w = 0; w < words && n < 1900; ++w) { + unsigned which = below(10); + const Pool *letters = which < 5 ? &latin : which < 8 ? &arabic : &hebrew; + const Pool *marks = which < 5 ? &marks_latin : which < 8 ? &marks_arabic : &marks_hebrew; + if (w) { + codes[n++] = between[below(sizeof(between) / sizeof(*between))]; + if (below(3)) codes[n++] = ' '; + } + unsigned length = 1 + below(12); + for (unsigned c = 0; c < length; ++c) { + codes[n++] = letters->items[below((uint32_t)letters->count)]; + if (marks->count && below(100) < (which < 5 ? 5u : 30u)) codes[n++] = marks->items[below((uint32_t)marks->count)]; + } + } + int direction = below(3) == 0 ? VEGA_BIDI_AUTO : below(2) ? VEGA_BIDI_LTR : VEGA_BIDI_RTL; + int32_t width = below(5) == 0 ? 0 : (int32_t)(shaping.upem * (1 + below(30))); + struct timespec a, b; + clock_gettime(CLOCK_MONOTONIC, &a); + if (!vega_layout(&layout, shaper, &shaping, codes, n, direction, width)) { + fail(k, "no layout"); + continue; + } + clock_gettime(CLOCK_MONOTONIC, &b); + seconds += (double)(b.tv_sec - a.tv_sec) + (double)(b.tv_nsec - a.tv_nsec) / 1e9; + lines += layout.line_count; + glyphs += layout.glyph_count; + characters += n; + vega_line_breaks(codes, n, breaks); + for (size_t i = 0; i < n; ++i) classes[i] = vega_bidi_class(codes[i]); + int paragraph = vega_bidi_levels(classes, codes, n, direction, levels); + size_t expect = 0; + for (size_t i = 0; i < layout.line_count; ++i) { + const VegaLine *line = &layout.lines[i]; + if (line->start != expect || line->end <= line->start || line->end > n) { + fail(k, "lines do not cover the paragraph in order"); + break; + } + expect = line->end; + for (size_t p = line->start + 1; p < line->end; ++p) + if (breaks[p] == VEGA_BREAK_MANDATORY) fail(k, "a line runs past a mandatory break"); + if (width && line->width > width) { + bool inside = false; + for (size_t p = line->start + 1; p < line->end && !inside; ++p) { + /* an opportunity after only spaces is no way to shorten the line */ + bool spaces_only = true; + for (size_t q = p; q < line->end && spaces_only; ++q) spaces_only = whitespace(classes[q]); + inside = breaks[p] != VEGA_BREAK_NONE && !spaces_only; + } + if (inside) fail(k, "a line wider than asked though it could break inside"); + } + for (size_t g = line->first; g < line->first + line->count; ++g) + if (layout.glyphs[g].cluster < line->start || layout.glyphs[g].cluster >= line->end) + fail(k, "a glyph's cluster is outside its line"); + /* L1 at the line's end, then the characters' visual order. */ + memcpy(line_levels, levels + line->start, line->end - line->start); + for (size_t i2 = line->end, trailing = 1; i2-- > line->start;) { + VegaBidiClass c = classes[i2]; + if (c == VEGA_BIDI_S || c == VEGA_BIDI_B) line_levels[i2 - line->start] = (uint8_t)paragraph, trailing = 1; + else if (trailing_kind(c)) { + if (trailing && line_levels[i2 - line->start] != VEGA_BIDI_REMOVED) + line_levels[i2 - line->start] = (uint8_t)paragraph; + } else + trailing = 0; + } + size_t shown = vega_bidi_order(line_levels, line->end - line->start, order); + for (size_t r = 0; r < line->end - line->start; ++r) rank[r] = UINT32_MAX; + for (size_t r = 0; r < shown; ++r) rank[order[r]] = (uint32_t)r; + uint32_t last = 0; + for (size_t g = line->first; g < line->first + line->count; ++g) { + uint32_t r = rank[layout.glyphs[g].cluster - line->start]; + if (r == UINT32_MAX) continue; + if (r < last) { + fail(k, "glyphs out of the characters' visual order"); + break; + } + last = r; + } + } + if (expect != n) fail(k, "lines end before the paragraph does"); + } + printf("{\"paragraphs\":%zu,\"characters\":%zu,\"lines\":%zu,\"glyphs\":%zu,\"ns_per_character\":%.1f," + "\"failures\":%u}\n", + paragraphs, characters, lines, glyphs, characters ? 1e9 * seconds / (double)characters : 0, failures); + vega_layout_free(&layout); + vega_shaper_free(shaper); + vega_shaping_close(&shaping); + free(bytes); + return failures ? 1 : 0; +} |
