libs-opal — CoreGraphics, CoreText & ImageIO gaps for WebKit Sub-plan

What must be added to gnustep/libs-opal to reach full CoreGraphics and CoreText coverage for the minimal WebKit v5 build. libs-opal provides CoreGraphics (OpalGraphics), CoreText (OpalText), and ImageIO in a single library with Cairo and FreeType/Fontconfig backends. Parent plan: WebKit v5 Cocoa-native porting plan.

TL;DR

Executive summary

Scope of libs-opal

libs-opal ships three distinct framework personalities from one build tree:

SubprojectFrameworkBackendHeadersWebKit entry point
OpalGraphicsCoreGraphicsCairo 1.x + LCMS2~35 public headersplatform/graphics/cg/, platform/cocoa/
OpalTextCoreTextFreeType 2 + Fontconfig + (optionally HarfBuzz)~15 public headersplatform/graphics/coretext/, ComplexTextControllerCoreText.mm
(inline)ImageIOlibpng, libjpeg, libtiff, giflib2 public headersplatform/image-decoders/ indirectly via CGImageSource

Post-merge audit — 31 DTW-Thalion PRs merged Done

31 PRs from DTW-Thalion were merged into pkgdemon/libs-opal (including PR #48 CGPattern which required manual conflict resolution). These added 5 major new feature implementations and fixed 12+ bugs with direct WebKit impact.

New features implemented by DTW PRs

PR Feature WebKit Impact
#52 CGContextSetBlendMode — all 27 Apple blend modes mapped to Cairo operators Direct — CSS mix-blend-mode, compositing
#57 + #51 CGContextClipToMask — A8 mask pattern for fills, strokes, images, gradients, text Direct — CSS masking, image masking
#56 CGContextDrawTiledImage — tile loop with correct anchoring Direct — CSS background-repeat
#37 CGShading / CGFunction — axial and radial shading with 64-sample function approximation Direct — gradient rendering
#48 CGPattern fills (colored and uncolored) — was returning nil, now implemented Direct — CSS repeating patterns, SVG pattern fills
#53 CGGeometry dictionary representations (CGPoint/CGSize/CGRect to/from NSDictionary) Property serialization

Critical bug fixes

PR Fix WebKit Impact
#39 Font ascent/descent — was using bbox.yMax (too large), now uses ft_face->ascender Critical — all text line heights and baselines were wrong
#47 CGBitmapContext bytesPerRow=0 — was storing 0 causing zero-length buffer Critical — WebKit creates contexts with bytesPerRow=0
#31 CGImageCreateWithImageInRect crop size — was reporting original dimensions Direct — sprite sheets, CSS object-fit, canvas drawImage
#45 CGDataProvider releaseData callback never called — memory leak Direct — every buffer passed through CGDataProviderCreateWithData leaked
#28 CGPathGetCurrentPoint after close — was returning close point, not move point Path construction correctness
#36 JPEG encoder was embedding garbage alpha channel Image output correctness
#50 Fill/stroke color reset on color space change Color correctness
#19 Dash array allocated as bytes not doubles — stack corruption Crash fix
#22 CGAffineTransformIsIdentity was treating any diagonal as identity Transform correctness
#26 CGColor component count didn't include alpha; equality broken Color correctness

Coverage update

CoreGraphics coverage: ~75% (up from ~62% pre-merge). The 5 new feature implementations (blend modes, clip-to-mask, tiled image, shading/function, pattern) close major rendering gaps.

CoreText coverage: ~45% (up from ~40%). The ascent/descent fix is the main gain — no new CT functions were added.

Remaining gaps

CoreGraphics remaining: CoreText remaining (these are the biggest blockers):

CoreGraphics coverage

Coverage is assessed against what WebKit's Cocoa port actually calls. Files audited: Source/WebCore/platform/graphics/cg/, Source/WebCore/platform/cocoa/, Source/WebKit/WebProcess/WebPage/Cocoa/, Source/WebCore/rendering/ (for geometry helpers).

API group Key types / functions libs-opal has it? Notes
CGContext — drawing CGContextDrawImage, CGContextFillRect, CGContextStrokePath, CGContextSaveGState / RestoreGState, CGContextClip*, blend modes, shadow, transparency layers Yes Full Cairo mapping in CGContext.m
CGContext — transforms CGContextConcatCTM, CGContextGetCTM, CGContextTranslateCTM, CGContextScaleCTM, CGContextRotateCTM Yes
CGBitmapContext CGBitmapContextCreate, CGBitmapContextCreateImage, CGBitmapContextGetData, bitmap info flags Yes Critical for off-screen compositing
CGPath CGPathCreateMutable, CGPathAddArc, CGPathAddCurveToPoint, CGPathAddEllipseInRect, CGPathAddRect, CGPathAddRoundedRect, CGPathApply Yes Missing: continuous rounded rect variants (see gaps)
CGImage CGImageCreate, CGImageCreateCopy, CGImageCreateWithImageInRect, CGImageGetWidth/Height/BitsPerComponent, CGImageRelease Yes
CGImageSource / Destination CGImageSourceCreateWithData, CGImageSourceCreateImageAtIndex, CGImageSourceGetCount, CGImageDestinationCreate*, CGImageDestinationFinalize Yes In the ImageIO layer
CGColor CGColorCreate, CGColorCreateGenericRGB, CGColorGetComponents, CGColorGetColorSpace, named system colors Yes
CGColorSpace CGColorSpaceCreateDeviceRGB, CGColorSpaceCreateWithName (kCGColorSpaceSRGB, kCGColorSpaceDisplayP3), ICC profile creation, LCMS2 backend Yes Wide-gamut names may need adding
CGFont CGFontCreateWithFontName, CGFontCreateWithDataProvider, CGFontGetGlyphBoundingBoxes, CGFontGetAscent/Descent/CapHeight/XHeight, CGFontCopyTableForTag Yes Backed by FreeType via OpalText
CGGradient — linear & radial CGGradientCreateWithColors, CGContextDrawLinearGradient, CGContextDrawRadialGradient, extend options Yes Cairo cairo_pattern_t mapping
CGGradient — conic CGContextDrawConicGradient No See gap 3a
CGShading CGShadingCreateAxial, CGShadingCreateRadial, CGContextDrawShading Yes
CGPattern CGPatternCreate, CGColorSpaceCreatePattern, CGContextSetFillPattern Yes
CGLayer CGLayerCreateWithContext, CGContextDrawLayerAtPoint, CGLayerGetContext Yes
CGPDFDocument / Page / Context CGPDFDocumentCreateWithURL, CGPDFPageGetDrawingTransform, CGContextBeginPage, CGPDFContextCreate Yes PDF disabled in minimal build anyway
CGAffineTransform CGAffineTransformMake, CGAffineTransformConcat, CGAffineTransformInvert, CGAffineTransformIsIdentity, point/rect application Yes
CGDataProvider / Consumer CGDataProviderCreateWithData, CGDataProviderCreateWithURL, sequential callbacks, direct-access callbacks Yes
CGFunction CGFunctionCreate with callbacks (used by CGShading) Yes
CGGeometry helpers CGRectUnion, CGRectIntersection, CGRectContainsRect, CGPointApplyAffineTransform, CGSizeApplyAffineTransform Yes
CGContextSetBaseCTM Apple SPI for base CTM No See gap 3c
CGColorSpaceUsesExtendedRange HDR range query No See gap 3d
CG SPI family CGStyleRef, CGGStateRef, CGContextDelegateRef, etc. No Stub header needed; see gap 3e

CoreGraphics gaps — must add

3a. CGContextDrawConicGradient Phase 6

Missing from libs-opal. WebKit uses this for CSS conic-gradient() and @property painted decorations.

Function signature (Apple private extension of CGGradient):

void CGContextDrawConicGradient(
    CGContextRef context,
    CGGradientRef gradient,
    CGPoint center,
    CGFloat angle          /* rotation in radians */
);

Cairo has no native conic gradient primitive. Three implementation strategies:

  1. Manual angular rasterization (recommended for correctness). Create a cairo_surface_t of the bounding box, fill each pixel by computing its angle from center, look up the gradient stop, write the ARGB value. Then paint that surface onto the context. ~150–200 LOC. This is exact and independent of Cairo version.
  2. Mesh gradient approximation. Cairo 1.12+ has cairo_pattern_create_mesh(). Divide the circle into N wedge-shaped mesh patches. Each patch is a Coon's patch approximating a pie slice. N=72 (5° steps) gives smooth output. ~250 LOC.
  3. Stub (return immediately). Conic gradients are absent from basic web content. The minimal build can stub this entirely — pages will simply paint the first gradient stop's solid color instead.

Plan: Stub in Phase 2–5. Implement via manual rasterization in Phase 6.

/* Source/OpalGraphics/CGGradient.m — stub */
void CGContextDrawConicGradient(CGContextRef ctx,
    CGGradientRef gradient,
    CGPoint center,
    CGFloat angle)
{
  /* TODO Phase 6: angular rasterization via cairo_image_surface */
  (void)ctx; (void)gradient; (void)center; (void)angle;
}

3b. CGPathAddContinuousRoundedRect / CGPathAddUnevenCornersRoundedRect Phase 4–5

Missing from libs-opal. WebKit calls these for CSS border-radius. Without them, rounded boxes fall back to rectangular clips or incorrect geometry.

Function signatures:

/* Continuous ("squircle") rounded rect — equal corner radii, iOS-style superellipse */
void CGPathAddContinuousRoundedRect(
    CGMutablePathRef path,
    const CGAffineTransform *transform,
    CGRect rect,
    CGFloat cornerWidth,
    CGFloat cornerHeight);

/* Per-corner radii — used for arbitrary border-radius values */
void CGPathAddUnevenCornersRoundedRect(
    CGMutablePathRef path,
    const CGAffineTransform *transform,
    CGRect rect,
    const CGSize topLeft,
    const CGSize topRight,
    const CGSize bottomRight,
    const CGSize bottomLeft);

Implementation approach for CGPathAddUnevenCornersRoundedRect: Each corner is a quarter-ellipse arc. Use CGPathAddArc (already implemented) for each of the four corners with its respective CGSize radii, connected by straight line segments. This is straightforward and ~100 LOC:

void CGPathAddUnevenCornersRoundedRect(
    CGMutablePathRef path,
    const CGAffineTransform *t,
    CGRect r,
    const CGSize tl, const CGSize tr,
    const CGSize br, const CGSize bl)
{
  CGFloat minX = CGRectGetMinX(r), minY = CGRectGetMinY(r);
  CGFloat maxX = CGRectGetMaxX(r), maxY = CGRectGetMaxY(r);

  /* Top edge, start after top-left corner */
  CGPathMoveToPoint(path, t, minX + tl.width, minY);

  /* Top-right */
  CGPathAddLineToPoint(path, t, maxX - tr.width, minY);
  CGPathAddCurveToPoint(path, t,
      maxX - tr.width * 0.4477f, minY,
      maxX, minY + tr.height * 0.4477f,
      maxX, minY + tr.height);

  /* Bottom-right */
  CGPathAddLineToPoint(path, t, maxX, maxY - br.height);
  CGPathAddCurveToPoint(path, t,
      maxX, maxY - br.height * 0.4477f,
      maxX - br.width * 0.4477f, maxY,
      maxX - br.width, maxY);

  /* Bottom-left */
  CGPathAddLineToPoint(path, t, minX + bl.width, maxY);
  CGPathAddCurveToPoint(path, t,
      minX + bl.width * 0.4477f, maxY,
      minX, maxY - bl.height * 0.4477f,
      minX, maxY - bl.height);

  /* Top-left */
  CGPathAddLineToPoint(path, t, minX, minY + tl.height);
  CGPathAddCurveToPoint(path, t,
      minX, minY + tl.height * 0.4477f,
      minX + tl.width * 0.4477f, minY,
      minX + tl.width, minY);

  CGPathCloseSubpath(path);
}

The constant 0.4477 is the standard cubic Bezier circle approximation factor (4*(sqrt(2)-1)/3). For CGPathAddContinuousRoundedRect, implement first as a simple CGPathAddRoundedRect alias using equal radii — the "squircle" superellipse shape can be refined later with a proper 5th-order superellipse approximation in Phase 6.

Estimated LOC: ~120 for both functions. File: Source/OpalGraphics/CGPath.m.

3c. CGContextSetBaseCTM Phase 2

Apple SPI. Missing from libs-opal. WebKit calls this early in the drawing pipeline to set the device-pixel-aligned base coordinate transform, below the user CTM. Without it, sub-pixel rendering alignment is off.
/* Apple SPI — sets the base (device) CTM separate from the user CTM */
void CGContextSetBaseCTM(CGContextRef ctx, CGAffineTransform transform);

The base CTM concept separates the "device to surface" transform from the "user space to device" transform. In Cairo terms, this maps to cairo_surface_set_device_transform() applied to the underlying surface, while the context CTM (cairo_transform()) handles the user space. The simplest approach for Phase 2 is to fold the base CTM into the context CTM — i.e., concatenate it with CGContextConcatCTM. This loses the semantic separation but is sufficient for correct pixel output:

void CGContextSetBaseCTM(CGContextRef ctx, CGAffineTransform transform)
{
  /* Phase 2 approximation: fold base CTM into context CTM.
   * Proper implementation: apply via cairo_surface_set_device_transform
   * on the underlying cairo_surface_t and track separately in OpalContext. */
  CGContextConcatCTM(ctx, transform);
}

This function should be declared in Headers/CoreGraphics/CoreGraphicsSPI.h (see gap 3e) and implemented in Source/OpalGraphics/CGContext.m.

3d. CGColorSpaceUsesExtendedRange Stub immediately

Missing from libs-opal. WebKit queries this to determine if a color space can represent HDR values (outside [0,1]). Used in compositing decisions for EDR/HDR content.
bool CGColorSpaceUsesExtendedRange(CGColorSpaceRef space);

All Cairo-backed color spaces in libs-opal operate in [0,1] SDR range. Return false unconditionally. This is correct for all color spaces OpalGraphics currently creates:

/* Source/OpalGraphics/CGColorSpace.m */
bool CGColorSpaceUsesExtendedRange(CGColorSpaceRef space)
{
  (void)space;
  return false; /* OpalGraphics has no HDR/EDR color spaces */
}

Add to Headers/CoreGraphics/CGColorSpace.h as a public API (it is technically public on macOS 10.12+).

3e. Apple SPI stub header — CoreGraphicsSPI.h Phase 2

Missing from libs-opal. WebKit includes <CoreGraphics/CoreGraphicsSPI.h> from numerous files in platform/graphics/cg/ and platform/cocoa/. Without this header, WebKit will not compile.

This header must live at Headers/CoreGraphics/CoreGraphicsSPI.h and declare (with stub implementations in Source/OpalGraphics/CGContextSPI.m) the following Apple-private entry points that WebKit references:

CGStyleRef family (focus ring, shadows, color matrices)

typedef struct CGStyle *CGStyleRef;
typedef struct CGGState *CGGStateRef;

/* Focus ring drawing */
CGStyleRef CGStyleCreateFocusRingWithColor(CGColorRef color);
void CGContextSetStyle(CGContextRef ctx, CGStyleRef style);
void CGStyleRelease(CGStyleRef style);

/* Color matrix / image effects */
typedef struct CGColorTransform *CGColorTransformRef;
CGColorTransformRef CGColorTransformCreate(CGColorSpaceRef space,
    CFDictionaryRef attributes);
void CGColorTransformRelease(CGColorTransformRef xform);
CGImageRef CGImageApplyColorTransform(CGImageRef image,
    CGColorTransformRef xform);

CGFont rendering style SPI

/* Font antialiasing / rendering style queries */
typedef uint32_t CGFontRenderingStyle;
typedef uint32_t CGFontAntialiasingStyle;

#define kCGFontRenderingStyleAntialiasing        (1 << 0)
#define kCGFontRenderingStyleSubpixelPositioning (1 << 1)
#define kCGFontRenderingStyleSubpixelQuantization (1 << 2)

CGFontRenderingStyle CGFontGetRenderingStyle(CGFontRef font);
CGFontAntialiasingStyle CGFontGetAntialiasingStyle(CGFontRef font);

CGContext delegate (compositor hooks)

typedef struct CGContextDelegate *CGContextDelegateRef;
/* Stub — WebKit checks for this on macOS only; always NULL on GNUstep */
CGContextDelegateRef CGContextGetDelegate(CGContextRef ctx);
void CGContextSetDelegate(CGContextRef ctx, CGContextDelegateRef delegate);

IOSurface context (disabled)

/* Stub — IOSurface is not available on GNUstep; WebKit guards with HAVE(IOSURFACE) */
typedef void *CGIOSurfaceContextRef; /* opaque, never non-NULL on GNUstep */
CGContextRef CGIOSurfaceContextCreate(CGIOSurfaceContextRef surface,
    size_t width, size_t height,
    size_t bitsPerComponent, size_t bytesPerRow,
    CGColorSpaceRef colorSpace, CGBitmapInfo bitmapInfo);
/* Returns NULL */

CGS Window Server functions (not applicable)

/* CGS* are Quartz Window Server SPI — not applicable to GNUstep.
 * WebKit guards these with PLATFORM(MAC). Define as dead stubs for
 * source compatibility; should never be called at runtime. */
typedef int CGSConnectionID;
typedef uint32_t CGSWindowID;
CGSConnectionID CGSMainConnectionID(void);   /* returns 0 */
CGError CGSSetWindowOpacity(CGSConnectionID cid,
    CGSWindowID wid, bool isOpaque);         /* returns kCGErrorFailure */

Image cache flags

typedef uint32_t CGImageCachingFlags;
#define kCGImageCachingTransient   0
#define kCGImageCachingTemporary   1
#define kCGImageCachingAlwaysCache 2
void CGImageSetCachingFlags(CGImageRef image, CGImageCachingFlags flags);
/* Stub: OpalGraphics has no image cache; this is a no-op */

CGContextSetBaseCTM (also declared here)

void CGContextSetBaseCTM(CGContextRef ctx, CGAffineTransform transform);

All stub implementations belong in a new file Source/OpalGraphics/CGContextSPI.m. Each function body is a no-op or returns a safe zero/NULL value. The goal is source-level compilation; none of these are called on the GNUstep code path at runtime.

CoreText coverage

Coverage assessed against Source/WebCore/platform/graphics/coretext/, Source/WebCore/platform/cocoa/FontCocoa.mm, Source/WebCore/platform/graphics/ComplexTextController.cpp, Source/WebCore/platform/graphics/ComplexTextControllerCoreText.mm, and font-loading code in Source/WebCore/platform/graphics/cocoa/FontPlatformDataCocoa.mm.

Architecture note: WebKit on macOS does not call HarfBuzz or FreeType directly. All text shaping goes through CT* APIs. OpalText implements those CT* APIs and may use HarfBuzz internally (behind the API surface), but WebKit is unaware of this. This matches exactly what macOS does — WebKit is unmodified.
API group Key types / functions libs-opal has it? Notes
CTFont — creation CTFontCreateWithName, CTFontCreateWithFontDescriptor, CTFontCreateCopyWithAttributes, CTFontCreateCopyWithSymbolicTraits, CTFontCreateWithGraphicsFont Yes FreeType backend
CTFont — metrics CTFontGetSize, CTFontGetAscent, CTFontGetDescent, CTFontGetLeading, CTFontGetCapHeight, CTFontGetXHeight, CTFontGetUnitsPerEm, CTFontGetBoundingBox Yes
CTFont — glyph access CTFontGetGlyphsForCharacters, CTFontGetAdvancesForGlyphs, CTFontGetBoundingRectsForGlyphs, CTFontGetVerticalTranslationsForGlyphs Yes
CTFont — table access CTFontCopyTable (kCTFontTableCmap, kCTFontTableGlyf, kCTFontTableOS2, etc.) Yes FreeType raw table access
CTFont — name attributes CTFontCopyPostScriptName, CTFontCopyFamilyName, CTFontCopyFullName, CTFontCopyDisplayName Yes
CTFontDescriptor CTFontDescriptorCreateWithAttributes, CTFontDescriptorCopyAttribute, CTFontDescriptorCreateMatchingFontDescriptors, CTFontDescriptorCreateWithNameAndSize Yes Fontconfig backend
CTFontCollection CTFontCollectionCreateFromAvailableFonts, CTFontCollectionCreateMatchingFontDescriptors Yes
CTFontManager CTFontManagerRegisterFontsForURL, CTFontManagerCopyAvailableFontFamilyNames Yes
CTLine — creation & drawing CTLineCreateWithAttributedString, CTLineDraw, CTLineGetOffsetForStringIndex, CTLineGetStringIndexForPosition Yes
CTLine — metrics CTLineGetTypographicBounds, CTLineGetImageBounds, CTLineGetTrailingWhitespaceWidth, CTLineGetGlyphRuns Yes
CTRun CTRunGetGlyphs, CTRunGetAdvances, CTRunGetPositions, CTRunGetAttributes, CTRunGetStatus, CTRunGetGlyphCount, CTRunGetStringRange, CTRunGetTypographicBounds Yes Core of complex text shaping output
CTFrame / CTFramesetter CTFramesetterCreateWithAttributedString, CTFramesetterCreateFrame, CTFrameGetLines, CTFrameDraw, CTFramesetterSuggestFrameSizeWithConstraints Yes
CTTypesetter CTTypesetterCreateWithAttributedString, CTTypesetterSuggestLineBreak, CTTypesetterCreateLine Yes
CTParagraphStyle CTParagraphStyleCreate, CTParagraphStyleGetValueForSpecifier, writing direction, alignment, line break mode Yes
CTTextTab CTTextTabCreate, CTTextTabGetLocation, CTTextTabGetAlignment Yes
CTGlyphInfo CTGlyphInfoCreateWithGlyph, CTGlyphInfoGetGlyph Yes
CTStringAttributes kCTFontAttributeName, kCTForegroundColorAttributeName, kCTKernAttributeName, kCTLigatureAttributeName, kCTUnderlineStyleAttributeName, kCTParagraphStyleAttributeName Yes
CTFont cascade list CTFontCopyDefaultCascadeListForLanguages No See gap 5a — CRITICAL
CTFontDrawGlyphs Public API to draw glyphs at positions Partial See gap 5c — verify & fix
CT SPI family CTFontDescriptorCreateForCSSFamily, CTFontCreateForCharactersWithLanguageAndOption, CTLineCreateWithUniCharProvider, etc. No See gaps 5b, 5d, 5f

CoreText gaps — must add

5a. CTFontCopyDefaultCascadeListForLanguages Phase 5–6

CRITICAL for web content. Missing from libs-opal. This is the primary font fallback mechanism. WebKit calls it to obtain the ordered list of fallback fonts for a given primary font and an array of BCP-47 language tags. Without it, characters not present in the primary font render as missing-glyph boxes for all non-Latin scripts (CJK, Arabic, Hebrew, Devanagari, etc.).
/* Returns CFArray of CTFontDescriptorRef, ordered by priority */
CFArrayRef CTFontCopyDefaultCascadeListForLanguages(
    CTFontRef font,
    CFArrayRef languagePrefList  /* CFArray of CFStringRef BCP-47 tags */
);

Implementation plan using Fontconfig:

  1. Extract the primary font's family name from font.
  2. For each language tag in languagePrefList, create an FcPattern with FC_LANG set to that language (converting BCP-47 zh-Hans to fontconfig zh-cn etc.).
  3. Call FcFontSort(config, pattern, FcTrue, &charset, &result) to get all fonts covering that language, sorted by match quality.
  4. Deduplicate across languages (fonts appearing for multiple languages appear once, at the highest-priority position).
  5. Exclude the primary font itself from the result (the cascade list is fallbacks only).
  6. Wrap each FcPattern result as a CTFontDescriptorRef via CTFontDescriptorCreateWithAttributes (already implemented in OpalText).
  7. Return as a CFArrayRef.
/* Source/OpalText/CTFont.m — sketch */
CFArrayRef CTFontCopyDefaultCascadeListForLanguages(
    CTFontRef font,
    CFArrayRef languagePrefList)
{
  FcConfig *config = FcConfigGetCurrent();
  CFMutableArrayRef result = CFArrayCreateMutable(
      kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);

  CFIndex langCount = CFArrayGetCount(languagePrefList);
  for (CFIndex i = 0; i < langCount; i++) {
    CFStringRef lang = CFArrayGetValueAtIndex(languagePrefList, i);
    char langBuf[64] = {0};
    CFStringGetCString(lang, langBuf, sizeof(langBuf),
        kCFStringEncodingUTF8);

    FcPattern *pat = FcPatternCreate();
    FcPatternAddString(pat, FC_LANG,
        (const FcChar8 *)opal_bcp47_to_fc_lang(langBuf));

    FcResult fcr;
    FcFontSet *fs = FcFontSort(config, pat, FcTrue, NULL, &fcr);
    if (fs) {
      for (int j = 0; j < fs->nfont; j++) {
        FcChar8 *family = NULL;
        FcPatternGetString(fs->fonts[j], FC_FAMILY, 0, &family);
        if (family && !opal_cascade_list_contains(result, family)) {
          CTFontDescriptorRef desc =
              opal_fd_from_fc_pattern(fs->fonts[j]);
          CFArrayAppendValue(result, desc);
          CFRelease(desc);
        }
      }
      FcFontSetDestroy(fs);
    }
    FcPatternDestroy(pat);
  }
  return result;
}

Estimated LOC: ~180 including the BCP-47 to fontconfig language tag conversion table and the deduplication helpers. File: Source/OpalText/CTFont.m.

5b. CTFontDescriptorCreateForCSSFamily Phase 5

Apple SPI. Missing from libs-opal. WebKit calls this to resolve CSS generic font families to system font descriptors. Without it, all CSS generic families silently fail and WebKit may assert or crash.
/* kCTFontCSSFamilySerif, kCTFontCSSFamilySansSerif, etc. */
typedef uint32_t CTFontCSSFamilyClass;
#define kCTFontCSSFamilySerif       0
#define kCTFontCSSFamilySansSerif   1
#define kCTFontCSSFamilyCursive     2
#define kCTFontCSSFamilyFantasy     3
#define kCTFontCSSFamilyMonospace   4
#define kCTFontCSSFamilySystemUI    5

CTFontDescriptorRef CTFontDescriptorCreateForCSSFamily(
    CTFontCSSFamilyClass cssFamily,
    CFStringRef *outFamilyName  /* optional; receives resolved family name */
);

Implementation: Map CSS family constants to fontconfig generic aliases (serif, sans-serif, monospace) and call CTFontDescriptorCreateWithNameAndSize:

static const char *opal_css_family_fc_name(CTFontCSSFamilyClass cls) {
  switch (cls) {
    case kCTFontCSSFamilySerif:     return "serif";
    case kCTFontCSSFamilySansSerif: return "sans-serif";
    case kCTFontCSSFamilyCursive:   return "cursive";
    case kCTFontCSSFamilyFantasy:   return "fantasy";
    case kCTFontCSSFamilyMonospace: return "monospace";
    case kCTFontCSSFamilySystemUI:  return "sans-serif"; /* best effort */
    default:                        return "sans-serif";
  }
}

CTFontDescriptorRef CTFontDescriptorCreateForCSSFamily(
    CTFontCSSFamilyClass cssFamily,
    CFStringRef *outFamilyName)
{
  const char *fcName = opal_css_family_fc_name(cssFamily);
  CFStringRef name = CFStringCreateWithCString(
      kCFAllocatorDefault, fcName, kCFStringEncodingUTF8);
  if (outFamilyName) *outFamilyName = CFRetain(name);
  CTFontDescriptorRef desc = CTFontDescriptorCreateWithNameAndSize(name, 0);
  CFRelease(name);
  return desc;
}

Estimated LOC: ~90. File: Source/OpalText/CTFontDescriptor.m. Declare in Headers/CoreText/CoreTextSPI.h.

5c. CTFontDrawGlyphs Phase 2

Partially present — verify and fix. CTFontDrawGlyphs is the public API that WebKit's DrawGlyphsRecorder and ComplexTextControllerCoreText.mm use to paint glyph arrays into a CGContext. If it has bugs or is missing, no text renders at all.
void CTFontDrawGlyphs(
    CTFontRef font,
    const CGGlyph glyphs[],
    const CGPoint positions[],
    size_t count,
    CGContextRef context);

Required behavior:

Action: Write a test program that calls CTFontDrawGlyphs via OpalText and inspects the output bitmap. If glyphs do not appear or appear mispositioned, audit Source/OpalText/CTFont.m for coordinate system errors (Cairo's Y axis is top-down; CoreGraphics is bottom-up — this flip is a common source of glyph inversion bugs).

5d. CTFontCreateForCharactersWithLanguageAndOption Phase 5–6

Apple SPI. Missing from libs-opal. WebKit uses this to find a font that can render specific Unicode characters, with a language preference and fallback options. Used in FontCascade::fontForCombiningCharacterSequence.
typedef uint32_t CTFontFallbackOption;
#define kCTFontFallbackOptionNone       0
#define kCTFontFallbackOptionSystem     (1 << 0)
#define kCTFontFallbackOptionPreferCJK  (1 << 1)

CTFontRef CTFontCreateForCharactersWithLanguageAndOption(
    CTFontRef currentFont,
    const UTF16Char *characters,
    CFIndex length,
    CFStringRef language,      /* BCP-47 language tag, may be NULL */
    CTFontFallbackOption option,
    CFIndex *coveredLength     /* out: how many chars the returned font covers */
);

Implementation: Build an FcCharSet from the Unicode code points in characters[0..length-1]. Call FcFontSort with FC_CHARSET matching. For each candidate from fontconfig, check if it covers all requested characters using FcCharSetHasChar. Return the first match as a CTFontRef with the same point size as currentFont. Set *coveredLength to the number of UTF-16 code units covered.

CTFontRef CTFontCreateForCharactersWithLanguageAndOption(
    CTFontRef currentFont,
    const UTF16Char *characters,
    CFIndex length,
    CFStringRef language,
    CTFontFallbackOption option,
    CFIndex *coveredLength)
{
  FcConfig *config = FcConfigGetCurrent();
  FcCharSet *cs = FcCharSetCreate();

  /* Convert UTF-16 to codepoints and add to charset */
  for (CFIndex i = 0; i < length; ) {
    UTF32Char cp = opal_utf16_codepoint(characters, length, &i);
    FcCharSetAddChar(cs, cp);
  }

  FcPattern *pat = FcPatternCreate();
  FcPatternAddCharSet(pat, FC_CHARSET, cs);
  if (language) {
    char langBuf[64] = {0};
    CFStringGetCString(language, langBuf, sizeof(langBuf),
        kCFStringEncodingUTF8);
    FcPatternAddString(pat, FC_LANG,
        (const FcChar8 *)opal_bcp47_to_fc_lang(langBuf));
  }
  FcConfigSubstitute(config, pat, FcMatchPattern);
  FcDefaultSubstitute(pat);

  FcResult result;
  FcPattern *match = FcFontMatch(config, pat, &result);
  CTFontRef fallback = NULL;
  if (match) {
    fallback = opal_ct_font_from_fc_pattern(match,
        CTFontGetSize(currentFont));
    if (coveredLength) *coveredLength = length; /* optimistic */
    FcPatternDestroy(match);
  }
  FcCharSetDestroy(cs);
  FcPatternDestroy(pat);
  return fallback; /* caller must CFRelease */
}

Estimated LOC: ~130 including the UTF-16 to UTF-32 helper. File: Source/OpalText/CTFont.m.

5e. CTFontCopyColorGlyphCoverage / CTFontIsAppleColorEmoji Phase 6+

Apple SPI. Missing. Stub initially. Used to detect color emoji fonts so WebKit can select the correct rendering path for emoji characters.
CFBitVectorRef CTFontCopyColorGlyphCoverage(CTFontRef font,
    CTFontTableTag tableTag);
bool CTFontIsAppleColorEmoji(CTFontRef font);

Stub implementation (Phase 2–5):

CFBitVectorRef CTFontCopyColorGlyphCoverage(CTFontRef font,
    CTFontTableTag tableTag)
{
  (void)font; (void)tableTag;
  return NULL; /* no color glyph coverage on GNUstep stub */
}

bool CTFontIsAppleColorEmoji(CTFontRef font)
{
  /* Check font PostScript name against known emoji font names.
   * On GNUstep the emoji font is typically NotoColorEmoji, not
   * Apple Color Emoji, so this correctly returns false. */
  CFStringRef name = CTFontCopyPostScriptName(font);
  if (!name) return false;
  bool result = (CFStringFind(name,
      CFSTR("AppleColorEmoji"),
      kCFCompareCaseInsensitive).length > 0);
  CFRelease(name);
  return result;
}

Proper implementation (Phase 6+): Use FreeType's FT_Load_Glyph with FT_LOAD_COLOR flag and check for FT_PIXEL_MODE_BGRA in the glyph slot. Check for OpenType table tags COLR, CBDT, CBLC, and sbix via CTFontCopyTable.

5f. Apple SPI stubs needed for CoreText Stub immediately

Missing from libs-opal. WebKit references these entry points by name. The build fails without declarations. At runtime on GNUstep, the code paths that call them are guarded by PLATFORM(MAC) or equivalent, but the symbols must exist for linking.

All of the following belong in Headers/CoreText/CoreTextSPI.h with stub implementations in Source/OpalText/CTFontSPI.m:

SymbolSignatureStub returnNotes
CTLineCreateWithUniCharProvider CTLineRef CTLineCreateWithUniCharProvider(UniCharProviderCallback, UniCharTokenizerCallback, CFDictionaryRef) NULL Performance optimization path; WebKit falls back to CTLineCreateWithAttributedString
CTRunGetBaseAdvancesAndOrigins void CTRunGetBaseAdvancesAndOrigins(CTRunRef, CFRange, CGSize[], CGPoint[]) no-op Vertical text layout; not needed for Phase 2–5
CTTypesetterCreateWithUniCharProviderAndOptions CTTypesetterRef CTTypesetterCreateWithUniCharProviderAndOptions(UniCharProviderCallback, UniCharTokenizerCallback, CFDictionaryRef) NULL Alternative typesetter creation path
CTParagraphStyleSetCompositionLanguage void CTParagraphStyleSetCompositionLanguage(CTParagraphStyleRef, CFStringRef) no-op Composition language hint; safe to ignore
CTFontShapeGlyphs bool CTFontShapeGlyphs(CTFontRef, CGGlyph[], const UniChar*, CFIndex, CTShapingOptions) false Direct shaping API; can delegate to HarfBuzz internally when implemented
CTFontGetOpticalBoundsForGlyphs CGRect CTFontGetOpticalBoundsForGlyphs(CTFontRef, const CGGlyph[], CFIndex, CFOptionFlags) delegate to CTFontGetBoundingRectsForGlyphs Optical bounds; bounding rect fallback is acceptable
OTSVG support bool CTFontHasTable(CTFontRef, uint32_t tag) ('SVG ' query) false Color font SVG tables; no OT-SVG support in Phase 2–5
kCTFontOpticalSizeAttribute CFStringRef constant CFSTR("NSCTFontOpticalSizeAttribute") Must be a defined constant, not a symbol lookup

ImageIO coverage

ImageIO is implemented in libs-opal as part of the OpalGraphics build, using libpng, libjpeg-turbo, libtiff, and giflib as format backends. The two primary public headers are CGImageSource.h and CGImageDestination.h.

FeatureStatusNotes
PNG decode/encodeYeslibpng
JPEG decode/encodeYeslibjpeg-turbo
TIFF decode/encodeYeslibtiff
GIF decode (animated)Yesgiflib; frame delay metadata needed
WebP decodePartiallibwebp optional dependency; needed for modern web
HEIC/HEIF decodeNoNot needed for web content in minimal build
AVIF decodeNoNot needed for minimal build; add via libavif later
BMP decodePartialWebKit has its own BMP decoder; CGImageSource BMP is uncommon
CGImageSourceGetStatusYesNeeded for progressive decode
CGImageSourceCopyPropertiesYesImage metadata dictionary
CGImageMetadata* familyNoStub — not used in minimal build rendering path
CGImageSourceCreateIncrementalYesNeeded for streaming decode as data arrives
CGImageDestinationSetPropertiesYes
Verdict: ImageIO is sufficient for the minimal WebKit build. The only action needed is adding CGImageMetadata stub declarations so WebKit compiles. WebP support (libwebp) is recommended for Phase 4 when real web pages are tested.

CGImageMetadata stub

/* Headers/ImageIO/CGImageMetadata.h — minimal stub */
typedef struct CGImageMetadata *CGImageMetadataRef;
typedef struct CGImageMetadataTag *CGImageMetadataTagRef;

CGImageMetadataRef CGImageSourceCopyMetadataAtIndex(
    CGImageSourceRef source, size_t index, CFDictionaryRef options);
/* Returns NULL on GNUstep — WebKit handles NULL gracefully */

CFStringRef CGImageMetadataTagCopyValue(CGImageMetadataTagRef tag);
void CGImageMetadataRelease(CGImageMetadataRef metadata);

Build notes

libs-opal source tree layout

libs-opal/
├── GNUmakefile                    # top-level, builds both subprojects
├── Headers/
│   ├── CoreGraphics/
│   │   ├── CoreGraphics.h         # umbrella
│   │   ├── CGContext.h
│   │   ├── CGPath.h
│   │   ├── CGGradient.h
│   │   ├── CGColorSpace.h
│   │   ├── CoreGraphicsSPI.h      # NEW — Apple SPI stubs (gap 3e)
│   │   └── ...
│   ├── CoreText/
│   │   ├── CoreText.h             # umbrella
│   │   ├── CTFont.h
│   │   ├── CTFontDescriptor.h
│   │   ├── CoreTextSPI.h          # NEW — CT SPI stubs (gaps 5b, 5f)
│   │   └── ...
│   └── ImageIO/
│       ├── CGImageSource.h
│       ├── CGImageDestination.h
│       └── CGImageMetadata.h      # NEW — stub (section 6)
├── Source/
│   ├── OpalGraphics/
│   │   ├── GNUmakefile
│   │   ├── CGContext.m
│   │   ├── CGPath.m               # add CGPathAddUnevenCornersRoundedRect
│   │   ├── CGGradient.m           # add CGContextDrawConicGradient stub
│   │   ├── CGColorSpace.m         # add CGColorSpaceUsesExtendedRange
│   │   ├── CGContextSPI.m         # NEW — CG SPI stubs
│   │   └── ...
│   └── OpalText/
│       ├── GNUmakefile
│       ├── CTFont.m               # add cascade list + char coverage functions
│       ├── CTFontDescriptor.m     # add CTFontDescriptorCreateForCSSFamily
│       ├── CTFontSPI.m            # NEW — CT SPI stubs
│       └── ...

Adding new files to GNUmakefile

libs-opal uses GNUstep-make. Adding a new .m file requires listing it in the subproject's GNUmakefile:

# In Source/OpalGraphics/GNUmakefile
OpalGraphics_OBJC_FILES += \
  CGContextSPI.m

# In Source/OpalText/GNUmakefile
OpalText_OBJC_FILES += \
  CTFontSPI.m

New headers are installed automatically if listed in OpalGraphics_HEADER_FILES / OpalText_HEADER_FILES.

Fork workflow

# Fork upstream to pkgdemon/libs-opal, then:
git clone https://github.com/pkgdemon/libs-opal
cd libs-opal
git remote add upstream https://github.com/gnustep/libs-opal

# Create a branch for WebKit gap work
git checkout -b webkit-v5-gaps

# Implement gaps in order of phase priority:
# Phase 2: CoreGraphicsSPI.h, CoreTextSPI.h, CGContextSetBaseCTM,
#           CGColorSpaceUsesExtendedRange, CTFontDrawGlyphs verification
# Phase 4: CGPathAddUnevenCornersRoundedRect, CGPathAddContinuousRoundedRect
# Phase 5: CTFontDescriptorCreateForCSSFamily,
#           CTFontCopyDefaultCascadeListForLanguages,
#           CTFontCreateForCharactersWithLanguageAndOption
# Phase 6: CGContextDrawConicGradient (rasterization),
#           CTFontCopyColorGlyphCoverage (FreeType COLR/CBDT)

CMake visibility from WebKit

WebKit's CMake must be directed to find the pkgdemon fork of libs-opal, not a system-installed copy. In OptionsGNUstep.cmake:

find_package(OpalGraphics REQUIRED
    PATHS /usr/local/lib/pkgconfig /opt/gnustep/lib/pkgconfig)
find_package(OpalText REQUIRED
    PATHS /usr/local/lib/pkgconfig /opt/gnustep/lib/pkgconfig)

# Ensure CoreGraphicsSPI.h is on the include path
target_include_directories(WebCore PRIVATE
    ${OpalGraphics_INCLUDE_DIRS})

Priority order summary

PhaseGapEst. LOCRisk if skipped
Phase 2 CoreGraphicsSPI.h + stub impls ~150 Build fails — WebKit does not compile
Phase 2 CoreTextSPI.h + stub impls ~100 Build fails — WebKit does not compile
Phase 2 CGColorSpaceUsesExtendedRange 5 Linker error
Phase 2 CGContextSetBaseCTM 10 Linker error; pixel misalignment
Phase 2 CTFontDrawGlyphs verify & fix varies No text rendered at all
Phase 2 CGImageMetadata stub header 20 Build fails
Phase 4 CGPathAddUnevenCornersRoundedRect ~120 Wrong border-radius geometry
Phase 4 CGPathAddContinuousRoundedRect ~80 Missing squircle corners (lower priority)
Phase 5 CTFontDescriptorCreateForCSSFamily ~90 CSS generic families fail to resolve
Phase 5 CTFontCopyDefaultCascadeListForLanguages ~180 All non-Latin text shows missing glyph boxes
Phase 5 CTFontCreateForCharactersWithLanguageAndOption ~130 Per-character font fallback broken
Phase 6 CGContextDrawConicGradient ~180 CSS conic-gradient() renders as solid color
Phase 6 CTFontCopyColorGlyphCoverage ~80 Emoji use wrong render path

8. HarfBuzz integration — optional text shaping backend

libs-opal's CTTypesetter currently has no text shaping implementation — the createLineWithRange: method returns an empty runs array with a FIXME comment. This must be implemented for WebKit to render any text at all.

On macOS, Apple's CoreText uses a proprietary internal shaper (AAT engine + OpenType). There is no open-source equivalent. Every non-Apple project that does complex text shaping uses HarfBuzz (MIT license). It is the only serious open-source option for full OpenType shaping (ligatures, reordering, joining, GSUB/GPOS).

Design: optional dependency via pkg-config auto-detection

HarfBuzz should be an optional dependency, not a hard requirement. Regular GNUstep desktop apps (NSTextView, etc.) work fine with basic FreeType glyph mapping for Latin text. Only WebKit and apps rendering complex scripts (Arabic, Devanagari, Thai, etc.) need the full shaper.

libs-opal uses GNUstep-make with no configure script. It already auto-detects dependencies via pkg-config (cairo, freetype2, lcms2, libpng). HarfBuzz follows the same pattern:

# In Source/GNUmakefile or Source/OpalText/GNUmakefile:

ifndef NO_HARFBUZZ
HARFBUZZ_CFLAGS := $(shell pkg-config --cflags harfbuzz 2>/dev/null)
HARFBUZZ_LIBS := $(shell pkg-config --libs harfbuzz 2>/dev/null)
ifneq ($(HARFBUZZ_LIBS),)
  ADDITIONAL_OBJCFLAGS += $(HARFBUZZ_CFLAGS) -DHAVE_HARFBUZZ=1
  LIBRARIES_DEPEND_UPON += $(HARFBUZZ_LIBS)
endif
endif

Three behaviors, zero configure step

ScenarioWhat happensText shaping
HarfBuzz installed (default) pkg-config finds it, sets -DHAVE_HARFBUZZ=1, links -lharfbuzz Full OpenType shaping — Arabic, Devanagari, Thai, CJK, ligatures, kerning
HarfBuzz not installed pkg-config returns empty, no flag set FreeType basic: FT_Get_Char_Index + FT_Get_Kerning — Latin/Cyrillic/Greek only
Explicitly disabled gmake NO_HARFBUZZ=yes FreeType basic (even if HarfBuzz is installed)

Implementation in CTTypesetter.m

- (CTLineRef)createLineWithRange:(CFRange)range
{
#ifdef HAVE_HARFBUZZ
  // Full OpenType shaping via hb_buffer_create(), hb_shape()
  // 1. Create hb_buffer, add Unicode codepoints from attributed string
  // 2. Set script, language, direction from string attributes
  // 3. Create hb_font from CTFont's underlying FT_Face
  // 4. Call hb_shape() — produces reordered, ligated glyph array
  // 5. Extract glyph IDs, advances, offsets into CTRun objects
  // 6. Split into runs by attribute changes
#else
  // Simple FreeType glyph mapping + pair kerning
  // 1. For each character: FT_Get_Char_Index() → glyph ID
  // 2. For each glyph pair: FT_Get_Kerning() → advance adjustment
  // 3. One CTRun per attribute run
  // Works for Latin/Cyrillic/Greek; breaks on complex scripts
#endif

  CTLineRef line = [[CTLine alloc] initWithRuns: runs];
  return line;
}
Note: This follows the same pattern as NO_OPALTEXT (which disables the entire CoreText subproject). WebKit builds will always have HarfBuzz installed, so it auto-detects. Basic GNUstep desktop users who don't need complex scripts get the simpler fallback path automatically if HarfBuzz isn't on their system.
Open question: HarfBuzz is already an indirect dependency on most systems — Pango, libraqm, libass, and FFmpeg all depend on it. On NextBSD it's installed as harfbuzz-14.2.1. Should we document this as a "soft dependency" (auto-detected) or a "recommended dependency" in the README?