Skip to content

Commit 9f514ee

Browse files
committed
Fix 2+ GB reads --without-performance
PIL.Image.frombuffer() raises OverflowError on buffers >= 2 GB when mapping color channels (python-pillow/Pillow#1475). Work around this by loading large buffers in smaller chunks and pasting them into the result image. Fixes #17.
1 parent 6263375 commit 9f514ee

File tree

2 files changed

+58
-10
lines changed

2 files changed

+58
-10
lines changed

openslide/lowlevel.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
rather than in the high-level interface.)
3030
"""
3131

32+
from __future__ import division
3233
from ctypes import *
3334
from itertools import count
3435
import PIL.Image
@@ -63,16 +64,49 @@ def _load_image(buf, size):
6364
return PIL.Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1)
6465
except ImportError:
6566
def _load_image(buf, size):
66-
'''buf can be a string, but should be a ctypes buffer to avoid an
67-
extra copy in the caller.'''
68-
# First reorder the bytes in a pixel from native-endian aRGB to
69-
# big-endian RGBa to work around limitations in RGBa loader
70-
rawmode = (sys.byteorder == 'little') and 'BGRA' or 'ARGB'
71-
buf = PIL.Image.frombuffer('RGBA', size, buf, 'raw', rawmode, 0, 1)
72-
# Image.tobytes() is named tostring() in Pillow 1.x and PIL
73-
buf = (getattr(buf, 'tobytes', None) or buf.tostring)()
74-
# Now load the image as RGBA, undoing premultiplication
75-
return PIL.Image.frombuffer('RGBA', size, buf, 'raw', 'RGBa', 0, 1)
67+
'''buf must be a buffer.'''
68+
69+
# Load entire buffer at once if possible
70+
MAX_PIXELS_PER_LOAD = (1 << 29) - 1
71+
# Otherwise, use chunks smaller than the maximum to reduce memory
72+
# requirements
73+
PIXELS_PER_LOAD = 1 << 26
74+
75+
def do_load(buf, size):
76+
'''buf can be a string, but should be a ctypes buffer to avoid an
77+
extra copy in the caller.'''
78+
# First reorder the bytes in a pixel from native-endian aRGB to
79+
# big-endian RGBa to work around limitations in RGBa loader
80+
rawmode = (sys.byteorder == 'little') and 'BGRA' or 'ARGB'
81+
buf = PIL.Image.frombuffer('RGBA', size, buf, 'raw', rawmode, 0, 1)
82+
# Image.tobytes() is named tostring() in Pillow 1.x and PIL
83+
buf = (getattr(buf, 'tobytes', None) or buf.tostring)()
84+
# Now load the image as RGBA, undoing premultiplication
85+
return PIL.Image.frombuffer('RGBA', size, buf, 'raw', 'RGBa', 0, 1)
86+
87+
# Fast path for small buffers
88+
w, h = size
89+
if w * h <= MAX_PIXELS_PER_LOAD:
90+
return do_load(buf, size)
91+
92+
# Load in chunks to avoid OverflowError in PIL.Image.frombuffer()
93+
# https://github.com/python-pillow/Pillow/issues/1475
94+
if w > PIXELS_PER_LOAD:
95+
# We could support this, but it seems like overkill
96+
raise ValueError('Width %d is too large (maximum %d)' %
97+
(w, PIXELS_PER_LOAD))
98+
rows_per_load = PIXELS_PER_LOAD // w
99+
img = PIL.Image.new('RGBA', (w, h))
100+
for y in range(0, h, rows_per_load):
101+
rows = min(h - y, rows_per_load)
102+
if sys.version[0] == '2':
103+
chunk = buffer(buf, 4 * y * w, 4 * rows * w)
104+
else:
105+
# PIL.Image.frombuffer() won't take a memoryview or
106+
# bytearray, so we can't avoid copying
107+
chunk = memoryview(buf)[y * w:(y + rows) * w].tobytes()
108+
img.paste(do_load(chunk, (w, rows)), (0, y))
109+
return img
76110

77111
class OpenSlideError(Exception):
78112
"""An error produced by the OpenSlide library.

tests/test_openslide.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ def test_read_region_bad_size(self):
120120
self.assertRaises(OpenSlideError,
121121
lambda: self.osr.read_region((0, 0), 1, (400, -5)))
122122

123+
def test_read_region_2GB(self):
124+
self.assertEqual(
125+
self.osr.read_region((1000, 1000), 0, (32768, 16384)).size,
126+
(32768, 16384))
127+
128+
def test_read_region_2GB_width(self):
129+
try:
130+
import openslide._convert
131+
return
132+
except ImportError:
133+
pass
134+
self.assertRaises(ValueError,
135+
lambda: self.osr.read_region((1000, 1000), 0, (1 << 29, 1)))
136+
123137
def test_thumbnail(self):
124138
self.assertEqual(self.osr.get_thumbnail((100, 100)).size, (100, 83))
125139

0 commit comments

Comments
 (0)