// SPDX-License-Identifier: Unlicense OR BSD-3-Clausepackage shapingimport (otucd)typeInputstruct {// Text is the body of text being shaped. Only the range Text[RunStart:RunEnd] is considered // for shaping, with the rest provided as context for the shaper. This helps with, for example, // cross-run Arabic shaping or handling combining marks at the start of a run. Text []rune// RunStart and RunEnd indicate the subslice of Text being shaped. RunStart, RunEnd int// Direction is the directionality of the text. Direction di.Direction// Face is the font face to render the text in. Face *font.Face// FontFeatures activates or deactivates optional features // provided by the font. // The settings are applied to the whole [Text]. FontFeatures []FontFeature// Size is the requested size of the font. // More generally, it is a scale factor applied to the resulting metrics. // For instance, given a device resolution (in dpi) and a point size (like 14), the `Size` to // get result in pixels is given by : pointSize * dpi / 72 Size fixed.Int26_6// Script is an identifier for the writing system used in the text. Script language.Script// Language is an identifier for the language of the text. Language language.Language}// FontFeature sets one font feature.//// A font feature is an optionnal behavior a font might expose,// identified by a 4 bytes [Tag].// Most features are disabled by default; setting a non zero [Value]// enables it.//// An exemple of font feature is the replacement of fractions (like 1/2, 3/4)// by specialized glyphs, which would be activated by using//// FontFeature{Tag: ot.MustNewTag("frac"), Value: 1}//// See also https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist// and https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_fonts/OpenType_fonts_guidetypeFontFeaturestruct { Tag ot.Tag Value uint32}// Fontmap provides a general mechanism to select// a face to use when shaping text.typeFontmapinterface {// ResolveFace is called by [SplitByFace] and [Segmenter.Split] for each input rune potentially // triggering a face change. // It must always return a valid (non nil) [*font.Face] value.ResolveFace(r rune) *font.Face}// FontmapScript is an optional interface supporting script hints.typeFontmapScriptinterface {Fontmap// SetScript set the script to which the (next) runes passed to [ResolveFace] // belong. // // Providing this information before calling [ResolveFace] is useful for two reasons : // - optimizing [ResolveFace] for runes with a shared script // - for runes with Common and Inherited script, the script resolved by a segmentation step // is more accurateSetScript(language.Script)}var _ Fontmap = fixedFontmap(nil)type fixedFontmap []*font.Face// ResolveFace panics if the slice is emptyfunc ( fixedFontmap) ( rune) *font.Face {for , := range {if , := .NominalGlyph(); {return } }return [0]}// SplitByFontGlyphs split the runes from 'input' to several items, sharing the same// characteristics as 'input', expected for the `Face` which is set to// the first font among 'availableFonts' providing support for all the runes// in the item.// Runes supported by no fonts are mapped to the first element of 'availableFonts', which// must not be empty.// The 'Face' field of 'input' is ignored: only 'availableFaces' are consulted.// Rune coverage is obtained by calling the NominalGlyph() method of each font.// See also [SplitByFace] for a more general approach of font selection.func ( Input, []*font.Face) []Input {returnSplitByFace(, fixedFontmap())}// SplitByFace split the runes from 'input' to several items, sharing the same// characteristics as 'input', expected for the `Face` which is set to// the return value of the [Fontmap.ResolveFace] call.// The 'Face' field of 'input' is ignored: only 'availableFaces' is used to select the face.func ( Input, Fontmap) []Input {returnsplitByFace(, , nil)}// Segmenter holds a state used to split input// according to three caracteristics : text direction (bidi),// script, and face.typeSegmenterstruct {// pools of inputs, used to reduce allocations, // which are alternatively swapped between each step of the segmentation input, output []Input// used to handle Common script delimStack []delimEntry// buffer used for bidi segmentation bidiParagraph bidi.Paragraph}type delimEntry struct { index int// in the [pairedDelims] list script language.Script// resolved from the context}// Split segments the given pre-configured input according to:// - text direction// - script// - language// - (vertical text only) glyph orientation// - face, as defined by [faces]//// Only the input runes in the range [text.RunStart] to [text.RunEnd] will be split.//// As a consequence, it sets the following fields of the returned runs:// - Text, RunStart, RunEnd// - Direction// - Script// - Language// - Face//// [text.Direction] is used during bidi ordering, and should refer to the general// context [text] is used in (typically the user system preference for GUI apps.)//// For vertical text, if its orientation is set, is copied as it is; otherwise, the// orientation is resolved using the Unicode recommendations (see https://www.unicode.org/reports/tr50/).//// When possible, the language are resolved to match the current script. For instance,// (language: 'fr', script: 'arabic') is resolved to language: 'arabic'.//// The returned sliced is owned by the [Segmenter] and is only valid until// the next call to [Split].func ( *Segmenter) ( Input, Fontmap) []Input { .reset() .splitByBidi() // fills output .input, .output = .output, .input// output is empty .splitByScript() .enforceLanguages()// if needed, resolve text orientation for vertical textif .Direction.IsVertical() && !.Direction.HasVerticalOrientation() { .input, .output = .output, .input .output = .output[:0] .splitByVertOrientation() } .input, .output = .output, .input .output = .output[:0] .splitByFace()return .output}func ( *Segmenter) () {// zero the slices to avoid 'memory leak' on pointer slice fieldsfor := range .input { .input[].Text = nil .input[].FontFeatures = nil }for := range .output { .output[].Text = nil .output[].FontFeatures = nil } .input = .input[:0] .output = .output[:0]// bidiParagraph is reset when using SetString .delimStack = .delimStack[:0]}func ( *Segmenter) ( Input) {// split vertical text like horizontal oneif .RunStart >= .RunEnd { .output = append(.output, )return } := bidi.LeftToRightif .Direction.Progression() == di.TowardTopLeft { = bidi.RightToLeft } .bidiParagraph.SetString(string(.Text[.RunStart:.RunEnd]), bidi.DefaultDirection()) , := .bidiParagraph.Order()if != nil || .NumRuns() == 0 { .output = append(.output, )return } := // start a rune 0 of the runfor := 0; < .NumRuns(); ++ { := := .Run() := .Direction() , := .Pos() += .RunStart// shift by the input run position .RunEnd = + 1// override the directionif == bidi.RightToLeft { .Direction.SetProgression(di.TowardTopLeft) } else { .Direction.SetProgression(di.FromTopLeft) } .output = append(.output, ) .RunStart = .RunEnd }}// lookupDelimIndex binary searches in the list of the paired delimiters,// and returns -1 if `ch` is not foundfunc lookupDelimIndex( rune) int { := 0 := len(pairedDelims) - 1for <= { := ( + ) / 2if < pairedDelims[] { = - 1 } elseif > pairedDelims[] { = + 1 } else {return } }return -1}// See https://unicode.org/reports/tr24/#Common for referencefunc ( *Segmenter) () {for , := range .input { := .Script = language.Commonfor := .RunStart; < .RunEnd; ++ { := .Text[] := language.LookupScript()// to properly handle Common script, // we register paired delimiters := -1if !.Strong() { = lookupDelimIndex() }if >= 0 { // handle paired charactersif %2 == 0 {// this is an open character : push it onto the stack .delimStack = append(.delimStack, delimEntry{, .Script}) } else {// this is a close character : try to look backward in the stack // for its counterpart := - 1 := len(.delimStack) - 1for ; >= 0; -- {if .delimStack[].index == { // found a match, use its script = .delimStack[].scriptbreak } }// in any case, pop the open charactersif == -1 { = 0 } .delimStack = .delimStack[:] } }// check if we have a 'real' change of script, or notif !.Strong() || == .Script {// no changecontinue } elseif .Script == language.Common {// update the pair stack to attribute the resolved scriptfor := range .delimStack { .delimStack[].script = }// set the resolved script to the current run, // but do NOT create a new run .Script = } else {// split to a new runif != .RunStart { // push the existing one .RunEnd = .output = append(.output, ) } .RunStart = .Script = } }// close and add the last input .RunEnd = .RunEnd .output = append(.output, ) }}// assume splitByScript has been called and enforce sane languagesfunc ( *Segmenter) () { := .output[0].Language// if no language is specified, use a default // known by the libraryif == "" { = "en" } , := language.NewLangID()if ! {// the language is unknown to the library, nothing to doreturn }for , := range .output { := enforceLang(, .Script) .output[].Language = .Language() }}// assume the script has been resolvedfunc ( *Segmenter) () {for , := range .input { := ucd.LookupVerticalOrientation(.Script) := for := .RunStart; < .RunEnd; ++ { := .Text[] := .Orientation()if == .RunStart {// first run : update the orientation, // but do not create a new run .Direction.SetSideways()continue }if != .Direction.IsSideways() {// create new run : push the current one ... .RunEnd = .output = append(.output, )// ... and update the 'new' .RunStart = .Direction.SetSideways() } }// close and add the last input .RunEnd = .RunEnd .output = append(.output, ) }}// assume [splitByScript] has been calledfunc ( *Segmenter) ( Fontmap) { , := .(FontmapScript)for , := range .input {if { .SetScript(.Script) } .output = splitByFace(, , .output) }}func splitByFace( Input, Fontmap, []Input) []Input { := for := .RunStart; < .RunEnd; ++ { := .Text[]// We can safely ignore characters if we have a face or if there is more text, // but we must force the choice of a face if we still don't have one and we reach // the final rune. Otherwise strings like all-whitespace are never assigned a face.ifignoreFaceChange() && (.Face != nil || < .RunEnd-1) {// add the rune to the current inputcontinue }// select the first font supporting r := .ResolveFace()// now that we have a font, apply it back, // but do NOT create a new runif .Face == nil { .Face = }if .Face == {// add the rune to the current inputcontinue }// new face neededif != .RunStart {// close the current input ... .RunEnd = // ... add it to the output ... = append(, ) }// ... and create a new one = .RunStart = .Face = }// close and add the last input .RunEnd = .RunEnd = append(, )return}// ignoreFaceChange returns `true` is the given rune should not trigger// a change of font.//// We don't want space characters to affect font selection; in general,// it's always wrong to select a font just to render a space.// We assume that all fonts have the ASCII space, and for other space// characters if they don't, HarfBuzz will compatibility-decompose them// to ASCII space...//// We don't want to change fonts for line or paragraph separators.//// Finaly, we also don't change fonts for what Harfbuzz consider// as ignorable (however, some Control Format runes like 06DD are not ignored).//// The rationale is taken from pango : see bugs// https://bugzilla.gnome.org/show_bug.cgi?id=355987// https://bugzilla.gnome.org/show_bug.cgi?id=701652// https://bugzilla.gnome.org/show_bug.cgi?id=781123// for more details.func ignoreFaceChange( rune) bool { := ucd.LookupGeneralCategory()return == ucd.Cc || // control == ucd.Cs || // surrogate == ucd.Zl || // line separator == ucd.Zp || // paragraph separator ( == ucd.Zs && != '\u1680') || // space separator != OGHAM SPACE MARKharfbuzz.IsDefaultIgnorable()}// enforceLang makes sure the returned language is compatible with// the given script, so that it maybe be usefull for Opentype shaping.//// A typical example is a Korean text found into a French paragraph ://// enforceLang("fr", language.Hangul) -> "ko"func enforceLang( language.LangID, language.Script) language.LangID {if .UseScript() {// the language is consistent with the script, return itreturn }// the language is not used for the script, try to replace itif := language.ScriptToLang[]; != 0 {return }// we do not have good replacement, leave it as it isreturn}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.