upload webpage to other dir.
[mftrace.git] / mftrace.py
blobf52ba65380c3e0cbaa1eb75d7e4d184eb15f6644
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',
208 'Extended TeX Font Encoding - Latin': 'tex256.enc',
210 # LilyPond.
211 'fetaBraces': 'feta-braces-a.enc',
212 'fetaNumber': 'feta-nummer10.enc',
213 'fetaMusic': 'feta20.enc',
214 'parmesanMusic': 'parmesan20.enc',
218 def find_file (nm):
219 for d in include_dirs:
220 p = os.path.join (d, nm)
221 try:
222 open (p)
223 return os.path.abspath (p)
224 except IOError:
225 pass
227 p = popen ('kpsewhich %s' % shell_escape_filename (nm)).read ()
228 p = p.strip ()
230 if options.dos_kpath:
231 orig = p
232 p = string.lower (p)
233 p = re.sub ('^([a-z]):', '/cygdrive/\\1', p)
234 p = re.sub ('\\\\', '/', p)
235 sys.stderr.write ("Got `%s' from kpsewhich, using `%s'\n" % (orig, p))
236 return p
239 def flag_error ():
240 global exit_value
241 exit_value = 1
243 ################################################################
244 # TRACING.
245 ################################################################
247 def autotrace_command (fn, opts):
248 opts = " " + opts + " --background-color=FFFFFF --output-format=eps --input-format=pbm "
249 return options.trace_binary + opts + backend_options \
250 + " --output-file=char.eps %s " % fn
252 def potrace_command (fn, opts):
253 return options.trace_binary + opts \
254 + ' -u %d ' % options.grid_scale \
255 + backend_options \
256 + " -q -c --eps --output=char.eps %s " % (fn)
258 trace_command = None
259 path_to_type1_ops = None
261 def trace_one (pbmfile, id):
263 Run tracer, do error handling
266 status = system (trace_command (pbmfile, ''), 1)
268 if status == 2:
269 sys.stderr.write ("\nUser interrupt. Exiting\n")
270 sys.exit (2)
272 if status == 0 and options.keep_temp_dir:
273 shutil.copy2 (pbmfile, '%s.pbm' % id)
274 shutil.copy2 ('char.eps', '%s.eps' % id)
276 if status != 0:
277 error_file = os.path.join (origdir, 'trace-bug-%s.pbm' % id)
278 shutil.copy2 (pbmfile, error_file)
279 msg = """Trace failed on bitmap. Bitmap left in `%s\'
280 Failed command was:
284 Please submit a bugreport to %s development.""" \
285 % (error_file, trace_command (error_file, ''), options.trace_binary)
287 if options.keep_trying:
288 warning (msg)
289 sys.stderr.write ("\nContinuing trace...\n")
290 flag_error ()
291 else:
292 msg = msg + '\nRun mftrace with --keep-trying to produce a font anyway\n'
293 error (msg)
294 else:
295 return 1
297 if status != 0:
298 warning ("Failed, skipping character.\n")
299 return 0
300 else:
301 return 1
303 def make_pbm (filename, outname, char_number):
304 """ Extract bitmap from the PK file FILENAME (absolute) using `gf2pbm'.
305 Return FALSE if the glyph is not valid.
308 command = "%s/gf2pbm -n %d -o %s %s" % (bindir, char_number, outname, filename)
309 status = system (command, ignore_error = 1)
310 return (status == 0)
312 def read_encoding (file):
313 sys.stderr.write (_ ("Using encoding file: `%s'\n") % file)
315 str = open (file).read ()
316 str = re.sub ("%.*", '', str)
317 str = re.sub ("[\n\t \f]+", ' ', str)
318 m = re.search ('/([^ ]+) \[([^\]]+)\] def', str)
319 if not m:
320 error ("Encoding file is invalid")
322 name = m.group (1)
323 cod = m.group (2)
324 cod = re.sub ('[ /]+', ' ', cod)
325 cods = string.split (cod)
327 return (name, cods)
329 def zip_to_pairs (as):
330 r = []
331 while as:
332 r.append ((as[0], as[1]))
333 as = as[2:]
334 return r
336 def unzip_pairs (tups):
337 lst = []
338 while tups:
339 lst = lst + list (tups[0])
340 tups = tups[1:]
341 return lst
343 def autotrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
344 inv_scale = 1000.0 / magnification
346 (size_y, size_x, off_x, off_y) = map (lambda m, s = inv_scale: m * s,
347 bitmap_metrics)
348 ls = open (at_file).readlines ()
349 bbox = (10000, 10000, -10000, -10000)
351 while ls and ls[0] != '*u\n':
352 ls = ls[1:]
354 if ls == []:
355 return (bbox, '')
357 ls = ls[1:]
359 commands = []
362 while ls[0] != '*U\n':
363 ell = ls[0]
364 ls = ls[1:]
366 toks = string.split (ell)
368 if len (toks) < 1:
369 continue
370 cmd = toks[-1]
371 args = map (lambda m, s = inv_scale: s * float (m),
372 toks[:-1])
373 if options.round_to_int:
374 args = zip_to_pairs (map (round, args))
375 else:
376 args = zip_to_pairs (args)
377 commands.append ((cmd, args))
379 expand = {
380 'l': 'rlineto',
381 'm': 'rmoveto',
382 'c': 'rrcurveto',
383 'f': 'closepath',
386 cx = 0
387 cy = size_y - off_y - inv_scale
389 # t1asm seems to fuck up when using sbw. Oh well.
390 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
391 bbox = (10000, 10000, -10000, -10000)
393 for (c, args) in commands:
395 na = []
396 for a in args:
397 (nx, ny) = a
398 if c == 'l' or c == 'c':
399 bbox = update_bbox_with_point (bbox, a)
401 na.append ((nx - cx, ny - cy))
402 (cx, cy) = (nx, ny)
404 a = na
405 c = expand[c]
406 if options.round_to_int:
407 a = map (lambda x: '%d' % int (round (x)),
408 unzip_pairs (a))
409 else:
410 a = map (lambda x: '%d %d div' \
411 % (int (round (x * options.grid_scale/inv_scale)),
412 int (round (options.grid_scale/inv_scale))),
413 unzip_pairs (a))
415 t1_outline = t1_outline + ' %s %s\n' % (string.join (a), c)
417 t1_outline = t1_outline + ' endchar '
418 t1_outline = '{\n %s } |- \n' % t1_outline
420 return (bbox, t1_outline)
422 # FIXME: Cut and paste programming
423 def potrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
424 inv_scale = 1000.0 / magnification
426 (size_y, size_x, off_x, off_y) = map (lambda m,
427 s = inv_scale: m * s,
428 bitmap_metrics)
429 ls = open (at_file).readlines ()
430 bbox = (10000, 10000, -10000, -10000)
432 while ls and ls[0] != '0 setgray\n':
433 ls = ls[1:]
435 if ls == []:
436 return (bbox, '')
437 ls = ls[1:]
438 commands = []
440 while ls and ls[0] != 'grestore\n':
441 ell = ls[0]
442 ls = ls[1:]
444 if ell == 'fill\n':
445 continue
447 toks = string.split (ell)
449 if len (toks) < 1:
450 continue
451 cmd = toks[-1]
452 args = map (lambda m, s = inv_scale: s * float (m),
453 toks[:-1])
454 args = zip_to_pairs (args)
455 commands.append ((cmd, args))
457 # t1asm seems to fuck up when using sbw. Oh well.
458 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
459 bbox = (10000, 10000, -10000, -10000)
461 # Type1 fonts have relative coordinates (doubly relative for
462 # rrcurveto), so must convert moveto and rcurveto.
464 z = (0.0, size_y - off_y - 1.0)
465 for (c, args) in commands:
466 args = map (lambda x: (x[0] * (1.0 / options.grid_scale),
467 x[1] * (1.0 / options.grid_scale)), args)
469 if c == 'moveto':
470 args = [(args[0][0] - z[0], args[0][1] - z[1])]
472 zs = []
473 for a in args:
474 lz = (z[0] + a[0], z[1] + a[1])
475 bbox = update_bbox_with_point (bbox, lz)
476 zs.append (lz)
478 if options.round_to_int:
479 last_discr_z = (int (round (z[0])), int (round (z[1])))
480 else:
481 last_discr_z = (z[0], z[1])
482 args = []
483 for a in zs:
484 if options.round_to_int:
485 a = (int (round (a[0])), int (round (a[1])))
486 else:
487 a = (a[0], a[1])
488 args.append ((a[0] - last_discr_z[0],
489 a[1] - last_discr_z[1]))
491 last_discr_z = a
493 if zs:
494 z = zs[-1]
495 c = { 'rcurveto': 'rrcurveto',
496 'moveto': 'rmoveto',
497 'closepath': 'closepath',
498 'rlineto': 'rlineto'}[c]
500 if c == 'rmoveto':
501 t1_outline += ' closepath '
503 if options.round_to_int:
504 args = map (lambda x: '%d' % int (round (x)),
505 unzip_pairs (args))
506 else:
507 args = map (lambda x: '%d %d div' \
508 % (int (round (x*options.grid_scale/inv_scale)),
509 int (round (options.grid_scale/inv_scale))),
510 unzip_pairs (args))
512 t1_outline = t1_outline + ' %s %s\n' % (string.join (args), c)
514 t1_outline = t1_outline + ' endchar '
515 t1_outline = '{\n %s } |- \n' % t1_outline
517 return (bbox, t1_outline)
519 def read_gf_dims (name, c):
520 str = popen ('%s/gf2pbm -n %d -s %s' % (bindir, c, name)).read ()
521 m = re.search ('size: ([0-9]+)+x([0-9]+), offset: \(([0-9-]+),([0-9-]+)\)', str)
523 return tuple (map (int, m.groups ()))
525 def trace_font (fontname, gf_file, metric, glyphs, encoding,
526 magnification, fontinfo):
527 t1os = []
528 font_bbox = (10000, 10000, -10000, -10000)
530 progress (_ ("Tracing bitmaps..."))
532 if options.verbose:
533 progress ('\n')
534 else:
535 progress (' ')
537 # for single glyph testing.
538 # glyphs = []
539 for a in glyphs:
540 if encoding[a] == ".notavail":
541 continue
542 valid = metric.has_char (a)
543 if not valid:
544 encoding[a] = ".notavail"
545 continue
547 valid = make_pbm (gf_file, 'char.pbm', a)
548 if not valid:
549 encoding[a] = ".notavail"
550 continue
552 (w, h, xo, yo) = read_gf_dims (gf_file, a)
554 if not options.verbose:
555 sys.stderr.write ('[%d' % a)
556 sys.stderr.flush ()
558 # this wants the id, not the filename.
559 success = trace_one ("char.pbm", '%s-%d' % (options.gffile, a))
560 if not success:
561 sys.stderr.write ("(skipping character)]")
562 sys.stderr.flush ()
563 encoding[a] = ".notavail"
564 continue
566 if not options.verbose:
567 sys.stderr.write (']')
568 sys.stderr.flush ()
569 metric_width = metric.get_char (a).width
570 tw = int (round (metric_width / metric.design_size * 1000))
571 (bbox, t1o) = path_to_type1_ops ("char.eps", (h, w, xo, yo),
572 tw, magnification)
574 if t1o == '':
575 encoding[a] = ".notavail"
576 continue
578 font_bbox = update_bbox_with_bbox (font_bbox, bbox)
580 t1os.append ('\n/%s %s ' % (encoding[a], t1o))
582 if not options.verbose:
583 progress ('\n')
584 to_type1 (t1os, font_bbox, fontname, encoding, magnification, fontinfo)
586 def ps_encode_encoding (encoding):
587 str = ' %d array\n0 1 %d {1 index exch /.notdef put} for\n' \
588 % (len (encoding), len (encoding)-1)
590 for i in range (0, len (encoding)):
591 if encoding[i] != ".notavail":
592 str = str + 'dup %d /%s put\n' % (i, encoding[i])
594 return str
597 def gen_unique_id (dict):
598 nm = 'FullName'
599 return 4000000 + (hash (nm) % 1000000)
601 def to_type1 (outlines, bbox, fontname, encoding, magnification, fontinfo):
604 Fill in the header template for the font, append charstrings,
605 and shove result through t1asm
607 template = r"""%%!PS-AdobeFont-1.0: %(FontName)s %(VVV)s.%(WWW)s
608 13 dict begin
609 /FontInfo 16 dict dup begin
610 /version (%(VVV)s.%(WWW)s) readonly def
611 /Notice (%(Notice)s) readonly def
612 /FullName (%(FullName)s) readonly def
613 /FamilyName (%(FamilyName)s) readonly def
614 /Weight (%(Weight)s) readonly def
615 /ItalicAngle %(ItalicAngle)s def
616 /isFixedPitch %(isFixedPitch)s def
617 /UnderlinePosition %(UnderlinePosition)s def
618 /UnderlineThickness %(UnderlineThickness)s def
619 end readonly def
620 /FontName /%(FontName)s def
621 /FontType 1 def
622 /PaintType 0 def
623 /FontMatrix [%(xrevscale)f 0 0 %(yrevscale)f 0 0] readonly def
624 /FontBBox {%(llx)d %(lly)d %(urx)d %(ury)d} readonly def
625 /Encoding %(Encoding)s readonly def
626 currentdict end
627 currentfile eexec
628 dup /Private 20 dict dup begin
629 /-|{string currentfile exch readstring pop}executeonly def
630 /|-{noaccess def}executeonly def
631 /|{noaccess put}executeonly def
632 /lenIV 4 def
633 /password 5839 def
634 /MinFeature {16 16} |-
635 /BlueValues [] |-
636 /OtherSubrs [ {} {} {} {} ] |-
637 /ForceBold false def
638 /Subrs 1 array
639 dup 0 { return } |
641 2 index
642 /CharStrings %(CharStringsLen)d dict dup begin
643 %(CharStrings)s
646 /.notdef { 0 0 hsbw endchar } |-
649 readonly put
650 noaccess put
651 dup/FontName get exch definefont
652 pop mark currentfile closefile
653 cleartomark
655 ## apparently, some fonts end the file with cleartomark. Don't know why.
657 copied_fields = ['FontName', 'FamilyName', 'FullName', 'DesignSize',
658 'ItalicAngle', 'isFixedPitch', 'Weight']
660 vars = {
661 'VVV': '001',
662 'WWW': '001',
663 'Notice': 'Generated from MetaFont bitmap by mftrace %s, http://www.xs4all.nl/~hanwen/mftrace/ ' % program_version,
664 'UnderlinePosition': '-100',
665 'UnderlineThickness': '50',
666 'xrevscale': 1.0/1000.0,
667 'yrevscale': 1.0/1000.0,
668 'llx': bbox[0],
669 'lly': bbox[1],
670 'urx': bbox[2],
671 'ury': bbox[3],
672 'Encoding': ps_encode_encoding (encoding),
674 # need one extra entry for .notdef
675 'CharStringsLen': len (outlines) + 1,
676 'CharStrings': string.join (outlines),
677 'CharBBox': '0 0 0 0',
680 for k in copied_fields:
681 vars[k] = fontinfo[k]
683 open ('mftrace.t1asm', 'w').write (template % vars)
685 def update_bbox_with_point (bbox, pt):
686 (llx, lly, urx, ury) = bbox
687 llx = min (pt[0], llx)
688 lly = min (pt[1], lly)
689 urx = max (pt[0], urx)
690 ury = max (pt[1], ury)
692 return (llx, lly, urx, ury)
694 def update_bbox_with_bbox (bb, dims):
695 (llx, lly, urx, ury) = bb
696 llx = min (llx, dims[0])
697 lly = min (lly, dims[1])
698 urx = max (urx, dims[2])
699 ury = max (ury, dims[3])
701 return (llx, lly, urx, ury)
703 def get_binary (name):
704 search_path = string.split (os.environ['PATH'], ':')
705 for p in search_path:
706 nm = os.path.join (p, name)
707 if os.path.exists (nm):
708 return nm
710 return ''
712 def get_fontforge_command ():
713 fontforge_cmd = ''
714 for ff in ['fontforge', 'pfaedit']:
715 if get_binary(ff):
716 fontforge_cmd = ff
718 stat = 1
719 if fontforge_cmd:
720 stat = system ("%s -usage > pfv 2>&1 " % fontforge_cmd,
721 ignore_error = 1)
723 if stat != 0:
724 warning ("Command `%s -usage' failed. Cannot simplify or convert to TTF.\n" % fontforge_cmd)
726 if fontforge_cmd == 'pfaedit' \
727 and re.search ("-script", open ('pfv').read ()) == None:
728 warning ("pfaedit does not support -script. Install 020215 or later.\nCannot simplify or convert to TTF.\n")
729 return ''
730 return fontforge_cmd
732 def tfm2kpx (tfmname, encoding):
733 kpx_lines = []
734 pl = popen ("tftopl %s" % (tfmname))
736 label_pattern = re.compile (
737 "\A \(LABEL ([DOHC]{1}) ([A-Za-z0-9]*)\)")
738 krn_pattern = re.compile (
739 "\A \(KRN ([DOHC]{1}) ([A-Za-z0-9]*) R (-?[\d\.]+)\)")
741 first = 0
742 second = 0
744 for line in pl.readlines ():
746 label_match = label_pattern.search (line)
747 if not (label_match is None):
748 if label_match.group (1) == "D":
749 first = int (label_match.group (2))
750 elif label_match.group (1) == "O":
751 first = int (label_match.group (2), 8)
752 elif label_match.group (1) == "C":
753 first = ord (label_match.group (2))
755 krn_match = krn_pattern.search (line)
756 if not (krn_match is None):
757 if krn_match.group (1) == "D":
758 second = int (krn_match.group (2))
759 elif krn_match.group (1) == "O":
760 second = int (krn_match.group (2), 8)
761 elif krn_match.group (1) == "C":
762 second = ord (krn_match.group (2))
764 krn = round (float (krn_match.group (3)) * 1000)
766 if (encoding[first] != '.notavail' and
767 encoding[first] != '.notdef' and
768 encoding[second] != '.notavail' and
769 encoding[second] != '.notdef'):
771 kpx_lines.append ("KPX %s %s %d\n" % (
772 encoding[first], encoding[second], krn))
774 return kpx_lines
776 def get_afm (t1_path, tfmname, encoding, out_path):
777 afm_stream = popen ("printafm %s" % (t1_path))
778 afm_lines = []
779 kpx_lines = tfm2kpx (tfmname, encoding)
781 for line in afm_stream.readlines ():
782 afm_lines.append (line)
784 if re.match (r"^EndCharMetrics", line, re.I):
785 afm_lines.append ("StartKernData\n")
786 afm_lines.append ("StartKernPairs %d\n" % len (kpx_lines))
788 for kpx_line in kpx_lines:
789 afm_lines.append (kpx_line)
791 afm_lines.append ("EndKernPairs\n")
792 afm_lines.append ("EndKernData\n")
794 progress (_ ("Writing metrics to `%s'... ") % out_path)
795 afm_file = open (out_path, 'w')
796 afm_file.writelines (afm_lines)
797 afm_file.flush ()
798 afm_file.close ()
800 progress ('\n')
802 def assemble_font (fontname, format, is_raw):
803 ext = '.' + format
804 asm_opt = '--pfa'
806 if format == 'pfb':
807 asm_opt = '--pfb'
809 if is_raw:
810 ext = ext + '.raw'
812 outname = fontname + ext
814 progress (_ ("Assembling raw font to `%s'... ") % outname)
815 if options.verbose:
816 progress ('\n')
817 system ('t1asm %s mftrace.t1asm %s' % (asm_opt, shell_escape_filename (outname)))
818 progress ('\n')
819 return outname
821 def make_outputs (fontname, formats, encoding):
823 run pfaedit to convert to other formats
826 ff_needed = 0
827 ff_command = ""
829 if (options.simplify or options.round_to_int or 'ttf' in formats or 'svg' in formats):
830 ff_needed = 1
831 if ff_needed:
832 ff_command = get_fontforge_command ()
834 if ff_needed and ff_command:
835 raw_name = assemble_font (fontname, 'pfa', 1)
837 simplify_cmd = ''
838 if options.round_to_int:
839 simplify_cmd = 'RoundToInt ();'
840 generate_cmds = ''
841 for f in formats:
842 generate_cmds += 'Generate("%s");' % (fontname + '.' + f)
844 if options.simplify:
845 simplify_cmd ='''SelectAll ();
847 AddExtrema();
848 Simplify ();
849 %(simplify_cmd)s
850 AutoHint ();''' % vars()
852 pe_script = ('''#!/usr/bin/env %(ff_command)s
853 Open ($1);
854 MergeKern($2);
855 %(simplify_cmd)s
856 %(generate_cmds)s
857 Quit (0);
858 ''' % vars())
860 open ('to-ttf.pe', 'w').write (pe_script)
861 if options.verbose:
862 print 'Fontforge script', pe_script
863 system ("%s -script to-ttf.pe %s %s" % (ff_command,
864 shell_escape_filename (raw_name), shell_escape_filename (options.tfm_file)))
865 else:
866 t1_path = ''
868 if ('pfa' in formats):
869 t1_path = assemble_font (fontname, 'pfa', 0)
871 if ('pfb' in formats):
872 t1_path = assemble_font (fontname, 'pfb', 0)
874 if (t1_path != '' and 'afm' in formats):
875 get_afm (t1_path, options.tfm_file, encoding, fontname + '.afm')
878 def getenv (var, default):
879 if os.environ.has_key (var):
880 return os.environ[var]
881 else:
882 return default
884 def gen_pixel_font (filename, metric, magnification):
886 Generate a GF file for FILENAME, such that `magnification'*mfscale
887 (default 1000 * 1.0) pixels fit on the designsize.
889 base_dpi = 1200
891 size = metric.design_size
893 size_points = size * 1/72.27 * base_dpi
895 mag = magnification / size_points
897 prod = mag * base_dpi
898 try:
899 open ('%s.%dgf' % (filename, prod))
900 except IOError:
902 ## MFINPUTS/TFMFONTS take kpathsea specific values;
903 ## we should analyse them any further.
904 os.environ['MFINPUTS'] = '%s:%s' % (origdir,
905 getenv ('MFINPUTS', ''))
906 os.environ['TFMFONTS'] = '%s:%s' % (origdir,
907 getenv ('TFMINPUTS', ''))
909 progress (_ ("Running Metafont..."))
911 cmdstr = r"mf '\mode:=lexmarks; mag:=%f; nonstopmode; input %s'" % (mag, filename)
912 if not options.verbose:
913 cmdstr = cmdstr + ' 1>/dev/null 2>/dev/null'
914 st = system (cmdstr, ignore_error = 1)
915 progress ('\n')
917 logfile = '%s.log' % filename
918 log = ''
919 prod = 0
920 if os.path.exists (logfile):
921 log = open (logfile).read ()
922 m = re.search ('Output written on %s.([0-9]+)gf' % re.escape (filename), log)
923 prod = int (m.group (1))
925 if st:
926 sys.stderr.write ('\n\nMetafont failed. Excerpt from the log file: \n\n*****')
927 m = re.search ("\n!", log)
928 start = m.start (0)
929 short_log = log[start:start+200]
930 sys.stderr.write (short_log)
931 sys.stderr.write ('\n*****\n')
932 if re.search ('Arithmetic overflow', log):
933 sys.stderr.write ("""
935 Apparently, some numbers overflowed. Try using --magnification with a
936 lower number. (Current magnification: %d)
937 """ % magnification)
939 if not options.keep_trying or prod == 0:
940 sys.exit (1)
941 else:
942 sys.stderr.write ('\n\nTrying to proceed despite of the Metafont errors...\n')
946 return "%s.%d" % (filename, prod)
948 def parse_command_line ():
949 p = optparse.OptionParser (version="""mftrace @VERSION@
951 This program is free software. It is covered by the GNU General Public
952 License and you are welcome to change it and/or distribute copies of it
953 under certain conditions. Invoke as `mftrace --warranty' for more
954 information.
956 Copyright (c) 2005--2006 by
957 Han-Wen Nienhuys <hanwen@xs4all.nl>
959 """)
960 p.usage = "mftrace [OPTION]... FILE..."
961 p.description = _ ("Generate Type1 or TrueType font from Metafont source.")
963 p.add_option ('-k', '--keep',
964 action="store_true",
965 dest="keep_temp_dir",
966 help=_ ("Keep all output in directory %s.dir") % program_name)
967 p.add_option ('','--magnification',
968 dest="magnification",
969 metavar="MAG",
970 default=1000.0,
971 type="float",
972 help=_("Set magnification for MF to MAG (default: 1000)"))
973 p.add_option ('-V', '--verbose',
974 action='store_true',
975 default=False,
976 help=_ ("Be verbose"))
977 p.add_option ('-f', '--formats',
978 action="append",
979 dest="formats",
980 default=[],
981 help=_("Which formats to generate (choices: AFM, PFA, PFB, TTF, SVG)"))
982 p.add_option ('', '--simplify',
983 action="store_true",
984 dest="simplify",
985 help=_ ("Simplify using fontforge"))
986 p.add_option ('', '--gffile',
987 dest="gffile",
988 help= _("Use gf FILE instead of running Metafont"))
989 p.add_option ('-I', '--include',
990 dest="include_dirs",
991 action="append",
992 default=[],
993 help=_("Add to path for searching files"))
994 p.add_option ('','--glyphs',
995 default=[],
996 action="append",
997 dest="glyphs",
998 metavar="LIST",
999 help= _('Process only these glyphs. LIST is comma separated'))
1000 p.add_option ('', '--tfmfile',
1001 metavar='FILE',
1002 action='store',
1003 dest='tfm_file')
1005 p.add_option ('-e', '--encoding',
1006 metavar="FILE",
1007 action='store',
1008 dest="encoding_file",
1009 default="",
1010 help= _ ("Use encoding file FILE"))
1011 p.add_option ('','--keep-trying',
1012 dest='keep_trying',
1013 default=False,
1014 action="store_true",
1015 help= _ ("Don't stop if tracing fails"))
1016 p.add_option ('-w', '--warranty',
1017 action="store_true",
1018 help=_ ("show warranty and copyright"))
1019 p.add_option ('','--dos-kpath',
1020 dest="dos_kpath",
1021 help=_("try to use Miktex kpsewhich"))
1022 p.add_option ('', '--potrace',
1023 dest='potrace',
1024 help=_ ("Use potrace"))
1025 p.add_option ('', '--autotrace',
1026 dest='autotrace',
1027 help=_ ("Use autotrace"))
1028 p.add_option ('', '--no-afm',
1029 action='store_false',
1030 dest="read_afm",
1031 default=True,
1032 help=_("Don't read AFM file"))
1033 p.add_option ('','--noround',
1034 action="store_false",
1035 dest='round_to_int',
1036 default=True,
1037 help= ("Do not round coordinates of control points to integer values (use with --grid)"))
1038 p.add_option ('','--grid',
1039 metavar='SCALE',
1040 dest='grid_scale',
1041 type='float',
1042 default = 1.0,
1043 help=_ ("Set reciprocal grid size in em units"))
1044 p.add_option ('-D','--define',
1045 metavar="SYMBOL=VALUE",
1046 dest="defs",
1047 default=[],
1048 action='append',help=_("Set the font info SYMBOL to VALUE"))
1050 global options
1051 (options, files) = p.parse_args ()
1053 if not files:
1054 sys.stderr.write ('Need argument on command line \n')
1055 p.print_help ()
1056 sys.exit (2)
1058 if options.warranty :
1059 warranty ()
1060 sys.exit (0)
1062 options.font_info = {}
1063 for d in options.defs:
1064 kv = d.split('=')
1065 if len (kv) == 1:
1066 options.font_info[kv] = 'true'
1067 elif len (kv) > 1:
1068 options.font_info[kv[0]] = '='.join (kv[1:])
1070 def comma_sepped_to_list (x):
1071 fs = []
1072 for f in x:
1073 fs += f.lower ().split (',')
1074 return fs
1076 options.formats = comma_sepped_to_list (options.formats)
1078 new_glyphs = []
1079 for r in options.glyphs:
1080 new_glyphs += r.split (',')
1081 options.glyphs = new_glyphs
1083 glyph_range = []
1084 for r in options.glyphs:
1085 glyph_subrange = map (int, string.split (r, '-'))
1086 if len (glyph_subrange) == 2 and glyph_subrange[0] < glyph_subrange[1] + 1:
1087 glyph_range += range (glyph_subrange[0], glyph_subrange[1] + 1)
1088 else:
1089 glyph_range.append (glyph_subrange[0])
1091 options.glyphs = glyph_range
1093 options.trace_binary = ''
1094 if options.potrace:
1095 options.trace_binary = 'potrace'
1096 elif options.autotrace:
1097 options.trace_binary = 'autotrace'
1099 if options.formats == []:
1100 options.formats = ['pfa']
1104 global trace_command
1105 global path_to_type1_ops
1107 stat = os.system ('potrace --version > /dev/null 2>&1 ')
1108 if options.trace_binary != 'autotrace' and stat == 0:
1109 options.trace_binary = 'potrace'
1111 trace_command = potrace_command
1112 path_to_type1_ops = potrace_path_to_type1_ops
1114 stat = os.system ('autotrace --version > /dev/null 2>&1 ')
1115 if options.trace_binary != 'potrace' and stat == 0:
1116 options.trace_binary = 'autotrace'
1117 trace_command = autotrace_command
1118 path_to_type1_ops = autotrace_path_to_type1_ops
1120 if not options.trace_binary:
1121 error (_ ("No tracing program found.\nInstall potrace or autotrace."))
1123 return files
1126 def derive_font_name (family, fullname):
1127 fullname = re.sub (family, '', fullname)
1128 family = re.sub (' ', '', family)
1129 fullname = re.sub ('Oldstyle Figures', 'OsF', fullname)
1130 fullname = re.sub ('Small Caps', 'SC', fullname)
1131 fullname = re.sub ('[Mm]edium', '', fullname)
1132 fullname = re.sub ('[^A-Za-z0-9]', '', fullname)
1133 return '%s-%s' % (family, fullname)
1135 def cm_guess_font_info (filename, fontinfo):
1136 # urg.
1137 filename = re.sub ("cm(.*)tt", r"cmtt\1", filename)
1138 m = re.search ("([0-9]+)$", filename)
1139 design_size = ''
1140 if m:
1141 design_size = int (m.group (1))
1142 fontinfo['DesignSize'] = design_size
1144 prefixes = [("cmtt", "Computer Modern Typewriter"),
1145 ("cmvtt", "Computer Modern Variable Width Typewriter"),
1146 ("cmss", "Computer Modern Sans"),
1147 ("cm", "Computer Modern")]
1149 family = ''
1150 for (k, v) in prefixes:
1151 if re.search (k, filename):
1152 family = v
1153 if k == 'cmtt':
1154 fontinfo['isFixedPitch'] = 'true'
1155 filename = re.sub (k, '', filename)
1156 break
1158 # shapes
1159 prefixes = [("r", "Roman"),
1160 ("mi", "Math italic"),
1161 ("u", "Unslanted italic"),
1162 ("sl", "Oblique"),
1163 ("csc", "Small Caps"),
1164 ("ex", "Math extension"),
1165 ("ti", "Text italic"),
1166 ("i", "Italic")]
1167 shape = ''
1168 for (k, v) in prefixes:
1169 if re.search (k, filename):
1170 shape = v
1171 filename = re.sub (k, '', filename)
1173 prefixes = [("b", "Bold"),
1174 ("d", "Demi bold")]
1175 weight = 'Regular'
1176 for (k, v) in prefixes:
1177 if re.search (k, filename):
1178 weight = v
1179 filename = re.sub (k, '', filename)
1181 prefixes = [("c", "Condensed"),
1182 ("x", "Extended")]
1183 stretch = ''
1184 for (k, v) in prefixes:
1185 if re.search (k, filename):
1186 stretch = v
1187 filename = re.sub (k, '', filename)
1189 fontinfo['ItalicAngle'] = 0
1190 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1191 a = -14
1192 if re.search ("Sans", family):
1193 a = -12
1195 fontinfo ["ItalicAngle"] = a
1197 fontinfo['Weight'] = weight
1198 fontinfo['FamilyName'] = family
1199 full = '%s %s %s %s %dpt' \
1200 % (family, shape, weight, stretch, design_size)
1201 full = re.sub (" +", ' ', full)
1203 fontinfo['FullName'] = full
1204 fontinfo['FontName'] = derive_font_name (family, full)
1206 return fontinfo
1208 def ec_guess_font_info (filename, fontinfo):
1209 design_size = 12
1210 m = re.search ("([0-9]+)$", filename)
1211 if m:
1212 design_size = int (m.group (1))
1213 fontinfo['DesignSize'] = design_size
1215 prefixes = [("ecss", "European Computer Modern Sans"),
1216 ("ectt", "European Computer Modern Typewriter"),
1217 ("ec", "European Computer Modern")]
1219 family = ''
1220 for (k, v) in prefixes:
1221 if re.search (k, filename):
1222 if k == 'ectt':
1223 fontinfo['isFixedPitch'] = 'true'
1224 family = v
1225 filename = re.sub (k, '', filename)
1226 break
1228 # shapes
1229 prefixes = [("r", "Roman"),
1230 ("mi", "Math italic"),
1231 ("u", "Unslanted italic"),
1232 ("sl", "Oblique"),
1233 ("cc", "Small caps"),
1234 ("ex", "Math extension"),
1235 ("ti", "Italic"),
1236 ("i", "Italic")]
1238 shape = ''
1239 for (k, v) in prefixes:
1240 if re.search (k, filename):
1241 shape = v
1242 filename = re.sub (k, '', filename)
1244 prefixes = [("b", "Bold"),
1245 ("d", "Demi bold")]
1246 weight = 'Regular'
1247 for (k, v) in prefixes:
1248 if re.search (k, filename):
1249 weight = v
1250 filename = re.sub (k, '', filename)
1252 prefixes = [("c", "Condensed"),
1253 ("x", "Extended")]
1254 stretch = ''
1255 for (k, v) in prefixes:
1256 if re.search (k, filename):
1257 stretch = v
1258 filename = re.sub (k, '', filename)
1260 fontinfo['ItalicAngle'] = 0
1261 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1262 a = -14
1263 if re.search ("Sans", family):
1264 a = -12
1266 fontinfo ["ItalicAngle"] = a
1268 fontinfo['Weight'] = weight
1269 fontinfo['FamilyName'] = family
1270 full = '%s %s %s %s %dpt' \
1271 % (family, shape, weight, stretch, design_size)
1272 full = re.sub (" +", ' ', full)
1274 fontinfo['FontName'] = derive_font_name (family, full)
1275 fontinfo['FullName'] = full
1277 return fontinfo
1280 def guess_fontinfo (filename):
1281 fi = {
1282 'FontName': filename,
1283 'FamilyName': filename,
1284 'Weight': 'Regular',
1285 'ItalicAngle': 0,
1286 'DesignSize' : 12,
1287 'isFixedPitch' : 'false',
1288 'FullName': filename,
1291 if re.search ('^cm', filename):
1292 fi.update (cm_guess_font_info (filename, fi))
1293 elif re.search ("^ec", filename):
1294 fi.update (ec_guess_font_info (filename, fi))
1295 elif options.read_afm:
1296 global afmfile
1297 if not afmfile:
1298 afmfile = find_file (filename + '.afm')
1300 if afmfile:
1301 afmfile = os.path.abspath (afmfile)
1302 afm_struct = afm.read_afm_file (afmfile)
1303 fi.update (afm_struct.__dict__)
1304 return fi
1305 else:
1306 sys.stderr.write ("Warning: no extra font information for this font.\n"
1307 + "Consider writing a XX_guess_font_info() routine.\n")
1309 return fi
1311 def do_file (filename):
1312 encoding_file = options.encoding_file
1313 global include_dirs
1314 include_dirs = options.include_dirs
1315 include_dirs.append (origdir)
1317 basename = strip_extension (filename, '.mf')
1318 progress (_ ("Font `%s'..." % basename))
1319 progress ('\n')
1321 ## setup encoding
1322 if encoding_file and not os.path.exists (encoding_file):
1323 encoding_file = find_file (encoding_file)
1324 elif encoding_file:
1325 encoding_file = os.path.abspath (encoding_file)
1327 ## setup TFM
1328 if options.tfm_file:
1329 options.tfm_file = os.path.abspath (options.tfm_file)
1330 else:
1331 tfm_try = find_file (basename + '.tfm')
1332 if tfm_try:
1333 options.tfm_file = tfm_try
1335 if not os.environ.has_key ("MFINPUTS"):
1336 os.environ["MFINPUTS"] = os.getcwd () + ":"
1338 ## must change dir before calling mktextfm.
1339 if options.keep_temp_dir:
1340 def nop():
1341 pass
1342 setup_temp (os.path.join (os.getcwd (), program_name + '.dir'))
1343 temp_dir.clean = nop
1344 else:
1345 setup_temp (None)
1347 if options.verbose:
1348 progress ('Temporary directory is `%s\'\n' % temp_dir)
1350 if not options.tfm_file:
1351 options.tfm_file = popen ("mktextfm %s 2>/dev/null" % shell_escape_filename (basename)).read ()
1352 if options.tfm_file:
1353 options.tfm_file = options.tfm_file.strip ()
1354 options.tfm_file = os.path.abspath (options.tfm_file)
1356 if not options.tfm_file:
1357 error (_ ("Can not find a TFM file to match `%s'") % basename)
1359 metric = tfm.read_tfm_file (options.tfm_file)
1361 fontinfo = guess_fontinfo (basename)
1362 fontinfo.update (options.font_info)
1364 if not encoding_file:
1365 codingfile = 'tex256.enc'
1366 if not coding_dict.has_key (metric.coding):
1367 sys.stderr.write ("Unknown encoding `%s'; assuming tex256.\n" % metric.coding)
1368 else:
1369 codingfile = coding_dict[metric.coding]
1371 encoding_file = find_file (codingfile)
1372 if not encoding_file:
1373 error (_ ("can't find file `%s'" % codingfile))
1375 (enc_name, encoding) = read_encoding (encoding_file)
1377 if not len (options.glyphs):
1378 options.glyphs = range (0, len (encoding))
1380 if not options.gffile:
1381 # run mf
1382 base = gen_pixel_font (basename, metric, options.magnification)
1383 options.gffile = base + 'gf'
1384 else:
1385 options.gffile = find_file (options.gffile)
1387 # the heart of the program:
1388 trace_font (basename, options.gffile, metric, options.glyphs, encoding,
1389 options.magnification, fontinfo)
1391 make_outputs (basename, options.formats, encoding)
1392 for format in options.formats:
1393 shutil.copy2 (basename + '.' + format, origdir)
1395 os.chdir (origdir)
1401 afmfile = ''
1402 backend_options = getenv ('MFTRACE_BACKEND_OPTIONS', '')
1403 def main ():
1404 files = parse_command_line ()
1405 identify (sys.stderr)
1407 for filename in files:
1408 do_file (filename)
1409 sys.exit (exit_value)
1411 if __name__ =='__main__':
1412 main()