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.
CGImageMetadata can be stubbed.ComplexTextControllerCoreText.mm and the platform/graphics/coretext/ files. It does not call HarfBuzz directly. We mirror that model: WebKit calls CT* APIs; libs-opal's OpalText may use HarfBuzz internally behind the API, but that is an implementation detail WebKit never sees.CTFontDrawGlyphs (any text at all), CTFontCopyDefaultCascadeListForLanguages (international content), and CGPathAddUnevenCornersRoundedRect (CSS border-radius).libs-opal ships three distinct framework personalities from one build tree:
| Subproject | Framework | Backend | Headers | WebKit entry point |
|---|---|---|---|---|
OpalGraphics | CoreGraphics | Cairo 1.x + LCMS2 | ~35 public headers | platform/graphics/cg/, platform/cocoa/ |
OpalText | CoreText | FreeType 2 + Fontconfig + (optionally HarfBuzz) | ~15 public headers | platform/graphics/coretext/, ComplexTextControllerCoreText.mm |
| (inline) | ImageIO | libpng, libjpeg, libtiff, giflib | 2 public headers | platform/image-decoders/ indirectly via CGImageSource |
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.
| 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 |
| 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 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 |
CGContextDrawConicGradient Phase 6conic-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:
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.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.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;
}
CGPathAddContinuousRoundedRect / CGPathAddUnevenCornersRoundedRect Phase 4–5border-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.
CGContextSetBaseCTM Phase 2/* 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.
CGColorSpaceUsesExtendedRange Stub immediatelybool 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+).
CoreGraphicsSPI.h Phase 2<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:
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);
/* 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);
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);
/* 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* 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 */
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 */
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.
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.
| 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 |
CTFontCopyDefaultCascadeListForLanguages Phase 5–6/* Returns CFArray of CTFontDescriptorRef, ordered by priority */
CFArrayRef CTFontCopyDefaultCascadeListForLanguages(
CTFontRef font,
CFArrayRef languagePrefList /* CFArray of CFStringRef BCP-47 tags */
);
Implementation plan using Fontconfig:
font.languagePrefList, create an FcPattern with FC_LANG set to that language (converting BCP-47 zh-Hans to fontconfig zh-cn etc.).FcFontSort(config, pattern, FcTrue, &charset, &result) to get all fonts covering that language, sorted by match quality.FcPattern result as a CTFontDescriptorRef via CTFontDescriptorCreateWithAttributes (already implemented in OpalText).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.
CTFontDescriptorCreateForCSSFamily Phase 5/* 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.
CTFontDrawGlyphs Phase 2CTFontDrawGlyphs 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:
CGContextGetTextMatrix (text matrix) for glyph orientation.CGContextSetShouldAntialias / CGContextSetShouldSmoothFonts.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).
CTFontCreateForCharactersWithLanguageAndOption Phase 5–6FontCascade::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.
CTFontCopyColorGlyphCoverage / CTFontIsAppleColorEmoji Phase 6+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.
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:
| Symbol | Signature | Stub return | Notes |
|---|---|---|---|
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 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.
| Feature | Status | Notes |
|---|---|---|
| PNG decode/encode | Yes | libpng |
| JPEG decode/encode | Yes | libjpeg-turbo |
| TIFF decode/encode | Yes | libtiff |
| GIF decode (animated) | Yes | giflib; frame delay metadata needed |
| WebP decode | Partial | libwebp optional dependency; needed for modern web |
| HEIC/HEIF decode | No | Not needed for web content in minimal build |
| AVIF decode | No | Not needed for minimal build; add via libavif later |
| BMP decode | Partial | WebKit has its own BMP decoder; CGImageSource BMP is uncommon |
CGImageSourceGetStatus | Yes | Needed for progressive decode |
CGImageSourceCopyProperties | Yes | Image metadata dictionary |
CGImageMetadata* family | No | Stub — not used in minimal build rendering path |
CGImageSourceCreateIncremental | Yes | Needed for streaming decode as data arrives |
CGImageDestinationSetProperties | Yes |
CGImageMetadata stub declarations so WebKit compiles. WebP support (libwebp) is recommended for Phase 4 when real web pages are tested.
/* 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);
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 │ └── ...
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 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)
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})
| Phase | Gap | Est. LOC | Risk 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 |
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).
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
| Scenario | What happens | Text 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) |
- (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;
}
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.
harfbuzz-14.2.1. Should we document this as a "soft dependency" (auto-detected) or a "recommended dependency" in the README?