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