arrow decorator: proper sign of constriction length for positioning reversed arrows
[PyX/mjg.git] / pyx / font / font.py
blob195c2536dcbc0a31482d565e0dcd9dd39bdffc14
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 processPS(self, file, writer, context, registry, bbox):
425 if not self.ignorebbox:
426 bbox += self.bbox()
428 if writer.text_as_path:
429 if self.decode:
430 if self.kerning:
431 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
432 else:
433 data = self.glyphnames
434 else:
435 data = self.charcodes
436 textpath = path.path()
437 x_pt = self.x_pt
438 y_pt = self.y_pt
439 for i, value in enumerate(data):
440 if self.kerning and i % 2:
441 if value is not None:
442 x_pt += value
443 else:
444 if i:
445 x_pt += self.spaced_pt
446 glyphpath, wx_pt, wy_pt = self.font.t1file.getglyphpathwxwy_pt(value, self.size_pt, convertcharcode=not self.decode)
447 textpath += glyphpath.transformed(trafo.translate_pt(x_pt, y_pt))
448 x_pt += wx_pt
449 y_pt += wy_pt
450 deco.decoratedpath(textpath, fillstyles=[]).processPS(file, writer, context, registry, bbox)
451 else:
452 # register resources
453 if self.font.t1file is not None:
454 if self.decode:
455 registry.add(PST1file(self.font.t1file, self.glyphnames, []))
456 else:
457 registry.add(PST1file(self.font.t1file, [], self.charcodes))
459 fontname = self.font.name
460 if self.decode:
461 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
462 encoding = writer.encodings[self.font.name][encodingname]
463 newfontname = "%s-%s" % (fontname, encodingname)
464 registry.add(_ReEncodeFont)
465 registry.add(PSreencodefont(fontname, newfontname, encoding))
466 fontname = newfontname
468 if self.slant:
469 newfontmatrix = trafo.trafo_pt(matrix=((1, self.slant), (0, 1))) * self.font.t1file.fontmatrix
470 newfontname = "%s-slant%f" % (fontname, self.slant)
471 registry.add(_ChangeFontMatrix)
472 registry.add(PSchangefontmatrix(fontname, newfontname, newfontmatrix))
473 fontname = newfontname
475 # select font if necessary
476 sf = selectedfont(fontname, self.size_pt)
477 if context.selectedfont is None or sf != context.selectedfont:
478 context.selectedfont = sf
479 sf.outputPS(file, writer)
481 file.write("%f %f moveto (" % (self.x_pt, self.y_pt))
482 if self.decode:
483 if self.kerning:
484 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
485 else:
486 data = self.glyphnames
487 else:
488 data = self.charcodes
489 for i, value in enumerate(data):
490 if self.kerning and i % 2:
491 if value is not None:
492 file.write(") show\n%f 0 rmoveto (" % (value+self.spaced_pt))
493 elif self.spaced_pt:
494 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
495 else:
496 if i and not self.kerning and self.spaced_pt:
497 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
498 if self.decode:
499 value = encoding[value]
500 if 32 < value < 127 and chr(value) not in "()[]<>\\":
501 file.write("%s" % chr(value))
502 else:
503 file.write("\\%03o" % value)
504 file.write(") show\n")
506 def processPDF(self, file, writer, context, registry, bbox):
507 if not self.ignorebbox:
508 bbox += self.bbox()
510 if writer.text_as_path:
511 if self.decode:
512 if self.kerning:
513 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
514 else:
515 data = self.glyphnames
516 else:
517 data = self.charcodes
518 textpath = path.path()
519 x_pt = self.x_pt
520 y_pt = self.y_pt
521 for i, value in enumerate(data):
522 if self.kerning and i % 2:
523 if value is not None:
524 x_pt += value
525 else:
526 if i:
527 x_pt += self.spaced_pt
528 glyphpath, wx_pt, wy_pt = self.font.t1file.getglyphpathwxwy_pt(value, self.size_pt, convertcharcode=not self.decode)
529 textpath += glyphpath.transformed(trafo.translate_pt(x_pt, y_pt))
530 x_pt += wx_pt
531 y_pt += wy_pt
532 deco.decoratedpath(textpath, fillstyles=[]).processPDF(file, writer, context, registry, bbox)
533 else:
534 if self.decode:
535 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
536 encoding = writer.encodings[self.font.name][encodingname]
537 charcodes = [encoding[glyphname] for glyphname in self.glyphnames]
538 else:
539 charcodes = self.charcodes
541 # create resources
542 fontname = self.font.name
543 if self.decode:
544 newfontname = "%s-%s" % (fontname, encodingname)
545 _encoding = PDFencoding(encoding, newfontname)
546 fontname = newfontname
547 else:
548 _encoding = None
549 if self.font.t1file is not None:
550 if self.decode:
551 fontfile = PDFfontfile(self.font.t1file, self.glyphnames, [])
552 else:
553 fontfile = PDFfontfile(self.font.t1file, [], self.charcodes)
554 else:
555 fontfile = None
556 fontdescriptor = PDFfontdescriptor(self.font.name, fontfile, self.font.metric)
557 font = PDFfont(fontname, self.font.name, charcodes, fontdescriptor, _encoding, self.font.metric)
559 # register resources
560 if fontfile is not None:
561 registry.add(fontfile)
562 registry.add(fontdescriptor)
563 if _encoding is not None:
564 registry.add(_encoding)
565 registry.add(font)
567 registry.addresource("Font", fontname, font, procset="Text")
569 if self.slant is None:
570 slantvalue = 0
571 else:
572 slantvalue = self.slant
574 # select font if necessary
575 sf = selectedfont(fontname, self.size_pt)
576 if context.selectedfont is None or sf != context.selectedfont:
577 context.selectedfont = sf
578 sf.outputPDF(file, writer)
580 if self.kerning:
581 file.write("1 0 %f 1 %f %f Tm [(" % (slantvalue, self.x_pt, self.y_pt))
582 else:
583 file.write("1 0 %f 1 %f %f Tm (" % (slantvalue, self.x_pt, self.y_pt))
584 if self.decode:
585 if self.kerning:
586 data = self.font.metric.resolvekernings(self.glyphnames)
587 else:
588 data = self.glyphnames
589 else:
590 data = self.charcodes
591 for i, value in enumerate(data):
592 if self.kerning and i % 2:
593 if value is not None:
594 file.write(")%f(" % (-value-self.spaced_pt))
595 elif self.spaced_pt:
596 file.write(")%f(" % (-self.spaced_pt))
597 else:
598 if i and not self.kerning and self.spaced_pt:
599 file.write(")%f(" % (-self.spaced_pt))
600 if self.decode:
601 value = encoding[value]
602 if 32 <= value <= 127 and chr(value) not in "()[]<>\\":
603 file.write("%s" % chr(value))
604 else:
605 file.write("\\%03o" % value)
606 if self.kerning:
607 file.write(")] TJ\n")
608 else:
609 file.write(") Tj\n")