2 * Copyright (C) 2005-2013 Team XBMC
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)
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.
15 * You should have received a copy of the GNU General Public License
16 * along with XBMC; see the file COPYING. If not, see
17 * <http://www.gnu.org/licenses/>.
22 #include <sys/types.h>
24 #define __STDC_FORMAT_MACROS
34 #include <SDL/SDL_image.h>
37 #include "guilib/XBTF.h"
38 #include "XBTFWriter.h"
40 #include "SDL_anigif.h"
41 #include "cmdlineargs.h"
42 #include "libsquish/squish.h"
45 #define strncasecmp strnicmp
48 #ifdef USE_LZO_PACKING
50 #include "../../lib/win32/liblzo/LZO1X.H"
52 #include <lzo/lzo1x.h>
58 #define FLAGS_USE_LZO 1
59 #define FLAGS_ALLOW_YCOCG 2
60 #define FLAGS_USE_DXT 4
62 #define DIR_SEPARATOR "/"
63 #define DIR_SEPARATOR_CHAR '/'
76 const char *GetFormatString(unsigned int format)
86 case XB_FMT_DXT5_YCoCg:
97 // returns true for png, bmp, tga, jpg and dds files, otherwise returns false
98 bool IsGraphicsFile(char *strFileName)
100 size_t n = strlen(strFileName);
104 if (strncasecmp(&strFileName[n-4], ".png", 4) &&
105 strncasecmp(&strFileName[n-4], ".bmp", 4) &&
106 strncasecmp(&strFileName[n-4], ".tga", 4) &&
107 strncasecmp(&strFileName[n-4], ".gif", 4) &&
108 strncasecmp(&strFileName[n-4], ".tbn", 4) &&
109 strncasecmp(&strFileName[n-4], ".jpg", 4))
115 // returns true for png, bmp, tga, jpg and dds files, otherwise returns false
116 bool IsGIF(const char *strFileName)
118 size_t n = strlen(strFileName);
122 if (strncasecmp(&strFileName[n-4], ".gif", 4))
128 void CreateSkeletonHeaderImpl(CXBTF& xbtf, std::string fullPath, std::string relativePath)
132 DIR *dirp = opendir(fullPath.c_str());
136 while ((dp = readdir(dirp)) != NULL)
138 if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0)
143 //stat to check for dir type (reiserfs fix)
144 std::string fileN = fullPath + "/" + dp->d_name;
145 if (stat(fileN.c_str(), &stat_p) == 0)
147 if (dp->d_type == DT_DIR || stat_p.st_mode & S_IFDIR)
149 std::string tmpPath = relativePath;
150 if (tmpPath.size() > 0)
155 CreateSkeletonHeaderImpl(xbtf, fullPath + DIR_SEPARATOR + dp->d_name, tmpPath + dp->d_name);
157 else if (IsGraphicsFile(dp->d_name))
159 std::string fileName = "";
160 if (relativePath.size() > 0)
162 fileName += relativePath;
166 fileName += dp->d_name;
169 file.SetPath(fileName);
170 xbtf.GetFiles().push_back(file);
179 printf("Error opening %s (%s)\n", fullPath.c_str(), strerror(errno));
183 void CreateSkeletonHeader(CXBTF& xbtf, std::string fullPath)
186 CreateSkeletonHeaderImpl(xbtf, fullPath, temp);
189 CXBTFFrame appendContent(CXBTFWriter &writer, int width, int height, unsigned char *data, unsigned int size, unsigned int format, bool hasAlpha, unsigned int flags)
192 #ifdef USE_LZO_PACKING
193 lzo_uint packedSize = size;
195 if ((flags & FLAGS_USE_LZO) == FLAGS_USE_LZO)
197 // grab a temporary buffer for unpacking into
198 packedSize = size + size / 16 + 64 + 3; // see simple.c in lzo
199 unsigned char *packed = new unsigned char[packedSize];
200 unsigned char *working = new unsigned char[LZO1X_999_MEM_COMPRESS];
201 if (packed && working)
203 if (lzo1x_999_compress(data, size, packed, &packedSize, working) != LZO_E_OK || packedSize > size)
205 // compression failed, or compressed size is bigger than uncompressed, so store as uncompressed
207 writer.AppendContent(data, size);
211 lzo_uint optimSize = size;
212 if (lzo1x_optimize(packed, packedSize, data, &optimSize, NULL) != LZO_E_OK || optimSize != size)
213 { //optimisation failed
215 writer.AppendContent(data, size);
219 writer.AppendContent(packed, packedSize);
228 unsigned int packedSize = size;
231 writer.AppendContent(data, size);
233 frame.SetPackedSize(packedSize);
234 frame.SetUnpackedSize(size);
235 frame.SetWidth(width);
236 frame.SetHeight(height);
237 frame.SetFormat(hasAlpha ? format : format | XB_FMT_OPAQUE);
238 frame.SetDuration(0);
242 void CompressImage(const squish::u8 *brga, int width, int height, squish::u8 *compressed, unsigned int flags, double &colorMSE, double &alphaMSE)
244 squish::CompressImage(brga, width, height, compressed, flags | squish::kSourceBGRA);
245 squish::ComputeMSE(brga, width, height, compressed, flags | squish::kSourceBGRA, colorMSE, alphaMSE);
248 bool HasAlpha(unsigned char *argb, unsigned int width, unsigned int height)
250 unsigned char *p = argb + 3; // offset of alpha
251 for (unsigned int i = 0; i < 4*width*height; i += 4)
259 CXBTFFrame createXBTFFrame(SDL_Surface* image, CXBTFWriter& writer, double maxMSE, unsigned int flags)
262 SDL_PixelFormat argbFormat;
263 memset(&argbFormat, 0, sizeof(SDL_PixelFormat));
264 argbFormat.BitsPerPixel = 32;
265 argbFormat.BytesPerPixel = 4;
267 // For DXT5 we need RGBA
268 #if defined(HOST_BIGENDIAN)
269 argbFormat.Amask = 0x000000ff;
270 argbFormat.Ashift = 0;
271 argbFormat.Rmask = 0x0000ff00;
272 argbFormat.Rshift = 8;
273 argbFormat.Gmask = 0x00ff0000;
274 argbFormat.Gshift = 16;
275 argbFormat.Bmask = 0xff000000;
276 argbFormat.Bshift = 24;
278 argbFormat.Amask = 0xff000000;
279 argbFormat.Ashift = 24;
280 argbFormat.Rmask = 0x00ff0000;
281 argbFormat.Rshift = 16;
282 argbFormat.Gmask = 0x0000ff00;
283 argbFormat.Gshift = 8;
284 argbFormat.Bmask = 0x000000ff;
285 argbFormat.Bshift = 0;
289 unsigned int format = 0;
290 SDL_Surface *argbImage = SDL_ConvertSurface(image, &argbFormat, 0);
291 unsigned char* argb = (unsigned char*)argbImage->pixels;
292 unsigned int compressedSize = 0;
293 unsigned char* compressed = NULL;
297 bool hasAlpha = HasAlpha(argb, width, height);
299 if (flags & FLAGS_USE_DXT)
301 double colorMSE, alphaMSE;
302 compressedSize = squish::GetStorageRequirements(width, height, squish::kDxt5);
303 compressed = new unsigned char[compressedSize];
304 // first try DXT1, which is only 4bits/pixel
305 CompressImage(argb, width, height, compressed, squish::kDxt1, colorMSE, alphaMSE);
306 if (colorMSE < maxMSE && alphaMSE < maxMSE)
307 { // success - use it
308 compressedSize = squish::GetStorageRequirements(width, height, squish::kDxt1);
309 format = XB_FMT_DXT1;
312 if (!format && alphaMSE == 0 && (flags & FLAGS_ALLOW_YCOCG) == FLAGS_ALLOW_YCOCG)
314 // no alpha channel, so DXT5YCoCg is going to be the best DXT5 format
315 CompressImage(argb, width, height, compressed, squish::kDxt5 | squish::kUseYCoCg, colorMSE, alphaMSE);
316 if (colorMSE < maxMSE && alphaMSE < maxMSE)
317 { // success - use it
318 compressedSize = squish::GetStorageRequirements(width, height, squish::kDxt5);
319 format = XB_FMT_DXT5_YCoCg;
324 { // try DXT3 and DXT5 - use whichever is better (color is the same, but alpha will be different)
325 CompressImage(argb, width, height, compressed, squish::kDxt3, colorMSE, alphaMSE);
326 if (colorMSE < maxMSE)
327 { // color is fine, test DXT5 as well
329 squish::u8* compressed2 = new squish::u8[squish::GetStorageRequirements(width, height, squish::kDxt5)];
330 CompressImage(argb, width, height, compressed2, squish::kDxt5, colorMSE, dxt5MSE);
331 if (alphaMSE < maxMSE && alphaMSE < dxt5MSE)
332 { // DXT3 passes and is best
333 compressedSize = squish::GetStorageRequirements(width, height, squish::kDxt3);
334 format = XB_FMT_DXT3;
336 else if (dxt5MSE < maxMSE)
338 compressedSize = squish::GetStorageRequirements(width, height, squish::kDxt5);
339 memcpy(compressed, compressed2, compressedSize);
340 format = XB_FMT_DXT5;
342 delete[] compressed2;
350 frame = appendContent(writer, width, height, compressed, compressedSize, format, hasAlpha, flags);
356 // none of the compressed stuff works for us, so we use 32bit texture
357 format = XB_FMT_A8R8G8B8;
358 frame = appendContent(writer, width, height, argb, (width * height * 4), format, hasAlpha, flags);
361 SDL_FreeSurface(argbImage);
368 puts(" -help Show this screen.");
369 puts(" -input <dir> Input directory. Default: current dir");
370 puts(" -output <dir> Output directory/filename. Default: Textures.xpr");
371 puts(" -dupecheck Enable duplicate file detection. Reduces output file size. Default: on");
372 puts(" -use_lzo Use lz0 packing. Default: on");
373 puts(" -use_dxt Use DXT compression. Default: on");
374 puts(" -use_none Use No compression. Default: off");
377 static bool checkDupe(struct MD5Context* ctx,
378 map<string,unsigned int>& hashes,
379 vector<unsigned int>& dupes, unsigned int pos)
381 unsigned char digest[17];
382 MD5Final(digest,ctx);
385 sprintf(hex, "%02X%02X%02X%02X%02X%02X%02X%02X"\
386 "%02X%02X%02X%02X%02X%02X%02X%02X", digest[0], digest[1], digest[2],
387 digest[3], digest[4], digest[5], digest[6], digest[7], digest[8],
388 digest[9], digest[10], digest[11], digest[12], digest[13], digest[14],
391 map<string,unsigned int>::iterator it = hashes.find(hex);
392 if (it != hashes.end())
394 dupes[pos] = it->second;
398 hashes.insert(make_pair(hex,pos));
404 int createBundle(const std::string& InputDir, const std::string& OutputFile, double maxMSE, unsigned int flags, bool dupecheck)
406 map<string,unsigned int> hashes;
407 vector<unsigned int> dupes;
409 CreateSkeletonHeader(xbtf, InputDir);
410 dupes.resize(xbtf.GetFiles().size());
413 for (unsigned int i=0;i<dupes.size();++i)
417 CXBTFWriter writer(xbtf, OutputFile);
418 if (!writer.Create())
420 printf("Error creating file\n");
424 std::vector<CXBTFFile>& files = xbtf.GetFiles();
425 for (size_t i = 0; i < files.size(); i++)
427 struct MD5Context ctx;
429 CXBTFFile& file = files[i];
431 std::string fullPath = InputDir;
432 fullPath += file.GetPath();
434 std::string output = file.GetPath();
435 output = output.substr(0, 40);
436 while (output.size() < 46)
438 if (!IsGIF(fullPath.c_str()))
441 SDL_Surface* image = IMG_Load(fullPath.c_str());
444 printf("...unable to load image %s\n", file.GetPath());
449 printf("%s", output.c_str());
452 MD5Update(&ctx,(const uint8_t*)image->pixels,image->h*image->pitch);
453 if (checkDupe(&ctx,hashes,dupes,i))
455 printf("**** duplicate of %s\n", files[dupes[i]].GetPath());
456 file.GetFrames().insert(file.GetFrames().end(),
457 files[dupes[i]].GetFrames().begin(), files[dupes[i]].GetFrames().end());
464 CXBTFFrame frame = createXBTFFrame(image, writer, maxMSE, flags);
466 printf("%s%c (%d,%d @ %"PRIu64" bytes)\n", GetFormatString(frame.GetFormat()), frame.HasAlpha() ? ' ' : '*',
467 frame.GetWidth(), frame.GetHeight(), frame.GetUnpackedSize());
470 file.GetFrames().push_back(frame);
472 SDL_FreeSurface(image);
476 int gnAG = AG_LoadGIF(fullPath.c_str(), NULL, 0);
477 AG_Frame* gpAG = new AG_Frame[gnAG];
478 AG_LoadGIF(fullPath.c_str(), gpAG, gnAG);
480 printf("%s\n", output.c_str());
484 for (int j = 0; j < gnAG; j++)
486 (const uint8_t*)gpAG[j].surface->pixels,
487 gpAG[j].surface->h * gpAG[j].surface->pitch);
489 if (checkDupe(&ctx,hashes,dupes,i))
491 printf("**** duplicate of %s\n", files[dupes[i]].GetPath());
492 file.GetFrames().insert(file.GetFrames().end(),
493 files[dupes[i]].GetFrames().begin(), files[dupes[i]].GetFrames().end());
500 for (int j = 0; j < gnAG; j++)
502 printf(" frame %4i ", j);
503 CXBTFFrame frame = createXBTFFrame(gpAG[j].surface, writer, maxMSE, flags);
504 frame.SetDuration(gpAG[j].delay);
505 file.GetFrames().push_back(frame);
506 printf("%s%c (%d,%d @ %"PRIu64" bytes)\n", GetFormatString(frame.GetFormat()), frame.HasAlpha() ? ' ' : '*',
507 frame.GetWidth(), frame.GetHeight(), frame.GetUnpackedSize());
510 AG_FreeSurfaces(gpAG, gnAG);
517 if (!writer.UpdateHeader(dupes))
519 printf("Error writing header to file\n");
525 printf("Error closing file\n");
532 int main(int argc, char* argv[])
534 #ifdef USE_LZO_PACKING
535 if (lzo_init() != LZO_E_OK)
539 unsigned int flags = 0;
540 bool dupecheck = false;
541 CmdLineArgs args(argc, (const char**)argv);
543 // setup some defaults, dxt with lzo post packing,
544 flags = FLAGS_USE_DXT;
545 #ifdef USE_LZO_PACKING
546 flags |= FLAGS_USE_LZO;
549 if (args.size() == 1)
555 std::string InputDir;
556 std::string OutputFilename = "Textures.xbt";
558 for (unsigned int i = 1; i < args.size(); ++i)
560 if (!stricmp(args[i], "-help") || !stricmp(args[i], "-h") || !stricmp(args[i], "-?"))
565 else if (!stricmp(args[i], "-input") || !stricmp(args[i], "-i"))
567 InputDir = args[++i];
570 else if (!strcmp(args[i], "-dupecheck"))
574 else if (!stricmp(args[i], "-output") || !stricmp(args[i], "-o"))
576 OutputFilename = args[++i];
580 while ((c = (char *)strchr(OutputFilename.c_str(), '\\')) != NULL) *c = '/';
583 else if (!stricmp(args[i], "-use_none"))
585 flags &= ~FLAGS_USE_DXT;
587 else if (!stricmp(args[i], "-use_dxt"))
589 flags |= FLAGS_USE_DXT;
591 #ifdef USE_LZO_PACKING
592 else if (!stricmp(args[i], "-use_lzo"))
594 flags |= FLAGS_USE_LZO;
599 printf("Unrecognized command line flag: %s\n", args[i]);
609 size_t pos = InputDir.find_last_of(DIR_SEPARATOR);
610 if (pos != InputDir.length() - 1)
611 InputDir += DIR_SEPARATOR;
613 double maxMSE = 1.5; // HQ only please
614 createBundle(InputDir, OutputFilename, maxMSE, flags, dupecheck);