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

Guard a race condition in vips new_from_file #1500

Merged
merged 1 commit into from
Apr 10, 2024
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
### Changes
- Improve algorithm sweep example options ([#1498](../../pull/1498))

### Bug Fixes
- Guard a race condition in vips new_from_file ([#1500](../../pull/1500))

## 1.28.0

### Features
Expand Down
6 changes: 6 additions & 0 deletions large_image/tilesource/utilities.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import io
import math
import os
import threading
import types
import xml.etree.ElementTree
from collections import defaultdict
Expand All @@ -27,6 +28,11 @@
# Turn off decompression warning check
PIL.Image.MAX_IMAGE_PIXELS = None

# This is used by any submodule that uses vips to avoid a race condition in
# new_from_file. Since vips is technically optional and the various modules
# might pull it in independently, it is located here to make is shareable.
_newFromFileLock = threading.RLock()

# Extend colors so G and GREEN map to expected values. CSS green is #0080ff,
# which is unfortunate.
colormap = {
Expand Down
11 changes: 7 additions & 4 deletions sources/vips/large_image_source_vips/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
dtypeToGValue)
from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
from large_image.tilesource import FileTileSource
from large_image.tilesource.utilities import _imageToNumpy
from large_image.tilesource.utilities import _imageToNumpy, _newFromFileLock

logging.getLogger('pyvips').setLevel(logging.ERROR)

Expand Down Expand Up @@ -72,7 +72,8 @@ def __init__(self, path, **kwargs):

config._ignoreSourceNames('vips', self._largeImagePath)
try:
self._image = pyvips.Image.new_from_file(self._largeImagePath)
with _newFromFileLock:
self._image = pyvips.Image.new_from_file(self._largeImagePath)
except pyvips.error.Error:
if not os.path.isfile(self._largeImagePath):
raise TileSourceFileNotFoundError(self._largeImagePath) from None
Expand All @@ -87,7 +88,8 @@ def __init__(self, path, **kwargs):
self._frames = [0]
for page in range(1, pages):
subInputPath = self._largeImagePath + '[page=%d]' % page
subImage = pyvips.Image.new_from_file(subInputPath)
with _newFromFileLock:
subImage = pyvips.Image.new_from_file(subInputPath)
if subImage.width == self.sizeX and subImage.height == self.sizeY:
self._frames.append(page)
continue
Expand Down Expand Up @@ -187,7 +189,8 @@ def _getFrameImage(self, frame=0):
with self._frameLock:
if frame not in self._recentFrames:
subpath = self._largeImagePath + '[page=%d]' % self._frames[frame]
img = pyvips.Image.new_from_file(subpath)
with _newFromFileLock:
img = pyvips.Image.new_from_file(subpath)
self._recentFrames[frame] = img
else:
img = self._recentFrames[frame]
Expand Down
21 changes: 14 additions & 7 deletions utilities/converter/large_image_converter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import tifftools

import large_image
from large_image.tilesource.utilities import _gdalParameters, _vipsCast, _vipsParameters
from large_image.tilesource.utilities import (_gdalParameters,
_newFromFileLock, _vipsCast,
_vipsParameters)

from . import format_aperio

Expand Down Expand Up @@ -77,7 +79,7 @@ def _data_from_large_image(path, outputPath, **kwargs):
_import_pyvips()
if not path.startswith('large_image://test'):
try:
ts = large_image.open(path)
ts = large_image.open(path, noCache=True)
except Exception:
return
else:
Expand Down Expand Up @@ -164,7 +166,8 @@ def _generate_multiframe_tiff(inputPath, outputPath, tempPath, lidata, **kwargs)
"""
_import_pyvips()

image = pyvips.Image.new_from_file(inputPath)
with _newFromFileLock:
image = pyvips.Image.new_from_file(inputPath)
width = image.width
height = image.height
pages = 1
Expand All @@ -180,7 +183,8 @@ def _generate_multiframe_tiff(inputPath, outputPath, tempPath, lidata, **kwargs)
# Process each image separately to pyramidize it
for page in range(pages):
subInputPath = inputPath + '[page=%d]' % page
subImage = pyvips.Image.new_from_file(subInputPath)
with _newFromFileLock:
subImage = pyvips.Image.new_from_file(subInputPath)
imageSizes.append((subImage.width, subImage.height, subInputPath, page))
if subImage.width != width or subImage.height != height:
if subImage.width * subImage.height <= width * height:
Expand Down Expand Up @@ -274,7 +278,8 @@ def _convert_via_vips(inputPathOrBuffer, outputPath, tempPath, forTiled=True,
image = pyvips.Image.new_from_buffer(inputPathOrBuffer, '')
else:
source = inputPathOrBuffer
image = pyvips.Image.new_from_file(inputPathOrBuffer)
with _newFromFileLock:
image = pyvips.Image.new_from_file(inputPathOrBuffer)
logger.info('Input: %s, Output: %s, Options: %r%s',
source, outputPath, convertParams, status)
image = image.autorot()
Expand Down Expand Up @@ -736,7 +741,8 @@ def _is_multiframe(path):
"""
_import_pyvips()
try:
image = pyvips.Image.new_from_file(path)
with _newFromFileLock:
image = pyvips.Image.new_from_file(path)
except Exception:
try:
open(path, 'rb').read(1)
Expand Down Expand Up @@ -971,7 +977,8 @@ def is_vips(path):
"""
_import_pyvips()
try:
image = pyvips.Image.new_from_file(path)
with _newFromFileLock:
image = pyvips.Image.new_from_file(path)
# image(0, 0) will throw if vips can't decode the image
if not image.width or not image.height or image(0, 0) is None:
return False
Expand Down