// imgDiff: textual diff between two PPM (P6) or PGM (P5) images. // Same dimensions required. Outputs counts, mean/max abs error per channel, // bbox of differing pixels, and a 16x12 ASCII density heatmap. // // Usage: imgDiff a.ppm b.ppm [--ascii] // // PNGs are not supported directly; convert via ImageMagick: // convert in.png ppm:out.ppm #include #include #include #include #include #include typedef struct { int width; int height; int channels; uint8_t *pixels; } ImageT; static int loadImage(const char *path, ImageT *out); static void freeImage(ImageT *img); static int skipWhitespaceAndComments(FILE *f); static int readToken(FILE *f, char *buf, size_t size); static void reportDiff(const ImageT *a, const ImageT *b, bool ascii); static void freeImage(ImageT *img) { free(img->pixels); img->pixels = NULL; } static int loadImage(const char *path, ImageT *out) { FILE *f = fopen(path, "rb"); if (f == NULL) { fprintf(stderr, "imgDiff: cannot open %s\n", path); return -1; } char magic[8]; if (readToken(f, magic, sizeof(magic)) < 0) { fclose(f); return -1; } int channels = 0; if (strcmp(magic, "P6") == 0) { channels = 3; } else if (strcmp(magic, "P5") == 0) { channels = 1; } else { fprintf(stderr, "imgDiff: %s: unsupported magic '%s' (want P5/P6)\n", path, magic); fclose(f); return -1; } char tok[32]; if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; } int width = atoi(tok); if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; } int height = atoi(tok); if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; } int maxval = atoi(tok); if (maxval != 255) { fprintf(stderr, "imgDiff: %s: maxval %d not supported\n", path, maxval); fclose(f); return -1; } // readToken already consumed the single whitespace separator // following maxval, so binary pixel data starts at the cursor. size_t need = (size_t)width * (size_t)height * (size_t)channels; uint8_t *pixels = malloc(need); if (pixels == NULL) { fclose(f); fprintf(stderr, "imgDiff: out of memory\n"); return -1; } if (fread(pixels, 1, need, f) != need) { fprintf(stderr, "imgDiff: %s: short read\n", path); free(pixels); fclose(f); return -1; } fclose(f); out->width = width; out->height = height; out->channels = channels; out->pixels = pixels; return 0; } static int readToken(FILE *f, char *buf, size_t size) { if (skipWhitespaceAndComments(f) < 0) { return -1; } size_t i = 0; int c; while ((c = fgetc(f)) != EOF && !isspace(c)) { if (i + 1 >= size) { return -1; } buf[i++] = (char)c; } buf[i] = '\0'; return (int)i; } static void reportDiff(const ImageT *a, const ImageT *b, bool ascii) { int w = a->width; int h = a->height; int ch = a->channels; size_t total = (size_t)w * (size_t)h; size_t differ = 0; uint64_t sumAbs = 0; int maxAbs = 0; int minX = w; int minY = h; int maxX = -1; int maxY = -1; uint64_t sumLumA = 0; uint64_t sumLumB = 0; const int gridX = 16; const int gridY = 12; int *grid = calloc((size_t)gridX * (size_t)gridY, sizeof(int)); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { const uint8_t *pa = a->pixels + ((size_t)y * w + x) * ch; const uint8_t *pb = b->pixels + ((size_t)y * w + x) * ch; int d = 0; for (int c = 0; c < ch; c++) { int da = abs((int)pa[c] - (int)pb[c]); if (da > d) { d = da; } sumAbs += (uint64_t)da; } if (ch == 3) { sumLumA += (uint64_t)((pa[0] + pa[1] + pa[2]) / 3); sumLumB += (uint64_t)((pb[0] + pb[1] + pb[2]) / 3); } else { sumLumA += pa[0]; sumLumB += pb[0]; } if (d != 0) { differ++; if (d > maxAbs) { maxAbs = d; } if (x < minX) { minX = x; } if (y < minY) { minY = y; } if (x > maxX) { maxX = x; } if (y > maxY) { maxY = y; } int gx = (x * gridX) / w; int gy = (y * gridY) / h; grid[gy * gridX + gx]++; } } } double pct = total > 0 ? (100.0 * (double)differ / (double)total) : 0.0; double mae = (double)sumAbs / ((double)total * (double)ch); double lumA = (double)sumLumA / (double)total; double lumB = (double)sumLumB / (double)total; printf("dims: %dx%d (%zu px, %d ch)\n", w, h, total, ch); printf("identical: %s\n", differ == 0 ? "true" : "false"); printf("pixels_differ: %zu (%.2f%%)\n", differ, pct); printf("mean_abs_err: %.2f\n", mae); printf("max_abs_err: %d\n", maxAbs); if (differ > 0) { printf("bbox_diff: (%d,%d)-(%d,%d) size=%dx%d\n", minX, minY, maxX, maxY, maxX - minX + 1, maxY - minY + 1); } else { printf("bbox_diff: \n"); } printf("luminance_mean: a=%.1f b=%.1f delta=%+.1f\n", lumA, lumB, lumB - lumA); if (ascii && differ > 0) { int peak = 0; for (int i = 0; i < gridX * gridY; i++) { if (grid[i] > peak) { peak = grid[i]; } } printf("heatmap (%dx%d, '.'=0 '#'=peak=%d):\n", gridX, gridY, peak); for (int gy = 0; gy < gridY; gy++) { printf(" "); for (int gx = 0; gx < gridX; gx++) { int v = grid[gy * gridX + gx]; char glyph; if (v == 0) { glyph = '.'; } else { int level = (v * 9 + peak - 1) / peak; if (level < 1) { level = 1; } if (level > 9) { level = 9; } glyph = (char)('0' + level); if (v == peak) { glyph = '#'; } } putchar(glyph); } putchar('\n'); } } free(grid); } static int skipWhitespaceAndComments(FILE *f) { int c; while ((c = fgetc(f)) != EOF) { if (c == '#') { while ((c = fgetc(f)) != EOF && c != '\n') { // discard comment } } else if (!isspace(c)) { ungetc(c, f); return 0; } } return -1; } int main(int argc, char **argv) { bool ascii = false; const char *aPath = NULL; const char *bPath = NULL; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--ascii") == 0) { ascii = true; } else if (aPath == NULL) { aPath = argv[i]; } else if (bPath == NULL) { bPath = argv[i]; } else { fprintf(stderr, "imgDiff: unexpected arg '%s'\n", argv[i]); return 2; } } if (aPath == NULL || bPath == NULL) { fprintf(stderr, "usage: %s a.ppm b.ppm [--ascii]\n", argv[0]); return 2; } ImageT a = {0}; ImageT b = {0}; if (loadImage(aPath, &a) < 0) { return 1; } if (loadImage(bPath, &b) < 0) { freeImage(&a); return 1; } if (a.width != b.width || a.height != b.height || a.channels != b.channels) { fprintf(stderr, "imgDiff: dimensions differ: %dx%dx%d vs %dx%dx%d\n", a.width, a.height, a.channels, b.width, b.height, b.channels); freeImage(&a); freeImage(&b); return 1; } reportDiff(&a, &b, ascii); freeImage(&a); freeImage(&b); return 0; }