@@ -195,6 +195,26 @@ def _make_ogonek(font, cp, base_name):
195195 c .addReference ('comma' , psMat .compose (psMat .scale (- 1 , - 1 ), psMat .translate (dx , dy )))
196196
197197
198+ def _make_spacing_modifier (font , cp , mark_name , y_bottom = None , lsb = 40 , rsb = 40 ):
199+ """Spacing modifier letter: reference to mark glyph, repositioned and given advance width.
200+
201+ Translates the mark so its bottom sits at y_bottom and its left ink edge is
202+ at lsb. Default y_bottom is 20 units above the x-height of 'a', placing
203+ the modifier at a natural reading height as if hovering over a lowercase letter.
204+ lsb/rsb: left/right side bearings of the resulting glyph.
205+ """
206+ if y_bottom is None :
207+ y_bottom = int (round (font ['a' ].boundingBox ()[3 ] + 20 ))
208+ g = font .createMappedChar (cp )
209+ g .clear ()
210+ mark_bb = font [mark_name ].boundingBox ()
211+ dx = lsb - mark_bb [0 ]
212+ dy = y_bottom - mark_bb [1 ]
213+ g .addReference (mark_name , psMat .translate (dx , dy ))
214+ g .width = int (round (mark_bb [2 ] + dx + rsb ))
215+ return g
216+
217+
198218# Codepoints that already exist as hand-drawn glyphs; skip to avoid overwriting.
199219_SKIP_CPS = frozenset ({
200220 0x00DC , # Ü — hand-drawn
@@ -276,18 +296,44 @@ def _accented(cp, base_name, mark_name, gap=20, x_adj=0):
276296 (0x2033 , 'quotedbl' ), # doubleprime
277297 (0x2035 , 'quoteleft' ), # backprime
278298 # Dash aliases
299+ (0x2010 , 'hyphen' ), # Unicode hyphen
279300 (0x2011 , 'hyphen' ), # non-breaking hyphen
280301 (0x00AD , 'hyphen' ), # soft hyphen
281302 (0x2212 , 'endash' ), # minus sign
282303 (0x2012 , 'endash' ), # figure dash
283304 (0x2015 , 'emdash' ), # horizontal bar
305+ # Modifier letter apostrophes (Polynesian, transliteration)
306+ (0x02BB , 'quoteleft' ), # modifier letter turned comma ʻ
307+ (0x02BC , 'quoteright' ), # modifier letter apostrophe ʼ
284308]
285309for codepoint , source_name in ref_aliases :
286310 c = font .createMappedChar (codepoint )
287311 c .addReference (source_name )
288312 c .width = font [source_name ].width
289313
290314
315+ # ---------------------------------------------------------------------------
316+ # Inverted punctuation
317+ # ---------------------------------------------------------------------------
318+
319+ # U+00A1 ¡ INVERTED EXCLAMATION MARK — ! flipped vertically.
320+ # U+00BF ¿ INVERTED QUESTION MARK — ? flipped vertically.
321+ # scale(1,-1) reflects about y=0; the translate re-maps [ymin,ymax] back to the
322+ # same vertical range so the glyph stays above the baseline.
323+ for _cp , _gname in [(0x00A1 , 'exclam' ), (0x00BF , 'question' )]:
324+ _src = font [_gname ]
325+ _src_bb = _src .boundingBox ()
326+ _g = font .createMappedChar (_cp )
327+ _g .clear ()
328+ _layer = fontforge .layer ()
329+ for _c in _src .foreground :
330+ _layer += _c
331+ _g .foreground = _layer
332+ _g .transform (psMat .compose (psMat .scale (1 , - 1 ), psMat .translate (0 , _src_bb [1 ] + _src_bb [3 ])))
333+ _g .correctDirection ()
334+ _g .width = _src .width
335+
336+
291337# ---------------------------------------------------------------------------
292338# Combining mark glyphs
293339# ---------------------------------------------------------------------------
@@ -467,6 +513,24 @@ def _accented(cp, base_name, mark_name, gap=20, x_adj=0):
467513_c0327 .width = 0
468514
469515
516+ # ---------------------------------------------------------------------------
517+ # Spacing modifier letters (U+00A8, U+00AF, U+02C6–U+02DD)
518+ # Each is a reference to the corresponding combining mark, repositioned so its
519+ # bottom sits just above the x-height and given an advance width.
520+ # ---------------------------------------------------------------------------
521+
522+ _sm_y = int (round (font ['a' ].boundingBox ()[3 ] + 20 ))
523+
524+ _make_spacing_modifier (font , 0x00A8 , '_diaeresis_mark' , _sm_y ) # ¨ spacing diaeresis
525+ _make_spacing_modifier (font , 0x00AF , '_macron_mark' , _sm_y ) # ¯ spacing macron
526+ _make_spacing_modifier (font , 0x02C6 , '_circumflex_mark' , _sm_y ) # ˆ modifier letter circumflex accent
527+ _make_spacing_modifier (font , 0x02C7 , '_caron_mark' , _sm_y ) # ˇ modifier letter caron
528+ _make_spacing_modifier (font , 0x02D8 , '_breve_mark' , _sm_y ) # ˘ modifier letter breve
529+ _make_spacing_modifier (font , 0x02D9 , '_dot_above_mark' , _sm_y ) # ˙ modifier letter dot above
530+ _make_spacing_modifier (font , 0x02DC , '_tilde_mark' , _sm_y ) # ˜ modifier letter small tilde
531+ _make_spacing_modifier (font , 0x02DD , '_double_acute_mark' , _sm_y ) # ˝ modifier letter double acute accent
532+
533+
470534# ---------------------------------------------------------------------------
471535# Accented character tables
472536# ---------------------------------------------------------------------------
@@ -1147,6 +1211,93 @@ def _greek_lc_to_uc(font, lc_cp, uc_cp, snap=True, weight_delta=0):
11471211_g .width = font [0x25BD ].width
11481212
11491213
1214+ # ---------------------------------------------------------------------------
1215+ # Additional composites
1216+ # ---------------------------------------------------------------------------
1217+
1218+ # U with diaeresis + tone mark: Ǖ Ǘ Ǚ Ǜ / ǖ ǘ ǚ ǜ (Pinyin)
1219+ # Ü / ü are used as bases so the new mark floats above the existing dots.
1220+ _Udi = font [0x00DC ].glyphname # Ü hand-drawn
1221+ _udi = font [0x00FC ].glyphname # ü derived
1222+ _accented (0x01D5 , _Udi , '_macron_mark' ) # Ǖ
1223+ _accented (0x01D6 , _udi , '_macron_mark' , gap = 8 ) # ǖ
1224+ _accented (0x01D7 , _Udi , '_acute_mark' ) # Ǘ
1225+ _accented (0x01D8 , _udi , '_acute_mark' , gap = 8 ) # ǘ
1226+ _accented (0x01D9 , _Udi , '_caron_mark' ) # Ǚ
1227+ _accented (0x01DA , _udi , '_caron_mark' , gap = 8 ) # ǚ
1228+ _accented (0x01DB , _Udi , '_grave_mark' ) # Ǜ
1229+ _accented (0x01DC , _udi , '_grave_mark' , gap = 8 ) # ǜ
1230+
1231+ # A/a with diaeresis + macron: Ǟ ǟ (Livonian, Skolt Sámi)
1232+ # gap=30/20: the base's bb top already includes the diaeresis dots; extra
1233+ # clearance prevents the macron from pressing against the dots.
1234+ _Adi = font [0x00C4 ].glyphname # Ä
1235+ _adi = font [0x00E4 ].glyphname # ä
1236+ _accented (0x01DE , _Adi , '_macron_mark' , gap = 30 ) # Ǟ
1237+ _accented (0x01DF , _adi , '_macron_mark' , gap = 20 ) # ǟ
1238+
1239+ # AE/ae + macron: Ǣ ǣ (Old Norse transliteration)
1240+ # x_adj shifts the macron rightward: Æ/æ's geometric centre sits left of the
1241+ # visual centre (E side is heavier than A side).
1242+ _Ae_name = font [0x00C6 ].glyphname # Æ
1243+ _ae_name = font [0x00E6 ].glyphname # æ
1244+ _accented (0x01E2 , _Ae_name , '_macron_mark' , x_adj = 100 ) # Ǣ
1245+ _accented (0x01E3 , _ae_name , '_macron_mark' , gap = 25 , x_adj = 50 ) # ǣ
1246+
1247+ # G/g + caron: Ǧ ǧ (Skolt Sámi, some transliteration systems)
1248+ # x_adj for ǧ: g's full bounding box includes a wide descender that pulls
1249+ # its geometric centre left of the bowl centre.
1250+ _accented (0x01E6 , 'G' , '_caron_mark' ) # Ǧ
1251+ _accented (0x01E7 , 'g' , '_caron_mark' , gap = 8 , x_adj = 80 ) # ǧ
1252+
1253+ # K/k + caron: Ǩ ǩ (Skolt Sámi)
1254+ _accented (0x01E8 , 'K' , '_caron_mark' ) # Ǩ
1255+ _accented (0x01E9 , 'k' , '_caron_mark' , gap = 8 ) # ǩ
1256+
1257+ # G/g + acute: Ǵ ǵ (Upper Sorbian)
1258+ # x_adj for ǵ: same descender-bowl offset as ǧ.
1259+ _accented (0x01F4 , 'G' , '_acute_mark' ) # Ǵ
1260+ _accented (0x01F5 , 'g' , '_acute_mark' , gap = 20 , x_adj = 80 ) # ǵ
1261+
1262+ # A-ring + acute: Ǻ ǻ (Northern Sámi)
1263+ # The acute floats above the existing ring; base bb already includes the ring.
1264+ _Aring_name = font [0x00C5 ].glyphname # Å
1265+ _aring_name = font [0x00E5 ].glyphname # å
1266+ _accented (0x01FA , _Aring_name , '_acute_mark' ) # Ǻ
1267+ _accented (0x01FB , _aring_name , '_acute_mark' , gap = 8 ) # ǻ
1268+
1269+ # AE/ae + acute: Ǽ ǽ (Faroese, Danish orthography)
1270+ _accented (0x01FC , _Ae_name , '_acute_mark' ) # Ǽ
1271+ _accented (0x01FD , _ae_name , '_acute_mark' , gap = 8 ) # ǽ
1272+
1273+ # O-stroke + acute: Ǿ ǿ (Faroese, Danish orthography)
1274+ # Ø/ø are derived earlier in this script; their bounding boxes include the stroke.
1275+ _Ost_name = font [0x00D8 ].glyphname # Ø
1276+ _ost_name = font [0x00F8 ].glyphname # ø
1277+ _accented (0x01FE , _Ost_name , '_acute_mark' ) # Ǿ
1278+ _accented (0x01FF , _ost_name , '_acute_mark' , gap = 8 ) # ǿ
1279+
1280+ # DZ/dz digraph ligatures: DZ Dz dz and DŽ/Dž/dž: DŽ Dž dž
1281+ # Letters are placed ink-edge to ink-edge with a small gap, like IJ above.
1282+ _Zcaron_name = font [0x017D ].glyphname # Ž
1283+ _zcaron_name = font [0x017E ].glyphname # ž
1284+ _dz_gap = 20
1285+ for _cp , _left , _right in [
1286+ (0x01F1 , 'D' , 'Z' ), # DZ capital DZ
1287+ (0x01F2 , 'D' , 'z' ), # Dz titlecase Dz
1288+ (0x01F3 , 'd' , 'z' ), # dz lowercase dz
1289+ (0x01C4 , 'D' , _Zcaron_name ), # DŽ capital DŽ
1290+ (0x01C5 , 'D' , _zcaron_name ), # Dž titlecase Dž
1291+ (0x01C6 , 'd' , _zcaron_name ), # dž lowercase dž
1292+ ]:
1293+ _g = font .createMappedChar (_cp )
1294+ _g .clear ()
1295+ _g .addReference (_left )
1296+ _dx = int (round (font [_left ].boundingBox ()[2 ] + _dz_gap - font [_right ].boundingBox ()[0 ]))
1297+ _g .addReference (_right , psMat .translate (_dx , 0 ))
1298+ _g .width = _dx + font [_right ].width
1299+
1300+
11501301# ---------------------------------------------------------------------------
11511302# Save
11521303# ---------------------------------------------------------------------------
0 commit comments