merged: bug fixes from trunk, r25757,r25782,r25863
[xbmc:xbmc-antiquated.git] / tools / TexturePacker / XBMCTex.cpp
1 /*
2  *      Copyright (C) 2005-2009 Team XBMC
3  *      http://www.xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, write to
17  *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18  *  http://www.gnu.org/copyleft/gpl.html
19  *
20  */
21
22 #include <sys/types.h>
23 #include <sys/stat.h>
24 #include <dirent.h>
25 #include <squish.h>
26 #include <string>
27 #define __STDC_FORMAT_MACROS
28 #include <inttypes.h>
29 #include <SDL/SDL.h>
30 #include <SDL/SDL_image.h>
31 #include "XBTF.h"
32 #include "XBTFWriter.h"
33 #include "SDL_anigif.h"
34 #include "cmdlineargs.h"
35 #ifdef _WIN32
36 #define strncasecmp strnicmp
37 #endif
38
39 #ifdef _LINUX
40 #include <lzo1x.h>
41 #else
42 #include "../../xbmc/lib/liblzo/LZO1X.H"
43 #endif
44
45 #define DIR_SEPARATOR "/"
46 #define DIR_SEPARATOR_CHAR '/'
47
48 #define FLAGS_USE_LZO     1
49 #define FLAGS_ALLOW_YCOCG 2
50
51 #undef main
52
53 const char *GetFormatString(unsigned int format)
54 {
55   switch (format)
56   {
57   case XB_FMT_DXT1:
58     return "DXT1 ";
59   case XB_FMT_DXT3:
60     return "DXT3 ";
61   case XB_FMT_DXT5:
62     return "DXT5 ";
63   case XB_FMT_DXT5_YCoCg:
64     return "YCoCg";
65   case XB_FMT_A8R8G8B8:
66     return "ARGB ";
67   case XB_FMT_A8:
68     return "A8   ";
69   default:
70     return "?????";
71   }
72 }
73
74 // returns true for png, bmp, tga, jpg and dds files, otherwise returns false
75 bool IsGraphicsFile(char *strFileName)
76 {
77   size_t n = strlen(strFileName);
78   if (n < 4)
79     return false;
80
81   if (strncasecmp(&strFileName[n-4], ".png", 4) &&
82       strncasecmp(&strFileName[n-4], ".bmp", 4) &&
83       strncasecmp(&strFileName[n-4], ".tga", 4) &&
84       strncasecmp(&strFileName[n-4], ".gif", 4) &&
85       strncasecmp(&strFileName[n-4], ".tbn", 4) &&
86       strncasecmp(&strFileName[n-4], ".jpg", 4))
87     return false;
88
89   return true;
90 }
91
92 // returns true for png, bmp, tga, jpg and dds files, otherwise returns false
93 bool IsGIF(const char *strFileName)
94 {
95   size_t n = strlen(strFileName);
96   if (n < 4)
97     return false;
98
99   if (strncasecmp(&strFileName[n-4], ".gif", 4))
100     return false;
101
102   return true;
103 }
104
105 void CreateSkeletonHeaderImpl(CXBTF& xbtf, std::string fullPath, std::string relativePath)
106 {
107   struct dirent* dp;
108   struct stat stat_p;
109   DIR *dirp = opendir(fullPath.c_str());
110
111   while ((dp = readdir(dirp)) != NULL)
112   {
113     if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) 
114     {
115       continue;
116     }
117
118     //stat to check for dir type (reiserfs fix)
119     std::string fileN = fullPath + "/" + dp->d_name;
120     stat(fileN.c_str(), &stat_p);
121
122     if (dp->d_type == DT_DIR || stat_p.st_mode & S_IFDIR)
123     {
124       std::string tmpPath = relativePath;
125       if (tmpPath.size() > 0)
126       {
127         tmpPath += "/";
128       }
129
130       CreateSkeletonHeaderImpl(xbtf, fullPath + DIR_SEPARATOR + dp->d_name, tmpPath + dp->d_name);
131     }
132     else if (IsGraphicsFile(dp->d_name))
133     {
134       std::string fileName = "";
135       if (relativePath.size() > 0)
136       {
137         fileName += relativePath;
138         fileName += "/";
139       }
140
141       fileName += dp->d_name;
142
143       CXBTFFile file;
144       file.SetPath(fileName);
145       xbtf.GetFiles().push_back(file);
146     }
147   }
148
149   closedir(dirp);
150 }
151
152 void CreateSkeletonHeader(CXBTF& xbtf, std::string fullPath)
153 {
154   std::string temp;
155   CreateSkeletonHeaderImpl(xbtf, fullPath, temp);
156 }
157
158 CXBTFFrame appendContent(CXBTFWriter &writer, int width, int height, unsigned char *data, unsigned int size, unsigned int format, unsigned int flags)
159 {
160   CXBTFFrame frame;
161   lzo_uint compressedSize = size;
162   if ((flags & FLAGS_USE_LZO) == FLAGS_USE_LZO)
163   {
164     // grab a temporary buffer for unpacking into
165     squish::u8 *compressed = new squish::u8[size + size / 16 + 64 + 3]; // see simple.c in lzo
166     squish::u8 *working = new squish::u8[LZO1X_999_MEM_COMPRESS];
167     if (compressed && working)
168     {
169       if (lzo1x_999_compress(data, size, compressed, (lzo_uint*)&compressedSize, working) != LZO_E_OK || compressedSize > size)
170       {
171         // compression failed, or compressed size is bigger than uncompressed, so store as uncompressed
172         compressedSize = size;
173         writer.AppendContent(data, size);
174       }
175       else
176       { // success
177         lzo_uint optimSize = size;
178         lzo1x_optimize(compressed, compressedSize, data, &optimSize, NULL);
179         writer.AppendContent(compressed, compressedSize);
180       }
181       delete[] working;
182       delete[] compressed;
183     }
184   }
185   else
186   {
187     writer.AppendContent(data, size);
188   }
189   frame.SetPackedSize(compressedSize);
190   frame.SetUnpackedSize(size);
191   frame.SetWidth(width);
192   frame.SetHeight(height);
193   frame.SetFormat(format);
194   frame.SetDuration(0);
195   return frame;
196 }
197
198 void CompressImage(const squish::u8 *brga, int width, int height, squish::u8 *compressed, unsigned int flags, double &colorMSE, double &alphaMSE)
199 {
200   squish::CompressImage(brga, width, height, compressed, flags | squish::kSourceBGRA);
201   squish::ComputeMSE(brga, width, height, compressed, flags | squish::kSourceBGRA, colorMSE, alphaMSE);
202 }
203
204 CXBTFFrame createXBTFFrame(SDL_Surface* image, CXBTFWriter& writer, double maxMSE, unsigned int flags)
205 {
206   // Convert to ARGB
207   SDL_PixelFormat argbFormat;
208   memset(&argbFormat, 0, sizeof(SDL_PixelFormat));
209   argbFormat.BitsPerPixel = 32;
210   argbFormat.BytesPerPixel = 4;
211
212   // For DXT5 we need RGBA
213 #if SDL_BYTEORDER == SDL_LIL_ENDIAN
214   argbFormat.Amask = 0xff000000;
215   argbFormat.Ashift = 24;
216   argbFormat.Rmask = 0x00ff0000;
217   argbFormat.Rshift = 16;
218   argbFormat.Gmask = 0x0000ff00;
219   argbFormat.Gshift = 8;
220   argbFormat.Bmask = 0x000000ff;
221   argbFormat.Bshift = 0;
222 #else
223   argbFormat.Amask = 0x000000ff;
224   argbFormat.Ashift = 0;
225   argbFormat.Rmask = 0x0000ff00;
226   argbFormat.Rshift = 8;
227   argbFormat.Gmask = 0x00ff0000;
228   argbFormat.Gshift = 16;
229   argbFormat.Bmask = 0xff000000;
230   argbFormat.Bshift = 24;
231 #endif
232
233   SDL_Surface *argbImage = SDL_ConvertSurface(image, &argbFormat, 0);
234
235   unsigned int format = 0;
236   double colorMSE, alphaMSE;
237   squish::u8* argb = (squish::u8 *)argbImage->pixels;
238   unsigned int compressedSize = squish::GetStorageRequirements(image->w, image->h, squish::kDxt5);
239   squish::u8* compressed = new squish::u8[compressedSize];
240   // first try DXT1, which is only 4bits/pixel
241   CompressImage(argb, image->w, image->h, compressed, squish::kDxt1, colorMSE, alphaMSE);
242   if (colorMSE < maxMSE && alphaMSE < maxMSE)
243   { // success - use it
244     compressedSize = squish::GetStorageRequirements(image->w, image->h, squish::kDxt1);
245     format = XB_FMT_DXT1;
246   }
247   if (!format && alphaMSE == 0 && (flags & FLAGS_ALLOW_YCOCG) == FLAGS_ALLOW_YCOCG)
248   { // no alpha channel, so DXT5YCoCg is going to be the best DXT5 format
249 /*    CompressImage(argb, image->w, image->h, compressed, squish::kDxt5 | squish::kUseYCoCg, colorMSE, alphaMSE);
250     if (colorMSE < maxMSE && alphaMSE < maxMSE)
251     { // success - use it
252       compressedSize = squish::GetStorageRequirements(image->w, image->h, squish::kDxt5);
253       format = XB_FMT_DXT5_YCoCg;
254     }
255     */
256   }
257   if (!format)
258   { // try DXT3 and DXT5 - use whichever is better (color is the same, but alpha will be different)
259     CompressImage(argb, image->w, image->h, compressed, squish::kDxt3, colorMSE, alphaMSE);
260     if (colorMSE < maxMSE)
261     { // color is fine, test DXT5 as well
262       double dxt5MSE;
263       squish::u8* compressed2 = new squish::u8[squish::GetStorageRequirements(image->w, image->h, squish::kDxt5)];
264       CompressImage(argb, image->w, image->h, compressed2, squish::kDxt5, colorMSE, dxt5MSE);
265       if (alphaMSE < maxMSE && alphaMSE < dxt5MSE)
266       { // DXT3 passes and is best
267         compressedSize = squish::GetStorageRequirements(image->w, image->h, squish::kDxt3);
268         format = XB_FMT_DXT3;
269       }
270       else if (dxt5MSE < maxMSE)
271       { // DXT5 passes
272         compressedSize = squish::GetStorageRequirements(image->w, image->h, squish::kDxt5);
273         memcpy(compressed, compressed2, compressedSize);
274         format = XB_FMT_DXT5;
275       }
276       delete[] compressed2;
277     }
278   }
279   CXBTFFrame frame; 
280   if (!format)
281   { // none of the compressed stuff works for us, so we use 32bit texture
282     format = XB_FMT_A8R8G8B8;
283     frame = appendContent(writer, image->w, image->h, argb, image->w * image->h * 4, format, flags);
284   }
285   else
286   {
287     frame = appendContent(writer, image->w, image->h, compressed, compressedSize, format, flags);
288   }
289   delete[] compressed;
290   SDL_FreeSurface(argbImage);
291   return frame;
292 }
293
294 void Usage()
295 {
296   puts("Usage:");
297   puts("  -help            Show this screen.");
298   puts("  -input <dir>     Input directory. Default: current dir");
299   puts("  -output <dir>    Output directory/filename. Default: Textures.xpr");
300 }
301
302 int createBundle(const std::string& InputDir, const std::string& OutputFile, double maxMSE, unsigned int flags)
303 {
304   CXBTF xbtf;
305   CreateSkeletonHeader(xbtf, InputDir);
306
307   CXBTFWriter writer(xbtf, OutputFile);
308   if (!writer.Create())
309   {
310     printf("Error creating file\n");
311     return 1;
312   }
313
314   std::vector<CXBTFFile>& files = xbtf.GetFiles();
315   for (size_t i = 0; i < files.size(); i++)
316   {
317     CXBTFFile& file = files[i];
318
319     std::string fullPath = InputDir;
320     fullPath += file.GetPath();
321
322     std::string output = file.GetPath();
323     output = output.substr(0, 40);
324     while (output.size() < 46)
325       output += ' ';
326     if (!IsGIF(fullPath.c_str()))
327     {
328       // Load the image
329       SDL_Surface* image = IMG_Load(fullPath.c_str());
330       if (!image)
331       {
332         printf("...unable to load image %s\n", file.GetPath());
333         continue;
334       }
335
336       printf("%s", output.c_str());
337
338       CXBTFFrame frame = createXBTFFrame(image, writer, maxMSE, flags);
339
340       printf("%s (%d,%d @ %"PRIu64" bytes)\n", GetFormatString(frame.GetFormat()), frame.GetWidth(), frame.GetHeight(), frame.GetUnpackedSize());
341
342       file.SetLoop(0);
343       file.GetFrames().push_back(frame);
344
345       SDL_FreeSurface(image);
346     }
347     else
348     {
349       int gnAG = AG_LoadGIF(fullPath.c_str(), NULL, 0);
350       AG_Frame* gpAG = new AG_Frame[gnAG];
351       AG_LoadGIF(fullPath.c_str(), gpAG, gnAG);
352
353       printf("%s\n", output.c_str());
354
355       for (int j = 0; j < gnAG; j++)
356       {
357         printf("    frame %4i                                ", j);
358         CXBTFFrame frame = createXBTFFrame(gpAG[j].surface, writer, maxMSE, flags);
359         frame.SetDuration(gpAG[j].delay);
360         file.GetFrames().push_back(frame);
361         printf("%s (%d,%d @ %"PRIu64" bytes)\n", GetFormatString(frame.GetFormat()), frame.GetWidth(), frame.GetHeight(), frame.GetUnpackedSize());
362       }
363
364       AG_FreeSurfaces(gpAG, gnAG);
365       delete [] gpAG;
366
367       file.SetLoop(0);
368     }
369   }
370
371   if (!writer.UpdateHeader())
372   {
373     printf("Error writing header to file\n");
374     return 1;
375   }
376
377   if (!writer.Close())
378   {
379     printf("Error closing file\n");
380     return 1;
381   }
382
383   return 0;
384 }
385
386 int main(int argc, char* argv[])
387 {
388   if (lzo_init() != LZO_E_OK)
389     return 1;
390
391   bool valid = false;
392   CmdLineArgs args(argc, (const char**)argv);
393
394   if (args.size() == 1)
395   {
396     Usage();
397     return 1;
398   }
399
400   std::string InputDir;
401   std::string OutputFilename = "Textures.xbt";
402
403   for (unsigned int i = 1; i < args.size(); ++i)
404   {
405     if (!stricmp(args[i], "-help") || !stricmp(args[i], "-h") || !stricmp(args[i], "-?"))
406     {
407       Usage();
408       return 1;
409     }
410     else if (!stricmp(args[i], "-input") || !stricmp(args[i], "-i"))
411     {
412       InputDir = args[++i];
413       valid = true;
414     }
415     else if (!stricmp(args[i], "-output") || !stricmp(args[i], "-o"))
416     {
417       OutputFilename = args[++i];
418       valid = true;
419 #ifdef _LINUX
420       char *c = NULL;
421       while ((c = (char *)strchr(OutputFilename.c_str(), '\\')) != NULL) *c = '/';
422 #endif
423     }
424     else
425     {
426       printf("Unrecognized command line flag: %s\n", args[i]);
427     }
428   }
429
430   if (!valid)
431   {
432     Usage();
433     return 1;
434   }
435
436   size_t pos = InputDir.find_last_of(DIR_SEPARATOR);
437   if (pos != InputDir.length() - 1)
438     InputDir += DIR_SEPARATOR;
439
440   double maxMSE = 1.5;    // HQ only please
441   unsigned int flags = FLAGS_USE_LZO; // TODO: currently no YCoCg (commandline option?)
442   createBundle(InputDir, OutputFilename, maxMSE, flags);
443 }