Bump version.
[mftrace.git] / mftrace.py
blobb57080ae7a8da5b4ee61b69096edb1a7b95e6177
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 temp_dir = None
117 class TempDirectory:
118 def __init__ (self, name=None):
119 import tempfile
120 if name:
121 if not os.path.isdir (name):
122 os.makedirs (name)
123 self.dir = name
124 else:
125 self.dir = tempfile.mkdtemp ()
127 os.chdir (self.dir)
129 def clean (self):
130 import shutil
131 shutil.rmtree (self.dir)
132 def __del__ (self):
133 self.clean ()
134 def __call__ (self):
135 return self.dir
136 def __repr__ (self):
137 return self.dir
138 def __str__ (self):
139 return self.dir
141 def setup_temp (name):
142 global temp_dir
143 if not temp_dir:
144 temp_dir = TempDirectory (name)
145 return temp_dir ()
147 def popen (cmd, mode = 'r', ignore_error = 0):
148 if options.verbose:
149 progress (_ ("Opening pipe `%s\'") % cmd)
150 pipe = os.popen (cmd, mode)
151 if options.verbose:
152 progress ('\n')
153 return pipe
155 def system (cmd, ignore_error = 0):
156 """Run CMD. If IGNORE_ERROR is set, don't complain when CMD returns non zero.
158 RETURN VALUE
160 Exit status of CMD
163 if options.verbose:
164 progress (_ ("Invoking `%s\'\n") % cmd)
165 st = os.system (cmd)
166 if st:
167 name = re.match ('[ \t]*([^ \t]*)', cmd).group (1)
168 msg = name + ': ' + _ ("command exited with value %d") % st
169 if ignore_error:
170 warning (msg + ' ' + _ ("(ignored)") + ' ')
171 else:
172 error (msg)
173 if options.verbose:
174 progress ('\n')
175 return st
177 def strip_extension (f, ext):
178 (p, e) = os.path.splitext (f)
179 if e == ext:
180 e = ''
181 return p + e
184 ################################################################
185 # END Library
189 options = None
190 exit_value = 0
191 backend_options = ''
192 program_name = 'mftrace'
193 temp_dir = None
194 program_version = '@VERSION@'
195 origdir = os.getcwd ()
197 coding_dict = {
199 # from TeTeX
200 'TeX typewriter text': '09fbbfac.enc', # cmtt10
201 'TeX math symbols': '10037936.enc', # cmbsy
202 'ASCII caps and digits': '1b6d048e', # cminch
203 'TeX math italic': 'aae443f0.enc', # cmmi10
204 'TeX extended ASCII': 'd9b29452.enc',
205 'TeX text': 'f7b6d320.enc',
206 'TeX text without f-ligatures': '0ef0afca.enc',
207 'Extended TeX Font Encoding - Latin': 'tex256.enc',
209 # LilyPond.
210 'fetaBraces': 'feta-braces-a.enc',
211 'fetaNumber': 'feta-nummer10.enc',
212 'fetaMusic': 'feta20.enc',
213 'parmesanMusic': 'parmesan20.enc',
217 def find_file (nm):
218 for d in include_dirs:
219 p = os.path.join (d, nm)
220 try:
221 open (p)
222 return os.path.abspath (p)
223 except IOError:
224 pass
226 p = popen ('kpsewhich %s' % shell_escape_filename (nm)).read ()
227 p = p.strip ()
229 if options.dos_kpath:
230 orig = p
231 p = string.lower (p)
232 p = re.sub ('^([a-z]):', '/cygdrive/\\1', p)
233 p = re.sub ('\\\\', '/', p)
234 sys.stderr.write ("Got `%s' from kpsewhich, using `%s'\n" % (orig, p))
235 return p
238 def flag_error ():
239 global exit_value
240 exit_value = 1
242 ################################################################
243 # TRACING.
244 ################################################################
246 def autotrace_command (fn, opts):
247 opts = " " + opts + " --background-color=FFFFFF --output-format=eps --input-format=pbm "
248 return options.trace_binary + opts + backend_options \
249 + " --output-file=char.eps %s " % fn
251 def potrace_command (fn, opts):
252 return options.trace_binary + opts \
253 + ' -u %d ' % options.grid_scale \
254 + backend_options \
255 + " -q -c --eps --output=char.eps %s " % (fn)
257 trace_command = None
258 path_to_type1_ops = None
260 def trace_one (pbmfile, id):
262 Run tracer, do error handling
265 status = system (trace_command (pbmfile, ''), 1)
267 if status == 2:
268 sys.stderr.write ("\nUser interrupt. Exiting\n")
269 sys.exit (2)
271 if status == 0 and options.keep_temp_dir:
272 shutil.copy2 (pbmfile, '%s.pbm' % id)
273 shutil.copy2 ('char.eps', '%s.eps' % id)
275 if status != 0:
276 error_file = os.path.join (origdir, 'trace-bug-%s.pbm' % id)
277 shutil.copy2 (pbmfile, error_file)
278 msg = """Trace failed on bitmap. Bitmap left in `%s\'
279 Failed command was:
283 Please submit a bugreport to %s development.""" \
284 % (error_file, trace_command (error_file, ''), options.trace_binary)
286 if options.keep_trying:
287 warning (msg)
288 sys.stderr.write ("\nContinuing trace...\n")
289 flag_error ()
290 else:
291 msg = msg + '\nRun mftrace with --keep-trying to produce a font anyway\n'
292 error (msg)
293 else:
294 return 1
296 if status != 0:
297 warning ("Failed, skipping character.\n")
298 return 0
299 else:
300 return 1
302 def make_pbm (filename, outname, char_number):
303 """ Extract bitmap from the PK file FILENAME (absolute) using `gf2pbm'.
304 Return FALSE if the glyph is not valid.
307 command = "%s/gf2pbm -n %d -o %s %s" % (bindir, char_number, outname, filename)
308 status = system (command, ignore_error = 1)
309 return (status == 0)
311 def read_encoding (file):
312 sys.stderr.write (_ ("Using encoding file: `%s'\n") % file)
314 str = open (file).read ()
315 str = re.sub ("%.*", '', str)
316 str = re.sub ("[\n\t \f]+", ' ', str)
317 m = re.search ('/([^ ]+) \[([^\]]+)\] def', str)
318 if not m:
319 error ("Encoding file is invalid")
321 name = m.group (1)
322 cod = m.group (2)
323 cod = re.sub ('[ /]+', ' ', cod)
324 cods = string.split (cod)
326 return (name, cods)
328 def zip_to_pairs (xs):
329 r = []
330 while xs:
331 r.append ((xs[0], xs[1]))
332 xs = xs[2:]
333 return r
335 def unzip_pairs (tups):
336 lst = []
337 while tups:
338 lst = lst + list (tups[0])
339 tups = tups[1:]
340 return lst
342 def autotrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
343 inv_scale = 1000.0 / magnification
345 (size_y, size_x, off_x, off_y) = map (lambda m, s = inv_scale: m * s,
346 bitmap_metrics)
347 ls = open (at_file).readlines ()
348 bbox = (10000, 10000, -10000, -10000)
350 while ls and ls[0] != '*u\n':
351 ls = ls[1:]
353 if ls == []:
354 return (bbox, '')
356 ls = ls[1:]
358 commands = []
361 while ls[0] != '*U\n':
362 ell = ls[0]
363 ls = ls[1:]
365 toks = string.split (ell)
367 if len (toks) < 1:
368 continue
369 cmd = toks[-1]
370 args = map (lambda m, s = inv_scale: s * float (m),
371 toks[:-1])
372 if options.round_to_int:
373 args = zip_to_pairs (map (round, args))
374 else:
375 args = zip_to_pairs (args)
376 commands.append ((cmd, args))
378 expand = {
379 'l': 'rlineto',
380 'm': 'rmoveto',
381 'c': 'rrcurveto',
382 'f': 'closepath',
385 cx = 0
386 cy = size_y - off_y - inv_scale
388 # t1asm seems to fuck up when using sbw. Oh well.
389 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
390 bbox = (10000, 10000, -10000, -10000)
392 for (c, args) in commands:
394 na = []
395 for a in args:
396 (nx, ny) = a
397 if c == 'l' or c == 'c':
398 bbox = update_bbox_with_point (bbox, a)
400 na.append ((nx - cx, ny - cy))
401 (cx, cy) = (nx, ny)
403 a = na
404 c = expand[c]
405 if options.round_to_int:
406 a = map (lambda x: '%d' % int (round (x)),
407 unzip_pairs (a))
408 else:
409 a = map (lambda x: '%d %d div' \
410 % (int (round (x * options.grid_scale/inv_scale)),
411 int (round (options.grid_scale/inv_scale))),
412 unzip_pairs (a))
414 t1_outline = t1_outline + ' %s %s\n' % (string.join (a), c)
416 t1_outline = t1_outline + ' endchar '
417 t1_outline = '{\n %s } |- \n' % t1_outline
419 return (bbox, t1_outline)
421 # FIXME: Cut and paste programming
422 def potrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
423 inv_scale = 1000.0 / magnification
425 (size_y, size_x, off_x, off_y) = map (lambda m,
426 s = inv_scale: m * s,
427 bitmap_metrics)
428 ls = open (at_file).readlines ()
429 bbox = (10000, 10000, -10000, -10000)
431 while ls and ls[0] != '0 setgray\n':
432 ls = ls[1:]
434 if ls == []:
435 return (bbox, '')
436 ls = ls[1:]
437 commands = []
439 while ls and ls[0] != 'grestore\n':
440 ell = ls[0]
441 ls = ls[1:]
443 if ell == 'fill\n':
444 continue
446 toks = string.split (ell)
448 if len (toks) < 1:
449 continue
450 cmd = toks[-1]
451 args = map (lambda m, s = inv_scale: s * float (m),
452 toks[:-1])
453 args = zip_to_pairs (args)
454 commands.append ((cmd, args))
456 # t1asm seems to fuck up when using sbw. Oh well.
457 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
458 bbox = (10000, 10000, -10000, -10000)
460 # Type1 fonts have relative coordinates (doubly relative for
461 # rrcurveto), so must convert moveto and rcurveto.
463 z = (0.0, size_y - off_y - 1.0)
464 for (c, args) in commands:
465 args = map (lambda x: (x[0] * (1.0 / options.grid_scale),
466 x[1] * (1.0 / options.grid_scale)), args)
468 if c == 'moveto':
469 args = [(args[0][0] - z[0], args[0][1] - z[1])]
471 zs = []
472 for a in args:
473 lz = (z[0] + a[0], z[1] + a[1])
474 bbox = update_bbox_with_point (bbox, lz)
475 zs.append (lz)
477 if options.round_to_int:
478 last_discr_z = (int (round (z[0])), int (round (z[1])))
479 else:
480 last_discr_z = (z[0], z[1])
481 args = []
482 for a in zs:
483 if options.round_to_int:
484 a = (int (round (a[0])), int (round (a[1])))
485 else:
486 a = (a[0], a[1])
487 args.append ((a[0] - last_discr_z[0],
488 a[1] - last_discr_z[1]))
490 last_discr_z = a
492 if zs:
493 z = zs[-1]
494 c = { 'rcurveto': 'rrcurveto',
495 'moveto': 'rmoveto',
496 'closepath': 'closepath',
497 'rlineto': 'rlineto'}[c]
499 if c == 'rmoveto':
500 t1_outline += ' closepath '
502 if options.round_to_int:
503 args = map (lambda x: '%d' % int (round (x)),
504 unzip_pairs (args))
505 else:
506 args = map (lambda x: '%d %d div' \
507 % (int (round (x*options.grid_scale/inv_scale)),
508 int (round (options.grid_scale/inv_scale))),
509 unzip_pairs (args))
511 t1_outline = t1_outline + ' %s %s\n' % (string.join (args), c)
513 t1_outline = t1_outline + ' endchar '
514 t1_outline = '{\n %s } |- \n' % t1_outline
516 return (bbox, t1_outline)
518 def read_gf_dims (name, c):
519 str = popen ('%s/gf2pbm -n %d -s %s' % (bindir, c, name)).read ()
520 m = re.search ('size: ([0-9]+)+x([0-9]+), offset: \(([0-9-]+),([0-9-]+)\)', str)
522 return tuple (map (int, m.groups ()))
524 def trace_font (fontname, gf_file, metric, glyphs, encoding,
525 magnification, fontinfo):
526 t1os = []
527 font_bbox = (10000, 10000, -10000, -10000)
529 progress (_ ("Tracing bitmaps..."))
531 if options.verbose:
532 progress ('\n')
533 else:
534 progress (' ')
536 # for single glyph testing.
537 # glyphs = []
538 for a in glyphs:
539 if encoding[a] == ".notavail":
540 continue
541 valid = metric.has_char (a)
542 if not valid:
543 encoding[a] = ".notavail"
544 continue
546 valid = make_pbm (gf_file, 'char.pbm', a)
547 if not valid:
548 encoding[a] = ".notavail"
549 continue
551 (w, h, xo, yo) = read_gf_dims (gf_file, a)
553 if not options.verbose:
554 sys.stderr.write ('[%d' % a)
555 sys.stderr.flush ()
557 # this wants the id, not the filename.
558 success = trace_one ("char.pbm", '%s-%d' % (options.gffile, a))
559 if not success:
560 sys.stderr.write ("(skipping character)]")
561 sys.stderr.flush ()
562 encoding[a] = ".notavail"
563 continue
565 if not options.verbose:
566 sys.stderr.write (']')
567 sys.stderr.flush ()
568 metric_width = metric.get_char (a).width
569 tw = int (round (metric_width / metric.design_size * 1000))
570 (bbox, t1o) = path_to_type1_ops ("char.eps", (h, w, xo, yo),
571 tw, magnification)
573 if t1o == '':
574 encoding[a] = ".notavail"
575 continue
577 font_bbox = update_bbox_with_bbox (font_bbox, bbox)
579 t1os.append ('\n/%s %s ' % (encoding[a], t1o))
581 if not options.verbose:
582 progress ('\n')
583 to_type1 (t1os, font_bbox, fontname, encoding, magnification, fontinfo)
585 def ps_encode_encoding (encoding):
586 str = ' %d array\n0 1 %d {1 index exch /.notdef put} for\n' \
587 % (len (encoding), len (encoding)-1)
589 for i in range (0, len (encoding)):
590 if encoding[i] != ".notavail":
591 str = str + 'dup %d /%s put\n' % (i, encoding[i])
593 return str
596 def gen_unique_id (dict):
597 nm = 'FullName'
598 return 4000000 + (hash (nm) % 1000000)
600 def to_type1 (outlines, bbox, fontname, encoding, magnification, fontinfo):
603 Fill in the header template for the font, append charstrings,
604 and shove result through t1asm
606 template = r"""%%!PS-AdobeFont-1.0: %(FontName)s %(VVV)s.%(WWW)s
607 13 dict begin
608 /FontInfo 16 dict dup begin
609 /version (%(VVV)s.%(WWW)s) readonly def
610 /Notice (%(Notice)s) readonly def
611 /FullName (%(FullName)s) readonly def
612 /FamilyName (%(FamilyName)s) readonly def
613 /Weight (%(Weight)s) readonly def
614 /ItalicAngle %(ItalicAngle)s def
615 /isFixedPitch %(isFixedPitch)s def
616 /UnderlinePosition %(UnderlinePosition)s def
617 /UnderlineThickness %(UnderlineThickness)s def
618 end readonly def
619 /FontName /%(FontName)s def
620 /FontType 1 def
621 /PaintType 0 def
622 /FontMatrix [%(xrevscale)f 0 0 %(yrevscale)f 0 0] readonly def
623 /FontBBox {%(llx)d %(lly)d %(urx)d %(ury)d} readonly def
624 /Encoding %(Encoding)s readonly def
625 currentdict end
626 currentfile eexec
627 dup /Private 20 dict dup begin
628 /-|{string currentfile exch readstring pop}executeonly def
629 /|-{noaccess def}executeonly def
630 /|{noaccess put}executeonly def
631 /lenIV 4 def
632 /password 5839 def
633 /MinFeature {16 16} |-
634 /BlueValues [] |-
635 /OtherSubrs [ {} {} {} {} ] |-
636 /ForceBold false def
637 /Subrs 1 array
638 dup 0 { return } |
640 2 index
641 /CharStrings %(CharStringsLen)d dict dup begin
642 %(CharStrings)s
645 /.notdef { 0 0 hsbw endchar } |-
648 readonly put
649 noaccess put
650 dup/FontName get exch definefont
651 pop mark currentfile closefile
652 cleartomark
654 ## apparently, some fonts end the file with cleartomark. Don't know why.
656 copied_fields = ['FontName', 'FamilyName', 'FullName', 'DesignSize',
657 'ItalicAngle', 'isFixedPitch', 'Weight']
659 vars = {
660 'VVV': '001',
661 'WWW': '001',
662 'Notice': 'Generated from MetaFont bitmap by mftrace %s, http://www.xs4all.nl/~hanwen/mftrace/ ' % program_version,
663 'UnderlinePosition': '-100',
664 'UnderlineThickness': '50',
665 'xrevscale': 1.0/1000.0,
666 'yrevscale': 1.0/1000.0,
667 'llx': bbox[0],
668 'lly': bbox[1],
669 'urx': bbox[2],
670 'ury': bbox[3],
671 'Encoding': ps_encode_encoding (encoding),
673 # need one extra entry for .notdef
674 'CharStringsLen': len (outlines) + 1,
675 'CharStrings': string.join (outlines),
676 'CharBBox': '0 0 0 0',
679 for k in copied_fields:
680 vars[k] = fontinfo[k]
682 open ('mftrace.t1asm', 'w').write (template % vars)
684 def update_bbox_with_point (bbox, pt):
685 (llx, lly, urx, ury) = bbox
686 llx = min (pt[0], llx)
687 lly = min (pt[1], lly)
688 urx = max (pt[0], urx)
689 ury = max (pt[1], ury)
691 return (llx, lly, urx, ury)
693 def update_bbox_with_bbox (bb, dims):
694 (llx, lly, urx, ury) = bb
695 llx = min (llx, dims[0])
696 lly = min (lly, dims[1])
697 urx = max (urx, dims[2])
698 ury = max (ury, dims[3])
700 return (llx, lly, urx, ury)
702 def get_binary (name):
703 search_path = string.split (os.environ['PATH'], ':')
704 for p in search_path:
705 nm = os.path.join (p, name)
706 if os.path.exists (nm):
707 return nm
709 return ''
711 def get_fontforge_command ():
712 fontforge_cmd = ''
713 for ff in ['fontforge', 'pfaedit']:
714 if get_binary(ff):
715 fontforge_cmd = ff
717 stat = 1
718 if fontforge_cmd:
719 stat = system ("%s -usage > pfv 2>&1 " % fontforge_cmd,
720 ignore_error = 1)
722 if stat != 0:
723 warning ("Command `%s -usage' failed. Cannot simplify or convert to TTF.\n" % fontforge_cmd)
725 if fontforge_cmd == 'pfaedit' \
726 and re.search ("-script", open ('pfv').read ()) == None:
727 warning ("pfaedit does not support -script. Install 020215 or later.\nCannot simplify or convert to TTF.\n")
728 return ''
729 return fontforge_cmd
731 def tfm2kpx (tfmname, encoding):
732 kpx_lines = []
733 pl = popen ("tftopl %s" % (tfmname))
735 label_pattern = re.compile (
736 "\A \(LABEL ([DOHC]{1}) ([A-Za-z0-9]*)\)")
737 krn_pattern = re.compile (
738 "\A \(KRN ([DOHC]{1}) ([A-Za-z0-9]*) R (-?[\d\.]+)\)")
740 first = 0
741 second = 0
743 for line in pl.readlines ():
745 label_match = label_pattern.search (line)
746 if not (label_match is None):
747 if label_match.group (1) == "D":
748 first = int (label_match.group (2))
749 elif label_match.group (1) == "O":
750 first = int (label_match.group (2), 8)
751 elif label_match.group (1) == "C":
752 first = ord (label_match.group (2))
754 krn_match = krn_pattern.search (line)
755 if not (krn_match is None):
756 if krn_match.group (1) == "D":
757 second = int (krn_match.group (2))
758 elif krn_match.group (1) == "O":
759 second = int (krn_match.group (2), 8)
760 elif krn_match.group (1) == "C":
761 second = ord (krn_match.group (2))
763 krn = round (float (krn_match.group (3)) * 1000)
765 if (encoding[first] != '.notavail' and
766 encoding[first] != '.notdef' and
767 encoding[second] != '.notavail' and
768 encoding[second] != '.notdef'):
770 kpx_lines.append ("KPX %s %s %d\n" % (
771 encoding[first], encoding[second], krn))
773 return kpx_lines
775 def get_afm (t1_path, tfmname, encoding, out_path):
776 afm_stream = popen ("printafm %s" % (t1_path))
777 afm_lines = []
778 kpx_lines = tfm2kpx (tfmname, encoding)
780 for line in afm_stream.readlines ():
781 afm_lines.append (line)
783 if re.match (r"^EndCharMetrics", line, re.I):
784 afm_lines.append ("StartKernData\n")
785 afm_lines.append ("StartKernPairs %d\n" % len (kpx_lines))
787 for kpx_line in kpx_lines:
788 afm_lines.append (kpx_line)
790 afm_lines.append ("EndKernPairs\n")
791 afm_lines.append ("EndKernData\n")
793 progress (_ ("Writing metrics to `%s'... ") % out_path)
794 afm_file = open (out_path, 'w')
795 afm_file.writelines (afm_lines)
796 afm_file.flush ()
797 afm_file.close ()
799 progress ('\n')
801 def assemble_font (fontname, format, is_raw):
802 ext = '.' + format
803 asm_opt = '--pfa'
805 if format == 'pfb':
806 asm_opt = '--pfb'
808 if is_raw:
809 ext = ext + '.raw'
811 outname = fontname + ext
813 progress (_ ("Assembling raw font to `%s'... ") % outname)
814 if options.verbose:
815 progress ('\n')
816 system ('t1asm %s mftrace.t1asm %s' % (asm_opt, shell_escape_filename (outname)))
817 progress ('\n')
818 return outname
820 def make_outputs (fontname, formats, encoding):
822 run pfaedit to convert to other formats
825 ff_needed = 0
826 ff_command = ""
828 if (options.simplify or options.round_to_int or 'ttf' in formats or 'svg' in formats):
829 ff_needed = 1
830 if ff_needed:
831 ff_command = get_fontforge_command ()
833 if ff_needed and ff_command:
834 raw_name = assemble_font (fontname, 'pfa', 1)
836 simplify_cmd = ''
837 if options.round_to_int:
838 simplify_cmd = 'RoundToInt ();'
839 generate_cmds = ''
840 for f in formats:
841 generate_cmds += 'Generate("%s");' % (fontname + '.' + f)
843 if options.simplify:
844 simplify_cmd ='''SelectAll ();
846 AddExtrema();
847 Simplify ();
848 %(simplify_cmd)s
849 AutoHint ();''' % vars()
851 pe_script = ('''#!/usr/bin/env %(ff_command)s
852 Open ($1);
853 MergeKern($2);
854 %(simplify_cmd)s
855 %(generate_cmds)s
856 Quit (0);
857 ''' % vars())
859 open ('to-ttf.pe', 'w').write (pe_script)
860 if options.verbose:
861 print 'Fontforge script', pe_script
862 system ("%s -script to-ttf.pe %s %s" % (ff_command,
863 shell_escape_filename (raw_name), shell_escape_filename (options.tfm_file)))
864 else:
865 t1_path = ''
867 if ('pfa' in formats):
868 t1_path = assemble_font (fontname, 'pfa', 0)
870 if ('pfb' in formats):
871 t1_path = assemble_font (fontname, 'pfb', 0)
873 if (t1_path != '' and 'afm' in formats):
874 get_afm (t1_path, options.tfm_file, encoding, fontname + '.afm')
877 def getenv (var, default):
878 if os.environ.has_key (var):
879 return os.environ[var]
880 else:
881 return default
883 def gen_pixel_font (filename, metric, magnification):
885 Generate a GF file for FILENAME, such that `magnification'*mfscale
886 (default 1000 * 1.0) pixels fit on the designsize.
888 base_dpi = 1200
890 size = metric.design_size
892 size_points = size * 1/72.27 * base_dpi
894 mag = magnification / size_points
896 prod = mag * base_dpi
897 try:
898 open ('%s.%dgf' % (filename, prod))
899 except IOError:
901 ## MFINPUTS/TFMFONTS take kpathsea specific values;
902 ## we should analyse them any further.
903 os.environ['MFINPUTS'] = '%s:%s' % (origdir,
904 getenv ('MFINPUTS', ''))
905 os.environ['TFMFONTS'] = '%s:%s' % (origdir,
906 getenv ('TFMINPUTS', ''))
908 progress (_ ("Running Metafont..."))
910 cmdstr = r"mf '\mode:=lexmarks; mag:=%f; nonstopmode; input %s'" % (mag, filename)
911 if not options.verbose:
912 cmdstr = cmdstr + ' 1>/dev/null 2>/dev/null'
913 st = system (cmdstr, ignore_error = 1)
914 progress ('\n')
916 logfile = '%s.log' % filename
917 log = ''
918 prod = 0
919 if os.path.exists (logfile):
920 log = open (logfile).read ()
921 m = re.search ('Output written on %s.([0-9]+)gf' % re.escape (filename), log)
922 prod = int (m.group (1))
924 if st:
925 sys.stderr.write ('\n\nMetafont failed. Excerpt from the log file: \n\n*****')
926 m = re.search ("\n!", log)
927 start = m.start (0)
928 short_log = log[start:start+200]
929 sys.stderr.write (short_log)
930 sys.stderr.write ('\n*****\n')
931 if re.search ('Arithmetic overflow', log):
932 sys.stderr.write ("""
934 Apparently, some numbers overflowed. Try using --magnification with a
935 lower number. (Current magnification: %d)
936 """ % magnification)
938 if not options.keep_trying or prod == 0:
939 sys.exit (1)
940 else:
941 sys.stderr.write ('\n\nTrying to proceed despite of the Metafont errors...\n')
945 return "%s.%d" % (filename, prod)
947 def parse_command_line ():
948 p = optparse.OptionParser (version="""mftrace @VERSION@
950 This program is free software. It is covered by the GNU General Public
951 License and you are welcome to change it and/or distribute copies of it
952 under certain conditions. Invoke as `mftrace --warranty' for more
953 information.
955 Copyright (c) 2005--2006 by
956 Han-Wen Nienhuys <hanwen@xs4all.nl>
958 """)
959 p.usage = "mftrace [OPTION]... FILE..."
960 p.description = _ ("Generate Type1 or TrueType font from Metafont source.")
962 p.add_option ('-k', '--keep',
963 action="store_true",
964 dest="keep_temp_dir",
965 help=_ ("Keep all output in directory %s.dir") % program_name)
966 p.add_option ('','--magnification',
967 dest="magnification",
968 metavar="MAG",
969 default=1000.0,
970 type="float",
971 help=_("Set magnification for MF to MAG (default: 1000)"))
972 p.add_option ('-V', '--verbose',
973 action='store_true',
974 default=False,
975 help=_ ("Be verbose"))
976 p.add_option ('-f', '--formats',
977 action="append",
978 dest="formats",
979 default=[],
980 help=_("Which formats to generate (choices: AFM, PFA, PFB, TTF, SVG)"))
981 p.add_option ('', '--simplify',
982 action="store_true",
983 dest="simplify",
984 help=_ ("Simplify using fontforge"))
985 p.add_option ('', '--gffile',
986 dest="gffile",
987 help= _("Use gf FILE instead of running Metafont"))
988 p.add_option ('-I', '--include',
989 dest="include_dirs",
990 action="append",
991 default=[],
992 help=_("Add to path for searching files"))
993 p.add_option ('','--glyphs',
994 default=[],
995 action="append",
996 dest="glyphs",
997 metavar="LIST",
998 help= _('Process only these glyphs. LIST is comma separated'))
999 p.add_option ('', '--tfmfile',
1000 metavar='FILE',
1001 action='store',
1002 dest='tfm_file')
1004 p.add_option ('-e', '--encoding',
1005 metavar="FILE",
1006 action='store',
1007 dest="encoding_file",
1008 default="",
1009 help= _ ("Use encoding file FILE"))
1010 p.add_option ('','--keep-trying',
1011 dest='keep_trying',
1012 default=False,
1013 action="store_true",
1014 help= _ ("Don't stop if tracing fails"))
1015 p.add_option ('-w', '--warranty',
1016 action="store_true",
1017 help=_ ("show warranty and copyright"))
1018 p.add_option ('','--dos-kpath',
1019 dest="dos_kpath",
1020 help=_("try to use Miktex kpsewhich"))
1021 p.add_option ('', '--potrace',
1022 dest='potrace',
1023 help=_ ("Use potrace"))
1024 p.add_option ('', '--autotrace',
1025 dest='autotrace',
1026 help=_ ("Use autotrace"))
1027 p.add_option ('', '--no-afm',
1028 action='store_false',
1029 dest="read_afm",
1030 default=True,
1031 help=_("Don't read AFM file"))
1032 p.add_option ('','--noround',
1033 action="store_false",
1034 dest='round_to_int',
1035 default=True,
1036 help= ("Do not round coordinates of control points to integer values (use with --grid)"))
1037 p.add_option ('','--grid',
1038 metavar='SCALE',
1039 dest='grid_scale',
1040 type='float',
1041 default = 1.0,
1042 help=_ ("Set reciprocal grid size in em units"))
1043 p.add_option ('-D','--define',
1044 metavar="SYMBOL=VALUE",
1045 dest="defs",
1046 default=[],
1047 action='append',help=_("Set the font info SYMBOL to VALUE"))
1049 global options
1050 (options, files) = p.parse_args ()
1052 if not files:
1053 sys.stderr.write ('Need argument on command line \n')
1054 p.print_help ()
1055 sys.exit (2)
1057 if options.warranty :
1058 warranty ()
1059 sys.exit (0)
1061 options.font_info = {}
1062 for d in options.defs:
1063 kv = d.split('=')
1064 if len (kv) == 1:
1065 options.font_info[kv] = 'true'
1066 elif len (kv) > 1:
1067 options.font_info[kv[0]] = '='.join (kv[1:])
1069 def comma_sepped_to_list (x):
1070 fs = []
1071 for f in x:
1072 fs += f.lower ().split (',')
1073 return fs
1075 options.formats = comma_sepped_to_list (options.formats)
1077 new_glyphs = []
1078 for r in options.glyphs:
1079 new_glyphs += r.split (',')
1080 options.glyphs = new_glyphs
1082 glyph_range = []
1083 for r in options.glyphs:
1084 glyph_subrange = map (int, string.split (r, '-'))
1085 if len (glyph_subrange) == 2 and glyph_subrange[0] < glyph_subrange[1] + 1:
1086 glyph_range += range (glyph_subrange[0], glyph_subrange[1] + 1)
1087 else:
1088 glyph_range.append (glyph_subrange[0])
1090 options.glyphs = glyph_range
1092 options.trace_binary = ''
1093 if options.potrace:
1094 options.trace_binary = 'potrace'
1095 elif options.autotrace:
1096 options.trace_binary = 'autotrace'
1098 if options.formats == []:
1099 options.formats = ['pfa']
1103 global trace_command
1104 global path_to_type1_ops
1106 stat = os.system ('potrace --version > /dev/null 2>&1 ')
1107 if options.trace_binary != 'autotrace' and stat == 0:
1108 options.trace_binary = 'potrace'
1110 trace_command = potrace_command
1111 path_to_type1_ops = potrace_path_to_type1_ops
1113 stat = os.system ('autotrace --version > /dev/null 2>&1 ')
1114 if options.trace_binary != 'potrace' and stat == 0:
1115 options.trace_binary = 'autotrace'
1116 trace_command = autotrace_command
1117 path_to_type1_ops = autotrace_path_to_type1_ops
1119 if not options.trace_binary:
1120 error (_ ("No tracing program found.\nInstall potrace or autotrace."))
1122 return files
1125 def derive_font_name (family, fullname):
1126 fullname = re.sub (family, '', fullname)
1127 family = re.sub (' ', '', family)
1128 fullname = re.sub ('Oldstyle Figures', 'OsF', fullname)
1129 fullname = re.sub ('Small Caps', 'SC', fullname)
1130 fullname = re.sub ('[Mm]edium', '', fullname)
1131 fullname = re.sub ('[^A-Za-z0-9]', '', fullname)
1132 return '%s-%s' % (family, fullname)
1134 def cm_guess_font_info (filename, fontinfo):
1135 # urg.
1136 filename = re.sub ("cm(.*)tt", r"cmtt\1", filename)
1137 m = re.search ("([0-9]+)$", filename)
1138 design_size = ''
1139 if m:
1140 design_size = int (m.group (1))
1141 fontinfo['DesignSize'] = design_size
1143 prefixes = [("cmtt", "Computer Modern Typewriter"),
1144 ("cmvtt", "Computer Modern Variable Width Typewriter"),
1145 ("cmss", "Computer Modern Sans"),
1146 ("cm", "Computer Modern")]
1148 family = ''
1149 for (k, v) in prefixes:
1150 if re.search (k, filename):
1151 family = v
1152 if k == 'cmtt':
1153 fontinfo['isFixedPitch'] = 'true'
1154 filename = re.sub (k, '', filename)
1155 break
1157 # shapes
1158 prefixes = [("r", "Roman"),
1159 ("mi", "Math italic"),
1160 ("u", "Unslanted italic"),
1161 ("sl", "Oblique"),
1162 ("csc", "Small Caps"),
1163 ("ex", "Math extension"),
1164 ("ti", "Text italic"),
1165 ("i", "Italic")]
1166 shape = ''
1167 for (k, v) in prefixes:
1168 if re.search (k, filename):
1169 shape = v
1170 filename = re.sub (k, '', filename)
1172 prefixes = [("b", "Bold"),
1173 ("d", "Demi bold")]
1174 weight = 'Regular'
1175 for (k, v) in prefixes:
1176 if re.search (k, filename):
1177 weight = v
1178 filename = re.sub (k, '', filename)
1180 prefixes = [("c", "Condensed"),
1181 ("x", "Extended")]
1182 stretch = ''
1183 for (k, v) in prefixes:
1184 if re.search (k, filename):
1185 stretch = v
1186 filename = re.sub (k, '', filename)
1188 fontinfo['ItalicAngle'] = 0
1189 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1190 a = -14
1191 if re.search ("Sans", family):
1192 a = -12
1194 fontinfo ["ItalicAngle"] = a
1196 fontinfo['Weight'] = weight
1197 fontinfo['FamilyName'] = family
1198 full = '%s %s %s %s %dpt' \
1199 % (family, shape, weight, stretch, design_size)
1200 full = re.sub (" +", ' ', full)
1202 fontinfo['FullName'] = full
1203 fontinfo['FontName'] = derive_font_name (family, full)
1205 return fontinfo
1207 def ec_guess_font_info (filename, fontinfo):
1208 design_size = 12
1209 m = re.search ("([0-9]+)$", filename)
1210 if m:
1211 design_size = int (m.group (1))
1212 fontinfo['DesignSize'] = design_size
1214 prefixes = [("ecss", "European Computer Modern Sans"),
1215 ("ectt", "European Computer Modern Typewriter"),
1216 ("ec", "European Computer Modern")]
1218 family = ''
1219 for (k, v) in prefixes:
1220 if re.search (k, filename):
1221 if k == 'ectt':
1222 fontinfo['isFixedPitch'] = 'true'
1223 family = v
1224 filename = re.sub (k, '', filename)
1225 break
1227 # shapes
1228 prefixes = [("r", "Roman"),
1229 ("mi", "Math italic"),
1230 ("u", "Unslanted italic"),
1231 ("sl", "Oblique"),
1232 ("cc", "Small caps"),
1233 ("ex", "Math extension"),
1234 ("ti", "Italic"),
1235 ("i", "Italic")]
1237 shape = ''
1238 for (k, v) in prefixes:
1239 if re.search (k, filename):
1240 shape = v
1241 filename = re.sub (k, '', filename)
1243 prefixes = [("b", "Bold"),
1244 ("d", "Demi bold")]
1245 weight = 'Regular'
1246 for (k, v) in prefixes:
1247 if re.search (k, filename):
1248 weight = v
1249 filename = re.sub (k, '', filename)
1251 prefixes = [("c", "Condensed"),
1252 ("x", "Extended")]
1253 stretch = ''
1254 for (k, v) in prefixes:
1255 if re.search (k, filename):
1256 stretch = v
1257 filename = re.sub (k, '', filename)
1259 fontinfo['ItalicAngle'] = 0
1260 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1261 a = -14
1262 if re.search ("Sans", family):
1263 a = -12
1265 fontinfo ["ItalicAngle"] = a
1267 fontinfo['Weight'] = weight
1268 fontinfo['FamilyName'] = family
1269 full = '%s %s %s %s %dpt' \
1270 % (family, shape, weight, stretch, design_size)
1271 full = re.sub (" +", ' ', full)
1273 fontinfo['FontName'] = derive_font_name (family, full)
1274 fontinfo['FullName'] = full
1276 return fontinfo
1279 def guess_fontinfo (filename):
1280 fi = {
1281 'FontName': filename,
1282 'FamilyName': filename,
1283 'Weight': 'Regular',
1284 'ItalicAngle': 0,
1285 'DesignSize' : 12,
1286 'isFixedPitch' : 'false',
1287 'FullName': filename,
1290 if re.search ('^cm', filename):
1291 fi.update (cm_guess_font_info (filename, fi))
1292 elif re.search ("^ec", filename):
1293 fi.update (ec_guess_font_info (filename, fi))
1294 elif options.read_afm:
1295 global afmfile
1296 if not afmfile:
1297 afmfile = find_file (filename + '.afm')
1299 if afmfile:
1300 afmfile = os.path.abspath (afmfile)
1301 afm_struct = afm.read_afm_file (afmfile)
1302 fi.update (afm_struct.__dict__)
1303 return fi
1304 else:
1305 sys.stderr.write ("Warning: no extra font information for this font.\n"
1306 + "Consider writing a XX_guess_font_info() routine.\n")
1308 return fi
1310 def do_file (filename):
1311 encoding_file = options.encoding_file
1312 global include_dirs
1313 include_dirs = options.include_dirs
1314 include_dirs.append (origdir)
1316 basename = strip_extension (filename, '.mf')
1317 progress (_ ("Font `%s'..." % basename))
1318 progress ('\n')
1320 ## setup encoding
1321 if encoding_file and not os.path.exists (encoding_file):
1322 encoding_file = find_file (encoding_file)
1323 elif encoding_file:
1324 encoding_file = os.path.abspath (encoding_file)
1326 ## setup TFM
1327 if options.tfm_file:
1328 options.tfm_file = os.path.abspath (options.tfm_file)
1329 else:
1330 tfm_try = find_file (basename + '.tfm')
1331 if tfm_try:
1332 options.tfm_file = tfm_try
1334 if not os.environ.has_key ("MFINPUTS"):
1335 os.environ["MFINPUTS"] = os.getcwd () + ":"
1337 ## must change dir before calling mktextfm.
1338 if options.keep_temp_dir:
1339 def nop():
1340 pass
1341 setup_temp (os.path.join (os.getcwd (), program_name + '.dir'))
1342 temp_dir.clean = nop
1343 else:
1344 setup_temp (None)
1346 if options.verbose:
1347 progress ('Temporary directory is `%s\'\n' % temp_dir)
1349 if not options.tfm_file:
1350 options.tfm_file = popen ("mktextfm %s 2>/dev/null" % shell_escape_filename (basename)).read ()
1351 if options.tfm_file:
1352 options.tfm_file = options.tfm_file.strip ()
1353 options.tfm_file = os.path.abspath (options.tfm_file)
1355 if not options.tfm_file:
1356 error (_ ("Can not find a TFM file to match `%s'") % basename)
1358 metric = tfm.read_tfm_file (options.tfm_file)
1360 fontinfo = guess_fontinfo (basename)
1361 fontinfo.update (options.font_info)
1363 if not encoding_file:
1364 codingfile = 'tex256.enc'
1365 if not coding_dict.has_key (metric.coding):
1366 sys.stderr.write ("Unknown encoding `%s'; assuming tex256.\n" % metric.coding)
1367 else:
1368 codingfile = coding_dict[metric.coding]
1370 encoding_file = find_file (codingfile)
1371 if not encoding_file:
1372 error (_ ("can't find file `%s'" % codingfile))
1374 (enc_name, encoding) = read_encoding (encoding_file)
1376 if not len (options.glyphs):
1377 options.glyphs = range (0, len (encoding))
1379 if not options.gffile:
1380 # run mf
1381 base = gen_pixel_font (basename, metric, options.magnification)
1382 options.gffile = base + 'gf'
1383 else:
1384 options.gffile = find_file (options.gffile)
1386 # the heart of the program:
1387 trace_font (basename, options.gffile, metric, options.glyphs, encoding,
1388 options.magnification, fontinfo)
1390 make_outputs (basename, options.formats, encoding)
1391 for format in options.formats:
1392 shutil.copy2 (basename + '.' + format, origdir)
1394 os.chdir (origdir)
1400 afmfile = ''
1401 backend_options = getenv ('MFTRACE_BACKEND_OPTIONS', '')
1402 def main ():
1403 files = parse_command_line ()
1404 identify (sys.stderr)
1406 for filename in files:
1407 do_file (filename)
1408 sys.exit (exit_value)
1410 if __name__ =='__main__':
1411 main()