1 //
2 // SPDX-License-Identifier: BSD-3-Clause
3 // Copyright (c) Contributors to the OpenEXR Project.
4 //
5
6
7 //-----------------------------------------------------------------------------
8 //
9 // Utility program to combine or separate multipart image files
10 //
11 //-----------------------------------------------------------------------------
12
13
14 #include <ImfMultiPartOutputFile.h>
15 #include <ImfMultiPartInputFile.h>
16 #include <ImfStringAttribute.h>
17 #include <ImfChannelList.h>
18 #include <ImfTiledInputPart.h>
19 #include <ImfTiledOutputPart.h>
20 #include <ImfInputPart.h>
21 #include <ImfOutputPart.h>
22 #include <ImfDeepScanLineInputPart.h>
23 #include <ImfDeepScanLineOutputPart.h>
24 #include <ImfDeepTiledInputPart.h>
25 #include <ImfDeepTiledOutputPart.h>
26 #include <ImfPartHelper.h>
27 #include <ImfPartType.h>
28
29 #include <OpenEXRConfig.h>
30 #include <Iex.h>
31
32 #include <iostream>
33 #include <vector>
34 #include <utility> // pair
35 #include <stdlib.h>
36 #include <sstream>
37 #include <assert.h>
38 #include <cctype>
39 #include <algorithm>
40
41 using std::cerr;
42 using std::cout;
43 using std::endl;
44 using std::vector;
45 using std::pair;
46 using std::set;
47 using std::ostringstream;
48 using std::min;
49 using std::max;
50 using std::string;
51 using std::make_pair;
52 using IMATH_NAMESPACE::Box2i;
53
54 using namespace OPENEXR_IMF_NAMESPACE;
55
56
57 #if defined(ANDROID) || defined(__ANDROID_API__)
58 #define IMF_PATH_SEPARATOR "/"
59 #elif defined(_WIN32) || defined(_WIN64)
60 #define IMF_PATH_SEPARATOR "\\"
61 #else
62 #define IMF_PATH_SEPARATOR "/"
63 #endif
64
65
66 void
copy_tile(MultiPartInputFile & input,MultiPartOutputFile & output,int inPart,int outPart)67 copy_tile (MultiPartInputFile & input,
68 MultiPartOutputFile & output,
69 int inPart, int outPart)
70 {
71 TiledInputPart in (input, inPart);
72 TiledOutputPart out (output, outPart);
73
74 out.copyPixels (in);
75 }
76
77 void
copy_tiledeep(MultiPartInputFile & input,MultiPartOutputFile & output,int inPart,int outPart)78 copy_tiledeep (MultiPartInputFile & input,
79 MultiPartOutputFile & output,
80 int inPart, int outPart)
81 {
82 DeepTiledInputPart in (input, inPart);
83 DeepTiledOutputPart out (output, outPart);
84
85 out.copyPixels (in);
86 }
87
88 void
copy_scanline(MultiPartInputFile & input,MultiPartOutputFile & output,int inPart,int outPart)89 copy_scanline (MultiPartInputFile & input,
90 MultiPartOutputFile & output,
91 int inPart, int outPart)
92 {
93 InputPart in (input, inPart);
94 OutputPart out (output, outPart);
95
96 out.copyPixels (in);
97 }
98
99 void
copy_scanlinedeep(MultiPartInputFile & input,MultiPartOutputFile & output,int inPart,int outPart)100 copy_scanlinedeep (MultiPartInputFile & input,
101 MultiPartOutputFile & output,
102 int inPart, int outPart)
103 {
104 DeepScanLineInputPart in (input, inPart);
105 DeepScanLineOutputPart out (output, outPart);
106
107 out.copyPixels (in);
108 }
109
110 bool
is_number(const std::string & s)111 is_number(const std::string& s)
112 {
113 std::string::const_iterator it = s.begin();
114 while (it != s.end() && std::isdigit(*it)) ++it;
115 return !s.empty() && it == s.end();
116 }
117
118 void
parse_partname(string & part_name)119 parse_partname (string & part_name)
120 {
121 // strip off a path delimitation
122 size_t posSlash = part_name.rfind(IMF_PATH_SEPARATOR);
123 if (posSlash != string::npos)
124 part_name = part_name.substr(posSlash+1);
125
126
127 // strip the exr ext
128 size_t pos = part_name.rfind(".exr");
129 if (pos != string::npos)
130 {
131 part_name = part_name.substr(0, pos);
132
133 // strip off the frame number
134 size_t pos2 = part_name.rfind(".");
135 if (pos2 != string::npos)
136 {
137 string frame = part_name.substr(pos2+1, pos);
138 if (is_number(frame))
139 part_name = part_name.substr(0, pos2);
140 }
141 }
142 }
143
144
145 ///
146 /// If input is <...>::<partname] then extract part name
147 /// Else, use the stripped version of the filename for partname
148 /// If input is <file>:<partnum> then extract part number,
149 /// Else, use all parts
150 ///
151 void
parse_filename(string & file_name,string & part_name,bool & force_part_name,int & part_num)152 parse_filename (string & file_name,
153 string & part_name,
154 bool & force_part_name,
155 int & part_num)
156 {
157 force_part_name = false;
158 part_name = file_name; // default is the file_name
159 size_t doublecolon = file_name.rfind ("::");
160 if (doublecolon != string::npos)
161 {
162 part_name = file_name.substr (doublecolon+2);
163 file_name = file_name.substr (0, doublecolon);
164 force_part_name = true;
165 }
166 else
167 {
168 parse_partname(part_name);
169 }
170
171 size_t colon = file_name.rfind (':');
172 if (colon == string::npos)
173 {
174 part_num = -1; // use all parts
175 }
176 else
177 {
178 string num = file_name.substr (colon + 1);
179 if (is_number(num))
180 {
181 part_num = atoi (num.c_str());
182 file_name = file_name.substr (0, colon);
183 }
184 else
185 {
186 cerr <<"\n" << "ERROR: part number must be a number" << endl;
187 exit(1);
188 }
189 }
190 }
191
192
193 void
make_unique_names(vector<Header> & headers)194 make_unique_names (vector<Header> & headers)
195 {
196 set<string> names;
197 for ( size_t i = 0 ; i < headers.size() ; i++ )
198 {
199 Header & h = headers[i];
200 std::string base_name;
201
202 // if no name at all, set it to <type><partnum> (first part is part 1)
203 if (!h.hasName())
204 {
205 // We should not be here, we have populated all headers with names.
206 cerr << "\n" << "Software Error: header does not have a valid name"
207 << ":" << __LINE__ << endl;
208 exit (1);
209 }
210 else
211 {
212 base_name = h.name();
213 }
214
215
216 // check name has already been used, if so add a _<number> to it
217 if (names.find (base_name) != names.end())
218 {
219 ostringstream s;
220 size_t backup=1;
221 do
222 {
223 s.clear();
224 s << h.name() << "_" << i << "_" << backup;
225 backup++;
226 }
227 while (names.find(s.str()) != names.end());
228 h.setName (s.str());
229 }
230
231 names.insert (h.name());
232 }
233 }
234
235 void
filename_check(vector<string> names,const char * aname)236 filename_check (vector <string> names, const char* aname)
237 {
238 string bname(aname);
239 for (size_t i = 0; i < names.size(); i++)
240 {
241 if (bname.compare (names[i]) == 0)
242 {
243 cerr << "\n" << "ERROR: "
244 "input and output file names cannot be the same." << endl;
245 exit (1);
246 }
247 }
248 }
249
250 void
convert(vector<const char * > in,vector<const char * > views,const char * outname,bool override)251 convert(vector <const char*> in,
252 vector<const char *> views,
253 const char* outname,
254 bool override)
255 {
256 if(in.size()!=1)
257 {
258 cerr <<"\n" << "ERROR: "
259 "can only convert one file at once - use 'combine' mode for multiple files" << endl;
260 exit(1);
261 }
262 try
263 {
264 MultiPartInputFile infile(in[0]);
265
266 if(infile.parts() != 1)
267 {
268 cerr <<"\n" << "ERROR: "
269 "can only convert single part EXRs to multipart EXR-2.0 files: use 'split' mode instead" << endl;
270 exit(1);
271 }
272
273 vector<MultiViewChannelName> input_channels;
274
275 string hero;
276 if(hasMultiView(infile.header(0)))
277 {
278 StringVector h = multiView (infile.header(0));
279 if(h.size()>0)
280 {
281 hero=h[0];
282 }
283 }
284
285 // retrieve channel names from input file in view-friendly format
286 GetChannelsInMultiPartFile(infile,input_channels);
287
288
289 vector< MultiViewChannelName > output_channels = input_channels;
290 // remap channels to multiple output parts
291 int parts = SplitChannels(output_channels.begin(),output_channels.end(),true,hero);
292
293 vector<Header> output_headers(parts);
294 vector<FrameBuffer> output_framebuffers(parts);
295 FrameBuffer input_framebuffer;
296
297 //
298 // make all output headers the same as the input header but
299 // with no channels
300 //
301 for(int i=0;i<parts;i++)
302 {
303 Header & h = output_headers[i];
304 h = infile.header(0);
305 if (hasMultiView(h))
306 h.erase("multiView");
307
308 string fn, pname;
309 int pnum;
310 bool pforce;
311 parse_filename(fn, pname, pforce, pnum);
312 if (!h.hasName() || pforce)
313 h.setName (pname);
314
315 h.channels()=ChannelList();
316 }
317
318 make_unique_names(output_headers);
319
320 const ChannelList & in_chanlist = infile.header(0).channels();
321
322 int channel_count = 0;
323 for(ChannelList::ConstIterator i=in_chanlist.begin();i!=in_chanlist.end();++i)
324 {
325 ++channel_count;
326 }
327
328 Box2i dataWindow = infile.header(0).dataWindow();
329 int pixel_count = (dataWindow.size().y+1)*(dataWindow.size().x+1);
330 int pixel_width = dataWindow.size().x+1;
331
332
333 // offset in pixels between base of array and 0,0
334 int pixel_base = dataWindow.min.y*pixel_width+dataWindow.min.x;
335
336 vector< vector<char> > channelstore(channel_count);
337
338
339 //
340 // insert channels into correct header and framebuffers
341 //
342 for( size_t i=0 ; i<input_channels.size() ; i++ )
343 {
344 // read the part we should be writing channel into, insert into header
345 int part = output_channels[i].part_number;
346 ChannelList::ConstIterator chan = in_chanlist.find(input_channels[i].internal_name);
347 Header & h = output_headers[part];
348 h.channels().insert(output_channels[i].name,chan.channel());
349
350 if( output_channels[i].view!="" )
351 {
352 h.setView( output_channels[i].view );
353 }
354
355 // compute size of channel
356 size_t samplesize=sizeof(float);
357 if(chan.channel().type==HALF)
358 {
359 samplesize=sizeof(half);
360 }
361 channelstore[i].resize(samplesize*pixel_count);
362
363 output_framebuffers[part].insert(output_channels[i].name,
364 Slice(chan.channel().type,
365 &channelstore[i][0]-pixel_base*samplesize,
366 samplesize,
367 pixel_width*samplesize));
368
369 input_framebuffer.insert(input_channels[i].internal_name,
370 Slice(chan.channel().type,
371 &channelstore[i][0] - pixel_base*samplesize,
372 samplesize,
373 pixel_width*samplesize));
374
375 }
376
377 //
378 // create output file
379 //
380 MultiPartOutputFile outfile(outname,&output_headers[0],
381 output_headers.size());
382 InputPart inpart(infile,0);
383
384
385 //
386 // read file
387 //
388 inpart.setFrameBuffer(input_framebuffer);
389 inpart.readPixels(dataWindow.min.y,dataWindow.max.y);
390
391 //
392 // write each part
393 //
394
395 for(size_t i=0;i<output_framebuffers.size();i++)
396 {
397 OutputPart outpart(outfile,i);
398 outpart.setFrameBuffer(output_framebuffers[i]);
399 outpart.writePixels(dataWindow.max.y+1-dataWindow.min.y);
400 }
401
402
403 }
404 catch (IEX_NAMESPACE::BaseExc &e)
405 {
406 cerr << "\n" << "ERROR:" << endl;
407 cerr << e.what() << endl;
408 exit (1);
409 }
410
411 }
412
413
414
415 void
combine(vector<const char * > in,vector<const char * > views,const char * outname,bool override)416 combine (vector <const char*> in,
417 vector<const char *> views,
418 const char* outname,
419 bool override)
420 {
421 size_t numInputs = in.size();
422 int numparts;
423 vector<int> partnums;
424 vector<MultiPartInputFile *> inputs;
425 vector<MultiPartInputFile *> fordelete;
426 MultiPartInputFile *infile;
427 vector<Header> headers;
428 vector<string> fornamecheck;
429
430 //
431 // parse all inputs
432 //
433
434 // Input format :
435 // We support the following syntax for each input
436 // <file>[:<partnum>][::<newpartname>]
437
438 for (size_t i = 0 ; i < numInputs; i++)
439 {
440 string filename (in[i]);
441 string partname;
442 bool forcepartname;
443 int partnum;
444 parse_filename (filename, partname, forcepartname, partnum);
445
446 if (partnum == -1)
447 {
448 fornamecheck.push_back (filename);
449
450 try
451 {
452 infile = new MultiPartInputFile (filename.c_str());
453 fordelete.push_back (infile);
454 numparts = infile->parts();
455
456 //copy header from all parts of input to our header array
457 for (int j = 0; j < numparts; j++)
458 {
459 inputs.push_back (infile);
460
461 Header h = infile->header(j);
462 if (!h.hasName() || forcepartname)
463 h.setName (partname);
464
465 headers.push_back (h);
466
467 if( views[i] != NULL )
468 headers[headers.size()-1].setView( views[i] );
469
470 partnums.push_back (j);
471 }
472 }
473 catch (IEX_NAMESPACE::BaseExc &e)
474 {
475 cerr << "\n" << "ERROR:" << endl;
476 cerr << e.what() << endl;
477 exit (1);
478 }
479 } // no user parts specified
480 else
481 {
482 fornamecheck.push_back (filename);
483
484 try
485 {
486 infile = new MultiPartInputFile (filename.c_str());
487 fordelete.push_back (infile);
488
489 if (partnum >= infile->parts())
490 {
491 cerr << "ERROR: you asked for part " << partnum << " in " << in[i];
492 cerr << ", which only has " << infile->parts() << " parts\n";
493 exit (1);
494 }
495 //copy header from required part of input to our header array
496 inputs.push_back (infile);
497
498 Header h = infile->header(partnum);
499 if (!h.hasName() || forcepartname)
500 h.setName (partname);
501
502 headers.push_back (h);
503
504
505 if( views[i] != NULL )
506 headers[headers.size()-1].setView( views[i] );
507
508 partnums.push_back (partnum);
509 }
510 catch (IEX_NAMESPACE::BaseExc &e)
511 {
512 cerr << "\n" << "ERROR:" << endl;
513 cerr << e.what()<< endl;
514 exit (1);
515 }
516 } // user parts specified
517 }
518
519 filename_check (fornamecheck, outname);
520 //
521 // sort out names - make unique
522 //
523 if (numInputs>1)
524 {
525 make_unique_names (headers);
526 }
527
528 //
529 // do combine
530 //
531
532 // early bail if need be
533 try
534 {
535 MultiPartOutputFile temp (outname, &headers[0], headers.size(), override);
536 }
537 catch (IEX_NAMESPACE::BaseExc &e)
538 {
539 cerr << "\n" << "ERROR: " << e.what() << endl;
540 exit (1);
541 }
542 MultiPartOutputFile out (outname, &headers[0], headers.size(), override);
543
544 for (size_t p = 0 ; p < partnums.size();p++)
545 {
546 std::string type = headers[p].type();
547 if (type == SCANLINEIMAGE)
548 {
549 cout << "part " << p << ": "<< "scanlineimage" << endl;
550 copy_scanline (*inputs[p], out, partnums[p], p);
551 }
552 else if (type == TILEDIMAGE)
553 {
554 cout << "part " << p << ": "<< "tiledimage" << endl;
555 copy_tile (*inputs[p], out, partnums[p], p);
556 }
557 else if (type == DEEPSCANLINE)
558 {
559 cout << "part " << p << ": "<< "deepscanlineimage" << endl;
560 copy_scanlinedeep (*inputs[p], out, partnums[p], p);
561 }
562 else if (type == DEEPTILE)
563 {
564 cout << "part " << p << ": "<< "deeptile" << endl;
565 copy_tiledeep (*inputs[p], out, partnums[p], p);
566 }
567 }
568
569
570 for (size_t k = 0; k < fordelete.size(); k++)
571 {
572 delete fordelete[k];
573 }
574
575 inputs.clear();
576 fordelete.size();
577
578 cout << "\n" << "Combine Success" << endl;
579 }
580
581 void
separate(vector<const char * > in,const char * out,bool override)582 separate (vector <const char*> in, const char* out, bool override)
583 {
584 if (in.size() > 1)
585 {
586 cerr << "ERROR: -separate only take one input file\n"
587 "syntax: exrmultipart -separate -i infile.exr -o outfileBaseName\n";
588 exit (1);
589 }
590
591 //
592 // parse the multipart input
593 //
594 string filename (in[0]);
595 MultiPartInputFile *inputimage;
596 int numOutputs;
597 vector<string> fornamecheck;
598
599 // add check for existance of the file
600 try
601 {
602 MultiPartInputFile temp (filename.c_str());
603 }
604 catch (IEX_NAMESPACE::BaseExc &e)
605 {
606 cerr << "\n" << "ERROR: " << e.what() << endl;
607 exit (1);
608 }
609
610 inputimage = new MultiPartInputFile (filename.c_str());
611 numOutputs = inputimage->parts();
612 cout << "numOutputs: " << numOutputs << endl;
613
614 //
615 // set outputs names
616 //
617 for (int p = 0 ; p <numOutputs;p++)
618 {
619 string outfilename (out);
620
621 //add number to outfilename
622 std::ostringstream oss;
623 oss << '.' << p + 1;
624 outfilename += oss.str();
625 outfilename += ".exr";
626 cout << "outputfilename: " << outfilename << endl;
627 fornamecheck.push_back (outfilename);
628 }
629
630 filename_check (fornamecheck, in[0]);
631
632 //
633 // separate outputs
634 //
635 for (int p = 0 ; p < numOutputs; p++)
636 {
637 Header header = inputimage->header (p);
638
639 MultiPartOutputFile out (fornamecheck[p].c_str(), &header, 1, override);
640
641 std::string type = header.type();
642 if (type == "scanlineimage")
643 {
644 cout << "scanlineimage" << endl;
645 copy_scanline (*inputimage, out, p, 0);
646 }
647 else if (type == "tiledimage")
648 {
649 cout << "tiledimage" << endl;
650 copy_tile (*inputimage, out, p, 0);
651 }
652 else if (type == "deepscanline")
653 {
654 cout << "deepscanline" << endl;
655 copy_scanlinedeep (*inputimage, out, p, 0);
656 }
657 else if (type == "deeptile")
658 {
659 cout << "deeptile" << endl;
660 copy_tiledeep (*inputimage, out, p, 0);
661 }
662 }
663
664 delete inputimage;
665 cout << "\n" << "Seperate Success" << endl;
666 }
667
668 void
usageMessage(const char argv[])669 usageMessage (const char argv[])
670 {
671 cout << argv << " handles the combining and splitting of multipart data\n";
672 cout << "\n" << "Usage: "
673 "exrmultipart -combine -i input.exr[:partnum][::partname] "
674 "[input2.exr[:partnum]][::partname] [...] -o outfile.exr [options]\n";
675 cout << " or: exrmultipart -separate -i infile.exr -o outfileBaseName "
676 "[options]\n";
677 cout << " or: exrmultipart -convert -i infile.exr -o outfile.exr "
678 "[options]\n";
679 cout << "\n" << "Options:\n";
680 cout << "-override [0/1] 0-do not override conflicting shared "
681 "attributes [default]\n"
682 " 1-override conflicting shared attributes\n";
683
684 cout << "-view name (after specifying -i) "
685 "assign following inputs to view 'name'\n";
686 exit (1);
687 }
688
689 int
main(int argc,char * argv[])690 main (int argc, char * argv[])
691 {
692 if (argc < 6)
693 {
694 usageMessage (argv[0]);
695 }
696
697 vector <const char*> inFiles;
698 vector <const char*> views;
699 const char* view = 0;
700 const char *outFile = 0;
701 bool override = false;
702
703 int i = 1;
704 int mode = 0; // 0-do not read input, 1-infiles, 2-outfile, 3-override, 4-view
705
706 while (i < argc)
707 {
708 if (!strcmp (argv[i], "-h"))
709 {
710 usageMessage (argv[0]);
711 }
712
713 if (!strcmp (argv[i], "-i"))
714 {
715 mode = 1;
716 }
717 else if (!strcmp (argv[i], "-o"))
718 {
719 mode = 2;
720 }
721 else if (!strcmp (argv[i], "-override"))
722 {
723 mode = 3;
724 }
725 else if (!strcmp (argv[i], "-view"))
726 {
727 if(mode !=1 )
728 {
729 usageMessage (argv[0]);
730 return 1;
731 }
732 mode = 4;
733 }
734 else
735 {
736 switch (mode)
737 {
738 case 1:
739 inFiles.push_back (argv[i]);
740 views.push_back (view);
741 break;
742 case 2: outFile = argv[i];
743 break;
744 case 3: override = atoi (argv[i]);
745 break;
746 case 4: view = argv[i];
747 mode=1;
748 break;
749 }
750 }
751 i++;
752 }
753
754 // check input and output files found or not
755 if (inFiles.size() == 0)
756 {
757 cerr << "\n" << "ERROR: found no input files" << endl;
758 exit (1);
759 }
760
761 cout << "input:" << endl;
762 for (size_t i = 0; i < inFiles.size(); i++)
763 {
764 cout << " " << inFiles[i];
765 if(views[i]) cout << " in view " << views[i];
766 cout << endl;
767 }
768
769 if (!outFile)
770 {
771 cerr << "\n"<<"ERROR: found no output file" << endl;
772 exit (1);
773 }
774
775 cout << "output:\n " << outFile << endl;
776 cout << "override:" << override << "\n" << endl;
777
778
779 if (!strcmp (argv[1], "-combine"))
780 {
781 cout << "-combine multipart input " << endl;
782 combine (inFiles, views, outFile, override);
783 }
784 else if (!strcmp(argv[1], "-separate"))
785 {
786 cout << "-separate multipart input " << endl;
787 separate (inFiles, outFile, override);
788 }
789 else if(!strcmp(argv[1],"-convert"))
790 {
791 cout << "-convert input to EXR2 multipart" << endl;
792 convert (inFiles, views, outFile, override);
793 }
794 else
795 {
796 usageMessage (argv[0]);
797 }
798
799 return 0;
800 }
801