make encodings a writer property
[PyX/mjg.git] / pyx / font / font.py
blob898fef41ee2a9847531b2a1ee6dc33a8f911f123
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 PDFstdfont(pdfwriter.PDFobject):
212 def __init__(self, name, basename):
213 pdfwriter.PDFobject.__init__(self, "font", "stdfont-%s" % name)
214 self.name = name
215 self.basename = basename
217 def write(self, file, writer, registry):
218 file.write("<</BaseFont /%s\n" % self.basename)
219 file.write("/Name /%s\n" % self.name)
220 file.write("/Type /Font\n")
221 file.write("/Subtype /Type1\n")
222 file.write(">>\n")
224 # the 14 standard fonts that are always available in PDF
225 PDFTimesRoman = PDFstdfont("Time", "Times-Roman")
226 PDFTimesBold = PDFstdfont("TiBo", "Times-Bold")
227 PDFTimesItalic = PDFstdfont("TiIt", "Times-Italic")
228 PDFTimesBoldItalic = PDFstdfont("TiBI", "Times-BoldItalic")
229 PDFHelvetica = PDFstdfont("Helv", "Helvetica")
230 PDFHelveticaBold = PDFstdfont("HeBo", "Helvetica-Bold")
231 PDFHelveticaOblique = PDFstdfont("HeOb", "Helvetica-Oblique")
232 PDFHelveticaBoldOblique = PDFstdfont("HeBO", "Helvetica-BoldOblique")
233 PDFCourier = PDFstdfont("Cour", "Courier")
234 PDFCourierBold = PDFstdfont("CoBo", "Courier-Bold")
235 PDFCourierOblique = PDFstdfont("CoOb", "Courier-Oblique")
236 PDFCourierBoldOblique = PDFstdfont("CoBO", "Courier-BoldOblique")
237 PDFSymbol = PDFstdfont("Symb", "Symbol")
238 PDFZapfDingbats = PDFstdfont("Zapf", "ZapfDingbats")
241 class PDFfontdescriptor(pdfwriter.PDFobject):
243 def __init__(self, fontname, fontfile, metric):
244 pdfwriter.PDFobject.__init__(self, "fontdescriptor", fontname)
245 self.fontname = fontname
246 self.fontfile = fontfile
247 self.metric = metric
249 def write(self, file, writer, registry):
250 file.write("<<\n"
251 "/Type /FontDescriptor\n"
252 "/FontName /%s\n" % self.fontname)
253 if self.metric is not None:
254 self.metric.writePDFfontinfo(file)
255 else:
256 self.fontfile.t1file.writePDFfontinfo(file)
257 if self.fontfile is not None:
258 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
259 file.write(">>\n")
262 class PDFfontfile(pdfwriter.PDFobject):
264 def __init__(self, t1file, glyphnames, charcodes):
265 pdfwriter.PDFobject.__init__(self, "fontfile", t1file.name)
266 self.t1file = t1file
267 self.glyphnames = set(glyphnames)
268 self.charcodes = set(charcodes)
270 def merge(self, other):
271 self.glyphnames.update(other.glyphnames)
272 self.charcodes.update(other.charcodes)
274 def write(self, file, writer, registry):
275 if writer.stripfonts:
276 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPDF(file, writer)
277 else:
278 self.t1file.outputPDF(file, writer)
281 class PDFencoding(pdfwriter.PDFobject):
283 def __init__(self, encoding, name):
284 pdfwriter.PDFobject.__init__(self, "encoding", name)
285 self.encoding = encoding
287 def getvector(self):
288 # As self.encoding might be appended after the constructur has set it,
289 # we need to defer the calculation until the whole content was constructed.
290 vector = [None] * len(self.encoding)
291 for glyphname, charcode in self.encoding.items():
292 vector[charcode] = glyphname
293 return vector
295 def write(self, file, writer, registry):
296 file.write("<<\n"
297 "/Type /Encoding\n"
298 "/Differences\n"
299 "[0")
300 for i, glyphname in enumerate(self.getvector()):
301 if i:
302 if not (i % 8):
303 file.write("\n")
304 else:
305 file.write(" ")
306 file.write("/%s" % glyphname)
307 file.write("]\n"
308 ">>\n")
311 ##############################################################################
312 # basic PyX text output
313 ##############################################################################
315 class font:
317 def text(self, x, y, charcodes, size_pt, **kwargs):
318 return self.text_pt(unit.topt(x), unit.topt(y), charcodes, size_pt, **kwargs)
321 class T1font(font):
323 def __init__(self, t1file, metric):
324 self.t1file = t1file
325 self.name = t1file.name
326 self.metric = metric
328 def text_pt(self, x, y, charcodes, size_pt, **kwargs):
329 return T1text_pt(self, x, y, charcodes, size_pt, **kwargs)
332 class T1builtinfont(T1font):
334 def __init__(self, name, metric):
335 self.name = name
336 self.t1file = None
337 self.metric = metric
340 class selectedfont:
342 def __init__(self, name, size_pt):
343 self.name = name
344 self.size_pt = size_pt
346 def __ne__(self, other):
347 return self.name != other.name or self.size_pt != other.size_pt
349 def outputPS(self, file, writer):
350 file.write("/%s %f selectfont\n" % (self.name, self.size_pt))
352 def outputPDF(self, file, writer):
353 file.write("/%s %f Tf\n" % (self.name, self.size_pt))
356 class text_pt(canvasitem.canvasitem):
358 pass
361 class T1text_pt(text_pt):
363 def __init__(self, font, x_pt, y_pt, charcodes, size_pt, decoding=None, slant=None, ignorebbox=False, kerning=False, ligatures=False, spaced_pt=0):
364 if decoding is not None:
365 self.glyphnames = [decoding[character] for character in charcodes]
366 self.decode = True
367 else:
368 self.charcodes = charcodes
369 self.decode = False
370 self.font = font
371 self.x_pt = x_pt
372 self.y_pt = y_pt
373 self.size_pt = size_pt
374 self.slant = slant
375 self.ignorebbox = ignorebbox
376 self.kerning = kerning
377 self.ligatures = ligatures
378 self.spaced_pt = spaced_pt
380 if self.kerning and not self.decode:
381 raise ValueError("decoding required for font metric access (kerning)")
382 if self.ligatures and not self.decode:
383 raise ValueError("decoding required for font metric access (ligatures)")
384 if self.ligatures:
385 self.glyphnames = self.font.metric.resolveligatures(self.glyphnames)
387 def bbox(self):
388 if self.font.metric is None:
389 raise ValueError("metric missing")
390 if not self.decode:
391 raise ValueError("decoding required for font metric access (bbox)")
392 return bbox.bbox_pt(self.x_pt,
393 self.y_pt+self.font.metric.depth_pt(self.glyphnames, self.size_pt),
394 self.x_pt+self.font.metric.width_pt(self.glyphnames, self.size_pt),
395 self.y_pt+self.font.metric.height_pt(self.glyphnames, self.size_pt))
397 def getencodingname(self, encodings):
398 """returns the name of the encoding (in encodings) mapping self.glyphnames to codepoints
399 If no such encoding can be found or extended, a new encoding is added to encodings
401 glyphnames = set(self.glyphnames)
402 if len(glyphnames) > 256:
403 raise ValueError("glyphs do not fit into one single encoding")
404 for encodingname, encoding in encodings.items():
405 glyphsmissing = []
406 for glyphname in glyphnames:
407 if glyphname not in encoding.keys():
408 glyphsmissing.append(glyphname)
410 if len(glyphsmissing) + len(encoding) < 256:
411 # new glyphs fit in existing encoding which will thus be extended
412 for glyphname in glyphsmissing:
413 encoding[glyphname] = len(encoding)
414 return encodingname
415 # create a new encoding for the glyphnames
416 encodingname = "encoding%d" % len(encodings)
417 encodings[encodingname] = dict([(glyphname, i) for i, glyphname in enumerate(glyphnames)])
418 return encodingname
420 def processPS(self, file, writer, context, registry, bbox):
421 if not self.ignorebbox:
422 bbox += self.bbox()
424 if writer.textaspath:
425 if self.decode:
426 if self.kerning:
427 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
428 else:
429 data = self.glyphnames
430 else:
431 data = self.charcodes
432 textpath = path.path()
433 x_pt = self.x_pt
434 y_pt = self.y_pt
435 for i, value in enumerate(data):
436 if self.kerning and i % 2:
437 if value is not None:
438 x_pt += value
439 else:
440 if i:
441 x_pt += self.spaced_pt
442 glyphpath, wx_pt, wy_pt = self.font.t1file.getglyphpathwxwy_pt(value, self.size_pt, convertcharcode=not self.decode)
443 textpath += glyphpath.transformed(trafo.translate_pt(x_pt, y_pt))
444 x_pt += wx_pt
445 y_pt += wy_pt
446 deco.decoratedpath(textpath, fillstyles=[]).processPS(file, writer, context, registry, bbox)
447 else:
448 # register resources
449 if self.font.t1file is not None:
450 if self.decode:
451 registry.add(PST1file(self.font.t1file, self.glyphnames, []))
452 else:
453 registry.add(PST1file(self.font.t1file, [], self.charcodes))
455 fontname = self.font.name
456 if self.decode:
457 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
458 encoding = writer.encodings[self.font.name][encodingname]
459 newfontname = "%s-%s" % (fontname, encodingname)
460 registry.add(_ReEncodeFont)
461 registry.add(PSreencodefont(fontname, newfontname, encoding))
462 fontname = newfontname
464 if self.slant:
465 newfontmatrix = trafo.trafo_pt(matrix=((1, self.slant), (0, 1))) * self.font.t1file.fontmatrix
466 newfontname = "%s-slant%f" % (fontname, self.slant)
467 registry.add(_ChangeFontMatrix)
468 registry.add(PSchangefontmatrix(fontname, newfontname, newfontmatrix))
469 fontname = newfontname
471 # select font if necessary
472 sf = selectedfont(fontname, self.size_pt)
473 if context.selectedfont is None or sf != context.selectedfont:
474 context.selectedfont = sf
475 sf.outputPS(file, writer)
477 file.write("%f %f moveto (" % (self.x_pt, self.y_pt))
478 if self.decode:
479 if self.kerning:
480 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
481 else:
482 data = self.glyphnames
483 else:
484 data = self.charcodes
485 for i, value in enumerate(data):
486 if self.kerning and i % 2:
487 if value is not None:
488 file.write(") show\n%f 0 rmoveto (" % (value+self.spaced_pt))
489 elif self.spaced_pt:
490 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
491 else:
492 if i and not self.kerning and self.spaced_pt:
493 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
494 if self.decode:
495 value = encoding[value]
496 if 32 < value < 127 and chr(value) not in "()[]<>\\":
497 file.write("%s" % chr(value))
498 else:
499 file.write("\\%03o" % value)
500 file.write(") show\n")
502 def processPDF(self, file, writer, context, registry, bbox):
503 if not self.ignorebbox:
504 bbox += self.bbox()
506 if writer.textaspath:
507 if self.decode:
508 if self.kerning:
509 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
510 else:
511 data = self.glyphnames
512 else:
513 data = self.charcodes
514 textpath = path.path()
515 x_pt = self.x_pt
516 y_pt = self.y_pt
517 for i, value in enumerate(data):
518 if self.kerning and i % 2:
519 if value is not None:
520 x_pt += value
521 else:
522 if i:
523 x_pt += self.spaced_pt
524 glyphpath, wx_pt, wy_pt = self.font.t1file.getglyphpathwxwy_pt(value, self.size_pt, convertcharcode=not self.decode)
525 textpath += glyphpath.transformed(trafo.translate_pt(x_pt, y_pt))
526 x_pt += wx_pt
527 y_pt += wy_pt
528 deco.decoratedpath(textpath, fillstyles=[]).processPDF(file, writer, context, registry, bbox)
529 else:
530 if self.decode:
531 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
532 encoding = writer.encodings[self.font.name][encodingname]
533 charcodes = [encoding[glyphname] for glyphname in self.glyphnames]
534 else:
535 charcodes = self.charcodes
537 # create resources
538 fontname = self.font.name
539 if self.decode:
540 newfontname = "%s-%s" % (fontname, encodingname)
541 _encoding = PDFencoding(encoding, newfontname)
542 fontname = newfontname
543 else:
544 _encoding = None
545 if self.font.t1file is not None:
546 if self.decode:
547 fontfile = PDFfontfile(self.font.t1file, self.glyphnames, [])
548 else:
549 fontfile = PDFfontfile(self.font.t1file, [], self.charcodes)
550 else:
551 fontfile = None
552 fontdescriptor = PDFfontdescriptor(self.font.name, fontfile, self.font.metric)
553 font = PDFfont(fontname, self.font.name, charcodes, fontdescriptor, _encoding, self.font.metric)
555 # register resources
556 if fontfile is not None:
557 registry.add(fontfile)
558 registry.add(fontdescriptor)
559 if _encoding is not None:
560 registry.add(_encoding)
561 registry.add(font)
563 registry.addresource("Font", fontname, font, procset="Text")
565 if self.slant is None:
566 slantvalue = 0
567 else:
568 slantvalue = self.slant
570 # select font if necessary
571 sf = selectedfont(fontname, self.size_pt)
572 if context.selectedfont is None or sf != context.selectedfont:
573 context.selectedfont = sf
574 sf.outputPDF(file, writer)
576 if self.kerning:
577 file.write("1 0 %f 1 %f %f Tm [(" % (slantvalue, self.x_pt, self.y_pt))
578 else:
579 file.write("1 0 %f 1 %f %f Tm (" % (slantvalue, self.x_pt, self.y_pt))
580 if self.decode:
581 if self.kerning:
582 data = self.font.metric.resolvekernings(self.glyphnames)
583 else:
584 data = self.glyphnames
585 else:
586 data = self.charcodes
587 for i, value in enumerate(data):
588 if self.kerning and i % 2:
589 if value is not None:
590 file.write(")%f(" % (-value-self.spaced_pt))
591 elif self.spaced_pt:
592 file.write(")%f(" % (-self.spaced_pt))
593 else:
594 if i and not self.kerning and self.spaced_pt:
595 file.write(")%f(" % (-self.spaced_pt))
596 if self.decode:
597 value = encoding[value]
598 if 32 <= value <= 127 and chr(value) not in "()[]<>\\":
599 file.write("%s" % chr(value))
600 else:
601 file.write("\\%03o" % value)
602 if self.kerning:
603 file.write(")] TJ\n")
604 else:
605 file.write(") Tj\n")