1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2004-2015 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
, io
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
, mode
, data
):
103 return self
.mode
, self
.data
108 def __init__(self
, width
, height
, mode
, data
, compressed
=None, palette
=None):
109 if width
<= 0 or height
<= 0:
110 raise ValueError("valid image size")
111 if mode
not in ["L", "RGB", "CMYK", "LA", "RGBA", "CMYKA", "AL", "ARGB", "ACMYK", "P"]:
112 raise ValueError("invalid mode")
113 if compressed
is None and len(mode
)*width
*height
!= len(data
):
114 raise ValueError("wrong size of uncompressed data")
115 self
.size
= width
, height
118 self
.compressed
= compressed
119 self
.palette
= palette
122 if self
.compressed
is not None:
123 raise RuntimeError("cannot extract bands from compressed image")
124 bands
= len(self
.mode
)
125 width
, height
= self
.size
126 return [image(width
, height
, "L", bytes(self
.data
[band
::bands
])) for band
in range(bands
)]
128 def tobytes(self
, *args
):
130 raise RuntimeError("encoding not supported in this implementation")
133 def convert(self
, model
):
134 raise RuntimeError("color model conversion not supported in this implementation")
136 def save(self
, file, format
=None, **attrs
):
138 raise RuntimeError("Uncompressed image can be output as PNG only.", format
, file)
140 raise ValueError("PNG output not available due to missing zlib module.")
142 pngmode
, bytesperpixel
= {"L": (0, 1), "LA": (4, 2), "RGB": (2, 3), "RGBA": (6, 4), "P": (3, 1)}[self
.mode
]
144 raise RuntimeError("Unsupported mode '%s' for PNG output." % self
.mode
)
145 width
, height
= self
.size
146 assert len(self
.data
) == width
*height
*bytesperpixel
147 # inject filter byte to each scanline
148 data
= b
"".join(b
"\x00" + self
.data
[bytesperpixel
*width
*pos
:bytesperpixel
*width
*(pos
+1)] for pos
in range(0, height
))
149 chunk
=lambda name
, data
=b
"": struct
.pack("!I", len(data
)) + name
+ data
+ struct
.pack("!I", 0xFFFFFFFF & zlib
.crc32(name
+ data
))
150 file.write(b
"\x89PNG\r\n\x1a\n")
151 file.write(chunk(b
"IHDR", struct
.pack("!2I5B", width
, height
, 8, pngmode
, 0, 0, 0)))
153 palettemode
, palettedata
= self
.palette
.getdata()
154 if palettemode
== "L":
156 palettedata
= b
"".join(bytes([x
, x
, x
]) for x
in palettedata
)
157 if palettemode
!= "RGB":
158 raise RuntimeError("Unsupported palette mode '%s' for PNG output." % palettemode
)
159 file.write(chunk(b
"PLTE", palettedata
))
160 file.write(chunk(b
"IDAT", zlib
.compress(data
, 9)))
161 file.write(chunk(b
"IEND"))
164 class jpegimage(image
):
166 def __init__(self
, file):
170 with
open(file, "rb") as f
:
176 if data
[pos
] == 0o377 and data
[pos
+1] not in [0, 0o377]:
177 # print("marker: 0x%02x \\%03o" % (data[pos+1], data[pos+1]))
178 if data
[pos
+1] == 0o330:
182 elif not nestinglevel
:
183 raise ValueError("begin marker expected")
184 elif data
[pos
+1] == 0o331:
189 elif data
[pos
+1] in [0o300, 0o302]:
190 l
, bits
, height
, width
, components
= struct
.unpack(">HBHHB", data
[pos
+2:pos
+10])
192 raise ValueError("implementation limited to 8 bit per component only")
194 mode
= {1: "L", 3: "RGB", 4: "CMYK"}[components
]
196 raise ValueError("invalid number of components")
198 elif data
[pos
+1] == 0o340:
199 l
, id, major
, minor
, dpikind
, xdpi
, ydpi
= struct
.unpack(">H5sBBBHH", data
[pos
+2:pos
+16])
201 self
.info
= {"dpi": (xdpi
, ydpi
)}
203 self
.info
= {"dpi": (xdpi
*2.54, ydpi
*2.45)}
204 # else do not provide dpi information
208 raise ValueError("end marker expected")
209 image
.__init
__(self
, width
, height
, mode
, data
[begin
:end
], compressed
="DCT")
211 def save(self
, file, format
=None, **attrs
):
213 raise RuntimeError("JPG image can be output as JPG only.")
214 file.write(self
.data
)
217 class PSimagedata(pswriter
.PSresource
):
219 def __init__(self
, name
, data
, singlestring
, maxstrlen
):
220 pswriter
.PSresource
.__init
__(self
, "imagedata", name
)
222 self
.singlestring
= singlestring
223 self
.maxstrlen
= maxstrlen
225 def output(self
, file, writer
, registry
):
226 file.write("%%%%BeginRessource: %s\n" % self
.id)
227 if self
.singlestring
:
228 file.write("%%%%BeginData: %i ASCII Lines\n"
229 "<~" % ascii85lines(len(self
.data
)))
230 ascii85stream(file, self
.data
)
234 datalen
= len(self
.data
)
235 tailpos
= datalen
- datalen
% self
.maxstrlen
236 file.write("%%%%BeginData: %i ASCII Lines\n" %
237 ((tailpos
/self
.maxstrlen
) * ascii85lines(self
.maxstrlen
) +
238 ascii85lines(datalen
-tailpos
)))
240 for i
in range(0, tailpos
, self
.maxstrlen
):
242 ascii85stream(file, self
.data
[i
: i
+self
.maxstrlen
])
244 if datalen
!= tailpos
:
246 ascii85stream(file, self
.data
[tailpos
:])
250 file.write("/%s exch def\n" % self
.id)
251 file.write("%%EndRessource\n")
254 class PDFimagepalettedata(pdfwriter
.PDFobject
):
256 def __init__(self
, name
, data
):
257 pdfwriter
.PDFobject
.__init
__(self
, "imagepalettedata", name
)
260 def write(self
, file, writer
, registry
):
262 "/Length %d\n" % len(self
.data
))
265 file.write(self
.data
)
270 class PDFimage(pdfwriter
.PDFobject
):
272 def __init__(self
, name
, width
, height
, palettemode
, palettedata
, mode
,
273 bitspercomponent
, compressmode
, data
, smask
, registry
, addresource
=True):
274 pdfwriter
.PDFobject
.__init
__(self
, "image", name
)
277 if palettedata
is not None:
283 registry
.addresource("XObject", name
, self
, procset
=procset
)
284 if palettedata
is not None:
285 # note that acrobat wants a palette to be an object (which clearly is a bug)
286 self
.PDFpalettedata
= PDFimagepalettedata(name
, palettedata
)
287 registry
.add(self
.PDFpalettedata
)
292 self
.palettemode
= palettemode
293 self
.palettedata
= palettedata
295 self
.bitspercomponent
= bitspercomponent
296 self
.compressmode
= compressmode
300 def write(self
, file, writer
, registry
):
304 "/Width %d\n" % self
.width
)
305 file.write("/Height %d\n" % self
.height
)
306 if self
.palettedata
is not None:
307 file.write("/ColorSpace [ /Indexed %s %i\n" % (devicenames
[self
.palettemode
], len(self
.palettedata
)/3-1))
308 file.write("%d 0 R\n" % registry
.getrefno(self
.PDFpalettedata
))
311 file.write("/ColorSpace %s\n" % devicenames
[self
.mode
])
313 file.write("/SMask %d 0 R\n" % registry
.getrefno(self
.smask
))
314 file.write("/BitsPerComponent %d\n" % self
.bitspercomponent
)
315 file.write("/Length %d\n" % len(self
.data
))
316 if self
.compressmode
:
317 file.write("/Filter /%sDecode\n" % self
.compressmode
)
320 file.write_bytes(self
.data
)
326 class bitmap_trafo(baseclasses
.canvasitem
):
328 def __init__(self
, trafo
, image
,
329 PSstoreimage
=0, PSmaxstrlen
=4093, PSbinexpand
=1,
330 compressmode
="Flate", flatecompresslevel
=6,
331 dctquality
=75, dctoptimize
=0, dctprogression
=0):
332 self
.pdftrafo
= trafo
334 self
.imagewidth
, self
.imageheight
= image
.size
336 self
.PSstoreimage
= PSstoreimage
337 self
.PSmaxstrlen
= PSmaxstrlen
338 self
.PSbinexpand
= PSbinexpand
339 self
.compressmode
= compressmode
340 self
.flatecompresslevel
= flatecompresslevel
341 self
.dctquality
= dctquality
342 self
.dctoptimize
= dctoptimize
343 self
.dctprogression
= dctprogression
346 self
.imagecompressed
= image
.compressed
348 self
.imagecompressed
= None
349 if self
.compressmode
not in [None, "Flate", "DCT"]:
350 raise ValueError("invalid compressmode '%s'" % self
.compressmode
)
351 if self
.imagecompressed
not in [None, "Flate", "DCT"]:
352 raise ValueError("invalid compressed image '%s'" % self
.imagecompressed
)
353 if self
.compressmode
is not None and self
.imagecompressed
is not None:
354 raise ValueError("compression of a compressed image not supported")
355 if not haszlib
and self
.compressmode
== "Flate":
356 logger
.warning("zlib module not available, disable compression")
357 self
.compressmode
= None
359 def imagedata(self
, interleavealpha
):
360 """ Returns a tuple (mode, data, alpha, palettemode, palettedata)
361 where mode does not contain the alpha channel anymore.
363 If there is an alpha channel, for interleavealpha == False it is
364 returned as a band in alpha itself. For interleavealpha == True
365 alpha will be True and the channel is interleaved in front of each
369 alpha
= palettemode
= palettedata
= None
372 if mode
.startswith("A"):
379 data
= image(self
.imagewidth
, self
.imageheight
, mode
,
380 b
"".join([bytes(values
)
381 for values
in zip(*[band
.tobytes()
382 for band
in bands
[1:]])]), palette
=data
.palette
)
383 if mode
.endswith("A"):
388 bands
= list(bands
[-1:]) + list(bands
[:-1])
389 data
= image(self
.imagewidth
, self
.imageheight
, "A%s" % mode
,
390 b
"".join([bytes(values
)
391 for values
in zip(*[band
.tobytes()
392 for band
in bands
])]), palette
=data
.palette
)
395 data
= image(self
.imagewidth
, self
.imageheight
, mode
,
396 b
"".join([bytes(values
)
397 for values
in zip(*[band
.tobytes()
398 for band
in bands
[:-1]])]), palette
=data
.palette
)
401 palettemode
, palettedata
= data
.palette
.getdata()
402 if palettemode
not in ["L", "RGB", "CMYK"]:
403 logger
.warning("image with invalid palette mode '%s' converted to rgb image" % palettemode
)
404 data
= data
.convert("RGB")
410 logger
.warning("specific single channel image mode not natively supported, converted to regular grayscale")
411 data
= data
.convert("L")
413 elif mode
not in ["CMYK", "RGB"]:
414 logger
.warning("image with invalid mode converted to rgb")
415 data
= data
.convert("RGB")
418 if self
.compressmode
== "Flate":
419 data
= zlib
.compress(data
.tobytes(), self
.flatecompresslevel
)
420 elif self
.compressmode
== "DCT":
421 data
= data
.tobytes("jpeg", mode
, self
.dctquality
, self
.dctoptimize
, self
.dctprogression
)
423 data
= data
.tobytes()
424 if alpha
and not interleavealpha
:
425 # we might want a separate alphacompressmode
426 if self
.compressmode
== "Flate":
427 alpha
= zlib
.compress(alpha
.tobytes(), self
.flatecompresslevel
)
428 elif self
.compressmode
== "DCT":
429 alpha
= alpha
.tobytes("jpeg", mode
, self
.dctquality
, self
.dctoptimize
, self
.dctprogression
)
431 alpha
= alpha
.tobytes()
433 return mode
, data
, alpha
, palettemode
, palettedata
437 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(0.0, 0.0))
438 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(0.0, 1.0))
439 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(1.0, 0.0))
440 bb
.includepoint_pt(*self
.pdftrafo
.apply_pt(1.0, 1.0))
443 def processPS(self
, file, writer
, context
, registry
, bbox
):
444 mode
, data
, alpha
, palettemode
, palettedata
= self
.imagedata(True)
445 pstrafo
= trafo
.translate_pt(0, -1.0).scaled(self
.imagewidth
, -self
.imageheight
)*self
.pdftrafo
.inverse()
447 PSsinglestring
= self
.PSstoreimage
and len(data
) < self
.PSmaxstrlen
449 PSimagename
= "image-%d-%s-singlestring" % (id(self
.image
), self
.compressmode
)
451 PSimagename
= "image-%d-%s-stringarray" % (id(self
.image
), self
.compressmode
)
453 if self
.PSstoreimage
and not PSsinglestring
:
454 registry
.add(pswriter
.PSdefinition("imagedataaccess",
455 b
"{ /imagedataindex load " # get list index
456 b
"dup 1 add /imagedataindex exch store " # store increased index
457 b
"/imagedataid load exch get }")) # select string from array
458 if self
.PSstoreimage
:
459 registry
.add(PSimagedata(PSimagename
, data
, PSsinglestring
, self
.PSmaxstrlen
))
462 file.write("gsave\n")
463 if palettedata
is not None:
464 file.write("[ /Indexed %s %i\n" % (devicenames
[palettemode
], len(palettedata
)/3-1))
465 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(palettedata
)))
467 ascii85stream(file, palettedata
)
470 file.write("] setcolorspace\n")
472 file.write("%s setcolorspace\n" % devicenames
[mode
])
474 if self
.PSstoreimage
and not PSsinglestring
:
475 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
476 "/imagedataid %s store\n" % PSimagename
)
480 file.write("/ImageType 3\n"
483 file.write("/ImageType 1\n"
484 "/Width %i\n" % self
.imagewidth
)
485 file.write("/Height %i\n" % self
.imageheight
)
486 file.write("/BitsPerComponent 8\n"
487 "/ImageMatrix %s\n" % pstrafo
)
488 file.write("/Decode %s\n" % decodestrings
[mode
])
490 file.write("/DataSource ")
491 if self
.PSstoreimage
:
493 file.write("/%s load" % PSimagename
)
495 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
497 if self
.PSbinexpand
== 2:
498 file.write("currentfile /ASCIIHexDecode filter")
500 file.write("currentfile /ASCII85Decode filter")
501 if self
.compressmode
or self
.imagecompressed
:
502 file.write(" /%sDecode filter" % (self
.compressmode
or self
.imagecompressed
))
508 file.write("/MaskDict\n"
511 "/Width %i\n" % self
.imagewidth
)
512 file.write("/Height %i\n" % self
.imageheight
)
513 file.write("/BitsPerComponent 8\n"
514 "/ImageMatrix %s\n" % pstrafo
)
515 file.write("/Decode [1 0]\n"
517 "/InterleaveType 1\n"
520 if self
.PSstoreimage
:
521 file.write("image\n")
523 if self
.PSbinexpand
== 2:
524 file.write("%%%%BeginData: %i ASCII Lines\n"
525 "image\n" % (asciihexlines(len(data
)) + 1))
526 asciihexstream(file, data
)
529 # the datasource is currentstream (plus some filters)
530 file.write("%%%%BeginData: %i ASCII Lines\n"
531 "image\n" % (ascii85lines(len(data
)) + 1))
532 ascii85stream(file, data
)
534 file.write("%%EndData\n")
536 file.write("grestore\n")
538 def processPDF(self
, file, writer
, context
, registry
, bbox
):
539 mode
, data
, alpha
, palettemode
, palettedata
= self
.imagedata(False)
541 name
= "image-%d-%s" % (id(self
.image
), self
.compressmode
or self
.imagecompressed
)
543 alpha
= PDFimage("%s-smask" % name
, self
.imagewidth
, self
.imageheight
,
545 self
.compressmode
, alpha
, None, registry
, addresource
=False)
547 registry
.add(PDFimage(name
, self
.imagewidth
, self
.imageheight
,
548 palettemode
, palettedata
, mode
, 8,
549 self
.compressmode
or self
.imagecompressed
, data
, alpha
, registry
))
554 self
.pdftrafo
.processPDF(file, writer
, context
, registry
)
555 file.write("/%s Do\n" % name
)
558 def processSVG(self
, xml
, writer
, context
, registry
, bbox
):
559 if self
.compressmode
== "Flate":
561 self
.image
.save(f
, "png")
562 inlinedata
= "data:image/png;base64," + binascii
.b2a_base64(f
.getvalue()).decode('ascii').replace("\n", "")
563 elif self
.compressmode
== "DCT" or self
.imagecompressed
== "DCT":
565 self
.image
.save(f
, "jpeg")
566 inlinedata
= "data:image/jpeg;base64," + binascii
.b2a_base64(f
.getvalue()).decode('ascii').replace("\n", "")
568 raise ValueError("SVG cannot store uncompressed image data.")
569 attrs
= {"preserveAspectRatio": "none", "x": "0", "y": "-1", "width": "1", "height": "1", "xlink:href": inlinedata
}
570 self
.pdftrafo
.processSVGattrs(attrs
, writer
, context
, registry
)
571 xml
.startSVGElement("image", attrs
)
572 xml
.endSVGElement("image")
575 class bitmap_pt(bitmap_trafo
):
577 def __init__(self
, xpos_pt
, ypos_pt
, image
, width_pt
=None, height_pt
=None, ratio
=None, **kwargs
):
578 imagewidth
, imageheight
= image
.size
579 if width_pt
is not None or height_pt
is not None:
582 width_pt
= height_pt
* imagewidth
/ float(imageheight
)
584 width_pt
= ratio
* height_pt
585 elif height_pt
is None:
587 height_pt
= width_pt
* imageheight
/ float(imagewidth
)
589 height_pt
= (1.0/ratio
) * width_pt
590 elif ratio
is not None:
591 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
593 if ratio
is not None:
594 raise ValueError("must specify width_pt or height_pt to set a ratio")
595 widthdpi
, heightdpi
= image
.info
["dpi"] # fails when no dpi information available
596 width_pt
= 72.0 * imagewidth
/ float(widthdpi
)
597 height_pt
= 72.0 * imageheight
/ float(heightdpi
)
599 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
)
602 class bitmap(bitmap_pt
):
604 def __init__(self
, xpos
, ypos
, image
, width
=None, height
=None, **kwargs
):
605 xpos_pt
= unit
.topt(xpos
)
606 ypos_pt
= unit
.topt(ypos
)
607 if width
is not None:
608 width_pt
= unit
.topt(width
)
611 if height
is not None:
612 height_pt
= unit
.topt(height
)
616 bitmap_pt
.__init
__(self
, xpos_pt
, ypos_pt
, image
, width_pt
=width_pt
, height_pt
=height_pt
, **kwargs
)