/* * img2oric Convert an image to Oric Atmos colours * Copyright (c) 2008 Sam Hocevar * All Rights Reserved * * Changes: * Jan 18, 2008: initial release * Jan 23, 2008: add support for inverse video on attribute change * improve Floyd-Steinberg coefficient values * Jun 14, 2008: Win32 version * Jun 18, 2008: add meaningful error messages * * This program is free software. It comes without any warranty, to * the extent permitted by applicable law. You can redistribute it * and/or modify it under the terms of the Do What The Fuck You Want * To Public License, Version 2, as published by Sam Hocevar. See * http://sam.zoy.org/wtfpl/COPYING for more details. * * To build this program on Linux: * cc -O3 -funroll-loops -W -Wall img2oric.c -o img2oric \ * $(pkg-config --cflags --libs sdl) -lSDL_image -lm * To build a Windows executable: * i586-mingw32msvc-cc -O3 -funroll-loops -W -Wall \ * img2oric.c -o img2oric.exe -lgdi32 */ #include #include #include #include #if defined _WIN32 # include # define uint8_t unsigned char #else # include #endif /* * BMP output name and Oric output name */ #define BMPFILE "output.bmp" #define ORICFILE "OUTPUT" /* ".TAP" will be appended */ /* * Image dimensions and recursion depth. DEPTH = 2 is a reasonable value, * DEPTH = 3 gives good quality, and higher values may improve the results * even more but at the cost of significantly longer computation times. */ #define WIDTH 240 #define HEIGHT 200 #define DEPTH 3 /* * Error diffusion table, similar to Floyd-Steinberg. I choose not to * propagate 100% of the error, because doing so creates awful artifacts * (full lines of the same colour, massive colour bleeding) for unclear * reasons. Atkinson dithering propagates 3/4 of the error, which is even * less than our 31/32. I also choose to propagate slightly more in the * X direction to avoid banding effects due to rounding errors. * It would be interesting, for future versions of this software, to * propagate the error to the second line, too. But right now I find it far * too complex to do. * * +-------+-------+ * | error |FS0/FSX| * +-------+-------+-------+ * |FS1/FSX|FS2/FSX|FS3/FSX| * +-------+-------+-------+ */ #define FS0 15 #define FS1 6 #define FS2 9 #define FS3 1 #define FSX 32 /* * The simple Oric RGB palette, made of the 8 Neugebauer primary colours. Each * colour is repeated 6 times so that we can point to the palette to paste * whole blocks of 6 pixels. It’s also organised so that palette[7-x] is the * RGB negative of palette[x], and screen command X uses palette[X & 7]. */ #define o 0x0000 #define X 0xffff static const int palette[8][6 * 3] = { { o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o }, { X, o, o, X, o, o, X, o, o, X, o, o, X, o, o, X, o, o }, { o, X, o, o, X, o, o, X, o, o, X, o, o, X, o, o, X, o }, { X, X, o, X, X, o, X, X, o, X, X, o, X, X, o, X, X, o }, { o, o, X, o, o, X, o, o, X, o, o, X, o, o, X, o, o, X }, { X, o, X, X, o, X, X, o, X, X, o, X, X, o, X, X, o, X }, { o, X, X, o, X, X, o, X, X, o, X, X, o, X, X, o, X, X }, { X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X }, }; /* * Gamma correction tables. itoc_table and ctoi_table accept overflow and * underflow values to a reasonable extent, so that we don’t have to check * for these cases later in the code. Tests kill performance. */ #define PAD 2048 static int itoc_table_clip[PAD + 256 + PAD], ctoi_table_clip[PAD + 256 + PAD]; static int *itoc_table = itoc_table_clip + PAD; static int *ctoi_table = ctoi_table_clip + PAD; static void init_tables(void) { int i; for(i = 0; i < PAD + 256 + PAD; i++) { double f = 1.0 * (i - PAD) / 255.999; if(f >= 0.) { itoc_table_clip[i] = (int)(65535.999 * pow(f, 1./2.2)); ctoi_table_clip[i] = (int)(65535.999 * pow(f, 2.2)); } else { itoc_table_clip[i] = - (int)(65535.999 * pow(-f, 1./2.2)); ctoi_table_clip[i] = - (int)(65535.999 * pow(-f, 2.2)); } } } static inline int itoc(int p) { return itoc_table[p / 0x100]; } static inline int ctoi(int p) { return ctoi_table[p / 0x100]; } /* * Set new background and foreground colours according to the given command. */ static inline void domove(uint8_t command, uint8_t *bg, uint8_t *fg) { if((command & 0x78) == 0x00) *fg = command & 0x7; else if((command & 0x78) == 0x10) *bg = command & 0x7; } /* * Clamp pixel value to avoid colour bleeding. Deactivated because it * does not give satisfactory results. */ #define CLAMP 0x1000 static inline int clamp(int p) { #if 0 /* FIXME: doesn’t give terribly good results on eg. eatme.png */ if(p < - CLAMP) return - CLAMP; if(p > 0xffff + CLAMP) return 0xffff + CLAMP; #endif return p; } /* * Compute the perceptual error caused by replacing the input pixels "in" * with the output pixels "out". "inerr" is the diffused error that should * be applied to "in"’s first pixel. "outerr" will hold the diffused error * to apply after "in"’s last pixel upon next call. The return value does * not mean much physically; it is one part of the algorithm where you need * to play a bit in order to get appealing results. That’s how image * processing works, dude. */ static inline int geterror(int const *in, int const *inerr, int const *out, int *outerr) { int tmperr[9 * 3]; int i, c, ret = 0; /* 9 cells: 1 for the end of line, 8 for the errors below */ memcpy(tmperr, inerr, 3 * sizeof(int)); memset(tmperr + 3, 0, 8 * 3 * sizeof(int)); for(i = 0; i < 6; i++) { for(c = 0; c < 3; c++) { /* Experiment shows that this is important at small depths */ int a = clamp(in[i * 3 + c] + tmperr[c]); int b = out[i * 3 + c]; tmperr[c] = (a - b) * FS0 / FSX; tmperr[c + (i * 3 + 3)] += (a - b) * FS1 / FSX; tmperr[c + (i * 3 + 6)] += (a - b) * FS2 / FSX; tmperr[c + (i * 3 + 9)] += (a - b) * FS3 / FSX; ret += (a - b) / 256 * (a - b) / 256; } } for(i = 0; i < 4; i++) { for(c = 0; c < 3; c++) { /* Experiment shows that this is important at large depths */ int a = itoc((in[i * 3 + c] + in[i * 3 + 3 + c] + in[i * 3 + 6 + c]) / 3); int b = itoc((out[i * 3 + c] + out[i * 3 + 3 + c] + out[i * 3 + 6 + c]) / 3); ret += (a - b) / 256 * (a - b) / 256; } } /* Using the diffused error as a perceptual error component is stupid, * because that’s not what it is at all, but I found that it helped a * bit in some cases. */ for(i = 0; i < 3; i++) ret += tmperr[i] / 256 * tmperr[i] / 256; memcpy(outerr, tmperr, 3 * sizeof(int)); return ret; } static uint8_t bestmove(int const *in, uint8_t bg, uint8_t fg, int const *errvec, int depth, int maxerror, int *error, int *out) { int voidvec[3], nvoidvec[3], bestrgb[6 * 3], tmprgb[6 * 3], tmpvec[3]; int const *voidrgb, *nvoidrgb, *vec, *rgb; int besterror, curerror, suberror, statice, voide, nvoide; int i, j, c; uint8_t command, bestcommand; /* Precompute error for the case where we change the foreground colour * and hence only print the background colour or its negative */ voidrgb = palette[bg]; voide = geterror(in, errvec, voidrgb, voidvec); nvoidrgb = palette[7 - bg]; nvoide = geterror(in, errvec, nvoidrgb, nvoidvec); /* Precompute sub-error for the case where we print pixels (and hence * don’t change the palette). It’s not the exact error because we should * be propagating the error to the first pixel here. */ if(depth > 0) { int tmp[3] = { 0, 0, 0 }; bestmove(in + 6 * 3, bg, fg, tmp, depth - 1, maxerror, &statice, NULL); } /* Check every likely command: * 0-7: change foreground to 0-7 * 8-15: change foreground to 0-7, print negative background * 16-23: change background to 0-7 * 24-31: change background to 0-7, print negative background * 32: normal stuff * 33: inverse video stuff */ besterror = 0x7ffffff; bestcommand = 0x10; memcpy(bestrgb, voidrgb, 6 * 3 * sizeof(int)); for(j = 0; j < 34; j++) { static uint8_t const lookup[] = { 0x00, 0x04, 0x01, 0x05, 0x02, 0x06, 0x03, 0x07, 0x80, 0x84, 0x81, 0x85, 0x82, 0x86, 0x83, 0x87, 0x10, 0x14, 0x11, 0x15, 0x12, 0x16, 0x13, 0x17, 0x90, 0x94, 0x91, 0x95, 0x92, 0x96, 0x93, 0x97, 0x40, 0xc0 }; uint8_t newbg = bg, newfg = fg; command = lookup[j]; domove(command, &newbg, &newfg); /* Keeping bg and fg is useless, because we could use standard * pixel printing instead */ if((command & 0x40) == 0x00 && newbg == bg && newfg == fg) continue; /* I *think* having newfg == newbg is useless, too, but I don’t * want to miss some corner case where swapping bg and fg may be * interesting, so we continue anyway. */ #if 0 /* Bit 6 off and bit 5 on seems illegal */ if((command & 0x60) == 0x20) continue; /* Bits 6 and 5 off and bit 3 on seems illegal */ if((command & 0x68) == 0x08) continue; #endif if((command & 0xf8) == 0x00) { curerror = voide; rgb = voidrgb; vec = voidvec; } else if((command & 0xf8) == 0x80) { curerror = nvoide; rgb = nvoidrgb; vec = nvoidvec; } else if((command & 0xf8) == 0x10) { rgb = palette[newbg]; curerror = geterror(in, errvec, rgb, tmpvec); vec = tmpvec; } else if((command & 0xf8) == 0x90) { rgb = palette[7 - newbg]; curerror = geterror(in, errvec, rgb, tmpvec); vec = tmpvec; } else { int const *bgcolor, *fgcolor; if((command & 0x80) == 0x00) { bgcolor = palette[bg]; fgcolor = palette[fg]; } else { bgcolor = palette[7 - bg]; fgcolor = palette[7 - fg]; } memcpy(tmpvec, errvec, 3 * sizeof(int)); curerror = 0; for(i = 0; i < 6; i++) { int vec1[3], vec2[3]; int smalle1 = 0, smalle2 = 0; memcpy(vec1, tmpvec, 3 * sizeof(int)); memcpy(vec2, tmpvec, 3 * sizeof(int)); for(c = 0; c < 3; c++) { int delta1, delta2; delta1 = clamp(in[i * 3 + c] + tmpvec[c]) - bgcolor[c]; vec1[c] = delta1 * FS0 / FSX; smalle1 += delta1 / 256 * delta1; delta2 = clamp(in[i * 3 + c] + tmpvec[c]) - fgcolor[c]; vec2[c] = delta2 * FS0 / FSX; smalle2 += delta2 / 256 * delta2; } if(smalle1 < smalle2) { memcpy(tmpvec, vec1, 3 * sizeof(int)); memcpy(tmprgb + i * 3, bgcolor, 3 * sizeof(int)); } else { memcpy(tmpvec, vec2, 3 * sizeof(int)); memcpy(tmprgb + i * 3, fgcolor, 3 * sizeof(int)); command |= (1 << (5 - i)); } } /* Recompute full error */ curerror += geterror(in, errvec, tmprgb, tmpvec); rgb = tmprgb; vec = tmpvec; } if(curerror > besterror) continue; /* Try to avoid bad decisions now that will have a high cost * later in the line by making the next error more important than * the current error. */ curerror = curerror * 3 / 4; if(depth == 0) suberror = 0; /* It’s the end of the tree */ else if((command & 0x68) == 0x00) { bestmove(in + 6 * 3, newbg, newfg, vec, depth - 1, besterror - curerror, &suberror, NULL); #if 0 /* Slight penalty for colour changes; they're hard to revert. The * value of 2 was determined empirically. 1.5 is not enough and * 3 is too much. */ if(newbg != bg) suberror = suberror * 10 / 8; else if(newfg != fg) suberror = suberror * 9 / 8; #endif } else suberror = statice; if(curerror + suberror < besterror) { besterror = curerror + suberror; bestcommand = command; memcpy(bestrgb, rgb, 6 * 3 * sizeof(int)); } } *error = besterror; if(out) memcpy(out, bestrgb, 6 * 3 * sizeof(int)); return bestcommand; } int main(int argc, char *argv[]) { #if defined _WIN32 PBITMAPINFO pbinfo; BITMAPINFO binfo; BITMAPFILEHEADER bfheader; ULONG bisize; HANDLE hfile; HBITMAP tmp; HDC hdc; DWORD ret; #else SDL_Surface *tmp, *surface; #endif FILE *f; uint8_t *pixels; int *src, *dst, *srcl, *dstl; int stride, x, y, depth, c; if(argc < 2) { fprintf(stderr, "Error: missing argument.\n"); fprintf(stderr, "Usage: img2oric \n"); return 1; } #if defined _WIN32 tmp = (HBITMAP)LoadImage(NULL, argv[1], IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); #else tmp = IMG_Load(argv[1]); #endif if(!tmp) { fprintf(stderr, "Error: could not load image %s.\n", argv[1]); #if defined _WIN32 fprintf(stderr, "Maybe try with an 8-bpp or 24-bpp BMP file?\n"); #endif return 2; } f = fopen(ORICFILE ".TAP", "w"); if(!f) { fprintf(stderr, "Error: could not open %s.TAP for writing.\n", ORICFILE); return 3; } fwrite("\x16\x16\x16\x16\x24", 1, 5, f); fwrite("\x00\xff\x80\x00\xbf\x3f\xa0\x00\x00", 1, 9, f); fwrite(ORICFILE, 1, strlen(ORICFILE), f); fwrite("\x00", 1, 1, f); init_tables(); /* Load the image into a friendly array of fast integers. We create it * slightly bigger than the image so that we don’t have to care about * borders when propagating the error later */ src = calloc((WIDTH + 1) * (HEIGHT + 1) * 3, sizeof(int)); dst = calloc((WIDTH + 1) * (HEIGHT + 1) * 3, sizeof(int)); stride = (WIDTH + 1) * 3; #if defined _WIN32 hdc = CreateCompatibleDC(NULL); SelectObject(hdc, tmp); for(y = 0; y < HEIGHT; y++) for(x = 0; x < WIDTH; x++) { COLORREF color = GetPixel(hdc, x, y); src[y * stride + x * 3] = ctoi(GetRValue(color) * 0x101); src[y * stride + x * 3 + 1] = ctoi(GetGValue(color) * 0x101); src[y * stride + x * 3 + 2] = ctoi(GetBValue(color) * 0x101); for(c = 0; c < 3; c++) dst[y * stride + x * 3 + c] = 0; } #else /* FIXME: endianness */ surface = SDL_CreateRGBSurface(SDL_SWSURFACE, WIDTH, HEIGHT, 32, 0xff0000, 0xff00, 0xff, 0x0); SDL_BlitSurface(tmp, NULL, surface, NULL); pixels = (uint8_t *)surface->pixels; for(y = 0; y < HEIGHT; y++) for(x = 0; x < WIDTH; x++) for(c = 0; c < 3; c++) { src[y * stride + x * 3 + c] = ctoi(pixels[y * surface->pitch + x * 4 + 2 - c] * 0x101); dst[y * stride + x * 3 + c] = 0; } #endif /* Let the fun begin */ for(y = 0; y < HEIGHT; y++) { uint8_t bg = 0, fg = 7; fprintf(stderr, "\rProcessing... %i%%", (y * 100 + 99) / HEIGHT); for(x = 0; x < WIDTH; x += 6) { int errvec[3] = { 0, 0, 0 }; int dummy, i; uint8_t command; depth = (x + DEPTH < WIDTH) ? DEPTH : (WIDTH - x) / 6 - 1; srcl = src + y * stride + x * 3; dstl = dst + y * stride + x * 3; /* Recursively compute and apply best command */ command = bestmove(srcl, bg, fg, errvec, depth, 0x7fffff, &dummy, dstl); /* Propagate error */ for(c = 0; c < 3; c++) { for(i = 0; i < 6; i++) { int error = srcl[i * 3 + c] - dstl[i * 3 + c]; srcl[i * 3 + c + 3] = clamp(srcl[i * 3 + c + 3] + error * FS0 / FSX); srcl[i * 3 + c + stride - 3] += error * FS1 / FSX; srcl[i * 3 + c + stride] += error * FS2 / FSX; srcl[i * 3 + c + stride + 3] += error * FS3 / FSX; } for(i = -1; i < 7; i++) srcl[i * 3 + c + stride] = clamp(srcl[i * 3 + c + stride]); } /* Iterate */ domove(command, &bg, &fg); /* Write byte to file */ fwrite(&command, 1, 1, f); } } fclose(f); fprintf(stderr, " done.\n"); /* Save everything */ #if defined _WIN32 for(y = 0; y < HEIGHT; y++) for(x = 0; x < WIDTH; x++) { uint8_t r = dst[y * stride + x * 3] / 0x100; uint8_t g = dst[y * stride + x * 3 + 1] / 0x100; uint8_t b = dst[y * stride + x * 3 + 2] / 0x100; SetPixel(hdc, x, y, RGB(r, g, b)); } binfo.bmiHeader.biSize = sizeof(binfo.bmiHeader); binfo.bmiHeader.biBitCount = 0; GetDIBits(hdc, tmp, 0, 0, NULL, &binfo, DIB_RGB_COLORS); switch(binfo.bmiHeader.biBitCount) { case 24: bisize = sizeof(BITMAPINFOHEADER); break; case 16: case 32: bisize = sizeof(BITMAPINFOHEADER) + sizeof(DWORD) * 3; break; default: bisize = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * (1 << binfo.bmiHeader.biBitCount); break; } pbinfo = (PBITMAPINFO)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, bisize); memcpy(pbinfo, &binfo, sizeof(BITMAPINFOHEADER)); bfheader.bfType = 0x4D42; /* "BM" */ bfheader.bfSize = sizeof(BITMAPFILEHEADER) + bisize + pbinfo->bmiHeader.biSizeImage; bfheader.bfReserved1 = 0; bfheader.bfReserved2 = 0; bfheader.bfOffBits = sizeof(BITMAPFILEHEADER) + bisize; pixels = GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, binfo.bmiHeader.biSizeImage); GetDIBits(hdc, tmp, 0, pbinfo->bmiHeader.biHeight, pixels, pbinfo, DIB_RGB_COLORS); hfile = CreateFile(BMPFILE, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_ARCHIVE, NULL); WriteFile(hfile, &bfheader, sizeof(BITMAPFILEHEADER), &ret, NULL); WriteFile(hfile, pbinfo, bisize, &ret, NULL); WriteFile(hfile, pixels, pbinfo->bmiHeader.biSizeImage, &ret, NULL); CloseHandle(hfile); GlobalFree(pbinfo); GlobalFree(pixels); #else for(y = 0; y < HEIGHT; y++) for(x = 0; x < WIDTH; x++) for(c = 0; c < 3; c++) pixels[y * surface->pitch + x * 4 + 2 - c] = itoc(dst[y * stride + x * 3 + c]) / 0x100; SDL_SaveBMP(surface, BMPFILE); #endif return 0; }