bring back support for font reencoding in PDF
[PyX/mjg.git] / pyx / font / font.py
blobf478bbf1b982e8131404c51b637e966028d6adbc
1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2005-2007 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2005-2007 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 from pyx import bbox, canvasitem, pswriter, pdfwriter, trafo, unit
24 import t1file
26 try:
27 set()
28 except NameError:
29 # Python 2.3
30 from sets import Set as set
33 ##############################################################################
34 # PS resources
35 ##############################################################################
37 class PST1file(pswriter.PSresource):
39 """ PostScript font definition included in the prolog """
41 def __init__(self, t1file, glyphnames, charcodes):
42 """ include type 1 font t1file stripped to the given glyphnames"""
43 self.type = "t1file"
44 self.t1file = t1file
45 self.id = t1file.name
46 self.glyphnames = set(glyphnames)
47 self.charcodes = set(charcodes)
48 self.strip = 1
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 self.strip:
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):
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
174 def merge(self, other):
175 self.charcodes.update(other.charcodes)
177 def write(self, file, writer, registry):
178 file.write("<<\n"
179 "/Type /Font\n"
180 "/Subtype /Type1\n")
181 file.write("/Name /%s\n" % self.fontname)
182 file.write("/BaseFont /%s\n" % self.basefontname)
183 firstchar = min(self.charcodes)
184 lastchar = max(self.charcodes)
185 file.write("/FirstChar %d\n" % firstchar)
186 file.write("/LastChar %d\n" % lastchar)
187 file.write("/Widths\n"
188 "[")
189 if self.encoding:
190 encoding = self.encoding.getvector()
191 else:
192 encoding = self.fontdescriptor.fontfile.t1file.encoding
193 for i in range(firstchar, lastchar+1):
194 if i and not (i % 8):
195 file.write("\n")
196 else:
197 file.write(" ")
198 file.write("%i" % self.fontdescriptor.fontfile.t1file.getglyphinfo(encoding[i])[0])
199 file.write(" ]\n")
200 file.write("/FontDescriptor %d 0 R\n" % registry.getrefno(self.fontdescriptor))
201 if self.encoding:
202 file.write("/Encoding %d 0 R\n" % registry.getrefno(self.encoding))
203 file.write(">>\n")
206 class PDFfontdescriptor(pdfwriter.PDFobject):
208 def __init__(self, fontname, fontfile):
209 pdfwriter.PDFobject.__init__(self, "fontdescriptor", fontname)
210 self.fontname = fontname
211 self.fontfile = fontfile
213 def write(self, file, writer, registry):
214 file.write("<<\n"
215 "/Type /FontDescriptor\n"
216 "/FontName /%s\n" % self.fontname)
217 if self.fontfile is not None:
218 self.fontfile.t1file.writePDFfontinfo(file)
219 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
220 else:
221 file.write("/Flags 32\n")
222 # TODO: add other required font info for PDF builtin fonts
223 # file.write("/FontBBox [%d %d %d %d]\n")
224 # file.write("/ItalicAngle %d\n")
225 # file.write("/Ascent %d\n")
226 # file.write("/Descent %d\n")
227 # file.write("/CapHeight %d\n")
228 # file.write("/StemV %d\n")
229 file.write(">>\n")
232 class PDFfontfile(pdfwriter.PDFobject):
234 def __init__(self, t1file, glyphnames, charcodes):
235 pdfwriter.PDFobject.__init__(self, "fontfile", t1file.name)
236 self.t1file = t1file
237 self.glyphnames = set(glyphnames)
238 self.charcodes = set(charcodes)
239 self.strip = 1
241 def merge(self, other):
242 self.glyphnames.update(other.glyphnames)
243 self.charcodes.update(other.charcodes)
245 def write(self, file, writer, registry):
246 if self.strip:
247 self.t1file.getstrippedfont(self.glyphnames, self.charcodes).outputPDF(file, writer)
248 else:
249 self.t1file.outputPDF(file, writer)
252 class PDFencoding(pdfwriter.PDFobject):
254 def __init__(self, encoding, name):
255 pdfwriter.PDFobject.__init__(self, "encoding", name)
256 self.encoding = encoding
258 def getvector(self):
259 # As self.encoding might be appended after the constructur has set it,
260 # we need to defer the calculation until the whole content was constructed.
261 vector = [None] * len(self.encoding)
262 for glyphname, charcode in self.encoding.items():
263 vector[charcode] = glyphname
264 return vector
266 def write(self, file, writer, registry):
267 file.write("<<\n"
268 "/Type /Encoding\n"
269 "/Differences\n"
270 "[0")
271 for i, glyphname in enumerate(self.getvector()):
272 if i:
273 if not (i % 8):
274 file.write("\n")
275 else:
276 file.write(" ")
277 file.write("/%s" % glyphname)
278 file.write("]\n"
279 ">>\n")
282 ##############################################################################
283 # basic PyX text output
284 ##############################################################################
286 class font:
288 def text(self, x, y, charcodes, size_pt, **kwargs):
289 return self.text_pt(unit.topt(x), unit.topt(y), charcodes, size_pt, **kwargs)
292 class T1font(font):
294 def __init__(self, t1file, metric):
295 self.t1file = t1file
296 self.name = t1file.name
297 self.metric = metric
299 def text_pt(self, x, y, charcodes, size_pt, **kwargs):
300 return T1text_pt(self, x, y, charcodes, size_pt, **kwargs)
303 class T1builtinfont(T1font):
305 def __init__(self, name, metric):
306 self.name = name
307 self.t1file = None
308 self.metric = metric
311 class selectedfont:
313 def __init__(self, name, size_pt):
314 self.name = name
315 self.size_pt = size_pt
317 def __ne__(self, other):
318 return self.name != other.name or self.size_pt != other.size_pt
320 def outputPS(self, file, writer):
321 file.write("/%s %f selectfont\n" % (self.name, self.size_pt))
323 def outputPDF(self, file, writer):
324 file.write("/%s %f Tf\n" % (self.name, self.size_pt))
327 class text_pt(canvasitem.canvasitem):
329 pass
332 class T1text_pt(text_pt):
334 def __init__(self, font, x_pt, y_pt, charcodes, size_pt, decoding=None, slant=None, ignorebbox=False): #, **features):
335 # features: kerning, ligatures
336 if decoding is not None:
337 self.glyphnames = [decoding[character] for character in charcodes]
338 self.reencode = True
339 else:
340 self.charcodes = charcodes
341 self.reencode = False
342 self.font = font
343 self.x_pt = x_pt
344 self.y_pt = y_pt
345 self.size_pt = size_pt
346 self.slant = slant
347 self.ignorebbox = ignorebbox
349 def bbox(self):
350 if self.font.metric is None:
351 raise NotImplementedError("we don't yet have access to the metric")
352 if not self.reencode:
353 raise NotImplementedError("can only handle glyphname based font metrics")
354 return bbox.bbox_pt(self.x_pt,
355 self.y_pt-self.font.metric.depth_pt(self.glyphnames, self.size_pt),
356 self.x_pt+self.font.metric.width_pt(self.glyphnames, self.size_pt),
357 self.y_pt+self.font.metric.height_pt(self.glyphnames, self.size_pt))
359 def getencodingname(self, encodings):
360 """returns the name of the encoding (in encodings) mapping self.glyphnames to codepoints
361 If no such encoding can be found or extended, a new encoding is added to encodings
363 glyphnames = set(self.glyphnames)
364 if len(glyphnames) > 256:
365 raise ValueError("glyphs do not fit into one single encoding")
366 for encodingname, encoding in encodings.items():
367 glyphsmissing = []
368 for glyphname in glyphnames:
369 if glyphname not in encoding.keys():
370 glyphsmissing.append(glyphname)
372 if len(glyphsmissing) + len(encoding) < 256:
373 # new glyphs fit in existing encoding which will thus be extended
374 for glyphname in glyphsmissing:
375 encoding[glyphname] = len(encoding)
376 return encodingname
377 # create a new encoding for the glyphnames
378 encodingname = "encoding%d" % len(encodings)
379 encodings[encodingname] = dict([(glyphname, i) for i, glyphname in enumerate(glyphnames)])
380 return encodingname
382 def processPS(self, file, writer, context, registry, bbox):
383 if not self.ignorebbox:
384 bbox += self.bbox()
386 # register resources
387 if self.font.t1file is not None:
388 if self.reencode:
389 registry.add(PST1file(self.font.t1file, self.glyphnames, []))
390 else:
391 registry.add(PST1file(self.font.t1file, [], self.charcodes))
393 fontname = self.font.name
394 if self.reencode:
395 encodingname = self.getencodingname(context.encodings.setdefault(self.font.name, {}))
396 encoding = context.encodings[self.font.name][encodingname]
397 newfontname = "%s-%s" % (fontname, encodingname)
398 registry.add(_ReEncodeFont)
399 registry.add(PSreencodefont(fontname, newfontname, encoding))
400 fontname = newfontname
402 if self.slant:
403 newfontmatrix = trafo.trafo_pt(matrix=((1, self.slant), (0, 1))) * self.font.t1file.fontmatrix
404 newfontname = "%s-slant%f" % (fontname, self.slant)
405 registry.add(_ChangeFontMatrix)
406 registry.add(PSchangefontmatrix(fontname, newfontname, newfontmatrix))
407 fontname = newfontname
409 # select font if necessary
410 sf = selectedfont(fontname, self.size_pt)
411 if context.selectedfont is None or sf != context.selectedfont:
412 context.selectedfont = sf
413 sf.outputPS(file, writer)
415 file.write("%f %f moveto (" % (self.x_pt, self.y_pt))
416 if self.reencode:
417 charcodes = [encoding[glyphname] for glyphname in self.glyphnames]
418 else:
419 charcodes = self.charcodes
420 for charcode in charcodes:
421 if 32 < charcode < 127 and chr(charcode) not in "()[]<>\\":
422 file.write("%s" % chr(charcode))
423 else:
424 file.write("\\%03o" % charcode)
425 file.write(") show\n")
427 def processPDF(self, file, writer, context, registry, bbox):
428 if not self.ignorebbox:
429 bbox += self.bbox()
431 if self.reencode:
432 encodingname = self.getencodingname(context.encodings.setdefault(self.font.name, {}))
433 encoding = context.encodings[self.font.name][encodingname]
434 charcodes = [encoding[glyphname] for glyphname in self.glyphnames]
435 else:
436 charcodes = self.charcodes
438 # create resources
439 fontname = self.font.name
440 if self.reencode:
441 newfontname = "%s-%s" % (fontname, encodingname)
442 encoding = PDFencoding(encoding, newfontname)
443 fontname = newfontname
444 else:
445 encoding = None
446 if self.font.t1file is not None:
447 if self.reencode:
448 fontfile = PDFfontfile(self.font.t1file, self.glyphnames, [])
449 else:
450 fontfile = PDFfontfile(self.font.t1file, [], self.charcodes)
451 else:
452 fontfile = None
453 fontdescriptor = PDFfontdescriptor(self.font.name, fontfile)
454 font = PDFfont(fontname, self.font.name, charcodes, fontdescriptor, encoding)
456 # register resources
457 if fontfile is not None:
458 registry.add(fontfile)
459 registry.add(fontdescriptor)
460 if encoding is not None:
461 registry.add(encoding)
462 registry.add(font)
464 registry.addresource("Font", fontname, font, procset="Text")
466 if self.slant is None:
467 slantvalue = 0
468 else:
469 slantvalue = self.slant
471 # select font if necessary
472 sf = selectedfont(fontname, self.size_pt)
473 if context.selectedfont is None or sf != context.selectedfont:
474 context.selectedfont = sf
475 sf.outputPDF(file, writer)
477 file.write("1 0 %f 1 %f %f Tm (" % (slantvalue, self.x_pt, self.y_pt))
478 for charcode in charcodes:
479 if 32 <= charcode <= 127 and chr(charcode) not in "()[]<>\\":
480 file.write("%s" % chr(charcode))
481 else:
482 file.write("\\%03o" % charcode)
483 file.write(") Tj\n")