remove code duplication
[PyX/mjg.git] / pyx / font / font.py
blobea319ed6a174d52c37ab38c4cf0feb4d497032b3
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2005-2011 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2006-2011 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2005-2011 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 from pyx import bbox, canvasitem, deco, path, pswriter, pdfwriter, trafo, unit
25 import t1file
27 try:
28 set()
29 except NameError:
30 # Python 2.3
31 from sets import Set as set
34 ##############################################################################
35 # PS resources
36 ##############################################################################
38 class PST1file(pswriter.PSresource):
40 """ PostScript font definition included in the prolog """
42 def __init__(self, t1file, glyphnames, charcodes):
43 """ include type 1 font t1file stripped to the given glyphnames"""
44 self.type = "t1file"
45 self.t1file = t1file
46 self.id = t1file.name
47 self.glyphnames = set(glyphnames)
48 self.charcodes = set(charcodes)
50 def merge(self, other):
51 self.glyphnames.update(other.glyphnames)
52 self.charcodes.update(other.charcodes)
54 def output(self, file, writer, registry):
55 file.write("%%%%BeginFont: %s\n" % self.t1file.name)
56 if writer.strip_fonts:
57 if self.glyphnames:
58 file.write("%%Included glyphs: %s\n" % " ".join(self.glyphnames))
59 if self.charcodes:
60 file.write("%%Included charcodes: %s\n" % " ".join([str(charcode) for charcode in self.charcodes]))
61 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPS(file, writer)
62 else:
63 self.t1file.outputPS(file, writer)
64 file.write("\n%%EndFont\n")
67 _ReEncodeFont = pswriter.PSdefinition("ReEncodeFont", """{
68 5 dict
69 begin
70 /newencoding exch def
71 /newfontname exch def
72 /basefontname exch def
73 /basefontdict basefontname findfont def
74 /newfontdict basefontdict maxlength dict def
75 basefontdict {
76 exch dup dup /FID ne exch /Encoding ne and
77 { exch newfontdict 3 1 roll put }
78 { pop pop }
79 ifelse
80 } forall
81 newfontdict /FontName newfontname put
82 newfontdict /Encoding newencoding put
83 newfontname newfontdict definefont pop
84 end
85 }""")
88 class PSreencodefont(pswriter.PSresource):
90 """ reencoded PostScript font"""
92 def __init__(self, basefontname, newfontname, encoding):
93 """ reencode the font """
95 self.type = "reencodefont"
96 self.basefontname = basefontname
97 self.id = self.newfontname = newfontname
98 self.encoding = encoding
100 def output(self, file, writer, registry):
101 file.write("%%%%BeginResource: %s\n" % self.newfontname)
102 file.write("/%s /%s\n[" % (self.basefontname, self.newfontname))
103 vector = [None] * len(self.encoding)
104 for glyphname, charcode in self.encoding.items():
105 vector[charcode] = glyphname
106 for i, glyphname in enumerate(vector):
107 if i:
108 if not (i % 8):
109 file.write("\n")
110 else:
111 file.write(" ")
112 file.write("/%s" % glyphname)
113 file.write("]\n")
114 file.write("ReEncodeFont\n")
115 file.write("%%EndResource\n")
118 _ChangeFontMatrix = pswriter.PSdefinition("ChangeFontMatrix", """{
119 5 dict
120 begin
121 /newfontmatrix exch def
122 /newfontname exch def
123 /basefontname exch def
124 /basefontdict basefontname findfont def
125 /newfontdict basefontdict maxlength dict def
126 basefontdict {
127 exch dup dup /FID ne exch /FontMatrix ne and
128 { exch newfontdict 3 1 roll put }
129 { pop pop }
130 ifelse
131 } forall
132 newfontdict /FontName newfontname put
133 newfontdict /FontMatrix newfontmatrix readonly put
134 newfontname newfontdict definefont pop
136 }""")
139 class PSchangefontmatrix(pswriter.PSresource):
141 """ change font matrix of a PostScript font"""
143 def __init__(self, basefontname, newfontname, newfontmatrix):
144 """ change the font matrix """
146 self.type = "changefontmatrix"
147 self.basefontname = basefontname
148 self.id = self.newfontname = newfontname
149 self.newfontmatrix = newfontmatrix
151 def output(self, file, writer, registry):
152 file.write("%%%%BeginResource: %s\n" % self.newfontname)
153 file.write("/%s /%s\n" % (self.basefontname, self.newfontname))
154 file.write(str(self.newfontmatrix))
155 file.write("\nChangeFontMatrix\n")
156 file.write("%%EndResource\n")
159 ##############################################################################
160 # PDF resources
161 ##############################################################################
163 class PDFfont(pdfwriter.PDFobject):
165 def __init__(self, fontname, basefontname, charcodes, fontdescriptor, encoding, metric):
166 pdfwriter.PDFobject.__init__(self, "font", fontname)
168 self.fontname = fontname
169 self.basefontname = basefontname
170 self.charcodes = set(charcodes)
171 self.fontdescriptor = fontdescriptor
172 self.encoding = encoding
173 self.metric = metric
175 def merge(self, other):
176 self.charcodes.update(other.charcodes)
178 def write(self, file, writer, registry):
179 file.write("<<\n"
180 "/Type /Font\n"
181 "/Subtype /Type1\n")
182 file.write("/Name /%s\n" % self.fontname)
183 file.write("/BaseFont /%s\n" % self.basefontname)
184 firstchar = min(self.charcodes)
185 lastchar = max(self.charcodes)
186 file.write("/FirstChar %d\n" % firstchar)
187 file.write("/LastChar %d\n" % lastchar)
188 file.write("/Widths\n"
189 "[")
190 if self.encoding:
191 encoding = self.encoding.getvector()
192 else:
193 encoding = self.fontdescriptor.fontfile.t1file.encoding
194 for i in range(firstchar, lastchar+1):
195 if i:
196 if not (i % 8):
197 file.write("\n")
198 else:
199 file.write(" ")
200 if i in self.charcodes:
201 if self.metric is not None:
202 file.write("%i" % self.metric.width_ds(encoding[i]))
203 else:
204 file.write("%i" % self.fontdescriptor.fontfile.t1file.getglyphinfo(encoding[i])[0])
205 else:
206 file.write("0")
207 file.write(" ]\n")
208 file.write("/FontDescriptor %d 0 R\n" % registry.getrefno(self.fontdescriptor))
209 if self.encoding:
210 file.write("/Encoding %d 0 R\n" % registry.getrefno(self.encoding))
211 file.write(">>\n")
214 class PDFstdfont(pdfwriter.PDFobject):
216 def __init__(self, basename):
217 pdfwriter.PDFobject.__init__(self, "font", "stdfont-%s" % basename)
218 self.name = basename # name is ignored by acroread
219 self.basename = basename
221 def write(self, file, writer, registry):
222 file.write("<</BaseFont /%s\n" % self.basename)
223 file.write("/Name /%s\n" % self.name)
224 file.write("/Type /Font\n")
225 file.write("/Subtype /Type1\n")
226 file.write(">>\n")
228 # the 14 standard fonts that are always available in PDF
229 PDFTimesRoman = PDFstdfont("Times-Roman")
230 PDFTimesBold = PDFstdfont("Times-Bold")
231 PDFTimesItalic = PDFstdfont("Times-Italic")
232 PDFTimesBoldItalic = PDFstdfont("Times-BoldItalic")
233 PDFHelvetica = PDFstdfont("Helvetica")
234 PDFHelveticaBold = PDFstdfont("Helvetica-Bold")
235 PDFHelveticaOblique = PDFstdfont("Helvetica-Oblique")
236 PDFHelveticaBoldOblique = PDFstdfont("Helvetica-BoldOblique")
237 PDFCourier = PDFstdfont("Courier")
238 PDFCourierBold = PDFstdfont("Courier-Bold")
239 PDFCourierOblique = PDFstdfont("Courier-Oblique")
240 PDFCourierBoldOblique = PDFstdfont("Courier-BoldOblique")
241 PDFSymbol = PDFstdfont("Symbol")
242 PDFZapfDingbats = PDFstdfont("ZapfDingbats")
245 class PDFfontdescriptor(pdfwriter.PDFobject):
247 def __init__(self, fontname, fontfile, metric):
248 pdfwriter.PDFobject.__init__(self, "fontdescriptor", fontname)
249 self.fontname = fontname
250 self.fontfile = fontfile
251 self.metric = metric
253 def write(self, file, writer, registry):
254 file.write("<<\n"
255 "/Type /FontDescriptor\n"
256 "/FontName /%s\n" % self.fontname)
257 if self.metric is not None:
258 self.metric.writePDFfontinfo(file)
259 else:
260 self.fontfile.t1file.writePDFfontinfo(file)
261 if self.fontfile is not None:
262 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
263 file.write(">>\n")
266 class PDFfontfile(pdfwriter.PDFobject):
268 def __init__(self, t1file, glyphnames, charcodes):
269 pdfwriter.PDFobject.__init__(self, "fontfile", t1file.name)
270 self.t1file = t1file
271 self.glyphnames = set(glyphnames)
272 self.charcodes = set(charcodes)
274 def merge(self, other):
275 self.glyphnames.update(other.glyphnames)
276 self.charcodes.update(other.charcodes)
278 def write(self, file, writer, registry):
279 if writer.strip_fonts:
280 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPDF(file, writer)
281 else:
282 self.t1file.outputPDF(file, writer)
285 class PDFencoding(pdfwriter.PDFobject):
287 def __init__(self, encoding, name):
288 pdfwriter.PDFobject.__init__(self, "encoding", name)
289 self.encoding = encoding
291 def getvector(self):
292 # As self.encoding might be appended after the constructur has set it,
293 # we need to defer the calculation until the whole content was constructed.
294 vector = [None] * len(self.encoding)
295 for glyphname, charcode in self.encoding.items():
296 vector[charcode] = glyphname
297 return vector
299 def write(self, file, writer, registry):
300 file.write("<<\n"
301 "/Type /Encoding\n"
302 "/Differences\n"
303 "[0")
304 for i, glyphname in enumerate(self.getvector()):
305 if i:
306 if not (i % 8):
307 file.write("\n")
308 else:
309 file.write(" ")
310 file.write("/%s" % glyphname)
311 file.write("]\n"
312 ">>\n")
315 ##############################################################################
316 # basic PyX text output
317 ##############################################################################
319 class font:
321 def text(self, x, y, charcodes, size_pt, **kwargs):
322 return self.text_pt(unit.topt(x), unit.topt(y), charcodes, size_pt, **kwargs)
325 class T1font(font):
327 def __init__(self, t1file, metric):
328 self.t1file = t1file
329 self.name = t1file.name
330 self.metric = metric
332 def text_pt(self, x, y, charcodes, size_pt, **kwargs):
333 return T1text_pt(self, x, y, charcodes, size_pt, **kwargs)
336 class T1builtinfont(T1font):
338 def __init__(self, name, metric):
339 self.name = name
340 self.t1file = None
341 self.metric = metric
344 class selectedfont:
346 def __init__(self, name, size_pt):
347 self.name = name
348 self.size_pt = size_pt
350 def __ne__(self, other):
351 return self.name != other.name or self.size_pt != other.size_pt
353 def outputPS(self, file, writer):
354 file.write("/%s %f selectfont\n" % (self.name, self.size_pt))
356 def outputPDF(self, file, writer):
357 file.write("/%s %f Tf\n" % (self.name, self.size_pt))
360 class text_pt(canvasitem.canvasitem):
362 pass
365 class T1text_pt(text_pt):
367 def __init__(self, font, x_pt, y_pt, charcodes, size_pt, decoding=None, slant=None, ignorebbox=False, kerning=False, ligatures=False, spaced_pt=0):
368 if decoding is not None:
369 self.glyphnames = [decoding[character] for character in charcodes]
370 self.decode = True
371 else:
372 self.charcodes = charcodes
373 self.decode = False
374 self.font = font
375 self.x_pt = x_pt
376 self.y_pt = y_pt
377 self.size_pt = size_pt
378 self.slant = slant
379 self.ignorebbox = ignorebbox
380 self.kerning = kerning
381 self.ligatures = ligatures
382 self.spaced_pt = spaced_pt
384 if self.kerning and not self.decode:
385 raise ValueError("decoding required for font metric access (kerning)")
386 if self.ligatures and not self.decode:
387 raise ValueError("decoding required for font metric access (ligatures)")
388 if self.ligatures:
389 self.glyphnames = self.font.metric.resolveligatures(self.glyphnames)
391 def bbox(self):
392 if self.font.metric is None:
393 raise ValueError("metric missing")
394 if not self.decode:
395 raise ValueError("decoding required for font metric access (bbox)")
396 return bbox.bbox_pt(self.x_pt,
397 self.y_pt+self.font.metric.depth_pt(self.glyphnames, self.size_pt),
398 self.x_pt+self.font.metric.width_pt(self.glyphnames, self.size_pt),
399 self.y_pt+self.font.metric.height_pt(self.glyphnames, self.size_pt))
401 def getencodingname(self, encodings):
402 """returns the name of the encoding (in encodings) mapping self.glyphnames to codepoints
403 If no such encoding can be found or extended, a new encoding is added to encodings
405 glyphnames = set(self.glyphnames)
406 if len(glyphnames) > 256:
407 raise ValueError("glyphs do not fit into one single encoding")
408 for encodingname, encoding in encodings.items():
409 glyphsmissing = []
410 for glyphname in glyphnames:
411 if glyphname not in encoding.keys():
412 glyphsmissing.append(glyphname)
414 if len(glyphsmissing) + len(encoding) < 256:
415 # new glyphs fit in existing encoding which will thus be extended
416 for glyphname in glyphsmissing:
417 encoding[glyphname] = len(encoding)
418 return encodingname
419 # create a new encoding for the glyphnames
420 encodingname = "encoding%d" % len(encodings)
421 encodings[encodingname] = dict([(glyphname, i) for i, glyphname in enumerate(glyphnames)])
422 return encodingname
424 def textpath(self):
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 return textpath
448 def processPS(self, file, writer, context, registry, bbox):
449 if not self.ignorebbox:
450 bbox += self.bbox()
452 if writer.text_as_path:
453 deco.decoratedpath(self.textpath(), fillstyles=[]).processPS(file, writer, context, registry, bbox)
454 else:
455 # register resources
456 if self.font.t1file is not None:
457 if self.decode:
458 registry.add(PST1file(self.font.t1file, self.glyphnames, []))
459 else:
460 registry.add(PST1file(self.font.t1file, [], self.charcodes))
462 fontname = self.font.name
463 if self.decode:
464 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
465 encoding = writer.encodings[self.font.name][encodingname]
466 newfontname = "%s-%s" % (fontname, encodingname)
467 registry.add(_ReEncodeFont)
468 registry.add(PSreencodefont(fontname, newfontname, encoding))
469 fontname = newfontname
471 if self.slant:
472 newfontmatrix = trafo.trafo_pt(matrix=((1, self.slant), (0, 1)))
473 if self.font.t1file is not None:
474 newfontmatrix = newfontmatrix * self.font.t1file.fontmatrix
475 newfontname = "%s-slant%f" % (fontname, self.slant)
476 registry.add(_ChangeFontMatrix)
477 registry.add(PSchangefontmatrix(fontname, newfontname, newfontmatrix))
478 fontname = newfontname
480 # select font if necessary
481 sf = selectedfont(fontname, self.size_pt)
482 if context.selectedfont is None or sf != context.selectedfont:
483 context.selectedfont = sf
484 sf.outputPS(file, writer)
486 file.write("%f %f moveto (" % (self.x_pt, self.y_pt))
487 if self.decode:
488 if self.kerning:
489 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
490 else:
491 data = self.glyphnames
492 else:
493 data = self.charcodes
494 for i, value in enumerate(data):
495 if self.kerning and i % 2:
496 if value is not None:
497 file.write(") show\n%f 0 rmoveto (" % (value+self.spaced_pt))
498 elif self.spaced_pt:
499 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
500 else:
501 if i and not self.kerning and self.spaced_pt:
502 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
503 if self.decode:
504 value = encoding[value]
505 if 32 < value < 127 and chr(value) not in "()[]<>\\":
506 file.write("%s" % chr(value))
507 else:
508 file.write("\\%03o" % value)
509 file.write(") show\n")
511 def processPDF(self, file, writer, context, registry, bbox):
512 if not self.ignorebbox:
513 bbox += self.bbox()
515 if writer.text_as_path:
516 deco.decoratedpath(self.textpath(), fillstyles=[]).processPDF(file, writer, context, registry, bbox)
517 else:
518 if self.decode:
519 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
520 encoding = writer.encodings[self.font.name][encodingname]
521 charcodes = [encoding[glyphname] for glyphname in self.glyphnames]
522 else:
523 charcodes = self.charcodes
525 # create resources
526 fontname = self.font.name
527 if self.decode:
528 newfontname = "%s-%s" % (fontname, encodingname)
529 _encoding = PDFencoding(encoding, newfontname)
530 fontname = newfontname
531 else:
532 _encoding = None
533 if self.font.t1file is not None:
534 if self.decode:
535 fontfile = PDFfontfile(self.font.t1file, self.glyphnames, [])
536 else:
537 fontfile = PDFfontfile(self.font.t1file, [], self.charcodes)
538 else:
539 fontfile = None
540 fontdescriptor = PDFfontdescriptor(self.font.name, fontfile, self.font.metric)
541 font = PDFfont(fontname, self.font.name, charcodes, fontdescriptor, _encoding, self.font.metric)
543 # register resources
544 if fontfile is not None:
545 registry.add(fontfile)
546 registry.add(fontdescriptor)
547 if _encoding is not None:
548 registry.add(_encoding)
549 registry.add(font)
551 registry.addresource("Font", fontname, font, procset="Text")
553 if self.slant is None:
554 slantvalue = 0
555 else:
556 slantvalue = self.slant
558 # select font if necessary
559 sf = selectedfont(fontname, self.size_pt)
560 if context.selectedfont is None or sf != context.selectedfont:
561 context.selectedfont = sf
562 sf.outputPDF(file, writer)
564 if self.kerning:
565 file.write("1 0 %f 1 %f %f Tm [(" % (slantvalue, self.x_pt, self.y_pt))
566 else:
567 file.write("1 0 %f 1 %f %f Tm (" % (slantvalue, self.x_pt, self.y_pt))
568 if self.decode:
569 if self.kerning:
570 data = self.font.metric.resolvekernings(self.glyphnames)
571 else:
572 data = self.glyphnames
573 else:
574 data = self.charcodes
575 for i, value in enumerate(data):
576 if self.kerning and i % 2:
577 if value is not None:
578 file.write(")%f(" % (-value-self.spaced_pt))
579 elif self.spaced_pt:
580 file.write(")%f(" % (-self.spaced_pt))
581 else:
582 if i and not self.kerning and self.spaced_pt:
583 file.write(")%f(" % (-self.spaced_pt))
584 if self.decode:
585 value = encoding[value]
586 if 32 <= value <= 127 and chr(value) not in "()[]<>\\":
587 file.write("%s" % chr(value))
588 else:
589 file.write("\\%03o" % value)
590 if self.kerning:
591 file.write(")] TJ\n")
592 else:
593 file.write(") Tj\n")