Merge git+ssh://repo.or.cz/srv/git/mftrace
[mftrace.git] / mftrace.py
blob49e7f04055593b6b53b6de771a041bbf85719848
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]
493 c = { 'rcurveto': 'rrcurveto',
494 'moveto': 'rmoveto',
495 'closepath': 'closepath',
496 'rlineto': 'rlineto'}[c]
498 if c == 'rmoveto':
499 t1_outline += ' closepath '
501 if options.round_to_int:
502 args = map (lambda x: '%d' % int (round (x)),
503 unzip_pairs (args))
504 else:
505 args = map (lambda x: '%d %d div' \
506 % (int (round (x*options.grid_scale/inv_scale)),
507 int (round (options.grid_scale/inv_scale))),
508 unzip_pairs (args))
510 t1_outline = t1_outline + ' %s %s\n' % (string.join (args), c)
512 t1_outline = t1_outline + ' endchar '
513 t1_outline = '{\n %s } |- \n' % t1_outline
515 return (bbox, t1_outline)
517 def read_gf_dims (name, c):
518 str = popen ('%s/gf2pbm -n %d -s %s' % (bindir, c, name)).read ()
519 m = re.search ('size: ([0-9]+)+x([0-9]+), offset: \(([0-9-]+),([0-9-]+)\)', str)
521 return tuple (map (int, m.groups ()))
523 def trace_font (fontname, gf_file, metric, glyphs, encoding,
524 magnification, fontinfo):
525 t1os = []
526 font_bbox = (10000, 10000, -10000, -10000)
528 progress (_ ("Tracing bitmaps..."))
530 if options.verbose:
531 progress ('\n')
532 else:
533 progress (' ')
535 # for single glyph testing.
536 # glyphs = []
537 for a in glyphs:
538 if encoding[a] == ".notavail":
539 continue
540 valid = metric.has_char (a)
541 if not valid:
542 encoding[a] = ".notavail"
543 continue
545 valid = make_pbm (gf_file, 'char.pbm', a)
546 if not valid:
547 encoding[a] = ".notavail"
548 continue
550 (w, h, xo, yo) = read_gf_dims (gf_file, a)
552 if not options.verbose:
553 sys.stderr.write ('[%d' % a)
554 sys.stderr.flush ()
556 # this wants the id, not the filename.
557 success = trace_one ("char.pbm", '%s-%d' % (options.gffile, a))
558 if not success:
559 sys.stderr.write ("(skipping character)]")
560 sys.stderr.flush ()
561 encoding[a] = ".notavail"
562 continue
564 if not options.verbose:
565 sys.stderr.write (']')
566 sys.stderr.flush ()
567 metric_width = metric.get_char (a).width
568 tw = int (round (metric_width / metric.design_size * 1000))
569 (bbox, t1o) = path_to_type1_ops ("char.eps", (h, w, xo, yo),
570 tw, magnification)
572 if t1o == '':
573 encoding[a] = ".notavail"
574 continue
576 font_bbox = update_bbox_with_bbox (font_bbox, bbox)
578 t1os.append ('\n/%s %s ' % (encoding[a], t1o))
580 if not options.verbose:
581 progress ('\n')
582 to_type1 (t1os, font_bbox, fontname, encoding, magnification, fontinfo)
584 def ps_encode_encoding (encoding):
585 str = ' %d array\n0 1 %d {1 index exch /.notdef put} for\n' \
586 % (len (encoding), len (encoding)-1)
588 for i in range (0, len (encoding)):
589 if encoding[i] != ".notavail":
590 str = str + 'dup %d /%s put\n' % (i, encoding[i])
592 return str
595 def gen_unique_id (dict):
596 nm = 'FullName'
597 return 4000000 + (hash (nm) % 1000000)
599 def to_type1 (outlines, bbox, fontname, encoding, magnification, fontinfo):
602 Fill in the header template for the font, append charstrings,
603 and shove result through t1asm
605 template = r"""%%!PS-AdobeFont-1.0: %(FontName)s %(VVV)s.%(WWW)s
606 13 dict begin
607 /FontInfo 16 dict dup begin
608 /version (%(VVV)s.%(WWW)s) readonly def
609 /Notice (%(Notice)s) readonly def
610 /FullName (%(FullName)s) readonly def
611 /FamilyName (%(FamilyName)s) readonly def
612 /Weight (%(Weight)s) readonly def
613 /ItalicAngle %(ItalicAngle)s def
614 /isFixedPitch %(isFixedPitch)s def
615 /UnderlinePosition %(UnderlinePosition)s def
616 /UnderlineThickness %(UnderlineThickness)s def
617 end readonly def
618 /FontName /%(FontName)s def
619 /FontType 1 def
620 /PaintType 0 def
621 /FontMatrix [%(xrevscale)f 0 0 %(yrevscale)f 0 0] readonly def
622 /FontBBox {%(llx)d %(lly)d %(urx)d %(ury)d} readonly def
623 /Encoding %(Encoding)s readonly def
624 currentdict end
625 currentfile eexec
626 dup /Private 20 dict dup begin
627 /-|{string currentfile exch readstring pop}executeonly def
628 /|-{noaccess def}executeonly def
629 /|{noaccess put}executeonly def
630 /lenIV 4 def
631 /password 5839 def
632 /MinFeature {16 16} |-
633 /BlueValues [] |-
634 /OtherSubrs [ {} {} {} {} ] |-
635 /ForceBold false def
636 /Subrs 1 array
637 dup 0 { return } |
639 2 index
640 /CharStrings %(CharStringsLen)d dict dup begin
641 %(CharStrings)s
644 /.notdef { 0 0 hsbw endchar } |-
647 readonly put
648 noaccess put
649 dup/FontName get exch definefont
650 pop mark currentfile closefile
651 cleartomark
653 ## apparently, some fonts end the file with cleartomark. Don't know why.
655 copied_fields = ['FontName', 'FamilyName', 'FullName', 'DesignSize',
656 'ItalicAngle', 'isFixedPitch', 'Weight']
658 vars = {
659 'VVV': '001',
660 'WWW': '001',
661 'Notice': 'Generated from MetaFont bitmap by mftrace %s, http://www.xs4all.nl/~hanwen/mftrace/ ' % program_version,
662 'UnderlinePosition': '-100',
663 'UnderlineThickness': '50',
664 'xrevscale': 1.0/1000.0,
665 'yrevscale': 1.0/1000.0,
666 'llx': bbox[0],
667 'lly': bbox[1],
668 'urx': bbox[2],
669 'ury': bbox[3],
670 'Encoding': ps_encode_encoding (encoding),
672 # need one extra entry for .notdef
673 'CharStringsLen': len (outlines) + 1,
674 'CharStrings': string.join (outlines),
675 'CharBBox': '0 0 0 0',
678 for k in copied_fields:
679 vars[k] = fontinfo[k]
681 open ('mftrace.t1asm', 'w').write (template % vars)
683 def update_bbox_with_point (bbox, pt):
684 (llx, lly, urx, ury) = bbox
685 llx = min (pt[0], llx)
686 lly = min (pt[1], lly)
687 urx = max (pt[0], urx)
688 ury = max (pt[1], ury)
690 return (llx, lly, urx, ury)
692 def update_bbox_with_bbox (bb, dims):
693 (llx, lly, urx, ury) = bb
694 llx = min (llx, dims[0])
695 lly = min (lly, dims[1])
696 urx = max (urx, dims[2])
697 ury = max (ury, dims[3])
699 return (llx, lly, urx, ury)
701 def get_binary (name):
702 search_path = string.split (os.environ['PATH'], ':')
703 for p in search_path:
704 nm = os.path.join (p, name)
705 if os.path.exists (nm):
706 return nm
708 return ''
710 def get_fontforge_command ():
711 fontforge_cmd = ''
712 for ff in ['fontforge', 'pfaedit']:
713 if get_binary(ff):
714 fontforge_cmd = ff
716 stat = 1
717 if fontforge_cmd:
718 stat = system ("%s -usage > pfv 2>&1 " % fontforge_cmd,
719 ignore_error = 1)
721 if stat != 0:
722 warning ("Command `%s -usage' failed. Cannot simplify or convert to TTF.\n" % fontforge_cmd)
723 return ''
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 or 'afm' 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 elif ff_needed and (options.simplify or options.round_to_int or 'ttf' in formats or 'svg' in formats):
865 error(_ ("fontforge is not installed; could not perform requested command"))
866 else:
867 t1_path = ''
869 if ('pfa' in formats):
870 t1_path = assemble_font (fontname, 'pfa', 0)
872 if ('pfb' in formats):
873 t1_path = assemble_font (fontname, 'pfb', 0)
875 if (t1_path != '' and 'afm' in formats):
876 if get_binary("printafm"):
877 get_afm (t1_path, options.tfm_file, encoding, fontname + '.afm')
878 else:
879 error(_ ("Neither fontforge nor ghostscript is not installed; could not perform requested command"))
882 def getenv (var, default):
883 if os.environ.has_key (var):
884 return os.environ[var]
885 else:
886 return default
888 def gen_pixel_font (filename, metric, magnification):
890 Generate a GF file for FILENAME, such that `magnification'*mfscale
891 (default 1000 * 1.0) pixels fit on the designsize.
893 base_dpi = 1200
895 size = metric.design_size
897 size_points = size * 1/72.27 * base_dpi
899 mag = magnification / size_points
901 prod = mag * base_dpi
902 try:
903 open ('%s.%dgf' % (filename, prod))
904 except IOError:
906 ## MFINPUTS/TFMFONTS take kpathsea specific values;
907 ## we should analyse them any further.
908 os.environ['MFINPUTS'] = '%s:%s' % (origdir,
909 getenv ('MFINPUTS', ''))
910 os.environ['TFMFONTS'] = '%s:%s' % (origdir,
911 getenv ('TFMINPUTS', ''))
913 progress (_ ("Running Metafont..."))
915 cmdstr = r"mf '\mode:=lexmarks; mag:=%f; nonstopmode; input %s'" % (mag, filename)
916 if not options.verbose:
917 cmdstr = cmdstr + ' 1>/dev/null 2>/dev/null'
918 st = system (cmdstr, ignore_error = 1)
919 progress ('\n')
921 logfile = '%s.log' % filename
922 log = ''
923 prod = 0
924 if os.path.exists (logfile):
925 log = open (logfile).read ()
926 m = re.search ('Output written on %s.([0-9]+)gf' % re.escape (filename), log)
927 prod = int (m.group (1))
929 if st:
930 sys.stderr.write ('\n\nMetafont failed. Excerpt from the log file: \n\n*****')
931 m = re.search ("\n!", log)
932 start = m.start (0)
933 short_log = log[start:start+200]
934 sys.stderr.write (short_log)
935 sys.stderr.write ('\n*****\n')
936 if re.search ('Arithmetic overflow', log):
937 sys.stderr.write ("""
939 Apparently, some numbers overflowed. Try using --magnification with a
940 lower number. (Current magnification: %d)
941 """ % magnification)
943 if not options.keep_trying or prod == 0:
944 sys.exit (1)
945 else:
946 sys.stderr.write ('\n\nTrying to proceed despite of the Metafont errors...\n')
950 return "%s.%d" % (filename, prod)
952 def parse_command_line ():
953 p = optparse.OptionParser (version="""mftrace @VERSION@
955 This program is free software. It is covered by the GNU General Public
956 License and you are welcome to change it and/or distribute copies of it
957 under certain conditions. Invoke as `mftrace --warranty' for more
958 information.
960 Copyright (c) 2005--2006 by
961 Han-Wen Nienhuys <hanwen@xs4all.nl>
963 """)
964 p.usage = "mftrace [OPTION]... FILE..."
965 p.description = _ ("Generate Type1 or TrueType font from Metafont source.")
967 p.add_option ('-k', '--keep',
968 action="store_true",
969 dest="keep_temp_dir",
970 help=_ ("Keep all output in directory %s.dir") % program_name)
971 p.add_option ('','--magnification',
972 dest="magnification",
973 metavar="MAG",
974 default=1000.0,
975 type="float",
976 help=_("Set magnification for MF to MAG (default: 1000)"))
977 p.add_option ('-V', '--verbose',
978 action='store_true',
979 default=False,
980 help=_ ("Be verbose"))
981 p.add_option ('-f', '--formats',
982 action="append",
983 dest="formats",
984 default=[],
985 help=_("Which formats to generate (choices: AFM, PFA, PFB, TTF, SVG)"))
986 p.add_option ('', '--simplify',
987 action="store_true",
988 dest="simplify",
989 help=_ ("Simplify using fontforge"))
990 p.add_option ('', '--gffile',
991 dest="gffile",
992 help= _("Use gf FILE instead of running Metafont"))
993 p.add_option ('-I', '--include',
994 dest="include_dirs",
995 action="append",
996 default=[],
997 help=_("Add to path for searching files"))
998 p.add_option ('','--glyphs',
999 default=[],
1000 action="append",
1001 dest="glyphs",
1002 metavar="LIST",
1003 help= _('Process only these glyphs. LIST is comma separated'))
1004 p.add_option ('', '--tfmfile',
1005 metavar='FILE',
1006 action='store',
1007 dest='tfm_file')
1009 p.add_option ('-e', '--encoding',
1010 metavar="FILE",
1011 action='store',
1012 dest="encoding_file",
1013 default="",
1014 help= _ ("Use encoding file FILE"))
1015 p.add_option ('','--keep-trying',
1016 dest='keep_trying',
1017 default=False,
1018 action="store_true",
1019 help= _ ("Don't stop if tracing fails"))
1020 p.add_option ('-w', '--warranty',
1021 action="store_true",
1022 help=_ ("show warranty and copyright"))
1023 p.add_option ('','--dos-kpath',
1024 dest="dos_kpath",
1025 help=_("try to use Miktex kpsewhich"))
1026 p.add_option ('', '--potrace',
1027 dest='potrace',
1028 help=_ ("Use potrace"))
1029 p.add_option ('', '--autotrace',
1030 dest='autotrace',
1031 help=_ ("Use autotrace"))
1032 p.add_option ('', '--no-afm',
1033 action='store_false',
1034 dest="read_afm",
1035 default=True,
1036 help=_("Don't read AFM file"))
1037 p.add_option ('','--noround',
1038 action="store_false",
1039 dest='round_to_int',
1040 default=True,
1041 help= ("Do not round coordinates of control points to integer values (use with --grid)"))
1042 p.add_option ('','--grid',
1043 metavar='SCALE',
1044 dest='grid_scale',
1045 type='float',
1046 default = 1.0,
1047 help=_ ("Set reciprocal grid size in em units"))
1048 p.add_option ('-D','--define',
1049 metavar="SYMBOL=VALUE",
1050 dest="defs",
1051 default=[],
1052 action='append',help=_("Set the font info SYMBOL to VALUE"))
1054 global options
1055 (options, files) = p.parse_args ()
1057 if not files:
1058 sys.stderr.write ('Need argument on command line \n')
1059 p.print_help ()
1060 sys.exit (2)
1062 if options.warranty :
1063 warranty ()
1064 sys.exit (0)
1066 options.font_info = {}
1067 for d in options.defs:
1068 kv = d.split('=')
1069 if len (kv) == 1:
1070 options.font_info[kv] = 'true'
1071 elif len (kv) > 1:
1072 options.font_info[kv[0]] = '='.join (kv[1:])
1074 def comma_sepped_to_list (x):
1075 fs = []
1076 for f in x:
1077 fs += f.lower ().split (',')
1078 return fs
1080 options.formats = comma_sepped_to_list (options.formats)
1082 new_glyphs = []
1083 for r in options.glyphs:
1084 new_glyphs += r.split (',')
1085 options.glyphs = new_glyphs
1087 glyph_range = []
1088 for r in options.glyphs:
1089 glyph_subrange = map (int, string.split (r, '-'))
1090 if len (glyph_subrange) == 2 and glyph_subrange[0] < glyph_subrange[1] + 1:
1091 glyph_range += range (glyph_subrange[0], glyph_subrange[1] + 1)
1092 else:
1093 glyph_range.append (glyph_subrange[0])
1095 options.glyphs = glyph_range
1097 options.trace_binary = ''
1098 if options.potrace:
1099 options.trace_binary = 'potrace'
1100 elif options.autotrace:
1101 options.trace_binary = 'autotrace'
1103 if options.formats == []:
1104 options.formats = ['pfa']
1108 global trace_command
1109 global path_to_type1_ops
1111 stat = os.system ('potrace --version > /dev/null 2>&1 ')
1112 if options.trace_binary != 'autotrace' and stat == 0:
1113 options.trace_binary = 'potrace'
1115 trace_command = potrace_command
1116 path_to_type1_ops = potrace_path_to_type1_ops
1117 elif options.trace_binary == 'potrace' and stat != 0:
1118 error (_ ("Could not run potrace; have you installed it?"))
1120 stat = os.system ('autotrace --version > /dev/null 2>&1 ')
1121 if options.trace_binary != 'potrace' and stat == 0:
1122 options.trace_binary = 'autotrace'
1123 trace_command = autotrace_command
1124 path_to_type1_ops = autotrace_path_to_type1_ops
1125 elif options.trace_binary == 'autotrace' and stat != 0:
1126 error (_ ("Could not run autotrace; have you installed it?"))
1128 if not options.trace_binary:
1129 error (_ ("No tracing program found.\nInstall potrace or autotrace."))
1131 return files
1134 def derive_font_name (family, fullname):
1135 fullname = re.sub (family, '', fullname)
1136 family = re.sub (' ', '', family)
1137 fullname = re.sub ('Oldstyle Figures', 'OsF', fullname)
1138 fullname = re.sub ('Small Caps', 'SC', fullname)
1139 fullname = re.sub ('[Mm]edium', '', fullname)
1140 fullname = re.sub ('[^A-Za-z0-9]', '', fullname)
1141 return '%s-%s' % (family, fullname)
1143 def cm_guess_font_info (filename, fontinfo):
1144 # urg.
1145 filename = re.sub ("cm(.*)tt", r"cmtt\1", filename)
1146 m = re.search ("([0-9]+)$", filename)
1147 design_size = ''
1148 if m:
1149 design_size = int (m.group (1))
1150 fontinfo['DesignSize'] = design_size
1152 prefixes = [("cmtt", "Computer Modern Typewriter"),
1153 ("cmvtt", "Computer Modern Variable Width Typewriter"),
1154 ("cmss", "Computer Modern Sans"),
1155 ("cm", "Computer Modern")]
1157 family = ''
1158 for (k, v) in prefixes:
1159 if re.search (k, filename):
1160 family = v
1161 if k == 'cmtt':
1162 fontinfo['isFixedPitch'] = 'true'
1163 filename = re.sub (k, '', filename)
1164 break
1166 # shapes
1167 prefixes = [("r", "Roman"),
1168 ("mi", "Math italic"),
1169 ("u", "Unslanted italic"),
1170 ("sl", "Oblique"),
1171 ("csc", "Small Caps"),
1172 ("ex", "Math extension"),
1173 ("ti", "Text italic"),
1174 ("i", "Italic")]
1175 shape = ''
1176 for (k, v) in prefixes:
1177 if re.search (k, filename):
1178 shape = v
1179 filename = re.sub (k, '', filename)
1181 prefixes = [("b", "Bold"),
1182 ("d", "Demi bold")]
1183 weight = 'Regular'
1184 for (k, v) in prefixes:
1185 if re.search (k, filename):
1186 weight = v
1187 filename = re.sub (k, '', filename)
1189 prefixes = [("c", "Condensed"),
1190 ("x", "Extended")]
1191 stretch = ''
1192 for (k, v) in prefixes:
1193 if re.search (k, filename):
1194 stretch = v
1195 filename = re.sub (k, '', filename)
1197 fontinfo['ItalicAngle'] = 0
1198 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1199 a = -14
1200 if re.search ("Sans", family):
1201 a = -12
1203 fontinfo ["ItalicAngle"] = a
1205 fontinfo['Weight'] = weight
1206 fontinfo['FamilyName'] = family
1207 full = '%s %s %s %s %dpt' \
1208 % (family, shape, weight, stretch, design_size)
1209 full = re.sub (" +", ' ', full)
1211 fontinfo['FullName'] = full
1212 fontinfo['FontName'] = derive_font_name (family, full)
1214 return fontinfo
1216 def ec_guess_font_info (filename, fontinfo):
1217 design_size = 12
1218 m = re.search ("([0-9]+)$", filename)
1219 if m:
1220 design_size = int (m.group (1))
1221 fontinfo['DesignSize'] = design_size
1223 prefixes = [("ecss", "European Computer Modern Sans"),
1224 ("ectt", "European Computer Modern Typewriter"),
1225 ("ec", "European Computer Modern")]
1227 family = ''
1228 for (k, v) in prefixes:
1229 if re.search (k, filename):
1230 if k == 'ectt':
1231 fontinfo['isFixedPitch'] = 'true'
1232 family = v
1233 filename = re.sub (k, '', filename)
1234 break
1236 # shapes
1237 prefixes = [("r", "Roman"),
1238 ("mi", "Math italic"),
1239 ("u", "Unslanted italic"),
1240 ("sl", "Oblique"),
1241 ("cc", "Small caps"),
1242 ("ex", "Math extension"),
1243 ("ti", "Italic"),
1244 ("i", "Italic")]
1246 shape = ''
1247 for (k, v) in prefixes:
1248 if re.search (k, filename):
1249 shape = v
1250 filename = re.sub (k, '', filename)
1252 prefixes = [("b", "Bold"),
1253 ("d", "Demi bold")]
1254 weight = 'Regular'
1255 for (k, v) in prefixes:
1256 if re.search (k, filename):
1257 weight = v
1258 filename = re.sub (k, '', filename)
1260 prefixes = [("c", "Condensed"),
1261 ("x", "Extended")]
1262 stretch = ''
1263 for (k, v) in prefixes:
1264 if re.search (k, filename):
1265 stretch = v
1266 filename = re.sub (k, '', filename)
1268 fontinfo['ItalicAngle'] = 0
1269 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1270 a = -14
1271 if re.search ("Sans", family):
1272 a = -12
1274 fontinfo ["ItalicAngle"] = a
1276 fontinfo['Weight'] = weight
1277 fontinfo['FamilyName'] = family
1278 full = '%s %s %s %s %dpt' \
1279 % (family, shape, weight, stretch, design_size)
1280 full = re.sub (" +", ' ', full)
1282 fontinfo['FontName'] = derive_font_name (family, full)
1283 fontinfo['FullName'] = full
1285 return fontinfo
1288 def guess_fontinfo (filename):
1289 fi = {
1290 'FontName': filename,
1291 'FamilyName': filename,
1292 'Weight': 'Regular',
1293 'ItalicAngle': 0,
1294 'DesignSize' : 12,
1295 'isFixedPitch' : 'false',
1296 'FullName': filename,
1299 if re.search ('^cm', filename):
1300 fi.update (cm_guess_font_info (filename, fi))
1301 elif re.search ("^ec", filename):
1302 fi.update (ec_guess_font_info (filename, fi))
1303 elif options.read_afm:
1304 global afmfile
1305 if not afmfile:
1306 afmfile = find_file (filename + '.afm')
1308 if afmfile:
1309 afmfile = os.path.abspath (afmfile)
1310 afm_struct = afm.read_afm_file (afmfile)
1311 fi.update (afm_struct.__dict__)
1312 return fi
1313 else:
1314 sys.stderr.write ("Warning: no extra font information for this font.\n"
1315 + "Consider writing a XX_guess_font_info() routine.\n")
1317 return fi
1319 def do_file (filename):
1320 encoding_file = options.encoding_file
1321 global include_dirs
1322 include_dirs = options.include_dirs
1323 include_dirs.append (origdir)
1325 basename = strip_extension (filename, '.mf')
1326 progress (_ ("Font `%s'..." % basename))
1327 progress ('\n')
1329 ## setup encoding
1330 if encoding_file and not os.path.exists (encoding_file):
1331 encoding_file = find_file (encoding_file)
1332 elif encoding_file:
1333 encoding_file = os.path.abspath (encoding_file)
1335 ## setup TFM
1336 if options.tfm_file:
1337 options.tfm_file = os.path.abspath (options.tfm_file)
1338 else:
1339 tfm_try = find_file (basename + '.tfm')
1340 if tfm_try:
1341 options.tfm_file = tfm_try
1343 if not os.environ.has_key ("MFINPUTS"):
1344 os.environ["MFINPUTS"] = os.getcwd () + ":"
1346 ## must change dir before calling mktextfm.
1347 if options.keep_temp_dir:
1348 def nop():
1349 pass
1350 setup_temp (os.path.join (os.getcwd (), program_name + '.dir'))
1351 temp_dir.clean = nop
1352 else:
1353 setup_temp (None)
1355 if options.verbose:
1356 progress ('Temporary directory is `%s\'\n' % temp_dir)
1358 if not options.tfm_file:
1359 options.tfm_file = popen ("mktextfm %s 2>/dev/null" % shell_escape_filename (basename)).read ()
1360 if options.tfm_file:
1361 options.tfm_file = options.tfm_file.strip ()
1362 options.tfm_file = os.path.abspath (options.tfm_file)
1364 if not options.tfm_file:
1365 error (_ ("Can not find a TFM file to match `%s'") % basename)
1367 metric = tfm.read_tfm_file (options.tfm_file)
1369 fontinfo = guess_fontinfo (basename)
1370 fontinfo.update (options.font_info)
1372 if not encoding_file:
1373 codingfile = 'tex256.enc'
1374 if not coding_dict.has_key (metric.coding):
1375 sys.stderr.write ("Unknown encoding `%s'; assuming tex256.\n" % metric.coding)
1376 else:
1377 codingfile = coding_dict[metric.coding]
1379 encoding_file = find_file (codingfile)
1380 if not encoding_file:
1381 error (_ ("can't find file `%s'" % codingfile))
1383 (enc_name, encoding) = read_encoding (encoding_file)
1385 if not len (options.glyphs):
1386 options.glyphs = range (0, len (encoding))
1388 if not options.gffile:
1389 # run mf
1390 base = gen_pixel_font (basename, metric, options.magnification)
1391 options.gffile = base + 'gf'
1392 else:
1393 options.gffile = find_file (options.gffile)
1395 # the heart of the program:
1396 trace_font (basename, options.gffile, metric, options.glyphs, encoding,
1397 options.magnification, fontinfo)
1399 make_outputs (basename, options.formats, encoding)
1400 for format in options.formats:
1401 shutil.copy2 (basename + '.' + format, origdir)
1403 os.chdir (origdir)
1409 afmfile = ''
1410 backend_options = getenv ('MFTRACE_BACKEND_OPTIONS', '')
1411 def main ():
1412 files = parse_command_line ()
1413 identify (sys.stderr)
1415 for filename in files:
1416 do_file (filename)
1417 sys.exit (exit_value)
1419 if __name__ =='__main__':
1420 main()