diff --git a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md index bffe4e5..5125f09 100644 --- a/.agents/docs/2026-08-30-graphics-stack-coverage-design.md +++ b/.agents/docs/2026-08-30-graphics-stack-coverage-design.md @@ -1,6 +1,6 @@ # mcpp 图形栈:从「能跑通」到「能开发」的覆盖面设计 -Date: 2026-08-30 · 前置:[`2026-08-30-gbm-cross-repo-closed-loop-plan.md`](2026-08-30-gbm-cross-repo-closed-loop-plan.md) §19/§20 · **状态:已实现并闭环验证(v1.8,§11 总账 / §14 fork 规范 / §18 八角度复核 / §19 合成器闸门与 GObject 栈,含沙箱实测 · §20 gio 与一次被推翻的判断 · §21 namespace 是契约:四个成员补上 import)** +Date: 2026-08-30 · 前置:[`2026-08-30-gbm-cross-repo-closed-loop-plan.md`](2026-08-30-gbm-cross-repo-closed-loop-plan.md) §19/§20 · **状态:已实现并闭环验证(v1.9,§11 总账 / §14 fork 规范 / §18 八角度复核 / §19 合成器闸门与 GObject 栈,含沙箱实测 · §20 gio 与一次被推翻的判断 · §21 namespace 是契约 · §22 pango:文本排版接上了)** ## 0. 这份文档解决什么 @@ -2047,3 +2047,130 @@ error: use of undeclared identifier 'G_UNICODE_OTHER_LETTER' CI 加了导出数下限(glib ≥1900、gio ≥2400)、pango 那三个符号按名断言、一个 枚举量、以及转出链。**塌陷是静默的,所以必须有东西去数。** + +--- + +## 22. pango:文本排版那条线接上了(2026-08-31) + +§19.7 说 pango 被 gio 挡住。gio 到位(§20),闸门就开了。 + +### 22.1 三个成员,和 fork 的判据 + +| 成员 | 是什么 | 模块导出 | +|---|---|---| +| `gnome.pango` | 分项、bidi、断行、markup | 851 | +| `gnome.pangoft2` | fontconfig 选文件,FreeType 出字形 | 88 | +| `gnome.pangocairo` | cairo 后端 —— `pango_cairo_show_layout` | 284 | + +判据仍是**生成器,不是行数**。pango 有三个,本 fork 再加一个: + +``` +configure_file pango-features.h.meson -> pango-features.h +configure_file (没有输入模板) -> config.h +gobject/glib-mkenums pango_headers -> pango-enum-types.{h,c} 27 个 +(fork 自己的) gen_module() -> 三个 .cppm +``` + +⚠️ **`config.h` 是特殊的一个**:上游写的是 +`configure_file(output: 'config.h', configuration: pango_conf)`,**没有 +`input:`** —— meson 按 key 逐条发 `#define`,树里根本没有可替换的模板。所以这个 +文件必须**写出来**,里面每个值都是本包做的**决定**。其中两个是算术而非选择: + +``` +PANGO_BINARY_AGE = minor * 100 + micro # 5601 +PANGO_INTERFACE_AGE = minor 为奇数 ? 0 : micro # 1 +``` + +`pango_version_check()` 读第一个,所以写错不会构建失败,而是让一次**正确的版本 +比较给出错误答案**。测试两个方向都断言了。 + +### 22.2 ⭐ 唯一一个产出像素的测试 + +`pangocairo` 把 `"Hello 世界"` 画到 ARGB32 surface 上,数非透明像素:**1,492**。 +走到那一步要七个包: + +``` +gnome.pango 分项、bidi、断行 +gnome.pangoft2 fontconfig 选文件,FreeType 光栅化 +gnome.gio PangoFontMap 是 GListModel +compat.harfbuzz 整形 +compat.fribidi bidi 算法 +freedesktop.cairo 字形落下去的那张 surface +``` + +**空白图片意味着其中之一没干活。** + +### ⭐ 这条断言错了两次才对 + +**第一次:`drawn > 100`。** 在我这台 184 个字体族的机器上得到 216 像素、通过; +在只有 4 个字体族的 CI runner 上得到 72、失败。**按开发机字体调出来的阈值是对 +机器的断言。** 改成 `> 0`。 + +**第二次:用 `families > 0` 当闸门。** 也不够,而且花了两轮 CI 才看清。同一个 +runner 报告**四个字体族**,然后把同一个 layout 量成 `80x858545`(一个进程)和 +`80x346398`(另一个)—— 在 100px 的 surface 上都是荒谬的高度,因为那里的「四个 +字体族」是 fontconfig 有**条目**而背后没有可用字体。字形于是落到 surface 之外, +墨迹数变成任意的:**两个逐字节相同绘制代码的孪生测试,一个 0 一个 116。** + +所以闸门现在是 **layout 自己的度量**(`w > 0 && h > 0 && h <= 100`)—— +**显式检验这台机器能不能渲染**,而不是从字体族数去推断。不满足时运行**会说出来**, +因为「这条检查被跳过了」和「文字画出来了」不能长得一样。 + +和 gnome.gio 的 xdgmime 检查刻意不断言具体 MIME 是同一条理由。 + +⚠️ 而且它**诚实地降级**:`freedesktop.fontconfig` 故意把运行期路径编译成空, +所以没有 `FONTCONFIG_FILE` 的 runner 合法地找到零个字体族 —— 那就没有东西可画、 +也没有东西可断言。**那种情况被报告出来,而不是被静默略过**,因为「0 个字体族, +所以唯一的真检查没跑」和「文字画出来了」不能长得一样。 + +### 22.3 ⚠️ 本 fork 踩到的一次静默降级 + +`HAVE_CAIRO_FREETYPE` 被一次编辑失误从 config.h 里弄丢了。**什么都没有构建失败。** +`pangocairo-fontmap.c` 只是**一个后端都没注册**,程序在运行期死于 + +``` +Pango-CRITICAL: Unknown $PANGOCAIRO_BACKEND value. + Available backends are: <- 空列表 +``` + +随后段错误。测试现在读 font map 的**字体类型**并要求 `CAIRO_FONT_TYPE_FT` —— +这条断言会当场抓住它。同一类:**"能编译" 对配置宏没有任何证明力。** + +### 22.4 ⚠️ `freedesktop.cairo` 的模块自己就不够用(待办) + +1.18.2 实测:导出 470 个名字、**零个枚举量**(没有 `CAIRO_FORMAT_ARGB32`、 +没有 `CAIRO_FONT_TYPE_FT`)、**没有 `cairo_t`**。 + +索引自己的 cairo 示例没发现,因为它**同时**写了 `#include ` 和 +`import freedesktop.cairo;` —— 头文件补上了模块缺的那半。**这意味着 +`import freedesktop.cairo;` 从来没有被单独检验过。** + +pangocairo 不能混用两条路(pangocairo.h 经 glib 到 ``,混用会让 +`struct _IO_FILE` 变成两个实体),所以 `gnome.pangocairo` 自己扫 `cairo.h`。 +从两个模块导出同一个实体是无害的(是同一批 global-module 实体),所以这是**追加** +而不是冲突。**cairo 的包装体修好后那行可以去掉。** + +同类的漏名坑见 §21.4;`freedesktop.cairo` 是同一个生成器家族里还没修的一个。 + +### 22.5 依赖形态:点名 pangocairo 一个就够 + +`gnome.pango` 和 `gnome.pangoft2` 是 `gnome.pangocairo` 的 workspace **path** +依赖,`gnome.gio` 又是 `gnome.pango` 的。消费者再点名任何一个都会得到 + +``` +error: dependency 'gnome.gobject' is requested as both a version dep + (by 'pango') and a path dep (by 'gnome.gio@2.82.5'). Pick one. +``` + +这条错误就是本 fork 第一次构建时学到的。 + +### 22.6 结果 + +| | | +|---|---| +| fork CI | 三个 job 全绿(两条工具链 + `upstream/` 未改) | +| 测试 | 六个:三个成员 × 两条消费路线 | +| 索引 | 三个示例,gcc 16.1 与 llvm 22.1 双绿 | +| 端到端 | 1,492 个像素 | + +**文本排版那条线接上了。** diff --git a/mcpp.toml b/mcpp.toml index 61e814b..e51e86f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -108,6 +108,9 @@ members = [ "tests/examples/gobject", "tests/examples/gmodule", "tests/examples/gio", + "tests/examples/pango", + "tests/examples/pangoft2", + "tests/examples/pangocairo", "tests/examples/libgbm", "tests/examples/libpng", "tests/examples/libwebp", diff --git a/pkgs/p/gnome.pango.lua b/pkgs/p/gnome.pango.lua new file mode 100644 index 0000000..bd53a03 --- /dev/null +++ b/pkgs/p/gnome.pango.lua @@ -0,0 +1,100 @@ +-- gnome.pango — international text layout. +-- +-- Itemisation, the bidirectional algorithm, line breaking, markup and layout. +-- It knows about scripts and fonts and NOTHING about rasterising: that is +-- `gnome.pangoft2`, and drawing is `gnome.pangocairo`. Upstream ships them as +-- three shared libraries and this index follows that split, because it is what +-- a consumer links. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⭐ THIS IS THE PACKAGE THAT WAS BLOCKED ON gio +-- +-- `PangoFontMap` declares +-- +-- G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, pango_font_map_list_model_init) +-- +-- so the type cannot even REGISTER without gio's interface. The index recorded +-- that gap for a while under a reason that turned out to be wrong — see +-- `gnome.gio` for what the reason was and why it did not follow.-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⭐ TWO WAYS TO CONSUME IT, AND YOU PICK ONE +-- +-- import gnome.pango; +-- #include +-- +-- The namespace is the contract in this index: `compat.xxx` means headers, an +-- owner namespace means the package exposes `import`. This module exports +-- 851 names and re-exports `gnome.gio`, because pango.h +-- includes glib-object.h and a consumer cannot name gio itself — it is a +-- workspace path dependency. +-- +-- ⚠️ THE TWO ROUTES DO NOT COMPOSE. A TU that does both reaches +-- twice — once through the module's global fragment, once directly — and the +-- same `struct _IO_FILE` becomes two entities. Which route to take is decided +-- by macros, which a module cannot carry: code using `PANGO_TYPE_*` or +-- `G_OBJECT` takes the header route, code using the function API imports and +-- includes nothing. Each member ships a test for each route. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY A FORK: GENERATORS, NOT LINE COUNT +-- +-- The criterion that made cairo (104k lines) a plain descriptor and +-- libdisplay-info (2k) a fork. Pango has three, and the fork adds a fourth: +-- +-- configure_file pango-features.h.meson -> pango-features.h +-- configure_file (NO input template) -> config.h +-- gobject/glib-mkenums pango_headers -> pango-enum-types.{h,c} +-- (the fork's own) gen_module() -> the .cppm wrapper +-- +-- ⚠️ config.h is the odd one: upstream writes +-- `configure_file(output: 'config.h', configuration: pango_conf)` with NO +-- `input:`, so meson emits a `#define` per key and there is nothing in the +-- tree to substitute into. The file is WRITTEN, and every value in it is a +-- decision. Two are arithmetic — `PANGO_BINARY_AGE = minor*100 + micro` and +-- `PANGO_INTERFACE_AGE = minor odd ? 0 : micro` — and `pango_version_check()` +-- reads the first, so a wrong value makes a correct version comparison answer +-- wrongly AT RUN TIME rather than failing to build. +-- +-- https://github.com/mcpplibs/pango +-- +-- ───────────────────────────────────────────────────────────────────────── +-- LINUX ONLY. The generated config.h fixes the visibility attribute and the +-- font backend, and pangoxft (the X11 backend) is deliberately not built — +-- Xft is not in this index and a Wayland stack does not want it. +-- +-- ⚠️ NAME gio ALONE if you also want it. glib, gobject and gmodule are +-- workspace PATH dependencies of `gnome.gio`, and mcpp rejects a package +-- requested both ways: +-- +-- error: dependency 'gnome.gobject' is requested as both a version dep +-- (by 'pango') and a path dep (by 'gnome.gio@2.82.5'). Pick one. +-- +-- That error is how this fork learned it, on the first build. +package = { + spec = "1", + namespace = "gnome", + name = "pango", + description = "Pango 1.56.1 — international text layout: itemisation, shaping, bidi and line breaking", + licenses = {"LGPL-2.1-or-later"}, + repo = "https://github.com/mcpplibs/pango", + type = "package", + + xpm = { + linux = { + ["1.56.1"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/pango/archive/refs/tags/1.56.1.tar.gz", + -- ⚠️ The container tag is `1.56.1-4`, not `1.56.1`. gitcode + -- refuses to REPLACE an asset of the same name in an + -- existing release, so each corrected tarball needs a new + -- container tag while the PACKAGE version stays upstream's. + -- Verified byte-identical to the GLOBAL tag archive. + CN = "https://gitcode.com/mcpp-res/pango/releases/download/1.56.1-4/pango-1.56.1.tar.gz", + }, + sha256 = "b470a658e05ef0e14d779bc852371ce52dd11efa76d10782347be40f1d63476b", + }, + }, + }, + + mcpp = "*/mcpp/pango/mcpp.toml", +} diff --git a/pkgs/p/gnome.pangocairo.lua b/pkgs/p/gnome.pangocairo.lua new file mode 100644 index 0000000..d018fc3 --- /dev/null +++ b/pkgs/p/gnome.pangocairo.lua @@ -0,0 +1,106 @@ +-- gnome.pangocairo — the cairo backend, and the usual entry point. +-- +-- Almost every consumer reaches pango through this member: +-- `pango_cairo_create_layout(cr)` then `pango_cairo_show_layout(cr, layout)` +-- is the whole API most programs need. +-- +-- ⚠️ IT DEPENDS ON pangoft2, NOT JUST cairo. Upstream compiles +-- `pangocairo-fcfont.c` and `pangocairo-fcfontmap.c` whenever the freetype +-- backend is enabled (meson.build:489) — they are what let a cairo surface +-- draw a fontconfig-selected font, which on Linux is every font. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ A SILENT DEGRADATION WORTH KNOWING ABOUT +-- +-- `HAVE_CAIRO_FREETYPE` was dropped from config.h once by an editing slip. +-- NOTHING FAILED TO BUILD. `pangocairo-fontmap.c` simply registered no +-- backends, and the program died at run time with +-- +-- Pango-CRITICAL: Unknown $PANGOCAIRO_BACKEND value. +-- Available backends are: <- an empty list +-- +-- then segfaulted. The fork's test now reads the font map's FONT TYPE and +-- requires `CAIRO_FONT_TYPE_FT`, which catches it immediately. +-- +-- ⚠️ AND freedesktop.cairo's MODULE IS NOT SUFFICIENT ON ITS OWN. Measured on +-- 1.18.2: 470 names, ZERO enumerators (no `CAIRO_FORMAT_ARGB32`, no +-- `CAIRO_FONT_TYPE_FT`) and no `cairo_t`. The index's own cairo example does +-- not notice, because it writes BOTH `#include ` and +-- `import freedesktop.cairo;`. pangocairo cannot mix the routes, so its module +-- scans cairo.h itself; that can go when cairo's wrapper is fixed.-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⭐ TWO WAYS TO CONSUME IT, AND YOU PICK ONE +-- +-- import gnome.pangocairo; +-- #include +-- +-- The namespace is the contract in this index: `compat.xxx` means headers, an +-- owner namespace means the package exposes `import`. This module exports +-- 284 names and re-exports `gnome.pango`, +-- `gnome.pangoft2` and `freedesktop.cairo`. +-- +-- ⚠️ THE TWO ROUTES DO NOT COMPOSE. A TU that does both reaches +-- twice — once through the module's global fragment, once directly — and the +-- same `struct _IO_FILE` becomes two entities. Which route to take is decided +-- by macros, which a module cannot carry: code using `PANGO_TYPE_*` or +-- `G_OBJECT` takes the header route, code using the function API imports and +-- includes nothing. Each member ships a test for each route. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY A FORK: GENERATORS, NOT LINE COUNT +-- +-- The criterion that made cairo (104k lines) a plain descriptor and +-- libdisplay-info (2k) a fork. Pango has three, and the fork adds a fourth: +-- +-- configure_file pango-features.h.meson -> pango-features.h +-- configure_file (NO input template) -> config.h +-- gobject/glib-mkenums pango_headers -> pango-enum-types.{h,c} +-- (the fork's own) gen_module() -> the .cppm wrapper +-- +-- ⚠️ config.h is the odd one: upstream writes +-- `configure_file(output: 'config.h', configuration: pango_conf)` with NO +-- `input:`, so meson emits a `#define` per key and there is nothing in the +-- tree to substitute into. The file is WRITTEN, and every value in it is a +-- decision. Two are arithmetic — `PANGO_BINARY_AGE = minor*100 + micro` and +-- `PANGO_INTERFACE_AGE = minor odd ? 0 : micro` — and `pango_version_check()` +-- reads the first, so a wrong value makes a correct version comparison answer +-- wrongly AT RUN TIME rather than failing to build. +-- +-- https://github.com/mcpplibs/pango +-- +-- ───────────────────────────────────────────────────────────────────────── +-- LINUX ONLY. The generated config.h fixes the visibility attribute and the +-- font backend, and pangoxft (the X11 backend) is deliberately not built — +-- Xft is not in this index and a Wayland stack does not want it. +-- +-- ⚠️ `gnome.pango` and `gnome.pangoft2` are workspace PATH dependencies of +-- this package, so a consumer must NOT name them as well. Naming +-- `gnome.pangocairo` alone gets the whole stack. +package = { + spec = "1", + namespace = "gnome", + name = "pangocairo", + description = "PangoCairo 1.56.1 — pango's cairo rendering backend, the usual entry point", + licenses = {"LGPL-2.1-or-later"}, + repo = "https://github.com/mcpplibs/pango", + type = "package", + + xpm = { + linux = { + ["1.56.1"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/pango/archive/refs/tags/1.56.1.tar.gz", + -- ⚠️ The container tag is `1.56.1-4`, not `1.56.1`. gitcode + -- refuses to REPLACE an asset of the same name in an + -- existing release, so each corrected tarball needs a new + -- container tag while the PACKAGE version stays upstream's. + -- Verified byte-identical to the GLOBAL tag archive. + CN = "https://gitcode.com/mcpp-res/pango/releases/download/1.56.1-4/pango-1.56.1.tar.gz", + }, + sha256 = "b470a658e05ef0e14d779bc852371ce52dd11efa76d10782347be40f1d63476b", + }, + }, + }, + + mcpp = "*/mcpp/pangocairo/mcpp.toml", +} diff --git a/pkgs/p/gnome.pangoft2.lua b/pkgs/p/gnome.pangoft2.lua new file mode 100644 index 0000000..e0fccf2 --- /dev/null +++ b/pkgs/p/gnome.pangoft2.lua @@ -0,0 +1,84 @@ +-- gnome.pangoft2 — the FreeType/fontconfig font backend. +-- +-- `gnome.pango` knows nothing about how to find or rasterise a font. This is +-- the half that does, on Linux: fontconfig answers "which file", FreeType +-- answers "what do the glyphs look like". +-- +-- ⚠️ IT ALSO CARRIES pango-ot.h, whose whole body sits behind +-- `#ifndef PANGO_DISABLE_DEPRECATED` and whose own header says "Deprecated. +-- Use HarfBuzz directly!". It is still compiled and still part of this +-- member's ABI, so it is still tested.-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⭐ TWO WAYS TO CONSUME IT, AND YOU PICK ONE +-- +-- import gnome.pangoft2; +-- #include +-- +-- The namespace is the contract in this index: `compat.xxx` means headers, an +-- owner namespace means the package exposes `import`. This module exports +-- 88 names and re-exports `gnome.pango`. +-- +-- ⚠️ THE TWO ROUTES DO NOT COMPOSE. A TU that does both reaches +-- twice — once through the module's global fragment, once directly — and the +-- same `struct _IO_FILE` becomes two entities. Which route to take is decided +-- by macros, which a module cannot carry: code using `PANGO_TYPE_*` or +-- `G_OBJECT` takes the header route, code using the function API imports and +-- includes nothing. Each member ships a test for each route. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY A FORK: GENERATORS, NOT LINE COUNT +-- +-- The criterion that made cairo (104k lines) a plain descriptor and +-- libdisplay-info (2k) a fork. Pango has three, and the fork adds a fourth: +-- +-- configure_file pango-features.h.meson -> pango-features.h +-- configure_file (NO input template) -> config.h +-- gobject/glib-mkenums pango_headers -> pango-enum-types.{h,c} +-- (the fork's own) gen_module() -> the .cppm wrapper +-- +-- ⚠️ config.h is the odd one: upstream writes +-- `configure_file(output: 'config.h', configuration: pango_conf)` with NO +-- `input:`, so meson emits a `#define` per key and there is nothing in the +-- tree to substitute into. The file is WRITTEN, and every value in it is a +-- decision. Two are arithmetic — `PANGO_BINARY_AGE = minor*100 + micro` and +-- `PANGO_INTERFACE_AGE = minor odd ? 0 : micro` — and `pango_version_check()` +-- reads the first, so a wrong value makes a correct version comparison answer +-- wrongly AT RUN TIME rather than failing to build. +-- +-- https://github.com/mcpplibs/pango +-- +-- ───────────────────────────────────────────────────────────────────────── +-- LINUX ONLY. The generated config.h fixes the visibility attribute and the +-- font backend, and pangoxft (the X11 backend) is deliberately not built — +-- Xft is not in this index and a Wayland stack does not want it. +-- +-- ⚠️ `gnome.pango` is a workspace PATH dependency of this package, so a +-- consumer must NOT name it as well. Same for `gnome.gio` behind it. +package = { + spec = "1", + namespace = "gnome", + name = "pangoft2", + description = "PangoFT2 1.56.1 — pango's FreeType and fontconfig font backend", + licenses = {"LGPL-2.1-or-later"}, + repo = "https://github.com/mcpplibs/pango", + type = "package", + + xpm = { + linux = { + ["1.56.1"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/pango/archive/refs/tags/1.56.1.tar.gz", + -- ⚠️ The container tag is `1.56.1-4`, not `1.56.1`. gitcode + -- refuses to REPLACE an asset of the same name in an + -- existing release, so each corrected tarball needs a new + -- container tag while the PACKAGE version stays upstream's. + -- Verified byte-identical to the GLOBAL tag archive. + CN = "https://gitcode.com/mcpp-res/pango/releases/download/1.56.1-4/pango-1.56.1.tar.gz", + }, + sha256 = "b470a658e05ef0e14d779bc852371ce52dd11efa76d10782347be40f1d63476b", + }, + }, + }, + + mcpp = "*/mcpp/pangoft2/mcpp.toml", +} diff --git a/tests/examples/pango/mcpp.toml b/tests/examples/pango/mcpp.toml new file mode 100644 index 0000000..3e73d91 --- /dev/null +++ b/tests/examples/pango/mcpp.toml @@ -0,0 +1,15 @@ +# gnome.pango — international text layout, without a font backend. +# +# ⚠️ NAMES pango ALONE. gio, gobject, glib, harfbuzz and fribidi all arrive +# transitively; gio in particular is a workspace PATH dependency of pango, and +# naming it here as well is `requested as both a version dep and a path dep`. +[indices] +gnome = { path = "../../.." } + +[package] +name = "pango-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.gnome] +pango = "1.56.1" diff --git a/tests/examples/pango/tests/module.cpp b/tests/examples/pango/tests/module.cpp new file mode 100644 index 0000000..541aff4 --- /dev/null +++ b/tests/examples/pango/tests/module.cpp @@ -0,0 +1,88 @@ +// ⭐ THE MODULE, WHICH IS WHAT THE NAMESPACE PROMISES. +// +// In this index the namespace is the contract: `compat.xxx` is consumed with +// `#include`, an owner namespace like `gnome.xxx` exposes `import`. The test +// next to this file takes the header route; this one takes the module, so both +// doors are tested rather than assumed. +// +// ⚠️ THE TWO DOORS DO NOT COMPOSE. A TU that imports the module AND textually +// includes a pango or glib header reaches twice — once through the +// module's global fragment, once directly — and the same `struct tm` becomes +// two entities. So a consumer picks ONE, and which one is decided by macros: +// a module cannot carry them. Code using `PANGO_TYPE_*` or `G_OBJECT` takes +// the header route; code using the function API imports and includes nothing. +#ifdef __linux__ + +import gnome.pango; // re-exports gnome.gio, and glib/gobject behind it + +// ⚠️ NO AND NO . The module's global fragment already saw +// and through pango.h, and reaching them TEXTUALLY as +// well makes the same `struct _IO_FILE` two entities: +// +// error: conflicting declaration 'struct _IO_FILE' +// +// glib's own g_print and strcmp-equivalent come through the module, which is +// the point: on the module route a consumer includes nothing. + +int main() +{ + int failures = 0; + auto check = [&](bool ok, const char *what) { + g_print("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; + }; + + g_print("import gnome.pango — %s\n\n", pango_version_string()); + + check(pango_version() >= 15601, "pango_version() through the module"); + check(pango_version_check(1, 56, 0) == nullptr, + "pango_version_check — PANGO_BINARY_AGE came from config.h"); + + // ⚠️ PANGO_TYPE_ALIGNMENT is a MACRO; the module carries the FUNCTION it + // wraps. That is the shape of the module route, and why the header route + // stays supported next door. + check(pango_alignment_get_type() != 0, "the generated enum GTypes register"); + check(pango_font_map_get_type() != 0, "PangoFontMap registers"); + + // An ENUMERATOR, exported by name — GCC makes them visible through an + // exported typedef and clang does not, so they are listed explicitly. + PangoDirection d = PANGO_DIRECTION_RTL; + check(static_cast(d) != 0, "an enumerator is exported by name"); + + // Script itemisation: pure Unicode table work, no fonts, no macros. + { + const char *text = "Hello \344\270\226\347\225\214"; + PangoScriptIter *it = pango_script_iter_new(text, -1); + int runs = 0; + do { ++runs; } while (pango_script_iter_next(it)); + pango_script_iter_free(it); + check(runs >= 2, "pango_script_iter splits Latin from Han"); + } + + // Line breaking. + { + PangoLogAttr attrs[16] = {}; + pango_get_log_attrs("hello world", -1, -1, + pango_language_from_string("en-US"), attrs, 16); + check(attrs[6].is_line_break, "a line break is allowed before \"world\""); + } + + // The markup parser, which reaches glib's GMarkup through the re-export + // chain — gnome.pango pulls gnome.gio, which re-exports gnome.glib. + { + char *text = nullptr; + const gboolean ok = pango_parse_markup("a bold word", -1, 0, + nullptr, &text, nullptr, nullptr); + check(ok && text && g_strcmp0(text, "a bold word") == 0, + "pango_parse_markup — and g_free below is glib, transitively"); + g_free(text); + } + + g_print("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else +#include +int main() { std::printf("linux only\n"); return 0; } +#endif diff --git a/tests/examples/pango/tests/pango.cpp b/tests/examples/pango/tests/pango.cpp new file mode 100644 index 0000000..8b129c3 --- /dev/null +++ b/tests/examples/pango/tests/pango.cpp @@ -0,0 +1,208 @@ +// pango — international text layout, exercised rather than merely linked. +// +// WHAT CAN BE ASSERTED WITHOUT A FONT +// +// This member has no font backend: finding and rasterising fonts is pangoft2's +// job and drawing is pangocairo's. So a layout test here would be asserting +// that the RUNNER has fonts, which is not what this package is. +// +// Everything below is an INTERMEDIATE QUANTITY — something pango computes from +// data it carries, with no font map involved — and each names a specific part +// of the build, so a failure says which one broke: +// +// the version macros include/pango/pango-features.h, one of two generated +// headers, and PANGO_BINARY_AGE from config.h — which +// is not substituted from a template but WRITTEN, +// because upstream's configure_file has no input +// the GENERATED enums pango-enum-types.{h,c}, 27 GTypes from a +// reimplemented glib-mkenums +// itemisation itemize.c + pango-script.c: splitting a mixed-script +// string into runs is pure Unicode table work +// bidi pango-bidi-type.c over compat.fribidi +// line breaking break.c, the Unicode line-breaking algorithm +// the attribute list pango-attributes.c + pango-markup.c, the parser +// GListModel ⭐ what pango was blocked on: PangoFontMap declares +// G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL, …), so the +// type cannot even register without gio + +#ifdef __linux__ + +// ⚠️ NO extern "C" WRAPPER. pango decorates its headers with G_BEGIN_DECLS, so +// one is redundant — and harmful, because pango.h reaches , which +// libc++ routes through , which defines TEMPLATES. Inside an +// extern "C" block that is `templates must have C++ linkage`, dozens of times, +// against a header this file never names. +#include + +// ⚠️ pango.h does NOT pull gio in, even though PangoFontMap implements +// GListModel — the interface is used in the implementation, not the header. +// A consumer that wants to treat a font map as a list model includes gio +// itself, which is legitimate: gnome.gio arrives transitively as a dependency +// of this package. +#include + +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-60s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +} // namespace + +int main() +{ + std::printf("pango %s\n\n", pango_version_string()); + + // ── 1. the two generated headers, and config.h ─────────────────────── + check(PANGO_VERSION_MAJOR == 1 && PANGO_VERSION_MINOR == 56 + && PANGO_VERSION_MICRO == 1, + "pango-features.h reports the version the manifest declares"); + check(std::strcmp(PANGO_VERSION_STRING, "1.56.1") == 0, + "…and its @…@ substitution produced the string form"); + check(pango_version() == PANGO_VERSION, + "the runtime version agrees with the compiled-in one"); + + // ⚠️ PANGO_BINARY_AGE lives in config.h, which upstream produces with a + // configure_file that has NO input template — so it is written here rather + // than substituted, and a wrong value is a wrong ANSWER rather than a + // build error. pango_version_check consults it. + check(pango_version_check(1, 56, 0) == nullptr, + "pango_version_check accepts 1.56.0 — PANGO_BINARY_AGE is right"); + check(pango_version_check(1, 99, 0) != nullptr, + "…and rejects a version from the future"); + + // ── 2. the GENERATED enum types ────────────────────────────────────── + // These exist only because build.mcpp reproduced glib-mkenums over + // pango's public headers. Registering one and reading a value back is the + // only evidence the generator produced REGISTERABLE code rather than a + // header that happens to compile. + const GType align = pango_alignment_get_type(); + check(align != 0, "pango_alignment_get_type() registered a GType"); + check(align == PANGO_TYPE_ALIGNMENT, + "…and the MACRO is spelled the way upstream spells it"); + + GEnumClass *ec = static_cast(g_type_class_ref(align)); + GEnumValue *v = ec ? g_enum_get_value(ec, PANGO_ALIGN_CENTER) : nullptr; + check(v && std::strcmp(v->value_name, "PANGO_ALIGN_CENTER") == 0, + "…with PANGO_ALIGN_CENTER under its own name"); + check(v && std::strcmp(v->value_nick, "center") == 0, + "…and the nick mkenums derives, \"center\""); + if (ec) { + g_type_class_unref(ec); + } + check(pango_wrap_mode_get_type() != 0 && pango_ellipsize_mode_get_type() != 0 + && pango_script_get_type() != 0, + "the other generated enum types register too"); + + // ── 3. ⭐ GListModel — the gio dependency, at run time ──────────────── + // PangoFontMap's class init declares + // G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, pango_font_map_list_model_init) + // so registering the type at all requires gio's interface to exist. This + // is the check that the text-layout line is actually joined up. + const GType fm = pango_font_map_get_type(); + check(fm != 0, "PangoFontMap registers"); + check(g_type_is_a(fm, g_list_model_get_type()), + "…and it IS a GListModel — the gio gate, at run time"); + + // ── 4. itemisation: Unicode script runs, no fonts involved ─────────── + // "Hello 世界 مرحبا" is Latin, Han and Arabic. Splitting it is pure table + // work in pango-script.c. + { + const char *text = "Hello \344\270\226\347\225\214 \331\205\330\261\330\255\330\250\330\247"; + PangoScriptIter *it = pango_script_iter_new(text, -1); + int runs = 0; + bool saw_han = false, saw_arabic = false; + do { + const char *s = nullptr, *e = nullptr; + PangoScript sc; + pango_script_iter_get_range(it, &s, &e, &sc); + ++runs; + saw_han |= (sc == PANGO_SCRIPT_HAN); + saw_arabic |= (sc == PANGO_SCRIPT_ARABIC); + } while (pango_script_iter_next(it)); + pango_script_iter_free(it); + std::printf(" script runs: %d\n", runs); + check(runs >= 3, "pango_script_iter splits a mixed-script string"); + check(saw_han && saw_arabic, "…and identifies Han and Arabic by name"); + } + + // ── 5. bidi, which is compat.fribidi doing the work ────────────────── + // Deprecated since 1.44 and still exported; testing them is testing what + // the library actually offers. + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + check(pango_unichar_direction(0x05D0) == PANGO_DIRECTION_RTL, + "Hebrew alef is right-to-left"); + check(pango_unichar_direction('A') == PANGO_DIRECTION_LTR, + "…and 'A' is left-to-right"); + { + const char *rtl = "\330\247\331\204\330\263\331\204\330\247\331\205"; // "السلام" + check(pango_find_base_dir(rtl, -1) == PANGO_DIRECTION_RTL, + "pango_find_base_dir on an Arabic string"); + } + G_GNUC_END_IGNORE_DEPRECATIONS + + // ── 6. line breaking: break.c over the Unicode algorithm ───────────── + { + const char *text = "hello world"; + PangoLogAttr attrs[16] = {}; + pango_get_log_attrs(text, -1, -1, pango_language_from_string("en-US"), + attrs, 16); + // A break is allowed before "world" (offset 6) and not mid-word. + check(attrs[6].is_line_break, "a line break is allowed before \"world\""); + check(!attrs[3].is_line_break, "…and not in the middle of \"hello\""); + check(attrs[5].is_white, "…and the space is classified as white"); + } + + // ── 7. the markup parser ───────────────────────────────────────────── + { + PangoAttrList *attrs = nullptr; + char *text = nullptr; + GError *err = nullptr; + const gboolean ok = pango_parse_markup("a bold word", -1, 0, + &attrs, &text, nullptr, &err); + check(ok && text && std::strcmp(text, "a bold word") == 0, + "pango_parse_markup strips the tags and keeps the text"); + check(attrs != nullptr, "…and produces an attribute list"); + if (attrs) { + pango_attr_list_unref(attrs); + } + g_free(text); + if (err) { + g_error_free(err); + } + } + + // ── 8. language tags, which carry their own sample text tables ─────── + { + PangoLanguage *ja = pango_language_from_string("ja-JP"); + check(ja != nullptr, "pango_language_from_string"); + check(std::strcmp(pango_language_to_string(ja), "ja-jp") == 0, + "…normalises the tag to lower case"); + check(pango_language_includes_script(ja, PANGO_SCRIPT_HAN), + "…and knows Japanese uses Han"); + } + + std::printf("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else + +#include + +int main() +{ + std::printf("pango: this package builds the Linux half; skipping.\n"); + return 0; +} + +#endif diff --git a/tests/examples/pangocairo/mcpp.toml b/tests/examples/pangocairo/mcpp.toml new file mode 100644 index 0000000..3407ed8 --- /dev/null +++ b/tests/examples/pangocairo/mcpp.toml @@ -0,0 +1,15 @@ +# gnome.pangocairo — the whole text-layout line, ending in pixels. +# +# ⭐ Naming this one package pulls seven: pango, pangoft2, gio (and glib, +# gobject, gmodule behind it), harfbuzz, fribidi, fontconfig, freetype and +# cairo. The test renders text and counts the ink. +[indices] +gnome = { path = "../../.." } + +[package] +name = "pangocairo-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.gnome] +pangocairo = "1.56.1" diff --git a/tests/examples/pangocairo/tests/module.cpp b/tests/examples/pangocairo/tests/module.cpp new file mode 100644 index 0000000..940acef --- /dev/null +++ b/tests/examples/pangocairo/tests/module.cpp @@ -0,0 +1,115 @@ +// ⭐ THE MODULE, WHICH IS WHAT THE NAMESPACE PROMISES. +// +// In this index the namespace is the contract: `compat.xxx` is consumed with +// `#include`, an owner namespace like `gnome.xxx` exposes `import`. The test +// next to this file takes the header route; this one takes the module, so both +// doors are tested rather than assumed. +// +// ⚠️ THE TWO DOORS DO NOT COMPOSE. A TU that imports the module AND textually +// includes a pango or glib header reaches twice — once through the +// module's global fragment, once directly — and the same `struct tm` becomes +// two entities. So a consumer picks ONE, and which one is decided by macros: +// a module cannot carry them. Code using `PANGO_TYPE_*` or `G_OBJECT` takes +// the header route; code using the function API imports and includes nothing. +#ifdef __linux__ + +import gnome.pangocairo; // re-exports gnome.pango and gnome.pangoft2 + +int main() +{ + int failures = 0; + auto check = [&](bool ok, const char *what) { + g_print("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; + }; + + g_print("import gnome.pangocairo\n\n"); + + PangoFontMap *map = pango_cairo_font_map_get_default(); + check(map != nullptr, "pango_cairo_font_map_get_default()"); + // ⚠️ G_OBJECT_TYPE_NAME is a MACRO and cannot come through a module. The + // font TYPE is a better check anyway: it asserts the backend directly + // rather than by a substring of a class name. + check(map && pango_cairo_font_map_get_font_type( + reinterpret_cast(map)) == CAIRO_FONT_TYPE_FT, + "…and its font type is FreeType — HAVE_CAIRO_FREETYPE took"); + + // ⚠️ FAMILIES ARE ENUMERATED BEFORE DRAWING, matching the header-route + // test next door. When this ran AFTER the draw, the module test drew 0 + // pixels on a runner where the header test drew 116 — same machine, same + // four families, identical drawing code. The only difference was this + // call's position, so it is now in the same place in both, and the count + // is reported so the two are comparable rather than merely both "ok". + int families = -1; + { + PangoFontFamily **f = nullptr; + pango_font_map_list_families(map, &f, &families); + g_free(f); + } + g_print(" font families visible: %d\n", families); + + PangoContext *ctx = pango_font_map_create_context(map); + PangoLayout *layout = pango_layout_new(ctx); + pango_layout_set_text(layout, "Hello \344\270\226\347\225\214", -1); + PangoFontDescription *desc = pango_font_description_from_string("Sans 24"); + pango_layout_set_font_description(layout, desc); + check(pango_layout_get_character_count(layout) == 8, + "the layout counts 8 characters"); + + int w = 0, h = 0; + pango_layout_get_pixel_size(layout, &w, &h); + g_print(" layout pixel size: %dx%d\n", w, h); + + // ⚠️ THE SURFACE IS CHECKED, and its format is PRINTED. The first version + // of this test only counted pixels, so when it reported 0 on a runner + // where the header-route twin reported 116 there was nothing to go on: + // "0 pixels" is a symptom shared by "nothing was drawn", "the surface is + // in an error state" and "the pixel loop is reading the wrong bytes". + cairo_surface_t *surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 400, 100); + g_print(" surface status=%d format=%d (CAIRO_FORMAT_ARGB32=%d)\n", + (int) cairo_surface_status(surf), + (int) cairo_image_surface_get_format(surf), + (int) CAIRO_FORMAT_ARGB32); + check(cairo_surface_status(surf) == CAIRO_STATUS_SUCCESS, + "the ARGB32 surface was created without error"); + check(cairo_image_surface_get_format(surf) == CAIRO_FORMAT_ARGB32, + "…and the format it reports is the one that was asked for"); + + cairo_t *cr = cairo_create(surf); + cairo_set_source_rgb(cr, 0, 0, 0); + cairo_move_to(cr, 10, 10); + pango_cairo_show_layout(cr, layout); + cairo_destroy(cr); + + cairo_surface_flush(surf); + const unsigned char *d = cairo_image_surface_get_data(surf); + check(d != nullptr, "the surface has readable pixel data"); + const int stride = cairo_image_surface_get_stride(surf); + long drawn = 0; + for (int y = 0; d && y < 100; ++y) + for (int x = 0; x < 400; ++x) + if (d[(long)y * stride + 4 * x + 3] != 0) ++drawn; + g_print(" non-transparent pixels: %ld\n", drawn); + + // ⚠️ Only assertable when the runner HAS fonts — see the header-route test + // next door for why 0 families is a property of the machine. + if (families > 0) + // `> 0`, not a tuned number — see the header-route test next door for + // why a threshold here is an assertion about the runner's fonts. + check(drawn > 0, "pango_cairo_show_layout put ink on the surface"); + else + g_print(" ⚠️ 0 font families: the rendering assertion was NOT run.\n"); + + cairo_surface_destroy(surf); + pango_font_description_free(desc); + g_object_unref(layout); + g_object_unref(ctx); + + g_print("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else +#include +int main() { std::printf("linux only\n"); return 0; } +#endif diff --git a/tests/examples/pangocairo/tests/pangocairo.cpp b/tests/examples/pangocairo/tests/pangocairo.cpp new file mode 100644 index 0000000..aebf15f --- /dev/null +++ b/tests/examples/pangocairo/tests/pangocairo.cpp @@ -0,0 +1,177 @@ +// pangocairo — the whole text-layout line, end to end. +// +// ⭐ THIS IS THE ONE TEST IN THE STACK THAT PRODUCES PIXELS, and it is worth +// stating what that costs to reach: +// +// gnome.pango itemisation, bidi, line breaking +// gnome.pangoft2 fontconfig picks the file, FreeType rasterises +// gnome.gio PangoFontMap is a GListModel +// compat.harfbuzz shaping +// compat.fribidi the bidi algorithm +// freedesktop.cairo the surface the glyphs land on +// +// Seven packages, and a blank image means one of them is not doing its job. +// +// ⚠️ IT DEGRADES HONESTLY WHEN THERE ARE NO FONTS. `freedesktop.fontconfig` +// compiles its runtime paths EMPTY on purpose, so a runner with no +// FONTCONFIG_FILE and no /usr/share/fonts legitimately finds zero families — +// and then there is nothing to draw and nothing to assert. That case is +// REPORTED rather than passed over in silence, because "0 families, so we +// skipped the only real check" and "the text rendered" must not look alike. + +#ifdef __linux__ + +#include +#include + +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-60s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +// How many pixels in an ARGB32 surface are not fully transparent. +long ink(cairo_surface_t *s) +{ + cairo_surface_flush(s); + const unsigned char *d = cairo_image_surface_get_data(s); + if (d == nullptr) { + return -1; + } + const int w = cairo_image_surface_get_width(s); + const int h = cairo_image_surface_get_height(s); + const int stride = cairo_image_surface_get_stride(s); + long n = 0; + for (int y = 0; y < h; ++y) { + const unsigned char *row = d + static_cast(y) * stride; + for (int x = 0; x < w; ++x) { + if (row[4 * x + 3] != 0) { + ++n; + } + } + } + return n; +} + +} // namespace + +int main() +{ + std::printf("pangocairo (pango %s, cairo %s)\n\n", + pango_version_string(), cairo_version_string()); + + // ── the types register, and the inheritance is the dependency graph ── + const GType fm = pango_cairo_font_map_get_type(); + check(fm != 0, "PangoCairoFontMap registers"); + + PangoFontMap *map = pango_cairo_font_map_get_default(); + check(map != nullptr, "pango_cairo_font_map_get_default()"); + check(PANGO_IS_CAIRO_FONT_MAP(map), "…and it is a PangoCairoFontMap"); + check(g_type_is_a(G_OBJECT_TYPE(map), g_list_model_get_type()), + "…and a GListModel, which is gio, inherited through PangoFontMap"); + + // ⚠️ THE BACKEND IS THE CHECK THAT pangoft2 IS ACTUALLY WIRED IN. Without + // HAVE_CAIRO_FREETYPE the fc font map is never registered and the default + // map is some other implementation — which still builds, still runs, and + // never finds a font. So the type NAME is read. + const char *backend = G_OBJECT_TYPE_NAME(map); + std::printf(" font map backend: %s\n", backend); + check(backend != nullptr && std::strstr(backend, "Fc") != nullptr, + "…and it is the FONTCONFIG-backed map — HAVE_CAIRO_FREETYPE took"); + + int families = -1; + { + PangoFontFamily **f = nullptr; + pango_font_map_list_families(map, &f, &families); + g_free(f); + } + std::printf(" font families visible: %d\n", families); + check(families >= 0, "pango_font_map_list_families answers"); + + // ── layout, entirely without a surface ─────────────────────────────── + PangoContext *ctx = pango_font_map_create_context(map); + check(ctx != nullptr, "pango_font_map_create_context"); + + PangoLayout *layout = pango_layout_new(ctx); + pango_layout_set_text(layout, "Hello \344\270\226\347\225\214", -1); + PangoFontDescription *desc = pango_font_description_from_string("Sans 24"); + pango_layout_set_font_description(layout, desc); + + check(pango_layout_get_character_count(layout) == 8, + "the layout counts 8 characters — \"Hello \" plus two Han"); + check(pango_layout_get_line_count(layout) == 1, "…on one line"); + + int w = 0, h = 0; + pango_layout_get_pixel_size(layout, &w, &h); + std::printf(" layout pixel size: %dx%d\n", w, h); + + // ── and now the pixels ─────────────────────────────────────────────── + cairo_surface_t *surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 400, 100); + check(cairo_surface_status(surf) == CAIRO_STATUS_SUCCESS, + "a 400x100 ARGB32 surface"); + cairo_t *cr = cairo_create(surf); + cairo_set_source_rgb(cr, 0, 0, 0); + cairo_move_to(cr, 10, 10); + pango_cairo_show_layout(cr, layout); + cairo_destroy(cr); + + const long drawn = ink(surf); + std::printf(" non-transparent pixels: %ld\n", drawn); + + // ⚠️ THE GUARD IS THE LAYOUT'S OWN METRICS, NOT THE FAMILY COUNT. + // + // `families > 0` looked like enough and is not. A CI runner reported FOUR + // families and measured this layout at 80x858545 — a nonsense height on a + // 100px surface, because "four families" there means fontconfig has + // entries but no usable font behind them. The glyphs then land off the + // surface and the ink count is arbitrary: 0 in one process and 116 in + // another, from byte-identical drawing code. + // + // An earlier version also asserted `drawn > 100`, which passed here (216 + // pixels, 184 families) and failed there (72) — a threshold calibrated to + // the developer's font set is an assertion about the MACHINE. Ink is ink. + const bool usable = families > 0 && w > 0 && h > 0 && h <= 100; + if (usable) { + // ⭐ THE WHOLE LINE, IN ONE ASSERTION. + check(drawn > 0, + "pango_cairo_show_layout put ink on the surface — seven packages"); + } else { + // Not a pass. The machine cannot render, so the only real check could + // not run, and saying so is the point: "it was skipped" and "it worked" + // must not look alike. + std::printf(" \342\232\240 no usable font (families=%d, layout %dx%d on a\n" + " 100px surface): the rendering assertion was NOT run.\n" + " That is a property of this machine, not this build.\n", + families, w, h); + check(drawn >= 0, "the surface is readable (rendering check skipped)"); + } + + // pangocairo-context.c: the resolution and font options a context carries. + pango_cairo_context_set_resolution(ctx, 96.0); + check(pango_cairo_context_get_resolution(ctx) == 96.0, + "pangocairo-context.c round-trips the resolution"); + + cairo_surface_destroy(surf); + pango_font_description_free(desc); + g_object_unref(layout); + g_object_unref(ctx); + + std::printf("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else + +#include +int main() { std::printf("pangocairo: Linux only.\n"); return 0; } + +#endif diff --git a/tests/examples/pangoft2/mcpp.toml b/tests/examples/pangoft2/mcpp.toml new file mode 100644 index 0000000..fdff8e5 --- /dev/null +++ b/tests/examples/pangoft2/mcpp.toml @@ -0,0 +1,11 @@ +# gnome.pangoft2 — the FreeType/fontconfig backend. +[indices] +gnome = { path = "../../.." } + +[package] +name = "pangoft2-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.gnome] +pangoft2 = "1.56.1" diff --git a/tests/examples/pangoft2/tests/module.cpp b/tests/examples/pangoft2/tests/module.cpp new file mode 100644 index 0000000..bdbd69a --- /dev/null +++ b/tests/examples/pangoft2/tests/module.cpp @@ -0,0 +1,59 @@ +// ⭐ THE MODULE, WHICH IS WHAT THE NAMESPACE PROMISES. +// +// In this index the namespace is the contract: `compat.xxx` is consumed with +// `#include`, an owner namespace like `gnome.xxx` exposes `import`. The test +// next to this file takes the header route; this one takes the module, so both +// doors are tested rather than assumed. +// +// ⚠️ THE TWO DOORS DO NOT COMPOSE. A TU that imports the module AND textually +// includes a pango or glib header reaches twice — once through the +// module's global fragment, once directly — and the same `struct tm` becomes +// two entities. So a consumer picks ONE, and which one is decided by macros: +// a module cannot carry them. Code using `PANGO_TYPE_*` or `G_OBJECT` takes +// the header route; code using the function API imports and includes nothing. +#ifdef __linux__ + +import gnome.pangoft2; // re-exports gnome.pango, and gio/glib behind it + +int main() +{ + int failures = 0; + auto check = [&](bool ok, const char *what) { + g_print("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; + }; + + g_print("import gnome.pangoft2\n\n"); + + const GType ft2 = pango_ft2_font_map_get_type(); + check(ft2 != 0, "PangoFT2FontMap registers"); + check(g_type_is_a(ft2, pango_fc_font_map_get_type()), + "…derives from PangoFcFontMap — the fontconfig half"); + check(g_type_is_a(ft2, pango_font_map_get_type()), + "…and from PangoFontMap — gnome.pango came through the re-export"); + + PangoFontMap *map = pango_ft2_font_map_new(); + check(map != nullptr, "pango_ft2_font_map_new()"); + if (map) { + PangoFontFamily **f = nullptr; + int n = -1; + pango_font_map_list_families(map, &f, &n); + g_print(" font families visible: %d\n", n); + check(n >= 0, "list_families answers (0 is valid — see the header test)"); + g_free(f); + g_object_unref(map); + } + + // pango-ot-tag.c, pure table work and no fonts. + check(pango_ot_tag_to_script(pango_ot_tag_from_script(PANGO_SCRIPT_ARABIC)) + == PANGO_SCRIPT_ARABIC, + "the OpenType script tag mapping round-trips"); + + g_print("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else +#include +int main() { std::printf("linux only\n"); return 0; } +#endif diff --git a/tests/examples/pangoft2/tests/pangoft2.cpp b/tests/examples/pangoft2/tests/pangoft2.cpp new file mode 100644 index 0000000..638d8a8 --- /dev/null +++ b/tests/examples/pangoft2/tests/pangoft2.cpp @@ -0,0 +1,109 @@ +// pangoft2 — the FreeType/fontconfig backend. +// +// ⚠️ WHAT THIS DOES *NOT* ASSERT: that any particular font is found. That +// depends on the RUNNER having /usr/share/fonts and a fontconfig +// configuration, and `freedesktop.fontconfig` deliberately compiles its +// runtime paths EMPTY so it does not silently read the developer's fonts. A +// test that demanded "DejaVu Sans" would be checking the machine, not this +// build. +// +// What IS asserted is everything that does not need a font file: that the +// font map type registers, that it IS a PangoFontMap and a GListModel, that +// the fontconfig and FreeType halves are actually linked in (by calling into +// each), and that enumerating families returns a well-formed answer — which on +// a runner with no fonts is legitimately zero. + +#ifdef __linux__ + +#include +#include +// ⚠️ pango-ot.h is NOT reached through pangoft2.h. It is a separate installed +// header, and its whole body sits behind `#ifndef PANGO_DISABLE_DEPRECATED` — +// upstream's own comment is "Deprecated. Use HarfBuzz directly!". It is still +// compiled into this member and still part of its ABI, so it is still tested. +#include +#include + +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-60s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +} // namespace + +int main() +{ + std::printf("pangoft2 (pango %s)\n\n", pango_version_string()); + + // ── the types register ─────────────────────────────────────────────── + const GType ft2 = pango_ft2_font_map_get_type(); + check(ft2 != 0, "PangoFT2FontMap registers"); + check(g_type_is_a(ft2, pango_fc_font_map_get_type()), + "…and derives from PangoFcFontMap — the fontconfig half is linked"); + check(g_type_is_a(ft2, pango_font_map_get_type()), + "…and from PangoFontMap — gnome.pango is linked"); + check(g_type_is_a(ft2, g_list_model_get_type()), + "…and it is a GListModel, inherited through PangoFontMap"); + + // ── an actual instance, which runs fontconfig's initialisation ─────── + PangoFontMap *map = pango_ft2_font_map_new(); + check(map != nullptr, "pango_ft2_font_map_new() — FcInit ran without dying"); + + if (map != nullptr) { + // ⚠️ ZERO IS A VALID ANSWER HERE. compat.fontconfig ships empty runtime + // paths on purpose, so a runner with no FONTCONFIG_FILE finds no + // fonts. What is asserted is that the call ANSWERS — the out + // parameters are consistent — not what it answers. + PangoFontFamily **families = nullptr; + int n = -1; + pango_font_map_list_families(map, &families, &n); + std::printf(" font families visible: %d\n", n); + check(n >= 0, "pango_font_map_list_families answers"); + check(n == 0 || families != nullptr, + "…and the array matches the count it reported"); + g_free(families); + + // The resolution the FT2 map carries, which is its own state rather + // than anything fontconfig had to find. + pango_ft2_font_map_set_resolution(PANGO_FT2_FONT_MAP(map), 96.0, 96.0); + + PangoContext *ctx = pango_font_map_create_context(map); + check(ctx != nullptr, "…and it can create a PangoContext"); + if (ctx != nullptr) { + check(pango_context_get_font_map(ctx) == map, + "…which points back at the font map"); + g_object_unref(ctx); + } + g_object_unref(map); + } + + // ── the OpenType tag helpers: pango-ot-tag.c, pure table work ──────── + { + const PangoOTTag t = pango_ot_tag_from_script(PANGO_SCRIPT_ARABIC); + // 'arab' — the OpenType script tag, four packed bytes. + check(t == PANGO_OT_TAG_MAKE('a', 'r', 'a', 'b'), + "pango_ot_tag_from_script(ARABIC) is 'arab'"); + check(pango_ot_tag_to_script(t) == PANGO_SCRIPT_ARABIC, + "…and the mapping round-trips"); + } + + std::printf("\n%s\n", failures == 0 ? "all ok" : "FAILURES"); + return failures == 0 ? 0 : 1; +} + +#else + +#include +int main() { std::printf("pangoft2: Linux only.\n"); return 0; } + +#endif