diff --git a/INSTALLATION.md b/INSTALLATION.md index 450d115..4113a62 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -43,23 +43,25 @@ The package will automatically install the following dependencies: After installation, the following commands will be available: -### Core Proofing Tools -- `text-proof` - Create example paragraphs for character sets -- `alphabet-proof` - Create alphabet proofs for different writing systems -- `charset-proof` - Generate character set proofs -- `context-proof` - Create contextual proofs for specific characters -- `reference-proof` - Compare multiple fonts side by side - -### Advanced Tools -- `glyph-proof` - Generate various glyph proofs -- `glyphset-proof` - Create comprehensive glyphset proofs -- `unicode-chart-proof` - Generate Unicode character charts -- `vertical-metrics-proof` - Analyze vertical metrics -- `vertical-metrics-comparison-proof` - Compare vertical metrics across fonts -- `waterfall-proof` - Create waterfall proofs -- `accent-proof` - Generate accent proofs -- `figure-spacing-proof` - Create figure spacing proofs -- `overlay-font-proof` - Create overlay comparisons between fonts +### Proofing Tools accepting fonts and UFOs: +- `glyph-proof` - compare glyphs +- `glyphset-proof` - the whole glyphset on one page +- `figure-spacing-proof` - compare figure spacing proofs +- `vertical-metrics-comparison-proof` - compare vertical metrics across fonts + +### Proofing Tools accepting fonts: +- `accent-proof` - check accents and their use accent proofs +- `alphabet-proof` - various basic proofs for different writing systems +- `charset-proof` - check for a given charset on one page +- `context-proof` - see characters in context +- `text-proof` - pages with example paragraphs +- `unicode-chart-proof` - generate Unicode character charts +- `vertical-metrics-proof` - visualize vertical metrics +- `waterfall-proof` - create various waterfalls + +### Other Proofing Tools: +- `overlay-font-proof` - overlay two fonts +- `reference-proof` - compare multiple fonts side by side ## Quick Start diff --git a/README.md b/README.md index e52ad46..49f0911 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,9 @@ You’re now ready to start proofing! ### `accent-proof` -Proof of all accents with a representation of all glyphs using that accent, -and example words for each accent (both upper- and lowercase). -Additionally, words with “merged” non-abc glyphs (such as æðøß) will be shown. - -This script is currently limited to AL-3, an extension to AL-4 and beyond is -thinkable. +Proof of all Latin accents supported by a given font, with example words for +each accent (both upper- and lowercase). Additionally, words with “atomic” +Latin base glyphs (such as æðøß) will be shown. Input: * font file(s), or folder(s) containing font files @@ -48,7 +45,11 @@ Creates example pages for: - some sample words Modes (`proof`, `spacing`, `sample`) can be chosen individually, or all at once -(`all`). Writing systems supported are `lat`, `grk`, `cyr`, and `figures`. +(`all`). + +Writing systems supported are `lat`, `grk`, `cyr`, and `figures`. By default, +supported writing systems are automatically chosen on a per-font basis. + Kerning can be toggled off (`-k`). Optionally, a sample string (`-s`), or an input text file file (`-t`) can be diff --git a/drawbot_proofing/__init__.py b/drawbot_proofing/__init__.py index 18a192f..9b34438 100644 --- a/drawbot_proofing/__init__.py +++ b/drawbot_proofing/__init__.py @@ -4,7 +4,7 @@ A collection of font proofing tools using DrawBot for type designers and developers. """ -__version__ = "1.0.0" +__version__ = "1.0.3" __author__ = "Adobe" __email__ = "opensource@adobe.com" diff --git a/drawbot_proofing/accentProof.py b/drawbot_proofing/accentProof.py index 37050c6..e83d24d 100644 --- a/drawbot_proofing/accentProof.py +++ b/drawbot_proofing/accentProof.py @@ -6,12 +6,9 @@ # it. ''' -Proof of all accents with a representation of all glyphs using that accent, -and example words for each accent (both upper- and lowercase). -Additionally, words with “merged” non-abc glyphs (such as æðøß) will be shown. - -This script is currently limited to AL-3, an extension to AL-4 and beyond is -thinkable. +Proof of all Latin accents supported by a given font, with example words for +each accent (both upper- and lowercase). Additionally, words with “atomic” +Latin base glyphs (such as æðøß) will be shown. Input: * font file(s), or folder(s) containing font files @@ -23,60 +20,26 @@ import subprocess import random import re +import unicodedata +from fontTools.ttLib import TTFont from pathlib import Path + from .proofing_helpers import fontSorter from .proofing_helpers.formatter import RawDescriptionAndDefaultsFormatter -from .proofing_helpers.stamps import timestamp from .proofing_helpers.files import get_font_paths, chain_charset_texts -from .proofing_helpers.globals import * - -DOC_SIZE = 'Letter' -PT_SIZE = 20 -MARGIN = PT_SIZE - -AL3_CMB_ACCENTS = { - # combining accents and letters they’re used in - 0x0300: 'ÀÈÌÒÙàèìòù', - 0x0301: 'ÁÉÍÓÚÝáéíóúýĆćĹĺŃńŔŕŚśŹź', - 0x0302: 'ÂÊÎÔÛâêîôû', - 0x0303: 'ÃÑÕãñõ', - 0x0304: 'ĀāĒēĪīŌōŪū', - 0x0306: 'ĂăĞğ', - 0x0307: 'ĖėĠġİŻżṄṅ', - 0x0308: 'ÄËÏÖÜäëïöüÿŸ', - 0x030A: 'ÅåŮů', - 0x030B: 'ŐőŰű', - 0x030C: 'ČčĎďĚ켾ŇňŘřŠšŤťŽž', - 0x0326: 'ȘșȚț', - 0x0327: 'ÇçĶķŖŗŞşŢţ', - 0x0328: 'ĄąĘęĮįŲų', -} -AL4_CMB_ACCENTS = { - # not used for this script - 0x0323: 'ḌḍḤḥḶḷṂṃṆṇṚṛṢṣṬṭẒẓẠạẸẹỊịỌọỢợỤụỰựỴỵ', - 0x0309: 'ẢảẨẩẲẳẺẻỂểỈỉỎỏỔổỞởỦủỬửỶỷ', - 0x031B: 'ƠơƯư', - 0x0331: 'ḎḏḺḻṈṉṞṟṮṯ', - 0x032E: 'Ḫḫ', -} -AL3_MERGED = { - # merged non-abc characters in AL3 - 0x0131: 'ı', - 0x00DF: 'ß', - 0x00E6: 'æ', - 0x00F0: 'ð', - 0x00F8: 'ø', - 0x00FE: 'þ', - 0x0111: 'đ', - 0x0142: 'ł', - 0x0153: 'œ', -} -LC_ONLY = [ - 0x017F, # longs - 0x00DF, # germandbls - 0x0131, # dotlessi -] +from .proofing_helpers.names import get_ps_name +from .proofing_helpers.stamps import timestamp +from .proofing_helpers.globals import FONT_MONO, ADOBE_BLANK + + +def get_supported_chars(font): + ''' + characters supported by a font + ''' + cmap = TTFont(font)['cmap'].getBestCmap() + mapped_chars = set([chr(c) for c in cmap.keys()]) + return mapped_chars def filter_content(filter_chars, content): @@ -86,130 +49,242 @@ def filter_content(filter_chars, content): return re.sub(f'[{re.escape(filter_chars)}]', '', content) -def collect_al3_words(): - raw_content = chain_charset_texts('AL', 3) +def collect_words(): + raw_content = chain_charset_texts('AL', 5) filtered_content = filter_content( '*,.;:(){{}}[]¹²³⁴⁵"¿¡!?/\'“”„-–—<>+=', raw_content) - al3_words = filtered_content.split() - return al3_words - - -def make_longs_wordlist(wordlist): - ''' - It is rare for an ſ to occur in the wild, so every word with s is - converted to be a word with ſ. - - ſ can never occur at the end of a word. - ''' - longs_words = [] - for word in wordlist: - if len(word) >= 3 and 's' in word[:-1]: - longs_word = word[:-1].replace('s', 'ſ') + word[-1] - longs_words.append(longs_word) - return longs_words + words = filtered_content.split() + return words def make_pages(content, my_font): - db.newPage(DOC_SIZE) + db.newPage('Letter') + ps_name = get_ps_name(my_font) + + pt_size = 20 + margin = pt_size + text_area = ( + 4 * margin, 3 * margin, + db.width() - 6 * margin, db.height() - 5 * margin) - fs = db.FormattedString( + content = db.FormattedString( content, font=my_font, - fontSize=PT_SIZE, + fontSize=pt_size, fallbackFont=ADOBE_BLANK, - openTypeFeatures=dict(liga=True), - ) + openTypeFeatures=dict(liga=True),) - fs_time = db.FormattedString( - timestamp(readable=True), + caption = db.FormattedString( + f'{ps_name} | {timestamp(readable=True)}', font=FONT_MONO, - fontSize=6, - ) - overflow = db.textBox( - fs, ( - 4 * MARGIN, 3 * MARGIN, - db.width() - 6 * MARGIN, db.height() - 5 * MARGIN) - ) + fontSize=6,) + + overflow = db.textBox(content, text_area) + db.textBox( - fs_time, - (4 * MARGIN, 0, db.width(), 1.75 * MARGIN) + caption, + (4 * margin, 0, db.width(), 1.75 * margin) ) if overflow and len(str(overflow).strip()): + # avoid starting a page with a line break + overflow = str(overflow).lstrip('\n') make_pages(overflow, my_font) def make_output_name(font_list): - name = ['accentProof'] + name = ['accent proof'] chunks = [] + folders = sorted(set([font.stem for font in font_list])) - if len(font_list) >= 1: - chunks.append(font_list[0].stem) - if len(font_list) >= 2: - chunks.append(font_list[1].stem) - if len(font_list) >= 3: + if len(folders) >= 1: + chunks.append(folders[0]) + if len(folders) >= 2: + chunks.append(folders[1]) + if len(folders) >= 3: chunks.append('etc') name.append(', '.join(chunks)) return ' '.join(name) -def make_example_chars(codepoint): - if codepoint in AL3_CMB_ACCENTS.keys(): - example_chars = ' '.join(sorted(AL3_CMB_ACCENTS.get(codepoint))) - elif codepoint in LC_ONLY: - example_chars = chr(codepoint) +def get_example_chars(cp, accent_dict, supported_chars): + + if cp in accent_dict.keys(): + example_chars = accent_dict.get(cp) + supported = set(example_chars) & set(supported_chars) + return ' '.join(sorted(supported)) + else: - example_chars = chr(codepoint).upper() + ' ' + chr(codepoint) - return example_chars + if chr(cp).upper() == chr(cp): + # does not have an uppercase variant + example_chars = [chr(cp)] + elif chr(cp) == 'ſ': + example_chars = [chr(cp)] + else: + example_chars = [chr(cp).upper(), chr(cp)] + supported = set(example_chars) & set(supported_chars) + + if supported: + return ' '.join(sorted(supported)) -def make_example_words(codepoint, words_lc, num_words=10, randomize=True): +def get_example_words(cp, words, supported_chars, length=10, randomize=True): + if randomize: - random.shuffle(words_lc) - words_lc = words_lc[:num_words] - words_uc = [word.upper() for word in words_lc] - - if codepoint in LC_ONLY: - example_words = words_lc - elif codepoint == 0x030C: # combining caron - # make sure that both forms of the caron are shown - example_words = ( - words_uc + ['neďeľné šťastný'] + words_lc[:num_words - 2]) - else: - example_words = words_uc + words_lc + random.shuffle(words) + words = words[:length] + words_uc = [word.upper() for word in words] + + if unicodedata.category(chr(cp)) == 'Mn': # combining marks + if cp == 0x030C: # combining caron + # make sure that both forms of the caron are shown + example_words = ( + words_uc + ['neďeľné šťastný'] + words) + else: + example_words = words_uc + words + + else: # atomic latin + if chr(cp).upper() == chr(cp): + # no uppercase available + example_words = words + elif len(chr(cp).upper()) > 1: + # uppercase splits into 2 (ß → SS) + example_words = words + elif cp == ord('ſ'): + # long s: + example_words = [ + # ſ can never occur at the end of a word. + word.replace('s', 'ſ') for word in words if not + word.endswith('s')] + else: + example_words = words_uc + words - return " ".join(example_words) + supported_words = [ + word for word in example_words if set(word) < set(supported_chars)] + return " ".join(supported_words) -def make_content_list(input_word_dict): +def make_content_list(font, words_for_cp, accents_dict): content_list = [] - for codepoint, word_list in sorted(input_word_dict.items()): - content_list.append( - f'{make_example_chars(codepoint)} – ' - f'{make_example_words(codepoint, word_list)}\n') + supported_chars = get_supported_chars(font) + + for cp, words in sorted(words_for_cp.items()): + example_chars = get_example_chars(cp, accents_dict, supported_chars) + example_words = get_example_words(cp, words, supported_chars) + if example_chars and example_words: + content_list.append(f'{example_chars} – {example_words}\n') return content_list -def find_words_containing(input_dict, word_list): +def find_words_containing(input_dict, words): ''' From a list of words, find all words containing a character. If the character is a combining mark, find all words with accented glyphs that could be composed using that combining mark. ''' output = {} - for codepoint, input_chars in input_dict.items(): + for cp, input_chars in input_dict.items(): if len(input_chars) == 1: # single (merged) character - regex = re.compile(rf'(\S*?({input_chars})\S*?)') + input_char = input_chars[0] + if input_char == 'ſ': + input_char = 's' + regex = re.compile(rf'(\S*?({input_char})\S*?)') else: # list of accented glyphs regex = re.compile(rf'(\S*?({"|".join(input_chars)})\S*?)') - words_filtered = filter(regex.match, word_list) + words_filtered = filter(regex.match, words) words_lower = sorted(set([word.lower() for word in words_filtered])) - output[codepoint] = words_lower + if len(words_lower) > 1: + output[cp] = words_lower return output +def get_cmb_accents_dict(report=False): + ''' + Create a dictionary of combining accents and their use, i.e. + + # COMBINING GRAVE ACCENT + 0x0300: 'ÀÈÌÒÙàèìòùǸǹẀẁỲỳ', + + # COMBINING ACUTE ACCENT + 0x0301: 'ÁÉÍÓÚÝáéíóúýĆćĹĺŃńŔŕŚśŹźǴǵǼǽǾǿḰḱḾḿṔṕẂẃ', + + # COMBINING CIRCUMFLEX ACCENT + 0x0302: 'ÂÊÎÔÛâêîôûĈĉĜĝĤĥĴĵŜŝŴŵŶŷẐẑ', + + Limited to those Latin base glyphs which cannot themselves be decomposed. + ''' + + accents_to_examples = {} + + # characters in the BMP which have decomposition + decomposing = [ + cp for cp in range(0xFFFF + 1) if unicodedata.decomposition(chr(cp))] + + for cp in decomposing: + decomposition = unicodedata.decomposition(chr(cp)) + # make sure the decomposition consists of 2 code points + if re.match(r'[0-9A-F]{4} [0-9A-F]{4}', decomposition): + hex_base, hex_accent = decomposition.split() + cp_base = int(hex_base, 16) + cp_accent = int(hex_accent, 16) + # we are focusing on Latin base glyphs, and single-level accents. + if ( + 'LATIN' in unicodedata.name(chr(cp_base)) and + cp_base not in decomposing + ): + accents_to_examples.setdefault(cp_accent, []).append(chr(cp)) + + if report: + for cp_accent, char_list in accents_to_examples.items(): + print(f'# {unicodedata.name(chr(cp_accent))}') + print(f'0x{cp_accent:04X}: \'{"".join(char_list)}\',') + print() + + return accents_to_examples + + +def get_atomic_latin(start=0): + + atomic_latin_basic = [ + cp for cp in range(start, 0xFFFF + 1) if not + unicodedata.decomposition(chr(cp)) and + 'LATIN' in unicodedata.name(chr(cp), '')] + + atomic_latin_outliers = [ + # characters with compatibility decomposition, such as ſ + cp for cp in range(start, 0xFFFF + 1) if + '' in unicodedata.decomposition(chr(cp)) and + 'LATIN' in unicodedata.name(chr(cp), '') and + 'LETTER' in unicodedata.name(chr(cp), '') and + unicodedata.category(chr(cp)) in ['Ll', 'Lu'] + ] + + atomic_latin = sorted(atomic_latin_basic + atomic_latin_outliers) + + atomic_latin_lc = list(filter( + lambda cp: unicodedata.category(chr(cp)) == 'Ll', atomic_latin)) + + atomic_latin_uc = list(filter( + lambda cp: unicodedata.category(chr(cp)) == 'Lu', atomic_latin)) + + atomic_latin_other = list(filter( + lambda cp: unicodedata.category(chr(cp)) == 'Lo', atomic_latin)) + + atomic = list(atomic_latin_lc) + + for cp in atomic_latin_uc: + uc_char = chr(cp) + cp_lower = ord(uc_char.lower()) + if cp_lower not in atomic_latin_lc: + # only-uppercase letters, of which there don’t seem to be any + atomic.append(cp) + + atomic += atomic_latin_other + return sorted(atomic) + + def get_options(args=None): parser = argparse.ArgumentParser( description=__doc__, @@ -232,6 +307,10 @@ def get_options(args=None): def main(test_args=None): args = get_options(test_args) + accents_dict = get_cmb_accents_dict() + atomic = get_atomic_latin(start=ord('ß')) + atomic_dict = {cp: chr(cp) for cp in atomic} + font_list = [] for item in args.input: # could be individual fonts or folder of fonts. @@ -241,16 +320,15 @@ def main(test_args=None): fontSorter.sort_fonts(get_font_paths(ip), alternate_italics=True)) if font_list: - al3_words = collect_al3_words() - accent_words = find_words_containing(AL3_CMB_ACCENTS, al3_words) - precomp_words = find_words_containing(AL3_MERGED, al3_words) - - content = '\n'.join( - make_content_list(accent_words) + - make_content_list(precomp_words)) - + words = collect_words() + accent_words = find_words_containing(accents_dict, words) + atomic_words = find_words_containing(atomic_dict, words) db.newDrawing() + for font_path in font_list: + content = '\n'.join( + make_content_list(font_path, accent_words, accents_dict) + + make_content_list(font_path, atomic_words, accents_dict)) make_pages(content, font_path) output_name = make_output_name(font_list) diff --git a/drawbot_proofing/alphabetProof.py b/drawbot_proofing/alphabetProof.py index af2cb14..abaf83c 100644 --- a/drawbot_proofing/alphabetProof.py +++ b/drawbot_proofing/alphabetProof.py @@ -13,7 +13,11 @@ - some sample words Modes (`proof`, `spacing`, `sample`) can be chosen individually, or all at once -(`all`). Writing systems supported are `lat`, `grk`, `cyr`, and `figures`. +(`all`). + +Writing systems supported are `lat`, `grk`, `cyr`, and `figures`. By default, +supported writing systems are automatically chosen on a per-font basis. + Kerning can be toggled off (`-k`). Optionally, a sample string (`-s`), or an input text file file (`-t`) can be @@ -30,9 +34,14 @@ import subprocess import drawBot as db + +from fontTools.ttLib import TTFont from pathlib import Path -from .proofing_helpers.files import get_font_paths, make_temp_font +from .proofing_helpers import fonts as fonts_helper +from .proofing_helpers import fontSorter +from .proofing_helpers.files import get_font_paths +from .proofing_helpers.fonts import make_temp_font, supports_text from .proofing_helpers.formatter import RawDescriptionAndDefaultsFormatter from .proofing_helpers.globals import FONT_MONO, ADOBE_BLANK from .proofing_helpers.names import ( @@ -43,7 +52,7 @@ def get_options(): mode_choices = ['proof', 'spacing', 'sample', 'all'] - ws_choices = ['lat', 'grk', 'cyr', 'figures', 'all'] + ws_choices = ['lat', 'grk', 'cyr', 'figures', 'auto'] parser = argparse.ArgumentParser( description=__doc__, @@ -60,7 +69,7 @@ def get_options(): parser.add_argument( '-w', '--writing_system', action='store', - default='lat', + default='auto', choices=ws_choices, help='writing system') @@ -144,6 +153,28 @@ def read_sample_text(kind='sample', w_system='lat'): print(text_path, 'does not exist') +def get_supported_writing_systems(font_file): + f = TTFont(font_file) + cmap = f['cmap'] + supported = [] + for ws_name in 'lat', 'grk', 'cyr': + if getattr(fonts_helper, f'supports_{ws_name}')(cmap): + supported.append(ws_name) + return supported + + +def get_all_supported_writing_systems(font_files): + ''' + collect all supported writing systems + ''' + all_wss = [] + for ff in font_files: + wss = get_supported_writing_systems(ff) + new_wss = [ws for ws in wss if ws not in all_wss] + all_wss.extend(new_wss) + return all_wss + + def make_proof(args, fonts, output_path): if args.text: @@ -156,92 +187,85 @@ def make_proof(args, fonts, output_path): else: mode = args.mode.lower() custom_string = args.string - writing_system = args.writing_system.lower() - proof_text = make_proof_text(mode, writing_system, custom_string) - db.newDrawing() + if args.writing_system.lower() == 'auto': + writing_systems = get_all_supported_writing_systems(fonts) + else: + writing_systems = [args.writing_system] + + proof_text = make_proof_text(mode, writing_systems, custom_string) MARGIN = 30 line_space = args.point_size * 1.2 + feature_dict = {'kern': not args.kerning_off} - # avoid PS name clash + # avoid PS name clash, and avoid making too many temp fonts tmp_fonts = [make_temp_font(i, font) for (i, font) in enumerate(fonts)] - for page in proof_text: - - feature_dict = {'kern': not args.kerning_off} - - # undocumented feature -- it is possible to add feature tags - # to the input text files. Not sure how useful. - # - # if page.startswith('#'): # features - # page_lines = page.splitlines() - # feature_line = page_lines[0].strip('#').strip() - # feature_dict = { - # feature_name: True for feature_name in feature_line.split()} - # page = '\n'.join(page_lines[1:]) + for page in proof_text: for font_index, font in enumerate(fonts): - tmp_font = tmp_fonts[font_index] - font_path = Path(font) - db.newPage('LetterLandscape') - - fs_stamp = db.FormattedString( - f'{font_path.name}', - font=FONT_MONO, - fontSize=10, - align='right') - if args.kerning_off: - fs_stamp += ' | no kerning' - fs_stamp += f' | {timestamp(readable=True)}' - - db.textBox(fs_stamp, (0, MARGIN, db.width() - MARGIN, 20)) - y_offset = db.height() - MARGIN - args.point_size - for line in page.split('\n'): - - fs = db.FormattedString( - line, - font=tmp_font, - fontSize=args.point_size, - fallbackFont=ADOBE_BLANK, - openTypeFeatures=feature_dict, - ) - db.text(fs, (MARGIN, y_offset)) - - if len(line) == 0: - y_offset -= line_space / 2 - else: - y_offset -= line_space + + # check if more than 50% of the required text per page is supported + if supports_text(font, page, 50): + + tmp_font = tmp_fonts[font_index] + font_path = Path(font) + db.newPage('LetterLandscape') + + caption = db.FormattedString( + f'{font_path.name}', + font=FONT_MONO, + fontSize=10, + align='right') + if args.kerning_off: + caption += ' | no kerning' + caption += f' | {timestamp(readable=True)}' + + db.textBox(caption, (0, MARGIN, db.width() - MARGIN, 20)) + y_offset = db.height() - MARGIN - args.point_size + for line in page.split('\n'): + + fs = db.FormattedString( + line, + font=tmp_font, + fontSize=args.point_size, + fallbackFont=ADOBE_BLANK, + openTypeFeatures=feature_dict, + ) + db.text(fs, (MARGIN, y_offset)) + + if len(line) == 0: + y_offset -= line_space / 2 + else: + y_offset -= line_space db.saveImage(output_path) db.endDrawing() -def make_proof_text(mode, writing_system, custom_string=None): +def make_proof_text(mode, writing_systems, custom_string=None): proof_text = [] + if 'figures' not in writing_systems: + # figures are always proofed, but not twice + writing_systems.append('figures') + if custom_string: proof_text.extend([custom_string]) - proof_text.extend(read_sample_text(mode, 'lat')) - else: - proof_text.extend(read_sample_text(mode, writing_system)) + for ws in writing_systems: + proof_text.extend(read_sample_text(mode, ws)) - if writing_system not in ['all', 'figures']: - # add the figures for good measure - proof_text.extend(read_sample_text(mode, 'figures')) return proof_text def make_pdf_name(args, fonts): ''' - Try to make a sensible filename for the PDF proof created. + Make a sensible filename for the PDF proof created. ''' - all_font_names = [get_ps_name(font) for font in fonts] - family_name = get_name_overlap(all_font_names) - if not family_name: - family_name = get_path_overlap(fonts) + chunks = [] if args.mode == 'proof': proof_name = 'alphabet proof' @@ -249,14 +273,23 @@ def make_pdf_name(args, fonts): proof_name = 'full proof' else: proof_name = f'{args.mode} proof' + chunks.append(proof_name) - pdf_name = f'{proof_name} {family_name} ({args.writing_system}).pdf' + all_font_names = [get_ps_name(font) for font in fonts] + family_name = get_name_overlap(all_font_names) + if not family_name: + family_name = get_path_overlap(fonts) + chunks.append(family_name) + + if args.writing_system != 'auto': + chunks.append(f'({args.writing_system})') + + pdf_name = ' '.join(chunks) + '.pdf' return pdf_name def main(): args = get_options() - fonts = [] for item in args.input: if Path(item).exists(): @@ -265,9 +298,10 @@ def main(): print(f'{item} is not a valid path') if fonts: - output_pdf_name = make_pdf_name(args, fonts) + sorted_fonts = fontSorter.sort_fonts(fonts) + output_pdf_name = make_pdf_name(args, sorted_fonts) output_path = Path(f'~/Desktop/{output_pdf_name}').expanduser() - make_proof(args, fonts, output_path) + make_proof(args, sorted_fonts, output_path) subprocess.call(['open', output_path]) diff --git a/drawbot_proofing/charsetProof.py b/drawbot_proofing/charsetProof.py index 62e866f..8e00d8b 100644 --- a/drawbot_proofing/charsetProof.py +++ b/drawbot_proofing/charsetProof.py @@ -26,7 +26,7 @@ import drawBot as db from pathlib import Path -from .proofing_helpers import fontSorter, charsets +from .proofing_helpers import charsets, fontSorter from .proofing_helpers.files import get_font_paths from .proofing_helpers.formatter import RawDescriptionAndDefaultsFormatter from .proofing_helpers.globals import ADOBE_NOTDEF diff --git a/drawbot_proofing/figureSpacingProof.py b/drawbot_proofing/figureSpacingProof.py index a184fcd..453d575 100644 --- a/drawbot_proofing/figureSpacingProof.py +++ b/drawbot_proofing/figureSpacingProof.py @@ -26,7 +26,7 @@ from pathlib import Path from .proofing_helpers.drawing import draw_glyph -from .proofing_helpers.files import get_ufo_paths, get_font_paths +from .proofing_helpers.files import get_font_paths, get_ufo_paths from .proofing_helpers.formatter import RawDescriptionAndDefaultsFormatter from .proofing_helpers.globals import FONT_MONO from .proofing_helpers.stamps import timestamp diff --git a/drawbot_proofing/glyphProof.py b/drawbot_proofing/glyphProof.py index d315750..31046e0 100644 --- a/drawbot_proofing/glyphProof.py +++ b/drawbot_proofing/glyphProof.py @@ -41,7 +41,7 @@ from .proofing_helpers import fontSorter from .proofing_helpers.drawing import draw_glyph, get_glyph_path -from .proofing_helpers.files import get_ufo_paths, get_font_paths +from .proofing_helpers.files import get_font_paths, get_ufo_paths from .proofing_helpers.formatter import RawDescriptionAndDefaultsFormatter from .proofing_helpers.globals import FONT_MONO from .proofing_helpers.stamps import timestamp @@ -899,19 +899,31 @@ def build_proofing_fonts(input_paths): * if fonts are found, return a list of ttFont objects ''' - if len(input_paths) == 1: - ufo_paths = get_ufo_paths(input_paths[0]) - font_paths = get_font_paths(input_paths[0]) - - if ufo_paths: - input_files = fontSorter.sort_fonts(ufo_paths) - elif font_paths: - input_files = fontSorter.sort_fonts(font_paths) - else: - input_files = [] + ufo_paths = [] + font_paths = [] + + for ip in input_paths: + if Path(ip).exists(): + # for each folder passed, sort the fonts. + # this seems to be the most logical approach, rather than + # sorting everything + ufos_found = get_ufo_paths(ip) + fonts_found = get_font_paths(ip) + + if Path(ip).is_dir(): + # do not sort .designspace input + ufos_found = fontSorter.sort_fonts(ufos_found) + fonts_found = fontSorter.sort_fonts(fonts_found) + + ufo_paths.extend(ufos_found) + font_paths.extend(fonts_found) + + if ufo_paths: + input_files = ufo_paths + elif font_paths: + input_files = font_paths else: - # no sorting, just passing single files - input_files = [Path(p) for p in input_paths] + input_files = [] proofing_fonts = list(map(ProofingFont, input_files)) return proofing_fonts diff --git a/drawbot_proofing/glyphsetProof.py b/drawbot_proofing/glyphsetProof.py index bbd370c..ea43815 100644 --- a/drawbot_proofing/glyphsetProof.py +++ b/drawbot_proofing/glyphsetProof.py @@ -28,7 +28,7 @@ from fontTools.ttLib import TTFont from pathlib import Path from .proofing_helpers.drawing import draw_glyph -from .proofing_helpers.files import get_ufo_paths, get_font_paths +from .proofing_helpers.files import get_font_paths, get_ufo_paths from .proofing_helpers.formatter import RawDescriptionAndDefaultsFormatter from .proofing_helpers.globals import FONT_MONO from .proofing_helpers.names import ( diff --git a/drawbot_proofing/proofing_helpers/files.py b/drawbot_proofing/proofing_helpers/files.py index a2d7144..b65a6b2 100644 --- a/drawbot_proofing/proofing_helpers/files.py +++ b/drawbot_proofing/proofing_helpers/files.py @@ -8,7 +8,6 @@ import os import tempfile -from fontTools import ttLib from fontTools.designspaceLib import DesignSpaceDocument from pathlib import Path @@ -116,26 +115,3 @@ def get_temp_file_path(extension=None): file_descriptor, path = tempfile.mkstemp(suffix=extension) os.close(file_descriptor) return path - - -def make_temp_font(file_index, font_file): - ''' - Make a temporary font file with a unique PS name, so two versions of the - same design can be embedded into the same PDF. - If PS names clash, the implication is that the same font outlines will be - seen throughout the whole document. - ''' - font = ttLib.TTFont(font_file) - file_extension = '.otf' if font.sfntVersion == 'OTTO' else '.ttf' - tmp_font_file = get_temp_file_path(file_extension) - tmp_ps_name = f'{Path(font_file).stem}_{file_index}' - for name_entry in font['name'].names: - if name_entry.nameID == 6: - font['name'].setName( - tmp_ps_name, - nameID=6, - platformID=name_entry.platformID, - platEncID=name_entry.platEncID, - langID=name_entry.langID) - font.save(tmp_font_file) - return(tmp_font_file) diff --git a/drawbot_proofing/proofing_helpers/fontSorter.py b/drawbot_proofing/proofing_helpers/fontSorter.py index cd44c12..855d6eb 100644 --- a/drawbot_proofing/proofing_helpers/fontSorter.py +++ b/drawbot_proofing/proofing_helpers/fontSorter.py @@ -79,7 +79,7 @@ def find_longest_match(attr_list, match_indices): def make_psname_dict(font_files): ''' Dictionary of PS names to font files that have them. - The dict values are lists, because multiple fonts might have the + The dict values are lists, as multiple fonts might have the same PS name. ''' psname_dict = {} @@ -218,23 +218,40 @@ def sort_ps_names(ps_name_list, alternate_italics=False, debug=False): # score_dict = {min(score_list): f for f, score_list in matches.items()} sorted_name_lists = [f for _, f in sorted(matches.items())] - sorted_names = list(chain.from_iterable(sorted_name_lists)) + sorted_ps_names = list(chain.from_iterable(sorted_name_lists)) - return sorted_names + return sorted_ps_names + + +def make_family_dict(psname_dict): + all_families = {} + for ps_name, font_paths in psname_dict.items(): + family_name = ps_name.split('-')[0] + family_dict = all_families.setdefault(family_name, {}) + family_dict[ps_name] = font_paths + return all_families def sort_fonts(font_files, alternate_italics=False, debug=False): if len(font_files) <= 1: return font_files - psname_dict = make_psname_dict(font_files) - sorted_names = sort_ps_names( - psname_dict.keys(), alternate_italics, debug) - sorted_files = [] - for ps_name in sorted_names: - sorted_files.extend(psname_dict.get(ps_name)) + sorted_font_files = [] + + # all ps names to matching font files + full_psname_dict = make_psname_dict(font_files) + + # psname dicts split into family names + family_dict = make_family_dict(full_psname_dict) + + for family_name in sorted(family_dict.keys()): + psname_dict = family_dict.get(family_name) + sorted_ps_names = sort_ps_names( + psname_dict.keys(), alternate_italics, debug) + for ps_name in sorted_ps_names: + sorted_font_files.extend(psname_dict.get(ps_name)) - return sorted_files + return sorted_font_files def get_font_paths(directory): @@ -257,11 +274,12 @@ def get_args(args=None): description='Font Sorting Test') parser.add_argument( - 'input_dir', + 'input', action='store', metavar='FOLDER', + nargs='+', help=( - 'Directory which may contain (in order of preference) ' + 'Folder(s) which may contain (in order of preference) ' 'UFOs, OTFs, or TTFs.')) parser.add_argument( @@ -284,18 +302,20 @@ def main(test_args=None): A test to sort the fonts ''' args = get_args(test_args) - input_dir = Path(args.input_dir) - print(input_dir) - if input_dir.exists(): - fonts_unsorted = get_font_paths(input_dir) - fonts_sorted = sort_fonts( - fonts_unsorted, - args.alternate_italics, - args.debug - ) - print(f'{"unsorted":<36} sorted') - for left, right in zip(fonts_unsorted, fonts_sorted): - print(f'{get_ps_name(left):<36} {get_ps_name(right)}') + fonts_unsorted = [] + for input_dir in args.input: + input_path = Path(input_dir) + if input_path.exists(): + fonts_unsorted.extend(get_font_paths(input_path)) + + fonts_sorted = sort_fonts( + fonts_unsorted, + args.alternate_italics, + args.debug + ) + print(f'{"unsorted":<36} sorted') + for left, right in zip(fonts_unsorted, fonts_sorted): + print(f'{get_ps_name(left):<36} {get_ps_name(right)}') if __name__ == '__main__': diff --git a/drawbot_proofing/proofing_helpers/fonts.py b/drawbot_proofing/proofing_helpers/fonts.py index 31ed185..7dbc8ef 100644 --- a/drawbot_proofing/proofing_helpers/fonts.py +++ b/drawbot_proofing/proofing_helpers/fonts.py @@ -5,22 +5,17 @@ # accordance with the terms of the Adobe license agreement accompanying # it. -import tempfile -import os from fontTools import ttLib from pathlib import Path - - -def get_temp_file_path(extension=None): - file_descriptor, path = tempfile.mkstemp(suffix=extension) - os.close(file_descriptor) - return path +from .files import get_temp_file_path def make_temp_font(file_index, font_file): ''' - Make a temporary font file with unique PS name, because the same PS name - implies that the same font outlines will be seen throughout the PDF. + Make a temporary font file with a unique PS name, so two versions of the + same design can be embedded into the same PDF. + If PS names clash, the implication is that the same font outlines will be + seen throughout the whole document. ''' font = ttLib.TTFont(font_file) file_extension = '.otf' if font.sfntVersion == 'OTTO' else '.ttf' @@ -49,3 +44,34 @@ def get_default_instance(font_file): else: return + + +def supports_charset(cmap, charset): + cmap_dict = cmap.getBestCmap() + to_support = set(charset) | set(charset.upper()) + supported_chars = set([chr(cp) for cp in cmap_dict.keys()]) + return to_support <= supported_chars + + +def supports_lat(cmap): + return supports_charset(cmap, 'abcdefghijklmnopqrstuvwxyz') + + +def supports_cyr(cmap): + return supports_charset(cmap, 'абвгдежзийклмнопрстуфхцчшщъыьэюя') + + +def supports_grk(cmap): + return supports_charset(cmap, 'αβγδεζηθικλμνξοπρστυφχψως') + + +def supports_text(font_file, text, min_percentage=80): + ttFont = ttLib.TTFont(font_file) + cmap_dict = ttFont['cmap'].getBestCmap() + to_support = set(text) + supported_chars = set([chr(cp) for cp in cmap_dict.keys()]) + leftover = to_support - supported_chars + support_percentage = (1 - (len(leftover) / len(to_support))) * 100 + if support_percentage > min_percentage: + return True + return False diff --git a/pyproject.toml b/pyproject.toml index 84349f2..e85a5f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "drawbot-proofing" -version = "1.0.2" +version = "1.0.3" description = "A collection of font proofing tools using DrawBot" readme = "README.md" requires-python = ">=3.11"