rename bitmap style to density style
[PyX.git] / pyx / font / font.py
blob3e8132cc5e0b2dda6d5736dbd8b71e333e0919eb
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 warnings
25 from pyx import bbox, canvasitem, deco, path, pswriter, pdfwriter, trafo, unit, pycompat
26 import t1file, afmfile
29 ##############################################################################
30 # PS resources
31 ##############################################################################
33 class PST1file(pswriter.PSresource):
35 """ PostScript font definition included in the prolog """
37 def __init__(self, t1file, glyphnames, charcodes):
38 """ include type 1 font t1file stripped to the given glyphnames"""
39 self.type = "t1file"
40 self.t1file = t1file
41 self.id = t1file.name
42 self.glyphnames = pycompat.set(glyphnames)
43 self.charcodes = pycompat.set(charcodes)
45 def merge(self, other):
46 self.glyphnames.update(other.glyphnames)
47 self.charcodes.update(other.charcodes)
49 def output(self, file, writer, registry):
50 file.write("%%%%BeginFont: %s\n" % self.t1file.name)
51 if writer.strip_fonts:
52 if self.glyphnames:
53 file.write("%%Included glyphs: %s\n" % " ".join(self.glyphnames))
54 if self.charcodes:
55 file.write("%%Included charcodes: %s\n" % " ".join([str(charcode) for charcode in self.charcodes]))
56 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPS(file, writer)
57 else:
58 self.t1file.outputPS(file, writer)
59 file.write("\n%%EndFont\n")
62 _ReEncodeFont = pswriter.PSdefinition("ReEncodeFont", """{
63 5 dict
64 begin
65 /newencoding exch def
66 /newfontname exch def
67 /basefontname exch def
68 /basefontdict basefontname findfont def
69 /newfontdict basefontdict maxlength dict def
70 basefontdict {
71 exch dup dup /FID ne exch /Encoding ne and
72 { exch newfontdict 3 1 roll put }
73 { pop pop }
74 ifelse
75 } forall
76 newfontdict /FontName newfontname put
77 newfontdict /Encoding newencoding put
78 newfontname newfontdict definefont pop
79 end
80 }""")
83 class PSreencodefont(pswriter.PSresource):
85 """ reencoded PostScript font"""
87 def __init__(self, basefontname, newfontname, encoding):
88 """ reencode the font """
90 self.type = "reencodefont"
91 self.basefontname = basefontname
92 self.id = self.newfontname = newfontname
93 self.encoding = encoding
95 def output(self, file, writer, registry):
96 file.write("%%%%BeginResource: %s\n" % self.newfontname)
97 file.write("/%s /%s\n[" % (self.basefontname, self.newfontname))
98 vector = [None] * len(self.encoding)
99 for glyphname, charcode in self.encoding.items():
100 vector[charcode] = glyphname
101 for i, glyphname in enumerate(vector):
102 if i:
103 if not (i % 8):
104 file.write("\n")
105 else:
106 file.write(" ")
107 file.write("/%s" % glyphname)
108 file.write("]\n")
109 file.write("ReEncodeFont\n")
110 file.write("%%EndResource\n")
113 _ChangeFontMatrix = pswriter.PSdefinition("ChangeFontMatrix", """{
114 5 dict
115 begin
116 /newfontmatrix exch def
117 /newfontname exch def
118 /basefontname exch def
119 /basefontdict basefontname findfont def
120 /newfontdict basefontdict maxlength dict def
121 basefontdict {
122 exch dup dup /FID ne exch /FontMatrix ne and
123 { exch newfontdict 3 1 roll put }
124 { pop pop }
125 ifelse
126 } forall
127 newfontdict /FontName newfontname put
128 newfontdict /FontMatrix newfontmatrix readonly put
129 newfontname newfontdict definefont pop
131 }""")
134 class PSchangefontmatrix(pswriter.PSresource):
136 """ change font matrix of a PostScript font"""
138 def __init__(self, basefontname, newfontname, newfontmatrix):
139 """ change the font matrix """
141 self.type = "changefontmatrix"
142 self.basefontname = basefontname
143 self.id = self.newfontname = newfontname
144 self.newfontmatrix = newfontmatrix
146 def output(self, file, writer, registry):
147 file.write("%%%%BeginResource: %s\n" % self.newfontname)
148 file.write("/%s /%s\n" % (self.basefontname, self.newfontname))
149 file.write(str(self.newfontmatrix))
150 file.write("\nChangeFontMatrix\n")
151 file.write("%%EndResource\n")
154 ##############################################################################
155 # PDF resources
156 ##############################################################################
158 class PDFfont(pdfwriter.PDFobject):
160 def __init__(self, fontname, basefontname, charcodes, fontdescriptor, encoding, metric):
161 pdfwriter.PDFobject.__init__(self, "font", fontname)
163 self.fontname = fontname
164 self.basefontname = basefontname
165 self.charcodes = pycompat.set(charcodes)
166 self.fontdescriptor = fontdescriptor
167 self.encoding = encoding
168 self.metric = metric
170 def merge(self, other):
171 self.charcodes.update(other.charcodes)
173 def write(self, file, writer, registry):
174 file.write("<<\n"
175 "/Type /Font\n"
176 "/Subtype /Type1\n")
177 file.write("/Name /%s\n" % self.fontname)
178 file.write("/BaseFont /%s\n" % self.basefontname)
179 firstchar = min(self.charcodes)
180 lastchar = max(self.charcodes)
181 file.write("/FirstChar %d\n" % firstchar)
182 file.write("/LastChar %d\n" % lastchar)
183 file.write("/Widths\n"
184 "[")
185 if self.encoding:
186 encoding = self.encoding.getvector()
187 else:
188 encoding = self.fontdescriptor.fontfile.t1file.encoding
189 for i in range(firstchar, lastchar+1):
190 if i:
191 if not (i % 8):
192 file.write("\n")
193 else:
194 file.write(" ")
195 if i in self.charcodes:
196 if self.metric is not None:
197 file.write("%i" % self.metric.width_ds(encoding[i]))
198 else:
199 file.write("%i" % self.fontdescriptor.fontfile.t1file.getglyphinfo(encoding[i])[0])
200 else:
201 file.write("0")
202 file.write(" ]\n")
203 file.write("/FontDescriptor %d 0 R\n" % registry.getrefno(self.fontdescriptor))
204 if self.encoding:
205 file.write("/Encoding %d 0 R\n" % registry.getrefno(self.encoding))
206 file.write(">>\n")
209 class PDFstdfont(pdfwriter.PDFobject):
211 def __init__(self, basename):
212 pdfwriter.PDFobject.__init__(self, "font", "stdfont-%s" % basename)
213 self.name = basename # name is ignored by acroread
214 self.basename = basename
216 def write(self, file, writer, registry):
217 file.write("<</BaseFont /%s\n" % self.basename)
218 file.write("/Name /%s\n" % self.name)
219 file.write("/Type /Font\n")
220 file.write("/Subtype /Type1\n")
221 file.write(">>\n")
223 # the 14 standard fonts that are always available in PDF
224 PDFTimesRoman = PDFstdfont("Times-Roman")
225 PDFTimesBold = PDFstdfont("Times-Bold")
226 PDFTimesItalic = PDFstdfont("Times-Italic")
227 PDFTimesBoldItalic = PDFstdfont("Times-BoldItalic")
228 PDFHelvetica = PDFstdfont("Helvetica")
229 PDFHelveticaBold = PDFstdfont("Helvetica-Bold")
230 PDFHelveticaOblique = PDFstdfont("Helvetica-Oblique")
231 PDFHelveticaBoldOblique = PDFstdfont("Helvetica-BoldOblique")
232 PDFCourier = PDFstdfont("Courier")
233 PDFCourierBold = PDFstdfont("Courier-Bold")
234 PDFCourierOblique = PDFstdfont("Courier-Oblique")
235 PDFCourierBoldOblique = PDFstdfont("Courier-BoldOblique")
236 PDFSymbol = PDFstdfont("Symbol")
237 PDFZapfDingbats = PDFstdfont("ZapfDingbats")
240 class PDFfontdescriptor(pdfwriter.PDFobject):
242 def __init__(self, fontname, fontfile, metric):
243 pdfwriter.PDFobject.__init__(self, "fontdescriptor", fontname)
244 self.fontname = fontname
245 self.fontfile = fontfile
246 self.metric = metric
248 def write(self, file, writer, registry):
249 file.write("<<\n"
250 "/Type /FontDescriptor\n"
251 "/FontName /%s\n" % self.fontname)
252 if self.metric is not None:
253 self.metric.writePDFfontinfo(file)
254 else:
255 self.fontfile.t1file.writePDFfontinfo(file)
256 if self.fontfile is not None:
257 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
258 file.write(">>\n")
261 class PDFfontfile(pdfwriter.PDFobject):
263 def __init__(self, t1file, glyphnames, charcodes):
264 pdfwriter.PDFobject.__init__(self, "fontfile", t1file.name)
265 self.t1file = t1file
266 self.glyphnames = pycompat.set(glyphnames)
267 self.charcodes = pycompat.set(charcodes)
269 def merge(self, other):
270 self.glyphnames.update(other.glyphnames)
271 self.charcodes.update(other.charcodes)
273 def write(self, file, writer, registry):
274 if writer.strip_fonts:
275 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPDF(file, writer)
276 else:
277 self.t1file.outputPDF(file, writer)
280 class PDFencoding(pdfwriter.PDFobject):
282 def __init__(self, encoding, name):
283 pdfwriter.PDFobject.__init__(self, "encoding", name)
284 self.encoding = encoding
286 def getvector(self):
287 # As self.encoding might be appended after the constructur has set it,
288 # we need to defer the calculation until the whole content was constructed.
289 vector = [None] * len(self.encoding)
290 for glyphname, charcode in self.encoding.items():
291 vector[charcode] = glyphname
292 return vector
294 def write(self, file, writer, registry):
295 file.write("<<\n"
296 "/Type /Encoding\n"
297 "/Differences\n"
298 "[0")
299 for i, glyphname in enumerate(self.getvector()):
300 if i:
301 if not (i % 8):
302 file.write("\n")
303 else:
304 file.write(" ")
305 file.write("/%s" % glyphname)
306 file.write("]\n"
307 ">>\n")
310 ##############################################################################
311 # basic PyX text output
312 ##############################################################################
314 class font:
316 def text(self, x, y, charcodes, size_pt, **kwargs):
317 return self.text_pt(unit.topt(x), unit.topt(y), charcodes, size_pt, **kwargs)
320 class T1font(font):
322 def __init__(self, t1file, metric=None):
323 self.t1file = t1file
324 self.name = t1file.name
325 self.metric = metric
327 def text_pt(self, x, y, charcodes, size_pt, **kwargs):
328 return T1text_pt(self, x, y, charcodes, size_pt, **kwargs)
331 class T1builtinfont(T1font):
333 def __init__(self, name, metric):
334 self.name = name
335 self.t1file = None
336 self.metric = metric
339 class selectedfont:
341 def __init__(self, name, size_pt):
342 self.name = name
343 self.size_pt = size_pt
345 def __ne__(self, other):
346 return self.name != other.name or self.size_pt != other.size_pt
348 def outputPS(self, file, writer):
349 file.write("/%s %f selectfont\n" % (self.name, self.size_pt))
351 def outputPDF(self, file, writer):
352 file.write("/%s %f Tf\n" % (self.name, self.size_pt))
355 class text_pt(canvasitem.canvasitem):
357 pass
360 class T1text_pt(text_pt):
362 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):
363 if decoding is not None:
364 self.glyphnames = [decoding[character] for character in charcodes]
365 self.decode = True
366 else:
367 self.charcodes = charcodes
368 self.decode = False
369 self.font = font
370 self.x_pt = x_pt
371 self.y_pt = y_pt
372 self.size_pt = size_pt
373 self.slant = slant
374 self.ignorebbox = ignorebbox
375 self.kerning = kerning
376 self.ligatures = ligatures
377 self.spaced_pt = spaced_pt
378 self._textpath = None
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 warnings.warn("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.")
390 return self.textpath().bbox()
391 if not self.decode:
392 raise ValueError("decoding required for font metric access (bbox)")
393 return bbox.bbox_pt(self.x_pt,
394 self.y_pt+self.font.metric.depth_pt(self.glyphnames, self.size_pt),
395 self.x_pt+self.font.metric.width_pt(self.glyphnames, self.size_pt),
396 self.y_pt+self.font.metric.height_pt(self.glyphnames, self.size_pt))
398 def getencodingname(self, encodings):
399 """returns the name of the encoding (in encodings) mapping self.glyphnames to codepoints
400 If no such encoding can be found or extended, a new encoding is added to encodings
402 glyphnames = pycompat.set(self.glyphnames)
403 if len(glyphnames) > 256:
404 raise ValueError("glyphs do not fit into one single encoding")
405 for encodingname, encoding in encodings.items():
406 glyphsmissing = []
407 for glyphname in glyphnames:
408 if glyphname not in encoding.keys():
409 glyphsmissing.append(glyphname)
411 if len(glyphsmissing) + len(encoding) < 256:
412 # new glyphs fit in existing encoding which will thus be extended
413 for glyphname in glyphsmissing:
414 encoding[glyphname] = len(encoding)
415 return encodingname
416 # create a new encoding for the glyphnames
417 encodingname = "encoding%d" % len(encodings)
418 encodings[encodingname] = dict([(glyphname, i) for i, glyphname in enumerate(glyphnames)])
419 return encodingname
421 def textpath(self):
422 if self._textpath is None:
423 if self.decode:
424 if self.kerning:
425 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
426 else:
427 data = self.glyphnames
428 else:
429 data = self.charcodes
430 self._textpath = path.path()
431 x_pt = self.x_pt
432 y_pt = self.y_pt
433 for i, value in enumerate(data):
434 if self.kerning and i % 2:
435 if value is not None:
436 x_pt += value
437 else:
438 if i:
439 x_pt += self.spaced_pt
440 glyphpath = self.font.t1file.getglyphpath_pt(x_pt, y_pt, value, self.size_pt, convertcharcode=not self.decode)
441 self._textpath += glyphpath.path
442 x_pt += glyphpath.wx_pt
443 y_pt += glyphpath.wy_pt
444 return self._textpath
446 def processPS(self, file, writer, context, registry, bbox):
447 if not self.ignorebbox:
448 bbox += self.bbox()
450 if writer.text_as_path:
451 deco.decoratedpath(self.textpath(), fillstyles=[]).processPS(file, writer, context, registry, bbox)
452 else:
453 # register resources
454 if self.font.t1file is not None:
455 if self.decode:
456 registry.add(PST1file(self.font.t1file, self.glyphnames, []))
457 else:
458 registry.add(PST1file(self.font.t1file, [], self.charcodes))
460 fontname = self.font.name
461 if self.decode:
462 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
463 encoding = writer.encodings[self.font.name][encodingname]
464 newfontname = "%s-%s" % (fontname, encodingname)
465 registry.add(_ReEncodeFont)
466 registry.add(PSreencodefont(fontname, newfontname, encoding))
467 fontname = newfontname
469 if self.slant:
470 newfontmatrix = trafo.trafo_pt(matrix=((1, self.slant), (0, 1)))
471 if self.font.t1file is not None:
472 newfontmatrix = newfontmatrix * self.font.t1file.fontmatrix
473 newfontname = "%s-slant%f" % (fontname, self.slant)
474 registry.add(_ChangeFontMatrix)
475 registry.add(PSchangefontmatrix(fontname, newfontname, newfontmatrix))
476 fontname = newfontname
478 # select font if necessary
479 sf = selectedfont(fontname, self.size_pt)
480 if context.selectedfont is None or sf != context.selectedfont:
481 context.selectedfont = sf
482 sf.outputPS(file, writer)
484 file.write("%f %f moveto (" % (self.x_pt, self.y_pt))
485 if self.decode:
486 if self.kerning:
487 data = self.font.metric.resolvekernings(self.glyphnames, self.size_pt)
488 else:
489 data = self.glyphnames
490 else:
491 data = self.charcodes
492 for i, value in enumerate(data):
493 if self.kerning and i % 2:
494 if value is not None:
495 file.write(") show\n%f 0 rmoveto (" % (value+self.spaced_pt))
496 elif self.spaced_pt:
497 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
498 else:
499 if i and not self.kerning and self.spaced_pt:
500 file.write(") show\n%f 0 rmoveto (" % self.spaced_pt)
501 if self.decode:
502 value = encoding[value]
503 if 32 < value < 127 and chr(value) not in "()[]<>\\":
504 file.write("%s" % chr(value))
505 else:
506 file.write("\\%03o" % value)
507 file.write(") show\n")
509 def processPDF(self, file, writer, context, registry, bbox):
510 if not self.ignorebbox:
511 bbox += self.bbox()
513 if writer.text_as_path:
514 deco.decoratedpath(self.textpath(), fillstyles=[]).processPDF(file, writer, context, registry, bbox)
515 else:
516 if self.decode:
517 encodingname = self.getencodingname(writer.encodings.setdefault(self.font.name, {}))
518 encoding = writer.encodings[self.font.name][encodingname]
519 charcodes = [encoding[glyphname] for glyphname in self.glyphnames]
520 else:
521 charcodes = self.charcodes
523 # create resources
524 fontname = self.font.name
525 if self.decode:
526 newfontname = "%s-%s" % (fontname, encodingname)
527 _encoding = PDFencoding(encoding, newfontname)
528 fontname = newfontname
529 else:
530 _encoding = None
531 if self.font.t1file is not None:
532 if self.decode:
533 fontfile = PDFfontfile(self.font.t1file, self.glyphnames, [])
534 else:
535 fontfile = PDFfontfile(self.font.t1file, [], self.charcodes)
536 else:
537 fontfile = None
538 fontdescriptor = PDFfontdescriptor(self.font.name, fontfile, self.font.metric)
539 font = PDFfont(fontname, self.font.name, charcodes, fontdescriptor, _encoding, self.font.metric)
541 # register resources
542 if fontfile is not None:
543 registry.add(fontfile)
544 registry.add(fontdescriptor)
545 if _encoding is not None:
546 registry.add(_encoding)
547 registry.add(font)
549 registry.addresource("Font", fontname, font, procset="Text")
551 if self.slant is None:
552 slantvalue = 0
553 else:
554 slantvalue = self.slant
556 # select font if necessary
557 sf = selectedfont(fontname, self.size_pt)
558 if context.selectedfont is None or sf != context.selectedfont:
559 context.selectedfont = sf
560 sf.outputPDF(file, writer)
562 if self.kerning:
563 file.write("1 0 %f 1 %f %f Tm [(" % (slantvalue, self.x_pt, self.y_pt))
564 else:
565 file.write("1 0 %f 1 %f %f Tm (" % (slantvalue, self.x_pt, self.y_pt))
566 if self.decode:
567 if self.kerning:
568 data = self.font.metric.resolvekernings(self.glyphnames)
569 else:
570 data = self.glyphnames
571 else:
572 data = self.charcodes
573 for i, value in enumerate(data):
574 if self.kerning and i % 2:
575 if value is not None:
576 file.write(")%f(" % (-value-self.spaced_pt))
577 elif self.spaced_pt:
578 file.write(")%f(" % (-self.spaced_pt))
579 else:
580 if i and not self.kerning and self.spaced_pt:
581 file.write(")%f(" % (-self.spaced_pt))
582 if self.decode:
583 value = encoding[value]
584 if 32 <= value <= 127 and chr(value) not in "()[]<>\\":
585 file.write("%s" % chr(value))
586 else:
587 file.write("\\%03o" % value)
588 if self.kerning:
589 file.write(")] TJ\n")
590 else:
591 file.write(") Tj\n")