Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Pure Python Decoder objects #1938

Merged
merged 23 commits into from
Mar 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ def isImageType(t):
SAVE = {}
SAVE_ALL = {}
EXTENSION = {}
DECODERS = {}
ENCODERS = {}

# --------------------------------------------------------------------
# Modes supported by this version
Expand Down Expand Up @@ -413,6 +415,11 @@ def _getdecoder(mode, decoder_name, args, extra=()):
elif not isinstance(args, tuple):
args = (args,)

try:
decoder = DECODERS[decoder_name]
return decoder(mode, *args + extra)
except KeyError:
pass
try:
# get decoder
decoder = getattr(core, decoder_name + "_decoder")
Expand All @@ -430,6 +437,11 @@ def _getencoder(mode, encoder_name, args, extra=()):
elif not isinstance(args, tuple):
args = (args,)

try:
encoder = ENCODERS[encoder_name]
return encoder(mode, *args + extra)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line isn't covered by tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we don't have a pure python encoder yet.

except KeyError:
pass
try:
# get encoder
encoder = getattr(core, encoder_name + "_encoder")
Expand Down Expand Up @@ -2526,6 +2538,33 @@ def registered_extensions():
init()
return EXTENSION

def register_decoder(name, decoder):
"""
Registers an image decoder. This function should not be
used in application code.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this function shouldn't be used in application code, should it begin with an underscore?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scroll up. This is the same as register_extension/save/open/mime/foo. It's not for application code, it's for plugin developers.


:param name: The name of the decoder
:param decoder: A callable(mode, args) that returns an
ImageFile.PyDecoder object

.. versionadded:: 4.1.0
"""
DECODERS[name] = decoder


def register_encoder(name, encoder):
"""
Registers an image encoder. This function should not be
used in application code.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this function shouldn't be used in application code, should it begin with an underscore?


:param name: The name of the encoder
:param encoder: A callable(mode, args) that returns an
ImageFile.PyEncoder object

.. versionadded:: 4.1.0
"""
ENCODERS[name] = encoder
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line isn't covered by tests.



# --------------------------------------------------------------------
# Simple display support. User code may override this.
Expand Down
130 changes: 126 additions & 4 deletions PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,7 @@ def load(self):
decoder = Image._getdecoder(self.mode, decoder_name,
args, self.decoderconfig)
seek(offset)
try:
decoder.setimage(self.im, extents)
except ValueError:
continue
decoder.setimage(self.im, extents)
if decoder.pulls_fd:
decoder.setfd(self.fp)
status, err_code = decoder.decode(b"")
Expand Down Expand Up @@ -520,3 +517,128 @@ def _safe_read(fp, size):
data.append(block)
size -= len(block)
return b"".join(data)


class PyCodecState(object):
def __init__(self):
self.xsize = 0
self.ysize = 0
self.xoff = 0
self.yoff = 0

def extents(self):
return (self.xoff, self.yoff,
self.xoff+self.xsize, self.yoff+self.ysize)

class PyDecoder(object):
"""
Python implementation of a format decoder. Override this class and
add the decoding logic in the `decode` method.

See :ref:`Writing Your Own File Decoder in Python<file-decoders-py>`
"""

_pulls_fd = False

def __init__(self, mode, *args):
self.im = None
self.state = PyCodecState()
self.fd = None
self.mode = mode
self.init(args)

def init(self, args):
"""
Override to perform decoder specific initialization

:param args: Array of args items from the tile entry
:returns: None
"""
self.args = args

@property
def pulls_fd(self):
return self._pulls_fd

def decode(self, buffer):
"""
Override to perform the decoding process.

:param buffer: A bytes object with the data to be decoded. If `handles_eof`
is set, then `buffer` will be empty and `self.fd` will be set.
:returns: A tuple of (bytes consumed, errcode). If finished with decoding
return <0 for the bytes consumed. Err codes are from `ERRORS`
"""
raise NotImplementedError()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line isn't covered by tests.


def cleanup(self):
"""
Override to perform decoder specific cleanup

:returns: None
"""
pass

def setfd(self, fd):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, name this set_fd

Copy link
Member Author

@wiredfool wiredfool Feb 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the name of the method on the C decoder object, which we're ducktyping.

"""
Called from ImageFile to set the python file-like object

:param fd: A python file-like object
:returns: None
"""
self.fd = fd

