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