6
\documentclass{article}

\usepackage{fontspec,filecontents}

\begin{filecontents*}{junicode.fea}
languagesystem DFLT dflt;
languagesystem latn dflt;

feature calt {
    sub \beta by \uni03D0;
} calt;
\end{filecontents*}

\setmainfont[FeatureFile={junicode.fea}]{junicode}

\begin{document}
βιβλίον\par
\addfontfeature{Contextuals=Alternate}
βιβλίον
\end{document}

produces

enter image description here

Is it possible to change the substitution so that the slim beta is used only inside words, keeping the ordinary one at their beginning? i.e., so that we get the following?

enter image description here

10

This is possible with a contextual feature. The class @greekletter defines all Greek letters (the example still needs to be completed), then a beta after a Greek letter is replaced by its slim form:

\documentclass{article}

\usepackage{fontspec,filecontents}

\begin{filecontents*}{junicode.fea}
languagesystem DFLT dflt;
languagesystem latn dflt;

# The list of Greek letters is now almost complete (some ligatures missing).
@greekletter = [
    Alpha Alphatonos Beta Chi Delta Epsilon Epsilontonos Eta Etatonos
    Gamma Iota Iotadieresis Iotatonos Kappa Lambda Mu Nu Omega
    Omegatonos Omicron Omicrontonos Phi Pi Psi Rho Sigma Tau Theta
    Upsilon Upsilondieresis Upsilontonos Xi Zeta alpha alphatonos beta
    chi delta epsilon epsilontonos eta etatonos gamma iota iotadieresis
    iotadieresistonos iotatonos kappa lambda mu nu omega omega1
    omegatonos omicron omicrontonos phi phi1 pi psi rho sigma sigma1 tau
    theta theta1 uni03D0 uni03D7-uni03D9 uni03DA-uni03DD uni03E0 uni03E1
    uni03F0 uni03F1 uni1F00-uni1F09 uni1F0A-uni1F0F uni1F10-uni1F15
    uni1F18 uni1F19 uni1F1A-uni1F1D uni1F20-uni1F29 uni1F2A-uni1F2F
    uni1F30-uni1F39 uni1F3A-uni1F3F uni1F40-uni1F45 uni1F48 uni1F49
    uni1F4A-uni1F4D uni1F50-uni1F57 uni1F59 uni1F5B uni1F5D uni1F5F
    uni1F60-uni1F69 uni1F6A-uni1F6F uni1F70-uni1F79 uni1F7A-uni1F7D
    uni1F80-uni1F89 uni1F8A-uni1F8F uni1F90-uni1F99 uni1F9A-uni1F9F
    uni1FA0-uni1FA9 uni1FAA-uni1FAF uni1FB0-uni1FB4 uni1FB6-uni1FB9
    uni1FBA-uni1FBC uni1FBE uni1FC2-uni1FC4 uni1FC6-uni1FC9
    uni1FCA-uni1FCC uni1FD0-uni1FD3 uni1FD6-uni1FD9 uni1FDA uni1FDB
    uni1FE0-uni1FE9 uni1FEA-uni1FEC uni1FF2-uni1FF4 uni1FF6-uni1FF9
    uni1FFA-uni1FFC upsilon upsilondieresis upsilondieresistonos
    upsilontonos xi zeta
];
feature calt {
    sub @greekletter beta' by uni03D0;
} calt;
\end{filecontents*}

\setmainfont[FeatureFile={junicode.fea}]{junicode}

\begin{document}
βιβλίον\par
\addfontfeature{Contextuals=Alternate}
βιβλίον\par
βιβββλίον\par
Αβ
\end{document}

Result

