mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-04 21:50:54 +03:00
Added support for PPM arbitrary maxval in plain formats
This commit is contained in:
parent
5051a29a4e
commit
c4d51fb268
|
@ -22,6 +22,21 @@ def test_sanity():
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"data, mode, pixels",
|
"data, mode, pixels",
|
||||||
(
|
(
|
||||||
|
(b"P2 3 1 4 0 2 4", "L", (0, 128, 255)),
|
||||||
|
(b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)),
|
||||||
|
# P3 with maxval < 255
|
||||||
|
(
|
||||||
|
b"P3 3 1 17 0 1 2 8 9 10 15 16 17",
|
||||||
|
"RGB",
|
||||||
|
((0, 15, 30), (120, 135, 150), (225, 240, 255)),
|
||||||
|
),
|
||||||
|
# P3 with maxval > 255
|
||||||
|
# Scale down to 255, since there is no RGB mode with more than 8-bit
|
||||||
|
(
|
||||||
|
b"P3 3 1 257 0 1 2 128 129 130 256 257 257",
|
||||||
|
"RGB",
|
||||||
|
((0, 1, 2), (127, 128, 129), (254, 255, 255)),
|
||||||
|
),
|
||||||
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
|
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
|
||||||
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
|
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
|
||||||
# P6 with maxval < 255
|
# P6 with maxval < 255
|
||||||
|
@ -35,7 +50,6 @@ def test_sanity():
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
# P6 with maxval > 255
|
# P6 with maxval > 255
|
||||||
# Scale down to 255, since there is no RGB mode with more than 8-bit
|
|
||||||
(
|
(
|
||||||
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
|
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
|
||||||
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
|
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
|
||||||
|
|
|
@ -105,6 +105,8 @@ class PpmImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
maxval = None
|
maxval = None
|
||||||
decoder_name = "raw"
|
decoder_name = "raw"
|
||||||
|
if magic_number in (b"P1", b"P2", b"P3"):
|
||||||
|
decoder_name = "ppm_plain"
|
||||||
for ix in range(3):
|
for ix in range(3):
|
||||||
token = int(self._read_token())
|
token = int(self._read_token())
|
||||||
if ix == 0: # token is the x size
|
if ix == 0: # token is the x size
|
||||||
|
@ -126,14 +128,13 @@ class PpmImageFile(ImageFile.ImageFile):
|
||||||
if maxval > 255 and mode == "L":
|
if maxval > 255 and mode == "L":
|
||||||
self.mode = "I"
|
self.mode = "I"
|
||||||
|
|
||||||
|
if decoder_name != "ppm_plain":
|
||||||
# If maxval matches a bit depth, use the raw decoder directly
|
# If maxval matches a bit depth, use the raw decoder directly
|
||||||
if maxval == 65535 and mode == "L":
|
if maxval == 65535 and mode == "L":
|
||||||
rawmode = "I;16B"
|
rawmode = "I;16B"
|
||||||
elif maxval != 255:
|
elif maxval != 255:
|
||||||
decoder_name = "ppm"
|
decoder_name = "ppm"
|
||||||
|
|
||||||
if magic_number in (b"P1", b"P2", b"P3"):
|
|
||||||
decoder_name = "ppm_plain"
|
|
||||||
args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
|
args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
|
||||||
self._size = xsize, ysize
|
self._size = xsize, ysize
|
||||||
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
|
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
|
||||||
|
@ -156,7 +157,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
|
||||||
|
|
||||||
def _ignore_comments(self, block):
|
def _ignore_comments(self, block):
|
||||||
"""
|
"""
|
||||||
Deletes comments from block.
|
Delete comments from block.
|
||||||
If comment does not end in this block, raises a flag.
|
If comment does not end in this block, raises a flag.
|
||||||
"""
|
"""
|
||||||
comment_spans = False
|
comment_spans = False
|
||||||
|
@ -176,14 +177,14 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
|
||||||
|
|
||||||
def _decode_bitonal(self):
|
def _decode_bitonal(self):
|
||||||
"""
|
"""
|
||||||
This is a separate method because the plain PBM format all data tokens
|
This is a separate method because in the plain PBM format, all data tokens are
|
||||||
are exactly one byte, and so the inter-token whitespace is optional.
|
exactly one byte, so the inter-token whitespace is optional.
|
||||||
"""
|
"""
|
||||||
decoded_data = bytearray()
|
data = bytearray()
|
||||||
total_bytes = self.state.xsize * self.state.ysize
|
total_bytes = self.state.xsize * self.state.ysize
|
||||||
|
|
||||||
comment_spans = False
|
comment_spans = False
|
||||||
while len(decoded_data) != total_bytes:
|
while len(data) != total_bytes:
|
||||||
block = self._read_block() # read next block
|
block = self._read_block() # read next block
|
||||||
if not block:
|
if not block:
|
||||||
# eof
|
# eof
|
||||||
|
@ -193,7 +194,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
|
||||||
comment_end = self._find_comment_end(block)
|
comment_end = self._find_comment_end(block)
|
||||||
if comment_end != -1: # comment ends in this block
|
if comment_end != -1: # comment ends in this block
|
||||||
block = block[comment_end + 1 :] # delete tail of previous comment
|
block = block[comment_end + 1 :] # delete tail of previous comment
|
||||||
comment_spans = False
|
break
|
||||||
else: # comment spans whole block
|
else: # comment spans whole block
|
||||||
block = self._read_block()
|
block = self._read_block()
|
||||||
|
|
||||||
|
@ -203,19 +204,21 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if token not in (48, 49):
|
if token not in (48, 49):
|
||||||
raise ValueError(f"Invalid token for this mode: {bytes([token])}")
|
raise ValueError(f"Invalid token for this mode: {bytes([token])}")
|
||||||
decoded_data = (decoded_data + tokens)[:total_bytes]
|
data = (data + tokens)[:total_bytes]
|
||||||
invert = bytes.maketrans(b"01", b"\xFF\x00")
|
invert = bytes.maketrans(b"01", b"\xFF\x00")
|
||||||
return decoded_data.translate(invert)
|
return data.translate(invert)
|
||||||
|
|
||||||
def _decode_blocks(self, channels, depth, maxval):
|
def _decode_blocks(self, maxval):
|
||||||
decoded_data = bytearray()
|
data = bytearray()
|
||||||
max_len = 10
|
max_len = 10
|
||||||
bytes_per_sample = depth // 8
|
out_byte_count = 4 if self.mode == "I" else 1
|
||||||
total_bytes = self.state.xsize * self.state.ysize * channels * bytes_per_sample
|
out_max = 65535 if self.mode == "I" else 255
|
||||||
|
bands = Image.getmodebands(self.mode)
|
||||||
|
total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count
|
||||||
|
|
||||||
comment_spans = False
|
comment_spans = False
|
||||||
half_token = False
|
half_token = False
|
||||||
while len(decoded_data) != total_bytes:
|
while len(data) != total_bytes:
|
||||||
block = self._read_block() # read next block
|
block = self._read_block() # read next block
|
||||||
if not block:
|
if not block:
|
||||||
if half_token:
|
if half_token:
|
||||||
|
@ -251,31 +254,24 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Token too long found in data: {token[:max_len + 1]}"
|
f"Token too long found in data: {token[:max_len + 1]}"
|
||||||
)
|
)
|
||||||
token = int(token)
|
value = int(token)
|
||||||
if token > maxval:
|
if value > maxval:
|
||||||
raise ValueError(f"Channel value too large for this mode: {token}")
|
raise ValueError(f"Channel value too large for this mode: {value}")
|
||||||
decoded_data += token.to_bytes(bytes_per_sample, "big")
|
value = round(value / maxval * out_max)
|
||||||
if len(decoded_data) == total_bytes: # finished!
|
data += o32(value) if self.mode == "I" else o8(value)
|
||||||
|
if len(data) == total_bytes: # finished!
|
||||||
break
|
break
|
||||||
return decoded_data
|
return data
|
||||||
|
|
||||||
def decode(self, buffer):
|
def decode(self, buffer):
|
||||||
rawmode, maxval = self.args
|
|
||||||
|
|
||||||
if self.mode == "1":
|
if self.mode == "1":
|
||||||
decoded_data = self._decode_bitonal()
|
data = self._decode_bitonal()
|
||||||
rawmode = "1;8"
|
rawmode = "1;8"
|
||||||
elif self.mode == "L":
|
else:
|
||||||
decoded_data = self._decode_blocks(1, 8, maxval)
|
maxval = self.args[-1]
|
||||||
elif self.mode == "I":
|
data = self._decode_blocks(maxval)
|
||||||
if rawmode == "I;16B":
|
rawmode = "I;32" if self.mode == "I" else self.mode
|
||||||
decoded_data = self._decode_blocks(1, 16, maxval)
|
self.set_as_raw(bytes(data), rawmode)
|
||||||
elif rawmode == "I;32B":
|
|
||||||
decoded_data = self._decode_blocks(1, 32, maxval)
|
|
||||||
elif self.mode == "RGB":
|
|
||||||
decoded_data = self._decode_blocks(3, 8, maxval)
|
|
||||||
|
|
||||||
self.set_as_raw(bytes(decoded_data), rawmode)
|
|
||||||
return -1, 0
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
|
@ -284,7 +280,7 @@ class PpmDecoder(ImageFile.PyDecoder):
|
||||||
|
|
||||||
def decode(self, buffer):
|
def decode(self, buffer):
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
maxval = min(self.args[-1], 65535)
|
maxval = self.args[-1]
|
||||||
in_byte_count = 1 if maxval < 256 else 2
|
in_byte_count = 1 if maxval < 256 else 2
|
||||||
out_byte_count = 4 if self.mode == "I" else 1
|
out_byte_count = 4 if self.mode == "I" else 1
|
||||||
out_max = 65535 if self.mode == "I" else 255
|
out_max = 65535 if self.mode == "I" else 255
|
||||||
|
@ -301,7 +297,7 @@ class PpmDecoder(ImageFile.PyDecoder):
|
||||||
value = min(out_max, round(value / maxval * out_max))
|
value = min(out_max, round(value / maxval * out_max))
|
||||||
data += o32(value) if self.mode == "I" else o8(value)
|
data += o32(value) if self.mode == "I" else o8(value)
|
||||||
rawmode = "I;32" if self.mode == "I" else self.mode
|
rawmode = "I;32" if self.mode == "I" else self.mode
|
||||||
self.set_as_raw(bytes(data), (rawmode, 0, 1))
|
self.set_as_raw(bytes(data), rawmode)
|
||||||
return -1, 0
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
|
@ -337,11 +333,12 @@ def _save(im, fp, filename):
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
Image.register_decoder("ppm_plain", PpmPlainDecoder)
|
|
||||||
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
|
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
|
||||||
Image.register_save(PpmImageFile.format, _save)
|
Image.register_save(PpmImageFile.format, _save)
|
||||||
|
|
||||||
Image.register_decoder("ppm", PpmDecoder)
|
Image.register_decoder("ppm", PpmDecoder)
|
||||||
|
Image.register_decoder("ppm_plain", PpmPlainDecoder)
|
||||||
|
|
||||||
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
|
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user