/ lib / PngToBmpConverter / PngToBmpConverter.cpp
PngToBmpConverter.cpp
  1  #include "PngToBmpConverter.h"
  2  
  3  #include <HardwareSerial.h>
  4  #include <SdFat.h>
  5  #include <pngle.h>
  6  
  7  #include <cstring>
  8  
  9  #include "BitmapHelpers.h"
 10  
 11  namespace {
 12  constexpr int MAX_IMAGE_WIDTH = 2048;
 13  constexpr int MAX_IMAGE_HEIGHT = 3072;
 14  
 15  inline void write16(Print& out, const uint16_t value) {
 16    out.write(value & 0xFF);
 17    out.write((value >> 8) & 0xFF);
 18  }
 19  
 20  inline void write32(Print& out, const uint32_t value) {
 21    out.write(value & 0xFF);
 22    out.write((value >> 8) & 0xFF);
 23    out.write((value >> 16) & 0xFF);
 24    out.write((value >> 24) & 0xFF);
 25  }
 26  
 27  inline void write32Signed(Print& out, const int32_t value) {
 28    out.write(value & 0xFF);
 29    out.write((value >> 8) & 0xFF);
 30    out.write((value >> 16) & 0xFF);
 31    out.write((value >> 24) & 0xFF);
 32  }
 33  
 34  void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
 35    const int bytesPerRow = (width * 2 + 31) / 32 * 4;
 36    const int imageSize = bytesPerRow * height;
 37    const uint32_t fileSize = 70 + imageSize;
 38  
 39    bmpOut.write('B');
 40    bmpOut.write('M');
 41    write32(bmpOut, fileSize);
 42    write32(bmpOut, 0);
 43    write32(bmpOut, 70);
 44  
 45    write32(bmpOut, 40);
 46    write32Signed(bmpOut, width);
 47    write32Signed(bmpOut, -height);
 48    write16(bmpOut, 1);
 49    write16(bmpOut, 2);
 50    write32(bmpOut, 0);
 51    write32(bmpOut, imageSize);
 52    write32(bmpOut, 2835);
 53    write32(bmpOut, 2835);
 54    write32(bmpOut, 4);
 55    write32(bmpOut, 4);
 56  
 57    uint8_t palette[16] = {
 58        0x00, 0x00, 0x00, 0x00,  // Black
 59        0x55, 0x55, 0x55, 0x00,  // Dark gray
 60        0xAA, 0xAA, 0xAA, 0x00,  // Light gray
 61        0xFF, 0xFF, 0xFF, 0x00   // White
 62    };
 63    for (const uint8_t i : palette) {
 64      bmpOut.write(i);
 65    }
 66  }
 67  
 68  struct PngContext {
 69    FsFile* pngFile;
 70    Print* bmpOut;
 71    int srcWidth;
 72    int srcHeight;
 73    int outWidth;
 74    int outHeight;
 75    int targetMaxWidth;
 76    int targetMaxHeight;
 77    uint32_t scaleX_fp;
 78    uint32_t scaleY_fp;
 79    bool needsScaling;
 80    bool headerWritten;
 81    bool quickMode;   // Fast preview: simple threshold instead of dithering
 82    bool initFailed;  // Set when allocation fails in pngInitCallback
 83    int currentSrcY;
 84    int currentOutY;
 85    uint32_t nextOutY_srcStart;
 86  
 87    // Row buffers
 88    uint8_t* srcRowBuffer;  // Source row grayscale
 89    uint8_t* outRowBuffer;  // Output BMP row
 90    uint32_t* rowAccum;     // Accumulator for scaling
 91    uint16_t* rowCount;     // Count for scaling
 92    AtkinsonDitherer* ditherer;
 93  
 94    int bytesPerRow;
 95  };
 96  
 97  void pngDrawCallback(pngle_t* pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) {
 98    auto* ctx = static_cast<PngContext*>(pngle_get_user_data(pngle));
 99    if (!ctx || ctx->initFailed || !ctx->srcRowBuffer) return;
100  
101    // Convert to grayscale using LUT
102    const uint8_t gray = rgbToGray(rgba[0], rgba[1], rgba[2]);
103  
104    // Handle alpha: blend with white background
105    const uint8_t alpha = rgba[3];
106    const uint8_t blendedGray = (gray * alpha + 255 * (255 - alpha)) / 255;
107  
108    // Store in source row buffer
109    if (x < static_cast<uint32_t>(ctx->srcWidth)) {
110      ctx->srcRowBuffer[x] = blendedGray;
111    }
112  
113    // Check if row complete (x is last pixel of row)
114    if (x == static_cast<uint32_t>(ctx->srcWidth - 1)) {
115      // Process complete row
116      if (!ctx->needsScaling) {
117        // Direct output
118        memset(ctx->outRowBuffer, 0, ctx->bytesPerRow);
119        for (int outX = 0; outX < ctx->outWidth; outX++) {
120          const uint8_t gray = adjustPixel(ctx->srcRowBuffer[outX]);
121          uint8_t twoBit;
122          if (ctx->quickMode) {
123            // Simple threshold quantization (faster)
124            twoBit = quantizeSimple(gray);
125          } else {
126            twoBit = ctx->ditherer ? ctx->ditherer->processPixel(gray, outX) : quantize(gray, outX, y);
127          }
128          const int byteIndex = (outX * 2) / 8;
129          const int bitOffset = 6 - ((outX * 2) % 8);
130          ctx->outRowBuffer[byteIndex] |= (twoBit << bitOffset);
131        }
132        if (ctx->ditherer && !ctx->quickMode) ctx->ditherer->nextRow();
133        ctx->bmpOut->write(ctx->outRowBuffer, ctx->bytesPerRow);
134      } else {
135        // Scaling: accumulate source pixels
136        for (int outX = 0; outX < ctx->outWidth; outX++) {
137          const int srcXStart = (static_cast<uint32_t>(outX) * ctx->scaleX_fp) >> 16;
138          const int srcXEnd = (static_cast<uint32_t>(outX + 1) * ctx->scaleX_fp) >> 16;
139  
140          int sum = 0;
141          int count = 0;
142          for (int srcX = srcXStart; srcX < srcXEnd && srcX < ctx->srcWidth; srcX++) {
143            sum += ctx->srcRowBuffer[srcX];
144            count++;
145          }
146          if (count == 0 && srcXStart < ctx->srcWidth) {
147            sum = ctx->srcRowBuffer[srcXStart];
148            count = 1;
149          }
150          ctx->rowAccum[outX] += sum;
151          ctx->rowCount[outX] += count;
152        }
153  
154        ctx->currentSrcY++;
155        const uint32_t srcY_fp = static_cast<uint32_t>(ctx->currentSrcY) << 16;
156  
157        if (srcY_fp >= ctx->nextOutY_srcStart && ctx->currentOutY < ctx->outHeight) {
158          memset(ctx->outRowBuffer, 0, ctx->bytesPerRow);
159          for (int outX = 0; outX < ctx->outWidth; outX++) {
160            const uint8_t gray = adjustPixel((ctx->rowCount[outX] > 0) ? (ctx->rowAccum[outX] / ctx->rowCount[outX]) : 0);
161            uint8_t twoBit;
162            if (ctx->quickMode) {
163              // Simple threshold quantization (faster)
164              twoBit = quantizeSimple(gray);
165            } else {
166              twoBit = ctx->ditherer ? ctx->ditherer->processPixel(gray, outX) : quantize(gray, outX, ctx->currentOutY);
167            }
168            const int byteIndex = (outX * 2) / 8;
169            const int bitOffset = 6 - ((outX * 2) % 8);
170            ctx->outRowBuffer[byteIndex] |= (twoBit << bitOffset);
171          }
172          if (ctx->ditherer && !ctx->quickMode) ctx->ditherer->nextRow();
173          ctx->bmpOut->write(ctx->outRowBuffer, ctx->bytesPerRow);
174          ctx->currentOutY++;
175  
176          memset(ctx->rowAccum, 0, ctx->outWidth * sizeof(uint32_t));
177          memset(ctx->rowCount, 0, ctx->outWidth * sizeof(uint16_t));
178          ctx->nextOutY_srcStart = static_cast<uint32_t>(ctx->currentOutY + 1) * ctx->scaleY_fp;
179        }
180      }
181    }
182  }
183  
184  void pngInitCallback(pngle_t* pngle, uint32_t w, uint32_t h) {
185    auto* ctx = static_cast<PngContext*>(pngle_get_user_data(pngle));
186    if (!ctx) return;
187  
188    ctx->srcWidth = w;
189    ctx->srcHeight = h;
190  
191    Serial.printf("[%lu] [PNG] Image dimensions: %dx%d\n", millis(), w, h);
192  
193    if (w > MAX_IMAGE_WIDTH || h > MAX_IMAGE_HEIGHT) {
194      Serial.printf("[%lu] [PNG] Image too large\n", millis());
195      return;
196    }
197  
198    // Calculate output dimensions
199    ctx->outWidth = w;
200    ctx->outHeight = h;
201    ctx->scaleX_fp = 65536;
202    ctx->scaleY_fp = 65536;
203    ctx->needsScaling = false;
204  
205    if (ctx->targetMaxWidth > 0 && ctx->targetMaxHeight > 0 &&
206        (static_cast<int>(w) > ctx->targetMaxWidth || static_cast<int>(h) > ctx->targetMaxHeight)) {
207      const float scaleToFitWidth = static_cast<float>(ctx->targetMaxWidth) / w;
208      const float scaleToFitHeight = static_cast<float>(ctx->targetMaxHeight) / h;
209      const float scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
210  
211      ctx->outWidth = static_cast<int>(w * scale);
212      ctx->outHeight = static_cast<int>(h * scale);
213      if (ctx->outWidth < 1) ctx->outWidth = 1;
214      if (ctx->outHeight < 1) ctx->outHeight = 1;
215  
216      ctx->scaleX_fp = (static_cast<uint32_t>(w) << 16) / ctx->outWidth;
217      ctx->scaleY_fp = (static_cast<uint32_t>(h) << 16) / ctx->outHeight;
218      ctx->needsScaling = true;
219  
220      Serial.printf("[%lu] [PNG] Scaling %dx%d -> %dx%d\n", millis(), w, h, ctx->outWidth, ctx->outHeight);
221    }
222  
223    // Allocate buffers
224    ctx->srcRowBuffer = static_cast<uint8_t*>(malloc(w));
225    ctx->bytesPerRow = (ctx->outWidth * 2 + 31) / 32 * 4;
226    ctx->outRowBuffer = static_cast<uint8_t*>(malloc(ctx->bytesPerRow));
227  
228    if (!ctx->srcRowBuffer || !ctx->outRowBuffer) {
229      Serial.printf("[%lu] [PNG] Failed to allocate row buffers\n", millis());
230      free(ctx->srcRowBuffer);  // safe if nullptr
231      free(ctx->outRowBuffer);  // safe if nullptr
232      ctx->srcRowBuffer = nullptr;
233      ctx->outRowBuffer = nullptr;
234      ctx->initFailed = true;
235      return;
236    }
237  
238    if (ctx->needsScaling) {
239      ctx->rowAccum = new (std::nothrow) uint32_t[ctx->outWidth]();
240      ctx->rowCount = new (std::nothrow) uint16_t[ctx->outWidth]();
241      if (!ctx->rowAccum || !ctx->rowCount) {
242        Serial.printf("[%lu] [PNG] Failed to allocate scaling buffers\n", millis());
243        free(ctx->srcRowBuffer);
244        free(ctx->outRowBuffer);
245        delete[] ctx->rowAccum;  // safe if nullptr
246        delete[] ctx->rowCount;  // safe if nullptr
247        ctx->srcRowBuffer = nullptr;
248        ctx->outRowBuffer = nullptr;
249        ctx->rowAccum = nullptr;
250        ctx->rowCount = nullptr;
251        ctx->initFailed = true;
252        return;
253      }
254      ctx->nextOutY_srcStart = ctx->scaleY_fp;
255    }
256  
257    // Skip ditherer allocation in quickMode for faster preview
258    if (!ctx->quickMode) {
259      ctx->ditherer = new (std::nothrow) AtkinsonDitherer(ctx->outWidth);
260      if (!ctx->ditherer) {
261        Serial.printf("[%lu] [PNG] Failed to allocate ditherer\n", millis());
262        free(ctx->srcRowBuffer);
263        free(ctx->outRowBuffer);
264        delete[] ctx->rowAccum;
265        delete[] ctx->rowCount;
266        ctx->srcRowBuffer = nullptr;
267        ctx->outRowBuffer = nullptr;
268        ctx->rowAccum = nullptr;
269        ctx->rowCount = nullptr;
270        ctx->initFailed = true;
271        return;
272      }
273    }
274    ctx->currentSrcY = 0;
275    ctx->currentOutY = 0;
276  
277    // Write BMP header
278    writeBmpHeader2bit(*ctx->bmpOut, ctx->outWidth, ctx->outHeight);
279    ctx->headerWritten = true;
280  }
281  
282  bool pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight,
283                                  bool quickMode) {
284    Serial.printf("[%lu] [PNG] Converting PNG to BMP (target: %dx%d)%s\n", millis(), targetMaxWidth, targetMaxHeight,
285                  quickMode ? " [QUICK]" : "");
286  
287    pngle_t* pngle = pngle_new();
288    if (!pngle) {
289      Serial.printf("[%lu] [PNG] Failed to create pngle instance\n", millis());
290      return false;
291    }
292  
293    PngContext ctx = {};
294    ctx.pngFile = &pngFile;
295    ctx.bmpOut = &bmpOut;
296    ctx.targetMaxWidth = targetMaxWidth;
297    ctx.targetMaxHeight = targetMaxHeight;
298    ctx.headerWritten = false;
299    ctx.quickMode = quickMode;
300  
301    pngle_set_user_data(pngle, &ctx);
302    pngle_set_init_callback(pngle, pngInitCallback);
303    pngle_set_draw_callback(pngle, pngDrawCallback);
304  
305    // Read and feed PNG data
306    uint8_t buffer[1024];
307    int bytesRead;
308    bool success = true;
309  
310    while ((bytesRead = pngFile.read(buffer, sizeof(buffer))) > 0) {
311      int fed = pngle_feed(pngle, buffer, bytesRead);
312      if (fed < 0) {
313        Serial.printf("[%lu] [PNG] pngle_feed error: %s\n", millis(), pngle_error(pngle));
314        success = false;
315        break;
316      }
317    }
318  
319    // Cleanup
320    if (ctx.srcRowBuffer) free(ctx.srcRowBuffer);
321    if (ctx.outRowBuffer) free(ctx.outRowBuffer);
322    if (ctx.rowAccum) delete[] ctx.rowAccum;
323    if (ctx.rowCount) delete[] ctx.rowCount;
324    if (ctx.ditherer) delete ctx.ditherer;
325  
326    pngle_destroy(pngle);
327  
328    if (success && ctx.headerWritten) {
329      Serial.printf("[%lu] [PNG] Successfully converted PNG to BMP (%dx%d)\n", millis(), ctx.outWidth, ctx.outHeight);
330      return true;
331    }
332  
333    return false;
334  }
335  
336  }  // namespace
337  
338  bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
339                                                     int targetMaxHeight) {
340    return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, false);
341  }
342  
343  bool PngToBmpConverter::pngFileToBmpStreamQuick(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
344                                                  int targetMaxHeight) {
345    return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, true);
346  }