Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs/text-ir-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ paint style is fill-only. Native Skia deliberately keeps using the `TextRun`
fallback in P12 because exact blob-backed typeface construction is not wired
yet.

P13 closes the first diagnostics layer for this contract. The export is still
schema v1 and still keeps `TextRun` fallback as the replay baseline, but it now
also reports `textV2` compatibility diagnostics: slot-level variant state,
structured validation issues, the v1 downgrade path, fallback-free profile
guards, and line-break risk telemetry for text runs whose shaped replay could
affect layout-sensitive behavior.

## Export Contract

Layer JSON now provides additive text metadata:
Expand All @@ -41,6 +48,13 @@ Layer JSON now provides additive text metadata:
- `fontResources`, an additive table for font blob/face identity.
- Optional `GlyphRun` sidecar ops with `variant`, `shapeKey`, glyph ids,
glyph positions, shaped clusters, and replay diagnostics.
- `textV2`, an additive diagnostics object with:
- `compatibilityProfile`, currently `v1Compat` for normal exports.
- `fallbackRequired`, which stays true for the v1 compatibility writer.
- `downgradePath=schemaV1FlattenedTextRunAndGlyphRun`.
- `slotDiagnostics`, one entry per v1 text variant group.
- `validationIssues`, using stable issue codes and severity.
- `lineBreakRisks`, report-only telemetry for complex text runs.

