fontstripping and textaspath writer options; psfontmaps and pdffontmaps config options
[PyX/mjg.git] / pyx / font / font.py
blobe8d6be3b764199f83b2e7854db404b1cf20d1214
1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2005-2007 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2005-2007 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 from pyx import bbox, canvasitem, deco, path, pswriter, pdfwriter, trafo, unit
24 import t1file
26 try:
27 set()
28 except NameError:
29 # Python 2.3
30 from sets import Set as set
33 ##############################################################################
34 # PS resources
35 ##############################################################################
37 class PST1file(pswriter.PSresource):
39 """ PostScript font definition included in the prolog """
41 def __init__(self, t1file, glyphnames, charcodes):
42 """ include type 1 font t1file stripped to the given glyphnames"""
43 self.type = "t1file"
44 self.t1file = t1file
45 self.id = t1file.name
46 self.glyphnames = set(glyphnames)
47 self.charcodes = set(charcodes)
49 def merge(self, other):
50 self.glyphnames.update(other.glyphnames)
51 self.charcodes.update(other.charcodes)
53 def output(self, file, writer, registry):
54 file.write("%%%%BeginFont: %s\n" % self.t1file.name)
55 if writer.stripfonts:
56 if self.glyphnames:
57 file.write("%%Included glyphs: %s\n" % " ".join(self.glyphnames))
58 if self.charcodes:
59 file.write("%%Included charcodes: %s\n" % " ".join([str(charcode) for charcode in self.charcodes]))
60 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPS(file, writer)
61 else:
62 self.t1file.outputPS(file, writer)
63 file.write("\n%%EndFont\n")
66 _ReEncodeFont = pswriter.PSdefinition("ReEncodeFont", """{
67 5 dict
68 begin
69 /newencoding exch def
70 /newfontname exch def
71 /basefontname exch def
72 /basefontdict basefontname findfont def
73 /newfontdict basefontdict maxlength dict def
74 basefontdict {
75 exch dup dup /FID ne exch /Encoding ne and
76 { exch newfontdict 3 1 roll put }
77 { pop pop }
78 ifelse
79 } forall
80 newfontdict /FontName newfontname put
81 newfontdict /Encoding newencoding put
82 newfontname newfontdict definefont pop
83 end
84 }""")
87 class PSreencodefont(pswriter.PSresource):
89 """ reencoded PostScript font"""
91 def __init__(self, basefontname, newfontname, encoding):
92 """ reencode the font """
94 self.type = "reencodefont"
95 self.basefontname = basefontname
96 self.id = self.newfontname = newfontname
97 self.encoding = encoding
99 def output(self, file, writer, registry):
100 file.write("%%%%BeginResource: %s\n" % self.newfontname)
101 file.write("/%s /%s\n[" % (self.basefontname, self.newfontname))
102 vector = [None] * len(self.encoding)
103 for glyphname, charcode in self.encoding.items():
104 vector[charcode] = glyphname
105 for i, glyphname in enumerate(vector):
106 if i:
107 if not (i % 8):
108 file.write("\n")
109 else:
110 file.write(" ")
111 file.write("/%s" % glyphname)
112 file.write("]\n")
113 file.write("ReEncodeFont\n")
114 file.write("%%EndResource\n")
117 _ChangeFontMatrix = pswriter.PSdefinition("ChangeFontMatrix", """{
118 5 dict
119 begin
120 /newfontmatrix exch def
121 /newfontname exch def
122 /basefontname exch def
123 /basefontdict basefontname findfont def
124 /newfontdict basefontdict maxlength dict def
125 basefontdict {
126 exch dup dup /FID ne exch /FontMatrix ne and
127 { exch newfontdict 3 1 roll put }
128 { pop pop }
129 ifelse
130 } forall
131 newfontdict /FontName newfontname put
132 newfontdict /FontMatrix newfontmatrix readonly put
133 newfontname newfontdict definefont pop
135 }""")
138 class PSchangefontmatrix(pswriter.PSresource):
140 """ change font matrix of a PostScript font"""
142 def __init__(self, basefontname, newfontname, newfontmatrix):
143 """ change the font matrix """
145 self.type = "changefontmatrix"
146 self.basefontname = basefontname
147 self.id = self.newfontname = newfontname
148 self.newfontmatrix = newfontmatrix
150 def output(self, file, writer, registry):
151 file.write("%%%%BeginResource: %s\n" % self.newfontname)
152 file.write("/%s /%s\n" % (self.basefontname, self.newfontname))
153 file.write(str(self.newfontmatrix))
154 file.write("\nChangeFontMatrix\n")
155 file.write("%%EndResource\n")
158 ##############################################################################
159 # PDF resources
160 ##############################################################################
162 class PDFfont(pdfwriter.PDFobject):
164 def __init__(self, fontname, basefontname, charcodes, fontdescriptor, encoding, metric):
165 pdfwriter.PDFobject.__init__(self, "font", fontname)
167 self.fontname = fontname
168 self.basefontname = basefontname
169 self.charcodes = set(charcodes)
170 self.fontdescriptor = fontdescriptor
171 self.encoding = encoding
172 self.metric = metric
174 def merge(self, other):
175 self.charcodes.update(other.charcodes)
177 def write(self, file, writer, registry):
178 file.write("<<\n"
179 "/Type /Font\n"
180 "/Subtype /Type1\n")
181 file.write("/Name /%s\n" % self.fontname)
182 file.write("/BaseFont /%s\n" % self.basefontname)
183 firstchar = min(self.charcodes)
184 lastchar = max(self.charcodes)
185 file.write("/FirstChar %d\n" % firstchar)
186 file.write("/LastChar %d\n" % lastchar)
187 file.write("/Widths\n"
188 "[")
189 if self.encoding:
190 encoding = self.encoding.getvector()
191 else:
192 encoding = self.fontdescriptor.fontfile.t1file.encoding
193 for i in range(firstchar, lastchar+1):
194 if i:
195 if not (i % 8):
196 file.write("\n")
197 else:
198 file.write(" ")
199 if self.metric is not None:
200 file.write("%i" % self.metric.width_ds(encoding[i]))
201 else:
202 file.write("%i" % self.fontdescriptor.fontfile.t1file.getglyphinfo(encoding[i])[0])
203 file.write("]\n")
204 file.write("/FontDescriptor %d 0 R\n" % registry.getrefno(self.fontdescriptor))
205 if self.encoding:
206 file.write("/Encoding %d 0 R\n" % registry.getrefno(self.encoding))
207 file.write(">>\n")
210 class PDFfontdescriptor(pdfwriter.PDFobject):
212 def __init__(self, fontname, fontfile, metric):
213 pdfwriter.PDFobject.__init__(self, "fontdescriptor", fontname)
214 self.fontname = fontname
215 self.fontfile = fontfile
216 self.metric = metric
218 def write(self, file, writer, registry):
219 file.write("<<\n"
220 "/Type /FontDescriptor\n"
221 "/FontName /%s\n" % self.fontname)
222 if self.metric is not None:
223 self.metric.writePDFfontinfo(file)
224 else:
225 self.fontfile.t1file.writePDFfontinfo(file)
226 if self.fontfile is not None:
227 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
228 file.write(">>\n")
231 class PDFfontfile(pdfwriter.PDFobject):
233 def __init__(self, t1file, glyphnames, charcodes):
234 pdfwriter.PDFobject.__init__(self, "fontfile", t1file.name)
235 self.t1file = t1file
236 self.glyphnames = set(glyphnames)
237 self.charcodes = set(charcodes)
239 def merge(self, other):
240 self.glyphnames.update(other.glyphnames)
241 self.charcodes.update(other.charcodes)
243 def write(self, file, writer, registry):
244 if writer.stripfonts:
245 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPDF(file, writer)
246 else:
247 self.t1file.outputPDF(file, writer)
250 class PDFencoding(pdfwriter.PDFobject):
252 def __init__(self, encoding, name):
253 pdfwriter.PDFobject.__init__(self, "encoding", name)
254 self.encoding = encoding
256 def getvector(self):
257 # As self.encoding might be appended after the constructur has set it,
258 # we need to defer the calculation until the whole content was constructed.
259 vector = [None] * len(self.encoding)
260 for glyphname, charcode in self.encoding.items():
261 vector[charcode] = glyphname
262 return vector
264 def write(self, file, writer, registry):
265 file.write("<<\n"
266 "/Type /Encoding\n"
267 "/Differences\n"
268 "[0")
269 for i, glyphname in enumerate(self.getvector()):
270 if i:
271 if not (i % 8):
272 file.write("\n")
273 else:
274 file.write(" ")
275 file.write("/%s" % glyphname)
276 file.write("]\n"
277 ">>\n")
280 ##############################################################################
281 # basic PyX text output
282 ##############################################################################
284 class font:
286 def text(self, x, y, charcodes, size_pt, **kwargs):
287 return self.text_pt(unit.topt(x), unit.topt(y), charcodes, size_pt, **kwargs)
290 class T1font(font):
292 def __init__(self, t1file, metric):
293 self.t1file = t1file
294 self.name = t1file.name
295 self.metric = metric
297 def text_pt(self, x, y, charcodes, size_pt, **kwargs):
298 return T1text_pt(self, x, y, charcodes, size_pt, **kwargs)
301 class T1builtinfont(T1font):
303 def __init__(self, name, metric):
304 self.name = name
305 self.t1file = None
306 self.metric = metric
309 class selectedfont:
311 def __init__(self, name, size_pt):
312 self.name = name
313 self.size_pt = size_pt
315 def __ne__(self, other):
316 return self.name != other.name or self.size_pt != other.size_pt
318 def outputPS(self, file, writer):
319 file.write("/%s %f selectfont\n" % (self.name, self.size_pt))
321 def outputPDF(self, file, writer):
322 file.write("/%s %f Tf\n" % (self.name, self.size_pt))
325 class text_pt(canvasitem.canvasitem):
327 pass
330 class T1text_pt(text_pt):
332 def __init__(self, font, x_pt, y_pt, charcodes, size_pt, decoding=None, slant=None, ignorebbox=False, kerning=False, ligatures=False):
333 if decoding is not None:
334 self.glyphnames = [decoding[character] for character in charcodes]
335 self.decode = True
336 else:
337 self.charcodes = charcodes
338 self.decode = False
339 self.font = font
340 self.x_pt = x_pt
341 self.y_pt = y_pt
342 self.size_pt = size_pt
343 self.slant = slant
344 self.ignorebbox = ignorebbox
345 self.kerning = kerning
346 self.ligatures = ligatures
348 if self.kerning and not self.decode:
349 raise ValueError("decoding required for font metric access (kerning)")
350 if self.ligatures and not self.decode:
351 raise ValueError("decoding required for font metric access (ligatures)")
352 if self.ligatures:
353 self.glyphnames = self.font.metric.resolveligatures(self.glyphnames)
355 def bbox(self):
356 if self.font.metric is None:
357 raise ValueError("metric missing")
358 if not self.decode:
359 raise ValueError("decoding required for font metric access (bbox)")
360 return bbox.bbox_pt(self.x_pt,
361 self.y_pt+self.font.metric.depth_pt(self.glyphnames, self.size_pt),
362 self.x_pt+self.font.metric.width_pt(self.glyphnames, self.size_pt),
363 self.y_pt+self.font.metric.height_pt(self.glyphnames, self.size_pt))
365 def getencodingname(self, encodings):
366 """returns the name of the encoding (in encodings) mapping self.glyphnames to codepoints
367 If no such encoding can be found or extended, a new encoding is added to encodings
369 glyphnames = set(self.glyphnames)
370 if len(glyphnames) > 256:
371 raise ValueError("glyphs do not fit into one single encoding")
372 for encodingname, encoding in encodings.items():
373 glyphsmissing = []
374 for glyphname in glyphnames:
375 if glyphname not in encoding.keys():
376 glyphsmissing.append(glyphname)
378 if len(glyphsmissing) + len(encoding) < 256:
379 # new glyphs fit in existing encoding which will thus be extended
380 for glyphname in glyphsmissing:
381 encoding[glyphname] = len(encoding)
382 return encodingname
383 # create a new encoding for the glyphnames
384 encodingname = "encoding%d" % len(encodings)
385 encodings[encodingname] = dict([(glyphname, i) for i, glyphname in enumerate(glyphnames)])
386 return encodingname
388 def processPS(self, file, writer, context, registry, bbox):
389 if not self.ignorebbox:
390 bbox += self.bbox()
392 if writer.textaspath:
393 if self.decode:
394 if self.kerning:
395 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
396 else:
397 data = self.glyphnames
398 else:
399 data = self.charcodes
400 textpath = path.path()
401 for i, value in enumerate(data):
402 if self.kerning and i % 2:
403 if value is not None:
404 self.x_pt += value
405 else:
406 glyphpath, wx_pt, wy_pt = self.font.t1file.getglyphpathwxwy_pt(value, self.size_pt, convertcharcode=not self.decode)
407 textpath += glyphpath.transformed(trafo.translate_pt(self.x_pt, self.y_pt))
408 self.x_pt += wx_pt
409 self.y_pt += wy_pt
410 deco.decoratedpath(textpath, fillstyles=[]).processPS(file, writer, context, registry, bbox)
411 else:
412 # register resources
413 if self.font.t1file is not None:
414 if self.decode:
415 registry.add(PST1file(self.font.t1file, self.glyphnames, []))
416 else:
417 registry.add(PST1file(self.font.t1file, [], self.charcodes))
419 fontname = self.font.name
420 if self.decode:
421 encodingname = self.getencodingname(context.encodings.setdefault(self.font.name, {}))
422 encoding = context.encodings[self.font.name][encodingname]
423 newfontname = "%s-%s" % (fontname, encodingname)
424 registry.add(_ReEncodeFont)
425 registry.add(PSreencodefont(fontname, newfontname, encoding))
426 fontname = newfontname
428 if self.slant:
429 newfontmatrix = trafo.trafo_pt(matrix=((1, self.slant), (0, 1))) * self.font.t1file.fontmatrix
430 newfontname = "%s-slant%f" % (fontname, self.slant)
431 registry.add(_ChangeFontMatrix)
432 registry.add(PSchangefontmatrix(fontname, newfontname, newfontmatrix))
433 fontname = newfontname
435 # select font if necessary
436 sf = selectedfont(fontname, self.size_pt)
437 if context.selectedfont is None or sf != context.selectedfont:
438 context.selectedfont = sf
439 sf.outputPS(file, writer)
441 file.write("%f %f moveto (" % (self.x_pt, self.y_pt))
442 if self.decode:
443 if self.kerning:
444 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
445 else:
446 data = self.glyphnames
447 else:
448 data = self.charcodes
449 for i, value in enumerate(data):
450 if self.kerning and i % 2:
451 if value is not None:
452 file.write(") show\n%f 0 rmoveto (" % value)
453 else:
454 if self.decode:
455 value = encoding[value]
456 if 32 < value < 127 and chr(value) not in "()[]<>\\":
457 file.write("%s" % chr(value))
458 else:
459 file.write("\\%03o" % value)
460 file.write(") show\n")
462 def processPDF(self, file, writer, context, registry, bbox):
463 if not self.ignorebbox:
464 bbox += self.bbox()
466 if writer.textaspath:
467 if self.decode:
468 if self.kerning:
469 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
470 else:
471 data = self.glyphnames
472 else:
473 data = self.charcodes
474 textpath = path.path()
475 for i, value in enumerate(data):
476 if self.kerning and i % 2:
477 if value is not None:
478 self.x_pt += value
479 else:
480 glyphpath, wx_pt, wy_pt = self.font.t1file.getglyphpathwxwy_pt(value, self.size_pt, convertcharcode=not self.decode)
481 textpath += glyphpath.transformed(trafo.translate_pt(self.x_pt, self.y_pt))
482 self.x_pt += wx_pt
483 self.y_pt += wy_pt
484 deco.decoratedpath(textpath, fillstyles=[]).processPDF(file, writer, context, registry, bbox)
485 else:
486 if self.decode:
487 encodingname = self.getencodingname(context.encodings.setdefault(self.font.name, {}))
488 encoding = context.encodings[self.font.name][encodingname]
489 charcodes = [encoding[glyphname] for glyphname in self.glyphnames]
490 else:
491 charcodes = self.charcodes
493 # create resources
494 fontname = self.font.name
495 if self.decode:
496 newfontname = "%s-%s" % (fontname, encodingname)
497 _encoding = PDFencoding(encoding, newfontname)
498 fontname = newfontname
499 else:
500 _encoding = None
501 if self.font.t1file is not None:
502 if self.decode:
503 fontfile = PDFfontfile(self.font.t1file, self.glyphnames, [])
504 else:
505 fontfile = PDFfontfile(self.font.t1file, [], self.charcodes)
506 else:
507 fontfile = None
508 fontdescriptor = PDFfontdescriptor(self.font.name, fontfile, self.font.metric)
509 font = PDFfont(fontname, self.font.name, charcodes, fontdescriptor, _encoding, self.font.metric)
511 # register resources
512 if fontfile is not None:
513 registry.add(fontfile)
514 registry.add(fontdescriptor)
515 if _encoding is not None:
516 registry.add(_encoding)
517 registry.add(font)
519 registry.addresource("Font", fontname, font, procset="Text")
521 if self.slant is None:
522 slantvalue = 0
523 else:
524 slantvalue = self.slant
526 # select font if necessary
527 sf = selectedfont(fontname, self.size_pt)
528 if context.selectedfont is None or sf != context.selectedfont:
529 context.selectedfont = sf
530 sf.outputPDF(file, writer)
532 if self.kerning:
533 file.write("1 0 %f 1 %f %f Tm [(" % (slantvalue, self.x_pt, self.y_pt))
534 else:
535 file.write("1 0 %f 1 %f %f Tm (" % (slantvalue, self.x_pt, self.y_pt))
536 if self.decode:
537 if self.kerning:
538 data = self.font.metric.resolvekernings(self.glyphnames)
539 else:
540 data = self.glyphnames
541 else:
542 data = self.charcodes
543 for i, value in enumerate(data):
544 if self.kerning and i % 2:
545 if value is not None:
546 file.write(")%f(" % (-value))
547 else:
548 if self.decode:
549 value = encoding[value]
550 if 32 <= value <= 127 and chr(value) not in "()[]<>\\":
551 file.write("%s" % chr(value))
552 else:
553 file.write("\\%03o" % value)
554 if self.kerning:
555 file.write(")] TJ\n")
556 else:
557 file.write(") Tj\n")