Skip to content
Open
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
Binary file added __test__/fonts/EBGaramond-Regular.ttf
Binary file not shown.
Binary file added __test__/fonts/Lato-Regular.ttf
Binary file not shown.
Binary file added __test__/snapshots/font-feature-settings-liga.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __test__/snapshots/font-feature-settings-tnum.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __test__/snapshots/font-kerning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __test__/snapshots/font-variant-position-sub.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __test__/snapshots/text-rendering-comparison.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
662 changes: 651 additions & 11 deletions __test__/text.spec.ts

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,19 +199,51 @@ type CanvasFontVariantCaps =
type CanvasTextAlign = 'center' | 'end' | 'left' | 'right' | 'start'
type CanvasTextBaseline = 'alphabetic' | 'bottom' | 'hanging' | 'ideographic' | 'middle' | 'top'
type CanvasTextRendering = 'auto' | 'geometricPrecision' | 'optimizeLegibility' | 'optimizeSpeed'
type CanvasFontOpticalSizing = 'auto' | 'none'
type CanvasFontVariantLigatures =
| 'normal'
| 'none'
| 'common-ligatures'
| 'no-common-ligatures'
| 'discretionary-ligatures'
| 'no-discretionary-ligatures'
| 'historical-ligatures'
| 'no-historical-ligatures'
| 'contextual'
| 'no-contextual'
| string // For combinations like "common-ligatures discretionary-ligatures"
type CanvasFontVariantNumeric =
| 'normal'
| 'lining-nums'
| 'oldstyle-nums'
| 'proportional-nums'
| 'tabular-nums'
| 'diagonal-fractions'
| 'stacked-fractions'
| 'ordinal'
| 'slashed-zero'
| string // For combinations
type CanvasFontVariantPosition = 'normal' | 'sub' | 'super'

interface CanvasTextDrawingStyles {
direction: CanvasDirection
font: string
fontKerning: CanvasFontKerning
fontStretch: CanvasFontStretch
fontVariantCaps: CanvasFontVariantCaps
fontOpticalSizing: CanvasFontOpticalSizing
fontVariantLigatures: CanvasFontVariantLigatures
fontVariantNumeric: CanvasFontVariantNumeric
fontVariantPosition: CanvasFontVariantPosition
fontSizeAdjust: number | null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

让我们标识出哪些是目前 canvas 规范中的,哪些是我们实验性的私有方法。

letterSpacing: string
textAlign: CanvasTextAlign
textBaseline: CanvasTextBaseline
textRendering: CanvasTextRendering
wordSpacing: string
fontVariationSettings: string
fontFeatureSettings: string
lang: string
}

