-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Changes from all commits
25bc5c7
be26453
1c0d1c5
36a22e2
c5d32d2
cac235b
e44773c
98b19a9
38c6771
8840f2b
069ad8c
949932f
7fe29ec
f13b335
b668db3
c2cd5fe
38f43c1
40a94d4
d70de6e
31c1a65
3828330
d718d35
184d4f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -208,6 +208,8 @@ def isImageType(t): | |
SAVE = {} | ||
SAVE_ALL = {} | ||
EXTENSION = {} | ||
DECODERS = {} | ||
ENCODERS = {} | ||
|
||
# -------------------------------------------------------------------- | ||
# Modes supported by this version | ||
|
@@ -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") | ||
|
@@ -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) | ||
except KeyError: | ||
pass | ||
try: | ||
# get encoder | ||
encoder = getattr(core, encoder_name + "_encoder") | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"") | ||
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency, name this There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency, name this There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line isn't covered by tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line isn't covered by tests. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,5 @@ | ||
# | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
# | ||
|
@@ -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" | ||
|
||
|
@@ -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) | ||
|
@@ -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 | ||
|
||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.