1 /** @file image.cpp  Image objects and related routines.
2  *
3  * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright © 2006-2015 Daniel Swanson <danij@dengine.net>
5  *
6  * @par License
7  * GPL: http://www.gnu.org/licenses/gpl.html
8  *
9  * <small>This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by the
11  * Free Software Foundation; either version 2 of the License, or (at your
12  * option) any later version. This program is distributed in the hope that it
13  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
15  * Public License for more details. You should have received a copy of the GNU
16  * General Public License along with this program; if not, write to the Free
17  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18  * 02110-1301 USA</small>
19  */
20 
21 #include "de_platform.h"
22 #include "resource/image.h"
23 #include "resource/tga.h"
24 
25 #include <doomsday/resource/patch.h>
26 #include <doomsday/resource/colorpalettes.h>
27 
28 #include <de/memory.h>
29 #include <de/LogBuffer>
30 #include <QByteArray>
31 #include <QImage>
32 #include <de/NativePath>
33 #include <doomsday/filesys/fs_main.h>
34 
35 #include "dd_main.h"
36 
37 #include <doomsday/res/Composite>
38 #include <doomsday/resource/pcx.h>
39 
40 #include "gl/gl_tex.h"
41 
42 #include "render/rend_main.h" // misc global vars awaiting new home
43 
44 #ifndef DENG2_QT_4_7_OR_NEWER // older than 4.7?
45 #  define constBits bits
46 #endif
47 
48 using namespace de;
49 using namespace res;
50 
51 struct GraphicFileType
52 {
53     /// Symbolic name of the resource type.
54     String name;
55 
56     /// Known file extension.
57     String ext;
58 
59     bool (*interpretFunc)(FileHandle &hndl, String filePath, image_t &img);
60 
61     //char const *(*getLastErrorFunc)(); ///< Can be NULL.
62 };
63 
interpretPcx(FileHandle & hndl,String,image_t & img)64 static bool interpretPcx(FileHandle &hndl, String /*filePath*/, image_t &img)
65 {
66     Image_Init(img);
67     img.pixels = PCX_Load(hndl, img.size, img.pixelSize);
68     return (0 != img.pixels);
69 }
70 
interpretJpg(FileHandle & hndl,String,image_t & img)71 static bool interpretJpg(FileHandle &hndl, String /*filePath*/, image_t &img)
72 {
73     return Image_LoadFromFileWithFormat(img, "JPG", hndl);
74 }
75 
interpretPng(FileHandle & hndl,String,image_t & img)76 static bool interpretPng(FileHandle &hndl, String /*filePath*/, image_t &img)
77 {
78     return Image_LoadFromFileWithFormat(img, "PNG", hndl);
79 }
80 
interpretTga(FileHandle & hndl,String,image_t & img)81 static bool interpretTga(FileHandle &hndl, String /*filePath*/, image_t &img)
82 {
83     Image_Init(img);
84     img.pixels = TGA_Load(hndl, img.size, img.pixelSize);
85     return img.pixels != nullptr;
86 }
87 
88 // Graphic resource types.
89 static GraphicFileType const graphicTypes[] = {
90     { "PNG",    "png",      interpretPng /*, 0*/ },
91     { "JPG",    "jpg",      interpretJpg /*, 0*/ }, // TODO: add alternate "jpeg" extension
92     { "TGA",    "tga",      interpretTga /*, TGA_LastError*/ },
93     { "PCX",    "pcx",      interpretPcx /*, PCX_LastError*/ },
94     { "",       "",         nullptr /*,            0*/ } // Terminate.
95 };
96 
guessGraphicFileTypeFromFileName(String fileName)97 static GraphicFileType const *guessGraphicFileTypeFromFileName(String fileName)
98 {
99     // The path must have an extension for this.
100     String ext = fileName.fileNameExtension();
101     if (!ext.isEmpty())
102     {
103         for (int i = 0; !graphicTypes[i].ext.isEmpty(); ++i)
104         {
105             GraphicFileType const &type = graphicTypes[i];
106             if (!ext.compareWithoutCase(type.ext))
107             {
108                 return &type;
109             }
110         }
111     }
112     return nullptr; // Unknown.
113 }
114 
interpretGraphic(FileHandle & hndl,String filePath,image_t & img)115 static void interpretGraphic(FileHandle &hndl, String filePath, image_t &img)
116 {
117     // Firstly try the interpreter for the guessed resource types.
118     GraphicFileType const *rtypeGuess = guessGraphicFileTypeFromFileName(filePath);
119     if (rtypeGuess)
120     {
121         rtypeGuess->interpretFunc(hndl, filePath, img);
122     }
123 
124     // If not yet interpreted - try each recognisable format in order.
125     if (!img.pixels)
126     {
127         // Try each recognisable format instead.
128         /// @todo Order here should be determined by the resource locator.
129         for (int i = 0; !graphicTypes[i].name.isEmpty(); ++i)
130         {
131             GraphicFileType const *graphicType = &graphicTypes[i];
132 
133             // Already tried this?
134             if (graphicType == rtypeGuess) continue;
135 
136             graphicTypes[i].interpretFunc(hndl, filePath, img);
137             if (img.pixels) break;
138         }
139     }
140 }
141 
142 /// @return  @c true if the file name in @a path ends with the "color key" suffix.
isColorKeyed(String path)143 static inline bool isColorKeyed(String path)
144 {
145     return path.fileNameWithoutExtension().endsWith("-ck", String::CaseInsensitive);
146 }
147 
Image_Init(image_t & img)148 void Image_Init(image_t &img)
149 {
150     img.size      = Vector2ui(0, 0);
151     img.pixelSize = 0;
152     img.flags     = 0;
153     img.paletteId = 0;
154     img.pixels    = 0;
155 }
156 
Image_InitFromImage(image_t & img,Image const & guiImage)157 void Image_InitFromImage(image_t &img, Image const &guiImage)
158 {
159     img.size      = guiImage.size();
160     img.pixelSize = guiImage.depth() / 8;
161     img.flags     = 0;
162     img.paletteId = 0;
163     img.pixels    = reinterpret_cast<uint8_t *>(M_Malloc(guiImage.byteCount()));
164     std::memcpy(img.pixels, guiImage.bits(), guiImage.byteCount());
165 }
166 
Image_ClearPixelData(image_t & img)167 void Image_ClearPixelData(image_t &img)
168 {
169     M_Free(img.pixels); img.pixels = 0;
170 }
171 
Image_Size(image_t const & img)172 image_t::Size Image_Size(image_t const &img)
173 {
174     return img.size;
175 }
176 
Image_Description(image_t const & img)177 String Image_Description(image_t const &img)
178 {
179     return String("Dimensions:%1 Flags:%2 %3:%4")
180                .arg(img.size.asText())
181                .arg(img.flags)
182                .arg(0 != img.paletteId? "ColorPalette" : "PixelSize")
183                .arg(0 != img.paletteId? img.paletteId : img.pixelSize);
184 }
185 
Image_ConvertToLuminance(image_t & img,bool retainAlpha)186 void Image_ConvertToLuminance(image_t &img, bool retainAlpha)
187 {
188     LOG_AS("Image_ConvertToLuminance");
189 
190     uint8_t *alphaChannel = 0, *ptr = 0;
191 
192     // Is this suitable?
193     if (0 != img.paletteId || (img.pixelSize < 3 && (img.flags & IMGF_IS_MASKED)))
194     {
195         LOG_RES_WARNING("Unknown paletted/masked image format");
196         return;
197     }
198 
199     long numPels = img.size.x * img.size.y;
200 
201     // Do we need to relocate the alpha data?
202     if (retainAlpha && img.pixelSize == 4)
203     {
204         // Yes. Take a copy.
205         alphaChannel = reinterpret_cast<uint8_t *>(M_Malloc(numPels));
206 
207         ptr = img.pixels;
208         for (long p = 0; p < numPels; ++p, ptr += img.pixelSize)
209         {
210             alphaChannel[p] = ptr[3];
211         }
212     }
213 
214     // Average the RGB colors.
215     ptr = img.pixels;
216     for (long p = 0; p < numPels; ++p, ptr += img.pixelSize)
217     {
218         int min = de::min(ptr[0], de::min(ptr[1], ptr[2]));
219         int max = de::max(ptr[0], de::max(ptr[1], ptr[2]));
220         img.pixels[p] = (min == max? min : (min + max) / 2);
221     }
222 
223     // Do we need to relocate the alpha data?
224     if (alphaChannel)
225     {
226         std::memcpy(img.pixels + numPels, alphaChannel, numPels);
227         img.pixelSize = 2;
228         M_Free(alphaChannel);
229         return;
230     }
231 
232     img.pixelSize = 1;
233 }
234 
Image_ConvertToAlpha(image_t & img,bool makeWhite)235 void Image_ConvertToAlpha(image_t &img, bool makeWhite)
236 {
237     Image_ConvertToLuminance(img);
238 
239     long total = img.size.x * img.size.y;
240     for (long p = 0; p < total; ++p)
241     {
242         img.pixels[total + p] = img.pixels[p];
243         if (makeWhite) img.pixels[p] = 255;
244     }
245     img.pixelSize = 2;
246 }
247 
Image_HasAlpha(image_t const & img)248 bool Image_HasAlpha(image_t const &img)
249 {
250     LOG_AS("Image_HasAlpha");
251 
252     if (0 != img.paletteId || (img.flags & IMGF_IS_MASKED))
253     {
254         LOG_RES_WARNING("Unknown paletted/masked image format");
255         return false;
256     }
257 
258     if (img.pixelSize == 3)
259     {
260         return false;
261     }
262 
263     if (img.pixelSize == 4)
264     {
265         long const numpels = img.size.x * img.size.y;
266         uint8_t const *in = img.pixels;
267         for (long i = 0; i < numpels; ++i, in += 4)
268         {
269             if (in[3] < 255)
270             {
271                 return true;
272             }
273         }
274     }
275 
276     return false;
277 }
278 
Image_LoadFromFile(image_t & img,FileHandle & file)279 uint8_t *Image_LoadFromFile(image_t &img, FileHandle &file)
280 {
281     LOG_AS("Image_LoadFromFile");
282 
283     String filePath = file.file().composePath();
284 
285     Image_Init(img);
286     interpretGraphic(file, filePath, img);
287 
288     // Still not interpreted?
289     if (!img.pixels)
290     {
291         LOG_RES_XVERBOSE("\"%s\" unrecognized, trying fallback loader...",
292                          NativePath(filePath).pretty());
293         return 0; // Not a recognised format. It may still be loadable, however.
294     }
295 
296     // How about some color-keying?
297     if (isColorKeyed(filePath))
298     {
299         uint8_t *out = ApplyColorKeying(img.pixels, img.size.x, img.size.y, img.pixelSize);
300         if (out != img.pixels)
301         {
302             // Had to allocate a larger buffer, free the old and attach the new.
303             M_Free(img.pixels);
304             img.pixels = out;
305         }
306 
307         // Color keying is done; now we have 4 bytes per pixel.
308         img.pixelSize = 4;
309     }
310 
311     // Any alpha pixels?
312     if (Image_HasAlpha(img))
313     {
314         img.flags |= IMGF_IS_MASKED;
315     }
316 
317     LOG_RES_VERBOSE("Loaded image from file \"%s\", size %s")
318             << NativePath(filePath).pretty() << img.size.asText();
319 
320     return img.pixels;
321 }
322 
Image_LoadFromFileWithFormat(image_t & img,char const * format,FileHandle & hndl)323 bool Image_LoadFromFileWithFormat(image_t &img, char const *format, FileHandle &hndl)
324 {
325     LOG_AS("Image_LoadFromFileWithFormat");
326 
327     /// @todo There are too many copies made here. It would be best if image_t
328     /// contained an instance of QImage. -jk
329 
330     // It is assumed that file's position stays the same (could be trying multiple interpreters).
331     size_t initPos = hndl.tell();
332 
333     Image_Init(img);
334 
335     // Load the file contents to a memory buffer.
336     QByteArray data;
337     data.resize(hndl.length() - initPos);
338     hndl.read(reinterpret_cast<uint8_t*>(data.data()), data.size());
339 
340     QImage image = QImage::fromData(data, format);
341     if (image.isNull())
342     {
343         // Back to the original file position.
344         hndl.seek(initPos, SeekSet);
345         return false;
346     }
347 
348     //LOG_TRACE("Loading \"%s\"...") << NativePath(hndl->file().composePath()).pretty();
349 
350     // Convert paletted images to RGB.
351     if (image.colorCount())
352     {
353         image = image.convertToFormat(QImage::Format_ARGB32);
354         DENG_ASSERT(!image.colorCount());
355         DENG_ASSERT(image.depth() == 32);
356     }
357 
358     // Swap the red and blue channels for GL.
359     image = image.rgbSwapped();
360 
361     img.size      = Vector2ui(image.width(), image.height());
362     img.pixelSize = image.depth() / 8;
363 
364     LOGDEV_RES_VERBOSE("size:%s depth:%i alpha:%b bytes:%i")
365             << img.size.asText() << img.pixelSize
366             << image.hasAlphaChannel() << image.byteCount();
367 
368     img.pixels = reinterpret_cast<uint8_t *>(M_MemDup(image.constBits(), image.byteCount()));
369 
370     // Back to the original file position.
371     hndl.seek(initPos, SeekSet);
372     return true;
373 }
374 
Image_Save(image_t const & img,char const * filePath)375 bool Image_Save(image_t const &img, char const *filePath)
376 {
377     // Compose the full path.
378     String fullPath = String(filePath);
379     if (fullPath.isEmpty())
380     {
381         static int n = 0;
382         fullPath = String("image%1x%2-%3").arg(img.size.x).arg(img.size.y).arg(n++, 3);
383     }
384 
385     if (fullPath.fileNameExtension().isEmpty())
386     {
387         fullPath += ".png";
388     }
389 
390     // Swap red and blue channels then save.
391     QImage image = QImage(img.pixels, img.size.x, img.size.y, QImage::Format_ARGB32);
392     image = image.rgbSwapped();
393 
394     return image.save(NativePath(fullPath));
395 }
396 
GL_LoadImage(image_t & image,String nativePath)397 uint8_t *GL_LoadImage(image_t &image, String nativePath)
398 {
399     try
400     {
401         // Relative paths are relative to the native working directory.
402         String path = (NativePath::workPath() / NativePath(nativePath).expand()).withSeparators('/');
403 
404         FileHandle &hndl = App_FileSystem().openFile(path, "rb");
405         uint8_t *pixels = Image_LoadFromFile(image, hndl);
406 
407         App_FileSystem().releaseFile(hndl.file());
408         delete &hndl;
409 
410         return pixels;
411     }
412     catch (FS1::NotFoundError const&)
413     {} // Ignore error.
414 
415     return 0; // Not loaded.
416 }
417 
GL_LoadExtImage(image_t & image,char const * _searchPath,gfxmode_t mode)418 Source GL_LoadExtImage(image_t &image, char const *_searchPath, gfxmode_t mode)
419 {
420     DENG_ASSERT(_searchPath);
421 
422     try
423     {
424         String foundPath = App_FileSystem().findPath(de::Uri(RC_GRAPHIC, _searchPath),
425                                                      RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC));
426         // Ensure the found path is absolute.
427         foundPath = App_BasePath() / foundPath;
428 
429         if (GL_LoadImage(image, foundPath))
430         {
431             // Force it to grayscale?
432             if (mode == LGM_GRAYSCALE_ALPHA || mode == LGM_WHITE_ALPHA)
433             {
434                 Image_ConvertToAlpha(image, mode == LGM_WHITE_ALPHA);
435             }
436             else if (mode == LGM_GRAYSCALE)
437             {
438                 Image_ConvertToLuminance(image);
439             }
440 
441             return External;
442         }
443     }
444     catch (FS1::NotFoundError const&)
445     {} // Ignore this error.
446 
447     return None;
448 }
449 
palettedIsMasked(uint8_t const * pixels,int width,int height)450 static dd_bool palettedIsMasked(uint8_t const *pixels, int width, int height)
451 {
452     DENG2_ASSERT(pixels != 0);
453     // Jump to the start of the alpha data.
454     pixels += width * height;
455     for (int i = 0; i < width * height; ++i)
456     {
457         if (255 != pixels[i])
458         {
459             return true;
460         }
461     }
462     return false;
463 }
464 
loadExternalTexture(image_t & image,String encodedSearchPath,String optionalSuffix="")465 static Source loadExternalTexture(image_t &image, String encodedSearchPath,
466     String optionalSuffix = "")
467 {
468     // First look for a version with an optional suffix.
469     try
470     {
471         String foundPath = App_FileSystem().findPath(de::Uri(encodedSearchPath + optionalSuffix, RC_GRAPHIC),
472                                                      RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC));
473         // Ensure the found path is absolute.
474         foundPath = App_BasePath() / foundPath;
475 
476         return GL_LoadImage(image, foundPath)? External : None;
477     }
478     catch (FS1::NotFoundError const&)
479     {} // Ignore this error.
480 
481     // Try again without the suffix?
482     if (!optionalSuffix.empty())
483     {
484         try
485         {
486             String foundPath = App_FileSystem().findPath(de::Uri(encodedSearchPath, RC_GRAPHIC),
487                                                          RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC));
488             // Ensure the found path is absolute.
489             foundPath = App_BasePath() / foundPath;
490 
491             return GL_LoadImage(image, foundPath)? External : None;
492         }
493         catch (FS1::NotFoundError const&)
494         {} // Ignore this error.
495     }
496 
497     return None;
498 }
499 
500 /**
501  * Draw the component image @a src into the composite @a dst.
502  *
503  * @param dst               The composite buffer (drawn to).
504  * @param dstDimensions     Pixel dimensions of @a dst.
505  * @param src               The component image to be composited (read from).
506  * @param srcDimensions     Pixel dimensions of @a src.
507  * @param origin            Coordinates (topleft) in @a dst to draw @a src.
508  *
509  * @todo Optimize: Should be redesigned to composite whole rows -ds
510  */
compositePaletted(dbyte * dst,Vector2ui const & dstDimensions,IByteArray const & src,Vector2ui const & srcDimensions,Vector2i const & origin)511 static void compositePaletted(dbyte *dst, Vector2ui const &dstDimensions,
512     IByteArray const &src, Vector2ui const &srcDimensions, Vector2i const &origin)
513 {
514     if (dstDimensions == Vector2ui()) return;
515     if (srcDimensions == Vector2ui()) return;
516 
517     int const       srcW = srcDimensions.x;
518     int const       srcH = srcDimensions.y;
519     size_t const srcPels = srcW * srcH;
520 
521     int const       dstW = dstDimensions.x;
522     int const       dstH = dstDimensions.y;
523     size_t const dstPels = dstW * dstH;
524 
525     int dstX, dstY;
526 
527     for (int srcY = 0; srcY < srcH; ++srcY)
528     for (int srcX = 0; srcX < srcW; ++srcX)
529     {
530         dstX = origin.x + srcX;
531         dstY = origin.y + srcY;
532         if (dstX < 0 || dstX >= dstW) continue;
533         if (dstY < 0 || dstY >= dstH) continue;
534 
535         dbyte srcAlpha;
536         src.get(srcY * srcW + srcX + srcPels, &srcAlpha, 1);
537         if (srcAlpha)
538         {
539             src.get(srcY * srcW + srcX, &dst[dstY * dstW + dstX], 1);
540             dst[dstY * dstW + dstX + dstPels] = srcAlpha;
541         }
542     }
543 }
544 
545 /// Returns a palette translation id for the given class and map.
546 /// Note that a zero-length id is returned when @a tclass =0 and @a tmap =0
toTranslationId(int tclass,int tmap)547 static String toTranslationId(int tclass, int tmap)
548 {
549 #define NUM_TRANSLATION_CLASSES         3
550 #define NUM_TRANSLATION_MAPS_PER_CLASS  7
551 
552     // Is translation unnecessary?
553     if (!tclass && !tmap) return String();
554 
555     int trans = de::max(0, NUM_TRANSLATION_MAPS_PER_CLASS * tclass + tmap - 1);
556     LOGDEV_RES_XVERBOSE("tclass=%i tmap=%i => TransPal# %i", tclass << tmap << trans);
557     return String::number(trans);
558 
559 #undef NUM_TRANSLATION_MAPS_PER_CLASS
560 #undef NUM_TRANSLATION_CLASSES
561 }
562 
loadAndTranslatePatch(IByteArray const & data,colorpaletteid_t palId,int tclass=0,int tmap=0)563 static Block loadAndTranslatePatch(IByteArray const &data, colorpaletteid_t palId,
564     int tclass = 0, int tmap = 0)
565 {
566     res::ColorPalette &palette = App_Resources().colorPalettes().colorPalette(palId);
567     if (res::ColorPaletteTranslation const *xlat = palette.translation(toTranslationId(tclass, tmap)))
568     {
569         return res::Patch::load(data, *xlat, res::Patch::ClipToLogicalDimensions);
570     }
571     else
572     {
573         return res::Patch::load(data, res::Patch::ClipToLogicalDimensions);
574     }
575 }
576 
loadPatch(image_t & image,FileHandle & hndl,int tclass=0,int tmap=0,int border=0)577 static Source loadPatch(image_t &image, FileHandle &hndl, int tclass = 0,
578     int tmap = 0, int border = 0)
579 {
580     LOG_AS("image_t::loadPatch");
581 
582     if (Image_LoadFromFile(image, hndl))
583     {
584         return External;
585     }
586 
587     File1 &file = hndl.file();
588     ByteRefArray fileData = ByteRefArray(file.cache(), file.size());
589 
590     // A DOOM patch?
591     if (Patch::recognize(fileData))
592     {
593         try
594         {
595             colorpaletteid_t colorPaletteId = App_Resources().colorPalettes().defaultColorPalette();
596 
597             Block patchImg = loadAndTranslatePatch(fileData, colorPaletteId, tclass, tmap);
598             PatchMetadata info = Patch::loadMetadata(fileData);
599 
600             Image_Init(image);
601             image.size      = Vector2ui(info.logicalDimensions.x + border*2,
602                                         info.logicalDimensions.y + border*2);
603             image.pixelSize = 1;
604             image.paletteId = colorPaletteId;
605 
606             image.pixels = (uint8_t *) M_Calloc(2 * image.size.x * image.size.y);
607 
608             compositePaletted(image.pixels, image.size,
609                               patchImg, info.logicalDimensions, Vector2i(border, border));
610 
611             if (palettedIsMasked(image.pixels, image.size.x, image.size.y))
612             {
613                 image.flags |= IMGF_IS_MASKED;
614             }
615 
616             return Original;
617         }
618         catch (IByteArray::OffsetError const &)
619         {
620             LOG_RES_WARNING("File \"%s:%s\" does not appear to be a valid Patch")
621                 << NativePath(file.container().composePath()).pretty()
622                 << NativePath(file.composePath()).pretty();
623         }
624     }
625 
626     file.unlock();
627     return None;
628 }
629 
loadPatchComposite(image_t & image,Texture const & tex,bool maskZero=false,bool useZeroOriginIfOneComponent=false)630 static Source loadPatchComposite(image_t &image, Texture const &tex,
631     bool maskZero = false, bool useZeroOriginIfOneComponent = false)
632 {
633     LOG_AS("image_t::loadPatchComposite");
634 
635     Image_Init(image);
636     image.pixelSize = 1;
637     image.size      = Vector2ui(tex.width(), tex.height());
638     image.paletteId = App_Resources().colorPalettes().defaultColorPalette();
639 
640     image.pixels = (uint8_t *) M_Calloc(2 * image.size.x * image.size.y);
641 
642     res::Composite const &texDef = *reinterpret_cast<res::Composite *>(tex.userDataPointer());
643     DENG2_FOR_EACH_CONST(res::Composite::Components, i, texDef.components())
644     {
645         File1 &file           = App_FileSystem().lump(i->lumpNum());
646         ByteRefArray fileData = ByteRefArray(file.cache(), file.size());
647 
648         // A DOOM patch?
649         if (Patch::recognize(fileData))
650         {
651             try
652             {
653                 Patch::Flags loadFlags;
654                 if (maskZero) loadFlags |= Patch::MaskZero;
655 
656                 Block patchImg     = Patch::load(fileData, loadFlags);
657                 PatchMetadata info = Patch::loadMetadata(fileData);
658 
659                 Vector2i origin = i->origin();
660                 if (useZeroOriginIfOneComponent && texDef.componentCount() == 1)
661                 {
662                     origin = Vector2i(0, 0);
663                 }
664 
665                 // Draw the patch in the buffer.
666                 compositePaletted(image.pixels, image.size,
667                                   patchImg, info.dimensions, origin);
668             }
669             catch (IByteArray::OffsetError const &)
670             {} // Ignore this error.
671         }
672 
673         file.unlock();
674     }
675 
676     if (maskZero || palettedIsMasked(image.pixels, image.size.x, image.size.y))
677     {
678         image.flags |= IMGF_IS_MASKED;
679     }
680 
681     // For debug:
682     // GL_DumpImage(&image, Str_Text(GL_ComposeCacheNameForTexture(tex)));
683 
684     return Original;
685 }
686 
loadFlat(image_t & image,FileHandle & hndl)687 static Source loadFlat(image_t &image, FileHandle &hndl)
688 {
689     if (Image_LoadFromFile(image, hndl))
690     {
691         return External;
692     }
693 
694     // A DOOM flat.
695     Image_Init(image);
696 
697     /// @todo not all flats are 64x64!
698     image.size      = Vector2ui(64, 64);
699     image.pixelSize = 1;
700     image.paletteId = App_Resources().colorPalettes().defaultColorPalette();
701 
702     File1 &file   = hndl.file();
703     size_t fileLength = hndl.length();
704 
705     size_t bufSize = de::max(fileLength, (size_t) image.size.x * image.size.y);
706     image.pixels = (uint8_t *) M_Malloc(bufSize);
707 
708     if (fileLength < bufSize)
709     {
710         std::memset(image.pixels, 0, bufSize);
711     }
712 
713     // Load the raw image data.
714     file.read(image.pixels, 0, fileLength);
715     return Original;
716 }
717 
loadDetail(image_t & image,FileHandle & hndl)718 static Source loadDetail(image_t &image, FileHandle &hndl)
719 {
720     if (Image_LoadFromFile(image, hndl))
721     {
722         return Original;
723     }
724 
725     // It must be an old-fashioned "raw" image.
726     Image_Init(image);
727 
728     // How big is it?
729     File1 &file = hndl.file();
730     size_t fileLength = hndl.length();
731     switch (fileLength)
732     {
733     case 256 * 256: image.size.x = image.size.y = 256; break;
734     case 128 * 128: image.size.x = image.size.y = 128; break;
735     case  64 *  64: image.size.x = image.size.y =  64; break;
736     default:
737         throw Error("image_t::loadDetail", "Must be 256x256, 128x128 or 64x64.");
738     }
739 
740     image.pixelSize = 1;
741     size_t bufSize = (size_t) image.size.x * image.size.y;
742     image.pixels = (uint8_t *) M_Malloc(bufSize);
743 
744     if (fileLength < bufSize)
745     {
746         std::memset(image.pixels, 0, bufSize);
747     }
748 
749     // Load the raw image data.
750     file.read(image.pixels, fileLength);
751     return Original;
752 }
753 
GL_LoadSourceImage(image_t & image,ClientTexture const & tex,TextureVariantSpec const & spec)754 Source GL_LoadSourceImage(image_t &image, ClientTexture const &tex,
755                           TextureVariantSpec const &spec)
756 {
757     de::FS1 &fileSys = App_FileSystem();
758     auto &cfg = R_Config();
759 
760     Source source = None;
761     variantspecification_t const &vspec = spec.variant;
762     if (!tex.manifest().schemeName().compareWithoutCase("Textures"))
763     {
764         // Attempt to load an external replacement for this composite texture?
765         if (cfg.noHighResTex->value().isFalse() &&
766                 (loadExtAlways || cfg.highResWithPWAD->value().isTrue() || !tex.isFlagged(Texture::Custom)))
767         {
768             // First try the textures scheme.
769             de::Uri uri = tex.manifest().composeUri();
770             source = loadExternalTexture(image, uri.compose(), "-ck");
771         }
772 
773         if (source == None)
774         {
775             if (TC_SKYSPHERE_DIFFUSE != vspec.context)
776             {
777                 source = loadPatchComposite(image, tex);
778             }
779             else
780             {
781                 bool const zeroMask = (vspec.flags & TSF_ZEROMASK) != 0;
782                 bool const useZeroOriginIfOneComponent = true;
783                 source = loadPatchComposite(image, tex, zeroMask, useZeroOriginIfOneComponent);
784             }
785         }
786     }
787     else if (!tex.manifest().schemeName().compareWithoutCase("Flats"))
788     {
789         // Attempt to load an external replacement for this flat?
790         if (cfg.noHighResTex->value().isFalse() &&
791                 (loadExtAlways || cfg.highResWithPWAD->value().isTrue() || !tex.isFlagged(Texture::Custom)))
792         {
793             // First try the flats scheme.
794             de::Uri uri = tex.manifest().composeUri();
795             source = loadExternalTexture(image, uri.compose(), "-ck");
796 
797             if (source == None)
798             {
799                 // How about the old-fashioned "flat-name" in the textures scheme?
800                 source = loadExternalTexture(image, "Textures:flat-" + uri.path().toStringRef(), "-ck");
801             }
802         }
803 
804         if (source == None)
805         {
806             if (tex.manifest().hasResourceUri())
807             {
808                 de::Uri resourceUri = tex.manifest().resourceUri();
809                 if (!resourceUri.scheme().compareWithoutCase("LumpIndex"))
810                 {
811                     try
812                     {
813                         lumpnum_t const lumpNum = resourceUri.path().toString().toInt();
814                         FileHandle &hndl    = fileSys.openLump(fileSys.lump(lumpNum));
815 
816                         source = loadFlat(image, hndl);
817 
818                         fileSys.releaseFile(hndl.file());
819                         delete &hndl;
820                     }
821                     catch (LumpIndex::NotFoundError const&)
822                     {} // Ignore this error.
823                 }
824             }
825         }
826     }
827     else if (!tex.manifest().schemeName().compareWithoutCase("Patches"))
828     {
829         int tclass = 0, tmap = 0;
830         if (vspec.flags & TSF_HAS_COLORPALETTE_XLAT)
831         {
832             tclass = vspec.tClass;
833             tmap   = vspec.tMap;
834         }
835 
836         // Attempt to load an external replacement for this patch?
837         if (cfg.noHighResTex->value().isFalse() &&
838                 (loadExtAlways || cfg.highResWithPWAD->value().isTrue() || !tex.isFlagged(Texture::Custom)))
839         {
840             de::Uri uri = tex.manifest().composeUri();
841             source = loadExternalTexture(image, uri.compose(), "-ck");
842         }
843 
844         if (source == None)
845         {
846             if (tex.manifest().hasResourceUri())
847             {
848                 de::Uri resourceUri = tex.manifest().resourceUri();
849                 if (!resourceUri.scheme().compareWithoutCase("LumpIndex"))
850                 {
851                     try
852                     {
853                         lumpnum_t const lumpNum = resourceUri.path().toString().toInt();
854                         FileHandle &hndl    = fileSys.openLump(fileSys.lump(lumpNum));
855 
856                         source = loadPatch(image, hndl, tclass, tmap, vspec.border);
857 
858                         fileSys.releaseFile(hndl.file());
859                         delete &hndl;
860                     }
861                     catch (LumpIndex::NotFoundError const&)
862                     {} // Ignore this error.
863                 }
864             }
865         }
866     }
867     else if (!tex.manifest().schemeName().compareWithoutCase("Sprites"))
868     {
869         int tclass = 0, tmap = 0;
870         if (vspec.flags & TSF_HAS_COLORPALETTE_XLAT)
871         {
872             tclass = vspec.tClass;
873             tmap   = vspec.tMap;
874         }
875 
876         // Attempt to load an external replacement for this sprite?
877         if (cfg.noHighResPatches->value().isFalse())
878         {
879             de::Uri uri = tex.manifest().composeUri();
880 
881             // Prefer psprite or translated versions if available.
882             if (TC_PSPRITE_DIFFUSE == vspec.context)
883             {
884                 source = loadExternalTexture(image, "Patches:" + uri.path() + "-hud", "-ck");
885             }
886             else if (tclass || tmap)
887             {
888                 source = loadExternalTexture(image, "Patches:" + uri.path() + String("-table%1%2").arg(tclass).arg(tmap), "-ck");
889             }
890 
891             if (!source)
892             {
893                 source = loadExternalTexture(image, "Patches:" + uri.path(), "-ck");
894             }
895         }
896 
897         if (source == None)
898         {
899             if (tex.manifest().hasResourceUri())
900             {
901                 de::Uri resourceUri = tex.manifest().resourceUri();
902                 if (!resourceUri.scheme().compareWithoutCase("LumpIndex"))
903                 {
904                     try
905                     {
906                         lumpnum_t const lumpNum = resourceUri.path().toString().toInt();
907                         FileHandle &hndl    = fileSys.openLump(fileSys.lump(lumpNum));
908 
909                         source = loadPatch(image, hndl, tclass, tmap, vspec.border);
910 
911                         fileSys.releaseFile(hndl.file());
912                         delete &hndl;
913                     }
914                     catch (LumpIndex::NotFoundError const&)
915                     {} // Ignore this error.
916                 }
917             }
918         }
919     }
920     else if (!tex.manifest().schemeName().compareWithoutCase("Details"))
921     {
922         if (tex.manifest().hasResourceUri())
923         {
924             de::Uri resourceUri = tex.manifest().resourceUri();
925             if (resourceUri.scheme().compareWithoutCase("Lumps"))
926             {
927                 source = loadExternalTexture(image, resourceUri.compose());
928             }
929             else
930             {
931                 lumpnum_t const lumpNum = fileSys.lumpNumForName(resourceUri.path());
932                 try
933                 {
934                     File1 &lump = fileSys.lump(lumpNum);
935                     FileHandle &hndl = fileSys.openLump(lump);
936 
937                     source = loadDetail(image, hndl);
938 
939                     fileSys.releaseFile(hndl.file());
940                     delete &hndl;
941                 }
942                 catch (LumpIndex::NotFoundError const&)
943                 {} // Ignore this error.
944             }
945         }
946     }
947     else
948     {
949         if (tex.manifest().hasResourceUri())
950         {
951             de::Uri resourceUri = tex.manifest().resourceUri();
952             source = loadExternalTexture(image, resourceUri.compose());
953         }
954     }
955     return source;
956 }
957