Prima::...::Glyphs(3)
🎀 Prima::Drawable::Glyphs(3) — the perl manual page, but make it make sense 🎀
okay so bestie, gather round, because this one is from the User Contributed Perl Documentation and it's literally the helper-routines page for bi-directional text input and complex scripts output. yes. both of those things. at once. it's giving "main character energy" for every script that isn't plain left-to-right ASCII, and honestly? obsessed.
✨ NAME ✨
Prima::Drawable::Glyphs — helper routines for bi-directional text input and complex scripts output 🦋
✨ SYNOPSIS ✨
use Prima;
$::application-> begin_paint;
$::application-> text_shape_out('אפס123', 0,0);
and the output is, iconically:
123ספא
like?? it just does that. no cap. 🔥
✨ DESCRIPTION ✨
So the way this works: the class implements an abstraction layer by organizing arrays filled with information about glyphs as a structure that can be used to render text strings. basically it takes all the messy little facts about your glyphs and arranges them into something a renderer can actually vibe with 💅
Objects of the class are created and returned by the Prima::Drawable::text_shape method — see more in text_shape in Prima::Drawable. and a Prima::Drawable::Glyphs object is a blessed array reference that can contain either two, four, or five packed arrays with 16-bit integers, representing, correspondingly:
- a set of glyph indexes,
- a set of character indexes,
- a set of glyph advances,
- a set of glyph position offsets per glyph,
- and a font index.
two, four, or five — pick your fighter 😌
Additionally, the class implements several sets of helper routines that aim to address common tasks when displaying glyph-based text. so it's not just a data blob, it has skills.
🌸 Structure 🌸
Each sub-array is an instance of the Prima::array class — an effective plain memory structure that provides a standard perl interface over a string scalar filled with fixed-width integers. lowkey genius, ngl.
The following methods provide read-only access to these arrays:
glyphs— Contains a set of unsigned 16-bit integers where each is a glyph number corresponding to the font that was used for shaping the text. The glyph numbers are only applicable to the font used in the shaping process. Zero is usually treated as a default glyph in vector fonts when shaping cannot map a character; in bitmap fonts, this number is usually the same asdefaultChar.
The glyphs array is recognized as a special case when is sent to the text_out or get_text_width methods that can process it natively. In this case, no special advances and glyph positions are taken into account. convenient? yes. 💅
Each glyph is not necessarily mapped to a text character, and quite often is not — even in English left-to-right texts. F ex character combinations like ff, fi, and fl may be mapped to single ligature glyphs. When right-to-left, RTL, text direction is taken into account, the glyph positions may change, too. See indexes below that addresses the mapping of glyphs to characters. 👀
indexes— Contains a set of unsigned 16-bit integers where each is a text offset corresponding to the text used in the shaping process. Each glyph position points to the first character in the text that maps to the glyph.
There can be more than one character per glyph, such as the above example with the ff ligature. There can also be cases with more than one character per more than one glyph, f ex in indic scripts. In these cases it is easier to operate neither by character offsets nor by glyph offsets, but rather by clusters, where each cluster is an individual syntax unit that contains one or more characters per one or more glyphs. squad goals but for text units 🫶
In addition to the text offset, each index value can be flagged with a to::RTL bit, signifying that the character in question has RTL direction. This is not necessarily semitic characters from RTL languages that only have that attribute set; spaces in these languages are normally attributed with the RTL bit too, sometimes also numbers. The use of explicit direction control characters from the U+20XX block can result in any character being assigned or not assigned the RTL bit. it's giving "the bit is doing whatever it wants" 😭
The array has an extra item added to its end — the length of the text that was used for the shaping. This helps calculate the cluster length in characters, especially of the last one, where the difference between indexes is, basically, the cluster length. cute little sentinel value, love that for us.
The array is not used for text drawing or calculation, but only for conversion between character, glyph, and cluster coordinates (see Coordinates below).
advances— Contains a set of unsigned 16-bit integers where each is a pixel distance of how much space the corresponding glyph occupies. Where the advances array is not present, or was force-filled byadvancesoptions by thetext_shapemethod, a glyph advance value is basically the sum of a, b, and c widths of the corresponding glyph. However there are cases when depending on the shaping input, these values can differ. rude but okay 💀
One of those cases is the combining graphemes, where the text consisting of two characters, A and the combining grave accent U+300 should be drawn as a single À symbol, and where the font doesn't have that single glyph but rather two individual glyphs A and `. Even though the grave glyph has its own advance for standalone usage, in this case, it should be ignored; this is achieved by the shaper setting the advance of the ` to zero. iconic behavior honestly.
The array content is respected by the text_out and get_text_width methods, and its content can be changed at will to produce gaps in the text quite easily. F ex Prima::Edit uses that to display tab characters as spaces with the 8x advance. tab squad rise up 🧚
positions— Contains a set of pairs of signed 16-bit integers where each is an X and Y pixel offset for each glyph. Like in the previous example with theÀsymbol, the grave glyph`may be positioned differently on the vertical axis inÀandàgraphemes, for example. tiny little y-offsets doing the absolute most ✨
The array is respected by text_out (but not by get_text_width). know the difference, bestie.
fonts— Contains a set of unsigned 16-bit integers where each is an index in the font substitution list (seefont_mapperin Prima::Drawable). Zero means the current font.
The font substitution is applied by the text_shape method when the polyfont option is set (it is by default), and when the shaper cannot match all characters in the text to the glyphs using the current font. If the current font contains all the needed glyphs, this entry is not present at all. it simply doesn't exist. gone. not invited to the party 🙈
The array is respected by the text_out and get_text_width methods.
🦋 Coordinates 🦋
In addition to the natural character coordinates, where each index is a text offset that can be directly used in the substr perl function, the Prima::Drawable::Glyphs class offers two additional coordinate systems that help abstract the object data for the display and navigation. three coordinate systems total. we contain multitudes.
The glyph coordinate system is a rather straightforward copy of the character coordinate system, where each number is an offset in the glyphs array. Similarly, these offsets can be used to address individual glyphs, indexes, advances, and positions. However, these are not easy to use when one needs, for example, to select a grapheme with a mouse, or break a set of glyphs in such a way that a grapheme is not broken. These use cases can be managed more easily in the cluster coordinate system. 🎀
The cluster coordinates represent a virtually superimposed set of offsets where each corresponds to a set of one or more characters displayed by one or more glyphs. The most useful functions below operate in this system. like, this is the one you actually want, iykyk.
⭐ Visual selection ⭐
The coordinates that are best used for implementing the visual selection are either characters or clusters, but not glyphs. The character-based selection makes it trivial to extract or replace the selected text, while the cluster-based makes it easier to manipulate (f ex with Shift-arrow keys) the selection itself. both have their moment, periodt.
The class supports both, by operating on selection maps or selection chunks, where each represents the same information but in different ways. For example, consider an embedded number in a bidi text. For the sake of clarity, I'll use Latin characters here. Let's imagine a text scalar containing these characters:
ABC123
where ABC is a right-to-left text that, if rendered on the screen should be displayed as
123CBA
(and the indexes, i e the offsets of the first characters for each glyph, are (3,4,5,2,1,0) ). it's reversing. it's serving. it's doing the most for no reason.
Next, the user clicks the mouse between the glyphs A and B (in the text offset of 1), drags the mouse to the left, and finally stops between the characters 2 and 3 (in the text offset of 4). The resulting selection then should not be, as one might naively expect, this:
123CBA
__^^^_
but this instead:
123CBA
^^_^^_
because the next character after C is 1, and the range of the selected sub-text is from characters 1 to 4. see?? the selection is not contiguous on screen and that's FINE. it's giving "visual selection is a lie but a beautiful one" 😭
The class offers means to encode such information in a map, i.e. an array of integers 1,1,0,1,1,0, where each entry is either 0 or 1 depending on whether the cluster is or is not selected. Alternatively, the same information can be encoded in chunks, or RLE sets, as an array 0,2,1,2,1, where the first integer signifies the number of non-selected clusters to display, the second — the number of selected clusters, the third the non-selected again, etc. If the first character belongs to the selected chunk, the first integer in the result is set to 0. run-length encoding my beloved 💜
🌈 Bidi input 🌈
When sending an input to a widget to type some text, the otherwise trivial case of figuring out at which position the text should be inserted (or removed, for that matter), becomes interesting when there are characters with mixed input direction. "interesting." sure. we'll call it that. 💀
F ex it is indeed trivial, when the Latin text is AB, and the cursor is positioned between A and B, to figure out that whenever the user types C, the result should become ACB. Likewise, when the text is RTL and both text and input are Arabic, the result is the same. easy. slay. no notes. 👑
However when f.ex. the text is A1, which is displayed as 1A because of the RTL shaping, and the cursor is positioned between the 1 (LTR) and A (RTL) glyphs, it is not clear whether that means the new input should be appended after 1 and become A1C, or after A, and become, correspondingly, AC1. and that's the whole drama. two outcomes. one cursor. chaos.
There is no easy solution for this problem, and different programs approach this differently, where some go as far as to provide two cursors for both input directions. two cursors!! main character energy but make it a text field.
The class offers its own solution that uses some primitive heuristics to detect whether the cursor belongs to the left or the right glyph. This is the area that can be enhanced, and any help from native users of the languages that use the right-to-left writing system can be greatly appreciated. 🫶 like genuinely — if you speak these languages, your input here is gold, please come through.
✨ API ✨
abc $CANVAS, $INDEX— Returns the a, b, c metrics from the glyph$INDEX.advances— A read-only accessor to the advances array, seeStructureabove.clone— Clones the object. one object becomes two. we love a duplicate-and-diverge moment 🦋
...continuing the full method map — no skips, no "you get the idea", we're doing all of them 💅✨
cluster2glyph $FROM, $LENGTH
Maps the range of clusters that starts at $FROM and is $LENGTH long into the matching range of glyphs 🎯. If $LENGTH is undefined, it just calcs the range from $FROM all the way to the object's end. Slay, lazy-friendly.
cluster2index $CLUSTER
Gives you back the character offset of the first character in cluster $CLUSTER 💖.
Note: the result may contain the to::RTL flag. so like, don't freak out if it looks extra.
cluster2range $CLUSTER
Returns two things: the character offset of the first character in cluster $CLUSTER, and how many characters that cluster has 🫶.
clusters
Returns an array of integers where each one is the offset of the first character in each cluster. That's it, that's the whole vibe.
cursor2offset $AT_CLUSTER, $PREFERRED_RTL
So picture this: the cursor is sitting next to cluster $AT_CLUSTER, and you need to know which character offset that actually is. This runs some cute simple heuristics to figure it out 🧠✨. The $PREFERRED_RTL flag is the fallback bae — it gets used when the object data doesn't have enough info to decide the text direction.
See "Bidi input" above, bestie. iykyk.
def $CANVAS, $INDEX
Hands you the d, e, f metrics from glyph $INDEX 🌸. Tiny but important.
fonts
A read-only accessor to the font indexes. Peep Structure above for the deets 👑.
get_box $CANVAS
Returns the box metrics of the glyph object 📦.
See "get_text_box" in Prima::Drawable if you want the whole story.
get_sub $FROM, $LENGTH
Extracts and clones a whole new object containing the data from cluster offset $FROM with cluster length $LENGTH 🦋. It's giving "copy-paste but make it legit".
get_sub_box $CANVAS, $FROM, $LENGTH
Calculates the box metrics of the glyph string starting at cluster $FROM with size $LENGTH 📐.
get_sub_width $CANVAS, $FROM, $LENGTH
Calculates the pixel width of the glyph string from cluster $FROM with size $LENGTH 📏✨.
get_width $CANVAS, $WITH_OVERHANGS
Returns the width of the glyph object — and yes, with overhangs if requested 💅.
glyph2cluster $GLYPH
Returns the cluster that contains $GLYPH. Reverse mapping, iconic behaviour 🔁.
glyphs
A read-only accessor to the glyph indexes array. Structure section above has the lore 👑.
glyph_lengths
Returns an array where each glyph position tells you how many glyphs the corresponding cluster occupies. Like a little census but for glyphs 🍓.
index2cluster $INDEX, $ADVANCE = 0
Returns the cluster that contains the character offset $INDEX 💖.
Set $ADVANCE to 1 if you want the RTL-dependent advance added to the resulting cluster. It's in the original, don't @ me.
indexes
A read-only accessor to the indexes — Structure above, as always, bestie 🌈.
index_lengths
Returns an array where each glyph position is the number of how many characters the corresponding cluster occupies. Same energy as glyph_lengths, different body count 💀.
justify CANVAS, TEXT, WIDTH, %OPTIONS
The umbrella call, the main character of justification ☂️✨. It routes to:
"justify_interspace"if$OPTIONS{letter}or$OPTIONS{word}is set"justify_arabic"if$OPTIONS{kashida}is set"justify_tabs"if$OPTIONS{tabs}is set
Returns a boolean flag telling you whether the glyph object was changed or not. 1 = glow up happened, 0 = untouched and unbothered.
justify_arabic CANVAS, TEXT, WIDTH, %OPTIONS
Performs justification of Arabic TEXT with kashida out to the given WIDTH 🎀. Returns either a success flag, or a new text with explicit tatweel characters inserted. Two moods, one method.
my $text = "\x{6a9}\x{634}\x{6cc}\x{62f}\x{647}";
my $g = $canvas->text_shape($text) or return;
$canvas->text_out($g, 10, 50);
$g->justify_arabic($canvas, $text, 200) or return;
$canvas->text_out($g, 10, 10);
Okay so the rules, and they're specific 🫶:
- Inserts tatweels only between Arabic letters that did not form any ligatures in the glyph object — max one tatweel set per word (if any at all).
- Does not apply the justification if the letters in the word are rendered as LTR due to embedding or explicit shaping options. It only justifies RTL letters, periodt.
- If, for some reason, the newly inserted tatweels don't form a monotonically increasing series after shaping, it just skips the justification in that word. No forcing, no drama 🙈.
Note: it does not use the JSTF font table. On Windows the results may differ from native rendering. Fr fr, that's a real caveat.
Options:
If justification is found to be needed, eventual ligatures with the newly inserted tatweel glyphs get resolved via a call to text_shape(%OPTIONS) — so any shaping options you need, like "language", can be passed straight through there 💜.
as_textBOOL = 0— If set, returns the new text with the inserted tatweels, orundefif no justification is possible. If unset, it runs in-place justification on the caller glyph object and returns the boolean success flag.min_kashidaINTEGER = 0— Specifies the minimal width of a kashida strike to be inserted ✨.kashida_widthINTEGER— During the calculation the width of the tatweel glyph is needed. Unless you supply it with this option, it's calculated dynamically. Also, when called in list context and it succeeds, it returns a"1, kashida_width"tuple that you can reuse in subsequent calls. Efficiency queen 👑.
justify_interspace CANVAS, TEXT, WIDTH, %OPTIONS
Performs an in-place inter-letter and/or inter-word justification of TEXT out to the given WIDTH 💅. Returns either a boolean flag for "did anything change" or the new text with explicit space characters inserted. Again, dual citizenship.
Options:
as_textBOOL = 0— If set, returns new text with inserted spaces, orundefif no justification is possible. If unset, it runs in-place justification on the caller glyph object and returns the boolean success flag.letterBOOL = 1— If set, runs inter-letter spacing on all glyphs 🔤.max_interletterFLOAT = 1.05— When inter-letter spacing is applied, it's applied first, so the width of the resulting text line can take up to$OPTIONS{max_interletter} * glyph_widthpixels. The inter-word spacing has no such limit, and in the worst case can produce two words moved to the left and right edges of the enclosing0 - WIDTH-1rectangle 😭. Yeah. Worst case is a vibe crime.space_widthINTEGER—as_textmode: during the calculation the width of the space glyph may be needed. Unless supplied by$OPTIONS{space_width}, it's calculated dynamically. Also, when called in list context and it succeeds, returns the"1, space_width"tuple that can be reused in subsequent calls 🎀.wordBOOL = 1— If set, runs inter-word spacing by extending advances on all space glyphs.min_text_to_space_ratioFLOAT = 0.75— If thewordoption is set, it does not run inter-word justification when the text-to-space ratio is too small. Which, honestly, is a mercy — it's so the text doesn't get spread too thin 🦋.
justify_tabs CANVAS, TEXT, %OPTIONS
Expands the tab characters as $OPTIONS{tabs} spaces — default is 8 📑✨.
It needs the advance of the space glyph to replace the tab glyph. If no $OPTIONS{glyph} and $OPTIONS{width} are specified, it calculates them itself. Independent queen.
Returns a boolean flag for whether any changes were made. On success, if called in list context, it also returns the space glyph ID and the space glyph width, so you can reuse them on later calls 💖.
left_overhang
The first integer from the "overhangs" result. That's the entire description. Short king 💀.
log2vis
Returns a map of integers where each character position corresponds to the glyph position 🗺️. The name is a rudiment from pure fribidi shaping, back when "log2vis" and "vis2log" were mapper functions with the same functionality. History lesson included, free of charge.
n_clusters
Calculates how many clusters are in the object 🔢. Counting is self care.
new @ARRAYS
Creates a new object ✨. You don't use this directly — it gets created automatically inside the "text_shape" method. It's giving backstage crew.
new_array NAME
Creates an array suitable for direct insertion into the object, in case you need to manually construct the object 🧚. For example, one may set the missing "fonts" array like this:
$obj->[ Prima::Drawable::Glyphs::FONTS() ] = $obj->new_array('fonts');
$obj->fonts->[0] = 1;
The newly created array is filled with zeros. Blank slate, clean start 🌸.
new_empty
Creates a new empty object. Empty but make it fashion 💅.
overhangs
Calculates two widths for overhangs at the beginning and at the end of the glyph string 🎀. This is what's used in the emulation of the "get_text_width" method with the "to::AddOverhangs" flag. Teamwork, honestly.
positions
A read-only accessor to the positions array. Structure above, you know the drill 👑.
reorder_text TEXT
Returns a visual representation of "TEXT", assuming it was the input of the "text_shape" call that created the object 🔁✨. Like a replay of what happened but visual.
reverse
Creates a new object that has all arrays reversed 🙃. Used for calculation of the pixel offset from the right end of a glyph string. Mirror mirror on the wall.
right_overhang
The second integer from the "overhangs" result. Iconic sequel 🍓.
selection2range $CLUSTER_START $CLUSTER_END
Converts a cluster selection range into a text selection range 🎯. Note the no-comma argument style in the signature — copy it exactly, that's how it's written.
selection_chunks_clusters, selection_chunks_glyphs $START, $END
Converts a text selection, given as the visual range between $START and $END, into a set of integers (chunks), where each one is the number of selected or not-selected clusters or glyphs 🧮💖.
Lowkey the important bit: the first chunk is a number of non-selected items, and it's 0 if the first cluster or glyph is selected. Yes, that's the trick of it.
selection_diff $OLD, $NEW
Given two chunk sets in the format that "selection_chunks_clusters" or "selection_chunks_glyphs" returns, this calculates the new set of chunks where each integer value corresponds to the number of clusters or glyphs affected by the transition from the $OLD to the $NEW visual selection ✨.
And the first chunk is the number of non-affected items, and it's 0 if the first cluster or glyph is affected by the selection change 🫶.
Where this eats: efficient repaints. When the user interactively changes text selection, you redraw only the changed regions. No full repaint, no lag, no tears 😭👉✨
selection_map_clusters, selection_map_glyphs $START, $END
Same as "selection_chunks_XXX", but instead of RLE chunks it returns a full array for each cluster/glyph, where each entry is a boolean value for whether that cluster/glyph is to be displayed as selected or not 🗺️. Same info, less compact, more direct.
selection_walk $CHUNKS, $FROM, $TO = length, $SUB
Walks the selection chunks array that "selection_chunks" returned, between $FROM and $TO clusters/glyphs 🚶♀️💨.
Calls your provided "$SUB->($offset, $length, $selected)" for each chunk — each call gets 2 integers (the chunk offset and its length) plus a boolean flag for whether the chunk is selected or not. Literally just hands you the data, no gatekeeping.
You can also run it on a result of "selection_diff", and in that case the $selected flag shows whether the chunk is affected by the selection change or not 🎀.
sub_text_out $CANVAS, $FROM, $LENGTH, $X, $Y
An optimized version of:
$CANVAS->text_out( $self->get_sub($FROM, $LENGTH), $X, $Y )
Same output, less work. Speed run behaviour 🔥.
okay that's the method wall and we cleared it, next bit incoming 💜
sub_text_wrap $CANVAS, $FROM, $LENGTH, $WIDTH, $OPT, $TABS— this is the glow-up version of$CANVAS->text_wrap( $self->get_sub($FROM, $LENGTH), $WIDTH, $OPT, $TABS )— same energy, way more optimized ✨ and the result comes back converted to chunks too, iykyk.text_length— hands you back the length of the text that got shaped and that produced the object. that's it, that's the method 💅x2cluster $CANVAS, $X, $FROM, $LENGTH— okay so you give it the sub-cluster starting at$FROMwith size$LENGTH, and it does the math on how many clusters would actually fit into$Xpixels. pixel-to-cluster conversion, no cap 📐_debug— dumps the glyph object's content in a readable format. the little peek-behind-the-curtain moment 🔍
✨ EXAMPLES ✨
okay bestie so this whole section is literally only here to test proper rendering. that's the assignment. nothing else. but honestly? it's giving aesthetic and i'm obsessed 🌈
🌸 Latin
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
🦋 Latin combining
look at these little combining marks go — the way they stack is iconic 😭
Duis͙ auteͬ irure̥ dolorͫ in̓
reprehenderit̅ in̷ voluptate̪
velit̽ esse̮ cillum͂ dolore̯ eu̅ fugiať nulla̒ pariatur̀
🎀 Cyrillic
Lorem Ipsum используют потому, что тот обеспечивает более или менее
стандартное заполнение шаблона.
а также реальное распределение букв и пробелов в абзацах
💜 Hebrew
זוהי עובדה מבוססת שדעתו של הקורא תהיה מוסחת על ידי טקטס קריא כאשר
הוא יביט בפריסתו.
המטרה בשימוש ב-Lorem Ipsum הוא שיש לו פחות או יותר תפוצה של אותיות, בניגוד למלל
⭐ Arabic
العديد من برامح النشر المكتبي وبرامح تحرير صفحات الويب تستخدم لوريم
إيبسوم بشكل إفتراضي
كنموذج عن النص، وإذا قمت بإدخال "lorem ipsum" في أي محرك بحث ستظهر العديد من
🧚 Hindi
Lorem Ipsum के अश कई रप मं उपलबध ह, लकिन बहमत को किसी अनय
रप मं परिवरतन का सामना करना पडा ह, हासय डालना या करमरहित शबद
,
जो तनिक भी विशवसनीय नहीं लग रहे हो. यदि आप Lorem Ipsum के एक अनचछद का उपयोग करने जा रहे ह, तो आप को यकीन दिला दं कि पाठ के मधय मं वहाँ कछ भी शरमनाक छिपा हआ नहीं ह.
🍓 Chinese
无可否认,当读者在浏览一个页面的排版时,难免会被可阅读的内容所分散注意力。
Lorem Ipsum的目的就是为了保持字母多多少少标准及平
🔥 Thai
มหลกฐานทเปนขอเทจจรงยนยนมานานแลว
วาเนอหาทอานรเรองนนจะไปกวนสมาธของคนอานใหเขวไปจากสวนทเปน
Layout เรานำ Lorem Ipsum
มาใชเพราะความทมนมการกระจายของตวอกษรธรรมดาๆ แบบพอประมาณ
ซงเอามาใชแทนการเขยนวา ‘ตรงนเปนเนอหา, ตรงนเปนเนอหา'
ได้ และยงทำใหมองดเหมอนกบภาษาองกฤษทอานไดปกติ
ปจจบนมแพกเกจของซอฟทแวรการทำสอสงพมพ์
และซอฟทแวรการสรางเวบเพจ
กวนสมาธของคนอานใหเขวไปจากสวนทเปน Layout เรานำ Lorem Ipsum
🍓 Note: libthai is required for text wrapping by the word boundary. fr fr, don't skip this one.
👑 Largest well-known grapheme cluster in Unicode
the main character of grapheme clusters, the final boss, the one and only:
ཧྂ
source material, if you're curious: <http://archives.miloush.net/michkap/archive/2010/04/28/10002896.html>.
💖 AUTHOR
Dmitry Karasik, <dmitry@karasik.eu.org>. thank you king 🫶
🌟 SEE ALSO
examples/bidi.pl
and that's a wrap on part 3, bestie 🎀 you made it all the way through. this page came to you courtesy of perl v5.42.0, dated 2024-08-20, and it lives under the name Prima::...::Glyphs(3) — go forth and shape those glyphs, you're literally the main character of text rendering now 💅✨
📦 where this page comes from
- opensuse-leap 16.0: perl-Prima 1.74000-bp160.1.13 · AGPL-3.0-only AND BSD-2-Clause
- opensuse-leap 16.0: perl-Prima 1.74000-bp160.1.13 · AGPL-3.0-only AND BSD-2-Clause
- opensuse-leap 16.0: perl-Prima 1.74000-bp160.1.13 · AGPL-3.0-only AND BSD-2-Clause
- opensuse-leap 16.0: perl-Prima 1.74000-bp160.1.13 · AGPL-3.0-only AND BSD-2-Clause
- 💖 girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms