Back to Skiasharp

SkiaSharp & HarfBuzz Domain Knowledge

.agents/skills/api-docs/references/skia-patterns.md

4.150.08.6 KB
Original Source

SkiaSharp & HarfBuzz Domain Knowledge

Domain-specific facts for documenting SkiaSharp and HarfBuzzSharp APIs. Always verify claims against source code — this file provides key facts but is not exhaustive.

Verification Rule

Before documenting byte layouts, struct defaults, or API overloads:

  1. Read the source file in binding/ for the C# type
  2. For native types (color formats, packed values), check the C/C++ header
  3. Never guess a byte layout — either verify it or leave it out

Two Libraries, Different Conventions

SkiaSharp wraps Skia (C++) and HarfBuzz (C). They have different conventions. Never assume one works like the other because the C# types look similar.

Color Types

TypeFormatPacked LayoutVerified From
SKColor (Skia)ARGB0xAARRGGBBinclude/core/SkColor.h
hb_color_t (HarfBuzz)BGRA0xBBGGRRAAharfbuzz/src/hb-common.h

Proof for hb_color_t: HB_COLOR(b,g,r,a) calls HB_TAG(b,g,r,a) which expands to (b<<24)|(g<<16)|(r<<8)|a. So blue is bits 31-24 (high byte), alpha is bits 7-0 (low byte). The accessors confirm: hb_color_get_alpha(c) = c & 0xFF, hb_color_get_blue(c) = c >> 24.

  • SKColor has alpha in the high byte. Standard .NET Color.ToArgb() order.
  • hb_color_t has blue in the high byte, alpha in the low byte.
  • SKFontPaletteOverride.Color is sk_color_t = SKColor (ARGB).
  • Face.GetPaletteColors() returns hb_color_t values (BGRA).

Do NOT describe both as "RGBA" just because both are uint32.

Naming Conventions

Color Type Suffixes

SuffixMeaningExample
No suffix or a at endHas alphaRgba8888, Bgra8888
x at endPadding — NOT alphaRgb888x, RgbF16F16F16x

The x means the fourth component is unused padding for alignment. The type is opaque.

Channel Order

The letters in the name specify channel order in memory: Rgba = R first, A last. Bgra = B first, A last. Match the docs to the name.

Type Categories

When documenting SkiaSharp types, match the disposal and threading guidance to the type's category:

CategoryExamplesThreadingDisposal
Mutable native objectsSKCanvas, SKPaint, SKPathNOT thread-safeMust dispose
Immutable native objectsSKImage, SKShader, SKDataThread-safeMust dispose
Value typesSKColor, SKPoint, SKRectThread-safeNo disposal

In examples: mutable types need using, immutable types need using, value types do not.

Struct Defaults

C# structs zero-initialize. When documenting struct properties:

  • Check what 0 means for each field — it may not be a usable default
  • Do not assume a "typical" value is the default (e.g., 72 DPI is NOT the default for a zero-initialized struct)
  • If the library provides a constant like SKDocument.DefaultRasterDpi, document that separately from the struct's zero-initialized state

This is a recurring error — be deliberate about it. Writers keep documenting SKDocumentXpsOptions.Dpi as "default 72" because 72 is the obvious "typical" DPI. But SKDocumentXpsOptions is a struct with no field initializer, so its Dpi defaults to 0. The 72 comes from SKDocument.DefaultRasterDpi, which only applies to the scalar CreateXps(stream, dpi) overloads — not to the struct. The rule: a struct property's default is whatever zero-init produces (0 / null / false) unless you can point to a field initializer in the source. Never copy a "default" from a sibling constant.

Standard-Based Enums (CICP, Vulkan, OpenType)

For enums whose members reference external standards, the member name almost always encodes the exact standard identifier — use it as a second source of truth alongside MemberValue:

