Skip to content

Commit 50978ee

Browse files
iv: tolerate partially-written EXR files
When iv opens an image file that was only partially written -- for example, an EXR that a renderer crashed on or was killed before it finished writing the pixel data -- the read fails and iv refuses to display anything at all. Now, if the straightforward read fails, iv reopens the file with the new "openexr:missing_data_ok" configuration hint, which asks the OpenEXR reader to fill unreadable scanlines or tiles with black instead of failing the read (building on the existing missingcolor machinery). Other readers simply ignore the hint. In the partial case, the status bar displays a note explaining that the file is only partially readable. For images that were read through the ImageCache (where pixel data isn't touched until display time), a cheap probe of the last scanline or tile checks that the pixel data is intact, and falls back to the hinted re-read if it isn't. The hint is documented alongside oiio:missingcolor. As a side effect, this also fixes an infinite recursion (stack overflow) in the chunk cache path that could already be triggered by the existing oiio:missingcolor feature when a multi-scanline chunk failed to decode. Fixes #4713 Assisted-by: Claude Code:glm-5.3-flash Signed-off-by: linsen <251731047+linsen458-spec@users.noreply.github.com>
1 parent b833e66 commit 50978ee

7 files changed

Lines changed: 184 additions & 19 deletions

File tree

src/doc/builtinplugins.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,6 +1718,14 @@ attributes are supported:
17181718
tile/scanline read failure to be an error. This can be helpful when
17191719
intentionally reading partially-written or incomplete files (such as
17201720
an in-progress render).
1721+
* - ``openexr:missing_data_ok``
1722+
- int
1723+
- If nonzero, missing tiles or scanlines are filled with black rather
1724+
than considering a tile/scanline read failure to be an error. This is
1725+
equivalent to ``oiio:missingcolor`` with a black fill color, for the
1726+
common case of reading a partially-written file (such as one from a
1727+
renderer that was interrupted) where no particular fill color is
1728+
needed. Other readers ignore this hint.
17211729
```
17221730

17231731
**Configuration settings for OpenEXR output**

src/iv/imageviewer.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,11 @@ ImageViewer::updateStatusBar()
11641164
message = Strutil::fmt::format("({}/{}) : ", m_current_image + 1,
11651165
(int)m_images.size());
11661166
message += cur()->shortinfo();
1167+
if (cur()->partially_loaded()) {
1168+
message += " [partially readable file: ";
1169+
message += cur()->partial_error();
1170+
message += "]";
1171+
}
11671172
statusImgInfo->setText(message.c_str());
11681173

11691174
message.clear();

src/iv/imageviewer.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ class IvImage final : public ImageBuf {
104104
///
105105
bool image_valid() const { return m_image_valid; }
106106

107+
/// True if the last read only partially succeeded, i.e. the image
108+
/// specification was readable but some of the pixel data could not be
109+
/// read (for example, a file that was only partially written before a
110+
/// renderer crashed).
111+
bool partially_loaded() const { return m_partially_loaded; }
112+
113+
/// If the last read was only partial (see partially_loaded()), a
114+
/// message describing the problem.
115+
const std::string& partial_error() const { return m_partial_error; }
116+
107117
/// Copies data from the read buffer to the secondary buffer, selecting the
108118
/// given channel:
109119
/// -2 = luminance
@@ -139,6 +149,10 @@ class IvImage final : public ImageBuf {
139149
mutable std::string m_longinfo;
140150
bool m_image_valid; ///< Image is valid and pixels can be read.
141151
bool m_auto_subimage; ///< Automatically use subimages when zooming-in/out.
152+
bool m_partially_loaded = false; ///< Only some pixel data was readable.
153+
std::string m_partial_error; ///< Description of what was unreadable.
154+
ImageSpec m_input_config; ///< Copy of the input configuration spec
155+
bool m_have_input_config = false; ///< Was an input configuration given?
142156
};
143157

144158

src/iv/ivimage.cpp

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <iostream>
77

88
#include "imageviewer.h"
9+
#include "ivutils.h"
910
#include <OpenImageIO/imagecache.h>
1011
#include <OpenImageIO/strutil.h>
1112

@@ -20,6 +21,10 @@ IvImage::IvImage(const std::string& filename, const ImageSpec* input_config)
2021
, m_image_valid(false)
2122
, m_auto_subimage(false)
2223
{
24+
if (input_config) {
25+
m_input_config = *input_config;
26+
m_have_input_config = true;
27+
}
2328
}
2429

2530

@@ -77,6 +82,46 @@ IvImage::read_iv(int subimage, int miplevel, bool force, TypeDesc format,
7782
progress_callback,
7883
progress_callback_data);
7984

85+
m_partially_loaded = false;
86+
m_partial_error.clear();
87+
if (m_image_valid && storage() == ImageBuf::IMAGECACHE) {
88+
// The image is backed by the ImageCache, which means that the
89+
// pixel data has not been touched yet and read failures will only
90+
// turn up later, awkwardly, as pixels are fetched for display.
91+
// For a file that was only partially written (for example, an EXR
92+
// that a renderer didn't finish), check now whether the pixel
93+
// data is really all readable, and if not, fall through to the
94+
// tolerant re-read below.
95+
ImageSpec* config = m_have_input_config ? &m_input_config : nullptr;
96+
m_image_valid = image_data_readable(name(), config, subimage, miplevel);
97+
}
98+
if (!m_image_valid) {
99+
// The straightforward read failed. This can happen for a file that
100+
// was only partially written -- for example, an EXR file from a
101+
// renderer that crashed or was killed before it finished writing
102+
// all of the pixel data. Rather than showing nothing at all, reopen
103+
// the file with the "openexr:missing_data_ok" hint, which asks the
104+
// OpenEXR reader to fill the unreadable scanlines or tiles with
105+
// black instead of failing the read. Other readers ignore the hint,
106+
// so the re-read simply fails the same way for them.
107+
ImageSpec config;
108+
if (m_have_input_config)
109+
config = m_input_config;
110+
config.attribute("openexr:missing_data_ok", 1);
111+
reset(name(), 0, 0, {}, &config);
112+
m_image_valid = ImageBuf::read(subimage, miplevel, force, format,
113+
progress_callback,
114+
progress_callback_data);
115+
if (m_image_valid) {
116+
// The read succeeded where the straightforward one failed, so
117+
// part of the pixel data must have been unreadable -- remember
118+
// that so the status bar can let the user know.
119+
m_partially_loaded = true;
120+
m_partial_error = "partially readable file, showing the "
121+
"readable portion";
122+
}
123+
}
124+
80125
if (m_image_valid && secondary_data && spec().format == TypeDesc::UINT8) {
81126
m_corrected_image.reset(ImageSpec(spec().width, spec().height,
82127
std::min(spec().nchannels, 4),
@@ -388,8 +433,10 @@ IvImage::invalidate()
388433
{
389434
ustring filename(name());
390435
reset(filename.string());
391-
m_thumbnail_valid = false;
392-
m_image_valid = false;
436+
m_thumbnail_valid = false;
437+
m_image_valid = false;
438+
m_partially_loaded = false;
439+
m_partial_error.clear();
393440
if (imagecache())
394441
imagecache()->invalidate(filename);
395442
}

src/iv/ivutils.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
#ifndef OPENIMAGEIO_IV_UTILS_H
66
#define OPENIMAGEIO_IV_UTILS_H
77

8+
#include <algorithm>
9+
#include <cstring>
10+
#include <string>
11+
#include <vector>
12+
13+
#include <OpenImageIO/imagebuf.h>
14+
#include <OpenImageIO/imageio.h>
815
#include <OpenImageIO/oiioversion.h>
916

1017
OIIO_NAMESPACE_BEGIN
@@ -33,6 +40,56 @@ floor2f(float f)
3340
return powf(2.0f, floorf(logval));
3441
}
3542

43+
44+
/// Probe whether all of the pixel data of the image named `filename` is
45+
/// readable, without reading the whole image: attempt to read only the
46+
/// last scanline (for scanline files) or the last tile (for tiled files),
47+
/// which is the most likely region to be missing from a file that was
48+
/// only partially written. This is cheap enough to use as a check on
49+
/// files that were read through an ImageCache, where the pixel data is
50+
/// not touched until it is needed for display, at which point read
51+
/// failures are much less gracefully handled.
52+
///
53+
/// Returns true if the last scanline/tile could be read (and therefore it
54+
/// is likely that the entire pixel data block is intact), false if it
55+
/// could not (meaning that the file is probably truncated or otherwise
56+
/// partially written).
57+
inline bool
58+
image_data_readable(string_view filename, const ImageSpec* config, int subimage,
59+
int miplevel)
60+
{
61+
auto in = ImageInput::open(filename, config);
62+
if (!in)
63+
return false;
64+
if (!in->seek_subimage(subimage, miplevel)) {
65+
in->close();
66+
return false;
67+
}
68+
ImageSpec spec = in->spec(subimage, miplevel);
69+
bool ok = false;
70+
if (spec.tile_width > 0) {
71+
// Try to read the bottom-most, right-most tile.
72+
int tx = spec.x
73+
+ ((spec.width - 1) / spec.tile_width) * spec.tile_width;
74+
int ty = spec.y
75+
+ ((spec.height - 1) / spec.tile_height) * spec.tile_height;
76+
int tz = spec.z
77+
+ ((std::max(spec.depth - 1, 0))
78+
/ std::max(spec.tile_depth, 1))
79+
* std::max(spec.tile_depth, 1);
80+
std::vector<char> buf(spec.tile_bytes());
81+
ok = in->read_tile(tx, ty, tz, spec.format, buf.data());
82+
} else {
83+
// Try to read the last scanline.
84+
int y = spec.y + spec.height - 1;
85+
std::vector<char> buf(spec.scanline_bytes());
86+
ok = in->read_scanlines(subimage, miplevel, y, y + 1, spec.z, 0,
87+
spec.nchannels, TypeDesc::UNKNOWN, buf.data());
88+
}
89+
in->close();
90+
return ok;
91+
}
92+
3693
OIIO_NAMESPACE_END
3794

3895
#endif // OPENIMAGEIO_IV_UTILS_H

src/openexr.imageio/exr_pvt.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,11 @@ class OpenEXRInput final : public ImageInput {
265265
bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend,
266266
int z, int chbegin, int chend,
267267
void* data) override;
268+
// Internal version with a flag letting the per-scanline retry bypass the
269+
// chunk cache (see read_native_scanlines_individually).
270+
bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend,
271+
int z, int chbegin, int chend, void* data,
272+
bool bypass_chunk_cache);
268273
bool read_native_tile(int subimage, int miplevel, int x, int y, int z,
269274
void* data) override;
270275
bool read_native_tiles(int subimage, int miplevel, int xbegin, int xend,
@@ -347,6 +352,7 @@ class OpenEXRInput final : public ImageInput {
347352
int m_nsubimages; ///< How many subimages are there?
348353
int m_miplevel; ///< What MIP level are we looking at?
349354
std::vector<float> m_missingcolor; ///< Color for missing tile/scanline
355+
bool m_missing_data_ok = false; ///< Tolerate missing tile/scanline
350356
std::string m_filename; // filename, if known
351357
ExrChunkCache m_chunkcache;
352358

@@ -365,6 +371,7 @@ class OpenEXRInput final : public ImageInput {
365371
m_io = nullptr;
366372
m_local_io.reset();
367373
m_missingcolor.clear();
374+
m_missing_data_ok = false;
368375
m_filename.clear();
369376
}
370377

@@ -377,7 +384,8 @@ class OpenEXRInput final : public ImageInput {
377384
bool read_native_scanlines_individually(int subimage, int miplevel,
378385
int ybegin, int yend, int z,
379386
int chbegin, int chend, void* data,
380-
stride_t ystride);
387+
stride_t ystride,
388+
bool bypass_chunk_cache = false);
381389
bool read_native_tiles_individually(int subimage, int miplevel, int xbegin,
382390
int xend, int ybegin, int yend,
383391
int zbegin, int zend, int chbegin,

src/openexr.imageio/exrinput.cpp

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@ OpenEXRInput::open(const std::string& name, ImageSpec& newspec,
275275
if (mc.size())
276276
m_missingcolor = Strutil::extract_from_list_string<float>(mc);
277277
}
278+
// "openexr:missing_data_ok" asks to tolerate missing scanlines or tiles
279+
// -- for example, in a partially written file from a renderer that was
280+
// interrupted -- by filling the unreadable regions with black instead of
281+
// failing the read. Other readers will simply ignore this hint.
282+
if (const ParamValue* mdo = config.find_attribute("openexr:missing_data_ok"))
283+
m_missing_data_ok = mdo->get_int() != 0;
278284

279285
// Before engaging further with OpenEXR, make sure it is using the right
280286
// number of threads.
@@ -1253,7 +1259,7 @@ OpenEXRInput::read_native_scanline(int subimage, int miplevel, int y, int z,
12531259
void* data)
12541260
{
12551261
return read_native_scanlines(subimage, miplevel, y, y + 1, z, 0,
1256-
m_spec.nchannels, data);
1262+
m_spec.nchannels, data, false);
12571263
}
12581264

12591265

@@ -1263,7 +1269,7 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
12631269
int yend, int z, void* data)
12641270
{
12651271
return read_native_scanlines(subimage, miplevel, ybegin, yend, z, 0,
1266-
m_spec.nchannels, data);
1272+
m_spec.nchannels, data, false);
12671273
}
12681274

12691275

@@ -1280,11 +1286,15 @@ OpenEXRInput::read_cached_chunk(int subimage, int miplevel, int ybegin,
12801286
ybegin, yend, scanlinebytes, data))
12811287
return true;
12821288

1283-
// Cache miss. Decode the whole chunk. This recursive call asks for
1284-
// exactly one full chunk, so it won't come back here.
1289+
// Cache miss. Decode the whole chunk. The bypass_chunk_cache flag of
1290+
// this recursive call asks for exactly one full chunk, so it won't come
1291+
// back here -- this matters when the decode fails and tolerance is on:
1292+
// without the bypass, the per-scanline retry would re-enter the chunk
1293+
// cache and recurse forever.
12851294
default_init_vector<uint8_t> chunk(scanlinebytes * size_t(cend - cbegin));
12861295
if (!read_native_scanlines(subimage, miplevel, cbegin, cend, 0, chbegin,
1287-
chend, chunk.data()))
1296+
chend, chunk.data(),
1297+
/*bypass_chunk_cache=*/true))
12881298
return false;
12891299
memcpy(data, chunk.data() + scanlinebytes * size_t(ybegin - cbegin),
12901300
scanlinebytes * size_t(yend - ybegin));
@@ -1298,6 +1308,15 @@ bool
12981308
OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
12991309
int yend, int z, int chbegin, int chend,
13001310
void* data)
1311+
{
1312+
return read_native_scanlines(subimage, miplevel, ybegin, yend, z, chbegin,
1313+
chend, data, /*bypass_chunk_cache=*/false);
1314+
}
1315+
1316+
bool
1317+
OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
1318+
int yend, int z, int chbegin, int chend,
1319+
void* data, bool bypass_chunk_cache)
13011320
{
13021321
lock_guard lock(*this);
13031322
if (!seek_subimage(subimage, miplevel))
@@ -1341,7 +1360,7 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
13411360
default_init_vector<uint8_t> scratch(fullscanbytes
13421361
* size_t(yend - ybegin));
13431362
if (!read_native_scanlines(subimage, miplevel, ybegin, yend, z, 0,
1344-
m_spec.nchannels, scratch.data()))
1363+
m_spec.nchannels, scratch.data(), false))
13451364
return false;
13461365
size_t choff = m_spec.pixel_bytes(0, chbegin, true);
13471366
for (int y = ybegin; y < yend; ++y) {
@@ -1364,7 +1383,7 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
13641383
// swath) at a time will ask for the rest of that chunk next. (The
13651384
// library has a stash of its own, but it drops it every time we set the
13661385
// frame buffer, which we must do on every read.)
1367-
if (part.scansperchunk > 1 && !part.luminance_chroma
1386+
if (part.scansperchunk > 1 && !part.luminance_chroma && !bypass_chunk_cache
13681387
&& ybegin >= m_spec.y) {
13691388
int ychunkstart = m_spec.y
13701389
+ round_down_to_multiple(ybegin - m_spec.y,
@@ -1441,7 +1460,7 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
14411460
}
14421461
} catch (const std::exception& e) {
14431462
std::string err = e.what();
1444-
if (m_missingcolor.size()) {
1463+
if (m_missingcolor.size() || m_missing_data_ok) {
14451464
// User said not to fail for bad or missing scanlines. If we
14461465
// failed reading a single scanline, use the fill pattern. If we
14471466
// failed reading many scanlines, we don't know which ones, so go
@@ -1456,10 +1475,10 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
14561475
} else {
14571476
// Read of many tiles -- don't know which failed, so try
14581477
// again to read them all individually.
1459-
return read_native_scanlines_individually(subimage, miplevel,
1460-
ybegin, yend, z,
1461-
chbegin, chend, data,
1462-
scanlinebytes);
1478+
return read_native_scanlines_individually(
1479+
subimage, miplevel, ybegin, yend, z, chbegin, chend, data,
1480+
scanlinebytes,
1481+
/*bypass_chunk_cache=*/true);
14631482
}
14641483
} else {
14651484
errorfmt("Failed OpenEXR read: {}", err);
@@ -1593,7 +1612,7 @@ OpenEXRInput::read_native_tiles(int subimage, int miplevel, int xbegin,
15931612
}
15941613
} catch (const std::exception& e) {
15951614
std::string err = e.what();
1596-
if (m_missingcolor.size()) {
1615+
if (m_missingcolor.size() || m_missing_data_ok) {
15971616
// User said not to fail for bad or missing tiles. If we failed
15981617
// reading a single tile, use the fill pattern. If we failed
15991618
// reading many tiles, we don't know which ones, so go back and
@@ -1631,15 +1650,16 @@ bool
16311650
OpenEXRInput::read_native_scanlines_individually(int subimage, int miplevel,
16321651
int ybegin, int yend, int z,
16331652
int chbegin, int chend,
1634-
void* data, stride_t ystride)
1653+
void* data, stride_t ystride,
1654+
bool bypass_chunk_cache)
16351655
{
16361656
// Note: this is only called by read_native_scanlines, which still holds
16371657
// the mutex, so it's safe to directly access m_spec.
16381658
bool ok = true;
16391659
for (int y = ybegin; y < yend; ++y) {
16401660
char* d = (char*)data + (y - ybegin) * ystride;
16411661
ok &= read_native_scanlines(subimage, miplevel, y, y + 1, z, chbegin,
1642-
chend, d);
1662+
chend, d, bypass_chunk_cache);
16431663
}
16441664
return ok;
16451665
}
@@ -1679,7 +1699,13 @@ OpenEXRInput::fill_missing(int xbegin, int xend, int ybegin, int yend,
16791699
void* data, stride_t xstride, stride_t ystride)
16801700
{
16811701
std::vector<float> missingcolor = m_missingcolor;
1682-
missingcolor.resize(chend, m_missingcolor.back());
1702+
if (missingcolor.empty()) {
1703+
// No explicit missing color (the "openexr:missing_data_ok" case):
1704+
// fill the unreadable regions with black.
1705+
missingcolor.resize(chend, 0.0f);
1706+
} else {
1707+
missingcolor.resize(chend, m_missingcolor.back());
1708+
}
16831709
bool stripe = missingcolor[0] < 0.0f;
16841710
if (stripe)
16851711
missingcolor[0] = fabsf(missingcolor[0]);

0 commit comments

Comments
 (0)