/ lib / Group5 / G5ImageCache.cpp
G5ImageCache.cpp
  1  #include "G5ImageCache.h"
  2  
  3  #include <SDCardManager.h>
  4  
  5  bool G5ImageCache::compressToFile(const uint8_t* bitmap, int width, int height, const char* path) {
  6    if (!bitmap || width <= 0 || height <= 0 || !path) {
  7      return false;
  8    }
  9  
 10    // Validate dimensions fit in uint16_t header fields
 11    if (width > UINT16_MAX || height > UINT16_MAX) {
 12      return false;
 13    }
 14  
 15    const int rowBytes = (width + 7) / 8;
 16  
 17    // Estimate buffer size - Group5 typically achieves good compression,
 18    // but we allocate for worst case
 19    const size_t maxCompressedSize = estimateMaxCompressedSize(width, height);
 20  
 21    // Allocate compression buffer
 22    uint8_t* compressBuffer = new (std::nothrow) uint8_t[maxCompressedSize];
 23    if (!compressBuffer) {
 24      return false;
 25    }
 26  
 27    G5ENCODER encoder;
 28    int result = encoder.init(width, height, compressBuffer, maxCompressedSize);
 29    if (result != G5_SUCCESS) {
 30      delete[] compressBuffer;
 31      return false;
 32    }
 33  
 34    // Encode all rows
 35    for (int y = 0; y < height; y++) {
 36      result = encoder.encodeLine(const_cast<uint8_t*>(bitmap + y * rowBytes));
 37      if (result != G5_SUCCESS && result != G5_ENCODE_COMPLETE) {
 38        delete[] compressBuffer;
 39        return false;
 40      }
 41    }
 42  
 43    const int compressedSize = encoder.size();
 44  
 45    // Write to file
 46    FsFile outFile;
 47    if (!SdMan.openFileForWrite("G5C", path, outFile)) {
 48      delete[] compressBuffer;
 49      return false;
 50    }
 51  
 52    // Write header
 53    G5ImageHeader header;
 54    header.magic = G5_MAGIC;
 55    header.width = width;
 56    header.height = height;
 57    header.compressedSize = compressedSize;
 58  
 59    if (outFile.write(reinterpret_cast<uint8_t*>(&header), sizeof(header)) != sizeof(header)) {
 60      outFile.close();
 61      SdMan.remove(path);
 62      delete[] compressBuffer;
 63      return false;
 64    }
 65  
 66    // Write compressed data
 67    if (outFile.write(compressBuffer, compressedSize) != static_cast<size_t>(compressedSize)) {
 68      outFile.close();
 69      SdMan.remove(path);
 70      delete[] compressBuffer;
 71      return false;
 72    }
 73  
 74    outFile.close();
 75    delete[] compressBuffer;
 76    return true;
 77  }
 78  
 79  bool G5ImageCache::decompressFromFile(const char* path, std::function<void(const uint8_t*, int, int)> rowCallback) {
 80    if (!path || !rowCallback) {
 81      return false;
 82    }
 83  
 84    FsFile inFile;
 85    if (!SdMan.openFileForRead("G5C", path, inFile)) {
 86      return false;
 87    }
 88  
 89    // Read header
 90    G5ImageHeader header;
 91    if (inFile.read(reinterpret_cast<uint8_t*>(&header), sizeof(header)) != sizeof(header)) {
 92      inFile.close();
 93      return false;
 94    }
 95  
 96    if (header.magic != G5_MAGIC) {
 97      inFile.close();
 98      return false;
 99    }
100  
101    const int rowBytes = (header.width + 7) / 8;
102  
103    // Allocate buffers
104    uint8_t* compressedData = new (std::nothrow) uint8_t[header.compressedSize];
105    uint8_t* rowBuffer = new (std::nothrow) uint8_t[rowBytes];
106  
107    if (!compressedData || !rowBuffer) {
108      delete[] compressedData;
109      delete[] rowBuffer;
110      inFile.close();
111      return false;
112    }
113  
114    // Read compressed data
115    if (inFile.read(compressedData, header.compressedSize) != header.compressedSize) {
116      delete[] compressedData;
117      delete[] rowBuffer;
118      inFile.close();
119      return false;
120    }
121    inFile.close();
122  
123    // Decode
124    G5DECODER decoder;
125    int result = decoder.init(header.width, header.height, compressedData, header.compressedSize);
126    if (result != G5_SUCCESS) {
127      delete[] compressedData;
128      delete[] rowBuffer;
129      return false;
130    }
131  
132    for (int y = 0; y < header.height; y++) {
133      result = decoder.decodeLine(rowBuffer);
134      if (result != G5_SUCCESS && result != G5_DECODE_COMPLETE) {
135        delete[] compressedData;
136        delete[] rowBuffer;
137        return false;
138      }
139      rowCallback(rowBuffer, rowBytes, y);
140    }
141  
142    delete[] compressedData;
143    delete[] rowBuffer;
144    return true;
145  }
146  
147  bool G5ImageCache::readHeader(const char* path, G5ImageHeader& header) {
148    if (!path) {
149      return false;
150    }
151  
152    FsFile inFile;
153    if (!SdMan.openFileForRead("G5C", path, inFile)) {
154      return false;
155    }
156  
157    if (inFile.read(reinterpret_cast<uint8_t*>(&header), sizeof(header)) != sizeof(header)) {
158      inFile.close();
159      return false;
160    }
161    inFile.close();
162  
163    return header.magic == G5_MAGIC;
164  }
165  
166  size_t G5ImageCache::estimateMaxCompressedSize(int width, int height) {
167    // Group5 can theoretically expand data in worst case (random noise)
168    // Worst case: horizontal mode with long codes for every pair
169    // Safe estimate: raw size + 50% overhead
170    const size_t rawSize = static_cast<size_t>((width + 7) / 8) * height;
171    return rawSize + (rawSize / 2) + 1024;  // Extra margin for safety
172  }