using System; using System.IO; using LibMMD.Unity3D.ImageLoader; using UnityEngine; namespace LibMMD.Unity3D { /// Decodes TGA textures, including memory-bounded loading of large RLE images. internal static class TgaTextureLoader { internal sealed class DecodedTexture { internal readonly int Width; internal readonly int Height; internal readonly byte[] Rgba; internal DecodedTexture(int width, int height, byte[] rgba) { Width = width; Height = height; Rgba = rgba; } } internal static Texture Load(string path, int maxSize) { if (maxSize > 0) { var decoded = TryDecodeTrueColor(path, maxSize, true); if (decoded != null) return CreateTexture(decoded); } return TargaImage.LoadTargaImage(path); } // File I/O and pixel expansion only. Safe to call from a worker thread. internal static DecodedTexture Decode(string path, int maxSize) { return TryDecodeTrueColor(path, maxSize, false); } internal static Texture2D CreateTexture(DecodedTexture decoded) { var texture = new Texture2D(decoded.Width, decoded.Height, TextureFormat.RGBA32, false); texture.LoadRawTextureData(decoded.Rgba); texture.Apply(false, false); return texture; } // The compatibility reader materializes the source file, rows, Color array and full-size // Texture2D at the same time. Decode large uncompressed and RLE true-color images directly // into the configured output size instead. private static DecodedTexture TryDecodeTrueColor(string path, int maxSize, bool requireDownscale) { try { using (var stream = File.OpenRead(path)) using (var reader = new BinaryReader(stream)) { if (stream.Length < 18) return null; var idLength = reader.ReadByte(); var colorMapType = reader.ReadByte(); var imageType = reader.ReadByte(); reader.ReadBytes(9); var width = reader.ReadUInt16(); var height = reader.ReadUInt16(); var pixelDepth = reader.ReadByte(); var descriptor = reader.ReadByte(); if (colorMapType != 0 || (imageType != 2 && imageType != 10) || (pixelDepth != 24 && pixelDepth != 32) || width == 0 || height == 0) return null; if (requireDownscale && (maxSize <= 0 || width <= maxSize && height <= maxSize)) return null; var outputWidth = maxSize > 0 ? Math.Min((int)width, maxSize) : width; var outputHeight = maxSize > 0 ? Math.Min((int)height, maxSize) : height; var sourceBytesPerPixel = pixelDepth / 8; var pixelDataOffset = 18L + idLength; var output = new byte[outputWidth * outputHeight * 4]; var topOrigin = (descriptor & 0x20) != 0; var rightOrigin = (descriptor & 0x10) != 0; if (imageType == 10) { stream.Position = pixelDataOffset; DecodeDownscaledRle(reader, width, height, sourceBytesPerPixel, outputWidth, outputHeight, topOrigin, rightOrigin, output); } else { DecodeDownscaledUncompressed(reader, stream, width, height, sourceBytesPerPixel, pixelDataOffset, outputWidth, outputHeight, topOrigin, rightOrigin, output); } return new DecodedTexture(outputWidth, outputHeight, output); } } catch (Exception) { return null; } } private static void DecodeDownscaledUncompressed(BinaryReader reader, Stream stream, int width, int height, int sourceBytesPerPixel, long pixelDataOffset, int outputWidth, int outputHeight, bool topOrigin, bool rightOrigin, byte[] output) { var sourceRow = new byte[width * sourceBytesPerPixel]; for (var y = 0; y < outputHeight; y++) { var sourceYBottom = (long)y * height / outputHeight; var fileY = topOrigin ? height - 1L - sourceYBottom : sourceYBottom; stream.Position = pixelDataOffset + fileY * sourceRow.Length; if (reader.Read(sourceRow, 0, sourceRow.Length) != sourceRow.Length) throw new EndOfStreamException(); for (var x = 0; x < outputWidth; x++) { var sourceXLeft = (long)x * width / outputWidth; var fileX = rightOrigin ? width - 1L - sourceXLeft : sourceXLeft; var source = (int)fileX * sourceBytesPerPixel; WriteRgba(output, (y * outputWidth + x) * 4, sourceRow[source], sourceRow[source + 1], sourceRow[source + 2], sourceBytesPerPixel == 4 ? sourceRow[source + 3] : (byte)255); } } } private static void DecodeDownscaledRle(BinaryReader reader, int width, int height, int sourceBytesPerPixel, int outputWidth, int outputHeight, bool topOrigin, bool rightOrigin, byte[] output) { var outputXByFileX = CreateOutputCoordinateMap(width, outputWidth, rightOrigin); var outputYByFileY = CreateOutputCoordinateMap(height, outputHeight, topOrigin); var totalPixels = width * height; var filePixel = 0; var fileX = 0; var fileY = 0; while (filePixel < totalPixels) { var packetHeader = reader.ReadByte(); var packetLength = (packetHeader & 0x7f) + 1; var runLengthPacket = (packetHeader & 0x80) != 0; byte blue = 0, green = 0, red = 0, alpha = 255; if (runLengthPacket) ReadPixel(reader, sourceBytesPerPixel, out blue, out green, out red, out alpha); for (var i = 0; i < packetLength && filePixel < totalPixels; i++, filePixel++) { if (!runLengthPacket) ReadPixel(reader, sourceBytesPerPixel, out blue, out green, out red, out alpha); var outputX = outputXByFileX[fileX]; var outputY = outputYByFileY[fileY]; if (outputX >= 0 && outputY >= 0) WriteRgba(output, (outputY * outputWidth + outputX) * 4, blue, green, red, alpha); if (++fileX < width) continue; fileX = 0; fileY++; } } } private static int[] CreateOutputCoordinateMap(int sourceSize, int outputSize, bool reverse) { var result = new int[sourceSize]; for (var i = 0; i < result.Length; i++) result[i] = -1; for (var output = 0; output < outputSize; output++) { var source = (long)output * sourceSize / outputSize; if (reverse) source = sourceSize - 1L - source; result[source] = output; } return result; } private static void ReadPixel(BinaryReader reader, int bytesPerPixel, out byte blue, out byte green, out byte red, out byte alpha) { blue = reader.ReadByte(); green = reader.ReadByte(); red = reader.ReadByte(); alpha = bytesPerPixel == 4 ? reader.ReadByte() : (byte)255; } private static void WriteRgba(byte[] output, int destination, byte blue, byte green, byte red, byte alpha) { output[destination] = red; output[destination + 1] = green; output[destination + 2] = blue; output[destination + 3] = alpha; } } }