Skip to content

Commit

Permalink
Harfbuzz: support shape features
Browse files Browse the repository at this point in the history
  • Loading branch information
tinchodias committed Dec 1, 2023
1 parent 76e8cab commit ec77e35
Show file tree
Hide file tree
Showing 5 changed files with 385 additions and 1 deletion.
44 changes: 44 additions & 0 deletions src/Alexandrie-Harfbuzz-Tests/AeHbBufferTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,50 @@ AeHbBufferTest >> testReset [

]

{ #category : 'tests' }
AeHbBufferTest >> testShapeExample [

| aBuffer aBlob aFace aFont |
aBuffer := AeHbBuffer new
addString: 'off';
guessSegmentProperties;
yourself.
aBlob := AeHbBlob newLocatedAt: AeFilesystemResources inriaSerifRegularTTF.
aFace := aBlob newHbFaceAtIndex: 0.
aFont := aFace newHbFont.

aBuffer shapeWithFont: aFont.

self assert: aBuffer length equals: 2.
self assert: aBuffer glyphInfos size equals: 2.
self assert: aBuffer glyphPositions size equals: 2.
]

{ #category : 'tests' }
AeHbBufferTest >> testShapeWithFontFeatures [
"By default, shape with this font replaces the ff sequence by a single glyph.
This test shows how to disable one of the possible replacements."

| aBuffer aBlob aFace aFont featuresArray aString |
aString := 'off;off;off;'.
aBuffer := AeHbBuffer new
addString: aString;
guessSegmentProperties;
yourself.

aBlob := AeHbBlob newLocatedAt: AeFilesystemResources inriaSerifRegularTTF.
aFace := aBlob newHbFaceAtIndex: 0.
aFont := aFace newHbFont.

featuresArray := FFIExternalArray newType: AeHbFeature size: 2.
featuresArray first readIntoSelf: '+liga[0:4]'.
featuresArray second readIntoSelf: '-liga[4:8]'.

aBuffer shapeWithFont: aFont features: featuresArray.
"if all ff occurences were replaced, it would be -3:"
self assert: aBuffer length equals: aString size - 2
]

{ #category : 'tests' }
AeHbBufferTest >> testSimpleExample [
"Adaptation of https://harfbuzz.github.io/a-simple-shaping-example.html"
Expand Down
134 changes: 134 additions & 0 deletions src/Alexandrie-Harfbuzz-Tests/AeHbFeatureTest.class.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"
An AeHbFeatureTest is a test class for testing the behavior of AeHbFeature
"
Class {
#name : 'AeHbFeatureTest',
#superclass : 'TestCase',
#category : 'Alexandrie-Harfbuzz-Tests',
#package : 'Alexandrie-Harfbuzz-Tests'
}

{ #category : 'tests' }
AeHbFeatureTest >> testAccessors [

| aFeature |
aFeature := AeHbFeature new.

aFeature tagString: 'aalt'.
self assert: aFeature tagString equals: 'aalt'.

aFeature value: 3.
self assert: aFeature value equals: 3.

aFeature setGlobalStart.
self assert: aFeature start equals: AeHbFeature globalStart.
self assert: aFeature startsAtGlobalStart.

aFeature start: 17.
self assert: aFeature start equals: 17.
self deny: aFeature startsAtGlobalStart.

aFeature setGlobalEnd.
self assert: aFeature end equals: AeHbFeature globalEnd.
self assert: aFeature endsAtGlobalEnd.

aFeature end: 23.
self assert: aFeature end equals: 23.
self deny: aFeature endsAtGlobalEnd.

]

{ #category : 'tests' }
AeHbFeatureTest >> testParseAfterInstanceCreation [

| aFeature |
aFeature := AeHbFeature new.
aFeature start: 5.
self assert: aFeature start equals: 5.
aFeature readIntoSelf: 'liga[3]'.
self assert: aFeature start equals: 3
]

{ #category : 'tests' }
AeHbFeatureTest >> testParseInstanceInArray [
"This is the most convenient use for the Harfbuzz shaping API."

| featuresArray aFeature |
featuresArray := FFIExternalArray newType: AeHbFeature size: 3.
featuresArray first readIntoSelf: '+kern[1:7]'.
featuresArray second readIntoSelf: '+smcp[5:11]'.
featuresArray third readIntoSelf: '-liga[:17]'.

aFeature := featuresArray first.
self assert: aFeature tagString equals: 'kern'.
self assert: aFeature value equals: 1.
self assert: aFeature start equals: 1.
self assert: aFeature end equals: 7.

aFeature := featuresArray second.
self assert: aFeature tagString equals: 'smcp'.
self assert: aFeature value equals: 1.
self assert: aFeature start equals: 5.
self assert: aFeature end equals: 11.

aFeature := featuresArray third.
self assert: aFeature tagString equals: 'liga'.
self assert: aFeature value equals: 0.
self assert: aFeature start equals: 0.
self assert: aFeature end equals: 17.
]

{ #category : 'tests' }
AeHbFeatureTest >> testParseSecondAlternativeInRange [

| aFeature |
aFeature := AeHbFeature readFrom: 'aalt[3:5]=2'.
self assert: aFeature tagString equals: 'aalt'.
self assert: aFeature value equals: 2.
self assert: aFeature start equals: 3.
self assert: aFeature end equals: 5
]

{ #category : 'tests' }
AeHbFeatureTest >> testParseTurnFeatureOn [

| aFeature |
aFeature := AeHbFeature readFrom: 'kern'.
self assert: aFeature tagString equals: 'kern'.
self assert: aFeature value equals: 1.
self assert: aFeature startsAtGlobalStart.
self assert: aFeature endsAtGlobalEnd
]

{ #category : 'tests' }
AeHbFeatureTest >> testParseTurnFeatureOnEndingAt [

| aFeature |
aFeature := AeHbFeature readFrom: 'kern[:5]'.
self assert: aFeature tagString equals: 'kern'.
self assert: aFeature value equals: 1.
self assert: aFeature start equals: 0.
self assert: aFeature end equals: 5
]

{ #category : 'tests' }
AeHbFeatureTest >> testParseTurnFeatureOnSingleCharacter [

| aFeature |
aFeature := AeHbFeature readFrom: 'kern[5]'.
self assert: aFeature tagString equals: 'kern'.
self assert: aFeature value equals: 1.
self assert: aFeature start equals: 5.
self assert: aFeature end equals: 6 "end is exclusive"
]

{ #category : 'tests' }
AeHbFeatureTest >> testParseTurnFeatureOnStartingAt [

| aFeature |
aFeature := AeHbFeature readFrom: 'kern[5:]'.
self assert: aFeature tagString equals: 'kern'.
self assert: aFeature value equals: 1.
self assert: aFeature start equals: 5.
self assert: aFeature endsAtGlobalEnd.
]
9 changes: 9 additions & 0 deletions src/Alexandrie-Harfbuzz/AeHbBuffer.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,15 @@ AeHbBuffer >> shapeWithFont: font [
numberOfFeatures: 0
]

{ #category : 'shaping text' }
AeHbBuffer >> shapeWithFont: font features: featuresArray [

^ self
shapeWithFont: font
features: featuresArray
numberOfFeatures: featuresArray size
]

{ #category : 'shaping text' }
AeHbBuffer >> shapeWithFont: font features: features numberOfFeatures: num_features [
"Shapes buffer using font turning its Unicode characters content to positioned glyphs. If features is not NULL, it will be used to control the features applied during shaping. If two features have the same tag but overlapping ranges the value of the feature with the higher index takes precedence.
Expand Down
197 changes: 197 additions & 0 deletions src/Alexandrie-Harfbuzz/AeHbFeature.class.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"
I represent FFI bindings to `hb_feature_t` struct.
The `hb_feature_t` is the structure that holds information about requested feature application. The feature will be applied with the given value to all glyphs which are in clusters between start (inclusive) and end (exclusive). Setting start to `HB_FEATURE_GLOBAL_START` (0) and end to `HB_FEATURE_GLOBAL_END` (-1) specifies that the feature always applies to the entire buffer.
Members:
* tag: The `hb_tag_t` tag of the feature
* value: The value of the feature. 0 disables the feature, non-zero (usually 1) enables the feature. For features implemented as lookup type 3 (like 'salt') the value is a one based index into the alternates.
* start: the cluster to start applying this feature setting (inclusive).
* end: the cluster to end applying this feature setting (exclusive).
See: https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-t
"
Class {
#name : 'AeHbFeature',
#superclass : 'FFIExternalStructure',
#traits : 'AeTHarfbuzzLibrary',
#classTraits : 'AeTHarfbuzzLibrary classTrait',
#classVars : [
'OFFSET_END',
'OFFSET_START',
'OFFSET_TAG',
'OFFSET_VALUE'
],
#pools : [
'AeHbTypes'
],
#category : 'Alexandrie-Harfbuzz-Structures',
#package : 'Alexandrie-Harfbuzz',
#tag : 'Structures'
}

{ #category : 'field definition' }
AeHbFeature class >> fieldsDesc [
"
self rebuildFieldAccessors
"

^ #(
hb_tag_t tag
uint32_t value
uint start
uint end
)
]

{ #category : 'accessing' }
AeHbFeature class >> globalEnd [
"Special setting to apply a feature up to the end of the buffer.
Migrated from src/hb-common.h:
`#define HB_FEATURE_GLOBAL_END ((unsigned int) -1)`
"

^ 16rffffffff
]

{ #category : 'accessing' }
AeHbFeature class >> globalStart [
"Special setting to apply a feature from the end of the buffer.
Migrated from src/hb-common.h:
`#define HB_FEATURE_GLOBAL_START 0`
"

^ 0
]

{ #category : 'instance creation' }
AeHbFeature class >> readFrom: aString [

^ self new
readIntoSelf: aString;
yourself
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> end [
"This method was automatically generated"
^handle unsignedLongAt: OFFSET_END
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> end: anObject [
"This method was automatically generated"
handle unsignedLongAt: OFFSET_END put: anObject
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> endsAtGlobalEnd [

^ self end = self class globalEnd
]

{ #category : 'printing' }
AeHbFeature >> printOn: aStream [

super printOn: aStream.
aStream nextPutAll: (
'({1}[{3}:{4}]={2})'
format: (#(tagString value start end)
collect: [ :each | self perform: each ]))

]

{ #category : 'parsing' }
AeHbFeature >> readIntoSelf: aString [

| utf8Encoded |
utf8Encoded := aString utf8Encoded.

^ self readIntoSelf: utf8Encoded size: utf8Encoded size
]

{ #category : 'parsing' }
AeHbFeature >> readIntoSelf: string size: stringSize [
"Parses a string into myself. The boolean answer indicates success.
See: https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string"

^ self ffiCall: #(
hb_bool_t
hb_feature_from_string (
const char *string,
int stringSize,
self )
)
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> setGlobalEnd [

self end: self class globalEnd
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> setGlobalStart [

self start: self class globalStart
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> start [
"This method was automatically generated"
^handle unsignedLongAt: OFFSET_START
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> start: anObject [
"This method was automatically generated"
handle unsignedLongAt: OFFSET_START put: anObject
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> startsAtGlobalStart [

^ self start = self class globalStart
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> tag [
"This method was automatically generated"
^handle unsignedLongAt: OFFSET_TAG
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> tag: anObject [
"This method was automatically generated"
handle unsignedLongAt: OFFSET_TAG put: anObject
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> tagString [

^ self tag asHbTagString
]

{ #category : 'accessing' }
AeHbFeature >> tagString: aString [
"Set my tag with the integer representation of the received tag.
See: https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-tag-t"

self tag: aString asHbTagInteger
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> value [
"This method was automatically generated"
^handle unsignedLongAt: OFFSET_VALUE
]

{ #category : 'accessing - structure variables' }
AeHbFeature >> value: anObject [
"This method was automatically generated"
handle unsignedLongAt: OFFSET_VALUE put: anObject
]
Loading

0 comments on commit ec77e35

Please sign in to comment.