Member name (enum)EncodesCorrect description
SmpteRp4312 (SKColorspacePrimariesCicp)SMPTE RP 431-2DCI-P3 primaries (NOT "RP 432-2")
SmpteEg4321 (SKColorspacePrimariesCicp)SMPTE EG 432-1Display P3 / P3-D65 primaries
SmpteSt4281 (SKColorspacePrimariesCicp)SMPTE ST 428-1CIE XYZ (D50 white point) primaries for D-Cinema
SmpteSt4281 (SKColorspaceTransferFnCicp)SMPTE ST 428-1D-Cinema transfer function — gamma ~2.6 (NOT "linear")

The same member name can mean different things in sibling enums. SmpteSt4281 appears in both SKColorspacePrimariesCicp (a set of color primaries) and SKColorspaceTransferFnCicp (a transfer function) — the standard (ST 428-1) defines both, but the member describes only the one relevant to its enum. Always confirm which enum you are documenting before copying a description; a "transfer function" gloss on a primaries member (or vice versa) is wrong even when the standard number is right.

Two failure modes to check explicitly:

  • Wrong identifier — the summary's standard number must match the digits in the member name (SmpteRp4312 → 431-2, not 432-2). Adjacent standards (431 vs 432) are easy to transpose.
  • Wrong behavior — the described nature must be correct, not just the number. ST 428-1 is a gamma-2.6 power transfer; calling it "linear" is wrong even though the number is right. A separate Linear member exists for the actual linear transfer. Verify the behavior against the standard, and never let a description match a similar-looking sibling member instead of its own value.

Caller-Owned vs Parent-Owned Objects

Most SkiaSharp objects are owned by the caller and must be disposed (using). But some are owned by a parent object and must NOT be disposed by the caller — the parent manages their lifetime:

ObjectOwned byIn examples
SKDocument.BeginPage(...) return valuethe SKDocumentdo NOT using/Dispose the canvas
SKSurface.Canvasthe SKSurfacedo NOT using/Dispose the canvas
SKDocument.CreateXps(...) return valuecallerDO using the document

BeginPage wraps its canvas with owns: false, so disposing it is the anti-pattern called out in AGENTS.md. In a code example, write var canvas = doc.BeginPage(...); (no using); the parent's using cleans everything up. Also note that some factories (e.g. SKDocument.CreateXps) return null on unsupported platforms even though the signature is non-nullable — examples that immediately dereference the result are misleading.

Obsolete APIs (Legacy Text Rendering)

Some members are marked [Obsolete("...", error: true)]. Because error: true makes them a compile error, any code example using them is broken. The extract JSON surfaces these in an obsolete field — never use a flagged member in an example or recommend it.

The most common trap is text rendering, which moved off SKPaint onto SKFont. These SKPaint properties are obsolete:

Obsolete SKPaint memberModern replacement
TextSizeSKFont.Size
TextScaleXSKFont.ScaleX
TextSkewXSKFont.SkewX
TypefaceSKFont.Typeface
SubpixelTextSKFont.Subpixel
LcdRenderTextSKFont.Edging
HintingLevelSKFont.Hinting
FakeBoldTextSKFont.Embolden
TextAlignpass SKTextAlign to the draw overload
TextEncodinguse the SKTextEncoding draw overloads

The old SKCanvas.DrawText(string, float, float, SKPaint) (and its SKPoint form) are also obsolete. Use an overload that takes an SKFont:

csharp
using var typeface = SKTypeface.FromFamilyName("Arial");
using var font = new SKFont(typeface, 24);
using var paint = new SKPaint { Color = SKColors.Black, IsAntialias = true };
canvas.DrawText("Hello", 10, 40, SKTextAlign.Left, font, paint);

When documenting one of these obsolete members itself, still write a factual summary — the [Obsolete] attribute already carries the deprecation warning; the doc should not invent compile-failing examples around it.

API Surface Verification

When writing code examples:

  • Verify the exact overload exists by reading the source file
  • Do not fabricate overloads that "should" exist (e.g., passing a struct to a method that only accepts scalar parameters)
  • If no suitable overload exists for an example, show direct property/field usage instead
  • SKColor to uint requires an explicit cast: (uint)SKColors.Red

String Handling

APIs that parse fixed-length strings often pad or truncate silently. Always read the method body to check actual behavior — don't assume "must be exactly N characters" if the source pads/truncates instead of throwing.