The list of Greek letters of the font Junicode.ttf can be generated programmatically:

  • Unicode defines two blocks for Greek letters (Blocks.txt:

    0370..03FF; Greek and Coptic
    1F00..1FFF; Greek Extended
    
  • Also Unicode defines categories for the Code points, an uppercase and lowercase letter, e.g.: Ω has Lu, an uppercase letter. We need only the first letter for the classification as letter.

  • Both the glyph names and their Unicode code points can be accessed by FontForge, which can be scripted by its Python bindings.

The following Python script first gathers the Greek letters from the Unicode standard. Then it calls FontForge and try to find the glyph names for these letter.

The sorted glyph names are formatted and printed for inclusion in the feature file. The formatting is optimized by collapsing consecutive glyph names to ranges according to the rules for feature files (see section "2.g.i Ranges" of the OpenType Feature File Specification).

#!/usr/bin/env python2
import fontforge
import re
import subprocess
import unicodedata

# Unicode ranges for Greek letters
greek_unicode_range = range(0x370, 0x3ff) + range(0x1f00, 0x1fff)

# Filter the unicode ranges for letters
greek_unicode_letters = []
for slot in greek_unicode_range:
    category = unicodedata.category(unichr(slot))
    if category[0] == 'L':
        greek_unicode_letters.append(slot)

# Get font file
font_name = subprocess.check_output(['kpsewhich', 'Junicode.ttf']).rstrip()
# print('Font: ' + font_name)

# Load font in fontforge
font = fontforge.open(font_name)

# Get all glyphs from the font
glyphs = [glyph
          for glyph in font.selection.byGlyphs.all()
          if glyph.isWorthOutputting]

glyph_names = [glyph.glyphname
               for glyph in glyphs
               if glyph.unicode in greek_unicode_letters]

# Unformatted output:
# glyph_names.sort()
# print(' '.join(glyph_names))

# Formatted output with ranges

def is_next_in_line(a, b):
    test_string = a + '-' + b
    match = is_next_in_line.letter_pattern.match(test_string)
    if match:
        a_num = ord(match.group(2))
        b_num = ord(match.group(4))
        signature = match.group(1) + '-' + match.group(3)
        if a_num + 1 == b_num:
            return (True, signature)
    match = is_next_in_line.number_pattern.match(test_string)
    if match:
        a_num = int(match.group(2))
        b_num = int(match.group(4))
        signature = match.group(1) + '-' + match.group(3)
        if a_num + 1 == b_num:
            return (True, signature)
    return (False, None)
is_next_in_line.letter_pattern = re.compile(
    r'(.*)([A-Za-z])(.*)-\1([A-Za-z])\3$')
is_next_in_line.number_pattern = re.compile(
    r'(.*)([0-9]{1,3})(.*)-\1([0-9]{1,3})\3$')

def make_range(names):
    if len(names) <= 2:
        return names
    return [names[0] + '-' + names[-1]]

def compact_names_with_same_length(names):
    result = []
    tmp = []
    tmp_signature = None
    for name in names:
        if len(tmp) == 0:
            tmp.append(name)
            continue
        (is_consecutive, signature) = is_next_in_line(tmp[-1], name)
        if is_consecutive:
            if tmp_signature is None:
                tmp_signature = signature
                tmp.append(name)
                continue
            if tmp_signature == signature:
                tmp.append(name)
                continue
        result += make_range(tmp)
        tmp = [name]
        tmp_signature = None
    result += make_range(tmp)
    return result

def compact_names(names):
    """Replaces three and more consecutive elements by ranges."""
    names.sort()
    result = []
    current_length = 0
    tmp = []
    for name in names:
        if len(tmp) == 0:
            tmp.append(name)
            current_length = len(name)
            continue
        length = len(name)
        if length == current_length:
            tmp.append(name)
            continue
        result += compact_names_with_same_length(tmp)
        tmp = [name]
        current_length = length
    result += compact_names_with_same_length(tmp)
    return result

names = compact_names(glyph_names)
lines = []
line = ''
max_length = 68
for name in names:
    if len(line) == 0:
        line = name
        continue
    if len(line) + 1 + len(name) <= max_length:
        line += ' ' + name
        continue
    lines.append(line)
    line = name
lines.append(line)

print('@greekletter = [')
for line in lines:
    print('    ' + line)
print('];')

Result:

@greekletter = [
    Alpha Alphatonos Beta Chi Delta Epsilon Epsilontonos Eta Etatonos
    Gamma Iota Iotadieresis Iotatonos Kappa Lambda Mu Nu Omega
    Omegatonos Omicron Omicrontonos Phi Pi Psi Rho Sigma Tau Theta
    Upsilon Upsilondieresis Upsilontonos Xi Zeta alpha alphatonos beta
    chi delta epsilon epsilontonos eta etatonos gamma iota iotadieresis
    iotadieresistonos iotatonos kappa lambda mu nu omega omega1
    omegatonos omicron omicrontonos phi phi1 pi psi rho sigma sigma1 tau
    theta theta1 uni03D0 uni03D7-uni03D9 uni03DA-uni03DD uni03E0 uni03E1
    uni03F0 uni03F1 uni1F00-uni1F09 uni1F0A-uni1F0F uni1F10-uni1F15
    uni1F18 uni1F19 uni1F1A-uni1F1D uni1F20-uni1F29 uni1F2A-uni1F2F
    uni1F30-uni1F39 uni1F3A-uni1F3F uni1F40-uni1F45 uni1F48 uni1F49
    uni1F4A-uni1F4D uni1F50-uni1F57 uni1F59 uni1F5B uni1F5D uni1F5F
    uni1F60-uni1F69 uni1F6A-uni1F6F uni1F70-uni1F79 uni1F7A-uni1F7D
    uni1F80-uni1F89 uni1F8A-uni1F8F uni1F90-uni1F99 uni1F9A-uni1F9F
    uni1FA0-uni1FA9 uni1FAA-uni1FAF uni1FB0-uni1FB4 uni1FB6-uni1FB9
    uni1FBA-uni1FBC uni1FBE uni1FC2-uni1FC4 uni1FC6-uni1FC9
    uni1FCA-uni1FCC uni1FD0-uni1FD3 uni1FD6-uni1FD9 uni1FDA uni1FDB
    uni1FE0-uni1FE9 uni1FEA-uni1FEC uni1FF2-uni1FF4 uni1FF6-uni1FF9
    uni1FFA-uni1FFC upsilon upsilondieresis upsilondieresistonos
    upsilontonos xi zeta
];
|improve this answer|||||
  • Thank you! But we should also take into account Greek letters with diacritical marks. Do you know if it is possible to give a set of unicode ranges instead of individual letters? In this case, they should be 0x0370--0x03ff and 0x1f00--0x1fff. (Alternatively, if it is possible to test if a letter is at the beginning of a word, then perhaps we could replace the broad with the slim globally, and then rereplace slim ones at the beginning of words?) – Toothrot Mar 31 '16 at 21:10
  • @Lawrence The Greek glyph names are not alphabetically sorted, therefore ranges will not work. – Heiko Oberdiek Mar 31 '16 at 21:17
  • @Lawrence AFAIK, there is no such thing like a word boundary. This is indirectly defined by the letter class. – Heiko Oberdiek Mar 31 '16 at 21:29
  • I don't quite understand, but---will not every initial beta be preceded by a space or---something else at the beginning of a line or paragraph (or nothing, would that be undetectable?)? – Toothrot Mar 31 '16 at 21:40
  • 3
    @Lawrence Glyph names: otfinfo -g Junicode.ttf – Heiko Oberdiek Mar 31 '16 at 22:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.