take into account kerning and inter-character spacing in bounding box
[PyX.git] / pyx / font / font.py
blob7c78e3102c8356924ef86ebed5786136907c6759
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 import logging
25 from pyx import bbox, baseclasses, deco, path, pswriter, pdfwriter, trafo, unit
26 from . import t1file, afmfile
28 logger = logging.getLogger("pyx")
30 ##############################################################################
31 # PS resources
32 ##############################################################################
34 class PST1file(pswriter.PSresource):
36 """ PostScript font definition included in the prolog """
38 def __init__(self, t1file, glyphnames, charcodes):
39 """ include type 1 font t1file stripped to the given glyphnames"""
40 self.type = "t1file"
41 self.t1file = t1file
42 self.id = t1file.name
43 self.glyphnames = set(glyphnames)
44 self.charcodes = set(charcodes)
46 def merge(self, other):
47 self.glyphnames.update(other.glyphnames)
48 self.charcodes.update(other.charcodes)
50 def output(self, file, writer, registry):
51 file.write("%%%%BeginFont: %s\n" % self.t1file.name)
52 if writer.strip_fonts:
53 if self.glyphnames:
54 file.write("%%Included glyphs: %s\n" % " ".join(self.glyphnames))
55 if self.charcodes:
56 file.write("%%Included charcodes: %s\n" % " ".join([str(charcode) for charcode in self.charcodes]))
57 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPS(file, writer)
58 else:
59 self.t1file.outputPS(file, writer)
60 file.write("\n%%EndFont\n")
63 _ReEncodeFont = pswriter.PSdefinition("ReEncodeFont", b"""{
64 5 dict
65 begin
66 /newencoding exch def
67 /newfontname exch def
68 /basefontname exch def
69 /basefontdict basefontname findfont def
70 /newfontdict basefontdict maxlength dict def
71 basefontdict {
72 exch dup dup /FID ne exch /Encoding ne and
73 { exch newfontdict 3 1 roll put }
74 { pop pop }
75 ifelse
76 } forall
77 newfontdict /FontName newfontname put
78 newfontdict /Encoding newencoding put
79 newfontname newfontdict definefont pop
80 end
81 }""")
84 class PSreencodefont(pswriter.PSresource):
86 """ reencoded PostScript font"""
88 def __init__(self, basefontname, newfontname, encoding):
89 """ reencode the font """
91 self.type = "reencodefont"
92 self.basefontname = basefontname
93 self.id = self.newfontname = newfontname
94 self.encoding = encoding
96 def output(self, file, writer, registry):
97 file.write("%%%%BeginResource: %s\n" % self.newfontname)
98 file.write("/%s /%s\n[" % (self.basefontname, self.newfontname))
99 vector = [None] * len(self.encoding)
100 for glyphname, charcode in list(self.encoding.items()):
101 vector[charcode] = glyphname
102 for i, glyphname in enumerate(vector):
103 if i:
104 if not (i % 8):
105 file.write("\n")
106 else:
107 file.write(" ")
108 file.write("/%s" % glyphname)
109 file.write("]\n")
110 file.write("ReEncodeFont\n")
111 file.write("%%EndResource\n")
114 _ChangeFontMatrix = pswriter.PSdefinition("ChangeFontMatrix", b"""{
115 5 dict
116 begin
117 /newfontmatrix exch def
118 /newfontname exch def
119 /basefontname exch def
120 /basefontdict basefontname findfont def
121 /newfontdict basefontdict maxlength dict def
122 basefontdict {
123 exch dup dup /FID ne exch /FontMatrix ne and
124 { exch newfontdict 3 1 roll put }
125 { pop pop }
126 ifelse
127 } forall
128 newfontdict /FontName newfontname put
129 newfontdict /FontMatrix newfontmatrix readonly put
130 newfontname newfontdict definefont pop
132 }""")
135 class PSchangefontmatrix(pswriter.PSresource):
137 """ change font matrix of a PostScript font"""
139 def __init__(self, basefontname, newfontname, newfontmatrix):
140 """ change the font matrix """
142 self.type = "changefontmatrix"
143 self.basefontname = basefontname
144 self.id = self.newfontname = newfontname
145 self.newfontmatrix = newfontmatrix
147 def output(self, file, writer, registry):
148 file.write("%%%%BeginResource: %s\n" % self.newfontname)
149 file.write("/%s /%s\n" % (self.basefontname, self.newfontname))
150 file.write(str(self.newfontmatrix))
151 file.write("\nChangeFontMatrix\n")
152 file.write("%%EndResource\n")
155 ##############################################################################
156 # PDF resources
157 ##############################################################################
159 class PDFfont(pdfwriter.PDFobject):
161 def __init__(self, fontname, basefontname, charcodes, fontdescriptor, encoding, metric):
162 pdfwriter.PDFobject.__init__(self, "font", fontname)
164 self.fontname = fontname
165 self.basefontname = basefontname
166 self.charcodes = set(charcodes)
167 self.fontdescriptor = fontdescriptor
168 self.encoding = encoding
169 self.metric = metric
171 def merge(self, other):
172 self.charcodes.update(other.charcodes)
174 def write(self, file, writer, registry):
175 file.write("<<\n"
176 "/Type /Font\n"
177 "/Subtype /Type1\n")
178 file.write("/Name /%s\n" % self.fontname)
179 file.write("/BaseFont /%s\n" % self.basefontname)
180 firstchar = min(self.charcodes)
181 lastchar = max(self.charcodes)
182 file.write("/FirstChar %d\n" % firstchar)
183 file.write("/LastChar %d\n" % lastchar)
184 file.write("/Widths\n"
185 "[")
186 if self.encoding:
187 encoding = self.encoding.getvector()
188 else:
189 if self.fontdescriptor.fontfile.t1file.encoding is None:
190 self.fontdescriptor.fontfile.t1file._encoding()
191 encoding = self.fontdescriptor.fontfile.t1file.encoding
192 for i in range(firstchar, lastchar+1):
193 if i:
194 if not (i % 8):
195 file.write("\n")
196 else:
197 file.write(" ")
198 if i in self.charcodes:
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 else:
204 file.write("0")
205 file.write(" ]\n")
206 file.write("/FontDescriptor %d 0 R\n" % registry.getrefno(self.fontdescriptor))
207 if self.encoding:
208 file.write("/Encoding %d 0 R\n" % registry.getrefno(self.encoding))
209 file.write(">>\n")
212 class PDFstdfont(pdfwriter.PDFobject):
214 def __init__(self, basename):
215 pdfwriter.PDFobject.__init__(self, "font", "stdfont-%s" % basename)
216 self.name = basename # name is ignored by acroread
217 self.basename = basename
219 def write(self, file, writer, registry):
220 file.write("<</BaseFont /%s\n" % self.basename)
221 file.write("/Name /%s\n" % self.name)
222 file.write("/Type /Font\n")
223 file.write("/Subtype /Type1\n")
224 file.write(">>\n")
226 # the 14 standard fonts that are always available in PDF
227 PDFTimesRoman = PDFstdfont("Times-Roman")
228 PDFTimesBold = PDFstdfont("Times-Bold")
229 PDFTimesItalic = PDFstdfont("Times-Italic")
230 PDFTimesBoldItalic = PDFstdfont("Times-BoldItalic")
231 PDFHelvetica = PDFstdfont("Helvetica")
232 PDFHelveticaBold = PDFstdfont("Helvetica-Bold")
233 PDFHelveticaOblique = PDFstdfont("Helvetica-Oblique")
234 PDFHelveticaBoldOblique = PDFstdfont("Helvetica-BoldOblique")
235 PDFCourier = PDFstdfont("Courier")
236 PDFCourierBold = PDFstdfont("Courier-Bold")
237 PDFCourierOblique = PDFstdfont("Courier-Oblique")
238 PDFCourierBoldOblique = PDFstdfont("Courier-BoldOblique")
239 PDFSymbol = PDFstdfont("Symbol")
240 PDFZapfDingbats = PDFstdfont("ZapfDingbats")
243 class PDFfontdescriptor(pdfwriter.PDFobject):
245 def __init__(self, fontname, fontfile, metric):
246 pdfwriter.PDFobject.__init__(self, "fontdescriptor", fontname)
247 self.fontname = fontname
248 self.fontfile = fontfile
249 self.metric = metric
251 def write(self, file, writer, registry):
252 file.write("<<\n"
253 "/Type /FontDescriptor\n"
254 "/FontName /%s\n" % self.fontname)
255 if self.metric is not None:
256 self.metric.writePDFfontinfo(file)
257 else:
258 self.fontfile.t1file.writePDFfontinfo(file)
259 if self.fontfile is not None:
260 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
261 file.write(">>\n")
264 class PDFfontfile(pdfwriter.PDFobject):
266 def __init__(self, t1file, glyphnames, charcodes):
267 pdfwriter.PDFobject.__init__(self, "fontfile", t1file.name)
268 self.t1file = t1file
269 self.glyphnames = set(glyphnames)
270 self.charcodes = set(charcodes)
272 def merge(self, other):
273 self.glyphnames.update(other.glyphnames)
274 self.charcodes.update(other.charcodes)
276 def write(self, file, writer, registry):
277 if writer.strip_fonts:
278 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPDF(file, writer)
279 else:
280 self.t1file.outputPDF(file, writer)
283 class PDFencoding(pdfwriter.PDFobject):
285 def __init__(self, encoding, name):
286 pdfwriter.PDFobject.__init__(self, "encoding", name)
287 self.encoding = encoding
289 def getvector(self):
290 # As self.encoding might be appended after the constructur has set it,
291 # we need to defer the calculation until the whole content was constructed.
292 vector = [None] * len(self.encoding)
293 for glyphname, charcode in list(self.encoding.items()):
294 vector[charcode] = glyphname
295 return vector
297 def write(self, file, writer, registry):
298 file.write("<<\n"
299 "/Type /Encoding\n"
300 "/Differences\n"
301 "[0")
302 for i, glyphname in enumerate(self.getvector()):
303 if i:
304 if not (i % 8):
305 file.write("\n")
306 else:
307 file.write(" ")
308 file.write("/%s" % glyphname)
309 file.write("]\n"
310 ">>\n")
313 ##############################################################################
314 # basic PyX text output
315 ##############################################################################
317 class font:
319 def text(self, x, y, charcodes, size_pt, **kwargs):
320 return self.text_pt(unit.topt(x), unit.topt(y), charcodes, size_pt, **kwargs)
323 class T1font(font):
325 def __init__(self, t1file, metric=None):
326 self.t1file = t1file
327 self.name = t1file.name
328 self.metric = metric
330 def text_pt(self, x, y, charcodes, size_pt, **kwargs):
331 return T1text_pt(self, x, y, charcodes, size_pt, **kwargs)
334 class T1builtinfont(T1font):
336 def __init__(self, name, metric):
337 self.name = name
338 self.t1file = None
339 self.metric = metric
342 class selectedfont:
344 def __init__(self, name, size_pt):
345 self.name = name
346 self.size_pt = size_pt
348 def __ne__(self, other):
349 return self.name != other.name or self.size_pt != other.size_pt
351 def outputPS(self, file, writer):
352 file.write("/%s %f selectfont\n" % (self.name, self.size_pt))
354 def outputPDF(self, file, writer):
355 file.write("/%s %f Tf\n" % (self.name, self.size_pt))
358 class text_pt(baseclasses.canvasitem):
360 def requiretextregion(self):
361 return True
364 class T1text_pt(text_pt):
366 def __init__(self, font, x_pt, y_pt, charcodes, size_pt, decoding=afmfile.unicodestring, 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
382 self._textpath = None
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 logger.warning("We are about to extract the bounding box from the path of the text. This is slow and differs from the font metric information. You should provide an afm file whenever possible.")
394 return self.textpath().bbox()
395 if not self.decode:
396 raise ValueError("decoding required for font metric access (bbox)")
397 if self.kerning:
398 kerning_correction = sum(value or 0 for i, value in enumerate(self.font.metric.resolvekernings(self.glyphnames, self.size_pt)) if i%2)
399 else:
400 kerning_correction = 0
401 return bbox.bbox_pt(self.x_pt,
402 self.y_pt+self.font.metric.depth_pt(self.glyphnames, self.size_pt),
403 self.x_pt+self.font.metric.width_pt(self.glyphnames, self.size_pt) + (len(self.glyphnames)-1)*self.spaced_pt + kerning_correction,
404 self.y_pt+self.font.metric.height_pt(self.glyphnames, self.size_pt))
406 def getencodingname(self, encodings):
407 """returns the name of the encoding (in encodings) mapping self.glyphnames to codepoints
408 If no such encoding can be found or extended, a new encoding is added to encodings
410 glyphnames = set(self.glyphnames)
411 if len(glyphnames) > 256:
412 raise ValueError("glyphs do not fit into one single encoding")
413 for encodingname, encoding in list(encodings.items()):
414 glyphsmissing = []
415 for glyphname in glyphnames:
416 if glyphname not in list(encoding.keys()):
417 glyphsmissing.append(glyphname)
419 if len(glyphsmissing) + len(encoding) < 256:
420 # new glyphs fit in existing encoding which will thus be extended
421 for glyphname in glyphsmissing:
422 encoding[glyphname] = len(encoding)
423 return encodingname
424 # create a new encoding for the glyphnames
425 encodingname = "encoding%d" % len(encodings)
426 encodings[encodingname] = dict([(glyphname, i) for i, glyphname in enumerate(glyphnames)])
427 return encodingname
429 def textpath(self):
430 if self._textpath is None:
431 if self.decode:
432 if self.kerning:
433 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
434 else:
435 data = self.glyphnames
436 else:
437 data = self.charcodes
438 self._textpath = path.path()
439 x_pt = self.x_pt
440 y_pt = self.y_pt
441 for i, value in enumerate(data):
442 if self.kerning and i % 2:
443 if value is not None:
444 x_pt += value
445 else:
446 if i:
447 x_pt += self.spaced_pt
448 glyphpath = self.font.t1file.getglyphpath_pt(x_pt, y_pt, value, self.size_pt, convertcharcode=not self.decode)
449 self._textpath += glyphpath.path
450 x_pt += glyphpath.wx_pt
451 y_pt += glyphpath.wy_pt
452 return self._textpath
454 def processPS(self, file, writer, context, registry, bbox):
455 if not self.ignorebbox:
456 bbox += self.bbox()
458 if writer.text_as_path and not self.font.t1file:
459 logger.warning("Cannot output text as path when font not given by a font file (like for builtin fonts).")
460 if writer.text_as_path and self.font.t1file:
461 deco.decoratedpath(self.textpath(), fillstyles=[]).processPS(file, writer, context, registry, bbox)
462 else:
463 # register resources
464 if self.font.t1file is not None:
465 if self.decode:
466 registry.add(PST1file(self.font.t1file, self.glyphnames, []))
467 else:
468 registry.add(PST1file(self.font.t1file, [], self.charcodes))
470 fontname = self.font.name
471 if self.decode:
472 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
473 encoding = writer.encodings[self.font.name][encodingname]
474 newfontname = "%s-%s" % (fontname, encodingname)
475 registry.add(_ReEncodeFont)
476 registry.add(PSreencodefont(fontname, newfontname, encoding))
477 fontname = newfontname
479 if self.slant:
480 newfontmatrix = trafo.trafo_pt(matrix=((1, self.slant), (0, 1)))
481 if self.font.t1file is not None:
482 newfontmatrix = newfontmatrix * self.font.t1file.fontmatrix
483 newfontname = "%s-slant%f" % (fontname, self.slant)
484 registry.add(_ChangeFontMatrix)
485 registry.add(PSchangefontmatrix(fontname, newfontname, newfontmatrix))
486 fontname = newfontname
488 # select font if necessary
489 sf = selectedfont(fontname, self.size_pt)
490 if context.selectedfont is None or sf != context.selectedfont:
491 context.selectedfont = sf
492 sf.outputPS(file, writer)
494 file.write("%f %f moveto (" % (self.x_pt, self.y_pt))
495 if self.decode:
496 if self.kerning:
497 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
498 else:
499 data = self.glyphnames
500 else:
501 data = self.charcodes
502 for i, value in enumerate(data):
503 if self.kerning and i % 2:
504 if value is not None:
505 file.write(") show\n%f 0 rmoveto (" % (value+self.spaced_pt))
506 elif self.spaced_pt:
507 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
508 else:
509 if i and not self.kerning and self.spaced_pt:
510 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
511 if self.decode:
512 value = encoding[value]
513 if 32 < value < 127 and chr(value) not in "()[]<>\\":
514 file.write("%s" % chr(value))
515 else:
516 file.write("\\%03o" % value)
517 file.write(") show\n")
519 def processPDF(self, file, writer, context, registry, bbox):
520 if not self.ignorebbox:
521 bbox += self.bbox()
523 if writer.text_as_path and not self.font.t1file:
524 logger.warning("Cannot output text as path when font not given by a font file (like for builtin fonts).")
525 if writer.text_as_path and self.font.t1file:
526 deco.decoratedpath(self.textpath(), fillstyles=[]).processPDF(file, writer, context, registry, bbox)
527 else:
528 if self.decode:
529 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
530 encoding = writer.encodings[self.font.name][encodingname]
531 charcodes = [encoding[glyphname] for glyphname in self.glyphnames]
532 else:
533 charcodes = self.charcodes
535 # create resources
536 fontname = self.font.name
537 if self.decode:
538 newfontname = "%s-%s" % (fontname, encodingname)
539 _encoding = PDFencoding(encoding, newfontname)
540 fontname = newfontname
541 else:
542 _encoding = None
543 if self.font.t1file is not None:
544 if self.decode:
545 fontfile = PDFfontfile(self.font.t1file, self.glyphnames, [])
546 else:
547 fontfile = PDFfontfile(self.font.t1file, [], self.charcodes)
548 else:
549 fontfile = None
550 fontdescriptor = PDFfontdescriptor(self.font.name, fontfile, self.font.metric)
551 font = PDFfont(fontname, self.font.name, charcodes, fontdescriptor, _encoding, self.font.metric)
553 # register resources
554 if fontfile is not None:
555 registry.add(fontfile)
556 registry.add(fontdescriptor)
557 if _encoding is not None:
558 registry.add(_encoding)
559 registry.add(font)
561 registry.addresource("Font", fontname, font, procset="Text")
563 if self.slant is None:
564 slantvalue = 0
565 else:
566 slantvalue = self.slant
568 # select font if necessary
569 sf = selectedfont(fontname, self.size_pt)
570 if context.selectedfont is None or sf != context.selectedfont:
571 context.selectedfont = sf
572 sf.outputPDF(file, writer)
574 if self.kerning:
575 file.write("1 0 %f 1 %f %f Tm [(" % (slantvalue, self.x_pt, self.y_pt))
576 else:
577 file.write("1 0 %f 1 %f %f Tm (" % (slantvalue, self.x_pt, self.y_pt))
578 if self.decode:
579 if self.kerning:
580 data = self.font.metric.resolvekernings(self.glyphnames)
581 else:
582 data = self.glyphnames
583 else:
584 data = self.charcodes
585 for i, value in enumerate(data):
586 if self.kerning and i % 2:
587 if value is not None:
588 file.write(")%f(" % (-value-self.spaced_pt))
589 elif self.spaced_pt:
590 file.write(")%f(" % (-self.spaced_pt))
591 else:
592 if i and not self.kerning and self.spaced_pt:
593 file.write(")%f(" % (-self.spaced_pt))
594 if self.decode:
595 value = encoding[value]
596 if 32 <= value <= 127 and chr(value) not in "()[]<>\\":
597 file.write("%s" % chr(value))
598 else:
599 file.write("\\%03o" % value)
600 if self.kerning:
601 file.write(")] TJ\n")
602 else:
603 file.write(") Tj\n")