├── README.md ├── png.py ├── raster2laser_gcode.inx └── raster2laser_gcode.py /README.md: -------------------------------------------------------------------------------- 1 | # Various Inkscape extensions 2 | 3 | - Raster 2 Laser GCode generator 4 | - 5 | 6 | #Descriptions 7 | - Raster 2 Laser GCode generator is an extension to generate Gcode for a laser cutter/engraver (or pen plotter), it can generate various type of outputs from a simple B&W (on/off) to a more detailed Grayscale (pwm) 8 | 9 | 10 | #Installing: 11 | 12 | Simply copy all the files in the folder "Extensions" of Inkscape 13 | 14 | >Windows ) "C:\<...>\Inkscape\share\extensions" 15 | 16 | >Linux ) "/usr/share/inkscape/extensions" 17 | 18 | >Mac ) "/Applications/Inkscape.app/Contents/Resources/extensions" 19 | 20 | 21 | for unix (& mac maybe) change the permission on the file: 22 | 23 | >>chmod 755 for all the *.py files 24 | 25 | >>chmod 644 for all the *.inx files 26 | 27 | 28 | 29 | #Usage of "Raster 2 Laser GCode generator": 30 | 31 | [Required file: png.py / raster2laser_gcode.inx / raster2laser_gcode.py] 32 | 33 | - Step 1) Resize the inkscape document to match the dimension of your working area on the laser cutter/engraver (Shift+Ctrl+D) 34 | 35 | - Step 2) Draw or import the image 36 | 37 | - Step 3) To run the extension go to: Extension > 305 Engineering > Raster 2 Laser GCode generator 38 | 39 | - Step 4) Play! 40 | 41 | 42 | 43 | 44 | #Note 45 | I have created all the file except for png.py , see that file for details on the license 46 | -------------------------------------------------------------------------------- /png.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # png.py - PNG encoder/decoder in pure Python 4 | # 5 | # Copyright (C) 2006 Johann C. Rocholl 6 | # Portions Copyright (C) 2009 David Jones 7 | # And probably portions Copyright (C) 2006 Nicko van Someren 8 | # 9 | # Original concept by Johann C. Rocholl. 10 | # 11 | # LICENCE (MIT) 12 | # 13 | # Permission is hereby granted, free of charge, to any person 14 | # obtaining a copy of this software and associated documentation files 15 | # (the "Software"), to deal in the Software without restriction, 16 | # including without limitation the rights to use, copy, modify, merge, 17 | # publish, distribute, sublicense, and/or sell copies of the Software, 18 | # and to permit persons to whom the Software is furnished to do so, 19 | # subject to the following conditions: 20 | # 21 | # The above copyright notice and this permission notice shall be 22 | # included in all copies or substantial portions of the Software. 23 | # 24 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 27 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 28 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 29 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 30 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | # SOFTWARE. 32 | 33 | """ 34 | Pure Python PNG Reader/Writer 35 | 36 | This Python module implements support for PNG images (see PNG 37 | specification at http://www.w3.org/TR/2003/REC-PNG-20031110/ ). It reads 38 | and writes PNG files with all allowable bit depths 39 | (1/2/4/8/16/24/32/48/64 bits per pixel) and colour combinations: 40 | greyscale (1/2/4/8/16 bit); RGB, RGBA, LA (greyscale with alpha) with 41 | 8/16 bits per channel; colour mapped images (1/2/4/8 bit). 42 | Adam7 interlacing is supported for reading and 43 | writing. A number of optional chunks can be specified (when writing) 44 | and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``. 45 | 46 | For help, type ``import png; help(png)`` in your python interpreter. 47 | 48 | A good place to start is the :class:`Reader` and :class:`Writer` 49 | classes. 50 | 51 | Requires Python 2.3. Limited support is available for Python 2.2, but 52 | not everything works. Best with Python 2.4 and higher. Installation is 53 | trivial, but see the ``README.txt`` file (with the source distribution) 54 | for details. 55 | 56 | This file can also be used as a command-line utility to convert 57 | `Netpbm `_ PNM files to PNG, and the 58 | reverse conversion from PNG to PNM. The interface is similar to that 59 | of the ``pnmtopng`` program from Netpbm. Type ``python png.py --help`` 60 | at the shell prompt for usage and a list of options. 61 | 62 | A note on spelling and terminology 63 | ---------------------------------- 64 | 65 | Generally British English spelling is used in the documentation. So 66 | that's "greyscale" and "colour". This not only matches the author's 67 | native language, it's also used by the PNG specification. 68 | 69 | The major colour models supported by PNG (and hence by PyPNG) are: 70 | greyscale, RGB, greyscale--alpha, RGB--alpha. These are sometimes 71 | referred to using the abbreviations: L, RGB, LA, RGBA. In this case 72 | each letter abbreviates a single channel: *L* is for Luminance or Luma 73 | or Lightness which is the channel used in greyscale images; *R*, *G*, 74 | *B* stand for Red, Green, Blue, the components of a colour image; *A* 75 | stands for Alpha, the opacity channel (used for transparency effects, 76 | but higher values are more opaque, so it makes sense to call it 77 | opacity). 78 | 79 | A note on formats 80 | ----------------- 81 | 82 | When getting pixel data out of this module (reading) and presenting 83 | data to this module (writing) there are a number of ways the data could 84 | be represented as a Python value. Generally this module uses one of 85 | three formats called "flat row flat pixel", "boxed row flat pixel", and 86 | "boxed row boxed pixel". Basically the concern is whether each pixel 87 | and each row comes in its own little tuple (box), or not. 88 | 89 | Consider an image that is 3 pixels wide by 2 pixels high, and each pixel 90 | has RGB components: 91 | 92 | Boxed row flat pixel:: 93 | 94 | list([R,G,B, R,G,B, R,G,B], 95 | [R,G,B, R,G,B, R,G,B]) 96 | 97 | Each row appears as its own list, but the pixels are flattened so 98 | that three values for one pixel simply follow the three values for 99 | the previous pixel. This is the most common format used, because it 100 | provides a good compromise between space and convenience. PyPNG regards 101 | itself as at liberty to replace any sequence type with any sufficiently 102 | compatible other sequence type; in practice each row is an array (from 103 | the array module), and the outer list is sometimes an iterator rather 104 | than an explicit list (so that streaming is possible). 105 | 106 | Flat row flat pixel:: 107 | 108 | [R,G,B, R,G,B, R,G,B, 109 | R,G,B, R,G,B, R,G,B] 110 | 111 | The entire image is one single giant sequence of colour values. 112 | Generally an array will be used (to save space), not a list. 113 | 114 | Boxed row boxed pixel:: 115 | 116 | list([ (R,G,B), (R,G,B), (R,G,B) ], 117 | [ (R,G,B), (R,G,B), (R,G,B) ]) 118 | 119 | Each row appears in its own list, but each pixel also appears in its own 120 | tuple. A serious memory burn in Python. 121 | 122 | In all cases the top row comes first, and for each row the pixels are 123 | ordered from left-to-right. Within a pixel the values appear in the 124 | order, R-G-B-A (or L-A for greyscale--alpha). 125 | 126 | There is a fourth format, mentioned because it is used internally, 127 | is close to what lies inside a PNG file itself, and has some support 128 | from the public API. This format is called packed. When packed, 129 | each row is a sequence of bytes (integers from 0 to 255), just as 130 | it is before PNG scanline filtering is applied. When the bit depth 131 | is 8 this is essentially the same as boxed row flat pixel; when the 132 | bit depth is less than 8, several pixels are packed into each byte; 133 | when the bit depth is 16 (the only value more than 8 that is supported 134 | by the PNG image format) each pixel value is decomposed into 2 bytes 135 | (and `packed` is a misnomer). This format is used by the 136 | :meth:`Writer.write_packed` method. It isn't usually a convenient 137 | format, but may be just right if the source data for the PNG image 138 | comes from something that uses a similar format (for example, 1-bit 139 | BMPs, or another PNG file). 140 | 141 | And now, my famous members 142 | -------------------------- 143 | """ 144 | 145 | # http://www.python.org/doc/2.2.3/whatsnew/node5.html 146 | from __future__ import generators 147 | 148 | __version__ = "0.0.16" 149 | 150 | from array import array 151 | try: # See :pyver:old 152 | import itertools 153 | except ImportError: 154 | pass 155 | import math 156 | # http://www.python.org/doc/2.4.4/lib/module-operator.html 157 | import operator 158 | import struct 159 | import sys 160 | import zlib 161 | # http://www.python.org/doc/2.4.4/lib/module-warnings.html 162 | import warnings 163 | try: 164 | # `cpngfilters` is a Cython module: it must be compiled by 165 | # Cython for this import to work. 166 | # If this import does work, then it overrides pure-python 167 | # filtering functions defined later in this file (see `class 168 | # pngfilters`). 169 | import cpngfilters as pngfilters 170 | except ImportError: 171 | pass 172 | 173 | 174 | __all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array'] 175 | 176 | 177 | # The PNG signature. 178 | # http://www.w3.org/TR/PNG/#5PNG-file-signature 179 | _signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10) 180 | 181 | _adam7 = ((0, 0, 8, 8), 182 | (4, 0, 8, 8), 183 | (0, 4, 4, 8), 184 | (2, 0, 4, 4), 185 | (0, 2, 2, 4), 186 | (1, 0, 2, 2), 187 | (0, 1, 1, 2)) 188 | 189 | def group(s, n): 190 | # See http://www.python.org/doc/2.6/library/functions.html#zip 191 | return zip(*[iter(s)]*n) 192 | 193 | def isarray(x): 194 | """Same as ``isinstance(x, array)`` except on Python 2.2, where it 195 | always returns ``False``. This helps PyPNG work on Python 2.2. 196 | """ 197 | 198 | try: 199 | return isinstance(x, array) 200 | except TypeError: 201 | # Because on Python 2.2 array.array is not a type. 202 | return False 203 | 204 | try: 205 | array.tobytes 206 | except AttributeError: 207 | try: # see :pyver:old 208 | array.tostring 209 | except AttributeError: 210 | def tostring(row): 211 | l = len(row) 212 | return struct.pack('%dB' % l, *row) 213 | else: 214 | def tostring(row): 215 | """Convert row of bytes to string. Expects `row` to be an 216 | ``array``. 217 | """ 218 | return row.tostring() 219 | else: 220 | def tostring(row): 221 | """ Python3 definition, array.tostring() is deprecated in Python3 222 | """ 223 | return row.tobytes() 224 | 225 | # Conditionally convert to bytes. Works on Python 2 and Python 3. 226 | try: 227 | bytes('', 'ascii') 228 | def strtobytes(x): return bytes(x, 'iso8859-1') 229 | def bytestostr(x): return str(x, 'iso8859-1') 230 | except (NameError, TypeError): 231 | # We get NameError when bytes() does not exist (most Python 232 | # 2.x versions), and TypeError when bytes() exists but is on 233 | # Python 2.x (when it is an alias for str() and takes at most 234 | # one argument). 235 | strtobytes = str 236 | bytestostr = str 237 | 238 | def interleave_planes(ipixels, apixels, ipsize, apsize): 239 | """ 240 | Interleave (colour) planes, e.g. RGB + A = RGBA. 241 | 242 | Return an array of pixels consisting of the `ipsize` elements of 243 | data from each pixel in `ipixels` followed by the `apsize` elements 244 | of data from each pixel in `apixels`. Conventionally `ipixels` 245 | and `apixels` are byte arrays so the sizes are bytes, but it 246 | actually works with any arrays of the same type. The returned 247 | array is the same type as the input arrays which should be the 248 | same type as each other. 249 | """ 250 | 251 | itotal = len(ipixels) 252 | atotal = len(apixels) 253 | newtotal = itotal + atotal 254 | newpsize = ipsize + apsize 255 | # Set up the output buffer 256 | # See http://www.python.org/doc/2.4.4/lib/module-array.html#l2h-1356 257 | out = array(ipixels.typecode) 258 | # It's annoying that there is no cheap way to set the array size :-( 259 | out.extend(ipixels) 260 | out.extend(apixels) 261 | # Interleave in the pixel data 262 | for i in range(ipsize): 263 | out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize] 264 | for i in range(apsize): 265 | out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize] 266 | return out 267 | 268 | def check_palette(palette): 269 | """Check a palette argument (to the :class:`Writer` class) 270 | for validity. Returns the palette as a list if okay; raises an 271 | exception otherwise. 272 | """ 273 | 274 | # None is the default and is allowed. 275 | if palette is None: 276 | return None 277 | 278 | p = list(palette) 279 | if not (0 < len(p) <= 256): 280 | raise ValueError("a palette must have between 1 and 256 entries") 281 | seen_triple = False 282 | for i,t in enumerate(p): 283 | if len(t) not in (3,4): 284 | raise ValueError( 285 | "palette entry %d: entries must be 3- or 4-tuples." % i) 286 | if len(t) == 3: 287 | seen_triple = True 288 | if seen_triple and len(t) == 4: 289 | raise ValueError( 290 | "palette entry %d: all 4-tuples must precede all 3-tuples" % i) 291 | for x in t: 292 | if int(x) != x or not(0 <= x <= 255): 293 | raise ValueError( 294 | "palette entry %d: values must be integer: 0 <= x <= 255" % i) 295 | return p 296 | 297 | def check_sizes(size, width, height): 298 | """Check that these arguments, in supplied, are consistent. 299 | Return a (width, height) pair. 300 | """ 301 | 302 | if not size: 303 | return width, height 304 | 305 | if len(size) != 2: 306 | raise ValueError( 307 | "size argument should be a pair (width, height)") 308 | if width is not None and width != size[0]: 309 | raise ValueError( 310 | "size[0] (%r) and width (%r) should match when both are used." 311 | % (size[0], width)) 312 | if height is not None and height != size[1]: 313 | raise ValueError( 314 | "size[1] (%r) and height (%r) should match when both are used." 315 | % (size[1], height)) 316 | return size 317 | 318 | def check_color(c, greyscale, which): 319 | """Checks that a colour argument for transparent or 320 | background options is the right form. Returns the colour 321 | (which, if it's a bar integer, is "corrected" to a 1-tuple). 322 | """ 323 | 324 | if c is None: 325 | return c 326 | if greyscale: 327 | try: 328 | l = len(c) 329 | except TypeError: 330 | c = (c,) 331 | if len(c) != 1: 332 | raise ValueError("%s for greyscale must be 1-tuple" % 333 | which) 334 | if not isinteger(c[0]): 335 | raise ValueError( 336 | "%s colour for greyscale must be integer" % which) 337 | else: 338 | if not (len(c) == 3 and 339 | isinteger(c[0]) and 340 | isinteger(c[1]) and 341 | isinteger(c[2])): 342 | raise ValueError( 343 | "%s colour must be a triple of integers" % which) 344 | return c 345 | 346 | class Error(Exception): 347 | def __str__(self): 348 | return self.__class__.__name__ + ': ' + ' '.join(self.args) 349 | 350 | class FormatError(Error): 351 | """Problem with input file format. In other words, PNG file does 352 | not conform to the specification in some way and is invalid. 353 | """ 354 | 355 | class ChunkError(FormatError): 356 | pass 357 | 358 | 359 | class Writer: 360 | """ 361 | PNG encoder in pure Python. 362 | """ 363 | 364 | def __init__(self, width=None, height=None, 365 | size=None, 366 | greyscale=False, 367 | alpha=False, 368 | bitdepth=8, 369 | palette=None, 370 | transparent=None, 371 | background=None, 372 | gamma=None, 373 | compression=None, 374 | interlace=False, 375 | bytes_per_sample=None, # deprecated 376 | planes=None, 377 | colormap=None, 378 | maxval=None, 379 | chunk_limit=2**20): 380 | """ 381 | Create a PNG encoder object. 382 | 383 | Arguments: 384 | 385 | width, height 386 | Image size in pixels, as two separate arguments. 387 | size 388 | Image size (w,h) in pixels, as single argument. 389 | greyscale 390 | Input data is greyscale, not RGB. 391 | alpha 392 | Input data has alpha channel (RGBA or LA). 393 | bitdepth 394 | Bit depth: from 1 to 16. 395 | palette 396 | Create a palette for a colour mapped image (colour type 3). 397 | transparent 398 | Specify a transparent colour (create a ``tRNS`` chunk). 399 | background 400 | Specify a default background colour (create a ``bKGD`` chunk). 401 | gamma 402 | Specify a gamma value (create a ``gAMA`` chunk). 403 | compression 404 | zlib compression level: 0 (none) to 9 (more compressed); 405 | default: -1 or None. 406 | interlace 407 | Create an interlaced image. 408 | chunk_limit 409 | Write multiple ``IDAT`` chunks to save memory. 410 | 411 | The image size (in pixels) can be specified either by using the 412 | `width` and `height` arguments, or with the single `size` 413 | argument. If `size` is used it should be a pair (*width*, 414 | *height*). 415 | 416 | `greyscale` and `alpha` are booleans that specify whether 417 | an image is greyscale (or colour), and whether it has an 418 | alpha channel (or not). 419 | 420 | `bitdepth` specifies the bit depth of the source pixel values. 421 | Each source pixel value must be an integer between 0 and 422 | ``2**bitdepth-1``. For example, 8-bit images have values 423 | between 0 and 255. PNG only stores images with bit depths of 424 | 1,2,4,8, or 16. When `bitdepth` is not one of these values, 425 | the next highest valid bit depth is selected, and an ``sBIT`` 426 | (significant bits) chunk is generated that specifies the 427 | original precision of the source image. In this case the 428 | supplied pixel values will be rescaled to fit the range of 429 | the selected bit depth. 430 | 431 | The details of which bit depth / colour model combinations the 432 | PNG file format supports directly, are somewhat arcane 433 | (refer to the PNG specification for full details). Briefly: 434 | "small" bit depths (1,2,4) are only allowed with greyscale and 435 | colour mapped images; colour mapped images cannot have bit depth 436 | 16. 437 | 438 | For colour mapped images (in other words, when the `palette` 439 | argument is specified) the `bitdepth` argument must match one of 440 | the valid PNG bit depths: 1, 2, 4, or 8. (It is valid to have a 441 | PNG image with a palette and an ``sBIT`` chunk, but the meaning 442 | is slightly different; it would be awkward to press the 443 | `bitdepth` argument into service for this.) 444 | 445 | The `palette` option, when specified, causes a colour mapped 446 | image to be created: the PNG colour type is set to 3; greyscale 447 | must not be set; alpha must not be set; transparent must not be 448 | set; the bit depth must be 1,2,4, or 8. When a colour mapped 449 | image is created, the pixel values are palette indexes and 450 | the `bitdepth` argument specifies the size of these indexes 451 | (not the size of the colour values in the palette). 452 | 453 | The palette argument value should be a sequence of 3- or 454 | 4-tuples. 3-tuples specify RGB palette entries; 4-tuples 455 | specify RGBA palette entries. If both 4-tuples and 3-tuples 456 | appear in the sequence then all the 4-tuples must come 457 | before all the 3-tuples. A ``PLTE`` chunk is created; if there 458 | are 4-tuples then a ``tRNS`` chunk is created as well. The 459 | ``PLTE`` chunk will contain all the RGB triples in the same 460 | sequence; the ``tRNS`` chunk will contain the alpha channel for 461 | all the 4-tuples, in the same sequence. Palette entries 462 | are always 8-bit. 463 | 464 | If specified, the `transparent` and `background` parameters must 465 | be a tuple with three integer values for red, green, blue, or 466 | a simple integer (or singleton tuple) for a greyscale image. 467 | 468 | If specified, the `gamma` parameter must be a positive number 469 | (generally, a float). A ``gAMA`` chunk will be created. 470 | Note that this will not change the values of the pixels as 471 | they appear in the PNG file, they are assumed to have already 472 | been converted appropriately for the gamma specified. 473 | 474 | The `compression` argument specifies the compression level to 475 | be used by the ``zlib`` module. Values from 1 to 9 specify 476 | compression, with 9 being "more compressed" (usually smaller 477 | and slower, but it doesn't always work out that way). 0 means 478 | no compression. -1 and ``None`` both mean that the default 479 | level of compession will be picked by the ``zlib`` module 480 | (which is generally acceptable). 481 | 482 | If `interlace` is true then an interlaced image is created 483 | (using PNG's so far only interace method, *Adam7*). This does 484 | not affect how the pixels should be presented to the encoder, 485 | rather it changes how they are arranged into the PNG file. 486 | On slow connexions interlaced images can be partially decoded 487 | by the browser to give a rough view of the image that is 488 | successively refined as more image data appears. 489 | 490 | .. note :: 491 | 492 | Enabling the `interlace` option requires the entire image 493 | to be processed in working memory. 494 | 495 | `chunk_limit` is used to limit the amount of memory used whilst 496 | compressing the image. In order to avoid using large amounts of 497 | memory, multiple ``IDAT`` chunks may be created. 498 | """ 499 | 500 | # At the moment the `planes` argument is ignored; 501 | # its purpose is to act as a dummy so that 502 | # ``Writer(x, y, **info)`` works, where `info` is a dictionary 503 | # returned by Reader.read and friends. 504 | # Ditto for `colormap`. 505 | 506 | width, height = check_sizes(size, width, height) 507 | del size 508 | 509 | if width <= 0 or height <= 0: 510 | raise ValueError("width and height must be greater than zero") 511 | if not isinteger(width) or not isinteger(height): 512 | raise ValueError("width and height must be integers") 513 | # http://www.w3.org/TR/PNG/#7Integers-and-byte-order 514 | if width > 2**32-1 or height > 2**32-1: 515 | raise ValueError("width and height cannot exceed 2**32-1") 516 | 517 | if alpha and transparent is not None: 518 | raise ValueError( 519 | "transparent colour not allowed with alpha channel") 520 | 521 | if bytes_per_sample is not None: 522 | warnings.warn('please use bitdepth instead of bytes_per_sample', 523 | DeprecationWarning) 524 | if bytes_per_sample not in (0.125, 0.25, 0.5, 1, 2): 525 | raise ValueError( 526 | "bytes per sample must be .125, .25, .5, 1, or 2") 527 | bitdepth = int(8*bytes_per_sample) 528 | del bytes_per_sample 529 | if not isinteger(bitdepth) or bitdepth < 1 or 16 < bitdepth: 530 | raise ValueError("bitdepth (%r) must be a postive integer <= 16" % 531 | bitdepth) 532 | 533 | self.rescale = None 534 | if palette: 535 | if bitdepth not in (1,2,4,8): 536 | raise ValueError("with palette, bitdepth must be 1, 2, 4, or 8") 537 | if transparent is not None: 538 | raise ValueError("transparent and palette not compatible") 539 | if alpha: 540 | raise ValueError("alpha and palette not compatible") 541 | if greyscale: 542 | raise ValueError("greyscale and palette not compatible") 543 | else: 544 | # No palette, check for sBIT chunk generation. 545 | if alpha or not greyscale: 546 | if bitdepth not in (8,16): 547 | targetbitdepth = (8,16)[bitdepth > 8] 548 | self.rescale = (bitdepth, targetbitdepth) 549 | bitdepth = targetbitdepth 550 | del targetbitdepth 551 | else: 552 | assert greyscale 553 | assert not alpha 554 | if bitdepth not in (1,2,4,8,16): 555 | if bitdepth > 8: 556 | targetbitdepth = 16 557 | elif bitdepth == 3: 558 | targetbitdepth = 4 559 | else: 560 | assert bitdepth in (5,6,7) 561 | targetbitdepth = 8 562 | self.rescale = (bitdepth, targetbitdepth) 563 | bitdepth = targetbitdepth 564 | del targetbitdepth 565 | 566 | if bitdepth < 8 and (alpha or not greyscale and not palette): 567 | raise ValueError( 568 | "bitdepth < 8 only permitted with greyscale or palette") 569 | if bitdepth > 8 and palette: 570 | raise ValueError( 571 | "bit depth must be 8 or less for images with palette") 572 | 573 | transparent = check_color(transparent, greyscale, 'transparent') 574 | background = check_color(background, greyscale, 'background') 575 | 576 | # It's important that the true boolean values (greyscale, alpha, 577 | # colormap, interlace) are converted to bool because Iverson's 578 | # convention is relied upon later on. 579 | self.width = width 580 | self.height = height 581 | self.transparent = transparent 582 | self.background = background 583 | self.gamma = gamma 584 | self.greyscale = bool(greyscale) 585 | self.alpha = bool(alpha) 586 | self.colormap = bool(palette) 587 | self.bitdepth = int(bitdepth) 588 | self.compression = compression 589 | self.chunk_limit = chunk_limit 590 | self.interlace = bool(interlace) 591 | self.palette = check_palette(palette) 592 | 593 | self.color_type = 4*self.alpha + 2*(not greyscale) + 1*self.colormap 594 | assert self.color_type in (0,2,3,4,6) 595 | 596 | self.color_planes = (3,1)[self.greyscale or self.colormap] 597 | self.planes = self.color_planes + self.alpha 598 | # :todo: fix for bitdepth < 8 599 | self.psize = (self.bitdepth/8) * self.planes 600 | 601 | def make_palette(self): 602 | """Create the byte sequences for a ``PLTE`` and if necessary a 603 | ``tRNS`` chunk. Returned as a pair (*p*, *t*). *t* will be 604 | ``None`` if no ``tRNS`` chunk is necessary. 605 | """ 606 | 607 | p = array('B') 608 | t = array('B') 609 | 610 | for x in self.palette: 611 | p.extend(x[0:3]) 612 | if len(x) > 3: 613 | t.append(x[3]) 614 | p = tostring(p) 615 | t = tostring(t) 616 | if t: 617 | return p,t 618 | return p,None 619 | 620 | def write(self, outfile, rows): 621 | """Write a PNG image to the output file. `rows` should be 622 | an iterable that yields each row in boxed row flat pixel 623 | format. The rows should be the rows of the original image, 624 | so there should be ``self.height`` rows of ``self.width * 625 | self.planes`` values. If `interlace` is specified (when 626 | creating the instance), then an interlaced PNG file will 627 | be written. Supply the rows in the normal image order; 628 | the interlacing is carried out internally. 629 | 630 | .. note :: 631 | 632 | Interlacing will require the entire image to be in working 633 | memory. 634 | """ 635 | 636 | if self.interlace: 637 | fmt = 'BH'[self.bitdepth > 8] 638 | a = array(fmt, itertools.chain(*rows)) 639 | return self.write_array(outfile, a) 640 | else: 641 | nrows = self.write_passes(outfile, rows) 642 | if nrows != self.height: 643 | raise ValueError( 644 | "rows supplied (%d) does not match height (%d)" % 645 | (nrows, self.height)) 646 | 647 | def write_passes(self, outfile, rows, packed=False): 648 | """ 649 | Write a PNG image to the output file. 650 | 651 | Most users are expected to find the :meth:`write` or 652 | :meth:`write_array` method more convenient. 653 | 654 | The rows should be given to this method in the order that 655 | they appear in the output file. For straightlaced images, 656 | this is the usual top to bottom ordering, but for interlaced 657 | images the rows should have already been interlaced before 658 | passing them to this function. 659 | 660 | `rows` should be an iterable that yields each row. When 661 | `packed` is ``False`` the rows should be in boxed row flat pixel 662 | format; when `packed` is ``True`` each row should be a packed 663 | sequence of bytes. 664 | """ 665 | 666 | # http://www.w3.org/TR/PNG/#5PNG-file-signature 667 | outfile.write(_signature) 668 | 669 | # http://www.w3.org/TR/PNG/#11IHDR 670 | write_chunk(outfile, 'IHDR', 671 | struct.pack("!2I5B", self.width, self.height, 672 | self.bitdepth, self.color_type, 673 | 0, 0, self.interlace)) 674 | 675 | # See :chunk:order 676 | # http://www.w3.org/TR/PNG/#11gAMA 677 | if self.gamma is not None: 678 | write_chunk(outfile, 'gAMA', 679 | struct.pack("!L", int(round(self.gamma*1e5)))) 680 | 681 | # See :chunk:order 682 | # http://www.w3.org/TR/PNG/#11sBIT 683 | if self.rescale: 684 | write_chunk(outfile, 'sBIT', 685 | struct.pack('%dB' % self.planes, 686 | *[self.rescale[0]]*self.planes)) 687 | 688 | # :chunk:order: Without a palette (PLTE chunk), ordering is 689 | # relatively relaxed. With one, gAMA chunk must precede PLTE 690 | # chunk which must precede tRNS and bKGD. 691 | # See http://www.w3.org/TR/PNG/#5ChunkOrdering 692 | if self.palette: 693 | p,t = self.make_palette() 694 | write_chunk(outfile, 'PLTE', p) 695 | if t: 696 | # tRNS chunk is optional. Only needed if palette entries 697 | # have alpha. 698 | write_chunk(outfile, 'tRNS', t) 699 | 700 | # http://www.w3.org/TR/PNG/#11tRNS 701 | if self.transparent is not None: 702 | if self.greyscale: 703 | write_chunk(outfile, 'tRNS', 704 | struct.pack("!1H", *self.transparent)) 705 | else: 706 | write_chunk(outfile, 'tRNS', 707 | struct.pack("!3H", *self.transparent)) 708 | 709 | # http://www.w3.org/TR/PNG/#11bKGD 710 | if self.background is not None: 711 | if self.greyscale: 712 | write_chunk(outfile, 'bKGD', 713 | struct.pack("!1H", *self.background)) 714 | else: 715 | write_chunk(outfile, 'bKGD', 716 | struct.pack("!3H", *self.background)) 717 | 718 | # http://www.w3.org/TR/PNG/#11IDAT 719 | if self.compression is not None: 720 | compressor = zlib.compressobj(self.compression) 721 | else: 722 | compressor = zlib.compressobj() 723 | 724 | # Choose an extend function based on the bitdepth. The extend 725 | # function packs/decomposes the pixel values into bytes and 726 | # stuffs them onto the data array. 727 | data = array('B') 728 | if self.bitdepth == 8 or packed: 729 | extend = data.extend 730 | elif self.bitdepth == 16: 731 | # Decompose into bytes 732 | def extend(sl): 733 | fmt = '!%dH' % len(sl) 734 | data.extend(array('B', struct.pack(fmt, *sl))) 735 | else: 736 | # Pack into bytes 737 | assert self.bitdepth < 8 738 | # samples per byte 739 | spb = int(8/self.bitdepth) 740 | def extend(sl): 741 | a = array('B', sl) 742 | # Adding padding bytes so we can group into a whole 743 | # number of spb-tuples. 744 | l = float(len(a)) 745 | extra = math.ceil(l / float(spb))*spb - l 746 | a.extend([0]*int(extra)) 747 | # Pack into bytes 748 | l = group(a, spb) 749 | l = map(lambda e: reduce(lambda x,y: 750 | (x << self.bitdepth) + y, e), l) 751 | data.extend(l) 752 | if self.rescale: 753 | oldextend = extend 754 | factor = \ 755 | float(2**self.rescale[1]-1) / float(2**self.rescale[0]-1) 756 | def extend(sl): 757 | oldextend(map(lambda x: int(round(factor*x)), sl)) 758 | 759 | # Build the first row, testing mostly to see if we need to 760 | # changed the extend function to cope with NumPy integer types 761 | # (they cause our ordinary definition of extend to fail, so we 762 | # wrap it). See 763 | # http://code.google.com/p/pypng/issues/detail?id=44 764 | enumrows = enumerate(rows) 765 | del rows 766 | 767 | # First row's filter type. 768 | data.append(0) 769 | # :todo: Certain exceptions in the call to ``.next()`` or the 770 | # following try would indicate no row data supplied. 771 | # Should catch. 772 | i,row = enumrows.next() 773 | try: 774 | # If this fails... 775 | extend(row) 776 | except: 777 | # ... try a version that converts the values to int first. 778 | # Not only does this work for the (slightly broken) NumPy 779 | # types, there are probably lots of other, unknown, "nearly" 780 | # int types it works for. 781 | def wrapmapint(f): 782 | return lambda sl: f(map(int, sl)) 783 | extend = wrapmapint(extend) 784 | del wrapmapint 785 | extend(row) 786 | 787 | for i,row in enumrows: 788 | # Add "None" filter type. Currently, it's essential that 789 | # this filter type be used for every scanline as we do not 790 | # mark the first row of a reduced pass image; that means we 791 | # could accidentally compute the wrong filtered scanline if 792 | # we used "up", "average", or "paeth" on such a line. 793 | data.append(0) 794 | extend(row) 795 | if len(data) > self.chunk_limit: 796 | compressed = compressor.compress(tostring(data)) 797 | if len(compressed): 798 | write_chunk(outfile, 'IDAT', compressed) 799 | # Because of our very witty definition of ``extend``, 800 | # above, we must re-use the same ``data`` object. Hence 801 | # we use ``del`` to empty this one, rather than create a 802 | # fresh one (which would be my natural FP instinct). 803 | del data[:] 804 | if len(data): 805 | compressed = compressor.compress(tostring(data)) 806 | else: 807 | compressed = '' 808 | flushed = compressor.flush() 809 | if len(compressed) or len(flushed): 810 | write_chunk(outfile, 'IDAT', compressed + flushed) 811 | # http://www.w3.org/TR/PNG/#11IEND 812 | write_chunk(outfile, 'IEND') 813 | return i+1 814 | 815 | def write_array(self, outfile, pixels): 816 | """ 817 | Write an array in flat row flat pixel format as a PNG file on 818 | the output file. See also :meth:`write` method. 819 | """ 820 | 821 | if self.interlace: 822 | self.write_passes(outfile, self.array_scanlines_interlace(pixels)) 823 | else: 824 | self.write_passes(outfile, self.array_scanlines(pixels)) 825 | 826 | def write_packed(self, outfile, rows): 827 | """ 828 | Write PNG file to `outfile`. The pixel data comes from `rows` 829 | which should be in boxed row packed format. Each row should be 830 | a sequence of packed bytes. 831 | 832 | Technically, this method does work for interlaced images but it 833 | is best avoided. For interlaced images, the rows should be 834 | presented in the order that they appear in the file. 835 | 836 | This method should not be used when the source image bit depth 837 | is not one naturally supported by PNG; the bit depth should be 838 | 1, 2, 4, 8, or 16. 839 | """ 840 | 841 | if self.rescale: 842 | raise Error("write_packed method not suitable for bit depth %d" % 843 | self.rescale[0]) 844 | return self.write_passes(outfile, rows, packed=True) 845 | 846 | def convert_pnm(self, infile, outfile): 847 | """ 848 | Convert a PNM file containing raw pixel data into a PNG file 849 | with the parameters set in the writer object. Works for 850 | (binary) PGM, PPM, and PAM formats. 851 | """ 852 | 853 | if self.interlace: 854 | pixels = array('B') 855 | pixels.fromfile(infile, 856 | (self.bitdepth/8) * self.color_planes * 857 | self.width * self.height) 858 | self.write_passes(outfile, self.array_scanlines_interlace(pixels)) 859 | else: 860 | self.write_passes(outfile, self.file_scanlines(infile)) 861 | 862 | def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile): 863 | """ 864 | Convert a PPM and PGM file containing raw pixel data into a 865 | PNG outfile with the parameters set in the writer object. 866 | """ 867 | pixels = array('B') 868 | pixels.fromfile(ppmfile, 869 | (self.bitdepth/8) * self.color_planes * 870 | self.width * self.height) 871 | apixels = array('B') 872 | apixels.fromfile(pgmfile, 873 | (self.bitdepth/8) * 874 | self.width * self.height) 875 | pixels = interleave_planes(pixels, apixels, 876 | (self.bitdepth/8) * self.color_planes, 877 | (self.bitdepth/8)) 878 | if self.interlace: 879 | self.write_passes(outfile, self.array_scanlines_interlace(pixels)) 880 | else: 881 | self.write_passes(outfile, self.array_scanlines(pixels)) 882 | 883 | def file_scanlines(self, infile): 884 | """ 885 | Generates boxed rows in flat pixel format, from the input file 886 | `infile`. It assumes that the input file is in a "Netpbm-like" 887 | binary format, and is positioned at the beginning of the first 888 | pixel. The number of pixels to read is taken from the image 889 | dimensions (`width`, `height`, `planes`) and the number of bytes 890 | per value is implied by the image `bitdepth`. 891 | """ 892 | 893 | # Values per row 894 | vpr = self.width * self.planes 895 | row_bytes = vpr 896 | if self.bitdepth > 8: 897 | assert self.bitdepth == 16 898 | row_bytes *= 2 899 | fmt = '>%dH' % vpr 900 | def line(): 901 | return array('H', struct.unpack(fmt, infile.read(row_bytes))) 902 | else: 903 | def line(): 904 | scanline = array('B', infile.read(row_bytes)) 905 | return scanline 906 | for y in range(self.height): 907 | yield line() 908 | 909 | def array_scanlines(self, pixels): 910 | """ 911 | Generates boxed rows (flat pixels) from flat rows (flat pixels) 912 | in an array. 913 | """ 914 | 915 | # Values per row 916 | vpr = self.width * self.planes 917 | stop = 0 918 | for y in range(self.height): 919 | start = stop 920 | stop = start + vpr 921 | yield pixels[start:stop] 922 | 923 | def array_scanlines_interlace(self, pixels): 924 | """ 925 | Generator for interlaced scanlines from an array. `pixels` is 926 | the full source image in flat row flat pixel format. The 927 | generator yields each scanline of the reduced passes in turn, in 928 | boxed row flat pixel format. 929 | """ 930 | 931 | # http://www.w3.org/TR/PNG/#8InterlaceMethods 932 | # Array type. 933 | fmt = 'BH'[self.bitdepth > 8] 934 | # Value per row 935 | vpr = self.width * self.planes 936 | for xstart, ystart, xstep, ystep in _adam7: 937 | if xstart >= self.width: 938 | continue 939 | # Pixels per row (of reduced image) 940 | ppr = int(math.ceil((self.width-xstart)/float(xstep))) 941 | # number of values in reduced image row. 942 | row_len = ppr*self.planes 943 | for y in range(ystart, self.height, ystep): 944 | if xstep == 1: 945 | offset = y * vpr 946 | yield pixels[offset:offset+vpr] 947 | else: 948 | row = array(fmt) 949 | # There's no easier way to set the length of an array 950 | row.extend(pixels[0:row_len]) 951 | offset = y * vpr + xstart * self.planes 952 | end_offset = (y+1) * vpr 953 | skip = self.planes * xstep 954 | for i in range(self.planes): 955 | row[i::self.planes] = \ 956 | pixels[offset+i:end_offset:skip] 957 | yield row 958 | 959 | def write_chunk(outfile, tag, data=strtobytes('')): 960 | """ 961 | Write a PNG chunk to the output file, including length and 962 | checksum. 963 | """ 964 | 965 | # http://www.w3.org/TR/PNG/#5Chunk-layout 966 | outfile.write(struct.pack("!I", len(data))) 967 | tag = strtobytes(tag) 968 | outfile.write(tag) 969 | outfile.write(data) 970 | checksum = zlib.crc32(tag) 971 | checksum = zlib.crc32(data, checksum) 972 | checksum &= 2**32-1 973 | outfile.write(struct.pack("!I", checksum)) 974 | 975 | def write_chunks(out, chunks): 976 | """Create a PNG file by writing out the chunks.""" 977 | 978 | out.write(_signature) 979 | for chunk in chunks: 980 | write_chunk(out, *chunk) 981 | 982 | def filter_scanline(type, line, fo, prev=None): 983 | """Apply a scanline filter to a scanline. `type` specifies the 984 | filter type (0 to 4); `line` specifies the current (unfiltered) 985 | scanline as a sequence of bytes; `prev` specifies the previous 986 | (unfiltered) scanline as a sequence of bytes. `fo` specifies the 987 | filter offset; normally this is size of a pixel in bytes (the number 988 | of bytes per sample times the number of channels), but when this is 989 | < 1 (for bit depths < 8) then the filter offset is 1. 990 | """ 991 | 992 | assert 0 <= type < 5 993 | 994 | # The output array. Which, pathetically, we extend one-byte at a 995 | # time (fortunately this is linear). 996 | out = array('B', [type]) 997 | 998 | def sub(): 999 | ai = -fo 1000 | for x in line: 1001 | if ai >= 0: 1002 | x = (x - line[ai]) & 0xff 1003 | out.append(x) 1004 | ai += 1 1005 | def up(): 1006 | for i,x in enumerate(line): 1007 | x = (x - prev[i]) & 0xff 1008 | out.append(x) 1009 | def average(): 1010 | ai = -fo 1011 | for i,x in enumerate(line): 1012 | if ai >= 0: 1013 | x = (x - ((line[ai] + prev[i]) >> 1)) & 0xff 1014 | else: 1015 | x = (x - (prev[i] >> 1)) & 0xff 1016 | out.append(x) 1017 | ai += 1 1018 | def paeth(): 1019 | # http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth 1020 | ai = -fo # also used for ci 1021 | for i,x in enumerate(line): 1022 | a = 0 1023 | b = prev[i] 1024 | c = 0 1025 | 1026 | if ai >= 0: 1027 | a = line[ai] 1028 | c = prev[ai] 1029 | p = a + b - c 1030 | pa = abs(p - a) 1031 | pb = abs(p - b) 1032 | pc = abs(p - c) 1033 | if pa <= pb and pa <= pc: Pr = a 1034 | elif pb <= pc: Pr = b 1035 | else: Pr = c 1036 | 1037 | x = (x - Pr) & 0xff 1038 | out.append(x) 1039 | ai += 1 1040 | 1041 | if not prev: 1042 | # We're on the first line. Some of the filters can be reduced 1043 | # to simpler cases which makes handling the line "off the top" 1044 | # of the image simpler. "up" becomes "none"; "paeth" becomes 1045 | # "left" (non-trivial, but true). "average" needs to be handled 1046 | # specially. 1047 | if type == 2: # "up" 1048 | type = 0 1049 | elif type == 3: 1050 | prev = [0]*len(line) 1051 | elif type == 4: # "paeth" 1052 | type = 1 1053 | if type == 0: 1054 | out.extend(line) 1055 | elif type == 1: 1056 | sub() 1057 | elif type == 2: 1058 | up() 1059 | elif type == 3: 1060 | average() 1061 | else: # type == 4 1062 | paeth() 1063 | return out 1064 | 1065 | 1066 | def from_array(a, mode=None, info={}): 1067 | """Create a PNG :class:`Image` object from a 2- or 3-dimensional 1068 | array. One application of this function is easy PIL-style saving: 1069 | ``png.from_array(pixels, 'L').save('foo.png')``. 1070 | 1071 | .. note : 1072 | 1073 | The use of the term *3-dimensional* is for marketing purposes 1074 | only. It doesn't actually work. Please bear with us. Meanwhile 1075 | enjoy the complimentary snacks (on request) and please use a 1076 | 2-dimensional array. 1077 | 1078 | Unless they are specified using the *info* parameter, the PNG's 1079 | height and width are taken from the array size. For a 3 dimensional 1080 | array the first axis is the height; the second axis is the width; 1081 | and the third axis is the channel number. Thus an RGB image that is 1082 | 16 pixels high and 8 wide will use an array that is 16x8x3. For 2 1083 | dimensional arrays the first axis is the height, but the second axis 1084 | is ``width*channels``, so an RGB image that is 16 pixels high and 8 1085 | wide will use a 2-dimensional array that is 16x24 (each row will be 1086 | 8*3==24 sample values). 1087 | 1088 | *mode* is a string that specifies the image colour format in a 1089 | PIL-style mode. It can be: 1090 | 1091 | ``'L'`` 1092 | greyscale (1 channel) 1093 | ``'LA'`` 1094 | greyscale with alpha (2 channel) 1095 | ``'RGB'`` 1096 | colour image (3 channel) 1097 | ``'RGBA'`` 1098 | colour image with alpha (4 channel) 1099 | 1100 | The mode string can also specify the bit depth (overriding how this 1101 | function normally derives the bit depth, see below). Appending 1102 | ``';16'`` to the mode will cause the PNG to be 16 bits per channel; 1103 | any decimal from 1 to 16 can be used to specify the bit depth. 1104 | 1105 | When a 2-dimensional array is used *mode* determines how many 1106 | channels the image has, and so allows the width to be derived from 1107 | the second array dimension. 1108 | 1109 | The array is expected to be a ``numpy`` array, but it can be any 1110 | suitable Python sequence. For example, a list of lists can be used: 1111 | ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``. The exact 1112 | rules are: ``len(a)`` gives the first dimension, height; 1113 | ``len(a[0])`` gives the second dimension; ``len(a[0][0])`` gives the 1114 | third dimension, unless an exception is raised in which case a 1115 | 2-dimensional array is assumed. It's slightly more complicated than 1116 | that because an iterator of rows can be used, and it all still 1117 | works. Using an iterator allows data to be streamed efficiently. 1118 | 1119 | The bit depth of the PNG is normally taken from the array element's 1120 | datatype (but if *mode* specifies a bitdepth then that is used 1121 | instead). The array element's datatype is determined in a way which 1122 | is supposed to work both for ``numpy`` arrays and for Python 1123 | ``array.array`` objects. A 1 byte datatype will give a bit depth of 1124 | 8, a 2 byte datatype will give a bit depth of 16. If the datatype 1125 | does not have an implicit size, for example it is a plain Python 1126 | list of lists, as above, then a default of 8 is used. 1127 | 1128 | The *info* parameter is a dictionary that can be used to specify 1129 | metadata (in the same style as the arguments to the 1130 | :class:``png.Writer`` class). For this function the keys that are 1131 | useful are: 1132 | 1133 | height 1134 | overrides the height derived from the array dimensions and allows 1135 | *a* to be an iterable. 1136 | width 1137 | overrides the width derived from the array dimensions. 1138 | bitdepth 1139 | overrides the bit depth derived from the element datatype (but 1140 | must match *mode* if that also specifies a bit depth). 1141 | 1142 | Generally anything specified in the 1143 | *info* dictionary will override any implicit choices that this 1144 | function would otherwise make, but must match any explicit ones. 1145 | For example, if the *info* dictionary has a ``greyscale`` key then 1146 | this must be true when mode is ``'L'`` or ``'LA'`` and false when 1147 | mode is ``'RGB'`` or ``'RGBA'``. 1148 | """ 1149 | 1150 | # We abuse the *info* parameter by modifying it. Take a copy here. 1151 | # (Also typechecks *info* to some extent). 1152 | info = dict(info) 1153 | 1154 | # Syntax check mode string. 1155 | bitdepth = None 1156 | try: 1157 | # Assign the 'L' or 'RGBA' part to `gotmode`. 1158 | if mode.startswith('L'): 1159 | gotmode = 'L' 1160 | mode = mode[1:] 1161 | elif mode.startswith('RGB'): 1162 | gotmode = 'RGB' 1163 | mode = mode[3:] 1164 | else: 1165 | raise Error() 1166 | if mode.startswith('A'): 1167 | gotmode += 'A' 1168 | mode = mode[1:] 1169 | 1170 | # Skip any optional ';' 1171 | while mode.startswith(';'): 1172 | mode = mode[1:] 1173 | 1174 | # Parse optional bitdepth 1175 | if mode: 1176 | try: 1177 | bitdepth = int(mode) 1178 | except (TypeError, ValueError): 1179 | raise Error() 1180 | except Error: 1181 | raise Error("mode string should be 'RGB' or 'L;16' or similar.") 1182 | mode = gotmode 1183 | 1184 | # Get bitdepth from *mode* if possible. 1185 | if bitdepth: 1186 | if info.get('bitdepth') and bitdepth != info['bitdepth']: 1187 | raise Error("mode bitdepth (%d) should match info bitdepth (%d)." % 1188 | (bitdepth, info['bitdepth'])) 1189 | info['bitdepth'] = bitdepth 1190 | 1191 | # Fill in and/or check entries in *info*. 1192 | # Dimensions. 1193 | if 'size' in info: 1194 | # Check width, height, size all match where used. 1195 | for dimension,axis in [('width', 0), ('height', 1)]: 1196 | if dimension in info: 1197 | if info[dimension] != info['size'][axis]: 1198 | raise Error( 1199 | "info[%r] should match info['size'][%r]." % 1200 | (dimension, axis)) 1201 | info['width'],info['height'] = info['size'] 1202 | if 'height' not in info: 1203 | try: 1204 | l = len(a) 1205 | except TypeError: 1206 | raise Error( 1207 | "len(a) does not work, supply info['height'] instead.") 1208 | info['height'] = l 1209 | # Colour format. 1210 | if 'greyscale' in info: 1211 | if bool(info['greyscale']) != ('L' in mode): 1212 | raise Error("info['greyscale'] should match mode.") 1213 | info['greyscale'] = 'L' in mode 1214 | if 'alpha' in info: 1215 | if bool(info['alpha']) != ('A' in mode): 1216 | raise Error("info['alpha'] should match mode.") 1217 | info['alpha'] = 'A' in mode 1218 | 1219 | planes = len(mode) 1220 | if 'planes' in info: 1221 | if info['planes'] != planes: 1222 | raise Error("info['planes'] should match mode.") 1223 | 1224 | # In order to work out whether we the array is 2D or 3D we need its 1225 | # first row, which requires that we take a copy of its iterator. 1226 | # We may also need the first row to derive width and bitdepth. 1227 | a,t = itertools.tee(a) 1228 | row = t.next() 1229 | del t 1230 | try: 1231 | row[0][0] 1232 | threed = True 1233 | testelement = row[0] 1234 | except (IndexError, TypeError): 1235 | threed = False 1236 | testelement = row 1237 | if 'width' not in info: 1238 | if threed: 1239 | width = len(row) 1240 | else: 1241 | width = len(row) // planes 1242 | info['width'] = width 1243 | 1244 | # Not implemented yet 1245 | assert not threed 1246 | 1247 | if 'bitdepth' not in info: 1248 | try: 1249 | dtype = testelement.dtype 1250 | # goto the "else:" clause. Sorry. 1251 | except AttributeError: 1252 | try: 1253 | # Try a Python array.array. 1254 | bitdepth = 8 * testelement.itemsize 1255 | except AttributeError: 1256 | # We can't determine it from the array element's 1257 | # datatype, use a default of 8. 1258 | bitdepth = 8 1259 | else: 1260 | # If we got here without exception, we now assume that 1261 | # the array is a numpy array. 1262 | if dtype.kind == 'b': 1263 | bitdepth = 1 1264 | else: 1265 | bitdepth = 8 * dtype.itemsize 1266 | info['bitdepth'] = bitdepth 1267 | 1268 | for thing in 'width height bitdepth greyscale alpha'.split(): 1269 | assert thing in info 1270 | return Image(a, info) 1271 | 1272 | # So that refugee's from PIL feel more at home. Not documented. 1273 | fromarray = from_array 1274 | 1275 | class Image: 1276 | """A PNG image. You can create an :class:`Image` object from 1277 | an array of pixels by calling :meth:`png.from_array`. It can be 1278 | saved to disk with the :meth:`save` method. 1279 | """ 1280 | 1281 | def __init__(self, rows, info): 1282 | """ 1283 | .. note :: 1284 | 1285 | The constructor is not public. Please do not call it. 1286 | """ 1287 | 1288 | self.rows = rows 1289 | self.info = info 1290 | 1291 | def save(self, file): 1292 | """Save the image to *file*. If *file* looks like an open file 1293 | descriptor then it is used, otherwise it is treated as a 1294 | filename and a fresh file is opened. 1295 | 1296 | In general, you can only call this method once; after it has 1297 | been called the first time and the PNG image has been saved, the 1298 | source data will have been streamed, and cannot be streamed 1299 | again. 1300 | """ 1301 | 1302 | w = Writer(**self.info) 1303 | 1304 | try: 1305 | file.write 1306 | def close(): pass 1307 | except AttributeError: 1308 | file = open(file, 'wb') 1309 | def close(): file.close() 1310 | 1311 | try: 1312 | w.write(file, self.rows) 1313 | finally: 1314 | close() 1315 | 1316 | class _readable: 1317 | """ 1318 | A simple file-like interface for strings and arrays. 1319 | """ 1320 | 1321 | def __init__(self, buf): 1322 | self.buf = buf 1323 | self.offset = 0 1324 | 1325 | def read(self, n): 1326 | r = self.buf[self.offset:self.offset+n] 1327 | if isarray(r): 1328 | r = r.tostring() 1329 | self.offset += n 1330 | return r 1331 | 1332 | 1333 | class Reader: 1334 | """ 1335 | PNG decoder in pure Python. 1336 | """ 1337 | 1338 | def __init__(self, _guess=None, **kw): 1339 | """ 1340 | Create a PNG decoder object. 1341 | 1342 | The constructor expects exactly one keyword argument. If you 1343 | supply a positional argument instead, it will guess the input 1344 | type. You can choose among the following keyword arguments: 1345 | 1346 | filename 1347 | Name of input file (a PNG file). 1348 | file 1349 | A file-like object (object with a read() method). 1350 | bytes 1351 | ``array`` or ``string`` with PNG data. 1352 | 1353 | """ 1354 | if ((_guess is not None and len(kw) != 0) or 1355 | (_guess is None and len(kw) != 1)): 1356 | raise TypeError("Reader() takes exactly 1 argument") 1357 | 1358 | # Will be the first 8 bytes, later on. See validate_signature. 1359 | self.signature = None 1360 | self.transparent = None 1361 | # A pair of (len,type) if a chunk has been read but its data and 1362 | # checksum have not (in other words the file position is just 1363 | # past the 4 bytes that specify the chunk type). See preamble 1364 | # method for how this is used. 1365 | self.atchunk = None 1366 | 1367 | if _guess is not None: 1368 | if isarray(_guess): 1369 | kw["bytes"] = _guess 1370 | elif isinstance(_guess, str): 1371 | kw["filename"] = _guess 1372 | elif hasattr(_guess, 'read'): 1373 | kw["file"] = _guess 1374 | 1375 | if "filename" in kw: 1376 | self.file = open(kw["filename"], "rb") 1377 | elif "file" in kw: 1378 | self.file = kw["file"] 1379 | elif "bytes" in kw: 1380 | self.file = _readable(kw["bytes"]) 1381 | else: 1382 | raise TypeError("expecting filename, file or bytes array") 1383 | 1384 | 1385 | def chunk(self, seek=None, lenient=False): 1386 | """ 1387 | Read the next PNG chunk from the input file; returns a 1388 | (*type*,*data*) tuple. *type* is the chunk's type as a string 1389 | (all PNG chunk types are 4 characters long). *data* is the 1390 | chunk's data content, as a string. 1391 | 1392 | If the optional `seek` argument is 1393 | specified then it will keep reading chunks until it either runs 1394 | out of file or finds the type specified by the argument. Note 1395 | that in general the order of chunks in PNGs is unspecified, so 1396 | using `seek` can cause you to miss chunks. 1397 | 1398 | If the optional `lenient` argument evaluates to True, 1399 | checksum failures will raise warnings rather than exceptions. 1400 | """ 1401 | 1402 | self.validate_signature() 1403 | 1404 | while True: 1405 | # http://www.w3.org/TR/PNG/#5Chunk-layout 1406 | if not self.atchunk: 1407 | self.atchunk = self.chunklentype() 1408 | length,type = self.atchunk 1409 | self.atchunk = None 1410 | data = self.file.read(length) 1411 | if len(data) != length: 1412 | raise ChunkError('Chunk %s too short for required %i octets.' 1413 | % (type, length)) 1414 | checksum = self.file.read(4) 1415 | if len(checksum) != 4: 1416 | raise ValueError('Chunk %s too short for checksum.', tag) 1417 | if seek and type != seek: 1418 | continue 1419 | verify = zlib.crc32(strtobytes(type)) 1420 | verify = zlib.crc32(data, verify) 1421 | # Whether the output from zlib.crc32 is signed or not varies 1422 | # according to hideous implementation details, see 1423 | # http://bugs.python.org/issue1202 . 1424 | # We coerce it to be positive here (in a way which works on 1425 | # Python 2.3 and older). 1426 | verify &= 2**32 - 1 1427 | verify = struct.pack('!I', verify) 1428 | if checksum != verify: 1429 | (a, ) = struct.unpack('!I', checksum) 1430 | (b, ) = struct.unpack('!I', verify) 1431 | message = "Checksum error in %s chunk: 0x%08X != 0x%08X." % (type, a, b) 1432 | if lenient: 1433 | warnings.warn(message, RuntimeWarning) 1434 | else: 1435 | raise ChunkError(message) 1436 | return type, data 1437 | 1438 | def chunks(self): 1439 | """Return an iterator that will yield each chunk as a 1440 | (*chunktype*, *content*) pair. 1441 | """ 1442 | 1443 | while True: 1444 | t,v = self.chunk() 1445 | yield t,v 1446 | if t == 'IEND': 1447 | break 1448 | 1449 | def undo_filter(self, filter_type, scanline, previous): 1450 | """Undo the filter for a scanline. `scanline` is a sequence of 1451 | bytes that does not include the initial filter type byte. 1452 | `previous` is decoded previous scanline (for straightlaced 1453 | images this is the previous pixel row, but for interlaced 1454 | images, it is the previous scanline in the reduced image, which 1455 | in general is not the previous pixel row in the final image). 1456 | When there is no previous scanline (the first row of a 1457 | straightlaced image, or the first row in one of the passes in an 1458 | interlaced image), then this argument should be ``None``. 1459 | 1460 | The scanline will have the effects of filtering removed, and the 1461 | result will be returned as a fresh sequence of bytes. 1462 | """ 1463 | 1464 | # :todo: Would it be better to update scanline in place? 1465 | # Yes, with the Cython extension making the undo_filter fast, 1466 | # updating scanline inplace makes the code 3 times faster 1467 | # (reading 50 images of 800x800 went from 40s to 16s) 1468 | result = scanline 1469 | 1470 | if filter_type == 0: 1471 | return result 1472 | 1473 | if filter_type not in (1,2,3,4): 1474 | raise FormatError('Invalid PNG Filter Type.' 1475 | ' See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .') 1476 | 1477 | # Filter unit. The stride from one pixel to the corresponding 1478 | # byte from the previous pixel. Normally this is the pixel 1479 | # size in bytes, but when this is smaller than 1, the previous 1480 | # byte is used instead. 1481 | fu = max(1, self.psize) 1482 | 1483 | # For the first line of a pass, synthesize a dummy previous 1484 | # line. An alternative approach would be to observe that on the 1485 | # first line 'up' is the same as 'null', 'paeth' is the same 1486 | # as 'sub', with only 'average' requiring any special case. 1487 | if not previous: 1488 | previous = array('B', [0]*len(scanline)) 1489 | 1490 | def sub(): 1491 | """Undo sub filter.""" 1492 | 1493 | ai = 0 1494 | # Loop starts at index fu. Observe that the initial part 1495 | # of the result is already filled in correctly with 1496 | # scanline. 1497 | for i in range(fu, len(result)): 1498 | x = scanline[i] 1499 | a = result[ai] 1500 | result[i] = (x + a) & 0xff 1501 | ai += 1 1502 | 1503 | def up(): 1504 | """Undo up filter.""" 1505 | 1506 | for i in range(len(result)): 1507 | x = scanline[i] 1508 | b = previous[i] 1509 | result[i] = (x + b) & 0xff 1510 | 1511 | def average(): 1512 | """Undo average filter.""" 1513 | 1514 | ai = -fu 1515 | for i in range(len(result)): 1516 | x = scanline[i] 1517 | if ai < 0: 1518 | a = 0 1519 | else: 1520 | a = result[ai] 1521 | b = previous[i] 1522 | result[i] = (x + ((a + b) >> 1)) & 0xff 1523 | ai += 1 1524 | 1525 | def paeth(): 1526 | """Undo Paeth filter.""" 1527 | 1528 | # Also used for ci. 1529 | ai = -fu 1530 | for i in range(len(result)): 1531 | x = scanline[i] 1532 | if ai < 0: 1533 | a = c = 0 1534 | else: 1535 | a = result[ai] 1536 | c = previous[ai] 1537 | b = previous[i] 1538 | p = a + b - c 1539 | pa = abs(p - a) 1540 | pb = abs(p - b) 1541 | pc = abs(p - c) 1542 | if pa <= pb and pa <= pc: 1543 | pr = a 1544 | elif pb <= pc: 1545 | pr = b 1546 | else: 1547 | pr = c 1548 | result[i] = (x + pr) & 0xff 1549 | ai += 1 1550 | 1551 | # Call appropriate filter algorithm. Note that 0 has already 1552 | # been dealt with. 1553 | (None, 1554 | pngfilters.undo_filter_sub, 1555 | pngfilters.undo_filter_up, 1556 | pngfilters.undo_filter_average, 1557 | pngfilters.undo_filter_paeth)[filter_type](fu, scanline, previous, result) 1558 | return result 1559 | 1560 | def deinterlace(self, raw): 1561 | """ 1562 | Read raw pixel data, undo filters, deinterlace, and flatten. 1563 | Return in flat row flat pixel format. 1564 | """ 1565 | 1566 | # Values per row (of the target image) 1567 | vpr = self.width * self.planes 1568 | 1569 | # Make a result array, and make it big enough. Interleaving 1570 | # writes to the output array randomly (well, not quite), so the 1571 | # entire output array must be in memory. 1572 | fmt = 'BH'[self.bitdepth > 8] 1573 | a = array(fmt, [0]*vpr*self.height) 1574 | source_offset = 0 1575 | 1576 | for xstart, ystart, xstep, ystep in _adam7: 1577 | if xstart >= self.width: 1578 | continue 1579 | # The previous (reconstructed) scanline. None at the 1580 | # beginning of a pass to indicate that there is no previous 1581 | # line. 1582 | recon = None 1583 | # Pixels per row (reduced pass image) 1584 | ppr = int(math.ceil((self.width-xstart)/float(xstep))) 1585 | # Row size in bytes for this pass. 1586 | row_size = int(math.ceil(self.psize * ppr)) 1587 | for y in range(ystart, self.height, ystep): 1588 | filter_type = raw[source_offset] 1589 | source_offset += 1 1590 | scanline = raw[source_offset:source_offset+row_size] 1591 | source_offset += row_size 1592 | recon = self.undo_filter(filter_type, scanline, recon) 1593 | # Convert so that there is one element per pixel value 1594 | flat = self.serialtoflat(recon, ppr) 1595 | if xstep == 1: 1596 | assert xstart == 0 1597 | offset = y * vpr 1598 | a[offset:offset+vpr] = flat 1599 | else: 1600 | offset = y * vpr + xstart * self.planes 1601 | end_offset = (y+1) * vpr 1602 | skip = self.planes * xstep 1603 | for i in range(self.planes): 1604 | a[offset+i:end_offset:skip] = \ 1605 | flat[i::self.planes] 1606 | return a 1607 | 1608 | def iterboxed(self, rows): 1609 | """Iterator that yields each scanline in boxed row flat pixel 1610 | format. `rows` should be an iterator that yields the bytes of 1611 | each row in turn. 1612 | """ 1613 | 1614 | def asvalues(raw): 1615 | """Convert a row of raw bytes into a flat row. Result may 1616 | or may not share with argument""" 1617 | 1618 | if self.bitdepth == 8: 1619 | return raw 1620 | if self.bitdepth == 16: 1621 | raw = tostring(raw) 1622 | return array('H', struct.unpack('!%dH' % (len(raw)//2), raw)) 1623 | assert self.bitdepth < 8 1624 | width = self.width 1625 | # Samples per byte 1626 | spb = 8//self.bitdepth 1627 | out = array('B') 1628 | mask = 2**self.bitdepth - 1 1629 | shifts = map(self.bitdepth.__mul__, reversed(range(spb))) 1630 | for o in raw: 1631 | out.extend(map(lambda i: mask&(o>>i), shifts)) 1632 | return out[:width] 1633 | 1634 | return itertools.imap(asvalues, rows) 1635 | 1636 | def serialtoflat(self, bytes, width=None): 1637 | """Convert serial format (byte stream) pixel data to flat row 1638 | flat pixel. 1639 | """ 1640 | 1641 | if self.bitdepth == 8: 1642 | return bytes 1643 | if self.bitdepth == 16: 1644 | bytes = tostring(bytes) 1645 | return array('H', 1646 | struct.unpack('!%dH' % (len(bytes)//2), bytes)) 1647 | assert self.bitdepth < 8 1648 | if width is None: 1649 | width = self.width 1650 | # Samples per byte 1651 | spb = 8//self.bitdepth 1652 | out = array('B') 1653 | mask = 2**self.bitdepth - 1 1654 | shifts = map(self.bitdepth.__mul__, reversed(range(spb))) 1655 | l = width 1656 | for o in bytes: 1657 | out.extend([(mask&(o>>s)) for s in shifts][:l]) 1658 | l -= spb 1659 | if l <= 0: 1660 | l = width 1661 | return out 1662 | 1663 | def iterstraight(self, raw): 1664 | """Iterator that undoes the effect of filtering, and yields 1665 | each row in serialised format (as a sequence of bytes). 1666 | Assumes input is straightlaced. `raw` should be an iterable 1667 | that yields the raw bytes in chunks of arbitrary size. 1668 | """ 1669 | 1670 | # length of row, in bytes 1671 | rb = self.row_bytes 1672 | a = array('B') 1673 | # The previous (reconstructed) scanline. None indicates first 1674 | # line of image. 1675 | recon = None 1676 | for some in raw: 1677 | a.extend(some) 1678 | while len(a) >= rb + 1: 1679 | filter_type = a[0] 1680 | scanline = a[1:rb+1] 1681 | del a[:rb+1] 1682 | recon = self.undo_filter(filter_type, scanline, recon) 1683 | yield recon 1684 | if len(a) != 0: 1685 | # :file:format We get here with a file format error: 1686 | # when the available bytes (after decompressing) do not 1687 | # pack into exact rows. 1688 | raise FormatError( 1689 | 'Wrong size for decompressed IDAT chunk.') 1690 | assert len(a) == 0 1691 | 1692 | def validate_signature(self): 1693 | """If signature (header) has not been read then read and 1694 | validate it; otherwise do nothing. 1695 | """ 1696 | 1697 | if self.signature: 1698 | return 1699 | self.signature = self.file.read(8) 1700 | if self.signature != _signature: 1701 | raise FormatError("PNG file has invalid signature.") 1702 | 1703 | def preamble(self, lenient=False): 1704 | """ 1705 | Extract the image metadata by reading the initial part of 1706 | the PNG file up to the start of the ``IDAT`` chunk. All the 1707 | chunks that precede the ``IDAT`` chunk are read and either 1708 | processed for metadata or discarded. 1709 | 1710 | If the optional `lenient` argument evaluates to True, checksum 1711 | failures will raise warnings rather than exceptions. 1712 | """ 1713 | 1714 | self.validate_signature() 1715 | 1716 | while True: 1717 | if not self.atchunk: 1718 | self.atchunk = self.chunklentype() 1719 | if self.atchunk is None: 1720 | raise FormatError( 1721 | 'This PNG file has no IDAT chunks.') 1722 | if self.atchunk[1] == 'IDAT': 1723 | return 1724 | self.process_chunk(lenient=lenient) 1725 | 1726 | def chunklentype(self): 1727 | """Reads just enough of the input to determine the next 1728 | chunk's length and type, returned as a (*length*, *type*) pair 1729 | where *type* is a string. If there are no more chunks, ``None`` 1730 | is returned. 1731 | """ 1732 | 1733 | x = self.file.read(8) 1734 | if not x: 1735 | return None 1736 | if len(x) != 8: 1737 | raise FormatError( 1738 | 'End of file whilst reading chunk length and type.') 1739 | length,type = struct.unpack('!I4s', x) 1740 | type = bytestostr(type) 1741 | if length > 2**31-1: 1742 | raise FormatError('Chunk %s is too large: %d.' % (type,length)) 1743 | return length,type 1744 | 1745 | def process_chunk(self, lenient=False): 1746 | """Process the next chunk and its data. This only processes the 1747 | following chunk types, all others are ignored: ``IHDR``, 1748 | ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``. 1749 | 1750 | If the optional `lenient` argument evaluates to True, 1751 | checksum failures will raise warnings rather than exceptions. 1752 | """ 1753 | 1754 | type, data = self.chunk(lenient=lenient) 1755 | method = '_process_' + type 1756 | m = getattr(self, method, None) 1757 | if m: 1758 | m(data) 1759 | 1760 | def _process_IHDR(self, data): 1761 | # http://www.w3.org/TR/PNG/#11IHDR 1762 | if len(data) != 13: 1763 | raise FormatError('IHDR chunk has incorrect length.') 1764 | (self.width, self.height, self.bitdepth, self.color_type, 1765 | self.compression, self.filter, 1766 | self.interlace) = struct.unpack("!2I5B", data) 1767 | 1768 | check_bitdepth_colortype(self.bitdepth, self.color_type) 1769 | 1770 | if self.compression != 0: 1771 | raise Error("unknown compression method %d" % self.compression) 1772 | if self.filter != 0: 1773 | raise FormatError("Unknown filter method %d," 1774 | " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ." 1775 | % self.filter) 1776 | if self.interlace not in (0,1): 1777 | raise FormatError("Unknown interlace method %d," 1778 | " see http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods ." 1779 | % self.interlace) 1780 | 1781 | # Derived values 1782 | # http://www.w3.org/TR/PNG/#6Colour-values 1783 | colormap = bool(self.color_type & 1) 1784 | greyscale = not (self.color_type & 2) 1785 | alpha = bool(self.color_type & 4) 1786 | color_planes = (3,1)[greyscale or colormap] 1787 | planes = color_planes + alpha 1788 | 1789 | self.colormap = colormap 1790 | self.greyscale = greyscale 1791 | self.alpha = alpha 1792 | self.color_planes = color_planes 1793 | self.planes = planes 1794 | self.psize = float(self.bitdepth)/float(8) * planes 1795 | if int(self.psize) == self.psize: 1796 | self.psize = int(self.psize) 1797 | self.row_bytes = int(math.ceil(self.width * self.psize)) 1798 | # Stores PLTE chunk if present, and is used to check 1799 | # chunk ordering constraints. 1800 | self.plte = None 1801 | # Stores tRNS chunk if present, and is used to check chunk 1802 | # ordering constraints. 1803 | self.trns = None 1804 | # Stores sbit chunk if present. 1805 | self.sbit = None 1806 | 1807 | def _process_PLTE(self, data): 1808 | # http://www.w3.org/TR/PNG/#11PLTE 1809 | if self.plte: 1810 | warnings.warn("Multiple PLTE chunks present.") 1811 | self.plte = data 1812 | if len(data) % 3 != 0: 1813 | raise FormatError( 1814 | "PLTE chunk's length should be a multiple of 3.") 1815 | if len(data) > (2**self.bitdepth)*3: 1816 | raise FormatError("PLTE chunk is too long.") 1817 | if len(data) == 0: 1818 | raise FormatError("Empty PLTE is not allowed.") 1819 | 1820 | def _process_bKGD(self, data): 1821 | try: 1822 | if self.colormap: 1823 | if not self.plte: 1824 | warnings.warn( 1825 | "PLTE chunk is required before bKGD chunk.") 1826 | self.background = struct.unpack('B', data) 1827 | else: 1828 | self.background = struct.unpack("!%dH" % self.color_planes, 1829 | data) 1830 | except struct.error: 1831 | raise FormatError("bKGD chunk has incorrect length.") 1832 | 1833 | def _process_tRNS(self, data): 1834 | # http://www.w3.org/TR/PNG/#11tRNS 1835 | self.trns = data 1836 | if self.colormap: 1837 | if not self.plte: 1838 | warnings.warn("PLTE chunk is required before tRNS chunk.") 1839 | else: 1840 | if len(data) > len(self.plte)/3: 1841 | # Was warning, but promoted to Error as it 1842 | # would otherwise cause pain later on. 1843 | raise FormatError("tRNS chunk is too long.") 1844 | else: 1845 | if self.alpha: 1846 | raise FormatError( 1847 | "tRNS chunk is not valid with colour type %d." % 1848 | self.color_type) 1849 | try: 1850 | self.transparent = \ 1851 | struct.unpack("!%dH" % self.color_planes, data) 1852 | except struct.error: 1853 | raise FormatError("tRNS chunk has incorrect length.") 1854 | 1855 | def _process_gAMA(self, data): 1856 | try: 1857 | self.gamma = struct.unpack("!L", data)[0] / 100000.0 1858 | except struct.error: 1859 | raise FormatError("gAMA chunk has incorrect length.") 1860 | 1861 | def _process_sBIT(self, data): 1862 | self.sbit = data 1863 | if (self.colormap and len(data) != 3 or 1864 | not self.colormap and len(data) != self.planes): 1865 | raise FormatError("sBIT chunk has incorrect length.") 1866 | 1867 | def read(self, lenient=False): 1868 | """ 1869 | Read the PNG file and decode it. Returns (`width`, `height`, 1870 | `pixels`, `metadata`). 1871 | 1872 | May use excessive memory. 1873 | 1874 | `pixels` are returned in boxed row flat pixel format. 1875 | 1876 | If the optional `lenient` argument evaluates to True, 1877 | checksum failures will raise warnings rather than exceptions. 1878 | """ 1879 | 1880 | def iteridat(): 1881 | """Iterator that yields all the ``IDAT`` chunks as strings.""" 1882 | while True: 1883 | try: 1884 | type, data = self.chunk(lenient=lenient) 1885 | except ValueError, e: 1886 | raise ChunkError(e.args[0]) 1887 | if type == 'IEND': 1888 | # http://www.w3.org/TR/PNG/#11IEND 1889 | break 1890 | if type != 'IDAT': 1891 | continue 1892 | # type == 'IDAT' 1893 | # http://www.w3.org/TR/PNG/#11IDAT 1894 | if self.colormap and not self.plte: 1895 | warnings.warn("PLTE chunk is required before IDAT chunk") 1896 | yield data 1897 | 1898 | def iterdecomp(idat): 1899 | """Iterator that yields decompressed strings. `idat` should 1900 | be an iterator that yields the ``IDAT`` chunk data. 1901 | """ 1902 | 1903 | # Currently, with no max_length paramter to decompress, this 1904 | # routine will do one yield per IDAT chunk. So not very 1905 | # incremental. 1906 | d = zlib.decompressobj() 1907 | # Each IDAT chunk is passed to the decompressor, then any 1908 | # remaining state is decompressed out. 1909 | for data in idat: 1910 | # :todo: add a max_length argument here to limit output 1911 | # size. 1912 | yield array('B', d.decompress(data)) 1913 | yield array('B', d.flush()) 1914 | 1915 | self.preamble(lenient=lenient) 1916 | raw = iterdecomp(iteridat()) 1917 | 1918 | if self.interlace: 1919 | raw = array('B', itertools.chain(*raw)) 1920 | arraycode = 'BH'[self.bitdepth>8] 1921 | # Like :meth:`group` but producing an array.array object for 1922 | # each row. 1923 | pixels = itertools.imap(lambda *row: array(arraycode, row), 1924 | *[iter(self.deinterlace(raw))]*self.width*self.planes) 1925 | else: 1926 | pixels = self.iterboxed(self.iterstraight(raw)) 1927 | meta = dict() 1928 | for attr in 'greyscale alpha planes bitdepth interlace'.split(): 1929 | meta[attr] = getattr(self, attr) 1930 | meta['size'] = (self.width, self.height) 1931 | for attr in 'gamma transparent background'.split(): 1932 | a = getattr(self, attr, None) 1933 | if a is not None: 1934 | meta[attr] = a 1935 | if self.plte: 1936 | meta['palette'] = self.palette() 1937 | return self.width, self.height, pixels, meta 1938 | 1939 | 1940 | def read_flat(self): 1941 | """ 1942 | Read a PNG file and decode it into flat row flat pixel format. 1943 | Returns (*width*, *height*, *pixels*, *metadata*). 1944 | 1945 | May use excessive memory. 1946 | 1947 | `pixels` are returned in flat row flat pixel format. 1948 | 1949 | See also the :meth:`read` method which returns pixels in the 1950 | more stream-friendly boxed row flat pixel format. 1951 | """ 1952 | 1953 | x, y, pixel, meta = self.read() 1954 | arraycode = 'BH'[meta['bitdepth']>8] 1955 | pixel = array(arraycode, itertools.chain(*pixel)) 1956 | return x, y, pixel, meta 1957 | 1958 | def palette(self, alpha='natural'): 1959 | """Returns a palette that is a sequence of 3-tuples or 4-tuples, 1960 | synthesizing it from the ``PLTE`` and ``tRNS`` chunks. These 1961 | chunks should have already been processed (for example, by 1962 | calling the :meth:`preamble` method). All the tuples are the 1963 | same size: 3-tuples if there is no ``tRNS`` chunk, 4-tuples when 1964 | there is a ``tRNS`` chunk. Assumes that the image is colour type 1965 | 3 and therefore a ``PLTE`` chunk is required. 1966 | 1967 | If the `alpha` argument is ``'force'`` then an alpha channel is 1968 | always added, forcing the result to be a sequence of 4-tuples. 1969 | """ 1970 | 1971 | if not self.plte: 1972 | raise FormatError( 1973 | "Required PLTE chunk is missing in colour type 3 image.") 1974 | plte = group(array('B', self.plte), 3) 1975 | if self.trns or alpha == 'force': 1976 | trns = array('B', self.trns or '') 1977 | trns.extend([255]*(len(plte)-len(trns))) 1978 | plte = map(operator.add, plte, group(trns, 1)) 1979 | return plte 1980 | 1981 | def asDirect(self): 1982 | """Returns the image data as a direct representation of an 1983 | ``x * y * planes`` array. This method is intended to remove the 1984 | need for callers to deal with palettes and transparency 1985 | themselves. Images with a palette (colour type 3) 1986 | are converted to RGB or RGBA; images with transparency (a 1987 | ``tRNS`` chunk) are converted to LA or RGBA as appropriate. 1988 | When returned in this format the pixel values represent the 1989 | colour value directly without needing to refer to palettes or 1990 | transparency information. 1991 | 1992 | Like the :meth:`read` method this method returns a 4-tuple: 1993 | 1994 | (*width*, *height*, *pixels*, *meta*) 1995 | 1996 | This method normally returns pixel values with the bit depth 1997 | they have in the source image, but when the source PNG has an 1998 | ``sBIT`` chunk it is inspected and can reduce the bit depth of 1999 | the result pixels; pixel values will be reduced according to 2000 | the bit depth specified in the ``sBIT`` chunk (PNG nerds should 2001 | note a single result bit depth is used for all channels; the 2002 | maximum of the ones specified in the ``sBIT`` chunk. An RGB565 2003 | image will be rescaled to 6-bit RGB666). 2004 | 2005 | The *meta* dictionary that is returned reflects the `direct` 2006 | format and not the original source image. For example, an RGB 2007 | source image with a ``tRNS`` chunk to represent a transparent 2008 | colour, will have ``planes=3`` and ``alpha=False`` for the 2009 | source image, but the *meta* dictionary returned by this method 2010 | will have ``planes=4`` and ``alpha=True`` because an alpha 2011 | channel is synthesized and added. 2012 | 2013 | *pixels* is the pixel data in boxed row flat pixel format (just 2014 | like the :meth:`read` method). 2015 | 2016 | All the other aspects of the image data are not changed. 2017 | """ 2018 | 2019 | self.preamble() 2020 | 2021 | # Simple case, no conversion necessary. 2022 | if not self.colormap and not self.trns and not self.sbit: 2023 | return self.read() 2024 | 2025 | x,y,pixels,meta = self.read() 2026 | 2027 | if self.colormap: 2028 | meta['colormap'] = False 2029 | meta['alpha'] = bool(self.trns) 2030 | meta['bitdepth'] = 8 2031 | meta['planes'] = 3 + bool(self.trns) 2032 | plte = self.palette() 2033 | def iterpal(pixels): 2034 | for row in pixels: 2035 | row = map(plte.__getitem__, row) 2036 | yield array('B', itertools.chain(*row)) 2037 | pixels = iterpal(pixels) 2038 | elif self.trns: 2039 | # It would be nice if there was some reasonable way 2040 | # of doing this without generating a whole load of 2041 | # intermediate tuples. But tuples does seem like the 2042 | # easiest way, with no other way clearly much simpler or 2043 | # much faster. (Actually, the L to LA conversion could 2044 | # perhaps go faster (all those 1-tuples!), but I still 2045 | # wonder whether the code proliferation is worth it) 2046 | it = self.transparent 2047 | maxval = 2**meta['bitdepth']-1 2048 | planes = meta['planes'] 2049 | meta['alpha'] = True 2050 | meta['planes'] += 1 2051 | typecode = 'BH'[meta['bitdepth']>8] 2052 | def itertrns(pixels): 2053 | for row in pixels: 2054 | # For each row we group it into pixels, then form a 2055 | # characterisation vector that says whether each 2056 | # pixel is opaque or not. Then we convert 2057 | # True/False to 0/maxval (by multiplication), 2058 | # and add it as the extra channel. 2059 | row = group(row, planes) 2060 | opa = map(it.__ne__, row) 2061 | opa = map(maxval.__mul__, opa) 2062 | opa = zip(opa) # convert to 1-tuples 2063 | yield array(typecode, 2064 | itertools.chain(*map(operator.add, row, opa))) 2065 | pixels = itertrns(pixels) 2066 | targetbitdepth = None 2067 | if self.sbit: 2068 | sbit = struct.unpack('%dB' % len(self.sbit), self.sbit) 2069 | targetbitdepth = max(sbit) 2070 | if targetbitdepth > meta['bitdepth']: 2071 | raise Error('sBIT chunk %r exceeds bitdepth %d' % 2072 | (sbit,self.bitdepth)) 2073 | if min(sbit) <= 0: 2074 | raise Error('sBIT chunk %r has a 0-entry' % sbit) 2075 | if targetbitdepth == meta['bitdepth']: 2076 | targetbitdepth = None 2077 | if targetbitdepth: 2078 | shift = meta['bitdepth'] - targetbitdepth 2079 | meta['bitdepth'] = targetbitdepth 2080 | def itershift(pixels): 2081 | for row in pixels: 2082 | yield map(shift.__rrshift__, row) 2083 | pixels = itershift(pixels) 2084 | return x,y,pixels,meta 2085 | 2086 | def asFloat(self, maxval=1.0): 2087 | """Return image pixels as per :meth:`asDirect` method, but scale 2088 | all pixel values to be floating point values between 0.0 and 2089 | *maxval*. 2090 | """ 2091 | 2092 | x,y,pixels,info = self.asDirect() 2093 | sourcemaxval = 2**info['bitdepth']-1 2094 | del info['bitdepth'] 2095 | info['maxval'] = float(maxval) 2096 | factor = float(maxval)/float(sourcemaxval) 2097 | def iterfloat(): 2098 | for row in pixels: 2099 | yield map(factor.__mul__, row) 2100 | return x,y,iterfloat(),info 2101 | 2102 | def _as_rescale(self, get, targetbitdepth): 2103 | """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`.""" 2104 | 2105 | width,height,pixels,meta = get() 2106 | maxval = 2**meta['bitdepth'] - 1 2107 | targetmaxval = 2**targetbitdepth - 1 2108 | factor = float(targetmaxval) / float(maxval) 2109 | meta['bitdepth'] = targetbitdepth 2110 | def iterscale(): 2111 | for row in pixels: 2112 | yield map(lambda x: int(round(x*factor)), row) 2113 | if maxval == targetmaxval: 2114 | return width, height, pixels, meta 2115 | else: 2116 | return width, height, iterscale(), meta 2117 | 2118 | def asRGB8(self): 2119 | """Return the image data as an RGB pixels with 8-bits per 2120 | sample. This is like the :meth:`asRGB` method except that 2121 | this method additionally rescales the values so that they 2122 | are all between 0 and 255 (8-bit). In the case where the 2123 | source image has a bit depth < 8 the transformation preserves 2124 | all the information; where the source image has bit depth 2125 | > 8, then rescaling to 8-bit values loses precision. No 2126 | dithering is performed. Like :meth:`asRGB`, an alpha channel 2127 | in the source image will raise an exception. 2128 | 2129 | This function returns a 4-tuple: 2130 | (*width*, *height*, *pixels*, *metadata*). 2131 | *width*, *height*, *metadata* are as per the 2132 | :meth:`read` method. 2133 | 2134 | *pixels* is the pixel data in boxed row flat pixel format. 2135 | """ 2136 | 2137 | return self._as_rescale(self.asRGB, 8) 2138 | 2139 | def asRGBA8(self): 2140 | """Return the image data as RGBA pixels with 8-bits per 2141 | sample. This method is similar to :meth:`asRGB8` and 2142 | :meth:`asRGBA`: The result pixels have an alpha channel, *and* 2143 | values are rescaled to the range 0 to 255. The alpha channel is 2144 | synthesized if necessary (with a small speed penalty). 2145 | """ 2146 | 2147 | return self._as_rescale(self.asRGBA, 8) 2148 | 2149 | def asRGB(self): 2150 | """Return image as RGB pixels. RGB colour images are passed 2151 | through unchanged; greyscales are expanded into RGB 2152 | triplets (there is a small speed overhead for doing this). 2153 | 2154 | An alpha channel in the source image will raise an 2155 | exception. 2156 | 2157 | The return values are as for the :meth:`read` method 2158 | except that the *metadata* reflect the returned pixels, not the 2159 | source image. In particular, for this method 2160 | ``metadata['greyscale']`` will be ``False``. 2161 | """ 2162 | 2163 | width,height,pixels,meta = self.asDirect() 2164 | if meta['alpha']: 2165 | raise Error("will not convert image with alpha channel to RGB") 2166 | if not meta['greyscale']: 2167 | return width,height,pixels,meta 2168 | meta['greyscale'] = False 2169 | typecode = 'BH'[meta['bitdepth'] > 8] 2170 | def iterrgb(): 2171 | for row in pixels: 2172 | a = array(typecode, [0]) * 3 * width 2173 | for i in range(3): 2174 | a[i::3] = row 2175 | yield a 2176 | return width,height,iterrgb(),meta 2177 | 2178 | def asRGBA(self): 2179 | """Return image as RGBA pixels. Greyscales are expanded into 2180 | RGB triplets; an alpha channel is synthesized if necessary. 2181 | The return values are as for the :meth:`read` method 2182 | except that the *metadata* reflect the returned pixels, not the 2183 | source image. In particular, for this method 2184 | ``metadata['greyscale']`` will be ``False``, and 2185 | ``metadata['alpha']`` will be ``True``. 2186 | """ 2187 | 2188 | width,height,pixels,meta = self.asDirect() 2189 | if meta['alpha'] and not meta['greyscale']: 2190 | return width,height,pixels,meta 2191 | typecode = 'BH'[meta['bitdepth'] > 8] 2192 | maxval = 2**meta['bitdepth'] - 1 2193 | maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width 2194 | def newarray(): 2195 | return array(typecode, maxbuffer) 2196 | 2197 | if meta['alpha'] and meta['greyscale']: 2198 | # LA to RGBA 2199 | def convert(): 2200 | for row in pixels: 2201 | # Create a fresh target row, then copy L channel 2202 | # into first three target channels, and A channel 2203 | # into fourth channel. 2204 | a = newarray() 2205 | pngfilters.convert_la_to_rgba(row, a) 2206 | yield a 2207 | elif meta['greyscale']: 2208 | # L to RGBA 2209 | def convert(): 2210 | for row in pixels: 2211 | a = newarray() 2212 | pngfilters.convert_l_to_rgba(row, a) 2213 | yield a 2214 | else: 2215 | assert not meta['alpha'] and not meta['greyscale'] 2216 | # RGB to RGBA 2217 | def convert(): 2218 | for row in pixels: 2219 | a = newarray() 2220 | pngfilters.convert_rgb_to_rgba(row, a) 2221 | yield a 2222 | meta['alpha'] = True 2223 | meta['greyscale'] = False 2224 | return width,height,convert(),meta 2225 | 2226 | def check_bitdepth_colortype(bitdepth, colortype): 2227 | """Check that `bitdepth` and `colortype` are both valid, 2228 | and specified in a valid combination. Returns if valid, 2229 | raise an Exception if not valid. 2230 | """ 2231 | 2232 | if bitdepth not in (1,2,4,8,16): 2233 | raise FormatError("invalid bit depth %d" % bitdepth) 2234 | if colortype not in (0,2,3,4,6): 2235 | raise FormatError("invalid colour type %d" % colortype) 2236 | # Check indexed (palettized) images have 8 or fewer bits 2237 | # per pixel; check only indexed or greyscale images have 2238 | # fewer than 8 bits per pixel. 2239 | if colortype & 1 and bitdepth > 8: 2240 | raise FormatError( 2241 | "Indexed images (colour type %d) cannot" 2242 | " have bitdepth > 8 (bit depth %d)." 2243 | " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." 2244 | % (bitdepth, colortype)) 2245 | if bitdepth < 8 and colortype not in (0,3): 2246 | raise FormatError("Illegal combination of bit depth (%d)" 2247 | " and colour type (%d)." 2248 | " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." 2249 | % (bitdepth, colortype)) 2250 | 2251 | def isinteger(x): 2252 | try: 2253 | return int(x) == x 2254 | except (TypeError, ValueError): 2255 | return False 2256 | 2257 | 2258 | # === Legacy Version Support === 2259 | 2260 | # :pyver:old: PyPNG works on Python versions 2.3 and 2.2, but not 2261 | # without some awkward problems. Really PyPNG works on Python 2.4 (and 2262 | # above); it works on Pythons 2.3 and 2.2 by virtue of fixing up 2263 | # problems here. It's a bit ugly (which is why it's hidden down here). 2264 | # 2265 | # Generally the strategy is one of pretending that we're running on 2266 | # Python 2.4 (or above), and patching up the library support on earlier 2267 | # versions so that it looks enough like Python 2.4. When it comes to 2268 | # Python 2.2 there is one thing we cannot patch: extended slices 2269 | # http://www.python.org/doc/2.3/whatsnew/section-slices.html. 2270 | # Instead we simply declare that features that are implemented using 2271 | # extended slices will not work on Python 2.2. 2272 | # 2273 | # In order to work on Python 2.3 we fix up a recurring annoyance involving 2274 | # the array type. In Python 2.3 an array cannot be initialised with an 2275 | # array, and it cannot be extended with a list (or other sequence). 2276 | # Both of those are repeated issues in the code. Whilst I would not 2277 | # normally tolerate this sort of behaviour, here we "shim" a replacement 2278 | # for array into place (and hope no-one notices). You never read this. 2279 | # 2280 | # In an amusing case of warty hacks on top of warty hacks... the array 2281 | # shimming we try and do only works on Python 2.3 and above (you can't 2282 | # subclass array.array in Python 2.2). So to get it working on Python 2283 | # 2.2 we go for something much simpler and (probably) way slower. 2284 | try: 2285 | array('B').extend([]) 2286 | array('B', array('B')) 2287 | # :todo:(drj) Check that TypeError is correct for Python 2.3 2288 | except TypeError: 2289 | # Expect to get here on Python 2.3 2290 | try: 2291 | class _array_shim(array): 2292 | true_array = array 2293 | def __new__(cls, typecode, init=None): 2294 | super_new = super(_array_shim, cls).__new__ 2295 | it = super_new(cls, typecode) 2296 | if init is None: 2297 | return it 2298 | it.extend(init) 2299 | return it 2300 | def extend(self, extension): 2301 | super_extend = super(_array_shim, self).extend 2302 | if isinstance(extension, self.true_array): 2303 | return super_extend(extension) 2304 | if not isinstance(extension, (list, str)): 2305 | # Convert to list. Allows iterators to work. 2306 | extension = list(extension) 2307 | return super_extend(self.true_array(self.typecode, extension)) 2308 | array = _array_shim 2309 | except TypeError: 2310 | # Expect to get here on Python 2.2 2311 | def array(typecode, init=()): 2312 | if type(init) == str: 2313 | return map(ord, init) 2314 | return list(init) 2315 | 2316 | # Further hacks to get it limping along on Python 2.2 2317 | try: 2318 | enumerate 2319 | except NameError: 2320 | def enumerate(seq): 2321 | i=0 2322 | for x in seq: 2323 | yield i,x 2324 | i += 1 2325 | 2326 | try: 2327 | reversed 2328 | except NameError: 2329 | def reversed(l): 2330 | l = list(l) 2331 | l.reverse() 2332 | for x in l: 2333 | yield x 2334 | 2335 | try: 2336 | itertools 2337 | except NameError: 2338 | class _dummy_itertools: 2339 | pass 2340 | itertools = _dummy_itertools() 2341 | def _itertools_imap(f, seq): 2342 | for x in seq: 2343 | yield f(x) 2344 | itertools.imap = _itertools_imap 2345 | def _itertools_chain(*iterables): 2346 | for it in iterables: 2347 | for element in it: 2348 | yield element 2349 | itertools.chain = _itertools_chain 2350 | 2351 | 2352 | # === Support for users without Cython === 2353 | 2354 | try: 2355 | pngfilters 2356 | except NameError: 2357 | class pngfilters(object): 2358 | def undo_filter_sub(filter_unit, scanline, previous, result): 2359 | """Undo sub filter.""" 2360 | 2361 | ai = 0 2362 | # Loops starts at index fu. Observe that the initial part 2363 | # of the result is already filled in correctly with 2364 | # scanline. 2365 | for i in range(filter_unit, len(result)): 2366 | x = scanline[i] 2367 | a = result[ai] 2368 | result[i] = (x + a) & 0xff 2369 | ai += 1 2370 | undo_filter_sub = staticmethod(undo_filter_sub) 2371 | 2372 | def undo_filter_up(filter_unit, scanline, previous, result): 2373 | """Undo up filter.""" 2374 | 2375 | for i in range(len(result)): 2376 | x = scanline[i] 2377 | b = previous[i] 2378 | result[i] = (x + b) & 0xff 2379 | undo_filter_up = staticmethod(undo_filter_up) 2380 | 2381 | def undo_filter_average(filter_unit, scanline, previous, result): 2382 | """Undo up filter.""" 2383 | 2384 | ai = -filter_unit 2385 | for i in range(len(result)): 2386 | x = scanline[i] 2387 | if ai < 0: 2388 | a = 0 2389 | else: 2390 | a = result[ai] 2391 | b = previous[i] 2392 | result[i] = (x + ((a + b) >> 1)) & 0xff 2393 | ai += 1 2394 | undo_filter_average = staticmethod(undo_filter_average) 2395 | 2396 | def undo_filter_paeth(filter_unit, scanline, previous, result): 2397 | """Undo Paeth filter.""" 2398 | 2399 | # Also used for ci. 2400 | ai = -filter_unit 2401 | for i in range(len(result)): 2402 | x = scanline[i] 2403 | if ai < 0: 2404 | a = c = 0 2405 | else: 2406 | a = result[ai] 2407 | c = previous[ai] 2408 | b = previous[i] 2409 | p = a + b - c 2410 | pa = abs(p - a) 2411 | pb = abs(p - b) 2412 | pc = abs(p - c) 2413 | if pa <= pb and pa <= pc: 2414 | pr = a 2415 | elif pb <= pc: 2416 | pr = b 2417 | else: 2418 | pr = c 2419 | result[i] = (x + pr) & 0xff 2420 | ai += 1 2421 | undo_filter_paeth = staticmethod(undo_filter_paeth) 2422 | 2423 | def convert_la_to_rgba(row, result): 2424 | for i in range(3): 2425 | result[i::4] = row[0::2] 2426 | result[3::4] = row[1::2] 2427 | convert_la_to_rgba = staticmethod(convert_la_to_rgba) 2428 | 2429 | def convert_l_to_rgba(row, result): 2430 | """Convert a grayscale image to RGBA. This method assumes 2431 | the alpha channel in result is already correctly 2432 | initialized. 2433 | """ 2434 | for i in range(3): 2435 | result[i::4] = row 2436 | convert_l_to_rgba = staticmethod(convert_l_to_rgba) 2437 | 2438 | def convert_rgb_to_rgba(row, result): 2439 | """Convert an RGB image to RGBA. This method assumes the 2440 | alpha channel in result is already correctly initialized. 2441 | """ 2442 | for i in range(3): 2443 | result[i::4] = row[i::3] 2444 | convert_rgb_to_rgba = staticmethod(convert_rgb_to_rgba) 2445 | 2446 | 2447 | # === Command Line Support === 2448 | 2449 | def read_pam_header(infile): 2450 | """ 2451 | Read (the rest of a) PAM header. `infile` should be positioned 2452 | immediately after the initial 'P7' line (at the beginning of the 2453 | second line). Returns are as for `read_pnm_header`. 2454 | """ 2455 | 2456 | # Unlike PBM, PGM, and PPM, we can read the header a line at a time. 2457 | header = dict() 2458 | while True: 2459 | l = infile.readline().strip() 2460 | if l == strtobytes('ENDHDR'): 2461 | break 2462 | if not l: 2463 | raise EOFError('PAM ended prematurely') 2464 | if l[0] == strtobytes('#'): 2465 | continue 2466 | l = l.split(None, 1) 2467 | if l[0] not in header: 2468 | header[l[0]] = l[1] 2469 | else: 2470 | header[l[0]] += strtobytes(' ') + l[1] 2471 | 2472 | required = ['WIDTH', 'HEIGHT', 'DEPTH', 'MAXVAL'] 2473 | required = [strtobytes(x) for x in required] 2474 | WIDTH,HEIGHT,DEPTH,MAXVAL = required 2475 | present = [x for x in required if x in header] 2476 | if len(present) != len(required): 2477 | raise Error('PAM file must specify WIDTH, HEIGHT, DEPTH, and MAXVAL') 2478 | width = int(header[WIDTH]) 2479 | height = int(header[HEIGHT]) 2480 | depth = int(header[DEPTH]) 2481 | maxval = int(header[MAXVAL]) 2482 | if (width <= 0 or 2483 | height <= 0 or 2484 | depth <= 0 or 2485 | maxval <= 0): 2486 | raise Error( 2487 | 'WIDTH, HEIGHT, DEPTH, MAXVAL must all be positive integers') 2488 | return 'P7', width, height, depth, maxval 2489 | 2490 | def read_pnm_header(infile, supported=('P5','P6')): 2491 | """ 2492 | Read a PNM header, returning (format,width,height,depth,maxval). 2493 | `width` and `height` are in pixels. `depth` is the number of 2494 | channels in the image; for PBM and PGM it is synthesized as 1, for 2495 | PPM as 3; for PAM images it is read from the header. `maxval` is 2496 | synthesized (as 1) for PBM images. 2497 | """ 2498 | 2499 | # Generally, see http://netpbm.sourceforge.net/doc/ppm.html 2500 | # and http://netpbm.sourceforge.net/doc/pam.html 2501 | 2502 | supported = [strtobytes(x) for x in supported] 2503 | 2504 | # Technically 'P7' must be followed by a newline, so by using 2505 | # rstrip() we are being liberal in what we accept. I think this 2506 | # is acceptable. 2507 | type = infile.read(3).rstrip() 2508 | if type not in supported: 2509 | raise NotImplementedError('file format %s not supported' % type) 2510 | if type == strtobytes('P7'): 2511 | # PAM header parsing is completely different. 2512 | return read_pam_header(infile) 2513 | # Expected number of tokens in header (3 for P4, 4 for P6) 2514 | expected = 4 2515 | pbm = ('P1', 'P4') 2516 | if type in pbm: 2517 | expected = 3 2518 | header = [type] 2519 | 2520 | # We have to read the rest of the header byte by byte because the 2521 | # final whitespace character (immediately following the MAXVAL in 2522 | # the case of P6) may not be a newline. Of course all PNM files in 2523 | # the wild use a newline at this point, so it's tempting to use 2524 | # readline; but it would be wrong. 2525 | def getc(): 2526 | c = infile.read(1) 2527 | if not c: 2528 | raise Error('premature EOF reading PNM header') 2529 | return c 2530 | 2531 | c = getc() 2532 | while True: 2533 | # Skip whitespace that precedes a token. 2534 | while c.isspace(): 2535 | c = getc() 2536 | # Skip comments. 2537 | while c == '#': 2538 | while c not in '\n\r': 2539 | c = getc() 2540 | if not c.isdigit(): 2541 | raise Error('unexpected character %s found in header' % c) 2542 | # According to the specification it is legal to have comments 2543 | # that appear in the middle of a token. 2544 | # This is bonkers; I've never seen it; and it's a bit awkward to 2545 | # code good lexers in Python (no goto). So we break on such 2546 | # cases. 2547 | token = strtobytes('') 2548 | while c.isdigit(): 2549 | token += c 2550 | c = getc() 2551 | # Slight hack. All "tokens" are decimal integers, so convert 2552 | # them here. 2553 | header.append(int(token)) 2554 | if len(header) == expected: 2555 | break 2556 | # Skip comments (again) 2557 | while c == '#': 2558 | while c not in '\n\r': 2559 | c = getc() 2560 | if not c.isspace(): 2561 | raise Error('expected header to end with whitespace, not %s' % c) 2562 | 2563 | if type in pbm: 2564 | # synthesize a MAXVAL 2565 | header.append(1) 2566 | depth = (1,3)[type == strtobytes('P6')] 2567 | return header[0], header[1], header[2], depth, header[3] 2568 | 2569 | def write_pnm(file, width, height, pixels, meta): 2570 | """Write a Netpbm PNM/PAM file. 2571 | """ 2572 | 2573 | bitdepth = meta['bitdepth'] 2574 | maxval = 2**bitdepth - 1 2575 | # Rudely, the number of image planes can be used to determine 2576 | # whether we are L (PGM), LA (PAM), RGB (PPM), or RGBA (PAM). 2577 | planes = meta['planes'] 2578 | # Can be an assert as long as we assume that pixels and meta came 2579 | # from a PNG file. 2580 | assert planes in (1,2,3,4) 2581 | if planes in (1,3): 2582 | if 1 == planes: 2583 | # PGM 2584 | # Could generate PBM if maxval is 1, but we don't (for one 2585 | # thing, we'd have to convert the data, not just blat it 2586 | # out). 2587 | fmt = 'P5' 2588 | else: 2589 | # PPM 2590 | fmt = 'P6' 2591 | file.write('%s %d %d %d\n' % (fmt, width, height, maxval)) 2592 | if planes in (2,4): 2593 | # PAM 2594 | # See http://netpbm.sourceforge.net/doc/pam.html 2595 | if 2 == planes: 2596 | tupltype = 'GRAYSCALE_ALPHA' 2597 | else: 2598 | tupltype = 'RGB_ALPHA' 2599 | file.write('P7\nWIDTH %d\nHEIGHT %d\nDEPTH %d\nMAXVAL %d\n' 2600 | 'TUPLTYPE %s\nENDHDR\n' % 2601 | (width, height, planes, maxval, tupltype)) 2602 | # Values per row 2603 | vpr = planes * width 2604 | # struct format 2605 | fmt = '>%d' % vpr 2606 | if maxval > 0xff: 2607 | fmt = fmt + 'H' 2608 | else: 2609 | fmt = fmt + 'B' 2610 | for row in pixels: 2611 | file.write(struct.pack(fmt, *row)) 2612 | file.flush() 2613 | 2614 | def color_triple(color): 2615 | """ 2616 | Convert a command line colour value to a RGB triple of integers. 2617 | FIXME: Somewhere we need support for greyscale backgrounds etc. 2618 | """ 2619 | if color.startswith('#') and len(color) == 4: 2620 | return (int(color[1], 16), 2621 | int(color[2], 16), 2622 | int(color[3], 16)) 2623 | if color.startswith('#') and len(color) == 7: 2624 | return (int(color[1:3], 16), 2625 | int(color[3:5], 16), 2626 | int(color[5:7], 16)) 2627 | elif color.startswith('#') and len(color) == 13: 2628 | return (int(color[1:5], 16), 2629 | int(color[5:9], 16), 2630 | int(color[9:13], 16)) 2631 | 2632 | def _add_common_options(parser): 2633 | """Call *parser.add_option* for each of the options that are 2634 | common between this PNG--PNM conversion tool and the gen 2635 | tool. 2636 | """ 2637 | parser.add_option("-i", "--interlace", 2638 | default=False, action="store_true", 2639 | help="create an interlaced PNG file (Adam7)") 2640 | parser.add_option("-t", "--transparent", 2641 | action="store", type="string", metavar="#RRGGBB", 2642 | help="mark the specified colour as transparent") 2643 | parser.add_option("-b", "--background", 2644 | action="store", type="string", metavar="#RRGGBB", 2645 | help="save the specified background colour") 2646 | parser.add_option("-g", "--gamma", 2647 | action="store", type="float", metavar="value", 2648 | help="save the specified gamma value") 2649 | parser.add_option("-c", "--compression", 2650 | action="store", type="int", metavar="level", 2651 | help="zlib compression level (0-9)") 2652 | return parser 2653 | 2654 | def _main(argv): 2655 | """ 2656 | Run the PNG encoder with options from the command line. 2657 | """ 2658 | 2659 | # Parse command line arguments 2660 | from optparse import OptionParser 2661 | import re 2662 | version = '%prog ' + __version__ 2663 | parser = OptionParser(version=version) 2664 | parser.set_usage("%prog [options] [imagefile]") 2665 | parser.add_option('-r', '--read-png', default=False, 2666 | action='store_true', 2667 | help='Read PNG, write PNM') 2668 | parser.add_option("-a", "--alpha", 2669 | action="store", type="string", metavar="pgmfile", 2670 | help="alpha channel transparency (RGBA)") 2671 | _add_common_options(parser) 2672 | 2673 | (options, args) = parser.parse_args(args=argv[1:]) 2674 | 2675 | # Convert options 2676 | if options.transparent is not None: 2677 | options.transparent = color_triple(options.transparent) 2678 | if options.background is not None: 2679 | options.background = color_triple(options.background) 2680 | 2681 | # Prepare input and output files 2682 | if len(args) == 0: 2683 | infilename = '-' 2684 | infile = sys.stdin 2685 | elif len(args) == 1: 2686 | infilename = args[0] 2687 | infile = open(infilename, 'rb') 2688 | else: 2689 | parser.error("more than one input file") 2690 | outfile = sys.stdout 2691 | if sys.platform == "win32": 2692 | import msvcrt, os 2693 | msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) 2694 | 2695 | if options.read_png: 2696 | # Encode PNG to PPM 2697 | png = Reader(file=infile) 2698 | width,height,pixels,meta = png.asDirect() 2699 | write_pnm(outfile, width, height, pixels, meta) 2700 | else: 2701 | # Encode PNM to PNG 2702 | format, width, height, depth, maxval = \ 2703 | read_pnm_header(infile, ('P5','P6','P7')) 2704 | # When it comes to the variety of input formats, we do something 2705 | # rather rude. Observe that L, LA, RGB, RGBA are the 4 colour 2706 | # types supported by PNG and that they correspond to 1, 2, 3, 4 2707 | # channels respectively. So we use the number of channels in 2708 | # the source image to determine which one we have. We do not 2709 | # care about TUPLTYPE. 2710 | greyscale = depth <= 2 2711 | pamalpha = depth in (2,4) 2712 | supported = map(lambda x: 2**x-1, range(1,17)) 2713 | try: 2714 | mi = supported.index(maxval) 2715 | except ValueError: 2716 | raise NotImplementedError( 2717 | 'your maxval (%s) not in supported list %s' % 2718 | (maxval, str(supported))) 2719 | bitdepth = mi+1 2720 | writer = Writer(width, height, 2721 | greyscale=greyscale, 2722 | bitdepth=bitdepth, 2723 | interlace=options.interlace, 2724 | transparent=options.transparent, 2725 | background=options.background, 2726 | alpha=bool(pamalpha or options.alpha), 2727 | gamma=options.gamma, 2728 | compression=options.compression) 2729 | if options.alpha: 2730 | pgmfile = open(options.alpha, 'rb') 2731 | format, awidth, aheight, adepth, amaxval = \ 2732 | read_pnm_header(pgmfile, 'P5') 2733 | if amaxval != '255': 2734 | raise NotImplementedError( 2735 | 'maxval %s not supported for alpha channel' % amaxval) 2736 | if (awidth, aheight) != (width, height): 2737 | raise ValueError("alpha channel image size mismatch" 2738 | " (%s has %sx%s but %s has %sx%s)" 2739 | % (infilename, width, height, 2740 | options.alpha, awidth, aheight)) 2741 | writer.convert_ppm_and_pgm(infile, pgmfile, outfile) 2742 | else: 2743 | writer.convert_pnm(infile, outfile) 2744 | 2745 | 2746 | if __name__ == '__main__': 2747 | try: 2748 | _main(sys.argv) 2749 | except Error, e: 2750 | print >>sys.stderr, e 2751 | 2752 | -------------------------------------------------------------------------------- /raster2laser_gcode.inx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Raster 2 Laser GCode generator 7 | 8 | com.305engineering.raster2laser_gcode 9 | 10 | 11 | raster2laser_gcode.py 12 | inkex.py 13 | 14 | 15 | 16 | 17 | 18 | Raster 2 Laser GCode generator 19 | created by 305 Engineering 20 | 21 | 22 | 23 | 24 | true 25 | 26 | 27 | <_item value="#ffffff">White 28 | <_item value="#000000">Black 29 | 30 | 31 | 32 | <_item value="1">1 pixel/mm 33 | <_item value="2">2 pixel/mm 34 | <_item value="5">5 pixel/mm 35 | <_item value="10">10 pixel/mm 36 | 37 | 38 | 39 | 40 | <_item value="1">0.21R + 0.71G + 0.07B 41 | <_item value="2">(R+G+B)/3 42 | <_item value="3">R 43 | <_item value="4">G 44 | <_item value="5">B 45 | <_item value="6">Max Color 46 | <_item value="7">Min Color 47 | 48 | 49 | 50 | 51 | <_item value="1">B/W fixed threshold 52 | <_item value="2">B/W random threshold 53 | <_item value="3">Halftone 54 | <_item value="4">Halftone row 55 | <_item value="5">Halftone column 56 | <_item value="6">Grayscale 57 | 58 | 59 | 60 | 128 61 | 62 | 63 | <_item value="1">256 64 | <_item value="2">128 65 | <_item value="4">64 66 | <_item value="8">32 67 | <_item value="16">16 68 | <_item value="32">8 69 | 70 | 71 | 72 | 73 | 200 74 | 75 | 76 | false 77 | 78 | 79 | 80 | <_item value="1">G28 (Standard) 81 | <_item value="2">$H (GRBL) 82 | <_item value="3">No Homing 83 | 84 | 85 | M03 86 | M05 87 | 88 | 89 | false 90 | If "Preview only" is true the gcode file will not be generated. 91 | 92 | 93 | 94 | 95 | 96 | 97 | all 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /raster2laser_gcode.py: -------------------------------------------------------------------------------- 1 | ''' 2 | # ---------------------------------------------------------------------------- 3 | # Copyright (C) 2014 305engineering <305engineering@gmail.com> 4 | # Original concept by 305engineering. 5 | # 6 | # "THE MODIFIED BEER-WARE LICENSE" (Revision: my own :P): 7 | # <305engineering@gmail.com> wrote this file. As long as you retain this notice you 8 | # can do whatever you want with this stuff (except sell). If we meet some day, 9 | # and you think this stuff is worth it, you can buy me a beer in return. 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 18 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 19 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # ---------------------------------------------------------------------------- 23 | ''' 24 | 25 | 26 | import sys 27 | import os 28 | import re 29 | 30 | sys.path.append('/usr/share/inkscape/extensions') 31 | sys.path.append('/Applications/Inkscape.app/Contents/Resources/extensions') 32 | 33 | import subprocess 34 | import math 35 | 36 | import inkex 37 | import png 38 | import array 39 | 40 | 41 | class GcodeExport(inkex.Effect): 42 | 43 | ######## Richiamata da _main() 44 | def __init__(self): 45 | """init the effetc library and get options from gui""" 46 | inkex.Effect.__init__(self) 47 | 48 | # Opzioni di esportazione dell'immagine 49 | self.OptionParser.add_option("-d", "--directory",action="store", type="string", dest="directory", default="/home/",help="Directory for files") ####check_dir 50 | self.OptionParser.add_option("-f", "--filename", action="store", type="string", dest="filename", default="-1.0", help="File name") 51 | self.OptionParser.add_option("","--add-numeric-suffix-to-filename", action="store", type="inkbool", dest="add_numeric_suffix_to_filename", default=True,help="Add numeric suffix to filename") 52 | self.OptionParser.add_option("","--bg_color",action="store",type="string",dest="bg_color",default="",help="") 53 | self.OptionParser.add_option("","--resolution",action="store", type="int", dest="resolution", default="5",help="") #Usare il valore su float(xy)/resolution e un case per i DPI dell export 54 | 55 | 56 | # Come convertire in scala di grigi 57 | self.OptionParser.add_option("","--grayscale_type",action="store", type="int", dest="grayscale_type", default="1",help="") 58 | 59 | # Modalita di conversione in Bianco e Nero 60 | self.OptionParser.add_option("","--conversion_type",action="store", type="int", dest="conversion_type", default="1",help="") 61 | 62 | # Opzioni modalita 63 | self.OptionParser.add_option("","--BW_threshold",action="store", type="int", dest="BW_threshold", default="128",help="") 64 | self.OptionParser.add_option("","--grayscale_resolution",action="store", type="int", dest="grayscale_resolution", default="1",help="") 65 | 66 | #Velocita Nero e spostamento 67 | self.OptionParser.add_option("","--speed_ON",action="store", type="int", dest="speed_ON", default="200",help="") 68 | 69 | # Mirror Y 70 | self.OptionParser.add_option("","--flip_y",action="store", type="inkbool", dest="flip_y", default=False,help="") 71 | 72 | # Homing 73 | self.OptionParser.add_option("","--homing",action="store", type="int", dest="homing", default="1",help="") 74 | 75 | # Commands 76 | self.OptionParser.add_option("","--laseron", action="store", type="string", dest="laseron", default="M03", help="") 77 | self.OptionParser.add_option("","--laseroff", action="store", type="string", dest="laseroff", default="M05", help="") 78 | 79 | 80 | # Anteprima = Solo immagine BN 81 | self.OptionParser.add_option("","--preview_only",action="store", type="inkbool", dest="preview_only", default=False,help="") 82 | 83 | #inkex.errormsg("BLA BLA BLA Messaggio da visualizzare") #DEBUG 84 | 85 | 86 | 87 | 88 | ######## Richiamata da __init__() 89 | ######## Qui si svolge tutto 90 | def effect(self): 91 | 92 | 93 | current_file = self.args[-1] 94 | bg_color = self.options.bg_color 95 | 96 | 97 | ##Implementare check_dir 98 | 99 | if (os.path.isdir(self.options.directory)) == True: 100 | 101 | ##CODICE SE ESISTE LA DIRECTORY 102 | #inkex.errormsg("OK") #DEBUG 103 | 104 | 105 | #Aggiungo un suffisso al nomefile per non sovrascrivere dei file 106 | if self.options.add_numeric_suffix_to_filename : 107 | dir_list = os.listdir(self.options.directory) #List di tutti i file nella directory di lavoro 108 | temp_name = self.options.filename 109 | max_n = 0 110 | for s in dir_list : 111 | r = re.match(r"^%s_0*(\d+)%s$"%(re.escape(temp_name),'.png' ), s) 112 | if r : 113 | max_n = max(max_n,int(r.group(1))) 114 | self.options.filename = temp_name + "_" + ( "0"*(4-len(str(max_n+1))) + str(max_n+1) ) 115 | 116 | 117 | #genero i percorsi file da usare 118 | 119 | suffix = "" 120 | if self.options.conversion_type == 1: 121 | suffix = "_BWfix_"+str(self.options.BW_threshold)+"_" 122 | elif self.options.conversion_type == 2: 123 | suffix = "_BWrnd_" 124 | elif self.options.conversion_type == 3: 125 | suffix = "_H_" 126 | elif self.options.conversion_type == 4: 127 | suffix = "_Hrow_" 128 | elif self.options.conversion_type == 5: 129 | suffix = "_Hcol_" 130 | else: 131 | if self.options.grayscale_resolution == 1: 132 | suffix = "_Gray_256_" 133 | elif self.options.grayscale_resolution == 2: 134 | suffix = "_Gray_128_" 135 | elif self.options.grayscale_resolution == 4: 136 | suffix = "_Gray_64_" 137 | elif self.options.grayscale_resolution == 8: 138 | suffix = "_Gray_32_" 139 | elif self.options.grayscale_resolution == 16: 140 | suffix = "_Gray_16_" 141 | elif self.options.grayscale_resolution == 32: 142 | suffix = "_Gray_8_" 143 | else: 144 | suffix = "_Gray_" 145 | 146 | 147 | pos_file_png_exported = os.path.join(self.options.directory,self.options.filename+".png") 148 | pos_file_png_BW = os.path.join(self.options.directory,self.options.filename+suffix+"preview.png") 149 | pos_file_gcode = os.path.join(self.options.directory,self.options.filename+suffix+"gcode.txt") 150 | 151 | 152 | #Esporto l'immagine in PNG 153 | self.exportPage(pos_file_png_exported,current_file,bg_color) 154 | 155 | 156 | 157 | #DA FARE 158 | #Manipolo l'immagine PNG per generare il file Gcode 159 | self.PNGtoGcode(pos_file_png_exported,pos_file_png_BW,pos_file_gcode) 160 | 161 | 162 | else: 163 | inkex.errormsg("Directory does not exist! Please specify existing directory!") 164 | 165 | 166 | 167 | 168 | ######## ESPORTA L IMMAGINE IN PNG 169 | ######## Richiamata da effect() 170 | 171 | def exportPage(self,pos_file_png_exported,current_file,bg_color): 172 | ######## CREAZIONE DEL FILE PNG ######## 173 | #Crea l'immagine dentro la cartella indicata da "pos_file_png_exported" 174 | # -d 127 = risoluzione 127DPI => 5 pixel/mm 1pixel = 0.2mm 175 | ###command="inkscape -C -e \"%s\" -b\"%s\" %s -d 127" % (pos_file_png_exported,bg_color,current_file) 176 | 177 | if self.options.resolution == 1: 178 | DPI = 25.4 179 | elif self.options.resolution == 2: 180 | DPI = 50.8 181 | elif self.options.resolution == 5: 182 | DPI = 127 183 | else: 184 | DPI = 254 185 | 186 | command="inkscape -C -e \"%s\" -b\"%s\" %s -d %s" % (pos_file_png_exported,bg_color,current_file,DPI) #Comando da linea di comando per esportare in PNG 187 | 188 | p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 189 | return_code = p.wait() 190 | f = p.stdout 191 | err = p.stderr 192 | 193 | 194 | ######## CREA IMMAGINE IN B/N E POI GENERA GCODE 195 | ######## Richiamata da effect() 196 | 197 | def PNGtoGcode(self,pos_file_png_exported,pos_file_png_BW,pos_file_gcode): 198 | 199 | ######## GENERO IMMAGINE IN SCALA DI GRIGI ######## 200 | #Scorro l immagine e la faccio diventare una matrice composta da list 201 | 202 | 203 | reader = png.Reader(pos_file_png_exported)#File PNG generato 204 | 205 | w, h, pixels, metadata = reader.read_flat() 206 | 207 | 208 | matrice = [[255 for i in range(w)]for j in range(h)] #List al posto di un array 209 | 210 | 211 | #Scrivo una nuova immagine in Scala di grigio 8bit 212 | #copia pixel per pixel 213 | 214 | if self.options.grayscale_type == 1: 215 | #0.21R + 0.71G + 0.07B 216 | for y in range(h): # y varia da 0 a h-1 217 | for x in range(w): # x varia da 0 a w-1 218 | pixel_position = (x + y * w)*4 if metadata['alpha'] else (x + y * w)*3 219 | matrice[y][x] = int(pixels[pixel_position]*0.21 + pixels[(pixel_position+1)]*0.71 + pixels[(pixel_position+2)]*0.07) 220 | 221 | elif self.options.grayscale_type == 2: 222 | #(R+G+B)/3 223 | for y in range(h): # y varia da 0 a h-1 224 | for x in range(w): # x varia da 0 a w-1 225 | pixel_position = (x + y * w)*4 if metadata['alpha'] else (x + y * w)*3 226 | matrice[y][x] = int((pixels[pixel_position] + pixels[(pixel_position+1)]+ pixels[(pixel_position+2)]) / 3 ) 227 | 228 | elif self.options.grayscale_type == 3: 229 | #R 230 | for y in range(h): # y varia da 0 a h-1 231 | for x in range(w): # x varia da 0 a w-1 232 | pixel_position = (x + y * w)*4 if metadata['alpha'] else (x + y * w)*3 233 | matrice[y][x] = int(pixels[pixel_position]) 234 | 235 | elif self.options.grayscale_type == 4: 236 | #G 237 | for y in range(h): # y varia da 0 a h-1 238 | for x in range(w): # x varia da 0 a w-1 239 | pixel_position = (x + y * w)*4 if metadata['alpha'] else (x + y * w)*3 240 | matrice[y][x] = int(pixels[(pixel_position+1)]) 241 | 242 | elif self.options.grayscale_type == 5: 243 | #B 244 | for y in range(h): # y varia da 0 a h-1 245 | for x in range(w): # x varia da 0 a w-1 246 | pixel_position = (x + y * w)*4 if metadata['alpha'] else (x + y * w)*3 247 | matrice[y][x] = int(pixels[(pixel_position+2)]) 248 | 249 | elif self.options.grayscale_type == 6: 250 | #Max Color 251 | for y in range(h): # y varia da 0 a h-1 252 | for x in range(w): # x varia da 0 a w-1 253 | pixel_position = (x + y * w)*4 if metadata['alpha'] else (x + y * w)*3 254 | list_RGB = pixels[pixel_position] , pixels[(pixel_position+1)] , pixels[(pixel_position+2)] 255 | matrice[y][x] = int(max(list_RGB)) 256 | 257 | else: 258 | #Min Color 259 | for y in range(h): # y varia da 0 a h-1 260 | for x in range(w): # x varia da 0 a w-1 261 | pixel_position = (x + y * w)*4 if metadata['alpha'] else (x + y * w)*3 262 | list_RGB = pixels[pixel_position] , pixels[(pixel_position+1)] , pixels[(pixel_position+2)] 263 | matrice[y][x] = int(min(list_RGB)) 264 | 265 | 266 | ####Ora matrice contiene l'immagine in scala di grigi 267 | 268 | 269 | ######## GENERO IMMAGINE IN BIANCO E NERO ######## 270 | #Scorro matrice e genero matrice_BN 271 | B=255 272 | N=0 273 | 274 | matrice_BN = [[255 for i in range(w)]for j in range(h)] 275 | 276 | 277 | if self.options.conversion_type == 1: 278 | #B/W fixed threshold 279 | soglia = self.options.BW_threshold 280 | for y in range(h): 281 | for x in range(w): 282 | if matrice[y][x] >= soglia : 283 | matrice_BN[y][x] = B 284 | else: 285 | matrice_BN[y][x] = N 286 | 287 | 288 | elif self.options.conversion_type == 2: 289 | #B/W random threshold 290 | from random import randint 291 | for y in range(h): 292 | for x in range(w): 293 | soglia = randint(20,235) 294 | if matrice[y][x] >= soglia : 295 | matrice_BN[y][x] = B 296 | else: 297 | matrice_BN[y][x] = N 298 | 299 | 300 | elif self.options.conversion_type == 3: 301 | #Halftone 302 | Step1 = [[B,B,B,B,B],[B,B,B,B,B],[B,B,N,B,B],[B,B,B,B,B],[B,B,B,B,B]] 303 | Step2 = [[B,B,B,B,B],[B,B,N,B,B],[B,N,N,N,B],[B,B,N,B,B],[B,B,B,B,B]] 304 | Step3 = [[B,B,N,B,B],[B,N,N,N,B],[N,N,N,N,N],[B,N,N,N,B],[B,B,N,B,B]] 305 | Step4 = [[B,N,N,N,B],[N,N,N,N,N],[N,N,N,N,N],[N,N,N,N,N],[B,N,N,N,B]] 306 | 307 | for y in range(h/5): 308 | for x in range(w/5): 309 | media = 0 310 | for y2 in range(5): 311 | for x2 in range(5): 312 | media += matrice[y*5+y2][x*5+x2] 313 | media = media /25 314 | for y3 in range(5): 315 | for x3 in range(5): 316 | if media >= 250 and media <= 255: 317 | matrice_BN[y*5+y3][x*5+x3] = B 318 | if media >= 190 and media < 250: 319 | matrice_BN[y*5+y3][x*5+x3] = Step1[y3][x3] 320 | if media >= 130 and media < 190: 321 | matrice_BN[y*5+y3][x*5+x3] = Step2[y3][x3] 322 | if media >= 70 and media < 130: 323 | matrice_BN[y*5+y3][x*5+x3] = Step3[y3][x3] 324 | if media >= 10 and media < 70: 325 | matrice_BN[y*5+y3][x*5+x3] = Step4[y3][x3] 326 | if media >= 0 and media < 10: 327 | matrice_BN[y*5+y3][x*5+x3] = N 328 | 329 | 330 | elif self.options.conversion_type == 4: 331 | #Halftone row 332 | Step1r = [B,B,N,B,B] 333 | Step2r = [B,N,N,B,B] 334 | Step3r = [B,N,N,N,B] 335 | Step4r = [N,N,N,N,B] 336 | 337 | for y in range(h): 338 | for x in range(w/5): 339 | media = 0 340 | for x2 in range(5): 341 | media += matrice[y][x*5+x2] 342 | media = media /5 343 | for x3 in range(5): 344 | if media >= 250 and media <= 255: 345 | matrice_BN[y][x*5+x3] = B 346 | if media >= 190 and media < 250: 347 | matrice_BN[y][x*5+x3] = Step1r[x3] 348 | if media >= 130 and media < 190: 349 | matrice_BN[y][x*5+x3] = Step2r[x3] 350 | if media >= 70 and media < 130: 351 | matrice_BN[y][x*5+x3] = Step3r[x3] 352 | if media >= 10 and media < 70: 353 | matrice_BN[y][x*5+x3] = Step4r[x3] 354 | if media >= 0 and media < 10: 355 | matrice_BN[y][x*5+x3] = N 356 | 357 | 358 | elif self.options.conversion_type == 5: 359 | #Halftone column 360 | Step1c = [B,B,N,B,B] 361 | Step2c = [B,N,N,B,B] 362 | Step3c = [B,N,N,N,B] 363 | Step4c = [N,N,N,N,B] 364 | 365 | for y in range(h/5): 366 | for x in range(w): 367 | media = 0 368 | for y2 in range(5): 369 | media += matrice[y*5+y2][x] 370 | media = media /5 371 | for y3 in range(5): 372 | if media >= 250 and media <= 255: 373 | matrice_BN[y*5+y3][x] = B 374 | if media >= 190 and media < 250: 375 | matrice_BN[y*5+y3][x] = Step1c[y3] 376 | if media >= 130 and media < 190: 377 | matrice_BN[y*5+y3][x] = Step2c[y3] 378 | if media >= 70 and media < 130: 379 | matrice_BN[y*5+y3][x] = Step3c[y3] 380 | if media >= 10 and media < 70: 381 | matrice_BN[y*5+y3][x] = Step4c[y3] 382 | if media >= 0 and media < 10: 383 | matrice_BN[y*5+y3][x] = N 384 | 385 | else: 386 | #Grayscale 387 | if self.options.grayscale_resolution == 1: 388 | matrice_BN = matrice 389 | else: 390 | for y in range(h): 391 | for x in range(w): 392 | if matrice[y][x] <= 1: 393 | matrice_BN[y][x] = 0 394 | 395 | if matrice[y][x] >= 254: 396 | matrice_BN[y][x] = 255 397 | 398 | if matrice[y][x] > 1 and matrice[y][x] <254: 399 | matrice_BN[y][x] = ( matrice[y][x] // self.options.grayscale_resolution ) * self.options.grayscale_resolution 400 | 401 | 402 | 403 | ####Ora matrice_BN contiene l'immagine in Bianco (255) e Nero (0) 404 | 405 | 406 | #### SALVO IMMAGINE IN BIANCO E NERO #### 407 | file_img_BN = open(pos_file_png_BW, 'wb') #Creo il file 408 | Costruttore_img = png.Writer(w, h, greyscale=True, bitdepth=8) #Impostazione del file immagine 409 | Costruttore_img.write(file_img_BN, matrice_BN) #Costruttore del file immagine 410 | file_img_BN.close() #Chiudo il file 411 | 412 | 413 | #### GENERO IL FILE GCODE #### 414 | if self.options.preview_only == False: #Genero Gcode solo se devo 415 | 416 | if self.options.flip_y == False: #Inverto asse Y solo se flip_y = False 417 | #-> coordinate Cartesiane (False) Coordinate "informatiche" (True) 418 | matrice_BN.reverse() 419 | 420 | 421 | Laser_ON = False 422 | F_G01 = self.options.speed_ON 423 | Scala = self.options.resolution 424 | 425 | file_gcode = open(pos_file_gcode, 'w') #Creo il file 426 | 427 | #Configurazioni iniziali standard Gcode 428 | file_gcode.write('; Generated with:\n; "Raster 2 Laser Gcode generator"\n; by 305 Engineering\n;\n;\n;\n') 429 | #HOMING 430 | if self.options.homing == 1: 431 | file_gcode.write('G28; home all axes\n') 432 | elif self.options.homing == 2: 433 | file_gcode.write('$H; home all axes\n') 434 | else: 435 | pass 436 | file_gcode.write('G21; Set units to millimeters\n') 437 | file_gcode.write('G90; Use absolute coordinates\n') 438 | file_gcode.write('G92; Coordinate Offset\n') 439 | 440 | #Creazione del Gcode 441 | 442 | #allargo la matrice per lavorare su tutta l'immagine 443 | for y in range(h): 444 | matrice_BN[y].append(B) 445 | w = w+1 446 | 447 | if self.options.conversion_type != 6: 448 | for y in range(h): 449 | if y % 2 == 0 : 450 | for x in range(w): 451 | if matrice_BN[y][x] == N : 452 | if Laser_ON == False : 453 | #file_gcode.write('G00 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) + ' F' + str(F_G00) + '\n') 454 | file_gcode.write('G00 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) + '\n') #tolto il Feed sul G00 455 | file_gcode.write(self.options.laseron + '\n') 456 | Laser_ON = True 457 | if Laser_ON == True : #DEVO evitare di uscire dalla matrice 458 | if x == w-1 : 459 | file_gcode.write('G01 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) +' F' + str(F_G01) + '\n') 460 | file_gcode.write(self.options.laseroff + '\n') 461 | Laser_ON = False 462 | else: 463 | if matrice_BN[y][x+1] != N : 464 | file_gcode.write('G01 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) + ' F' + str(F_G01) +'\n') 465 | file_gcode.write(self.options.laseroff + '\n') 466 | Laser_ON = False 467 | else: 468 | for x in reversed(range(w)): 469 | if matrice_BN[y][x] == N : 470 | if Laser_ON == False : 471 | #file_gcode.write('G00 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) + ' F' + str(F_G00) + '\n') 472 | file_gcode.write('G00 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) + '\n') #tolto il Feed sul G00 473 | file_gcode.write(self.options.laseron + '\n') 474 | Laser_ON = True 475 | if Laser_ON == True : #DEVO evitare di uscire dalla matrice 476 | if x == 0 : 477 | file_gcode.write('G01 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) +' F' + str(F_G01) + '\n') 478 | file_gcode.write(self.options.laseroff + '\n') 479 | Laser_ON = False 480 | else: 481 | if matrice_BN[y][x-1] != N : 482 | file_gcode.write('G01 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) + ' F' + str(F_G01) +'\n') 483 | file_gcode.write(self.options.laseroff + '\n') 484 | Laser_ON = False 485 | 486 | else: ##SCALA DI GRIGI 487 | for y in range(h): 488 | if y % 2 == 0 : 489 | for x in range(w): 490 | if matrice_BN[y][x] != B : 491 | if Laser_ON == False : 492 | file_gcode.write('G00 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) +'\n') 493 | file_gcode.write(self.options.laseron + ' '+ ' S' + str(255 - matrice_BN[y][x]) +'\n') 494 | Laser_ON = True 495 | 496 | if Laser_ON == True : #DEVO evitare di uscire dalla matrice 497 | if x == w-1 : #controllo fine riga 498 | file_gcode.write('G01 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) +' F' + str(F_G01) + '\n') 499 | file_gcode.write(self.options.laseroff + '\n') 500 | Laser_ON = False 501 | 502 | else: 503 | if matrice_BN[y][x+1] == B : 504 | file_gcode.write('G01 X' + str(float(x+1)/Scala) + ' Y' + str(float(y)/Scala) + ' F' + str(F_G01) +'\n') 505 | file_gcode.write(self.options.laseroff + '\n') 506 | Laser_ON = False 507 | 508 | elif matrice_BN[y][x] != matrice_BN[y][x+1] : 509 | file_gcode.write('G01 X' + str(float(x+1)/Scala) + ' Y' + str(float(y)/Scala) + ' F' + str(F_G01) +'\n') 510 | file_gcode.write(self.options.laseron + ' '+ ' S' + str(255 - matrice_BN[y][x+1]) +'\n') 511 | 512 | 513 | else: 514 | for x in reversed(range(w)): 515 | if matrice_BN[y][x] != B : 516 | if Laser_ON == False : 517 | file_gcode.write('G00 X' + str(float(x+1)/Scala) + ' Y' + str(float(y)/Scala) +'\n') 518 | file_gcode.write(self.options.laseron + ' '+ ' S' + str(255 - matrice_BN[y][x]) +'\n') 519 | Laser_ON = True 520 | 521 | if Laser_ON == True : #DEVO evitare di uscire dalla matrice 522 | if x == 0 : #controllo fine riga ritorno 523 | file_gcode.write('G01 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) +' F' + str(F_G01) + '\n') 524 | file_gcode.write(self.options.laseroff + '\n') 525 | Laser_ON = False 526 | 527 | else: 528 | if matrice_BN[y][x-1] == B : 529 | file_gcode.write('G01 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) + ' F' + str(F_G01) +'\n') 530 | file_gcode.write(self.options.laseroff + '\n') 531 | Laser_ON = False 532 | 533 | elif matrice_BN[y][x] != matrice_BN[y][x-1] : 534 | file_gcode.write('G01 X' + str(float(x)/Scala) + ' Y' + str(float(y)/Scala) + ' F' + str(F_G01) +'\n') 535 | file_gcode.write(self.options.laseron + ' '+ ' S' + str(255 - matrice_BN[y][x-1]) +'\n') 536 | 537 | 538 | 539 | #Configurazioni finali standard Gcode 540 | file_gcode.write('G00 X0 Y0; home\n') 541 | #HOMING 542 | if self.options.homing == 1: 543 | file_gcode.write('G28; home all axes\n') 544 | elif self.options.homing == 2: 545 | file_gcode.write('$H; home all axes\n') 546 | else: 547 | pass 548 | 549 | file_gcode.close() #Chiudo il file 550 | 551 | 552 | 553 | 554 | ######## ######## ######## ######## ######## ######## ######## ######## ######## 555 | 556 | 557 | def _main(): 558 | e=GcodeExport() 559 | e.affect() 560 | 561 | exit() 562 | 563 | if __name__=="__main__": 564 | _main() 565 | 566 | 567 | 568 | 569 | --------------------------------------------------------------------------------