Skip to content

Commit 2be42e1

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 existing "oiio:missingcolor" config attribute set to black, which asks the OpenEXR reader to fill unreadable scanlines or tiles with that color instead of failing the read. Readers without that support ignore the config, so the re-read simply fails the same way for them. 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 re-read if it isn't. One prerequisite fix along the way: a failed multi-scanline chunk decode with oiio:missingcolor set re-entered the per-scanline retry, which re-entered the chunk cache until the stack blew up -- the per-scanline retry now bypasses the chunk cache, fixing that for missingcolor users too and making the iv fallback actually work on compressed multi- scanline files. 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 2be42e1

6 files changed

Lines changed: 160 additions & 16 deletions

File tree

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: 50 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,47 @@ 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 "oiio:missingcolor" config attribute, which asks
104+
// the OpenEXR reader to fill unreadable scanlines or tiles with the
105+
// given color (black here) instead of failing the read. Readers
106+
// without that support ignore the config, so the re-read simply
107+
// fails the same way for them.
108+
ImageSpec config;
109+
if (m_have_input_config)
110+
config = m_input_config;
111+
config.attribute("oiio:missingcolor", "0");
112+
reset(name(), 0, 0, {}, &config);
113+
m_image_valid = ImageBuf::read(subimage, miplevel, force, format,
114+
progress_callback,
115+
progress_callback_data);
116+
if (m_image_valid) {
117+
// The read succeeded where the straightforward one failed, so
118+
// part of the pixel data must have been unreadable -- remember
119+
// that so the status bar can let the user know.
120+
m_partially_loaded = true;
121+
m_partial_error = "partially readable file, showing the "
122+
"readable portion";
123+
}
124+
}
125+
80126
if (m_image_valid && secondary_data && spec().format == TypeDesc::UINT8) {
81127
m_corrected_image.reset(ImageSpec(spec().width, spec().height,
82128
std::min(spec().nchannels, 4),
@@ -388,8 +434,10 @@ IvImage::invalidate()
388434
{
389435
ustring filename(name());
390436
reset(filename.string());
391-
m_thumbnail_valid = false;
392-
m_image_valid = false;
437+
m_thumbnail_valid = false;
438+
m_image_valid = false;
439+
m_partially_loaded = false;
440+
m_partial_error.clear();
393441
if (imagecache())
394442
imagecache()->invalidate(filename);
395443
}

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: 7 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,
@@ -377,7 +382,8 @@ class OpenEXRInput final : public ImageInput {
377382
bool read_native_scanlines_individually(int subimage, int miplevel,
378383
int ybegin, int yend, int z,
379384
int chbegin, int chend, void* data,
380-
stride_t ystride);
385+
stride_t ystride,
386+
bool bypass_chunk_cache = false);
381387
bool read_native_tiles_individually(int subimage, int miplevel, int xbegin,
382388
int xend, int ybegin, int yend,
383389
int zbegin, int zend, int chbegin,

src/openexr.imageio/exrinput.cpp

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,7 +1253,7 @@ OpenEXRInput::read_native_scanline(int subimage, int miplevel, int y, int z,
12531253
void* data)
12541254
{
12551255
return read_native_scanlines(subimage, miplevel, y, y + 1, z, 0,
1256-
m_spec.nchannels, data);
1256+
m_spec.nchannels, data, false);
12571257
}
12581258

12591259

@@ -1263,7 +1263,7 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
12631263
int yend, int z, void* data)
12641264
{
12651265
return read_native_scanlines(subimage, miplevel, ybegin, yend, z, 0,
1266-
m_spec.nchannels, data);
1266+
m_spec.nchannels, data, false);
12671267
}
12681268

12691269

@@ -1280,11 +1280,15 @@ OpenEXRInput::read_cached_chunk(int subimage, int miplevel, int ybegin,
12801280
ybegin, yend, scanlinebytes, data))
12811281
return true;
12821282

