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