1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2004-2012 André Wobst <wobsta@users.sourceforge.net>
5 # Copyright (C) 2011 Michael Schindler<m-schindler@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 import struct
, warnings
, binascii
30 from . import bbox
, baseclasses
, pswriter
, pdfwriter
, trafo
, unit
32 devicenames
= {"L": "/DeviceGray",
34 "CMYK": "/DeviceCMYK"}
35 decodestrings
= {"L": "[0 1]",
36 "RGB": "[0 1 0 1 0 1]",
37 "CMYK": "[0 1 0 1 0 1 0 1]",
41 def ascii85lines(datalen
):
44 return (datalen
+ 56)/60
46 def ascii85stream(file, data
):
47 """Encodes the string data in ASCII85 and writes it to
48 the stream file. The number of lines written to the stream
49 is known just from the length of the data by means of the
50 ascii85lines function. Note that the tailing newline character
51 of the last line is not added by this function, but it is taken
52 into account in the ascii85lines function."""
53 i
= 3 # go on smoothly in case of data length equals zero
55 l
= [None, None, None, None]
56 for i
in range(len(data
)):
60 if i
%60 == 3 and i
!= 3:
64 # l[3], c5 = divmod(256*256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
65 # l[2], c4 = divmod(l[3], 85)
66 # we have to avoid number > 2**31 by
67 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
68 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
69 l
[1], c3
= divmod(l
[2], 85)
70 c1
, c2
= divmod(l
[1], 85)
71 file.write_bytes(struct
.pack("BBBBB", c1
+33, c2
+33, c3
+33, c4
+33, c5
+33))
75 for j
in range((i
%4) + 1, 4):
77 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
78 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
79 l
[1], c3
= divmod(l
[2], 85)
80 c1
, c2
= divmod(l
[1], 85)
81 file.write_bytes(struct
.pack("BBBB", c1
+33, c2
+33, c3
+33, c4
+33)[:(i
%4)+2])
83 _asciihexlinelength
= 64
84 def asciihexlines(datalen
):
85 return (datalen
*2 + _asciihexlinelength
- 1) / _asciihexlinelength
87 def asciihexstream(file, data
):
88 hexdata
= binascii
.b2a_hex(data
)
89 for i
in range((len(hexdata
)-1)/_asciihexlinelength
+ 1):
90 file.write(hexdata
[i
*_asciihexlinelength
: i
*_asciihexlinelength
+_asciihexlinelength
])
96 def __init__(self
, width
, height
, mode
, data
, compressed
=None, palette
=None):
97 if width
<= 0 or height
<= 0:
98 raise ValueError("valid image size")
99 if mode
not in ["L", "RGB", "CMYK", "LA", "RGBA", "CMYKA", "AL", "ARGB", "ACMYK"]:
100 raise ValueError("invalid mode")
101 if compressed
is None and len(mode
)*width
*height
!= len(data
):
102 raise ValueError("wrong size of uncompressed data")
103 self
.size
= width
, height
106 self
.compressed
= compressed
107 self
.palette
= palette
110 if self
.compressed
is not None:
111 raise RuntimeError("cannot extract bands from compressed image")
112 bands
= len(self
.mode
)
113 width
, height
= self
.size
114 return [image(width
, height
, "L", "".join([self
.data
[i
*bands
+band
]
115 for i
in range(width
*height
)]))
116 for band
in range(bands
)]
118 def tobytes(self
, *args
):
120 raise RuntimeError("encoding not supported in this implementation")
123 def convert(self
, model
):
124 raise RuntimeError("color model conversion not supported in this implementation")
127 class jpegimage(image
):
129 def __init__(self
, file):
140 if data
[pos
] == 0o377 and data
[pos
+1] not in [0, 0o377]:
141 # print("marker: 0x%02x \\%03o" % (data[pos+1], data[pos+1]))
142 if data
[pos
+1] == 0o330:
146 elif not nestinglevel
:
147 raise ValueError("begin marker expected")
148 elif data
[pos
+1] == 0o331:
153 elif data
[pos
+1] in [0o300, 0o301]:
154 l
, bits
, height
, width
, components
= struct
.unpack(">HBHHB", data
[pos
+2:pos
+10])
156 raise ValueError("implementation limited to 8 bit per component only")
158 mode
= {1: "L", 3: "RGB", 4: "CMYK"}[components
]
160 raise ValueError("invalid number of components")
162 elif data
[pos
+1] == 0o340:
163 l
, id, major
, minor
, dpikind
, xdpi
, ydpi
= struct
.unpack(">H5sBBBHH", data
[pos
+2:pos
+16])
165 self
.info
= {"dpi": (xdpi
, ydpi
)}
167 self
.info
= {"dpi": (xdpi
*2.54, ydpi
*2.45)}
168 # else do not provide dpi information
172 raise ValueError("end marker expected")
173 image
.__init
__(self
, width
, height
, mode
, data
[begin
:end
], compressed
="DCT")
176 class PSimagedata(pswriter
.PSresource
):
178 def __init__(self
, name
, data
, singlestring
, maxstrlen
):
179 pswriter
.PSresource
.__init
__(self
, "imagedata", name
)
181 self
.singlestring
= singlestring
182 self
.maxstrlen
= maxstrlen
184 def output(self
, file, writer
, registry
):
185 file.write("%%%%BeginRessource: %s\n" % self
.id)
186 if self
.singlestring
:
187 file.write("%%%%BeginData: %i ASCII Lines\n"
188 "<~" % ascii85lines(len(self
.data
)))
189 ascii85stream(file, self
.data
)
193 datalen
= len(self
.data
)
194 tailpos
= datalen
- datalen
% self
.maxstrlen
195 file.write("%%%%BeginData: %i ASCII Lines\n" %
196 ((tailpos
/self
.maxstrlen
) * ascii85lines(self
.maxstrlen
) +
197 ascii85lines(datalen
-tailpos
)))
199 for i
in range(0, tailpos
, self
.maxstrlen
):
201 ascii85stream(file, self
.data
[i
: i
+self
.maxstrlen
])
203 if datalen
!= tailpos
:
205 ascii85stream(file, self
.data
[tailpos
:])
209 file.write("/%s exch def\n" % self
.id)
210 file.write("%%EndRessource\n")
213 class PDFimagepalettedata(pdfwriter
.PDFobject
):
215 def __init__(self
, name
, data
):
216 pdfwriter
.PDFobject
.__init
__(self
, "imagepalettedata", name
)
219 def write(self
, file, writer
, registry
):
221 "/Length %d\n" % len(self
.data
))
224 file.write(self
.data
)
229 class PDFimage(pdfwriter
.PDFobject
):
231 def __init__(self
, name
, width
, height
, palettemode
, palettedata
, mode
,
232 bitspercomponent
, compressmode
, data
, smask
, registry
, addresource
=True):
233 pdfwriter
.PDFobject
.__init
__(self
, "image", name
)
236 if palettedata
is not None:
242 registry
.addresource("XObject", name
, self
, procset
=procset
)
243 if palettedata
is not None:
244 # note that acrobat wants a palette to be an object (which clearly is a bug)
245 self
.PDFpalettedata
= PDFimagepalettedata(name
, palettedata
)
246 registry
.add(self
.PDFpalettedata
)
251 self
.palettemode
= palettemode
252 self
.palettedata
= palettedata
254 self
.bitspercomponent
= bitspercomponent
255 self
.compressmode
= compressmode
259 def write(self
, file, writer
, registry
):
263 "/Width %d\n" % self
.width
)
264 file.write("/Height %d\n" % self
.height
)
265 if self
.palettedata
is not None:
266 file.write("/ColorSpace [ /Indexed %s %i\n" % (devicenames
[self
.palettemode
], len(self
.palettedata
)/3-1))
267 file.write("%d 0 R\n" % registry
.getrefno(self
.PDFpalettedata
))
270 file.write("/ColorSpace %s\n" % devicenames
[self
.mode
])
272 file.write("/SMask %d 0 R\n" % registry
.getrefno(self
.smask
))
273 file.write("/BitsPerComponent %d\n" % self
.bitspercomponent
)
274 file.write("/Length %d\n" % len(self
.data
))
275 if self
.compressmode
:
276 file.write("/Filter /%sDecode\n" % self
.compressmode
)
279 file.write_bytes(self
.data
)
283 class bitmap_trafo(baseclasses
.canvasitem
):
285 def __init__(self
, trafo
, image
,
286 PSstoreimage
=0, PSmaxstrlen
=4093, PSbinexpand
=1,
287 compressmode
="Flate", flatecompresslevel
=6,
288 dctquality
=75, dctoptimize
=0, dctprogression
=0):
289 self
.pdftrafo
= trafo
291 self
.imagewidth
, self
.imageheight
= image
.size
293 self
.PSstoreimage
= PSstoreimage
294 self
.PSmaxstrlen
= PSmaxstrlen
295 self
.PSbinexpand
= PSbinexpand
296 self
.compressmode
= compressmode
297 self
.flatecompresslevel
= flatecompresslevel
298 self
.dctquality
= dctquality
299 self
.dctoptimize
= dctoptimize
300 self
.dctprogression
= dctprogression
303 self
.imagecompressed
= image
.compressed
305 self
.imagecompressed
= None
306 if self
.compressmode
not in [None, "Flate", "DCT"]:
307 raise ValueError("invalid compressmode '%s'" % self
.compressmode
)
308 if self
.imagecompressed
not in [None, "Flate", "DCT"]:
309 raise ValueError("invalid compressed image '%s'" % self
.imagecompressed
)
310 if self
.compressmode
is not None and self
.imagecompressed
is not None:
311 raise ValueError("compression of a compressed image not supported")
312 if not haszlib
and self
.compressmode
== "Flate":
313 warnings
.warn("zlib module not available, disable compression")
314 self
.compressmode
= None
316 def imagedata(self
, interleavealpha
):
319 returns a tuple (mode, data, alpha, palettemode, palettedata)
320 where mode does not contain antialiasing anymore
323 alpha
= palettemode
= palettedata
= None
326 if mode
.startswith("A"):
333 data
= image(self
.imagewidth
, self
.imageheight
, mode
,
334 "".join(["".join(values
)
335 for values
in zip(*[band
.tobytes()
336 for band
in bands
[1:]])]), palette
=data
.palette
)
337 if mode
.endswith("A"):
339 bands
= list(bands
[-1:]) + list(bands
[:-1])
343 # TODO: this is slow, but we don't want to depend on PIL or anything ... still, its incredibly slow to do it with lists and joins
344 data
= image(self
.imagewidth
, self
.imageheight
, "A%s" % mode
,
345 "".join(["".join(values
)
346 for values
in zip(*[band
.tobytes()
347 for band
in bands
])]), palette
=data
.palette
)
350 data
= image(self
.imagewidth
, self
.imageheight
, mode
,
351 "".join(["".join(values
)
352 for values
in zip(*[band
.tobytes()
353 for band
in bands
[1:]])]), palette
=data
.palette
)
356 palettemode
, palettedata
= data
.palette
.getdata()
357 if palettemode
not in ["L", "RGB", "CMYK"]:
358 warnings
.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode
)
359 data
= data
.convert("RGB")
365 warnings
.warn("specific single channel image mode not natively supported, converted to regular grayscale")
366 data
= data
.convert("L")
368 elif mode
not in ["CMYK", "RGB"]:
369 warnings
.warn("image with unknown mode converted to rgb")
370 data
= data
.convert("RGB")
373 if self
.compressmode
== "Flate":
374 data
= zlib
.compress(data
.tobytes(), self
.flatecompresslevel
)
375 elif self
.compressmode
== "DCT":
376 data
= data
.tobytes("jpeg", mode
, self
.dctquality
, self
.dctoptimize
, self
.dctprogression
)
378 data
= data
.tobytes()
379 if alpha
and not interleavealpha
:
380 if self
.compressmode
== "Flate":
381 alpha
= zlib
.compress(alpha
.tobytes(), self
.flatecompresslevel
)
382 elif self
.compressmode
== "DCT":
383 # well, this here is strange, we might want a alphacompressmode ...
384 alpha
= alpha
.tobytes("jpeg", mode
, self
.dctquality
, self
.dctoptimize
, self
.dctprogression
)
386 alpha
= alpha
.tobytes()
388 return mode
, data
, alpha
, palettemode
, palettedata
392 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(0.0, 0.0))
393 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(0.0, 1.0))
394 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(1.0, 0.0))
395 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(1.0, 1.0))
398 def processPS(self
, file, writer
, context
, registry
, bbox
):
399 mode
, data
, alpha
, palettemode
, palettedata
= self
.imagedata(True)
400 pstrafo
= trafo
.translate_pt(0, -1.0).scaled(self
.imagewidth
, -self
.imageheight
)*self
.pdftrafo
.inverse()
402 PSsinglestring
= self
.PSstoreimage
and len(data
) < self
.PSmaxstrlen
404 PSimagename
= "image-%d-%s-singlestring" % (id(self
.image
), self
.compressmode
)
406 PSimagename
= "image-%d-%s-stringarray" % (id(self
.image
), self
.compressmode
)
408 if self
.PSstoreimage
and not PSsinglestring
:
409 registry
.add(pswriter
.PSdefinition("imagedataaccess",
410 b
"{ /imagedataindex load " # get list index
411 b
"dup 1 add /imagedataindex exch store " # store increased index
412 b
"/imagedataid load exch get }")) # select string from array
413 if self
.PSstoreimage
:
414 registry
.add(PSimagedata(PSimagename
, data
, PSsinglestring
, self
.PSmaxstrlen
))
417 file.write("gsave\n")
418 if palettedata
is not None:
419 file.write("[ /Indexed %s %i\n" % (devicenames
[palettemode
], len(palettedata
)/3-1))
420 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(palettedata
)))
422 ascii85stream(file, palettedata
)
425 file.write("] setcolorspace\n")
427 file.write("%s setcolorspace\n" % devicenames
[mode
])
429 if self
.PSstoreimage
and not PSsinglestring
:
430 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
431 "/imagedataid %s store\n" % PSimagename
)
435 file.write("/ImageType 3\n"
438 file.write("/ImageType 1\n"
439 "/Width %i\n" % self
.imagewidth
)
440 file.write("/Height %i\n" % self
.imageheight
)
441 file.write("/BitsPerComponent 8\n"
442 "/ImageMatrix %s\n" % pstrafo
)
443 file.write("/Decode %s\n" % decodestrings
[mode
])
445 file.write("/DataSource ")
446 if self
.PSstoreimage
:
448 file.write("/%s load" % PSimagename
)
450 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
452 if self
.PSbinexpand
== 2:
453 file.write("currentfile /ASCIIHexDecode filter")
455 file.write("currentfile /ASCII85Decode filter")
456 if self
.compressmode
or self
.imagecompressed
:
457 file.write(" /%sDecode filter" % (self
.compressmode
or self
.imagecompressed
))
463 file.write("/MaskDict\n"
466 "/Width %i\n" % self
.imagewidth
)
467 file.write("/Height %i\n" % self
.imageheight
)
468 file.write("/BitsPerComponent 8\n"
469 "/ImageMatrix %s\n" % pstrafo
)
470 file.write("/Decode [1 0]\n"
472 "/InterleaveType 1\n"
475 if self
.PSstoreimage
:
476 file.write("image\n")
478 if self
.PSbinexpand
== 2:
479 file.write("%%%%BeginData: %i ASCII Lines\n"
480 "image\n" % (asciihexlines(len(data
)) + 1))
481 asciihexstream(file, data
)
484 # the datasource is currentstream (plus some filters)
485 file.write("%%%%BeginData: %i ASCII Lines\n"
486 "image\n" % (ascii85lines(len(data
)) + 1))
487 ascii85stream(file, data
)
489 file.write("%%EndData\n")
491 file.write("grestore\n")
493 def processPDF(self
, file, writer
, context
, registry
, bbox
):
494 mode
, data
, alpha
, palettemode
, palettedata
= self
.imagedata(False)
496 name
= "image-%d-%s" % (id(self
.image
), self
.compressmode
or self
.imagecompressed
)
498 alpha
= PDFimage("%s-smask" % name
, self
.imagewidth
, self
.imageheight
,
500 self
.compressmode
, alpha
, None, registry
, addresource
=False)
502 registry
.add(PDFimage(name
, self
.imagewidth
, self
.imageheight
,
503 palettemode
, palettedata
, mode
, 8,
504 self
.compressmode
or self
.imagecompressed
, data
, alpha
, registry
))
509 self
.pdftrafo
.processPDF(file, writer
, context
, registry
)
510 file.write("/%s Do\n" % name
)
514 class bitmap_pt(bitmap_trafo
):
516 def __init__(self
, xpos_pt
, ypos_pt
, image
, width_pt
=None, height_pt
=None, ratio
=None, **kwargs
):
517 imagewidth
, imageheight
= image
.size
518 if width_pt
is not None or height_pt
is not None:
521 width_pt
= height_pt
* imagewidth
/ float(imageheight
)
523 width_pt
= ratio
* height_pt
524 elif height_pt
is None:
526 height_pt
= width_pt
* imageheight
/ float(imagewidth
)
528 height_pt
= (1.0/ratio
) * width_pt
529 elif ratio
is not None:
530 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
532 if ratio
is not None:
533 raise ValueError("must specify width_pt or height_pt to set a ratio")
534 widthdpi
, heightdpi
= image
.info
["dpi"] # fails when no dpi information available
535 width_pt
= 72.0 * imagewidth
/ float(widthdpi
)
536 height_pt
= 72.0 * imageheight
/ float(heightdpi
)
538 bitmap_trafo
.__init
__(self
, trafo
.trafo_pt(((float(width_pt
), 0.0), (0.0, float(height_pt
))), (float(xpos_pt
), float(ypos_pt
))), image
, **kwargs
)
541 class bitmap(bitmap_pt
):
543 def __init__(self
, xpos
, ypos
, image
, width
=None, height
=None, **kwargs
):
544 xpos_pt
= unit
.topt(xpos
)
545 ypos_pt
= unit
.topt(ypos
)
546 if width
is not None:
547 width_pt
= unit
.topt(width
)
550 if height
is not None:
551 height_pt
= unit
.topt(height
)
555 bitmap_pt
.__init
__(self
, xpos_pt
, ypos_pt
, image
, width_pt
=width_pt
, height_pt
=height_pt
, **kwargs
)