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
25 from pyx
import bbox
, baseclasses
, deco
, path
, pswriter
, pdfwriter
, trafo
, unit
26 from . import t1file
, afmfile
28 logger
= logging
.getLogger("pyx")
30 ##############################################################################
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"""
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
:
54 file.write("%%Included glyphs: %s\n" % " ".join(self
.glyphnames
))
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
)
59 self
.t1file
.outputPS(file, writer
)
60 file.write("\n%%EndFont\n")
63 _ReEncodeFont
= pswriter
.PSdefinition("ReEncodeFont", b
"""{
68 /basefontname exch def
69 /basefontdict basefontname findfont def
70 /newfontdict basefontdict maxlength dict def
72 exch dup dup /FID ne exch /Encoding ne and
73 { exch newfontdict 3 1 roll put }
77 newfontdict /FontName newfontname put
78 newfontdict /Encoding newencoding put
79 newfontname newfontdict definefont pop
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
):
108 file.write("/%s" % glyphname
)
110 file.write("ReEncodeFont\n")
111 file.write("%%EndResource\n")
114 _ChangeFontMatrix
= pswriter
.PSdefinition("ChangeFontMatrix", b
"""{
117 /newfontmatrix exch def
118 /newfontname exch def
119 /basefontname exch def
120 /basefontdict basefontname findfont def
121 /newfontdict basefontdict maxlength dict def
123 exch dup dup /FID ne exch /FontMatrix ne and
124 { exch newfontdict 3 1 roll put }
128 newfontdict /FontName newfontname put
129 newfontdict /FontMatrix newfontmatrix readonly put
130 newfontname newfontdict definefont pop
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 ##############################################################################
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
171 def merge(self
, other
):
172 self
.charcodes
.update(other
.charcodes
)
174 def write(self
, file, writer
, registry
):
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"
187 encoding
= self
.encoding
.getvector()
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):
198 if i
in self
.charcodes
:
199 if self
.metric
is not None:
200 file.write("%i" % self
.metric
.width_ds(encoding
[i
]))
202 file.write("%i" % self
.fontdescriptor
.fontfile
.t1file
.getglyphinfo(encoding
[i
])[0])
206 file.write("/FontDescriptor %d 0 R\n" % registry
.getrefno(self
.fontdescriptor
))
208 file.write("/Encoding %d 0 R\n" % registry
.getrefno(self
.encoding
))
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")
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
251 def write(self
, file, writer
, registry
):
253 "/Type /FontDescriptor\n"
254 "/FontName /%s\n" % self
.fontname
)
255 if self
.metric
is not None:
256 self
.metric
.writePDFfontinfo(file)
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
))
264 class PDFfontfile(pdfwriter
.PDFobject
):
266 def __init__(self
, t1file
, glyphnames
, charcodes
):
267 pdfwriter
.PDFobject
.__init
__(self
, "fontfile", t1file
.name
)
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
)
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
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
297 def write(self
, file, writer
, registry
):
302 for i
, glyphname
in enumerate(self
.getvector()):
308 file.write("/%s" % glyphname
)
313 ##############################################################################
314 # basic PyX text output
315 ##############################################################################
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
)
325 def __init__(self
, t1file
, metric
=None):
327 self
.name
= t1file
.name
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
):
344 def __init__(self
, name
, size_pt
):
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
):
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
]
371 self
.charcodes
= charcodes
376 self
.size_pt
= size_pt
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)")
389 self
.glyphnames
= self
.font
.metric
.resolveligatures(self
.glyphnames
)
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()
396 raise ValueError("decoding required for font metric access (bbox)")
397 return bbox
.bbox_pt(self
.x_pt
,
398 self
.y_pt
+self
.font
.metric
.depth_pt(self
.glyphnames
, self
.size_pt
),
399 self
.x_pt
+self
.font
.metric
.width_pt(self
.glyphnames
, self
.size_pt
),
400 self
.y_pt
+self
.font
.metric
.height_pt(self
.glyphnames
, self
.size_pt
))
402 def getencodingname(self
, encodings
):
403 """returns the name of the encoding (in encodings) mapping self.glyphnames to codepoints
404 If no such encoding can be found or extended, a new encoding is added to encodings
406 glyphnames
= set(self
.glyphnames
)
407 if len(glyphnames
) > 256:
408 raise ValueError("glyphs do not fit into one single encoding")
409 for encodingname
, encoding
in list(encodings
.items()):
411 for glyphname
in glyphnames
:
412 if glyphname
not in list(encoding
.keys()):
413 glyphsmissing
.append(glyphname
)
415 if len(glyphsmissing
) + len(encoding
) < 256:
416 # new glyphs fit in existing encoding which will thus be extended
417 for glyphname
in glyphsmissing
:
418 encoding
[glyphname
] = len(encoding
)
420 # create a new encoding for the glyphnames
421 encodingname
= "encoding%d" % len(encodings
)
422 encodings
[encodingname
] = dict([(glyphname
, i
) for i
, glyphname
in enumerate(glyphnames
)])
426 if self
._textpath
is None:
429 data
= self
.font
.metric
.resolvekernings(self
.glyphnames
, self
.size_pt
)
431 data
= self
.glyphnames
433 data
= self
.charcodes
434 self
._textpath
= path
.path()
437 for i
, value
in enumerate(data
):
438 if self
.kerning
and i
% 2:
439 if value
is not None:
443 x_pt
+= self
.spaced_pt
444 glyphpath
= self
.font
.t1file
.getglyphpath_pt(x_pt
, y_pt
, value
, self
.size_pt
, convertcharcode
=not self
.decode
)
445 self
._textpath
+= glyphpath
.path
446 x_pt
+= glyphpath
.wx_pt
447 y_pt
+= glyphpath
.wy_pt
448 return self
._textpath
450 def processPS(self
, file, writer
, context
, registry
, bbox
):
451 if not self
.ignorebbox
:
454 if writer
.text_as_path
and not self
.font
.t1file
:
455 logger
.warning("Cannot output text as path when font not given by a font file (like for builtin fonts).")
456 if writer
.text_as_path
and self
.font
.t1file
:
457 deco
.decoratedpath(self
.textpath(), fillstyles
=[]).processPS(file, writer
, context
, registry
, bbox
)
460 if self
.font
.t1file
is not None:
462 registry
.add(PST1file(self
.font
.t1file
, self
.glyphnames
, []))
464 registry
.add(PST1file(self
.font
.t1file
, [], self
.charcodes
))
466 fontname
= self
.font
.name
468 encodingname
= self
.getencodingname(writer
.encodings
.setdefault(self
.font
.name
, {}))
469 encoding
= writer
.encodings
[self
.font
.name
][encodingname
]
470 newfontname
= "%s-%s" % (fontname
, encodingname
)
471 registry
.add(_ReEncodeFont
)
472 registry
.add(PSreencodefont(fontname
, newfontname
, encoding
))
473 fontname
= newfontname
476 newfontmatrix
= trafo
.trafo_pt(matrix
=((1, self
.slant
), (0, 1)))
477 if self
.font
.t1file
is not None:
478 newfontmatrix
= newfontmatrix
* self
.font
.t1file
.fontmatrix
479 newfontname
= "%s-slant%f" % (fontname
, self
.slant
)
480 registry
.add(_ChangeFontMatrix
)
481 registry
.add(PSchangefontmatrix(fontname
, newfontname
, newfontmatrix
))
482 fontname
= newfontname
484 # select font if necessary
485 sf
= selectedfont(fontname
, self
.size_pt
)
486 if context
.selectedfont
is None or sf
!= context
.selectedfont
:
487 context
.selectedfont
= sf
488 sf
.outputPS(file, writer
)
490 file.write("%f %f moveto (" % (self
.x_pt
, self
.y_pt
))
493 data
= self
.font
.metric
.resolvekernings(self
.glyphnames
, self
.size_pt
)
495 data
= self
.glyphnames
497 data
= self
.charcodes
498 for i
, value
in enumerate(data
):
499 if self
.kerning
and i
% 2:
500 if value
is not None:
501 file.write(") show\n%f 0 rmoveto (" % (value
+self
.spaced_pt
))
503 file.write(") show\n%f 0 rmoveto (" % self
.spaced_pt
)
505 if i
and not self
.kerning
and self
.spaced_pt
:
506 file.write(") show\n%f 0 rmoveto (" % self
.spaced_pt
)
508 value
= encoding
[value
]
509 if 32 < value
< 127 and chr(value
) not in "()[]<>\\":
510 file.write("%s" % chr(value
))
512 file.write("\\%03o" % value
)
513 file.write(") show\n")
515 def processPDF(self
, file, writer
, context
, registry
, bbox
):
516 if not self
.ignorebbox
:
519 if writer
.text_as_path
and not self
.font
.t1file
:
520 logger
.warning("Cannot output text as path when font not given by a font file (like for builtin fonts).")
521 if writer
.text_as_path
and self
.font
.t1file
:
522 deco
.decoratedpath(self
.textpath(), fillstyles
=[]).processPDF(file, writer
, context
, registry
, bbox
)
525 encodingname
= self
.getencodingname(writer
.encodings
.setdefault(self
.font
.name
, {}))
526 encoding
= writer
.encodings
[self
.font
.name
][encodingname
]
527 charcodes
= [encoding
[glyphname
] for glyphname
in self
.glyphnames
]
529 charcodes
= self
.charcodes
532 fontname
= self
.font
.name
534 newfontname
= "%s-%s" % (fontname
, encodingname
)
535 _encoding
= PDFencoding(encoding
, newfontname
)
536 fontname
= newfontname
539 if self
.font
.t1file
is not None:
541 fontfile
= PDFfontfile(self
.font
.t1file
, self
.glyphnames
, [])
543 fontfile
= PDFfontfile(self
.font
.t1file
, [], self
.charcodes
)
546 fontdescriptor
= PDFfontdescriptor(self
.font
.name
, fontfile
, self
.font
.metric
)
547 font
= PDFfont(fontname
, self
.font
.name
, charcodes
, fontdescriptor
, _encoding
, self
.font
.metric
)
550 if fontfile
is not None:
551 registry
.add(fontfile
)
552 registry
.add(fontdescriptor
)
553 if _encoding
is not None:
554 registry
.add(_encoding
)
557 registry
.addresource("Font", fontname
, font
, procset
="Text")
559 if self
.slant
is None:
562 slantvalue
= self
.slant
564 # select font if necessary
565 sf
= selectedfont(fontname
, self
.size_pt
)
566 if context
.selectedfont
is None or sf
!= context
.selectedfont
:
567 context
.selectedfont
= sf
568 sf
.outputPDF(file, writer
)
571 file.write("1 0 %f 1 %f %f Tm [(" % (slantvalue
, self
.x_pt
, self
.y_pt
))
573 file.write("1 0 %f 1 %f %f Tm (" % (slantvalue
, self
.x_pt
, self
.y_pt
))
576 data
= self
.font
.metric
.resolvekernings(self
.glyphnames
)
578 data
= self
.glyphnames
580 data
= self
.charcodes
581 for i
, value
in enumerate(data
):
582 if self
.kerning
and i
% 2:
583 if value
is not None:
584 file.write(")%f(" % (-value
-self
.spaced_pt
))
586 file.write(")%f(" % (-self
.spaced_pt
))
588 if i
and not self
.kerning
and self
.spaced_pt
:
589 file.write(")%f(" % (-self
.spaced_pt
))
591 value
= encoding
[value
]
592 if 32 <= value
<= 127 and chr(value
) not in "()[]<>\\":
593 file.write("%s" % chr(value
))
595 file.write("\\%03o" % value
)
597 file.write(")] TJ\n")