Merge pull request #8281 from Yay295/eps_test
BIN
Tests/images/eps/1.bmp
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
Tests/images/eps/1_boundingbox_after_imagedata.eps
Normal file
BIN
Tests/images/eps/1_second_imagedata.eps
Normal file
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
@ -8,6 +8,7 @@ import pytest
|
|||
from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal_tofile,
|
||||
assert_image_similar,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
|
@ -19,18 +20,18 @@ from .helper import (
|
|||
HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript()
|
||||
|
||||
# Our two EPS test files (they are identical except for their bounding boxes)
|
||||
FILE1 = "Tests/images/zero_bb.eps"
|
||||
FILE2 = "Tests/images/non_zero_bb.eps"
|
||||
FILE1 = "Tests/images/eps/zero_bb.eps"
|
||||
FILE2 = "Tests/images/eps/non_zero_bb.eps"
|
||||
|
||||
# Due to palletization, we'll need to convert these to RGB after load
|
||||
FILE1_COMPARE = "Tests/images/zero_bb.png"
|
||||
FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png"
|
||||
FILE1_COMPARE = "Tests/images/eps/zero_bb.png"
|
||||
FILE1_COMPARE_SCALE2 = "Tests/images/eps/zero_bb_scale2.png"
|
||||
|
||||
FILE2_COMPARE = "Tests/images/non_zero_bb.png"
|
||||
FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png"
|
||||
FILE2_COMPARE = "Tests/images/eps/non_zero_bb.png"
|
||||
FILE2_COMPARE_SCALE2 = "Tests/images/eps/non_zero_bb_scale2.png"
|
||||
|
||||
# EPS test files with binary preview
|
||||
FILE3 = "Tests/images/binary_preview_map.eps"
|
||||
FILE3 = "Tests/images/eps/binary_preview_map.eps"
|
||||
|
||||
# Three unsigned 32bit little-endian values:
|
||||
# 0xC6D3D0C5 magic number
|
||||
|
@ -126,6 +127,15 @@ def test_binary_header_only() -> None:
|
|||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_simple_eps_file(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file))
|
||||
with Image.open(data) as img:
|
||||
assert img.mode == "RGB"
|
||||
assert img.size == (100, 100)
|
||||
assert img.format == "EPS"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_missing_version_comment(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
|
||||
|
@ -141,23 +151,21 @@ def test_missing_boundingbox_comment(prefix: bytes) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_invalid_boundingbox_comment(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
|
||||
@pytest.mark.parametrize(
|
||||
"file_lines",
|
||||
(
|
||||
simple_eps_file_with_invalid_boundingbox,
|
||||
simple_eps_file_with_invalid_boundingbox_valid_imagedata,
|
||||
),
|
||||
)
|
||||
def test_invalid_boundingbox_comment(
|
||||
prefix: bytes, file_lines: tuple[bytes, ...]
|
||||
) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(file_lines))
|
||||
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None:
|
||||
data = io.BytesIO(
|
||||
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
|
||||
)
|
||||
with Image.open(data) as img:
|
||||
assert img.mode == "RGB"
|
||||
assert img.size == (100, 100)
|
||||
assert img.format == "EPS"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_ascii_comment_too_long(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
|
||||
|
@ -177,7 +185,7 @@ def test_load_long_binary_data(prefix: bytes) -> None:
|
|||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
|
||||
with Image.open(data) as img:
|
||||
img.load()
|
||||
assert img.mode == "RGB"
|
||||
assert img.mode == "1"
|
||||
assert img.size == (100, 100)
|
||||
assert img.format == "EPS"
|
||||
|
||||
|
@ -187,7 +195,7 @@ def test_load_long_binary_data(prefix: bytes) -> None:
|
|||
)
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_cmyk() -> None:
|
||||
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
|
||||
with Image.open("Tests/images/eps/pil_sample_cmyk.eps") as cmyk_image:
|
||||
assert cmyk_image.mode == "CMYK"
|
||||
assert cmyk_image.size == (100, 100)
|
||||
assert cmyk_image.format == "EPS"
|
||||
|
@ -204,8 +212,8 @@ def test_cmyk() -> None:
|
|||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_showpage() -> None:
|
||||
# See https://github.com/python-pillow/Pillow/issues/2615
|
||||
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
|
||||
with Image.open("Tests/images/reqd_showpage.png") as target:
|
||||
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
|
||||
with Image.open("Tests/images/eps/reqd_showpage.png") as target:
|
||||
# should not crash/hang
|
||||
plot_image.load()
|
||||
# fonts could be slightly different
|
||||
|
@ -214,11 +222,11 @@ def test_showpage() -> None:
|
|||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_transparency() -> None:
|
||||
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
|
||||
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
|
||||
plot_image.load(transparency=True)
|
||||
assert plot_image.mode == "RGBA"
|
||||
|
||||
with Image.open("Tests/images/reqd_showpage_transparency.png") as target:
|
||||
with Image.open("Tests/images/eps/reqd_showpage_transparency.png") as target:
|
||||
# fonts could be slightly different
|
||||
assert_image_similar(plot_image, target, 6)
|
||||
|
||||
|
@ -245,9 +253,19 @@ def test_bytesio_object() -> None:
|
|||
assert_image_similar(img, image1_scale1_compare, 5)
|
||||
|
||||
|
||||
def test_1_mode() -> None:
|
||||
with Image.open("Tests/images/1.eps") as im:
|
||||
assert im.mode == "1"
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
@pytest.mark.parametrize(
|
||||
# These images have an "ImageData" descriptor.
|
||||
"filename",
|
||||
(
|
||||
"Tests/images/eps/1.eps",
|
||||
"Tests/images/eps/1_boundingbox_after_imagedata.eps",
|
||||
"Tests/images/eps/1_second_imagedata.eps",
|
||||
),
|
||||
)
|
||||
def test_1(filename: str) -> None:
|
||||
with Image.open(filename) as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/eps/1.bmp")
|
||||
|
||||
|
||||
def test_image_mode_not_supported(tmp_path: Path) -> None:
|
||||
|
@ -302,7 +320,9 @@ def test_render_scale2() -> None:
|
|||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
|
||||
@pytest.mark.parametrize(
|
||||
"filename", (FILE1, FILE2, "Tests/images/eps/illu10_preview.eps")
|
||||
)
|
||||
def test_resize(filename: str) -> None:
|
||||
with Image.open(filename) as im:
|
||||
new_size = (100, 100)
|
||||
|
@ -344,10 +364,10 @@ def test_readline(prefix: bytes, line_ending: bytes) -> None:
|
|||
@pytest.mark.parametrize(
|
||||
"filename",
|
||||
(
|
||||
"Tests/images/illu10_no_preview.eps",
|
||||
"Tests/images/illu10_preview.eps",
|
||||
"Tests/images/illuCS6_no_preview.eps",
|
||||
"Tests/images/illuCS6_preview.eps",
|
||||
"Tests/images/eps/illu10_no_preview.eps",
|
||||
"Tests/images/eps/illu10_preview.eps",
|
||||
"Tests/images/eps/illuCS6_no_preview.eps",
|
||||
"Tests/images/eps/illuCS6_preview.eps",
|
||||
),
|
||||
)
|
||||
def test_open_eps(filename: str) -> None:
|
||||
|
@ -359,7 +379,7 @@ def test_open_eps(filename: str) -> None:
|
|||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_emptyline() -> None:
|
||||
# Test file includes an empty line in the header data
|
||||
emptyline_file = "Tests/images/zero_bb_emptyline.eps"
|
||||
emptyline_file = "Tests/images/eps/zero_bb_emptyline.eps"
|
||||
|
||||
with Image.open(emptyline_file) as image:
|
||||
image.load()
|
||||
|
@ -371,7 +391,7 @@ def test_emptyline() -> None:
|
|||
@pytest.mark.timeout(timeout=5)
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
|
||||
["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
|
||||
)
|
||||
def test_timeout(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
|
@ -384,7 +404,7 @@ def test_bounding_box_in_trailer() -> None:
|
|||
# Check bounding boxes are parsed in the same way
|
||||
# when specified in the header and the trailer
|
||||
with (
|
||||
Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image,
|
||||
Image.open("Tests/images/eps/zero_bb_trailer.eps") as trailer_image,
|
||||
Image.open(FILE1) as header_image,
|
||||
):
|
||||
assert trailer_image.size == header_image.size
|
||||
|
@ -392,12 +412,12 @@ def test_bounding_box_in_trailer() -> None:
|
|||
|
||||
def test_eof_before_bounding_box() -> None:
|
||||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
||||
with Image.open("Tests/images/eps/zero_bb_eof_before_boundingbox.eps"):
|
||||
pass
|
||||
|
||||
|
||||
def test_invalid_data_after_eof() -> None:
|
||||
with open("Tests/images/illuCS6_preview.eps", "rb") as f:
|
||||
with open("Tests/images/eps/illuCS6_preview.eps", "rb") as f:
|
||||
img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255))
|
||||
|
||||
with Image.open(img_bytes) as img:
|
||||
|
|
|
@ -56,10 +56,10 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
|
|||
),
|
||||
("Tests/images/hopper.tif", None),
|
||||
("Tests/images/test-card.png", None),
|
||||
("Tests/images/zero_bb.png", None),
|
||||
("Tests/images/zero_bb_scale2.png", None),
|
||||
("Tests/images/non_zero_bb.png", None),
|
||||
("Tests/images/non_zero_bb_scale2.png", None),
|
||||
("Tests/images/eps/zero_bb.png", None),
|
||||
("Tests/images/eps/zero_bb_scale2.png", None),
|
||||
("Tests/images/eps/non_zero_bb.png", None),
|
||||
("Tests/images/eps/non_zero_bb_scale2.png", None),
|
||||
("Tests/images/p_trns_single.png", None),
|
||||
("Tests/images/pil123p.png", None),
|
||||
("Tests/images/itxt_chunks.png", None),
|
||||
|
|
|
@ -121,7 +121,13 @@ def Ghostscript(
|
|||
lengthfile -= len(s)
|
||||
f.write(s)
|
||||
|
||||
device = "pngalpha" if transparency else "ppmraw"
|
||||
if transparency:
|
||||
# "RGBA"
|
||||
device = "pngalpha"
|
||||
else:
|
||||
# "pnmraw" automatically chooses between
|
||||
# PBM ("1"), PGM ("L"), and PPM ("RGB").
|
||||
device = "pnmraw"
|
||||
|
||||
# Build Ghostscript command
|
||||
command = [
|
||||
|
@ -151,8 +157,9 @@ def Ghostscript(
|
|||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
subprocess.check_call(command, startupinfo=startupinfo)
|
||||
out_im = Image.open(outfile)
|
||||
out_im.load()
|
||||
with Image.open(outfile) as out_im:
|
||||
out_im.load()
|
||||
return out_im.im.copy()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
|
@ -161,10 +168,6 @@ def Ghostscript(
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
im = out_im.im.copy()
|
||||
out_im.close()
|
||||
return im
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
|
||||
|
@ -191,6 +194,11 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
|
||||
self._mode = "RGB"
|
||||
|
||||
# When reading header comments, the first comment is used.
|
||||
# When reading trailer comments, the last comment is used.
|
||||
bounding_box: list[int] | None = None
|
||||
imagedata_size: tuple[int, int] | None = None
|
||||
|
||||
byte_arr = bytearray(255)
|
||||
bytes_mv = memoryview(byte_arr)
|
||||
bytes_read = 0
|
||||
|
@ -211,8 +219,8 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
msg = 'EPS header missing "%%BoundingBox" comment'
|
||||
raise SyntaxError(msg)
|
||||
|
||||
def _read_comment(s: str) -> bool:
|
||||
nonlocal reading_trailer_comments
|
||||
def read_comment(s: str) -> bool:
|
||||
nonlocal bounding_box, reading_trailer_comments
|
||||
try:
|
||||
m = split.match(s)
|
||||
except re.error as e:
|
||||
|
@ -227,18 +235,12 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
if k == "BoundingBox":
|
||||
if v == "(atend)":
|
||||
reading_trailer_comments = True
|
||||
elif not self.tile or (trailer_reached and reading_trailer_comments):
|
||||
elif not bounding_box or (trailer_reached and reading_trailer_comments):
|
||||
try:
|
||||
# Note: The DSC spec says that BoundingBox
|
||||
# fields should be integers, but some drivers
|
||||
# put floating point values there anyway.
|
||||
box = [int(float(i)) for i in v.split()]
|
||||
self._size = box[2] - box[0], box[3] - box[1]
|
||||
self.tile = [
|
||||
ImageFile._Tile(
|
||||
"eps", (0, 0) + self.size, offset, (length, box)
|
||||
)
|
||||
]
|
||||
bounding_box = [int(float(i)) for i in v.split()]
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
@ -289,7 +291,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
continue
|
||||
|
||||
s = str(bytes_mv[:bytes_read], "latin-1")
|
||||
if not _read_comment(s):
|
||||
if not read_comment(s):
|
||||
m = field.match(s)
|
||||
if m:
|
||||
k = m.group(1)
|
||||
|
@ -308,6 +310,12 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
# Check for an "ImageData" descriptor
|
||||
# https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
|
||||
|
||||
# If we've already read an "ImageData" descriptor,
|
||||
# don't read another one.
|
||||
if imagedata_size:
|
||||
bytes_read = 0
|
||||
continue
|
||||
|
||||
# Values:
|
||||
# columns
|
||||
# rows
|
||||
|
@ -333,22 +341,35 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
else:
|
||||
break
|
||||
|
||||
self._size = columns, rows
|
||||
return
|
||||
# Parse the columns and rows after checking the bit depth and mode
|
||||
# in case the bit depth and/or mode are invalid.
|
||||
imagedata_size = columns, rows
|
||||
elif bytes_mv[:5] == b"%%EOF":
|
||||
break
|
||||
elif trailer_reached and reading_trailer_comments:
|
||||
# Load EPS trailer
|
||||
s = str(bytes_mv[:bytes_read], "latin-1")
|
||||
_read_comment(s)
|
||||
read_comment(s)
|
||||
elif bytes_mv[:9] == b"%%Trailer":
|
||||
trailer_reached = True
|
||||
bytes_read = 0
|
||||
|
||||
if not self.tile:
|
||||
# A "BoundingBox" is always required,
|
||||
# even if an "ImageData" descriptor size exists.
|
||||
if not bounding_box:
|
||||
msg = "cannot determine EPS bounding box"
|
||||
raise OSError(msg)
|
||||
|
||||
# An "ImageData" size takes precedence over the "BoundingBox".
|
||||
self._size = imagedata_size or (
|
||||
bounding_box[2] - bounding_box[0],
|
||||
bounding_box[3] - bounding_box[1],
|
||||
)
|
||||
|
||||
self.tile = [
|
||||
ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
|
||||
]
|
||||
|
||||
def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
|
||||
s = fp.read(4)
|
||||
|
||||
|
|