1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2004-2013 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 binascii
, logging
, struct
30 from . import bbox
, baseclasses
, pswriter
, pdfwriter
, trafo
, unit
32 logger
= logging
.getLogger("pyx")
34 devicenames
= {"L": "/DeviceGray",
36 "CMYK": "/DeviceCMYK"}
37 decodestrings
= {"L": "[0 1]",
38 "RGB": "[0 1 0 1 0 1]",
39 "CMYK": "[0 1 0 1 0 1 0 1]",
43 def ascii85lines(datalen
):
46 return (datalen
+ 56)/60
48 def ascii85stream(file, data
):
49 """Encodes the string data in ASCII85 and writes it to
50 the stream file. The number of lines written to the stream
51 is known just from the length of the data by means of the
52 ascii85lines function. Note that the tailing newline character
53 of the last line is not added by this function, but it is taken
54 into account in the ascii85lines function."""
55 i
= 3 # go on smoothly in case of data length equals zero
57 l
= [None, None, None, None]
58 for i
in range(len(data
)):
62 if i
%60 == 3 and i
!= 3:
66 # l[3], c5 = divmod(256*256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
67 # l[2], c4 = divmod(l[3], 85)
68 # we have to avoid number > 2**31 by
69 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
70 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
71 l
[1], c3
= divmod(l
[2], 85)
72 c1
, c2
= divmod(l
[1], 85)
73 file.write_bytes(struct
.pack("BBBBB", c1
+33, c2
+33, c3
+33, c4
+33, c5
+33))
77 for j
in range((i
%4) + 1, 4):
79 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
80 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
81 l
[1], c3
= divmod(l
[2], 85)
82 c1
, c2
= divmod(l
[1], 85)
83 file.write_bytes(struct
.pack("BBBB", c1
+33, c2
+33, c3
+33, c4
+33)[:(i
%4)+2])
85 _asciihexlinelength
= 64
86 def asciihexlines(datalen
):
87 return (datalen
*2 + _asciihexlinelength
- 1) / _asciihexlinelength
89 def asciihexstream(file, data
):
90 hexdata
= binascii
.b2a_hex(data
)
91 for i
in range((len(hexdata
)-1)/_asciihexlinelength
+ 1):
92 file.write(hexdata
[i
*_asciihexlinelength
: i
*_asciihexlinelength
+_asciihexlinelength
])
98 def __init__(self
, width
, height
, mode
, data
, compressed
=None, palette
=None):
99 if width
<= 0 or height
<= 0:
100 raise ValueError("valid image size")
101 if mode
not in ["L", "RGB", "CMYK", "LA", "RGBA", "CMYKA", "AL", "ARGB", "ACMYK"]:
102 raise ValueError("invalid mode")
103 if compressed
is None and len(mode
)*width
*height
!= len(data
):
104 raise ValueError("wrong size of uncompressed data")
105 self
.size
= width
, height
108 self
.compressed
= compressed
109 self
.palette
= palette
112 if self
.compressed
is not None:
113 raise RuntimeError("cannot extract bands from compressed image")
114 bands
= len(self
.mode
)
115 width
, height
= self
.size
116 return [image(width
, height
, "L", bytes(self
.data
[band
::bands
])) 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):
133 with
open(file, "rb") as f
:
139 if data
[pos
] == 0o377 and data
[pos
+1] not in [0, 0o377]:
140 # print("marker: 0x%02x \\%03o" % (data[pos+1], data[pos+1]))
141 if data
[pos
+1] == 0o330:
145 elif not nestinglevel
:
146 raise ValueError("begin marker expected")
147 elif data
[pos
+1] == 0o331:
152 elif data
[pos
+1] in [0o300, 0o302]:
153 l
, bits
, height
, width
, components
= struct
.unpack(">HBHHB", data
[pos
+2:pos
+10])
155 raise ValueError("implementation limited to 8 bit per component only")
157 mode
= {1: "L", 3: "RGB", 4: "CMYK"}[components
]
159 raise ValueError("invalid number of components")
161 elif data
[pos
+1] == 0o340:
162 l
, id, major
, minor
, dpikind
, xdpi
, ydpi
= struct
.unpack(">H5sBBBHH", data
[pos
+2:pos
+16])
164 self
.info
= {"dpi": (xdpi
, ydpi
)}
166 self
.info
= {"dpi": (xdpi
*2.54, ydpi
*2.45)}
167 # else do not provide dpi information
171 raise ValueError("end marker expected")
172 image
.__init
__(self
, width
, height
, mode
, data
[begin
:end
], compressed
="DCT")
175 class PSimagedata(pswriter
.PSresource
):
177 def __init__(self
, name
, data
, singlestring
, maxstrlen
):
178 pswriter
.PSresource
.__init
__(self
, "imagedata", name
)
180 self
.singlestring
= singlestring
181 self
.maxstrlen
= maxstrlen
183 def output(self
, file, writer
, registry
):
184 file.write("%%%%BeginRessource: %s\n" % self
.id)
185 if self
.singlestring
:
186 file.write("%%%%BeginData: %i ASCII Lines\n"
187 "<~" % ascii85lines(len(self
.data
)))
188 ascii85stream(file, self
.data
)
192 datalen
= len(self
.data
)
193 tailpos
= datalen
- datalen
% self
.maxstrlen
194 file.write("%%%%BeginData: %i ASCII Lines\n" %
195 ((tailpos
/self
.maxstrlen
) * ascii85lines(self
.maxstrlen
) +
196 ascii85lines(datalen
-tailpos
)))
198 for i
in range(0, tailpos
, self
.maxstrlen
):
200 ascii85stream(file, self
.data
[i
: i
+self
.maxstrlen
])
202 if datalen
!= tailpos
:
204 ascii85stream(file, self
.data
[tailpos
:])
208 file.write("/%s exch def\n" % self
.id)
209 file.write("%%EndRessource\n")
212 class PDFimagepalettedata(pdfwriter
.PDFobject
):
214 def __init__(self
, name
, data
):
215 pdfwriter
.PDFobject
.__init
__(self
, "imagepalettedata", name
)
218 def write(self
, file, writer
, registry
):
220 "/Length %d\n" % len(self
.data
))
223 file.write(self
.data
)
228 class PDFimage(pdfwriter
.PDFobject
):
230 def __init__(self
, name
, width
, height
, palettemode
, palettedata
, mode
,
231 bitspercomponent
, compressmode
, data
, smask
, registry
, addresource
=True):
232 pdfwriter
.PDFobject
.__init
__(self
, "image", name
)
235 if palettedata
is not None:
241 registry
.addresource("XObject", name
, self
, procset
=procset
)
242 if palettedata
is not None:
243 # note that acrobat wants a palette to be an object (which clearly is a bug)
244 self
.PDFpalettedata
= PDFimagepalettedata(name
, palettedata
)
245 registry
.add(self
.PDFpalettedata
)
250 self
.palettemode
= palettemode
251 self
.palettedata
= palettedata
253 self
.bitspercomponent
= bitspercomponent
254 self
.compressmode
= compressmode
258 def write(self
, file, writer
, registry
):
262 "/Width %d\n" % self
.width
)
263 file.write("/Height %d\n" % self
.height
)
264 if self
.palettedata
is not None:
265 file.write("/ColorSpace [ /Indexed %s %i\n" % (devicenames
[self
.palettemode
], len(self
.palettedata
)/3-1))
266 file.write("%d 0 R\n" % registry
.getrefno(self
.PDFpalettedata
))
269 file.write("/ColorSpace %s\n" % devicenames
[self
.mode
])
271 file.write("/SMask %d 0 R\n" % registry
.getrefno(self
.smask
))
272 file.write("/BitsPerComponent %d\n" % self
.bitspercomponent
)
273 file.write("/Length %d\n" % len(self
.data
))
274 if self
.compressmode
:
275 file.write("/Filter /%sDecode\n" % self
.compressmode
)
278 file.write_bytes(self
.data
)
282 class bitmap_trafo(baseclasses
.canvasitem
):
284 def __init__(self
, trafo
, image
,
285 PSstoreimage
=0, PSmaxstrlen
=4093, PSbinexpand
=1,
286 compressmode
="Flate", flatecompresslevel
=6,
287 dctquality
=75, dctoptimize
=0, dctprogression
=0):
288 self
.pdftrafo
= trafo
290 self
.imagewidth
, self
.imageheight
= image
.size
292 self
.PSstoreimage
= PSstoreimage
293 self
.PSmaxstrlen
= PSmaxstrlen
294 self
.PSbinexpand
= PSbinexpand
295 self
.compressmode
= compressmode
296 self
.flatecompresslevel
= flatecompresslevel
297 self
.dctquality
= dctquality
298 self
.dctoptimize
= dctoptimize
299 self
.dctprogression
= dctprogression
302 self
.imagecompressed
= image
.compressed
304 self
.imagecompressed
= None
305 if self
.compressmode
not in [None, "Flate", "DCT"]:
306 raise ValueError("invalid compressmode '%s'" % self
.compressmode
)
307 if self
.imagecompressed
not in [None, "Flate", "DCT"]:
308 raise ValueError("invalid compressed image '%s'" % self
.imagecompressed
)
309 if self
.compressmode
is not None and self
.imagecompressed
is not None:
310 raise ValueError("compression of a compressed image not supported")
311 if not haszlib
and self
.compressmode
== "Flate":
312 logger
.warning("zlib module not available, disable compression")
313 self
.compressmode
= None
315 def imagedata(self
, interleavealpha
):
318 returns a tuple (mode, data, alpha, palettemode, palettedata)
319 where mode does not contain antialiasing anymore
322 alpha
= palettemode
= palettedata
= None
325 if mode
.startswith("A"):
332 data
= image(self
.imagewidth
, self
.imageheight
, mode
,
333 b
"".join([bytes(values
)
334 for values
in zip(*[band
.tobytes()
335 for band
in bands
[1:]])]), palette
=data
.palette
)
336 if mode
.endswith("A"):
338 bands
= list(bands
[-1:]) + list(bands
[:-1])
342 # 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
343 data
= image(self
.imagewidth
, self
.imageheight
, "A%s" % mode
,
344 b
"".join([bytes(values
)
345 for values
in zip(*[band
.tobytes()
346 for band
in bands
])]), palette
=data
.palette
)
349 data
= image(self
.imagewidth
, self
.imageheight
, mode
,
350 b
"".join([bytes(values
)
351 for values
in zip(*[band
.tobytes()
352 for band
in bands
[1:]])]), palette
=data
.palette
)
355 palettemode
, palettedata
= data
.palette
.getdata()
356 if palettemode
not in ["L", "RGB", "CMYK"]:
357 logger
.warning("image with unknown palette mode '%s' converted to rgb image" % palettemode
)
358 data
= data
.convert("RGB")
364 logger
.warning("specific single channel image mode not natively supported, converted to regular grayscale")
365 data
= data
.convert("L")
367 elif mode
not in ["CMYK", "RGB"]:
368 logger
.warning("image with unknown mode converted to rgb")
369 data
= data
.convert("RGB")
372 if self
.compressmode
== "Flate":
373 data
= zlib
.compress(data
.tobytes(), self
.flatecompresslevel
)
374 elif self
.compressmode
== "DCT":
375 data
= data
.tobytes("jpeg", mode
, self
.dctquality
, self
.dctoptimize
, self
.dctprogression
)
377 data
= data
.tobytes()
378 if alpha
and not interleavealpha
:
379 if self
.compressmode
== "Flate":
380 alpha
= zlib
.compress(alpha
.tobytes(), self
.flatecompresslevel
)
381 elif self
.compressmode
== "DCT":
382 # well, this here is strange, we might want a alphacompressmode ...
383 alpha
= alpha
.tobytes("jpeg", mode
, self
.dctquality
, self
.dctoptimize
, self
.dctprogression
)
385 alpha
= alpha
.tobytes()
387 return mode
, data
, alpha
, palettemode
, palettedata
391 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(0.0, 0.0))
392 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(0.0, 1.0))
393 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(1.0, 0.0))
394 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(1.0, 1.0))
397 def processPS(self
, file, writer
, context
, registry
, bbox
):
398 mode
, data
, alpha
, palettemode
, palettedata
= self
.imagedata(True)
399 pstrafo
= trafo
.translate_pt(0, -1.0).scaled(self
.imagewidth
, -self
.imageheight
)*self
.pdftrafo
.inverse()
401 PSsinglestring
= self
.PSstoreimage
and len(data
) < self
.PSmaxstrlen
403 PSimagename
= "image-%d-%s-singlestring" % (id(self
.image
), self
.compressmode
)
405 PSimagename
= "image-%d-%s-stringarray" % (id(self
.image
), self
.compressmode
)
407 if self
.PSstoreimage
and not PSsinglestring
:
408 registry
.add(pswriter
.PSdefinition("imagedataaccess",
409 b
"{ /imagedataindex load " # get list index
410 b
"dup 1 add /imagedataindex exch store " # store increased index
411 b
"/imagedataid load exch get }")) # select string from array
412 if self
.PSstoreimage
:
413 registry
.add(PSimagedata(PSimagename
, data
, PSsinglestring
, self
.PSmaxstrlen
))
416 file.write("gsave\n")
417 if palettedata
is not None:
418 file.write("[ /Indexed %s %i\n" % (devicenames
[palettemode
], len(palettedata
)/3-1))
419 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(palettedata
)))
421 ascii85stream(file, palettedata
)
424 file.write("] setcolorspace\n")
426 file.write("%s setcolorspace\n" % devicenames
[mode
])
428 if self
.PSstoreimage
and not PSsinglestring
:
429 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
430 "/imagedataid %s store\n" % PSimagename
)
434 file.write("/ImageType 3\n"
437 file.write("/ImageType 1\n"
438 "/Width %i\n" % self
.imagewidth
)
439 file.write("/Height %i\n" % self
.imageheight
)
440 file.write("/BitsPerComponent 8\n"
441 "/ImageMatrix %s\n" % pstrafo
)
442 file.write("/Decode %s\n" % decodestrings
[mode
])
444 file.write("/DataSource ")
445 if self
.PSstoreimage
:
447 file.write("/%s load" % PSimagename
)
449 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
451 if self
.PSbinexpand
== 2:
452 file.write("currentfile /ASCIIHexDecode filter")
454 file.write("currentfile /ASCII85Decode filter")
455 if self
.compressmode
or self
.imagecompressed
:
456 file.write(" /%sDecode filter" % (self
.compressmode
or self
.imagecompressed
))
462 file.write("/MaskDict\n"
465 "/Width %i\n" % self
.imagewidth
)
466 file.write("/Height %i\n" % self
.imageheight
)
467 file.write("/BitsPerComponent 8\n"
468 "/ImageMatrix %s\n" % pstrafo
)
469 file.write("/Decode [1 0]\n"
471 "/InterleaveType 1\n"
474 if self
.PSstoreimage
:
475 file.write("image\n")
477 if self
.PSbinexpand
== 2:
478 file.write("%%%%BeginData: %i ASCII Lines\n"
479 "image\n" % (asciihexlines(len(data
)) + 1))
480 asciihexstream(file, data
)
483 # the datasource is currentstream (plus some filters)
484 file.write("%%%%BeginData: %i ASCII Lines\n"
485 "image\n" % (ascii85lines(len(data
)) + 1))
486 ascii85stream(file, data
)
488 file.write("%%EndData\n")
490 file.write("grestore\n")
492 def processPDF(self
, file, writer
, context
, registry
, bbox
):
493 mode
, data
, alpha
, palettemode
, palettedata
= self
.imagedata(False)
495 name
= "image-%d-%s" % (id(self
.image
), self
.compressmode
or self
.imagecompressed
)
497 alpha
= PDFimage("%s-smask" % name
, self
.imagewidth
, self
.imageheight
,
499 self
.compressmode
, alpha
, None, registry
, addresource
=False)
501 registry
.add(PDFimage(name
, self
.imagewidth
, self
.imageheight
,
502 palettemode
, palettedata
, mode
, 8,
503 self
.compressmode
or self
.imagecompressed
, data
, alpha
, registry
))
508 self
.pdftrafo
.processPDF(file, writer
, context
, registry
)
509 file.write("/%s Do\n" % name
)
513 class bitmap_pt(bitmap_trafo
):
515 def __init__(self
, xpos_pt
, ypos_pt
, image
, width_pt
=None, height_pt
=None, ratio
=None, **kwargs
):
516 imagewidth
, imageheight
= image
.size
517 if width_pt
is not None or height_pt
is not None:
520 width_pt
= height_pt
* imagewidth
/ float(imageheight
)
522 width_pt
= ratio
* height_pt
523 elif height_pt
is None:
525 height_pt
= width_pt
* imageheight
/ float(imagewidth
)
527 height_pt
= (1.0/ratio
) * width_pt
528 elif ratio
is not None:
529 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
531 if ratio
is not None:
532 raise ValueError("must specify width_pt or height_pt to set a ratio")
533 widthdpi
, heightdpi
= image
.info
["dpi"] # fails when no dpi information available
534 width_pt
= 72.0 * imagewidth
/ float(widthdpi
)
535 height_pt
= 72.0 * imageheight
/ float(heightdpi
)
537 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
)
540 class bitmap(bitmap_pt
):
542 def __init__(self
, xpos
, ypos
, image
, width
=None, height
=None, **kwargs
):
543 xpos_pt
= unit
.topt(xpos
)
544 ypos_pt
= unit
.topt(ypos
)
545 if width
is not None:
546 width_pt
= unit
.topt(width
)
549 if height
is not None:
550 height_pt
= unit
.topt(height
)
554 bitmap_pt
.__init
__(self
, xpos_pt
, ypos_pt
, image
, width_pt
=width_pt
, height_pt
=height_pt
, **kwargs
)