bump version
[mftrace.git] / mftrace.py
blob5f8af273e31c357d9b1a881800bc3df04369b2c3
1 #!@PYTHON@
4 # this file is part of mftrace - a tool to generate scalable fonts from MF sources
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License version 2
9 # as published by the Free Software Foundation
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Library General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 # Copyright (c) 2001--2006 by
22 # Han-Wen Nienhuys, Jan Nieuwenhuizen
24 import string
25 import os
26 import optparse
27 import sys
28 import re
29 import tempfile
30 import shutil
32 prefix = '@prefix@'
33 bindir = '@bindir@'
34 datadir = '@datadir@'
35 localedir = datadir + '/locale'
36 libdir = '@libdir@'
37 exec_prefix = '@exec_prefix@'
39 def interpolate (str):
40 str = string.replace (str, '{', '(')
41 str = string.replace (str, '}', ')s')
42 str = string.replace (str, '$', '%')
43 return str
45 if prefix != '@' + 'prefix@':
46 exec_prefix = interpolate (exec_prefix) % vars ()
47 bindir = interpolate (bindir) % vars ()
48 datadir = os.path.join (interpolate (datadir) % vars (), 'mftrace')
49 libdir = interpolate (libdir) % vars ()
51 if datadir == '@' + "datadir" + "@":
52 datadir = os.getcwd ()
53 bindir = os.getcwd ()
55 sys.path.append (datadir)
57 import afm
58 import tfm
60 errorport = sys.stderr
62 ################################################################
63 # lilylib.py -- options and stuff
65 # source file of the GNU LilyPond music typesetter
67 try:
68 import gettext
69 gettext.bindtextdomain ('mftrace', localedir)
70 gettext.textdomain ('mftrace')
71 _ = gettext.gettext
72 except:
73 def _ (s):
74 return s
76 def shell_escape_filename (str):
77 str = re.sub ('([\'" ])', r'\\\1', str)
78 return str
80 def identify (port):
81 port.write ('%s %s\n' % (program_name, program_version))
83 def warranty ():
84 identify (sys.stdout)
85 sys.stdout.write ('\n')
86 sys.stdout.write (_ ('Copyright (c) %s by' % ' 2001--2004'))
87 sys.stdout.write ('\n')
88 sys.stdout.write (' Han-Wen Nienhuys')
89 sys.stdout.write (' Jan Nieuwenhuizen')
90 sys.stdout.write ('\n')
91 sys.stdout.write (_ (r'''
92 Distributed under terms of the GNU General Public License. It comes with
93 NO WARRANTY.'''))
94 sys.stdout.write ('\n')
96 def progress (s):
97 errorport.write (s)
99 def warning (s):
100 errorport.write (_ ("warning: ") + s)
102 def error (s):
103 '''Report the error S. Exit by raising an exception. Please
104 do not abuse by trying to catch this error. If you do not want
105 a stack trace, write to the output directly.
107 RETURN VALUE
109 None
113 errorport.write (_ ("error: ") + s + '\n')
114 raise _ ("Exiting ... ")
116 def setup_temp ():
118 Create a temporary directory, and return its name.
120 global temp_dir
121 if not options.keep_temp_dir:
122 temp_dir = tempfile.mkdtemp (program_name)
124 try:
125 os.mkdir (temp_dir, 0700)
126 except OSError:
127 pass
129 os.chdir (temp_dir)
131 def popen (cmd, mode = 'r', ignore_error = 0):
132 if options.verbose:
133 progress (_ ("Opening pipe `%s\'") % cmd)
134 pipe = os.popen (cmd, mode)
135 if options.verbose:
136 progress ('\n')
137 return pipe
139 def system (cmd, ignore_error = 0):
140 """Run CMD. If IGNORE_ERROR is set, don't complain when CMD returns non zero.
142 RETURN VALUE
144 Exit status of CMD
147 if options.verbose:
148 progress (_ ("Invoking `%s\'\n") % cmd)
149 st = os.system (cmd)
150 if st:
151 name = re.match ('[ \t]*([^ \t]*)', cmd).group (1)
152 msg = name + ': ' + _ ("command exited with value %d") % st
153 if ignore_error:
154 warning (msg + ' ' + _ ("(ignored)") + ' ')
155 else:
156 error (msg)
157 if options.verbose:
158 progress ('\n')
159 return st
161 def cleanup_temp ():
162 if not options.keep_temp_dir:
163 if options.verbose:
164 progress (_ ("Cleaning %s...") % temp_dir)
165 shutil.rmtree (temp_dir)
168 def strip_extension (f, ext):
169 (p, e) = os.path.splitext (f)
170 if e == ext:
171 e = ''
172 return p + e
175 ################################################################
176 # END Library
180 options = None
181 exit_value = 0
182 backend_options = ''
183 program_name = 'mftrace'
184 temp_dir = os.path.join (os.getcwd (), program_name + '.dir')
185 program_version = '@VERSION@'
186 origdir = os.getcwd ()
188 coding_dict = {
190 # from TeTeX
191 'TeX typewriter text': '09fbbfac.enc', # cmtt10
192 'TeX math symbols':'10037936.enc ', # cmbsy
193 'ASCII caps and digits':'1b6d048e', # cminch
194 'TeX math italic': 'aae443f0.enc ', # cmmi10
195 'TeX extended ASCII':'d9b29452.enc',
196 'TeX text': 'f7b6d320.enc',
197 'TeX text without f-ligatures': '0ef0afca.enc',
199 'Extended TeX Font Encoding - Latin': 'tex256.enc',
201 # LilyPond.
202 'fetaBraces': 'feta-braces-a.enc',
203 'fetaNumber': 'feta-nummer10.enc',
204 'fetaMusic': 'feta20.enc',
205 'parmesanMusic': 'parmesan20.enc',
209 def find_file (nm):
210 for d in include_dirs:
211 p = os.path.join (d, nm)
212 try:
213 open (p)
214 return os.path.abspath (p)
215 except IOError:
216 pass
218 p = popen ('kpsewhich %s' % shell_escape_filename (nm)).read ()
219 p = p.strip ()
221 if options.dos_kpath:
222 orig = p
223 p = string.lower (p)
224 p = re.sub ('^([a-z]):', '/cygdrive/\\1', p)
225 p = re.sub ('\\\\', '/', p)
226 sys.stderr.write ("Got `%s' from kpsewhich, using `%s'\n" % (orig, p))
227 return p
230 def flag_error ():
231 global exit_value
232 exit_value = 1
234 ################################################################
235 # TRACING.
236 ################################################################
238 def autotrace_command (fn, opts):
239 opts = " " + opts + " --background-color=FFFFFF --output-format=eps --input-format=pbm "
240 return options.trace_binary + opts + backend_options \
241 + " --output-file=char.eps %s " % fn
243 def potrace_command (fn, opts):
244 return options.trace_binary + opts \
245 + ' -u %d ' % options.grid_scale \
246 + backend_options \
247 + " -q -c --eps --output=char.eps %s " % (fn)
249 trace_command = None
250 path_to_type1_ops = None
252 def trace_one (pbmfile, id):
254 Run tracer, do error handling
257 status = system (trace_command (pbmfile, ''), 1)
259 if status == 2:
260 sys.stderr.write ("\nUser interrupt. Exiting\n")
261 sys.exit (2)
263 if status == 0 and options.keep_temp_dir:
264 shutil.copy2 (pbmfile, '%s.pbm' % id)
265 shutil.copy2 ('char.eps', '%s.eps' % id)
267 if status != 0:
268 error_file = os.path.join (origdir, 'trace-bug-%s.pbm' % id)
269 shutil.copy2 (pbmfile, error_file)
270 msg = """Trace failed on bitmap. Bitmap left in `%s\'
271 Failed command was:
275 Please submit a bugreport to %s development.""" \
276 % (error_file, trace_command (error_file, ''), options.trace_binary)
278 if options.keep_trying:
279 warning (msg)
280 sys.stderr.write ("\nContinuing trace...\n")
281 flag_error ()
282 else:
283 msg = msg + '\nRun mftrace with --keep-trying to produce a font anyway\n'
284 error (msg)
285 else:
286 return 1
288 if status != 0:
289 warning ("Failed, skipping character.\n")
290 return 0
291 else:
292 return 1
294 def make_pbm (filename, outname, char_number):
295 """ Extract bitmap from the PK file FILENAME (absolute) using `gf2pbm'.
296 Return FALSE if the glyph is not valid.
299 command = "%s/gf2pbm -n %d -o %s %s" % (bindir, char_number, outname, filename)
300 status = system (command, ignore_error = 1)
301 return (status == 0)
303 def read_encoding (file):
304 sys.stderr.write (_ ("Using encoding file: `%s'\n") % file)
306 str = open (file).read ()
307 str = re.sub ("%.*", '', str)
308 str = re.sub ("[\n\t \f]+", ' ', str)
309 m = re.search ('/([^ ]+) \[([^\]]+)\] def', str)
310 if not m:
311 error ("Encoding file is invalid")
313 name = m.group (1)
314 cod = m.group (2)
315 cod = re.sub ('[ /]+', ' ', cod)
316 cods = string.split (cod)
318 return (name, cods)
320 def zip_to_pairs (as):
321 r = []
322 while as:
323 r.append ((as[0], as[1]))
324 as = as[2:]
325 return r
327 def unzip_pairs (tups):
328 lst = []
329 while tups:
330 lst = lst + list (tups[0])
331 tups = tups[1:]
332 return lst
334 def autotrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
335 inv_scale = 1000.0 / magnification
337 (size_y, size_x, off_x, off_y) = map (lambda m, s = inv_scale: m * s,
338 bitmap_metrics)
339 ls = open (at_file).readlines ()
340 bbox = (10000, 10000, -10000, -10000)
342 while ls and ls[0] != '*u\n':
343 ls = ls[1:]
345 if ls == []:
346 return (bbox, '')
348 ls = ls[1:]
350 commands = []
353 while ls[0] != '*U\n':
354 ell = ls[0]
355 ls = ls[1:]
357 toks = string.split (ell)
359 if len (toks) < 1:
360 continue
361 cmd = toks[-1]
362 args = map (lambda m, s = inv_scale: s * float (m),
363 toks[:-1])
364 if options.round_to_int:
365 args = zip_to_pairs (map (round, args))
366 else:
367 args = zip_to_pairs (args)
368 commands.append ((cmd, args))
370 expand = {
371 'l': 'rlineto',
372 'm': 'rmoveto',
373 'c': 'rrcurveto',
374 'f': 'closepath',
377 cx = 0
378 cy = size_y - off_y - inv_scale
380 # t1asm seems to fuck up when using sbw. Oh well.
381 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
382 bbox = (10000, 10000, -10000, -10000)
384 for (c, args) in commands:
386 na = []
387 for a in args:
388 (nx, ny) = a
389 if c == 'l' or c == 'c':
390 bbox = update_bbox_with_point (bbox, a)
392 na.append ((nx - cx, ny - cy))
393 (cx, cy) = (nx, ny)
395 a = na
396 c = expand[c]
397 if options.round_to_int:
398 a = map (lambda x: '%d' % int (round (x)),
399 unzip_pairs (a))
400 else:
401 a = map (lambda x: '%d %d div' \
402 % (int (round (x * options.grid_scale/inv_scale)),
403 int (round (options.grid_scale/inv_scale))),
404 unzip_pairs (a))
406 t1_outline = t1_outline + ' %s %s\n' % (string.join (a), c)
408 t1_outline = t1_outline + ' endchar '
409 t1_outline = '{\n %s } |- \n' % t1_outline
411 return (bbox, t1_outline)
413 # FIXME: Cut and paste programming
414 def potrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
415 inv_scale = 1000.0 / magnification
417 (size_y, size_x, off_x, off_y) = map (lambda m,
418 s = inv_scale: m * s,
419 bitmap_metrics)
420 ls = open (at_file).readlines ()
421 bbox = (10000, 10000, -10000, -10000)
423 while ls and ls[0] != '0 setgray\n':
424 ls = ls[1:]
426 if ls == []:
427 return (bbox, '')
428 ls = ls[1:]
429 commands = []
431 while ls and ls[0] != 'grestore\n':
432 ell = ls[0]
433 ls = ls[1:]
435 if ell == 'fill\n':
436 continue
438 toks = string.split (ell)
440 if len (toks) < 1:
441 continue
442 cmd = toks[-1]
443 args = map (lambda m, s = inv_scale: s * float (m),
444 toks[:-1])
445 args = zip_to_pairs (args)
446 commands.append ((cmd, args))
448 # t1asm seems to fuck up when using sbw. Oh well.
449 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
450 bbox = (10000, 10000, -10000, -10000)
452 # Type1 fonts have relative coordinates (doubly relative for
453 # rrcurveto), so must convert moveto and rcurveto.
455 z = (0.0, size_y - off_y - 1.0)
456 for (c, args) in commands:
457 args = map (lambda x: (x[0] * (1.0 / options.grid_scale),
458 x[1] * (1.0 / options.grid_scale)), args)
460 if c == 'moveto':
461 args = [(args[0][0] - z[0], args[0][1] - z[1])]
463 zs = []
464 for a in args:
465 lz = (z[0] + a[0], z[1] + a[1])
466 bbox = update_bbox_with_point (bbox, lz)
467 zs.append (lz)
469 if options.round_to_int:
470 last_discr_z = (int (round (z[0])), int (round (z[1])))
471 else:
472 last_discr_z = (z[0], z[1])
473 args = []
474 for a in zs:
475 if options.round_to_int:
476 a = (int (round (a[0])), int (round (a[1])))
477 else:
478 a = (a[0], a[1])
479 args.append ((a[0] - last_discr_z[0],
480 a[1] - last_discr_z[1]))
482 last_discr_z = a
484 if zs:
485 z = zs[-1]
486 c = { 'rcurveto': 'rrcurveto',
487 'moveto': 'rmoveto',
488 'closepath': 'closepath',
489 'rlineto': 'rlineto'}[c]
491 if c == 'rmoveto':
492 t1_outline += ' closepath '
494 if options.round_to_int:
495 args = map (lambda x: '%d' % int (round (x)),
496 unzip_pairs (args))
497 else:
498 args = map (lambda x: '%d %d div' \
499 % (int (round (x*options.grid_scale/inv_scale)),
500 int (round (options.grid_scale/inv_scale))),
501 unzip_pairs (args))
503 t1_outline = t1_outline + ' %s %s\n' % (string.join (args), c)
505 t1_outline = t1_outline + ' endchar '
506 t1_outline = '{\n %s } |- \n' % t1_outline
508 return (bbox, t1_outline)
510 def read_gf_dims (name, c):
511 str = popen ('%s/gf2pbm -n %d -s %s' % (bindir, c, name)).read ()
512 m = re.search ('size: ([0-9]+)+x([0-9]+), offset: \(([0-9-]+),([0-9-]+)\)', str)
514 return tuple (map (int, m.groups ()))
516 def trace_font (fontname, gf_file, metric, glyphs, encoding,
517 magnification, fontinfo):
518 t1os = []
519 font_bbox = (10000, 10000, -10000, -10000)
521 progress (_ ("Tracing bitmaps... "))
523 # for single glyph testing.
524 # glyphs = []
525 for a in glyphs:
526 if encoding[a] == ".notavail":
527 continue
528 valid = metric.has_char (a)
529 if not valid:
530 encoding[a] = ".notavail"
531 continue
533 valid = make_pbm (gf_file, 'char.pbm', a)
534 if not valid:
535 encoding[a] = ".notavail"
536 continue
538 (w, h, xo, yo) = read_gf_dims (gf_file, a)
540 if not options.verbose:
541 sys.stderr.write ('[%d' % a)
542 sys.stderr.flush ()
544 # this wants the id, not the filename.
545 success = trace_one ("char.pbm", '%s-%d' % (options.gffile, a))
546 if not success:
547 sys.stderr.write ("(skipping character)]")
548 sys.stderr.flush ()
549 encoding[a] = ".notavail"
550 continue
552 if not options.verbose:
553 sys.stderr.write (']')
554 sys.stderr.flush ()
555 metric_width = metric.get_char (a).width
556 tw = int (round (metric_width / metric.design_size * 1000))
557 (bbox, t1o) = path_to_type1_ops ("char.eps", (h, w, xo, yo),
558 tw, magnification)
560 if t1o == '':
561 encoding[a] = ".notavail"
562 continue
564 font_bbox = update_bbox_with_bbox (font_bbox, bbox)
566 t1os.append ('\n/%s %s ' % (encoding[a], t1o))
568 progress ('\n')
569 to_type1 (t1os, font_bbox, fontname, encoding, magnification, fontinfo)
571 def ps_encode_encoding (encoding):
572 str = ' %d array\n0 1 %d {1 index exch /.notdef put} for\n' \
573 % (len (encoding), len (encoding)-1)
575 for i in range (0, len (encoding)):
576 if encoding[i] != ".notavail":
577 str = str + 'dup %d /%s put\n' % (i, encoding[i])
579 return str
582 def gen_unique_id (dict):
583 nm = 'FullName'
584 return 4000000 + (hash (nm) % 1000000)
586 def to_type1 (outlines, bbox, fontname, encoding, magnification, fontinfo):
589 Fill in the header template for the font, append charstrings,
590 and shove result through t1asm
592 template = r"""%%!PS-AdobeFont-1.0: %(FontName)s %(VVV)s.%(WWW)s
593 13 dict begin
594 /FontInfo 16 dict dup begin
595 /version (%(VVV)s.%(WWW)s) readonly def
596 /Notice (%(Notice)s) readonly def
597 /FullName (%(FullName)s) readonly def
598 /FamilyName (%(FamilyName)s) readonly def
599 /Weight (%(Weight)s) readonly def
600 /ItalicAngle %(ItalicAngle)s def
601 /isFixedPitch %(isFixedPitch)s def
602 /UnderlinePosition %(UnderlinePosition)s def
603 /UnderlineThickness %(UnderlineThickness)s def
604 end readonly def
605 /FontName /%(FontName)s def
606 /FontType 1 def
607 /PaintType 0 def
608 /FontMatrix [%(xrevscale)f 0 0 %(yrevscale)f 0 0] readonly def
609 /FontBBox {%(llx)d %(lly)d %(urx)d %(ury)d} readonly def
610 /Encoding %(Encoding)s readonly def
611 currentdict end
612 currentfile eexec
613 dup /Private 20 dict dup begin
614 /-|{string currentfile exch readstring pop}executeonly def
615 /|-{noaccess def}executeonly def
616 /|{noaccess put}executeonly def
617 /lenIV 4 def
618 /password 5839 def
619 /MinFeature {16 16} |-
620 /BlueValues [] |-
621 /OtherSubrs [ {} {} {} {} ] |-
622 /ForceBold false def
623 /Subrs 1 array
624 dup 0 { return } |
626 2 index
627 /CharStrings %(CharStringsLen)d dict dup begin
628 %(CharStrings)s
631 /.notdef { 0 0 hsbw endchar } |-
634 readonly put
635 noaccess put
636 dup/FontName get exch definefont
637 pop mark currentfile closefile
638 cleartomark
640 ## apparently, some fonts end the file with cleartomark. Don't know why.
642 copied_fields = ['FontName', 'FamilyName', 'FullName', 'DesignSize',
643 'ItalicAngle', 'isFixedPitch', 'Weight']
645 vars = {
646 'VVV': '001',
647 'WWW': '001',
648 'Notice': 'Generated from MetaFont bitmap by mftrace %s, http://www.xs4all.nl/~hanwen/mftrace/ ' % program_version,
649 'UnderlinePosition': '-100',
650 'UnderlineThickness': '50',
651 'xrevscale': 1.0/1000.0,
652 'yrevscale': 1.0/1000.0,
653 'llx': bbox[0],
654 'lly': bbox[1],
655 'urx': bbox[2],
656 'ury': bbox[3],
657 'Encoding': ps_encode_encoding (encoding),
659 # need one extra entry for .notdef
660 'CharStringsLen': len (outlines) + 1,
661 'CharStrings': string.join (outlines),
662 'CharBBox': '0 0 0 0',
665 for k in copied_fields:
666 vars[k] = fontinfo[k]
668 open ('mftrace.t1asm', 'w').write (template % vars)
670 def update_bbox_with_point (bbox, pt):
671 (llx, lly, urx, ury) = bbox
672 llx = min (pt[0], llx)
673 lly = min (pt[1], lly)
674 urx = max (pt[0], urx)
675 ury = max (pt[1], ury)
677 return (llx, lly, urx, ury)
679 def update_bbox_with_bbox (bb, dims):
680 (llx, lly, urx, ury) = bb
681 llx = min (llx, dims[0])
682 lly = min (lly, dims[1])
683 urx = max (urx, dims[2])
684 ury = max (ury, dims[3])
686 return (llx, lly, urx, ury)
688 def get_binary (name):
689 search_path = string.split (os.environ['PATH'], ':')
690 for p in search_path:
691 nm = os.path.join (p, name)
692 if os.path.exists (nm):
693 return nm
695 return ''
697 def get_fontforge_command ():
698 fontforge_cmd = ''
699 for ff in ['fontforge', 'pfaedit']:
700 if get_binary(ff):
701 fontforge_cmd = ff
703 stat = 1
704 if fontforge_cmd:
705 stat = system ("%s -usage > pfv 2>&1 " % fontforge_cmd,
706 ignore_error = 1)
708 if stat != 0:
709 warning ("Command `%s -usage' failed. Cannot simplify or convert to TTF.\n" % fontforge_cmd)
711 if fontforge_cmd == 'pfaedit' \
712 and re.search ("-script", open ('pfv').read ()) == None:
713 warning ("pfaedit does not support -script. Install 020215 or later.\nCannot simplify or convert to TTF.\n")
714 return ''
715 return fontforge_cmd
717 def tfm2kpx (tfmname, encoding):
718 kpx_lines = []
719 pl = popen ("tftopl %s" % (tfmname))
721 label_pattern = re.compile (
722 "\A \(LABEL ([DOHC]{1}) ([A-Za-z0-9]*)\)")
723 krn_pattern = re.compile (
724 "\A \(KRN ([DOHC]{1}) ([A-Za-z0-9]*) R (-?[\d\.]+)\)")
726 first = 0
727 second = 0
729 for line in pl.readlines ():
731 label_match = label_pattern.search (line)
732 if not (label_match is None):
733 if label_match.group (1) == "D":
734 first = int (label_match.group (2))
735 elif label_match.group (1) == "O":
736 first = int (label_match.group (2), 8)
737 elif label_match.group (1) == "C":
738 first = ord (label_match.group (2))
740 krn_match = krn_pattern.search (line)
741 if not (krn_match is None):
742 if krn_match.group (1) == "D":
743 second = int (krn_match.group (2))
744 elif krn_match.group (1) == "O":
745 second = int (krn_match.group (2), 8)
746 elif krn_match.group (1) == "C":
747 second = ord (krn_match.group (2))
749 krn = round (float (krn_match.group (3)) * 1000)
751 if (encoding[first] != '.notavail' and
752 encoding[first] != '.notdef' and
753 encoding[second] != '.notavail' and
754 encoding[second] != '.notdef'):
756 kpx_lines.append ("KPX %s %s %d\n" % (
757 encoding[first], encoding[second], krn))
759 return kpx_lines
761 def get_afm (t1_path, tfmname, encoding, out_path):
762 afm_stream = popen ("printafm %s" % (t1_path))
763 afm_lines = []
764 kpx_lines = tfm2kpx (tfmname, encoding)
766 for line in afm_stream.readlines ():
767 afm_lines.append (line)
769 if re.match (r"^EndCharMetrics", line, re.I):
770 afm_lines.append ("StartKernData\n")
771 afm_lines.append ("StartKernPairs %d\n" % len (kpx_lines))
773 for kpx_line in kpx_lines:
774 afm_lines.append (kpx_line)
776 afm_lines.append ("EndKernPairs\n")
777 afm_lines.append ("EndKernData\n")
779 progress (_ ("Writing metrics to `%s'... ") % out_path)
780 afm_file = open (out_path, 'w')
781 afm_file.writelines (afm_lines)
782 afm_file.flush ()
783 afm_file.close ()
785 progress ('\n')
787 def assemble_font (fontname, format, is_raw):
788 ext = '.' + format
789 asm_opt = '--pfa'
791 if format == 'pfb':
792 asm_opt = '--pfb'
794 if is_raw:
795 ext = ext + '.raw'
797 outname = fontname + ext
799 progress (_ ("Assembling raw font to `%s'... ") % outname)
800 system ('t1asm %s mftrace.t1asm %s' % (asm_opt, shell_escape_filename (outname)))
801 progress ('\n')
802 return outname
804 def make_outputs (fontname, formats, encoding):
806 run pfaedit to convert to other formats
809 ff_needed = 0
810 ff_command = ""
812 if (options.simplify or options.round_to_int or 'ttf' in formats or 'svg' in formats):
813 ff_needed = 1
814 if ff_needed:
815 ff_command = get_fontforge_command ()
817 if ff_needed and ff_command:
818 raw_name = assemble_font (fontname, 'pfa', 1)
820 simplify_cmd = ''
821 if options.round_to_int:
822 simplify_cmd = 'RoundToInt ();'
823 generate_cmds = ''
824 for f in formats:
825 generate_cmds += 'Generate("%s");' % (fontname + '.' + f)
827 if options.simplify:
828 simplify_cmd ='''SelectAll ();
830 AddExtrema();
831 Simplify ();
832 %(simplify_cmd)s
833 AutoHint ();''' % vars()
835 pe_script = ('''#!/usr/bin/env %(ff_command)s
836 Open ($1);
837 MergeKern($2);
838 %(simplify_cmd)s
839 %(generate_cmds)s
840 Quit (0);
841 ''' % vars())
843 open ('to-ttf.pe', 'w').write (pe_script)
844 if options.verbose:
845 print 'Fontforge script', pe_script
846 system ("%s -script to-ttf.pe %s %s" % (ff_command,
847 shell_escape_filename (raw_name), shell_escape_filename (options.tfm_file)))
848 else:
849 t1_path = ''
851 if ('pfa' in formats):
852 t1_path = assemble_font (fontname, 'pfa', 0)
854 if ('pfb' in formats):
855 t1_path = assemble_font (fontname, 'pfb', 0)
857 if (t1_path != '' and 'afm' in formats):
858 get_afm (t1_path, options.tfm_file, encoding, fontname + '.afm')
861 def getenv (var, default):
862 if os.environ.has_key (var):
863 return os.environ[var]
864 else:
865 return default
867 def gen_pixel_font (filename, metric, magnification):
869 Generate a GF file for FILENAME, such that `magnification'*mfscale
870 (default 1000 * 1.0) pixels fit on the designsize.
872 base_dpi = 1200
874 size = metric.design_size
876 size_points = size * 1/72.27 * base_dpi
878 mag = magnification / size_points
880 prod = mag * base_dpi
881 try:
882 open ('%s.%dgf' % (filename, prod))
883 except IOError:
884 os.environ['KPSE_DOT'] = '%s:' % origdir
886 os.environ['MFINPUTS'] = '%s:%s' % (origdir,
887 getenv ('MFINPUTS', ''))
888 os.environ['TFMFONTS'] = '%s:%s' % (origdir,
889 getenv ('TFMINPUTS', ''))
891 # FIXME: we should not change to another (tmp) dir?
892 # or else make all relavitive dirs in paths absolute.
893 def abs_dir (x, dir):
894 if x and os.path.abspath (x) != x:
895 return os.path.join (dir, x)
896 return x
898 def abs_path (path, dir):
899 # Python's ABSPATH means ABSDIR
900 dir = os.path.abspath (dir)
901 return string.join (map (lambda x: abs_dir (x, dir),
902 string.split (path,
903 os.pathsep)),
904 os.pathsep)
906 os.environ['MFINPUTS'] = abs_path (os.environ['MFINPUTS'],
907 origdir)
908 os.environ['TFMFONTS'] = abs_path (os.environ['TFMFONTS'],
909 origdir)
911 progress (_ ("Running Metafont..."))
913 cmdstr = r"mf '\mode:=lexmarks; mag:=%f; nonstopmode; input %s'" % (mag, filename)
914 if not options.verbose:
915 cmdstr = cmdstr + ' 1>/dev/null 2>/dev/null'
916 st = system (cmdstr, ignore_error = 1)
917 progress ('\n')
919 logfile = '%s.log' % filename
920 log = ''
921 prod = 0
922 if os.path.exists (logfile):
923 log = open (logfile).read ()
924 m = re.search ('Output written on %s.([0-9]+)gf' % re.escape (filename), log)
925 prod = int (m.group (1))
927 if st:
928 sys.stderr.write ('\n\nMetafont failed. Excerpt from the log file: \n\n*****')
929 m = re.search ("\n!", log)
930 start = m.start (0)
931 short_log = log[start:start+200]
932 sys.stderr.write (short_log)
933 sys.stderr.write ('\n*****\n')
934 if re.search ('Arithmetic overflow', log):
935 sys.stderr.write ("""
937 Apparently, some numbers overflowed. Try using --magnification with a
938 lower number. (Current magnification: %d)
939 """ % magnification)
941 if not options.keep_trying or prod == 0:
942 sys.exit (1)
943 else:
944 sys.stderr.write ('\n\nTrying to proceed despite of the Metafont errors...\n')
948 return "%s.%d" % (filename, prod)
950 def parse_command_line ():
951 p = optparse.OptionParser (version="""mftrace @VERSION@
953 This program is free software. It is covered by the GNU General Public
954 License and you are welcome to change it and/or distribute copies of it
955 under certain conditions. Invoke as `mftrace --warranty' for more
956 information.
958 Copyright (c) 2005--2006 by
959 Han-Wen Nienhuys <hanwen@xs4all.nl>
961 """)
962 p.usage = "mftrace [OPTION]... FILE..."
963 p.description = _ ("Generate Type1 or TrueType font from Metafont source.")
965 p.add_option ('-k', '--keep',
966 action="store_true",
967 dest="keep_temp_dir",
968 help=_ ("Keep all output in directory %s.dir") % program_name)
969 p.add_option ('','--magnification',
970 dest="magnification",
971 metavar="MAG",
972 default=1000.0,
973 type="float",
974 help=_("Set magnification for MF to MAG (default: 1000)"))
975 p.add_option ('-V', '--verbose',
976 action='store_true',
977 default=False,
978 help=_ ("Be verbose"))
979 p.add_option ('-f', '--formats',
980 action="append",
981 dest="formats",
982 default=[],
983 help=_("Which formats to generate (choices: AFM, PFA, PFB, TTF, SVG)"))
984 p.add_option ('', '--simplify',
985 action="store_true",
986 dest="simplify",
987 help=_ ("Simplify using fontforge"))
988 p.add_option ('', '--gffile',
989 dest="gffile",
990 help= _("Use gf FILE instead of running Metafont"))
991 p.add_option ('-I', '--include',
992 dest="include_dirs",
993 action="append",
994 default=[],
995 help=_("Add to path for searching files"))
996 p.add_option ('','--glyphs',
997 default=[],
998 action="append",
999 dest="glyphs",
1000 metavar="LIST",
1001 help= _('Process only these glyphs. LIST is comma separated'))
1002 p.add_option ('', '--tfmfile',
1003 metavar='FILE',
1004 action='store',
1005 dest='tfm_file')
1007 p.add_option ('-e', '--encoding',
1008 metavar="FILE",
1009 action='store',
1010 dest="encoding_file",
1011 default="",
1012 help= _ ("Use encoding file FILE"))
1013 p.add_option ('','--keep-trying',
1014 dest='keep_trying',
1015 default=False,
1016 action="store_true",
1017 help= _ ("Don't stop if tracing fails"))
1018 p.add_option ('-w', '--warranty',
1019 action="store_true",
1020 help=_ ("show warranty and copyright"))
1021 p.add_option ('','--dos-kpath',
1022 dest="dos_kpath",
1023 help=_("try to use Miktex kpsewhich"))
1024 p.add_option ('', '--potrace',
1025 dest='potrace',
1026 help=_ ("Use potrace"))
1027 p.add_option ('', '--autotrace',
1028 dest='autotrace',
1029 help=_ ("Use autotrace"))
1030 p.add_option ('', '--no-afm',
1031 action='store_false',
1032 dest="read_afm",
1033 default=True,
1034 help=_("Don't read AFM file"))
1035 p.add_option ('','--noround',
1036 action="store_false",
1037 dest='round_to_int',
1038 default=True,
1039 help= ("Do not round coordinates of control points to integer values (use with --grid)"))
1040 p.add_option ('','--grid',
1041 metavar='SCALE',
1042 dest='grid_scale',
1043 type='float',
1044 default = 1.0,
1045 help=_ ("Set reciprocal grid size in em units"))
1046 p.add_option ('-D','--define',
1047 metavar="SYMBOL=VALUE",
1048 dest="defs",
1049 default=[],
1050 action='append',help=_("Set the font info SYMBOL to VALUE"))
1052 global options
1053 (options, files) = p.parse_args ()
1055 if not files:
1056 sys.stderr.write ('Need argument on command line \n')
1057 p.print_help ()
1058 sys.exit (2)
1060 if options.warranty :
1061 warranty ()
1062 sys.exit (0)
1064 options.font_info = {}
1065 for d in options.defs:
1066 kv = d.split('=')
1067 if len (kv) == 1:
1068 options.font_info[kv] = 'true'
1069 elif len (kv) > 1:
1070 options.font_info[kv[0]] = '='.join (kv[1:])
1072 def comma_sepped_to_list (x):
1073 fs = []
1074 for f in x:
1075 fs += f.lower ().split (',')
1076 return fs
1078 options.formats = comma_sepped_to_list (options.formats)
1080 new_glyphs = []
1081 for r in options.glyphs:
1082 new_glyphs += r.split (',')
1083 options.glyphs = new_glyphs
1085 glyph_range = []
1086 for r in options.glyphs:
1087 glyph_subrange = map (int, string.split (r, '-'))
1088 if len (glyph_subrange) == 2 and glyph_subrange[0] < glyph_subrange[1] + 1:
1089 glyph_range += range (glyph_subrange[0], glyph_subrange[1] + 1)
1090 else:
1091 glyph_range.append (glyph_subrange[0])
1093 options.glyphs = glyph_range
1095 options.trace_binary = ''
1096 if options.potrace:
1097 options.trace_binary = 'potrace'
1098 elif options.autotrace:
1099 options.trace_binary = 'autotrace'
1101 if options.formats == []:
1102 options.formats = ['pfa']
1106 global trace_command
1107 global path_to_type1_ops
1109 stat = os.system ('potrace --version > /dev/null 2>&1 ')
1110 if options.trace_binary != 'autotrace' and stat == 0:
1111 options.trace_binary = 'potrace'
1113 trace_command = potrace_command
1114 path_to_type1_ops = potrace_path_to_type1_ops
1116 stat = os.system ('autotrace --version > /dev/null 2>&1 ')
1117 if options.trace_binary != 'potrace' and stat == 0:
1118 options.trace_binary = 'autotrace'
1119 trace_command = autotrace_command
1120 path_to_type1_ops = autotrace_path_to_type1_ops
1122 if not options.trace_binary:
1123 error (_ ("No tracing program found.\nInstall potrace or autotrace."))
1125 return files
1128 def derive_font_name (family, fullname):
1129 fullname = re.sub (family, '', fullname)
1130 family = re.sub (' ', '', family)
1131 fullname = re.sub ('Oldstyle Figures', 'OsF', fullname)
1132 fullname = re.sub ('Small Caps', 'SC', fullname)
1133 fullname = re.sub ('[Mm]edium', '', fullname)
1134 fullname = re.sub ('[^A-Za-z0-9]', '', fullname)
1135 return '%s-%s' % (family, fullname)
1137 def cm_guess_font_info (filename, fontinfo):
1138 # urg.
1139 filename = re.sub ("cm(.*)tt", r"cmtt\1", filename)
1140 m = re.search ("([0-9]+)$", filename)
1141 design_size = ''
1142 if m:
1143 design_size = int (m.group (1))
1144 fontinfo['DesignSize'] = design_size
1146 prefixes = [("cmtt", "Computer Modern Typewriter"),
1147 ("cmvtt", "Computer Modern Variable Width Typewriter"),
1148 ("cmss", "Computer Modern Sans"),
1149 ("cm", "Computer Modern")]
1151 family = ''
1152 for (k, v) in prefixes:
1153 if re.search (k, filename):
1154 family = v
1155 if k == 'cmtt':
1156 fontinfo['isFixedPitch'] = 'true'
1157 filename = re.sub (k, '', filename)
1158 break
1160 # shapes
1161 prefixes = [("r", "Roman"),
1162 ("mi", "Math italic"),
1163 ("u", "Unslanted italic"),
1164 ("sl", "Oblique"),
1165 ("csc", "Small Caps"),
1166 ("ex", "Math extension"),
1167 ("ti", "Text italic"),
1168 ("i", "Italic")]
1169 shape = ''
1170 for (k, v) in prefixes:
1171 if re.search (k, filename):
1172 shape = v
1173 filename = re.sub (k, '', filename)
1175 prefixes = [("b", "Bold"),
1176 ("d", "Demi bold")]
1177 weight = 'Regular'
1178 for (k, v) in prefixes:
1179 if re.search (k, filename):
1180 weight = v
1181 filename = re.sub (k, '', filename)
1183 prefixes = [("c", "Condensed"),
1184 ("x", "Extended")]
1185 stretch = ''
1186 for (k, v) in prefixes:
1187 if re.search (k, filename):
1188 stretch = v
1189 filename = re.sub (k, '', filename)
1191 fontinfo['ItalicAngle'] = 0
1192 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1193 a = -14
1194 if re.search ("Sans", family):
1195 a = -12
1197 fontinfo ["ItalicAngle"] = a
1199 fontinfo['Weight'] = weight
1200 fontinfo['FamilyName'] = family
1201 full = '%s %s %s %s %dpt' \
1202 % (family, shape, weight, stretch, design_size)
1203 full = re.sub (" +", ' ', full)
1205 fontinfo['FullName'] = full
1206 fontinfo['FontName'] = derive_font_name (family, full)
1208 return fontinfo
1210 def ec_guess_font_info (filename, fontinfo):
1211 design_size = 12
1212 m = re.search ("([0-9]+)$", filename)
1213 if m:
1214 design_size = int (m.group (1))
1215 fontinfo['DesignSize'] = design_size
1217 prefixes = [("ecss", "European Computer Modern Sans"),
1218 ("ectt", "European Computer Modern Typewriter"),
1219 ("ec", "European Computer Modern")]
1221 family = ''
1222 for (k, v) in prefixes:
1223 if re.search (k, filename):
1224 if k == 'ectt':
1225 fontinfo['isFixedPitch'] = 'true'
1226 family = v
1227 filename = re.sub (k, '', filename)
1228 break
1230 # shapes
1231 prefixes = [("r", "Roman"),
1232 ("mi", "Math italic"),
1233 ("u", "Unslanted italic"),
1234 ("sl", "Oblique"),
1235 ("cc", "Small caps"),
1236 ("ex", "Math extension"),
1237 ("ti", "Italic"),
1238 ("i", "Italic")]
1240 shape = ''
1241 for (k, v) in prefixes:
1242 if re.search (k, filename):
1243 shape = v
1244 filename = re.sub (k, '', filename)
1246 prefixes = [("b", "Bold"),
1247 ("d", "Demi bold")]
1248 weight = 'Regular'
1249 for (k, v) in prefixes:
1250 if re.search (k, filename):
1251 weight = v
1252 filename = re.sub (k, '', filename)
1254 prefixes = [("c", "Condensed"),
1255 ("x", "Extended")]
1256 stretch = ''
1257 for (k, v) in prefixes:
1258 if re.search (k, filename):
1259 stretch = v
1260 filename = re.sub (k, '', filename)
1262 fontinfo['ItalicAngle'] = 0
1263 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1264 a = -14
1265 if re.search ("Sans", family):
1266 a = -12
1268 fontinfo ["ItalicAngle"] = a
1270 fontinfo['Weight'] = weight
1271 fontinfo['FamilyName'] = family
1272 full = '%s %s %s %s %dpt' \
1273 % (family, shape, weight, stretch, design_size)
1274 full = re.sub (" +", ' ', full)
1276 fontinfo['FontName'] = derive_font_name (family, full)
1277 fontinfo['FullName'] = full
1279 return fontinfo
1282 def guess_fontinfo (filename):
1283 fi = {
1284 'FontName': filename,
1285 'FamilyName': filename,
1286 'Weight': 'Regular',
1287 'ItalicAngle': 0,
1288 'DesignSize' : 12,
1289 'isFixedPitch' : 'false',
1290 'FullName': filename,
1293 if re.search ('^cm', filename):
1294 fi.update (cm_guess_font_info (filename, fi))
1295 elif re.search ("^ec", filename):
1296 fi.update (ec_guess_font_info (filename, fi))
1297 elif options.read_afm:
1298 global afmfile
1299 if not afmfile:
1300 afmfile = find_file (filename + '.afm')
1302 if afmfile:
1303 afmfile = os.path.abspath (afmfile)
1304 afm_struct = afm.read_afm_file (afmfile)
1305 fi.update (afm_struct.__dict__)
1306 return fi
1307 else:
1308 sys.stderr.write ("Warning: no extra font information for this font.\n"
1309 + "Consider writing a XX_guess_font_info() routine.\n")
1311 return fi
1313 def do_file (filename):
1314 encoding_file = options.encoding_file
1315 global include_dirs
1316 include_dirs = options.include_dirs
1317 include_dirs.append (origdir)
1319 basename = strip_extension (filename, '.mf')
1320 progress (_ ("Font `%s'..." % basename))
1321 progress ('\n')
1323 ## setup encoding
1324 if encoding_file and not os.path.exists (encoding_file):
1325 encoding_file = find_file (encoding_file)
1326 else:
1327 encoding_file = os.path.abspath (encoding_file)
1329 ## setup TFM
1330 if options.tfm_file:
1331 options.tfm_file = os.path.abspath (options.tfm_file)
1333 ## must change dir before calling mktextfm.
1334 setup_temp ()
1335 if options.verbose:
1336 progress ('Temporary directory is `%s\'\n' % temp_dir)
1338 if not options.tfm_file:
1339 options.tfm_file = popen ("mktextfm %s 2>/dev/null" % shell_escape_filename (basename)).read ()
1340 if options.tfm_file:
1341 options.tfm_file = options.tfm_file.strip ()
1343 options.tfm_file = os.path.abspath (options.tfm_file)
1345 if not options.tfm_file:
1346 error (_ ("Can not find a TFM file to match `%s'") % basename)
1348 metric = tfm.read_tfm_file (options.tfm_file)
1350 fontinfo = guess_fontinfo (basename)
1351 fontinfo.update (options.font_info)
1353 if not encoding_file:
1354 codingfile = 'tex256.enc'
1355 if not coding_dict.has_key (metric.coding):
1356 sys.stderr.write ("Unknown encoding `%s'; assuming tex256.\n" % metric.coding)
1357 else:
1358 codingfile = coding_dict[metric.coding]
1360 encoding_file = find_file (codingfile)
1361 if not encoding_file:
1362 error (_ ("can't find file `%s'" % codingfile))
1364 (enc_name, encoding) = read_encoding (encoding_file)
1366 if not len (options.glyphs):
1367 options.glyphs = range (0, len (encoding))
1369 if not options.gffile:
1370 # run mf
1371 base = gen_pixel_font (basename, metric, options.magnification)
1372 options.gffile = base + 'gf'
1373 else:
1374 options.gffile = find_file (options.gffile)
1376 # the heart of the program:
1377 trace_font (basename, options.gffile, metric, options.glyphs, encoding,
1378 options.magnification, fontinfo)
1380 make_outputs (basename, options.formats, encoding)
1381 for format in options.formats:
1382 shutil.copy2 (basename + '.' + format, origdir)
1384 os.chdir (origdir)
1385 cleanup_temp ()
1391 afmfile = ''
1392 backend_options = getenv ('MFTRACE_BACKEND_OPTIONS', '')
1393 def main ():
1394 files = parse_command_line ()
1395 identify (sys.stderr)
1397 for filename in files:
1398 do_file (filename)
1399 sys.exit (exit_value)
1401 if __name__ =='__main__':
1402 main()