1 // Copyright 2008-present Contributors to the OpenImageIO project.
2 // SPDX-License-Identifier: BSD-3-Clause
3 // https://github.com/OpenImageIO/oiio/blob/master/LICENSE.md
4
5 /////////////////////////////////////////////////////////////////////////
6 // Tests related to ImageInput and ImageOutput
7 /////////////////////////////////////////////////////////////////////////
8
9 #include <iostream>
10
11 #include <OpenImageIO/benchmark.h>
12 #include <OpenImageIO/filesystem.h>
13 #include <OpenImageIO/imagebuf.h>
14 #include <OpenImageIO/imagebufalgo.h>
15 #include <OpenImageIO/imageio.h>
16 #include <OpenImageIO/unittest.h>
17
18 using namespace OIIO;
19
20
21
22 // Generate a small test image appropriate to the given format
23 static ImageBuf
make_test_image(string_view formatname)24 make_test_image(string_view formatname)
25 {
26 ImageBuf buf;
27 auto out = ImageOutput::create(formatname);
28 OIIO_DASSERT(out);
29 ImageSpec spec(64, 64, 4, TypeFloat);
30 float pval = 1.0f;
31 // Fill with 0 for lossy HEIF
32 if (formatname == "heif")
33 pval = 0.0f;
34
35 // Accommodate limited numbers of channels
36 if (formatname == "zfile" || formatname == "fits")
37 spec.nchannels = 1; // these formats are single channel
38 else if (!out->supports("alpha"))
39 spec.nchannels = std::min(spec.nchannels, 3);
40
41 // Force a fixed datetime metadata so it can't differ between writes
42 // and make different file patterns for these tests.
43 spec.attribute("DateTime", "01/01/2000 00:00:00");
44
45 buf.reset(spec);
46 ImageBufAlgo::fill(buf, { pval, pval, pval, 1.0f });
47 return buf;
48 }
49
50
51
52 #define CHECKED(obj, call) \
53 if (!obj->call) { \
54 if (do_asserts) \
55 OIIO_CHECK_ASSERT(false && #call); \
56 if (errmsg) \
57 *errmsg = obj->geterror(); \
58 else \
59 std::cout << " " << obj->geterror() << "\n"; \
60 return false; \
61 }
62
63
64 static bool
checked_write(ImageOutput * out,string_view filename,const ImageSpec & spec,TypeDesc type,const void * data,bool do_asserts=true,std::string * errmsg=nullptr,Filesystem::IOProxy * ioproxy=nullptr)65 checked_write(ImageOutput* out, string_view filename, const ImageSpec& spec,
66 TypeDesc type, const void* data, bool do_asserts = true,
67 std::string* errmsg = nullptr,
68 Filesystem::IOProxy* ioproxy = nullptr)
69 {
70 if (errmsg)
71 *errmsg = "";
72 std::unique_ptr<ImageOutput> out_local;
73 if (!out) {
74 out_local = ImageOutput::create(filename, ioproxy);
75 out = out_local.get();
76 }
77 OIIO_CHECK_ASSERT(out && "Failed to create output");
78 if (!out) {
79 if (errmsg)
80 *errmsg = OIIO::geterror();
81 else
82 std::cout << " " << OIIO::geterror() << "\n";
83 return false;
84 }
85
86 CHECKED(out, open(filename, spec));
87 CHECKED(out, write_image(type, data));
88 CHECKED(out, close());
89 return true;
90 }
91
92
93
94 static bool
checked_read(ImageInput * in,string_view filename,std::vector<unsigned char> & data,bool already_opened=false,bool do_asserts=true,std::string * errmsg=nullptr)95 checked_read(ImageInput* in, string_view filename,
96 std::vector<unsigned char>& data, bool already_opened = false,
97 bool do_asserts = true, std::string* errmsg = nullptr)
98 {
99 if (errmsg)
100 *errmsg = "";
101 if (!already_opened) {
102 ImageSpec spec;
103 CHECKED(in, open(filename, spec));
104 }
105 data.resize(in->spec().image_pixels() * in->spec().nchannels
106 * sizeof(float));
107 CHECKED(in, read_image(TypeFloat, data.data()));
108 CHECKED(in, close());
109 return true;
110 }
111
112
113
114 // Helper for test_all_formats: write the pixels in buf to an in-memrory
115 // IOProxy, make sure it matches byte for byte the file named by disk_filename.
116 static bool
test_write_proxy(string_view formatname,string_view extension,const std::string & disk_filename,ImageBuf & buf)117 test_write_proxy(string_view formatname, string_view extension,
118 const std::string& disk_filename, ImageBuf& buf)
119 {
120 std::cout << " Writing Proxy " << formatname << " ... ";
121 std::cout.flush();
122 bool ok = true;
123 Sysutil::Term term(stdout);
124
125 // Use ImageOutput interface with a proxy
126 Filesystem::IOVecOutput outproxy;
127 std::string memname = Strutil::sprintf("mem.%s", extension);
128 ok = checked_write(nullptr, memname, buf.spec(), buf.spec().format,
129 buf.localpixels(), true, nullptr, &outproxy);
130
131 // Use ImageBuf write interface with a proxy
132 Filesystem::IOVecOutput outproxybuf;
133 buf.set_write_ioproxy(&outproxybuf);
134 buf.write(memname);
135
136 // The in-memory vectors we wrote should match, byte-for-byte,
137 // the version we wrote to disk earlier.
138 uint64_t bytes_written = Filesystem::file_size(disk_filename);
139 std::vector<unsigned char> readbuf(bytes_written);
140 size_t bread = Filesystem::read_bytes(disk_filename, readbuf.data(),
141 bytes_written);
142
143 ok = (bread == bytes_written && outproxy.buffer() == readbuf
144 && outproxybuf.buffer() == readbuf);
145 OIIO_CHECK_ASSERT(bread == bytes_written
146 && "Bytes read didn't match bytes written");
147 OIIO_CHECK_ASSERT(outproxy.buffer() == readbuf
148 && "Write proxy via ImageOutput didn't match write file");
149 OIIO_CHECK_ASSERT(outproxybuf.buffer() == readbuf
150 && "Write proxy via ImageBuf didn't match write file");
151 if (ok)
152 std::cout << term.ansi("green", "OK\n");
153 return ok;
154 }
155
156
157
158 // Helper for test_all_formats: read the pixels of the given disk file into
159 // a buffer, then use an IOProxy to read the "file" from the buffer, and
160 // the pixels ought to match those of ImageBuf buf.
161 static bool
test_read_proxy(string_view formatname,string_view extension,const std::string & disk_filename,const ImageBuf & buf)162 test_read_proxy(string_view formatname, string_view extension,
163 const std::string& disk_filename, const ImageBuf& buf)
164 {
165 bool ok = true;
166 Sysutil::Term term(stdout);
167 std::cout << " Reading Proxy " << formatname << " ... ";
168 std::cout.flush();
169
170 // Read the disk file into readbuf as a blob -- just a byte-for-byte
171 // copy of the file, but in memory.
172 uint64_t bytes_written = Filesystem::file_size(disk_filename);
173 std::vector<unsigned char> readbuf(bytes_written);
174 Filesystem::read_bytes(disk_filename, readbuf.data(), bytes_written);
175
176 // Read the in-memory file using an ioproxy, with ImageInput
177 Filesystem::IOMemReader inproxy(readbuf);
178 std::string memname = Strutil::sprintf("mem.%s", extension);
179 auto in = ImageInput::open(memname, nullptr, &inproxy);
180 OIIO_CHECK_ASSERT(in && "Failed to open input with proxy");
181 if (in) {
182 std::vector<unsigned char> readpixels;
183 ok &= checked_read(in.get(), memname, readpixels, true);
184 ok &= memcmp(readpixels.data(), buf.localpixels(), readpixels.size())
185 == 0;
186 OIIO_CHECK_ASSERT(
187 ok && "Read proxy with ImageInput didn't match original");
188 } else {
189 ok = false;
190 std::cout << "Error was: " << OIIO::geterror() << "\n";
191 }
192
193 // Read the in-memory file using an ioproxy again, but with ImageInput
194 Filesystem::IOMemReader inproxybuf(readbuf);
195 ImageBuf inbuf(memname, 0, 0, nullptr, nullptr, &inproxybuf);
196 bool ok2 = inbuf.read(0, 0, /*force*/ true, TypeFloat);
197 if (!ok2) {
198 std::cout << "Read failed: " << inbuf.geterror() << "\n";
199 OIIO_CHECK_ASSERT(ok2);
200 return false;
201 }
202 OIIO_ASSERT(inbuf.localpixels());
203 OIIO_ASSERT(buf.localpixels());
204 OIIO_CHECK_EQUAL(buf.spec().format, inbuf.spec().format);
205 OIIO_CHECK_EQUAL(buf.spec().image_bytes(), inbuf.spec().image_bytes());
206 ok2 &= memcmp(inbuf.localpixels(), buf.localpixels(),
207 buf.spec().image_bytes())
208 == 0;
209 OIIO_CHECK_ASSERT(ok2 && "Read proxy with ImageBuf didn't match original");
210 ok &= ok2;
211
212 if (ok)
213 std::cout << term.ansi("green", "OK\n");
214 return ok;
215 }
216
217
218
219 // Test writer's ability to detect and recover from errors when asked to
220 // write an unwritable file (such as in a nonexistent directory).
221 static bool
test_write_unwritable(string_view extension,const ImageBuf & buf)222 test_write_unwritable(string_view extension, const ImageBuf& buf)
223 {
224 bool ok = true;
225 Sysutil::Term term(stdout);
226 std::string bad_filename = Strutil::sprintf("bad/bad.%s", extension);
227 std::cout << " Writing bad to " << bad_filename << " ... ";
228 auto badout = ImageOutput::create(bad_filename);
229 if (badout) {
230 std::string errmsg;
231 ok = checked_write(badout.get(), bad_filename, buf.spec(),
232 buf.spec().format, buf.localpixels(),
233 /*do_asserts=*/false, &errmsg);
234 if (!ok)
235 std::cout << term.ansi("green", "OK") << " ("
236 << errmsg.substr(0, 60) << ")\n";
237 else
238 OIIO_CHECK_ASSERT(0 && "Bad write should not have 'succeeded'");
239 } else {
240 OIIO_CHECK_ASSERT(badout);
241 ok = false;
242 }
243 return ok;
244 }
245
246
247
248 static void
test_all_formats()249 test_all_formats()
250 {
251 Sysutil::Term term(stdout);
252 std::cout << "Testing formats:\n";
253 auto all_fmts
254 = Strutil::splitsv(OIIO::get_string_attribute("extension_list"), ";");
255 for (auto& e : all_fmts) {
256 auto fmtexts = Strutil::splitsv(e, ":");
257 string_view formatname = fmtexts[0];
258 // Skip "formats" that aren't amenable to this kind of testing
259 if (formatname == "null" || formatname == "socket"
260 || formatname == "term")
261 continue;
262 // Field3d very finicky. Skip for now. FIXME?
263 if (formatname == "field3d")
264 continue;
265 auto extensions = Strutil::splitsv(fmtexts[1], ",");
266 bool ok = true;
267
268 //
269 // Try writing the file
270 //
271 std::string filename = Strutil::sprintf("imageinout_test-%s.%s",
272 formatname, extensions[0]);
273 auto out = ImageOutput::create(filename);
274 if (!out) {
275 std::cout << " [skipping " << formatname << " -- no writer]\n";
276 (void)OIIO::geterror(); // discard error
277 continue;
278 }
279 bool ioproxy_write_supported = out->supports("ioproxy");
280 std::cout << " " << formatname << " ("
281 << Strutil::join(extensions, ", ") << "):\n";
282
283 ImageBuf buf = make_test_image(formatname);
284 const float* orig_pixels = (const float*)buf.localpixels();
285
286 std::cout << " Writing " << filename << " ... ";
287 ok = checked_write(out.get(), filename, buf.spec(), buf.spec().format,
288 orig_pixels);
289 if (ok)
290 std::cout << term.ansi("green", "OK\n");
291
292 //
293 // Try reading the file, and make sure it matches what we wrote
294 //
295 std::vector<unsigned char> pixels;
296 auto in = ImageInput::create(filename);
297 OIIO_CHECK_ASSERT(in && "Could not create reader");
298 bool ioproxy_read_supported = in && in->supports("ioproxy");
299 if (in) {
300 std::cout << " Reading " << filename << " ... ";
301 ok = checked_read(in.get(), filename, pixels);
302 if (!ok)
303 continue;
304 ok = memcmp(orig_pixels, pixels.data(), pixels.size()) == 0;
305 OIIO_CHECK_ASSERT(ok && "Failed read/write comparison");
306 if (ok)
307 std::cout << term.ansi("green", "OK\n");
308 } else {
309 (void)OIIO::geterror(); // discard error
310 }
311 if (!ok)
312 continue;
313
314 //
315 // If this format supports proxies, round trip through memory
316 //
317 if (ioproxy_write_supported)
318 test_write_proxy(formatname, extensions[0], filename, buf);
319 if (ioproxy_read_supported)
320 test_read_proxy(formatname, extensions[0], filename, buf);
321
322 //
323 // Test what happens when we write to an unwritable or nonexistent
324 // directory. It should not crash! But appropriately return some
325 // error.
326 //
327 test_write_unwritable(extensions[0], buf);
328
329 Filesystem::remove(filename);
330 }
331 std::cout << "\n";
332 }
333
334
335
336 // This tests a particular troublesome case where we got the logic wrong.
337 // Read 1-channel float exr into 4-channel uint8 buffer with 4-byte xstride.
338 // The correct behavior is to translate the one channel from float to uint8
339 // and put it in channel 0, leaving channels 1-3 untouched. The bug was that
340 // because the buffer stride and native stride were both 4 bytes, it was
341 // incorrectly doing a straight data copy.
342 void
test_read_tricky_sizes()343 test_read_tricky_sizes()
344 {
345 // Make 4x4 1-channel float source image, value 0.5, write it.
346 char srcfilename[] = "tmp_f1.exr";
347 ImageSpec fsize1(4, 4, 1, TypeFloat);
348 ImageBuf src(fsize1);
349 ImageBufAlgo::fill(src, 0.5f);
350 src.write(srcfilename);
351
352 // Make a 4x4 4-channel uint8 buffer, initialize with 0
353 unsigned char buf[4][4][4];
354 memset(buf, 0, 4 * 4 * 4);
355
356 // Read in, make sure it's right, several different ways
357 {
358 auto imgin = ImageInput::open(srcfilename);
359 imgin->read_image(TypeUInt8, buf, 4 /* xstride */);
360 OIIO_CHECK_EQUAL(int(buf[0][0][0]), 128);
361 OIIO_CHECK_EQUAL(int(buf[0][0][1]), 0);
362 OIIO_CHECK_EQUAL(int(buf[0][0][2]), 0);
363 OIIO_CHECK_EQUAL(int(buf[0][0][3]), 0);
364 }
365 {
366 memset(buf, 0, 4 * 4 * 4);
367 auto imgin = ImageInput::open(srcfilename);
368 imgin->read_scanlines(0, 0, 0, 4, 0, 0, 4, TypeUInt8, buf,
369 /*xstride=*/4);
370 OIIO_CHECK_EQUAL(int(buf[0][0][0]), 128);
371 OIIO_CHECK_EQUAL(int(buf[0][0][1]), 0);
372 OIIO_CHECK_EQUAL(int(buf[0][0][2]), 0);
373 OIIO_CHECK_EQUAL(int(buf[0][0][3]), 0);
374 }
375 {
376 memset(buf, 0, 4 * 4 * 4);
377 auto imgin = ImageInput::open(srcfilename);
378 for (int y = 0; y < 4; ++y)
379 imgin->read_scanline(y, 0, TypeUInt8, buf, /*xstride=*/4);
380 OIIO_CHECK_EQUAL(int(buf[0][0][0]), 128);
381 OIIO_CHECK_EQUAL(int(buf[0][0][1]), 0);
382 OIIO_CHECK_EQUAL(int(buf[0][0][2]), 0);
383 OIIO_CHECK_EQUAL(int(buf[0][0][3]), 0);
384 }
385 // And repeat for tiled
386 src.set_write_tiles(2, 2);
387 src.write(srcfilename);
388 {
389 memset(buf, 0, 4 * 4 * 4);
390 auto imgin = ImageInput::open(srcfilename);
391 imgin->read_image(TypeUInt8, buf, 4 /* xstride */);
392 OIIO_CHECK_EQUAL(int(buf[0][0][0]), 128);
393 OIIO_CHECK_EQUAL(int(buf[0][0][1]), 0);
394 OIIO_CHECK_EQUAL(int(buf[0][0][2]), 0);
395 OIIO_CHECK_EQUAL(int(buf[0][0][3]), 0);
396 }
397 {
398 memset(buf, 0, 4 * 4 * 4);
399 auto imgin = ImageInput::open(srcfilename);
400 imgin->read_tiles(0, 0, 0, 4, 0, 4, 0, 1, 0, 4, TypeUInt8, buf,
401 /*xstride=*/4);
402 OIIO_CHECK_EQUAL(int(buf[0][0][0]), 128);
403 OIIO_CHECK_EQUAL(int(buf[0][0][1]), 0);
404 OIIO_CHECK_EQUAL(int(buf[0][0][2]), 0);
405 OIIO_CHECK_EQUAL(int(buf[0][0][3]), 0);
406 }
407 {
408 memset(buf, 0, 4 * 4 * 4);
409 auto imgin = ImageInput::open(srcfilename);
410 imgin->read_tile(0, 0, 0, TypeUInt8, buf, /*xstride=*/4);
411 OIIO_CHECK_EQUAL(int(buf[0][0][0]), 128);
412 OIIO_CHECK_EQUAL(int(buf[0][0][1]), 0);
413 OIIO_CHECK_EQUAL(int(buf[0][0][2]), 0);
414 OIIO_CHECK_EQUAL(int(buf[0][0][3]), 0);
415 }
416
417 // Clean up
418 Filesystem::remove(srcfilename);
419 }
420
421
422
423 int
main(int,char * [])424 main(int /*argc*/, char* /*argv*/[])
425 {
426 test_all_formats();
427 test_read_tricky_sizes();
428
429 return unit_test_failures;
430 }
431