interface CanvasFilters {
Expand Down
137 changes: 131 additions & 6 deletions skia-c/skia_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,13 @@ void skiac_canvas_get_line_metrics_or_draw_text(
const skiac_font_variation* variations,
int variations_count,
int kerning,
int variant_caps) {
int variant_caps,
const skiac_font_feature* features,
int features_count,
int font_optical_sizing,
const char* lang,
float font_size_adjust,
int text_rendering) {
auto font_collection = c_collection->collection;
auto font_style = SkFontStyle(weight, stretch, (SkFontStyle::Slant)slant);
auto text_direction = (TextDirection)direction;
Expand All @@ -491,28 +497,101 @@ void skiac_canvas_get_line_metrics_or_draw_text(
families_vec.emplace_back(family);
}
text_style.setFontFamilies(families_vec);
text_style.setFontSize(font_size);

// Apply font-size-adjust if specified (positive value means apply adjustment)
float effective_font_size = font_size;
if (font_size_adjust > 0) {
// Create a temporary paragraph to get the font's actual x-height
TextStyle temp_style;
temp_style.setFontFamilies(families_vec);
temp_style.setFontSize(font_size);
temp_style.setFontStyle(font_style);

ParagraphStyle temp_para_style;
temp_para_style.setTextStyle(temp_style);
ParagraphBuilderImpl temp_builder(temp_para_style, font_collection,
SkUnicodes::ICU::Make());
temp_builder.addText("x", 1); // Use 'x' to measure x-height
auto temp_paragraph =
static_cast<ParagraphImpl*>(temp_builder.Build().release());
temp_paragraph->layout(1000);

if (temp_paragraph->lineNumber() > 0) {
auto run = temp_paragraph->run(0);
auto font = run.font();
SkFontMetrics metrics;
font.getMetrics(&metrics);

// fXHeight is the height of lowercase 'x', typically negative in Skia
if (metrics.fXHeight != 0) {
// Calculate actual aspect ratio: |x-height| / font-size
float actual_aspect = std::abs(metrics.fXHeight) / font_size;
// Apply font-size-adjust formula:
// adjusted_size = (target_aspect / actual_aspect) * font_size
effective_font_size = (font_size_adjust / actual_aspect) * font_size;
}
}
delete temp_paragraph;
}

text_style.setFontSize(effective_font_size);
text_style.setWordSpacing(world_spacing);
text_style.setLetterSpacing(letter_spacing);
text_style.setHeight(1);
text_style.setFontStyle(font_style);

std::vector<SkFontArguments::VariationPosition::Coordinate> coords;

// Track if wght/wdth are already set via fontVariationSettings
bool has_wght = false;
bool has_wdth = false;

// Apply variable font variations if provided
if (variations && variations_count > 0) {
coords.reserve(variations_count + 1);
coords.reserve(variations_count + 2);
for (int i = 0; i < variations_count; i++) {
coords.push_back({variations[i].tag, variations[i].value});
// Check if user explicitly set wght or wdth
if (variations[i].tag == SkSetFourByteTag('w', 'g', 'h', 't')) {
has_wght = true;
}
if (variations[i].tag == SkSetFourByteTag('w', 'd', 't', 'h')) {
has_wdth = true;
}
}
}

// Apply font weight as 'wght' variation for variable fonts
// Only if not already set via fontVariationSettings (explicit settings take
// precedence) 'wght' tag = 0x77676874
if (weight != 400 && !has_wght) {
coords.push_back(
{SkSetFourByteTag('w', 'g', 'h', 't'), static_cast<float>(weight)});
}

// Apply font stretch as 'wdth' variation for variable fonts
// 'wdth' tag = 0x77647468
if (stretch_width != 100.0f) {
// Only if not already set via fontVariationSettings (explicit settings take
// precedence) 'wdth' tag = 0x77647468
if (stretch_width != 100.0f && !has_wdth) {
coords.push_back({SkSetFourByteTag('w', 'd', 't', 'h'), stretch_width});
}

// Apply font optical sizing ('opsz' axis)
// font_optical_sizing: 0=auto (set to font_size), 1=none (don't set)
if (font_optical_sizing == 0) {
// Check if opsz is already set via fontVariationSettings
bool has_opsz = false;
for (const auto& coord : coords) {
if (coord.axis == SkSetFourByteTag('o', 'p', 's', 'z')) {
has_opsz = true;
break;
}
}
if (!has_opsz) {
coords.push_back({SkSetFourByteTag('o', 'p', 's', 'z'), font_size});
}
}

if (!coords.empty()) {
SkFontArguments font_args;
font_args.setVariationDesignPosition(
Expand All @@ -528,7 +607,21 @@ void skiac_canvas_get_line_metrics_or_draw_text(
text_style.addFontFeature(SkString("kern"), 1);
}

// TODO: Support fontFeatureSettings
// Apply fontFeatureSettings (explicit features from the fontFeatureSettings
// property) These are applied AFTER font-variant-* properties, so they take
// precedence
if (features && features_count > 0) {
for (int i = 0; i < features_count; i++) {
uint32_t tag = features[i].tag;
// Convert tag to 4-char string
char tag_str[5] = {static_cast<char>((tag >> 24) & 0xFF),
static_cast<char>((tag >> 16) & 0xFF),
static_cast<char>((tag >> 8) & 0xFF),
static_cast<char>(tag & 0xFF), '\0'};
text_style.addFontFeature(SkString(tag_str), features[i].value);
}
}

// Apply font variant caps features
// variant_caps: 0=normal, 1=small-caps, 2=all-small-caps, 3=petite-caps,
// 4=all-petite-caps, 5=unicase, 6=titling-caps
Expand All @@ -554,6 +647,38 @@ void skiac_canvas_get_line_metrics_or_draw_text(
text_style.addFontFeature(SkString("titl"), 1);
}

// Apply language/locale for language-specific glyph variants
// lang: BCP-47 language tag (e.g., "en", "tr", "zh-Hans") or
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

按照 Chrome 开发者给出的答复,这里我们不需要兼容 BCP-47 算法,交给底层的 harfbuzz 来处理就好了

// nullptr/"inherit"
if (lang != nullptr && strlen(lang) > 0 && strcmp(lang, "inherit") != 0) {
text_style.setLocale(SkString(lang));

// Turkish locale: disable "fi" ligature
// In Turkish, the dotless i (ı) is a separate letter, and the fi ligature
// would incorrectly merge "f" with "i" when it should remain separate.
// This matches browser behavior per MDN CanvasRenderingContext2D.lang spec.
if (strncmp(lang, "tr", 2) == 0 &&
(lang[2] == '\0' || lang[2] == '-' || lang[2] == '_')) {
text_style.addFontFeature(SkString("liga"), 0);
}
}

// Apply textRendering overrides (these take precedence over fontKerning and
// fontVariantLigatures) text_rendering: 0=auto, 1=optimizeSpeed,
// 2=optimizeLegibility, 3=geometricPrecision
if (text_rendering == 1) {
// optimizeSpeed: disable kerning and ligatures for speed
text_style.addFontFeature(SkString("kern"), 0);
text_style.addFontFeature(SkString("liga"), 0);
text_style.addFontFeature(SkString("clig"), 0);
} else if (text_rendering == 2 || text_rendering == 3) {
// optimizeLegibility / geometricPrecision: enable kerning and ligatures
text_style.addFontFeature(SkString("kern"), 1);
text_style.addFontFeature(SkString("liga"), 1);
text_style.addFontFeature(SkString("clig"), 1);
}
// auto (0): respect existing fontKerning and fontVariantLigatures settings

text_style.setForegroundColor(*PAINT_CAST);
text_style.setTextBaseline(TextBaseline::kAlphabetic);
StrutStyle struct_style;
Expand Down
13 changes: 12 additions & 1 deletion skia-c/skia_c.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ struct skiac_font_variation {
float value; // Value for this axis
};

struct skiac_font_feature {
uint32_t tag; // OpenType feature tag (e.g., 'liga', 'kern')
int value; // Feature value (typically 0=off, 1=on)
};

extern "C" {
void skiac_clear_all_cache();
// Surface
Expand Down Expand Up @@ -400,7 +405,13 @@ void skiac_canvas_get_line_metrics_or_draw_text(
const skiac_font_variation* variations,
int variations_count,
int kerning,
int variant_caps);
int variant_caps,
const skiac_font_feature* features,
int features_count,
int font_optical_sizing,
const char* lang,
float font_size_adjust,
int text_rendering);
void skiac_canvas_reset_transform(skiac_canvas* c_canvas);
void skiac_canvas_clip_rect(skiac_canvas* c_canvas,
float x,
Expand Down
Loading