def setimage(self, im, extents=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, name this set_image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, that's the name of the method in the C decoder object.

"""
Called from ImageFile to set the core output image for the decoder

:param im: A core image object
:param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle
for this tile
:returns: None
"""

# following c code
self.im = im

if extents:
(x0, y0, x1, y1) = extents
else:
(x0, y0, x1, y1) = (0, 0, 0, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line isn't covered by tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is, essentially, translated from the C implementation. We've currently only got one decoder implementation that's using it, and that implementation doesn't use any of the more advanced tile based functionality.



if x0 == 0 and x1 == 0:
self.state.xsize, self.state.ysize = self.im.size
else:
self.state.xoff = x0
self.state.yoff = y0
self.state.xsize = x1 - x0
self.state.ysize = y1 - y0

if self.state.xsize <= 0 or self.state.ysize <= 0:
raise ValueError("Size cannot be negative")

if (self.state.xsize + self.state.xoff > self.im.size[0] or
self.state.ysize + self.state.yoff > self.im.size[1]):
raise ValueError("Tile cannot extend outside image")

def set_as_raw(self, data, rawmode=None):
"""
Convenience method to set the internal image from a stream of raw data

:param data: Bytes to be set
:param rawmode: The rawmode to be used for the decoder. If not specified,
it will default to the mode of the image
:returns: None
"""

if not rawmode:
rawmode = self.mode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line isn't covered by tests.

d = Image._getdecoder(self.mode, 'raw', (rawmode))
d.setimage(self.im, self.state.extents())
s = d.decode(data)

if s[0] >= 0:
raise ValueError("not enough image data")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line isn't covered by tests.

if s[1] != 0:
raise ValueError("cannot decode image data")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line isn't covered by tests.

99 changes: 95 additions & 4 deletions PIL/MspImagePlugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains tabs and spaces. Let's remove tabs.

# The Python Imaging Library.
# $Id$
#
# MSP file handling
#
Expand All @@ -9,16 +8,24 @@
# History:
# 95-09-05 fl Created
# 97-01-03 fl Read/write MSP images
# 17-02-21 es Fixed RLE interpretation
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1995-97.
# Copyright (c) Eric Soroos 2017.
#
# See the README file for information on usage and redistribution.
#

# More info on this format: https://archive.org/details/gg243631
# Page 313:
# Figure 205. Windows Paint Version 1: "DanM" Format
# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
#
# See also: http://www.fileformat.info/format/mspaint/egff.htm

from . import Image, ImageFile
from ._binary import i16le as i16, o16le as o16
from ._binary import i16le as i16, o16le as o16, i8
import struct, io

__version__ = "0.1"

Expand Down Expand Up @@ -60,7 +67,90 @@ def _open(self):
if s[:4] == b"DanM":
self.tile = [("raw", (0, 0)+self.size, 32, ("1", 0, 1))]
else:
self.tile = [("msp", (0, 0)+self.size, 32+2*self.size[1], None)]
self.tile = [("MSP", (0, 0)+self.size, 32, None)]


class MspDecoder(ImageFile.PyDecoder):
# The algo for the MSP decoder is from
# http://www.fileformat.info/format/mspaint/egff.htm
# cc-by-attribution -- That page references is taken from the
# Encyclopedia of Graphics File Formats and is licensed by
# O'Reilly under the Creative Common/Attribution license
#
# For RLE encoded files, the 32byte header is followed by a scan
# line map, encoded as one 16bit word of encoded byte length per
# line.
#
# NOTE: the encoded length of the line can be 0. This was not
# handled in the previous version of this encoder, and there's no
# mention of how to handle it in the documentation. From the few
# examples I've seen, I've assumed that it is a fill of the
# background color, in this case, white.
#
#
# Pseudocode of the decoder:
# Read a BYTE value as the RunType
# If the RunType value is zero
# Read next byte as the RunCount
# Read the next byte as the RunValue
# Write the RunValue byte RunCount times
# If the RunType value is non-zero
# Use this value as the RunCount
# Read and write the next RunCount bytes literally
#
# e.g.:
# 0x00 03 ff 05 00 01 02 03 04
# would yield the bytes:
# 0xff ff ff 00 01 02 03 04
#
# which are then interpreted as a bit packed mode '1' image


_pulls_fd = True

def decode(self, buffer):

img = io.BytesIO()
blank_line = bytearray((0xff,)*((self.state.xsize+7)//8))
try:
self.fd.seek(32)
rowmap = struct.unpack_from("<%dH" % (self.state.ysize),
self.fd.read(self.state.ysize*2))
except struct.error:
raise IOError("Truncated MSP file in row map")

for x, rowlen in enumerate(rowmap):
try:
if rowlen == 0:
img.write(blank_line)
continue
row = self.fd.read(rowlen)
if len(row) != rowlen:
raise IOError("Truncated MSP file, expected %d bytes on row %s",
(rowlen, x))
idx = 0
while idx < rowlen:
runtype = i8(row[idx])
idx += 1
if runtype == 0:
(runcount, runval) = struct.unpack("Bc", row[idx:idx+2])
img.write(runval * runcount)
idx += 2
else:
runcount = runtype
img.write(row[idx:idx+runcount])
idx += runcount

except struct.error:
raise IOError("Corrupted MSP file in row %d" %x)

self.set_as_raw(img.getvalue(), ("1", 0, 1))

return 0,0


Image.register_decoder('MSP', MspDecoder)


#
# write MSP files (uncompressed only)
Expand Down Expand Up @@ -92,6 +182,7 @@ def _save(im, fp, filename):
# image body
ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 32, ("1", 0, 1))])


#
# registry

Expand Down
Binary file added Tests/images/hopper_bad_checksum.msp
Binary file not shown.
Loading