pharo-graphics / alexandrie Goto Github PK
View Code? Open in Web Editor NEWFFI bindings and a 2D canvas for Pharo based on Cairo, Freetype and Harfbuzz
License: MIT License
FFI bindings and a 2D canvas for Pharo based on Cairo, Freetype and Harfbuzz
License: MIT License
Radial pattern should have 3 parameters:
The third one is missing in the API. As the API exist, It can only display circle with gradient color. The focal point allow to display something more like a half-ellipse with gradient color.
To illustrate this, here is a short example in Athens:
| surface |
surface := AthensCairoSurface extent: 200 @ 200.
surface drawDuring: [ :canvas |
surface clear: (Color purple alpha: 0.3).
"linear gradient fill"
canvas setPaint:
((LinearGradientPaint from: 0 @ 0 to: 100 @ 100) colorRamp: {
(0 -> Color white).
(1 -> Color black) }).
canvas drawShape: (0 @ 0 extent: 100 @ 100).
"plain color fill"
canvas setPaint: (Color yellow alpha: 0.9).
canvas drawShape: (100 @ 0 extent: 200 @ 100).
"Bitmap fill"
canvas setPaint:
(PolymorphSystemSettings pharoLogoForm asAthensPaintOn: canvas).
canvas paintTransform translateX: 0 Y: 135.
canvas paintTransform scaleBy: 0.25.
canvas drawShape: (0 @ 100 extent: 100 @ 200).
"Radial gradient fill"
canvas paintTransform loadIdentity.
canvas setPaint: (RadialGradientPaint new
colorRamp: {
(0 -> Color white).
(1 -> Color black) };
center: 150 @ 150;
radius: 50;
focalPoint: 180 @ 180).
canvas drawShape: (100 @ 100 extent: 200 @ 200) ].
^ surface asForm
A similar result in Alexandrie
| canvas form |
canvas := AeCanvas extent: 200 @ 200.
canvas clear: (Color purple alpha: 0.3).
"linear gradient fill"
canvas pathFactory: [ :cairoContext |
cairoContext rectangleTo: 100 @ 100 ].
canvas setBackgroundWith: [
canvas
setSourceLinearPatternStops: {
(0 -> Color white).
(1 -> Color black) }
start: 0 @ 0
end: 100 @ 100 ].
canvas setBorderOff.
canvas drawFigure.
"plain color fill"
canvas pathFactory: [ :cairoContext |
cairoContext rectangleTo: 100 @ 100 ].
canvas pathTranslate: 100 @ 0.
canvas setBackgroundWith: [
canvas setSourceColor: (Color yellow alpha: 0.9) ].
canvas setBorderOff.
canvas drawFigure.
"Bitmap fill"
form := PolymorphSystemSettings pharoLogoForm.
canvas pathFactory: [ :cairoContext |
cairoContext rectangleTo: form extent ].
canvas
pathTranslate: -100 @ 0135;
pathScale: 0.25 @ 0.25.
canvas setBackgroundWithForm: form alpha: 1.0.
canvas setBorderOff.
canvas drawFigure.
"Radial gradient fill"
canvas pathFactory: [ :cairoContext |
cairoContext rectangleTo: 100 @ 100 ].
canvas setBackgroundWith: [
canvas
setSourceRadialPatternStops: {
(0 -> Color white).
(1 -> Color black) }
center: 50 @ 50
radius: 50 ].
canvas
pathTranslate: 400 @ -135;
pathScale: 4 @ 4.
canvas setBorderOff.
canvas drawFigure.
^ canvas asForm
The radial pattern cannot be completed in its current API state...
Note
Explanation how to do this explained here: discord example
I reproduced it locally.
Some notes:
On CI, ubuntu is 20.04.5.
Lib versions differ a bit:
-----------------------------
Main graphic library versions
-----------------------------
Cairo: 1.16.0
Freetype: 2.10.1
Harfbuzz: 2.6.4
SDL: 2.0.7
(CI is executed with smalltalkci -s Pharo64-11 .ci.ston
)
Source: https://github.com/pharo-graphics/Alexandrie/actions/runs/3270294977/jobs/5378749209
On my machine, Ubuntu 22.04:
-----------------------------
Main graphic library versions
-----------------------------
Cairo: 1.16.0
Freetype: 2.11.1
Harfbuzz: 2.7.4
SDL: 2.0.7
These 2 tests signal "Instance of FFIExternalStructureReferenceHandle did not understand #tfPointerAddress":
AeHbFeatureTest
✗ #testParseInstanceInArray (1ms)
AeHbBufferTest
✗ #testShapeWithFontFeatures (4ms)
I proposed a fix to Pharo 12 and if approved might be backported to Pharo 11. But for the moment, to have green CI I should either mark the tests as <expectedFailure>
or add my fix as method extensions to the FFI library.
I will wait some days more for feedback on the PR.
This is related to premultiplied alpha conversion, that Cairo surface in ARGB32 format requires, and Form not.
Repsoruce with:
f := Form extent: 1@1 depth: 32.
f fillColor: (Color red alpha: 0.4).
f colorAt: 0@0. "(Color r: 1.0 g: 0.0 b: 0.0 alpha: 0.4)"
s := AeCairoImageSurface fromForm: f.
s asForm colorAt: 0@0. "(Color r: 0.3998044965786901 g: 0.0 b: 0.0 alpha: 0.4)"
the answer should be the same
CI runs on the 3 main platforms, and has been failing on Windows for long.
It raises an error.
an example with ToSandbox class >> example_BasicComboBox
Strange drawing behavior. #pathTranslate: related ?
Here is an example. See screenshots for results with Sparta/Cairo and with Alexandrie
| text space |
text := 'ABCD' asRopedText.
text attribute: (BlTextBackgroundAttribute paint: (Color blue alpha: 0.3)) from: 1 to: 1.
text attribute: (BlTextBackgroundAttribute paint: (Color yellow alpha: 0.5)) from: 2 to: 2.
text attribute: (BlTextBackgroundAttribute paint: (Color red alpha: 0.3)) from: 3 to: 3.
text
fontSize: 24;
fontName: 'Helvetica Neue'.
space := BlSpace new.
space root addChild: text asElement.
space title: 'attribute drawing issue'.
space show.
@tinchodias , I can still experience the problem of #42
(as of 15 of dec 2023). My script is
| aFTLibrary aFace aeCanvas cairoScaledFont fontHeight string glyphs|
aFTLibrary := AeFTLibrary newInitialized.
aFace := AeCascadiaCodeDownloadedFont new
downloadDirectory: AeFilesystemResources downloadedFontsDirectory;
ensureDownloaded;
firstFaceUsing: aFTLibrary.
fontHeight := 20.
string := 'a := A->B->>C <= c|=>d~~>e.'.
aeCanvas := AeCanvas extent: 1000 @ (fontHeight * 4).
aeCanvas clear: Color white.
cairoScaledFont := aeCanvas scaledFontForFace: aFace size: fontHeight.
"Margin"
aeCanvas pathTranslate: (fontHeight/2) @ 0.
"Draw text with Harfbuzz:"
aeCanvas pathTranslate: 0 @ (fontHeight*1.1).
aeCanvas setSourceColor: Color green muchDarker.
glyphs := AeHbBuffer new
direction: AeHbDirection leftToRight;
script: AeHbScript latin;
language: AeHbLanguage en;
clusterLevel: AeHbBufferClusterLevel recommended;
flags: AeHbBufferFlags beginningOrEndingOfText;
addString: string;
cairoGlyphArrayForFace: aFace size: fontHeight.
aeCanvas
drawText: glyphs
font: cairoScaledFont.
^ aeCanvas asForm
If I execute it several time very quickly in a playground, I'll experience a crash. I have the same behaviour on Windows 10 and Fedora Linux 38, latest Pharo 11 fresh image and latest VM update. It's frustrating because this script is a direct write of AeHarfbuzzRenderExample class >> example5CascadiaCode which seems to work OK (no crash experienced), so it's look like a timing issue, but as I don't have any crash message, I can't figure out what goes wrong. I added crash.dmp as watched on my linux machine.
This example do not work :
example_EditorAsTextAreaWithHighlight
| ed space text |
ed := AlbEditor new.
text := 'AB CD' asRopedText.
"text attribute: (BlTextBackgroundAttribute paint: (Color blue alpha: 0.3)) from: 1 to: 1."
text attribute: (BlTextBackgroundAttribute paint: (Color yellow alpha: 0.5)) from: 3 to: 3.
text attribute: (BlTextDecorationAttribute new wavy color: Color blue) from: 4 to: 6.
"text attribute: (BlTextBackgroundAttribute paint: (Color yellow alpha: 0.5)) from: 2 to: 2."
"text attribute: (BlTextBackgroundAttribute paint: (Color red alpha: 0.3)) from: 3 to: 3."
text
fontSize: 24;
fontName: 'Helvetica Neue'.
ed text: text.
space := BlSpace new.
space root addChild: ed.
space title: 'editor'.
"ed inspect."
space show.
^ ed
BlRenderDifferenceInspector new pixelComparisonFor: #buildClippingMany
An example showing the bug:
=> The output should look as on the right, but it's currently rendering as on the left:
since the last changes.
shouldClipAfterDrawingFigure: aBoolean
shouldClipAfterDrawingFigure := aBoolean
3 reasons in this direction:
We need to have
renderBackgroundAlpha: backgroundAlpha hasBorder: hasBorder outskirtsSelector: outskirtsSelector
| aeCanvas drawFigureBlock referenceExtent |
referenceExtent := 50 asPoint.
aeCanvas := AeCanvas extent: referenceExtent.
aeCanvas clear: Color yellow.
drawFigureBlock := [ :depth |
aeCanvas restoreContextAfter: [
aeCanvas
pathTranslate: referenceExtent / -10;
pathScale: 0.75 asPoint.
aeCanvas pathFactory: [ :cairoContext |
cairoContext circleRadius: referenceExtent x / 2 ].
aeCanvas
setBackgroundWith: [
aeCanvas setSourceColor:
(depth odd
ifTrue: [ Color green ]
ifFalse: [ Color red ]) ]
alpha: backgroundAlpha.
hasBorder
ifTrue: [
aeCanvas setBorderBlock: [
aeCanvas
setSourceColor: Color black;
"setCapButt; setJoinRound;
--> Redundant: both are defaults"
setBorderWidth: referenceExtent x / 10 ].
aeCanvas perform: outskirtsSelector ]
ifFalse: [ aeCanvas setBorderOff ].
aeCanvas drawFigureAndPrepareToClip: depth odd.
depth > 0 ifTrue: [
drawFigureBlock value: depth-1 ] ] ].
"Move to initial position and draw with depth=2"
aeCanvas pathTranslate: referenceExtent / 4.
drawFigureBlock value: 2.
^ aeCanvas
How is the process to support a new Cairo version ?
The current supported version of Cairo (1.17.4) have 3 years, do you have some tools to compare performances between differents Cairo version ?
More informations here : https://www.cairographics.org/news/cairo-1.18.0/
In the project we currently have code like:
| antialiasOption |
antialiasOption := AeCairoAntialias CAIRO_ANTIALIAS_FAST.
cairoContext
primSetTolerance: 0.2;
primSetAntialias: antialiasOption.
fontOptions := AeCairoFontOptions new
antialias: antialiasOption;
hintMetrics: AeCairoHintMetrics CAIRO_HINT_METRICS_ON;
hintStyle: AeCairoHintStyle CAIRO_HINT_STYLE_SLIGHT;
subpixelOrder: AeCairoSubpixelOrder CAIRO_SUBPIXEL_ORDER_DEFAULT;
yourself
Where the underscore capitalized class-side methods are defines literally copy-pasted from the C header.
Probably we should convert to AeCairoAntialias CAIRO_ANTIALIAS_FAST
-> AeCairoAntialias fast
, or at least AeCairoAntialias CAIRO_ANTIALIAS_FAST
-> AeCairoAntialias cairoAntialiasFast
, but that's quite redundant.
For antialiasing and other options that are now set on initialization, only depending on the device scale.
The API can be cleaned with this new FFI call option, that we proposed with @tesonep in:
pharo-project/pharo#13229
So not yet, but maybe in Pharo 12: it requires this PR to be merged.
It seems I recently introduced this error:
AeCanvasTest
✗ #testFormSelector (0ms)
Instance of AeFTFace did not understand #newCairoFontFace
AeFTFace(Object)>>doesNotUnderstand: #newCairoFontFace
AeCanvas>>scaledFontForFace:size:
AeCanvasTest>>renderLigatureTextWithoutHarfbuzz
AeCanvasTest(AePixelMatchTest)>>actualForm
AeCanvasTest(AePixelMatchTest)>>newPixelComparison
AeCanvasTest(AePixelMatchTest)>>testFormSelector ...newPixelComparison
AeCanvasTest(TestCase)>>performTest
AeCanvasTest(AePixelMatchTest)>>performTest
I had a bad idea about exposing sourceColorRGB: aColor
and sourceColorRGBA: aColor
instead of just sourceColor:
. The low-level selectors are okay (e.g. sourceColorR: aColor red g: aColor green b: aColor blue
).
CI started to show a regression on Pharo12 because the Form of this icon slightly changed, and the test detects the small difference. Instead of this Form, we should create a new one for the test, independent from the system icons.
I’m reviewing BASpan to be a direct subclass of BlTextParagraphSegment.
So I’ve removed a lot of common accessors and renamed some instance var in BASpan.
Moreover I would rename it as BATextParagraphSpan to clarify.
Object subclass: #BlTextParagraphSegment
uses: TBlDebug
instanceVariableNames: 'paragraph left top width height advance ascent descent baseline from to'
classVariableNames: ''
package: 'Bloc-Text-Text-Paragraph’
BlTextParagraphSegment subclass: #BASpan
instanceVariableNames: 'paragraphBlText baTextMeasurer blSpan cairoScaledFont cairoGlyphsArray fontAndStyleBuilder'
classVariableNames: ''
package: 'Alexandrie-Canvas-Text’
(Extract from this report)
We detected an issue rendering text with Zero-width joiner sequences. What is that? Fonts can specify a glyph for certain sequence of characters. This is the case of the following example, where the same Unicode sequence is rendered on first row using only Cairo, and the second row using Harfbuzz before Cairo:
The sequence can be generated in this way:
WideString streamContents: [ :stream |
#( 16r1F468 16r1F469 16r1F467 16r1F466 ) "<-- individual faces"
do: [ :each | stream nextPut: (Unicode value: each) ]
separatedBy: [ stream nextPut: Unicode zeroWidthSeparator ]]
The Noto Color Emoji font has a specific glyph for that sequence: the family shown in the second row of the screen capture. Without Harfbuzz to apply the font rule, we can only render the individual characters of the sequence (as in the first row of the capture).
The bug: Some times the family glyph is not rendered... that portion of the image is just white. Good news is that this is reproducible, not random. This example can be found in: AeHarfbuzzRenderExample class>>#example6Emoji
, but should become a test.
Cairo doesn't necessarily need the use Freetype API. There is Harfbuzz-Cairo API which could avoid talking to Freetype directly from Alexandrie, to load fonts and render them.
It's not much code for this new package:
AeCairoFreetypeFontFace
AeFTFace>>#newCairoFontFace
)Reported by Renaud @rvillemeur in mailing-list:
(+ very small adaptation)
| surface context text buffer fontLibrary freetypeFace fontHeight scaledFont fontOptions |
fontHeight := 22.
text := 'a := A->B->>C <= c|=>d~~>e.'.
surface := AeCairoImageSurface
extent: 1000 @ (fontHeight * 2)
format: AeCairoSurfaceFormat argb32.
context := surface newContext.
context
sourceColorRGBA: Color white;
paint.
"Margin"
context translateBy: fontHeight / 2 @ 0.
"----------------------------------------------------------"
fontLibrary := AeFTLibrary newInitialized.
freetypeFace := AeCascadiaCodeDownloadedFont new
downloadDirectory:
AeFilesystemResources downloadedFontsDirectory;
ensureDownloaded;
firstFaceUsing: fontLibrary.
fontOptions := AeCairoFontOptions new
antialias: AeCairoAntialias fast;
hintMetrics: AeCairoHintMetrics on;
hintStyle: AeCairoHintStyle slight;
subpixelOrder: AeCairoSubpixelOrder default.
scaledFont := AeCairoScaledFont
fontFace:
(AeCairoFreetypeFontFace newForFace: freetypeFace)
fontMatrix:
(AeCairoMatrix newScalingByX: fontHeight y: fontHeight)
userToDeviceMatrix: AeCairoMatrix newIdentity
options: fontOptions.
"Draw text with Harfbuzz:"
buffer := AeHbBuffer new
direction: AeHbDirection leftToRight;
script: AeHbScript latin;
language: AeHbLanguage en;
clusterLevel: AeHbBufferClusterLevel recommended;
flags: AeHbBufferFlags beginningOrEndingOfText;
addString: text.
context translateBy: 0 @ (fontHeight * 1.1).
context sourceColorRGB: Color green muchDarker.
context
scaledFont: scaledFont;
showGlyphs:
(buffer cairoGlyphArrayForFace: freetypeFace size: fontHeight).
^ surface asForm
We are talking of our bindings to cairo_append_path function, in AeCairoContext>>#appendPath:
, which seem to be quite slow in benchmarks. Is it justified?
Reproduce:
AeBenchFigureGridRunner new cachePath: true; run
Tested on a Macbook 2018 (intel) and Windows PC.
Just that, add it to BaselineOfAlexandrie.
Requested by @estebanlm
Example of use:
scaleMatrix
| aCairoMatrix |
aCairoMatrix := AeCairoMatrix externalNew autoRelease; yourself.
self getScaleMatrixInto: aCairoMatrix.
^ aCairoMatrix
I observed 10x speedup:
134616.830 per second (AeCairoMatrix externalNew)
1137675.000 per second (AeCairoMatrix new)
It seems the version is too old for the bindings.
Evaluate:
AeFontManager resetGlobalInstance.
AeFontManager globalInstance scanDirectory: AeFilesystemResources fontsDirectory.
Inspect: AeFontManager globalInstance
and navigate:
The scan process registers duplicated the fonts as the directory contains the a TTF and an OTF file for each "Inria Serif" font:
Alexandrie-Canvas-Tests test are all failing because Color>>random: is missing in P11
These:
AeCairoExamplesRenderTest
surfaceWithTextAsPath
surfaceWithToyAPIAndSetFaceFont
An example from CI:
AeHbBlobTest
✗ #testOpenFromMemorySuccess (1ms)
FFIUnsupportedUntypedLiteral
FFICallout>>unsupportedUntypedLiteral:
FFIStrictResolutionMode>>resolveUndeclaredTypeForArgument:withResolver:
FFICallout>>resolveUntypedArgument:
FFIUndefinedTypeDeclaration>>resolveUsing:forArgument:
FFIConstantArgument>>resolveUsing:
[ :e | e resolveUsing: aResolver ] in FFIFunctionSpec>>resolveUsing:
OrderedCollection>>do:
FFIFunctionSpec>>resolveUsing:
TFCalloutMethodBuilder(FFICalloutMethodBuilder)>>generateMethodFromSpec:
TFCalloutMethodBuilder(FFICalloutMethodBuilder)>>generate
TFCalloutMethodBuilder(FFICalloutMethodBuilder)>>build:
TFCalloutAPI(FFICalloutAPI)>>function:library:
AeHbBlob class(Object)>>ffiCall:library:options:fixedArgumentCount:
AeHbBlob class(Object)>>ffiCall:library:options:
AeHbBlob class(Object)>>ffiCall:
AeHbBlob class>>unownedNewFrom:size:
AeHbBlob class>>unownedNewFrom:ifSuccess:ifFail:
AeHbBlob class>>newFrom:
AeHbBlobTest>>sourceSansProBlob
AeHbBlobTest>>testOpenFromMemorySuccess ...sourceSansProBlob
AeHbBlobTest(TestCase)>>performTest
Extracted from: https://github.com/pharo-graphics/Alexandrie/actions/runs/7956427152/job/21745103523
te := BlTextElement new
text: ('g' asRopedText
fontSize: 20;
fontName: 'Times New Roman';
yourself) ;
relocate: 0@0;
transformDo: [ :t |
t topLeftOrigin; scaleBy: 10 ];
yourself.
host := BAMorphicWindowHost new.
"host := BlMorphicWindowHost new."
BlSpace new
addChild: te;
host: host;
title: host asString;
extent: 200 asPoint;
show;
yourself.
Shouldn't it use BAMorphicWindowHost ?
BlSpaceBuilder>>initialize
super initialize.
space := BlSpace new.
spaceExtent := 100 asPoint.
host := BlMorphicWindowHost new.
....
Not too important, I guess. But no idea what caused it. In the 3 platforms apparently:
Ae26Dot6FractionalPointTest
✗ #testOnFloat (91ms)
TestFailure: Got 1234.546875 instead of 1234.56.
"This is a feature expected from any BlHost but creates a dependency to Sparta" flag in containsPoint: element: geometry:, offscreenMeasureTextParagraph: and visualBoundsOf:.
Maybe we should have a look at the GlamorousToolkit version which seems better designed.
Loading log shows:
Warning: Package Alexandrie-Cairo-Tests depends on the following classes:
AePixelMatchTest
You must resolve these dependencies before you will be able to load these definitions:
AeCairoRenderTest
AeCairoRenderTest>>#actualFormFor:
AeCairoRenderTest>>#allFormSelectors
AeCairoRenderTest>>#deleteAllExports
AeCairoRenderTest>>#drawTestPaintCheckeredOn:
AeCairoRenderTest>>#expectedFormsDirectory
AeCairoRenderTest>>#exportAll
AeCairoRenderTest>>#newCheckSurfaceExtent:
AeCairoRenderTest>>#newSurfaceExtent:
AeCairoRenderTest>>#surfaceWithFillAndStroke
AeCairoRenderTest>>#surfaceWithFillAndStrokeAlpha
AeCairoRenderTest>>#surfaceWithFillAndStrokeAlphaAdd
AeCairoRenderTest>>#surfaceWithGradientAlpha
AeCairoRenderTest>>#surfaceWithGradientConstantAlpha
AeCairoRenderTest>>#surfaceWithGradientZeroStops
AeCairoRenderTest>>#surfaceWithGradientZeroStopsMask
AeCairoRenderTest>>#surfaceWithGroupClip
AeCairoRenderTest>>#surfaceWithGroupPaint
AeCairoRenderTest>>#surfaceWithGroupUnaligned
The package Bloc-Alexandrie fails to load given it includes alexandrieFixedMeasurer, an extension to an Album class.
I think we should move it to Album, and make Bloc-Alexandrie loaded by default.
@plantec ?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.