1283-
// Cache miss. Decode the whole chunk. This recursive call asks for
1284-
// exactly one full chunk, so it won't come back here.
1283+
// Cache miss. Decode the whole chunk. The bypass_chunk_cache flag of
1284+
// this recursive call asks for exactly one full chunk, so it won't come
1285+
// back here -- this matters when the decode fails and tolerance is on:
1286+
// without the bypass, the per-scanline retry would re-enter the chunk
1287+
// cache and recurse forever.
12851288
default_init_vector<uint8_t> chunk(scanlinebytes * size_t(cend - cbegin));
12861289
if (!read_native_scanlines(subimage, miplevel, cbegin, cend, 0, chbegin,
1287-
chend, chunk.data()))
1290+
chend, chunk.data(),
1291+
/*bypass_chunk_cache=*/true))
12881292
return false;
12891293
memcpy(data, chunk.data() + scanlinebytes * size_t(ybegin - cbegin),
12901294
scanlinebytes * size_t(yend - ybegin));
@@ -1298,6 +1302,15 @@ bool
12981302
OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
12991303
int yend, int z, int chbegin, int chend,
13001304
void* data)
1305+
{
1306+
return read_native_scanlines(subimage, miplevel, ybegin, yend, z, chbegin,
1307+
chend, data, /*bypass_chunk_cache=*/false);
1308+
}
1309+
1310+
bool
1311+
OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
1312+
int yend, int z, int chbegin, int chend,
1313+
void* data, bool bypass_chunk_cache)
13011314
{
13021315
lock_guard lock(*this);
13031316
if (!seek_subimage(subimage, miplevel))
@@ -1341,7 +1354,7 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
13411354
default_init_vector<uint8_t> scratch(fullscanbytes
13421355
* size_t(yend - ybegin));
13431356
if (!read_native_scanlines(subimage, miplevel, ybegin, yend, z, 0,
1344-
m_spec.nchannels, scratch.data()))
1357+
m_spec.nchannels, scratch.data(), false))
13451358
return false;
13461359
size_t choff = m_spec.pixel_bytes(0, chbegin, true);
13471360
for (int y = ybegin; y < yend; ++y) {
@@ -1364,7 +1377,7 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
13641377
// swath) at a time will ask for the rest of that chunk next. (The
13651378
// library has a stash of its own, but it drops it every time we set the
13661379
// frame buffer, which we must do on every read.)
1367-
if (part.scansperchunk > 1 && !part.luminance_chroma
1380+
if (part.scansperchunk > 1 && !part.luminance_chroma && !bypass_chunk_cache
13681381
&& ybegin >= m_spec.y) {
13691382
int ychunkstart = m_spec.y
13701383
+ round_down_to_multiple(ybegin - m_spec.y,
@@ -1456,10 +1469,10 @@ OpenEXRInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
14561469
} else {
14571470
// Read of many tiles -- don't know which failed, so try
14581471
// again to read them all individually.
1459-
return read_native_scanlines_individually(subimage, miplevel,
1460-
ybegin, yend, z,
1461-
chbegin, chend, data,
1462-
scanlinebytes);
1472+
return read_native_scanlines_individually(
1473+
subimage, miplevel, ybegin, yend, z, chbegin, chend, data,
1474+
scanlinebytes,
1475+
/*bypass_chunk_cache=*/true);
14631476
}
14641477
} else {
14651478
errorfmt("Failed OpenEXR read: {}", err);
@@ -1631,15 +1644,16 @@ bool
16311644
OpenEXRInput::read_native_scanlines_individually(int subimage, int miplevel,
16321645
int ybegin, int yend, int z,
16331646
int chbegin, int chend,
1634-
void* data, stride_t ystride)
1647+
void* data, stride_t ystride,
1648+
bool bypass_chunk_cache)
16351649
{
16361650
// Note: this is only called by read_native_scanlines, which still holds
16371651
// the mutex, so it's safe to directly access m_spec.
16381652
bool ok = true;
16391653
for (int y = ybegin; y < yend; ++y) {
16401654
char* d = (char*)data + (y - ybegin) * ystride;
16411655
ok &= read_native_scanlines(subimage, miplevel, y, y + 1, z, chbegin,
1642-
chend, d);
1656+
chend, d, bypass_chunk_cache);
16431657
}
16441658
return ok;
16451659
}

0 commit comments

Comments
 (0)