Add support for potrace 1.9: it inserts 'restore' and '%%EOF'.
[mftrace.git] / mftrace.py
blob390464931be4b5ec42f633b6175202c9320e62cf
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 and exit with an error status of 1.
105 RETURN VALUE
107 None
111 errorport.write (_ ("error: ") + s + '\n')
112 errorport.write (_ ("Exiting ...") + '\n')
113 sys.exit(1)
115 temp_dir = None
116 class TempDirectory:
117 def __init__ (self, name=None):
118 import tempfile
119 if name:
120 if not os.path.isdir (name):
121 os.makedirs (name)
122 self.dir = name
123 else:
124 self.dir = tempfile.mkdtemp ()
126 os.chdir (self.dir)
128 def clean (self):
129 import shutil
130 shutil.rmtree (self.dir)
131 def __del__ (self):
132 self.clean ()
133 def __call__ (self):
134 return self.dir
135 def __repr__ (self):
136 return self.dir
137 def __str__ (self):
138 return self.dir
140 def setup_temp (name):
141 global temp_dir
142 if not temp_dir:
143 temp_dir = TempDirectory (name)
144 return temp_dir ()
146 def popen (cmd, mode = 'r', ignore_error = 0):
147 if options.verbose:
148 progress (_ ("Opening pipe `%s\'") % cmd)
149 pipe = os.popen (cmd, mode)
150 if options.verbose:
151 progress ('\n')
152 return pipe
154 def system (cmd, ignore_error = 0):
155 """Run CMD. If IGNORE_ERROR is set, don't complain when CMD returns non zero.
157 RETURN VALUE
159 Exit status of CMD
162 if options.verbose:
163 progress (_ ("Invoking `%s\'\n") % cmd)
164 st = os.system (cmd)
165 if st:
166 name = re.match ('[ \t]*([^ \t]*)', cmd).group (1)
167 msg = name + ': ' + _ ("command exited with value %d") % st
168 if ignore_error:
169 warning (msg + ' ' + _ ("(ignored)") + ' ')
170 else:
171 error (msg)
172 if options.verbose:
173 progress ('\n')
174 return st
176 def strip_extension (f, ext):
177 (p, e) = os.path.splitext (f)
178 if e == ext:
179 e = ''
180 return p + e
183 ################################################################
184 # END Library
188 options = None
189 exit_value = 0
190 backend_options = ''
191 program_name = 'mftrace'
192 temp_dir = None
193 program_version = '@VERSION@'
194 origdir = os.getcwd ()
196 coding_dict = {
198 # from TeTeX
199 'TeX typewriter text': '09fbbfac.enc', # cmtt10
200 'TeX math symbols': '10037936.enc', # cmbsy
201 'ASCII caps and digits': '1b6d048e', # cminch
202 'TeX math italic': 'aae443f0.enc', # cmmi10
203 'TeX extended ASCII': 'd9b29452.enc',
204 'TeX text': 'f7b6d320.enc',
205 'TeX text without f-ligatures': '0ef0afca.enc',
206 'Extended TeX Font Encoding - Latin': 'tex256.enc',
208 # LilyPond.
209 'fetaBraces': 'feta-braces-a.enc',
210 'fetaNumber': 'feta-nummer10.enc',
211 'fetaMusic': 'feta20.enc',
212 'parmesanMusic': 'parmesan20.enc',
216 def find_file (nm):
217 for d in include_dirs:
218 p = os.path.join (d, nm)
219 try:
220 open (p)
221 return os.path.abspath (p)
222 except IOError:
223 pass
225 p = popen ('kpsewhich %s' % shell_escape_filename (nm)).read ()
226 p = p.strip ()
228 if options.dos_kpath:
229 orig = p
230 p = string.lower (p)
231 p = re.sub ('^([a-z]):', '/cygdrive/\\1', p)
232 p = re.sub ('\\\\', '/', p)
233 sys.stderr.write ("Got `%s' from kpsewhich, using `%s'\n" % (orig, p))
234 return p
237 def flag_error ():
238 global exit_value
239 exit_value = 1
241 ################################################################
242 # TRACING.
243 ################################################################
245 def autotrace_command (fn, opts):
246 opts = " " + opts + " --background-color=FFFFFF --output-format=eps --input-format=pbm "
247 return options.trace_binary + opts + backend_options \
248 + " --output-file=char.eps %s " % fn
250 def potrace_command (fn, opts):
251 return options.trace_binary + opts \
252 + ' -u %d ' % options.grid_scale \
253 + backend_options \
254 + " -q -c --eps --output=char.eps %s " % (fn)
256 trace_command = None
257 path_to_type1_ops = None
259 def trace_one (pbmfile, id):
261 Run tracer, do error handling
264 status = system (trace_command (pbmfile, ''), 1)
266 if status == 2:
267 sys.stderr.write ("\nUser interrupt. Exiting\n")
268 sys.exit (2)
270 if status == 0 and options.keep_temp_dir:
271 shutil.copy2 (pbmfile, '%s.pbm' % id)
272 shutil.copy2 ('char.eps', '%s.eps' % id)
274 if status != 0:
275 error_file = os.path.join (origdir, 'trace-bug-%s.pbm' % id)
276 shutil.copy2 (pbmfile, error_file)
277 msg = """Trace failed on bitmap. Bitmap left in `%s\'
278 Failed command was:
282 Please submit a bugreport to %s development.""" \
283 % (error_file, trace_command (error_file, ''), options.trace_binary)
285 if options.keep_trying:
286 warning (msg)
287 sys.stderr.write ("\nContinuing trace...\n")
288 flag_error ()
289 else:
290 msg = msg + '\nRun mftrace with --keep-trying to produce a font anyway\n'
291 error (msg)
292 else:
293 return 1
295 if status != 0:
296 warning ("Failed, skipping character.\n")
297 return 0
298 else:
299 return 1
301 def make_pbm (filename, outname, char_number):
302 """ Extract bitmap from the PK file FILENAME (absolute) using `gf2pbm'.
303 Return FALSE if the glyph is not valid.
306 command = "%s/gf2pbm -n %d -o %s %s" % (bindir, char_number, outname, filename)
307 status = system (command, ignore_error = 1)
308 return (status == 0)
310 def read_encoding (file):
311 sys.stderr.write (_ ("Using encoding file: `%s'\n") % file)
313 str = open (file).read ()
314 str = re.sub ("%.*", '', str)
315 str = re.sub ("[\n\t \f]+", ' ', str)
316 m = re.search ('/([^ ]+) \[([^\]]+)\] def', str)
317 if not m:
318 error ("Encoding file is invalid")
320 name = m.group (1)
321 cod = m.group (2)
322 cod = re.sub ('[ /]+', ' ', cod)
323 cods = string.split (cod)
325 return (name, cods)
327 def zip_to_pairs (xs):
328 r = []
329 while xs:
330 r.append ((xs[0], xs[1]))
331 xs = xs[2:]
332 return r
334 def unzip_pairs (tups):
335 lst = []
336 while tups:
337 lst = lst + list (tups[0])
338 tups = tups[1:]
339 return lst
341 def autotrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
342 inv_scale = 1000.0 / magnification
344 (size_y, size_x, off_x, off_y) = map (lambda m, s = inv_scale: m * s,
345 bitmap_metrics)
346 ls = open (at_file).readlines ()
347 bbox = (10000, 10000, -10000, -10000)
349 while ls and ls[0] != '*u\n':
350 ls = ls[1:]
352 if ls == []:
353 return (bbox, '')
355 ls = ls[1:]
357 commands = []
360 while ls[0] != '*U\n':
361 ell = ls[0]
362 ls = ls[1:]
364 toks = string.split (ell)
366 if len (toks) < 1:
367 continue
368 cmd = toks[-1]
369 args = map (lambda m, s = inv_scale: s * float (m),
370 toks[:-1])
371 if options.round_to_int:
372 args = zip_to_pairs (map (round, args))
373 else:
374 args = zip_to_pairs (args)
375 commands.append ((cmd, args))
377 expand = {
378 'l': 'rlineto',
379 'm': 'rmoveto',
380 'c': 'rrcurveto',
381 'f': 'closepath',
384 cx = 0
385 cy = size_y - off_y - inv_scale
387 # t1asm seems to fuck up when using sbw. Oh well.
388 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
389 bbox = (10000, 10000, -10000, -10000)
391 for (c, args) in commands:
393 na = []
394 for a in args:
395 (nx, ny) = a
396 if c == 'l' or c == 'c':
397 bbox = update_bbox_with_point (bbox, a)
399 na.append ((nx - cx, ny - cy))
400 (cx, cy) = (nx, ny)
402 a = na
403 c = expand[c]
404 if options.round_to_int:
405 a = map (lambda x: '%d' % int (round (x)),
406 unzip_pairs (a))
407 else:
408 a = map (lambda x: '%d %d div' \
409 % (int (round (x * options.grid_scale/inv_scale)),
410 int (round (options.grid_scale/inv_scale))),
411 unzip_pairs (a))
413 t1_outline = t1_outline + ' %s %s\n' % (string.join (a), c)
415 t1_outline = t1_outline + ' endchar '
416 t1_outline = '{\n %s } |- \n' % t1_outline
418 return (bbox, t1_outline)
420 # FIXME: Cut and paste programming
421 def potrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
422 inv_scale = 1000.0 / magnification
424 (size_y, size_x, off_x, off_y) = map (lambda m,
425 s = inv_scale: m * s,
426 bitmap_metrics)
427 ls = open (at_file).readlines ()
428 bbox = (10000, 10000, -10000, -10000)
430 while ls and ls[0] != '0 setgray\n':
431 ls = ls[1:]
433 if ls == []:
434 return (bbox, '')
435 ls = ls[1:]
436 commands = []
438 while ls and ls[0] != 'grestore\n':
439 ell = ls[0]
440 ls = ls[1:]
442 if ell == 'fill\n':
443 continue
445 toks = string.split (ell)
447 if len (toks) < 1:
448 continue
449 cmd = toks[-1]
450 args = map (lambda m, s = inv_scale: s * float (m),
451 toks[:-1])
452 args = zip_to_pairs (args)
453 commands.append ((cmd, args))
455 # t1asm seems to fuck up when using sbw. Oh well.
456 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
457 bbox = (10000, 10000, -10000, -10000)
459 # Type1 fonts have relative coordinates (doubly relative for
460 # rrcurveto), so must convert moveto and rcurveto.
462 z = (0.0, size_y - off_y - 1.0)
463 for (c, args) in commands:
464 args = map (lambda x: (x[0] * (1.0 / options.grid_scale),
465 x[1] * (1.0 / options.grid_scale)), args)
467 if c == 'moveto':
468 args = [(args[0][0] - z[0], args[0][1] - z[1])]
470 zs = []
471 for a in args:
472 lz = (z[0] + a[0], z[1] + a[1])
473 bbox = update_bbox_with_point (bbox, lz)
474 zs.append (lz)
476 if options.round_to_int:
477 last_discr_z = (int (round (z[0])), int (round (z[1])))
478 else:
479 last_discr_z = (z[0], z[1])
480 args = []
481 for a in zs:
482 if options.round_to_int:
483 a = (int (round (a[0])), int (round (a[1])))
484 else:
485 a = (a[0], a[1])
486 args.append ((a[0] - last_discr_z[0],
487 a[1] - last_discr_z[1]))
489 last_discr_z = a
491 if zs:
492 z = zs[-1]
494 c = {
495 'closepath': 'closepath',
496 'moveto': 'rmoveto',
497 'rcurveto': 'rrcurveto',
498 # Potrace 1.9
499 'restore': '',
500 'rlineto': 'rlineto',
501 '%%EOF': '',
502 }[c]
504 if c == 'rmoveto':
505 t1_outline += ' closepath '
507 if options.round_to_int:
508 args = map (lambda x: '%d' % int (round (x)),
509 unzip_pairs (args))
510 else:
511 args = map (lambda x: '%d %d div' \
512 % (int (round (x*options.grid_scale/inv_scale)),
513 int (round (options.grid_scale/inv_scale))),
514 unzip_pairs (args))
516 t1_outline = t1_outline + ' %s %s\n' % (string.join (args), c)
518 t1_outline = t1_outline + ' endchar '
519 t1_outline = '{\n %s } |- \n' % t1_outline
521 return (bbox, t1_outline)
523 def read_gf_dims (name, c):
524 str = popen ('%s/gf2pbm -n %d -s %s' % (bindir, c, name)).read ()
525 m = re.search ('size: ([0-9]+)+x([0-9]+), offset: \(([0-9-]+),([0-9-]+)\)', str)
527 return tuple (map (int, m.groups ()))
529 def trace_font (fontname, gf_file, metric, glyphs, encoding,
530 magnification, fontinfo):
531 t1os = []
532 font_bbox = (10000, 10000, -10000, -10000)
534 progress (_ ("Tracing bitmaps..."))
536 if options.verbose:
537 progress ('\n')
538 else:
539 progress (' ')
541 # for single glyph testing.
542 # glyphs = []
543 for a in glyphs:
544 if encoding[a] == ".notavail":
545 continue
546 valid = metric.has_char (a)
547 if not valid:
548 encoding[a] = ".notavail"
549 continue
551 valid = make_pbm (gf_file, 'char.pbm', a)
552 if not valid:
553 encoding[a] = ".notavail"
554 continue
556 (w, h, xo, yo) = read_gf_dims (gf_file, a)
558 if not options.verbose:
559 sys.stderr.write ('[%d' % a)
560 sys.stderr.flush ()
562 # this wants the id, not the filename.
563 success = trace_one ("char.pbm", '%s-%d' % (options.gffile, a))
564 if not success:
565 sys.stderr.write ("(skipping character)]")
566 sys.stderr.flush ()
567 encoding[a] = ".notavail"
568 continue
570 if not options.verbose:
571 sys.stderr.write (']')
572 sys.stderr.flush ()
573 metric_width = metric.get_char (a).width
574 tw = int (round (metric_width / metric.design_size * 1000))
575 (bbox, t1o) = path_to_type1_ops ("char.eps", (h, w, xo, yo),
576 tw, magnification)
578 if t1o == '':
579 encoding[a] = ".notavail"
580 continue
582 font_bbox = update_bbox_with_bbox (font_bbox, bbox)
584 t1os.append ('\n/%s %s ' % (encoding[a], t1o))
586 if not options.verbose:
587 progress ('\n')
588 to_type1 (t1os, font_bbox, fontname, encoding, magnification, fontinfo)
590 def ps_encode_encoding (encoding):
591 str = ' %d array\n0 1 %d {1 index exch /.notdef put} for\n' \
592 % (len (encoding), len (encoding)-1)
594 for i in range (0, len (encoding)):
595 if encoding[i] != ".notavail":
596 str = str + 'dup %d /%s put\n' % (i, encoding[i])
598 return str
601 def gen_unique_id (dict):
602 nm = 'FullName'
603 return 4000000 + (hash (nm) % 1000000)
605 def to_type1 (outlines, bbox, fontname, encoding, magnification, fontinfo):
608 Fill in the header template for the font, append charstrings,
609 and shove result through t1asm
611 template = r"""%%!PS-AdobeFont-1.0: %(FontName)s %(VVV)s.%(WWW)s
612 13 dict begin
613 /FontInfo 16 dict dup begin
614 /version (%(VVV)s.%(WWW)s) readonly def
615 /Notice (%(Notice)s) readonly def
616 /FullName (%(FullName)s) readonly def
617 /FamilyName (%(FamilyName)s) readonly def
618 /Weight (%(Weight)s) readonly def
619 /ItalicAngle %(ItalicAngle)s def
620 /isFixedPitch %(isFixedPitch)s def
621 /UnderlinePosition %(UnderlinePosition)s def
622 /UnderlineThickness %(UnderlineThickness)s def
623 end readonly def
624 /FontName /%(FontName)s def
625 /FontType 1 def
626 /PaintType 0 def
627 /FontMatrix [%(xrevscale)f 0 0 %(yrevscale)f 0 0] readonly def
628 /FontBBox {%(llx)d %(lly)d %(urx)d %(ury)d} readonly def
629 /Encoding %(Encoding)s readonly def
630 currentdict end
631 currentfile eexec
632 dup /Private 20 dict dup begin
633 /-|{string currentfile exch readstring pop}executeonly def
634 /|-{noaccess def}executeonly def
635 /|{noaccess put}executeonly def
636 /lenIV 4 def
637 /password 5839 def
638 /MinFeature {16 16} |-
639 /BlueValues [] |-
640 /OtherSubrs [ {} {} {} {} ] |-
641 /ForceBold false def
642 /Subrs 1 array
643 dup 0 { return } |
645 2 index
646 /CharStrings %(CharStringsLen)d dict dup begin
647 %(CharStrings)s
650 /.notdef { 0 0 hsbw endchar } |-
653 readonly put
654 noaccess put
655 dup/FontName get exch definefont
656 pop mark currentfile closefile
657 cleartomark
659 ## apparently, some fonts end the file with cleartomark. Don't know why.
661 copied_fields = ['FontName', 'FamilyName', 'FullName', 'DesignSize',
662 'ItalicAngle', 'isFixedPitch', 'Weight']
664 vars = {
665 'VVV': '001',
666 'WWW': '001',
667 'Notice': 'Generated from MetaFont bitmap by mftrace %s, http://www.xs4all.nl/~hanwen/mftrace/ ' % program_version,
668 'UnderlinePosition': '-100',
669 'UnderlineThickness': '50',
670 'xrevscale': 1.0/1000.0,
671 'yrevscale': 1.0/1000.0,
672 'llx': bbox[0],
673 'lly': bbox[1],
674 'urx': bbox[2],
675 'ury': bbox[3],
676 'Encoding': ps_encode_encoding (encoding),
678 # need one extra entry for .notdef
679 'CharStringsLen': len (outlines) + 1,
680 'CharStrings': string.join (outlines),
681 'CharBBox': '0 0 0 0',
684 for k in copied_fields:
685 vars[k] = fontinfo[k]
687 open ('mftrace.t1asm', 'w').write (template % vars)
689 def update_bbox_with_point (bbox, pt):
690 (llx, lly, urx, ury) = bbox
691 llx = min (pt[0], llx)
692 lly = min (pt[1], lly)
693 urx = max (pt[0], urx)
694 ury = max (pt[1], ury)
696 return (llx, lly, urx, ury)
698 def update_bbox_with_bbox (bb, dims):
699 (llx, lly, urx, ury) = bb
700 llx = min (llx, dims[0])
701 lly = min (lly, dims[1])
702 urx = max (urx, dims[2])
703 ury = max (ury, dims[3])
705 return (llx, lly, urx, ury)
707 def get_binary (name):
708 search_path = string.split (os.environ['PATH'], ':')
709 for p in search_path:
710 nm = os.path.join (p, name)
711 if os.path.exists (nm):
712 return nm
714 return ''
716 def get_fontforge_command ():
717 fontforge_cmd = ''
718 for ff in ['fontforge', 'pfaedit']:
719 if get_binary(ff):
720 fontforge_cmd = ff
722 stat = 1
723 if fontforge_cmd:
724 stat = system ("%s -usage > pfv 2>&1 " % fontforge_cmd,
725 ignore_error = 1)
727 if stat != 0:
728 warning ("Command `%s -usage' failed. Cannot simplify or convert to TTF.\n" % fontforge_cmd)
729 return ''
731 if fontforge_cmd == 'pfaedit' \
732 and re.search ("-script", open ('pfv').read ()) == None:
733 warning ("pfaedit does not support -script. Install 020215 or later.\nCannot simplify or convert to TTF.\n")
734 return ''
735 return fontforge_cmd
737 def tfm2kpx (tfmname, encoding):
738 kpx_lines = []
739 pl = popen ("tftopl %s" % (tfmname))
741 label_pattern = re.compile (
742 "\A \(LABEL ([DOHC]{1}) ([A-Za-z0-9]*)\)")
743 krn_pattern = re.compile (
744 "\A \(KRN ([DOHC]{1}) ([A-Za-z0-9]*) R (-?[\d\.]+)\)")
746 first = 0
747 second = 0
749 for line in pl.readlines ():
751 label_match = label_pattern.search (line)
752 if not (label_match is None):
753 if label_match.group (1) == "D":
754 first = int (label_match.group (2))
755 elif label_match.group (1) == "O":
756 first = int (label_match.group (2), 8)
757 elif label_match.group (1) == "C":
758 first = ord (label_match.group (2))
760 krn_match = krn_pattern.search (line)
761 if not (krn_match is None):
762 if krn_match.group (1) == "D":
763 second = int (krn_match.group (2))
764 elif krn_match.group (1) == "O":
765 second = int (krn_match.group (2), 8)
766 elif krn_match.group (1) == "C":
767 second = ord (krn_match.group (2))
769 krn = round (float (krn_match.group (3)) * 1000)
771 if (encoding[first] != '.notavail' and
772 encoding[first] != '.notdef' and
773 encoding[second] != '.notavail' and
774 encoding[second] != '.notdef'):
776 kpx_lines.append ("KPX %s %s %d\n" % (
777 encoding[first], encoding[second], krn))
779 return kpx_lines
781 def get_afm (t1_path, tfmname, encoding, out_path):
782 afm_stream = popen ("printafm %s" % (t1_path))
783 afm_lines = []
784 kpx_lines = tfm2kpx (tfmname, encoding)
786 for line in afm_stream.readlines ():
787 afm_lines.append (line)
789 if re.match (r"^EndCharMetrics", line, re.I):
790 afm_lines.append ("StartKernData\n")
791 afm_lines.append ("StartKernPairs %d\n" % len (kpx_lines))
793 for kpx_line in kpx_lines:
794 afm_lines.append (kpx_line)
796 afm_lines.append ("EndKernPairs\n")
797 afm_lines.append ("EndKernData\n")
799 progress (_ ("Writing metrics to `%s'... ") % out_path)
800 afm_file = open (out_path, 'w')
801 afm_file.writelines (afm_lines)
802 afm_file.flush ()
803 afm_file.close ()
805 progress ('\n')
807 def assemble_font (fontname, format, is_raw):
808 ext = '.' + format
809 asm_opt = '--pfa'
811 if format == 'pfb':
812 asm_opt = '--pfb'
814 if is_raw:
815 ext = ext + '.raw'
817 outname = fontname + ext
819 progress (_ ("Assembling raw font to `%s'... ") % outname)
820 if options.verbose:
821 progress ('\n')
822 system ('t1asm %s mftrace.t1asm %s' % (asm_opt, shell_escape_filename (outname)))
823 progress ('\n')
824 return outname
826 def make_outputs (fontname, formats, encoding):
828 run pfaedit to convert to other formats
831 ff_needed = 0
832 ff_command = ""
834 if (options.simplify or options.round_to_int or 'ttf' in formats or 'svg' in formats or 'afm' in formats):
835 ff_needed = 1
836 if ff_needed:
837 ff_command = get_fontforge_command ()
839 if ff_needed and ff_command:
840 raw_name = assemble_font (fontname, 'pfa', 1)
842 simplify_cmd = ''
843 if options.round_to_int:
844 simplify_cmd = 'RoundToInt ();'
845 generate_cmds = ''
846 for f in formats:
847 generate_cmds += 'Generate("%s");' % (fontname + '.' + f)
849 if options.simplify:
850 simplify_cmd ='''SelectAll ();
852 AddExtrema();
853 Simplify ();
854 %(simplify_cmd)s
855 AutoHint ();''' % vars()
857 pe_script = ('''#!/usr/bin/env %(ff_command)s
858 Open ($1);
859 MergeKern($2);
860 %(simplify_cmd)s
861 %(generate_cmds)s
862 Quit (0);
863 ''' % vars())
865 open ('to-ttf.pe', 'w').write (pe_script)
866 if options.verbose:
867 print 'Fontforge script', pe_script
868 system ("%s -script to-ttf.pe %s %s" % (ff_command,
869 shell_escape_filename (raw_name), shell_escape_filename (options.tfm_file)))
870 elif ff_needed and (options.simplify or options.round_to_int or 'ttf' in formats or 'svg' in formats):
871 error(_ ("fontforge is not installed; could not perform requested command"))
872 else:
873 t1_path = ''
875 if ('pfa' in formats):
876 t1_path = assemble_font (fontname, 'pfa', 0)
878 if ('pfb' in formats):
879 t1_path = assemble_font (fontname, 'pfb', 0)
881 if (t1_path != '' and 'afm' in formats):
882 if get_binary("printafm"):
883 get_afm (t1_path, options.tfm_file, encoding, fontname + '.afm')
884 else:
885 error(_ ("Neither fontforge nor ghostscript is not installed; could not perform requested command"))
888 def getenv (var, default):
889 if os.environ.has_key (var):
890 return os.environ[var]
891 else:
892 return default
894 def gen_pixel_font (filename, metric, magnification):
896 Generate a GF file for FILENAME, such that `magnification'*mfscale
897 (default 1000 * 1.0) pixels fit on the designsize.
899 base_dpi = 1200
901 size = metric.design_size
903 size_points = size * 1/72.27 * base_dpi
905 mag = magnification / size_points
907 prod = mag * base_dpi
908 try:
909 open ('%s.%dgf' % (filename, prod))
910 except IOError:
912 ## MFINPUTS/TFMFONTS take kpathsea specific values;
913 ## we should analyse them any further.
914 os.environ['MFINPUTS'] = '%s:%s' % (origdir,
915 getenv ('MFINPUTS', ''))
916 os.environ['TFMFONTS'] = '%s:%s' % (origdir,
917 getenv ('TFMINPUTS', ''))
919 progress (_ ("Running Metafont..."))
921 cmdstr = r"mf '\mode:=lexmarks; mag:=%f; nonstopmode; input %s'" % (mag, filename)
922 if not options.verbose:
923 cmdstr = cmdstr + ' 1>/dev/null 2>/dev/null'
924 st = system (cmdstr, ignore_error = 1)
925 progress ('\n')
927 logfile = '%s.log' % filename
928 log = ''
929 prod = 0
930 if os.path.exists (logfile):
931 log = open (logfile).read ()
932 m = re.search ('Output written on %s.([0-9]+)gf' % re.escape (filename), log)
933 prod = int (m.group (1))
935 if st:
936 sys.stderr.write ('\n\nMetafont failed. Excerpt from the log file: \n\n*****')
937 m = re.search ("\n!", log)
938 start = m.start (0)
939 short_log = log[start:start+200]
940 sys.stderr.write (short_log)
941 sys.stderr.write ('\n*****\n')
942 if re.search ('Arithmetic overflow', log):
943 sys.stderr.write ("""
945 Apparently, some numbers overflowed. Try using --magnification with a
946 lower number. (Current magnification: %d)
947 """ % magnification)
949 if not options.keep_trying or prod == 0:
950 sys.exit (1)
951 else:
952 sys.stderr.write ('\n\nTrying to proceed despite of the Metafont errors...\n')
956 return "%s.%d" % (filename, prod)
958 def parse_command_line ():
959 p = optparse.OptionParser (version="""mftrace @VERSION@
961 This program is free software. It is covered by the GNU General Public
962 License and you are welcome to change it and/or distribute copies of it
963 under certain conditions. Invoke as `mftrace --warranty' for more
964 information.
966 Copyright (c) 2005--2006 by
967 Han-Wen Nienhuys <hanwen@xs4all.nl>
969 """)
970 p.usage = "mftrace [OPTION]... FILE..."
971 p.description = _ ("Generate Type1 or TrueType font from Metafont source.")
973 p.add_option ('-k', '--keep',
974 action="store_true",
975 dest="keep_temp_dir",
976 help=_ ("Keep all output in directory %s.dir") % program_name)
977 p.add_option ('','--magnification',
978 dest="magnification",
979 metavar="MAG",
980 default=1000.0,
981 type="float",
982 help=_("Set magnification for MF to MAG (default: 1000)"))
983 p.add_option ('-V', '--verbose',
984 action='store_true',
985 default=False,
986 help=_ ("Be verbose"))
987 p.add_option ('-f', '--formats',
988 action="append",
989 dest="formats",
990 default=[],
991 help=_("Which formats to generate (choices: AFM, PFA, PFB, TTF, SVG)"))
992 p.add_option ('', '--simplify',
993 action="store_true",
994 dest="simplify",
995 help=_ ("Simplify using fontforge"))
996 p.add_option ('', '--gffile',
997 dest="gffile",
998 help= _("Use gf FILE instead of running Metafont"))
999 p.add_option ('-I', '--include',
1000 dest="include_dirs",
1001 action="append",
1002 default=[],
1003 help=_("Add to path for searching files"))
1004 p.add_option ('','--glyphs',
1005 default=[],
1006 action="append",
1007 dest="glyphs",
1008 metavar="LIST",
1009 help= _('Process only these glyphs. LIST is comma separated'))
1010 p.add_option ('', '--tfmfile',
1011 metavar='FILE',
1012 action='store',
1013 dest='tfm_file')
1015 p.add_option ('-e', '--encoding',
1016 metavar="FILE",
1017 action='store',
1018 dest="encoding_file",
1019 default="",
1020 help= _ ("Use encoding file FILE"))
1021 p.add_option ('','--keep-trying',
1022 dest='keep_trying',
1023 default=False,
1024 action="store_true",
1025 help= _ ("Don't stop if tracing fails"))
1026 p.add_option ('-w', '--warranty',
1027 action="store_true",
1028 help=_ ("show warranty and copyright"))
1029 p.add_option ('','--dos-kpath',
1030 dest="dos_kpath",
1031 help=_("try to use Miktex kpsewhich"))
1032 p.add_option ('', '--potrace',
1033 dest='potrace',
1034 help=_ ("Use potrace"))
1035 p.add_option ('', '--autotrace',
1036 dest='autotrace',
1037 help=_ ("Use autotrace"))
1038 p.add_option ('', '--no-afm',
1039 action='store_false',
1040 dest="read_afm",
1041 default=True,
1042 help=_("Don't read AFM file"))
1043 p.add_option ('','--noround',
1044 action="store_false",
1045 dest='round_to_int',
1046 default=True,
1047 help= ("Do not round coordinates of control points to integer values (use with --grid)"))
1048 p.add_option ('','--grid',
1049 metavar='SCALE',
1050 dest='grid_scale',
1051 type='float',
1052 default = 1.0,
1053 help=_ ("Set reciprocal grid size in em units"))
1054 p.add_option ('-D','--define',
1055 metavar="SYMBOL=VALUE",
1056 dest="defs",
1057 default=[],
1058 action='append',help=_("Set the font info SYMBOL to VALUE"))
1060 global options
1061 (options, files) = p.parse_args ()
1063 if not files:
1064 sys.stderr.write ('Need argument on command line \n')
1065 p.print_help ()
1066 sys.exit (2)
1068 if options.warranty :
1069 warranty ()
1070 sys.exit (0)
1072 options.font_info = {}
1073 for d in options.defs:
1074 kv = d.split('=')
1075 if len (kv) == 1:
1076 options.font_info[kv] = 'true'
1077 elif len (kv) > 1:
1078 options.font_info[kv[0]] = '='.join (kv[1:])
1080 def comma_sepped_to_list (x):
1081 fs = []
1082 for f in x:
1083 fs += f.lower ().split (',')
1084 return fs
1086 options.formats = comma_sepped_to_list (options.formats)
1088 new_glyphs = []
1089 for r in options.glyphs:
1090 new_glyphs += r.split (',')
1091 options.glyphs = new_glyphs
1093 glyph_range = []
1094 for r in options.glyphs:
1095 glyph_subrange = map (int, string.split (r, '-'))
1096 if len (glyph_subrange) == 2 and glyph_subrange[0] < glyph_subrange[1] + 1:
1097 glyph_range += range (glyph_subrange[0], glyph_subrange[1] + 1)
1098 else:
1099 glyph_range.append (glyph_subrange[0])
1101 options.glyphs = glyph_range
1103 options.trace_binary = ''
1104 if options.potrace:
1105 options.trace_binary = 'potrace'
1106 elif options.autotrace:
1107 options.trace_binary = 'autotrace'
1109 if options.formats == []:
1110 options.formats = ['pfa']
1114 global trace_command
1115 global path_to_type1_ops
1117 stat = os.system ('potrace --version > /dev/null 2>&1 ')
1118 if options.trace_binary != 'autotrace' and stat == 0:
1119 options.trace_binary = 'potrace'
1121 trace_command = potrace_command
1122 path_to_type1_ops = potrace_path_to_type1_ops
1123 elif options.trace_binary == 'potrace' and stat != 0:
1124 error (_ ("Could not run potrace; have you installed it?"))
1126 stat = os.system ('autotrace --version > /dev/null 2>&1 ')
1127 if options.trace_binary != 'potrace' and stat == 0:
1128 options.trace_binary = 'autotrace'
1129 trace_command = autotrace_command
1130 path_to_type1_ops = autotrace_path_to_type1_ops
1131 elif options.trace_binary == 'autotrace' and stat != 0:
1132 error (_ ("Could not run autotrace; have you installed it?"))
1134 if not options.trace_binary:
1135 error (_ ("No tracing program found.\nInstall potrace or autotrace."))
1137 return files
1140 def derive_font_name (family, fullname):
1141 fullname = re.sub (family, '', fullname)
1142 family = re.sub (' ', '', family)
1143 fullname = re.sub ('Oldstyle Figures', 'OsF', fullname)
1144 fullname = re.sub ('Small Caps', 'SC', fullname)
1145 fullname = re.sub ('[Mm]edium', '', fullname)
1146 fullname = re.sub ('[^A-Za-z0-9]', '', fullname)
1147 return '%s-%s' % (family, fullname)
1149 def cm_guess_font_info (filename, fontinfo):
1150 # urg.
1151 filename = re.sub ("cm(.*)tt", r"cmtt\1", filename)
1152 m = re.search ("([0-9]+)$", filename)
1153 design_size = ''
1154 if m:
1155 design_size = int (m.group (1))
1156 fontinfo['DesignSize'] = design_size
1158 prefixes = [("cmtt", "Computer Modern Typewriter"),
1159 ("cmvtt", "Computer Modern Variable Width Typewriter"),
1160 ("cmss", "Computer Modern Sans"),
1161 ("cm", "Computer Modern")]
1163 family = ''
1164 for (k, v) in prefixes:
1165 if re.search (k, filename):
1166 family = v
1167 if k == 'cmtt':
1168 fontinfo['isFixedPitch'] = 'true'
1169 filename = re.sub (k, '', filename)
1170 break
1172 # shapes
1173 prefixes = [("r", "Roman"),
1174 ("mi", "Math italic"),
1175 ("u", "Unslanted italic"),
1176 ("sl", "Oblique"),
1177 ("csc", "Small Caps"),
1178 ("ex", "Math extension"),
1179 ("ti", "Text italic"),
1180 ("i", "Italic")]
1181 shape = ''
1182 for (k, v) in prefixes:
1183 if re.search (k, filename):
1184 shape = v
1185 filename = re.sub (k, '', filename)
1187 prefixes = [("b", "Bold"),
1188 ("d", "Demi bold")]
1189 weight = 'Regular'
1190 for (k, v) in prefixes:
1191 if re.search (k, filename):
1192 weight = v
1193 filename = re.sub (k, '', filename)
1195 prefixes = [("c", "Condensed"),
1196 ("x", "Extended")]
1197 stretch = ''
1198 for (k, v) in prefixes:
1199 if re.search (k, filename):
1200 stretch = v
1201 filename = re.sub (k, '', filename)
1203 fontinfo['ItalicAngle'] = 0
1204 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1205 a = -14
1206 if re.search ("Sans", family):
1207 a = -12
1209 fontinfo ["ItalicAngle"] = a
1211 fontinfo['Weight'] = weight
1212 fontinfo['FamilyName'] = family
1213 full = '%s %s %s %s %dpt' \
1214 % (family, shape, weight, stretch, design_size)
1215 full = re.sub (" +", ' ', full)
1217 fontinfo['FullName'] = full
1218 fontinfo['FontName'] = derive_font_name (family, full)
1220 return fontinfo
1222 def ec_guess_font_info (filename, fontinfo):
1223 design_size = 12
1224 m = re.search ("([0-9]+)$", filename)
1225 if m:
1226 design_size = int (m.group (1))
1227 fontinfo['DesignSize'] = design_size
1229 prefixes = [("ecss", "European Computer Modern Sans"),
1230 ("ectt", "European Computer Modern Typewriter"),
1231 ("ec", "European Computer Modern")]
1233 family = ''
1234 for (k, v) in prefixes:
1235 if re.search (k, filename):
1236 if k == 'ectt':
1237 fontinfo['isFixedPitch'] = 'true'
1238 family = v
1239 filename = re.sub (k, '', filename)
1240 break
1242 # shapes
1243 prefixes = [("r", "Roman"),
1244 ("mi", "Math italic"),
1245 ("u", "Unslanted italic"),
1246 ("sl", "Oblique"),
1247 ("cc", "Small caps"),
1248 ("ex", "Math extension"),
1249 ("ti", "Italic"),
1250 ("i", "Italic")]
1252 shape = ''
1253 for (k, v) in prefixes:
1254 if re.search (k, filename):
1255 shape = v
1256 filename = re.sub (k, '', filename)
1258 prefixes = [("b", "Bold"),
1259 ("d", "Demi bold")]
1260 weight = 'Regular'
1261 for (k, v) in prefixes:
1262 if re.search (k, filename):
1263 weight = v
1264 filename = re.sub (k, '', filename)
1266 prefixes = [("c", "Condensed"),
1267 ("x", "Extended")]
1268 stretch = ''
1269 for (k, v) in prefixes:
1270 if re.search (k, filename):
1271 stretch = v
1272 filename = re.sub (k, '', filename)
1274 fontinfo['ItalicAngle'] = 0
1275 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1276 a = -14
1277 if re.search ("Sans", family):
1278 a = -12
1280 fontinfo ["ItalicAngle"] = a
1282 fontinfo['Weight'] = weight
1283 fontinfo['FamilyName'] = family
1284 full = '%s %s %s %s %dpt' \
1285 % (family, shape, weight, stretch, design_size)
1286 full = re.sub (" +", ' ', full)
1288 fontinfo['FontName'] = derive_font_name (family, full)
1289 fontinfo['FullName'] = full
1291 return fontinfo
1294 def guess_fontinfo (filename):
1295 fi = {
1296 'FontName': filename,
1297 'FamilyName': filename,
1298 'Weight': 'Regular',
1299 'ItalicAngle': 0,
1300 'DesignSize' : 12,
1301 'isFixedPitch' : 'false',
1302 'FullName': filename,
1305 if re.search ('^cm', filename):
1306 fi.update (cm_guess_font_info (filename, fi))
1307 elif re.search ("^ec", filename):
1308 fi.update (ec_guess_font_info (filename, fi))
1309 elif options.read_afm:
1310 global afmfile
1311 if not afmfile:
1312 afmfile = find_file (filename + '.afm')
1314 if afmfile:
1315 afmfile = os.path.abspath (afmfile)
1316 afm_struct = afm.read_afm_file (afmfile)
1317 fi.update (afm_struct.__dict__)
1318 return fi
1319 else:
1320 sys.stderr.write ("Warning: no extra font information for this font.\n"
1321 + "Consider writing a XX_guess_font_info() routine.\n")
1323 return fi
1325 def do_file (filename):
1326 encoding_file = options.encoding_file
1327 global include_dirs
1328 include_dirs = options.include_dirs
1329 include_dirs.append (origdir)
1331 basename = strip_extension (filename, '.mf')
1332 progress (_ ("Font `%s'..." % basename))
1333 progress ('\n')
1335 ## setup encoding
1336 if encoding_file and not os.path.exists (encoding_file):
1337 encoding_file = find_file (encoding_file)
1338 elif encoding_file:
1339 encoding_file = os.path.abspath (encoding_file)
1341 ## setup TFM
1342 if options.tfm_file:
1343 options.tfm_file = os.path.abspath (options.tfm_file)
1344 else:
1345 tfm_try = find_file (basename + '.tfm')
1346 if tfm_try:
1347 options.tfm_file = tfm_try
1349 if not os.environ.has_key ("MFINPUTS"):
1350 os.environ["MFINPUTS"] = os.getcwd () + ":"
1352 ## must change dir before calling mktextfm.
1353 if options.keep_temp_dir:
1354 def nop():
1355 pass
1356 setup_temp (os.path.join (os.getcwd (), program_name + '.dir'))
1357 temp_dir.clean = nop
1358 else:
1359 setup_temp (None)
1361 if options.verbose:
1362 progress ('Temporary directory is `%s\'\n' % temp_dir)
1364 if not options.tfm_file:
1365 options.tfm_file = popen ("mktextfm %s 2>/dev/null" % shell_escape_filename (basename)).read ()
1366 if options.tfm_file:
1367 options.tfm_file = options.tfm_file.strip ()
1368 options.tfm_file = os.path.abspath (options.tfm_file)
1370 if not options.tfm_file:
1371 error (_ ("Can not find a TFM file to match `%s'") % basename)
1373 metric = tfm.read_tfm_file (options.tfm_file)
1375 fontinfo = guess_fontinfo (basename)
1376 fontinfo.update (options.font_info)
1378 if not encoding_file:
1379 codingfile = 'tex256.enc'
1380 if not coding_dict.has_key (metric.coding):
1381 sys.stderr.write ("Unknown encoding `%s'; assuming tex256.\n" % metric.coding)
1382 else:
1383 codingfile = coding_dict[metric.coding]
1385 encoding_file = find_file (codingfile)
1386 if not encoding_file:
1387 error (_ ("can't find file `%s'" % codingfile))
1389 (enc_name, encoding) = read_encoding (encoding_file)
1391 if not len (options.glyphs):
1392 options.glyphs = range (0, len (encoding))
1394 if not options.gffile:
1395 # run mf
1396 base = gen_pixel_font (basename, metric, options.magnification)
1397 options.gffile = base + 'gf'
1398 else:
1399 options.gffile = find_file (options.gffile)
1401 # the heart of the program:
1402 trace_font (basename, options.gffile, metric, options.glyphs, encoding,
1403 options.magnification, fontinfo)
1405 make_outputs (basename, options.formats, encoding)
1406 for format in options.formats:
1407 shutil.copy2 (basename + '.' + format, origdir)
1409 os.chdir (origdir)
1415 afmfile = ''
1416 backend_options = getenv ('MFTRACE_BACKEND_OPTIONS', '')
1417 def main ():
1418 files = parse_command_line ()
1419 identify (sys.stderr)
1421 for filename in files:
1422 do_file (filename)
1423 sys.exit (exit_value)
1425 if __name__ =='__main__':
1426 main()