The explicit visual ops are additive. Existing renderers skip them and keep
drawing the paired `TextRun` mirror, so visual output does not double-paint.
Expand Down Expand Up @@ -70,6 +84,18 @@ fallback instead.
but native Skia selection remains disabled until it can instantiate the exact
referenced font blob/face. Normal layer lowering still emits `TextRun` only
unless a shaping pass explicitly inserts glyph alternatives.
- P13 `textV2` diagnostics are additive and report-only for normal exports.
They must not change renderer output or make `GlyphRun` the canonical path.
- A fallback-free text profile is only valid when every text variant slot has a
strict visual variant. In schema v1 the default writer still exports the
fallback, and the fallback-free profile is only exposed as a guard/validator.
- `slotDiagnostics.strictVariantAvailable` requires exact or position-adjusted
quality, strict visual eligibility, replayable font eligibility, no missing
glyphs, no cluster mismatch, and no unsplit fallback-font use.
- `lineBreakRisks` is explanatory telemetry. It marks cases such as char
overlap, vertical/rotated text, ratio/spacing changes, tab leaders, visible
text effects, field markers, and explicit line/paragraph-end markers. It is
not a layout decision source.
- Canvas2D/layered SVG keep using the `TextRun` fallback and ignore glyph
sidecars.
- Glyph ids require portable font identity. Consumers must not replay glyph ids
Expand All @@ -81,4 +107,5 @@ fallback instead.
- Add CanvasKit glyph replay behind the same variant gate.
- Add native glyph outline replay behind a separate strict visual variant.
- Add resource table entries for font blobs and face identity.
- Promote renderer diagnostics once glyph alternatives exist.
- Promote renderer diagnostics from report-only to backend selection telemetry
once CanvasKit/native glyph alternatives are actually consumed.
17 changes: 12 additions & 5 deletions src/paint/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::paint::{
GroupKind, LayerAffineTransform, LayerNode, LayerNodeKind, LayerPoint, LayerVector,
PageLayerTree, PaintOp, PaintTextStyle, PaintVariantMeta, RenderProfile, ShapeKey,
TextDecorationKind, TextSourceAnnotation, TextSourceEntry, TextSourceId, TextSourceRange,
TextSourceSpan, TextSourceTable, LAYER_TREE_SCHEMA,
TextSourceSpan, TextSourceTable, TextV2Diagnostics, LAYER_TREE_SCHEMA,
};
use crate::renderer::layout::compute_char_positions;
use crate::renderer::render_tree::{BoundingBox, FieldMarkerType, ShapeTransform, TextRunNode};
Expand Down Expand Up @@ -53,6 +53,8 @@ impl PageLayerTree {
buf.push_str(",\"fontResources\":");
write_font_resources(&mut buf, self.resources.font_resources());
write_text_export_metadata(&mut buf, &self.root);
buf.push_str(",\"textV2\":");
TextV2Diagnostics::from_layer_tree(self).write_json(&mut buf);
buf.push('}');
Comment thread
seo-rii marked this conversation as resolved.
buf
}
Expand All @@ -62,7 +64,7 @@ fn write_text_export_metadata(buf: &mut String, root: &LayerNode) {
let externalized_visuals = externalized_text_visuals(root);
let has_variant_groups = has_text_variant_groups(root);
let has_glyph_runs = has_glyph_runs(root);
buf.push_str(",\"usedFeatures\":[\"text.paintStyle\",\"text.sourceTable\",\"text.sourceSpan\",\"text.v2.placement\",\"text.v2.clusters\",\"text.projectionKind\",\"text.legacyVisuals\"");
buf.push_str(",\"usedFeatures\":[\"text.paintStyle\",\"text.sourceTable\",\"text.sourceSpan\",\"text.v2.placement\",\"text.v2.clusters\",\"text.v2.diagnostics\",\"text.projectionKind\",\"text.legacyVisuals\"");
if has_glyph_runs {
buf.push_str(",\"fontResources\",\"text.glyphRun\"");
}
Expand All @@ -85,7 +87,7 @@ fn write_text_export_metadata(buf: &mut String, root: &LayerNode) {
if has_glyph_runs {
buf.push_str("\"fontResources\",\"text.glyphRun\"");
}
buf.push_str("],\"knownFeatures\":[\"fontResources\",\"fontResources.blobFaceSplit\",\"text.variantGroups\",\"text.shapeDiagnostics\",\"text.glyphRun\",\"text.outlineGlyph\",\"text.specialVisualOps\",\"text.charOverlapOp\",\"text.controlMarkOp\",\"text.tabLeaderOp\",\"text.decorationOp\",\"text.vertical.mixedPerGlyph\"],\"requiredFeatures\":[],\"text\":{\"defaultVariant\":\"textRun\",\"variants\":[\"textRun\"");
buf.push_str("],\"knownFeatures\":[\"fontResources\",\"fontResources.blobFaceSplit\",\"text.variantGroups\",\"text.shapeDiagnostics\",\"text.v2.diagnostics\",\"text.v2.slotDiagnostics\",\"text.v2.validationIssues\",\"text.lineBreakRiskTelemetry\",\"text.fallbackFreeStrictProfile\",\"text.glyphRun\",\"text.outlineGlyph\",\"text.specialVisualOps\",\"text.charOverlapOp\",\"text.controlMarkOp\",\"text.tabLeaderOp\",\"text.decorationOp\",\"text.vertical.mixedPerGlyph\"],\"requiredFeatures\":[],\"text\":{\"defaultVariant\":\"textRun\",\"variants\":[\"textRun\"");
if has_glyph_runs {
buf.push_str(",\"glyphRun\"");
}
Expand Down Expand Up @@ -1878,8 +1880,8 @@ mod tests {

assert!(json.contains("\"kind\":\"leaf\""));
assert!(json.contains("\"schemaVersion\":1"));
assert!(json.contains("\"schemaMinorVersion\":9"));
assert!(json.contains("\"schema\":{\"major\":1,\"minor\":9}"));
assert!(json.contains("\"schemaMinorVersion\":10"));
assert!(json.contains("\"schema\":{\"major\":1,\"minor\":10}"));
assert!(json.contains("\"resourceTableVersion\":1"));
assert!(json.contains("\"resourceTableMinorVersion\":3"));
assert!(json.contains("\"resourceTable\":{\"major\":1,\"minor\":3}"));
Expand All @@ -1902,8 +1904,11 @@ mod tests {
assert!(json.contains("\"fieldMarker\":{\"kind\":\"fieldBegin\"}"));
assert!(json.contains("\"charOverlap\":{\"borderType\":1,\"innerCharSize\":90}"));
assert!(json.contains("\"usedFeatures\":[\"text.paintStyle\""));
assert!(json.contains("\"text.v2.diagnostics\""));
assert!(json.contains("\"knownFeatures\":[\"fontResources\""));
assert!(json.contains("\"fontResources\":{\"blobs\":[],\"faces\":[]}"));
assert!(json.contains("\"textV2\":{\"compatibilityProfile\":\"v1Compat\""));
assert!(json.contains("\"downgradePath\":\"schemaV1FlattenedTextRunAndGlyphRun\""));
assert!(json.contains("\"text\":{\"defaultVariant\":\"textRun\""));
assert!(json.contains("\"fontFamily\":\"Noto Sans KR\""));
assert!(json.contains("\"italic\":true"));
Expand Down Expand Up @@ -2142,6 +2147,8 @@ mod tests {
assert!(json.contains("\"glyphIds\":[42]"));
assert!(json.contains("\"replayEligibility\":\"portable\""));
assert!(json.contains("\"strictVisualEligible\":true"));
assert!(json.contains("\"slotDiagnostics\":[{\"paintOrderSlotId\":\"text-0\""));
assert!(json.contains("\"strictVariantAvailable\":true"));
}

#[test]
Expand Down
5 changes: 5 additions & 0 deletions src/paint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod profile;
pub mod resources;
pub mod schema;
pub mod text_shape;
pub mod text_v2;
pub mod text_variants;

pub use builder::LayerBuilder;
Expand Down Expand Up @@ -46,4 +47,8 @@ pub use text_shape::{
FontRequest, FontResolver, GlyphRunQuality, NoopFontResolver, ResolvedFontFace,
ResolvedGlyphRun, TextShapeDiagnostic, TextShapeLowerer, TextShapeReport,
};
pub use text_v2::{
TextV2CompatibilityProfile, TextV2Diagnostics, TextV2LineBreakRisk, TextV2LineBreakRiskLevel,
TextV2SlotDiagnostic, TextV2ValidationIssue, TextV2ValidationSeverity, TextV2VariantDiagnostic,
};
pub use text_variants::{validate_text_variant_scope, TextVariantScopeError};
4 changes: 2 additions & 2 deletions src/paint/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub struct LayerTreeSchema {

pub const LAYER_TREE_SCHEMA: LayerTreeSchema = LayerTreeSchema {
schema_version: 1,
schema_minor_version: 9,
schema_minor_version: 10,
resource_table_version: 1,
resource_table_minor_version: 3,
unit: "px",
Expand All @@ -37,7 +37,7 @@ mod tests {
#[test]
fn layer_tree_schema_contract_is_stable() {
assert_eq!(LAYER_TREE_SCHEMA.schema_version, 1);
assert_eq!(LAYER_TREE_SCHEMA.schema_minor_version, 9);
assert_eq!(LAYER_TREE_SCHEMA.schema_minor_version, 10);
assert_eq!(LAYER_TREE_SCHEMA.resource_table_version, 1);
assert_eq!(LAYER_TREE_SCHEMA.resource_table_minor_version, 3);
assert_eq!(LAYER_TREE_SCHEMA.unit, "px");
Expand Down
Loading
Loading