source: www/img2oric/img2oric.c @ 2394

Last change on this file since 2394 was 2394, checked in by Sam Hocevar, 15 years ago
  • Build the win32 binary with aggressive optimisation flags.
File size: 19.1 KB
Line 
1/*
2 *  img2oric      Convert an image to Oric Atmos colours
3 *  Copyright (c) 2008 Sam Hocevar <sam@zoy.org>
4 *                All Rights Reserved
5 *
6 *  Changes:
7 *   Jan 18, 2008: initial release
8 *   Jan 23, 2008: add support for inverse video on attribute change
9 *                 improve Floyd-Steinberg coefficient values
10 *   Jun 14, 2008: Win32 version
11 *
12 *  This program is free software. It comes without any warranty, to
13 *  the extent permitted by applicable law. You can redistribute it
14 *  and/or modify it under the terms of the Do What The Fuck You Want
15 *  To Public License, Version 2, as published by Sam Hocevar. See
16 *  http://sam.zoy.org/wtfpl/COPYING for more details.
17 *
18 *  To build this program on Linux:
19 *   cc -O3 -funroll-loops -W -Wall img2oric.c -o img2oric \
20 *               $(pkg-config --cflags --libs sdl) -lSDL_image -lm
21 *  To build a Windows executable:
22 *   i586-mingw32msvc-cc -O3 -funroll-loops -W -Wall \
23 *               img2oric.c -o img2oric.exe -lgdi32
24 */
25
26#include <stdio.h>
27#include <stdlib.h>
28#include <string.h>
29
30#include <math.h>
31
32#if defined _WIN32
33#   include <windows.h>
34#   define uint8_t unsigned char
35#else
36#   include <SDL_image.h>
37#endif
38
39/*
40 * BMP output name and Oric output name
41 */
42#define BMPFILE "output.bmp"
43#define ORICFILE "OUTPUT" /* ".TAP" will be appended */
44
45/*
46 * Image dimensions and recursion depth. DEPTH = 2 is a reasonable value,
47 * DEPTH = 3 gives good quality, and higher values may improve the results
48 * even more but at the cost of significantly longer computation times.
49 */
50#define WIDTH 240
51#define HEIGHT 200
52#define DEPTH 3
53
54/*
55 * Error diffusion table, similar to Floyd-Steinberg. I choose not to
56 * propagate 100% of the error, because doing so creates awful artifacts
57 * (full lines of the same colour, massive colour bleeding) for unclear
58 * reasons. Atkinson dithering propagates 3/4 of the error, which is even
59 * less than our 31/32. I also choose to propagate slightly more in the
60 * X direction to avoid banding effects due to rounding errors.
61 * It would be interesting, for future versions of this software, to
62 * propagate the error to the second line, too. But right now I find it far
63 * too complex to do.
64 *
65 *             +-------+-------+
66 *             | error |FS0/FSX|
67 *     +-------+-------+-------+
68 *     |FS1/FSX|FS2/FSX|FS3/FSX|
69 *     +-------+-------+-------+
70 */
71#define FS0 15
72#define FS1 6
73#define FS2 9
74#define FS3 1
75#define FSX 32
76
77/*
78 * The simple Oric RGB palette, made of the 8 Neugebauer primary colours. Each
79 * colour is repeated 6 times so that we can point to the palette to paste
80 * whole blocks of 6 pixels. It’s also organised so that palette[7-x] is the
81 * RGB negative of palette[x], and screen command X uses palette[X & 7].
82 */
83#define o 0x0000
84#define X 0xffff
85static const int palette[8][6 * 3] =
86{
87    { o, o, o,   o, o, o,   o, o, o,   o, o, o,   o, o, o,   o, o, o },
88    { X, o, o,   X, o, o,   X, o, o,   X, o, o,   X, o, o,   X, o, o },
89    { o, X, o,   o, X, o,   o, X, o,   o, X, o,   o, X, o,   o, X, o },
90    { X, X, o,   X, X, o,   X, X, o,   X, X, o,   X, X, o,   X, X, o },
91    { o, o, X,   o, o, X,   o, o, X,   o, o, X,   o, o, X,   o, o, X },
92    { X, o, X,   X, o, X,   X, o, X,   X, o, X,   X, o, X,   X, o, X },
93    { o, X, X,   o, X, X,   o, X, X,   o, X, X,   o, X, X,   o, X, X },
94    { X, X, X,   X, X, X,   X, X, X,   X, X, X,   X, X, X,   X, X, X },
95};
96
97/*
98 * Gamma correction tables. itoc_table and ctoi_table accept overflow and
99 * underflow values to a reasonable extent, so that we don’t have to check
100 * for these cases later in the code. Tests kill performance.
101 */
102#define PAD 2048
103static int itoc_table_clip[PAD + 256 + PAD], ctoi_table_clip[PAD + 256 + PAD];
104static int *itoc_table = itoc_table_clip + PAD;
105static int *ctoi_table = ctoi_table_clip + PAD;
106
107static void init_tables(void)
108{
109    int i;
110
111    for(i = 0; i < PAD + 256 + PAD; i++)
112    {
113        double f = 1.0 * (i - PAD) / 255.999;
114        if(f >= 0.)
115        {
116            itoc_table_clip[i] = (int)(65535.999 * pow(f, 1./2.2));
117            ctoi_table_clip[i] = (int)(65535.999 * pow(f, 2.2));
118        }
119        else
120        {
121            itoc_table_clip[i] = - (int)(65535.999 * pow(-f, 1./2.2));
122            ctoi_table_clip[i] = - (int)(65535.999 * pow(-f, 2.2));
123        }
124    }
125}
126
127static inline int itoc(int p) { return itoc_table[p / 0x100]; }
128static inline int ctoi(int p) { return ctoi_table[p / 0x100]; }
129
130/*
131 * Set new background and foreground colours according to the given command.
132 */
133static inline void domove(uint8_t command, uint8_t *bg, uint8_t *fg)
134{
135    if((command & 0x78) == 0x00)
136        *fg = command & 0x7;
137    else if((command & 0x78) == 0x10)
138        *bg = command & 0x7;
139}
140
141/*
142 * Clamp pixel value to avoid colour bleeding. Deactivated because it
143 * does not give satisfactory results.
144 */
145#define CLAMP 0x1000
146static inline int clamp(int p)
147{
148#if 0
149    /* FIXME: doesn’t give terribly good results on eg. eatme.png */
150    if(p < - CLAMP) return - CLAMP;
151    if(p > 0xffff + CLAMP) return 0xffff + CLAMP;
152#endif
153    return p;
154}
155
156/*
157 * Compute the perceptual error caused by replacing the input pixels "in"
158 * with the output pixels "out". "inerr" is the diffused error that should
159 * be applied to "in"’s first pixel. "outerr" will hold the diffused error
160 * to apply after "in"’s last pixel upon next call. The return value does
161 * not mean much physically; it is one part of the algorithm where you need
162 * to play a bit in order to get appealing results. That’s how image
163 * processing works, dude.
164 */
165static inline int geterror(int const *in, int const *inerr,
166                           int const *out, int *outerr)
167{
168    int tmperr[9 * 3];
169    int i, c, ret = 0;
170
171    /* 9 cells: 1 for the end of line, 8 for the errors below */
172    memcpy(tmperr, inerr, 3 * sizeof(int));
173    memset(tmperr + 3, 0, 8 * 3 * sizeof(int));
174
175    for(i = 0; i < 6; i++)
176    {
177        for(c = 0; c < 3; c++)
178        {
179            /* Experiment shows that this is important at small depths */
180            int a = clamp(in[i * 3 + c] + tmperr[c]);
181            int b = out[i * 3 + c];
182            tmperr[c] = (a - b) * FS0 / FSX;
183            tmperr[c + (i * 3 + 3)] += (a - b) * FS1 / FSX;
184            tmperr[c + (i * 3 + 6)] += (a - b) * FS2 / FSX;
185            tmperr[c + (i * 3 + 9)] += (a - b) * FS3 / FSX;
186            ret += (a - b) / 256 * (a - b) / 256;
187        }
188    }
189
190    for(i = 0; i < 4; i++)
191    {
192        for(c = 0; c < 3; c++)
193        {
194            /* Experiment shows that this is important at large depths */
195            int a = itoc((in[i * 3 + c] + in[i * 3 + 3 + c]
196                                        + in[i * 3 + 6 + c]) / 3);
197            int b = itoc((out[i * 3 + c] + out[i * 3 + 3 + c]
198                                         + out[i * 3 + 6 + c]) / 3);
199            ret += (a - b) / 256 * (a - b) / 256;
200        }
201    }
202
203    /* Using the diffused error as a perceptual error component is stupid,
204     * because that’s not what it is at all, but I found that it helped a
205     * bit in some cases. */
206    for(i = 0; i < 3; i++)
207        ret += tmperr[i] / 256 * tmperr[i] / 256;
208
209    memcpy(outerr, tmperr, 3 * sizeof(int));
210
211    return ret;
212}
213
214static uint8_t bestmove(int const *in, uint8_t bg, uint8_t fg,
215                        int const *errvec, int depth, int maxerror,
216                        int *error, int *out)
217{
218    int voidvec[3], nvoidvec[3], bestrgb[6 * 3], tmprgb[6 * 3], tmpvec[3];
219    int const *voidrgb, *nvoidrgb, *vec, *rgb;
220    int besterror, curerror, suberror, statice, voide, nvoide;
221    int i, j, c;
222    uint8_t command, bestcommand;
223
224    /* Precompute error for the case where we change the foreground colour
225     * and hence only print the background colour or its negative */
226    voidrgb = palette[bg];
227    voide = geterror(in, errvec, voidrgb, voidvec);
228    nvoidrgb = palette[7 - bg];
229    nvoide = geterror(in, errvec, nvoidrgb, nvoidvec);
230
231    /* Precompute sub-error for the case where we print pixels (and hence
232     * don’t change the palette). It’s not the exact error because we should
233     * be propagating the error to the first pixel here. */
234    if(depth > 0)
235    {
236        int tmp[3] = { 0, 0, 0 };
237        bestmove(in + 6 * 3, bg, fg, tmp, depth - 1, maxerror, &statice, NULL);
238    }
239
240    /* Check every likely command:
241     * 0-7: change foreground to 0-7
242     * 8-15: change foreground to 0-7, print negative background
243     * 16-23: change background to 0-7
244     * 24-31: change background to 0-7, print negative background
245     * 32: normal stuff
246     * 33: inverse video stuff */
247    besterror = 0x7ffffff;
248    bestcommand = 0x10;
249    memcpy(bestrgb, voidrgb, 6 * 3 * sizeof(int));
250    for(j = 0; j < 34; j++)
251    {
252        static uint8_t const lookup[] =
253        {
254            0x00, 0x04, 0x01, 0x05, 0x02, 0x06, 0x03, 0x07,
255            0x80, 0x84, 0x81, 0x85, 0x82, 0x86, 0x83, 0x87,
256            0x10, 0x14, 0x11, 0x15, 0x12, 0x16, 0x13, 0x17,
257            0x90, 0x94, 0x91, 0x95, 0x92, 0x96, 0x93, 0x97,
258            0x40, 0xc0
259        };
260
261        uint8_t newbg = bg, newfg = fg;
262
263        command = lookup[j];
264        domove(command, &newbg, &newfg);
265
266        /* Keeping bg and fg is useless, because we could use standard
267         * pixel printing instead */
268        if((command & 0x40) == 0x00 && newbg == bg && newfg == fg)
269            continue;
270
271        /* I *think* having newfg == newbg is useless, too, but I don’t
272         * want to miss some corner case where swapping bg and fg may be
273         * interesting, so we continue anyway. */
274
275#if 0
276        /* Bit 6 off and bit 5 on seems illegal */
277        if((command & 0x60) == 0x20)
278            continue;
279
280        /* Bits 6 and 5 off and bit 3 on seems illegal */
281        if((command & 0x68) == 0x08)
282            continue;
283#endif
284
285        if((command & 0xf8) == 0x00)
286        {
287            curerror = voide;
288            rgb = voidrgb;
289            vec = voidvec;
290        }
291        else if((command & 0xf8) == 0x80)
292        {
293            curerror = nvoide;
294            rgb = nvoidrgb;
295            vec = nvoidvec;
296        }
297        else if((command & 0xf8) == 0x10)
298        {
299            rgb = palette[newbg];
300            curerror = geterror(in, errvec, rgb, tmpvec);
301            vec = tmpvec;
302        }
303        else if((command & 0xf8) == 0x90)
304        {
305            rgb = palette[7 - newbg];
306            curerror = geterror(in, errvec, rgb, tmpvec);
307            vec = tmpvec;
308        }
309        else
310        {
311            int const *bgcolor, *fgcolor;
312
313            if((command & 0x80) == 0x00)
314            {
315                bgcolor = palette[bg]; fgcolor = palette[fg];
316            }
317            else
318            {
319                bgcolor = palette[7 - bg]; fgcolor = palette[7 - fg];
320            }
321
322            memcpy(tmpvec, errvec, 3 * sizeof(int));
323            curerror = 0;
324
325            for(i = 0; i < 6; i++)
326            {
327                int vec1[3], vec2[3];
328                int smalle1 = 0, smalle2 = 0;
329
330                memcpy(vec1, tmpvec, 3 * sizeof(int));
331                memcpy(vec2, tmpvec, 3 * sizeof(int));
332                for(c = 0; c < 3; c++)
333                {
334                    int delta1, delta2;
335                    delta1 = clamp(in[i * 3 + c] + tmpvec[c]) - bgcolor[c];
336                    vec1[c] = delta1 * FS0 / FSX;
337                    smalle1 += delta1 / 256 * delta1;
338                    delta2 = clamp(in[i * 3 + c] + tmpvec[c]) - fgcolor[c];
339                    vec2[c] = delta2 * FS0 / FSX;
340                    smalle2 += delta2 / 256 * delta2;
341                }
342
343                if(smalle1 < smalle2)
344                {
345                    memcpy(tmpvec, vec1, 3 * sizeof(int));
346                    memcpy(tmprgb + i * 3, bgcolor, 3 * sizeof(int));
347                }
348                else
349                {
350                    memcpy(tmpvec, vec2, 3 * sizeof(int));
351                    memcpy(tmprgb + i * 3, fgcolor, 3 * sizeof(int));
352                    command |= (1 << (5 - i));
353                }
354            }
355
356            /* Recompute full error */
357            curerror += geterror(in, errvec, tmprgb, tmpvec);
358
359            rgb = tmprgb;
360            vec = tmpvec;
361        }
362
363        if(curerror > besterror)
364            continue;
365
366        /* Try to avoid bad decisions now that will have a high cost
367         * later in the line by making the next error more important than
368         * the current error. */
369        curerror = curerror * 3 / 4;
370
371        if(depth == 0)
372            suberror = 0; /* It’s the end of the tree */
373        else if((command & 0x68) == 0x00)
374        {
375            bestmove(in + 6 * 3, newbg, newfg, vec, depth - 1,
376                     besterror - curerror, &suberror, NULL);
377
378#if 0
379            /* Slight penalty for colour changes; they're hard to revert. The
380             * value of 2 was determined empirically. 1.5 is not enough and
381             * 3 is too much. */
382            if(newbg != bg)
383                suberror = suberror * 10 / 8;
384            else if(newfg != fg)
385                suberror = suberror * 9 / 8;
386#endif
387        }
388        else
389            suberror = statice;
390
391        if(curerror + suberror < besterror)
392        {
393            besterror = curerror + suberror;
394            bestcommand = command;
395            memcpy(bestrgb, rgb, 6 * 3 * sizeof(int));
396        }
397    }
398
399    *error = besterror;
400    if(out)
401        memcpy(out, bestrgb, 6 * 3 * sizeof(int));
402
403    return bestcommand;
404}
405
406int main(int argc, char *argv[])
407{
408#if defined _WIN32
409    PBITMAPINFO pbinfo;
410    BITMAPINFO binfo;
411    BITMAPFILEHEADER bfheader;
412    ULONG bisize;
413    HANDLE hfile;
414    HBITMAP tmp;
415    HDC hdc;
416    DWORD ret;
417#else
418    SDL_Surface *tmp, *surface;
419#endif
420    FILE *f;
421    uint8_t *pixels;
422    int *src, *dst, *srcl, *dstl;
423    int stride, x, y, depth, c;
424
425    if(argc < 2)
426        return 1;
427
428#if defined _WIN32
429    tmp = (HBITMAP)LoadImage(NULL, argv[1], IMAGE_BITMAP,
430                             0, 0, LR_LOADFROMFILE);
431#else
432    tmp = IMG_Load(argv[1]);
433#endif
434    if(!tmp)
435        return 2;
436
437    f = fopen(ORICFILE ".TAP", "w");
438    if(!f)
439        return 3;
440    fwrite("\x16\x16\x16\x16\x24", 1, 5, f);
441    fwrite("\x00\xff\x80\x00\xbf\x3f\xa0\x00", 1, 8, f);
442    fwrite(ORICFILE, 1, strlen(ORICFILE), f);
443    fwrite("\x00", 1, 1, f);
444
445    init_tables();
446
447    /* Load the image into a friendly array of fast integers. We create it
448     * slightly bigger than the image so that we don’t have to care about
449     * borders when propagating the error later */
450    src = calloc((WIDTH + 1) * (HEIGHT + 1) * 3, sizeof(int));
451    dst = calloc((WIDTH + 1) * (HEIGHT + 1) * 3, sizeof(int));
452    stride = (WIDTH + 1) * 3;
453
454#if defined _WIN32
455    hdc = CreateCompatibleDC(NULL);
456    SelectObject(hdc, tmp);
457    for(y = 0; y < HEIGHT; y++)
458        for(x = 0; x < WIDTH; x++)
459        {
460            COLORREF color = GetPixel(hdc, x, y);
461            src[y * stride + x * 3] = ctoi(GetRValue(color) * 0x101);
462            src[y * stride + x * 3 + 1] = ctoi(GetGValue(color) * 0x101);
463            src[y * stride + x * 3 + 2] = ctoi(GetBValue(color) * 0x101);
464            for(c = 0; c < 3; c++)
465                dst[y * stride + x * 3 + c] = 0;
466        }
467#else
468    /* FIXME: endianness */
469    surface = SDL_CreateRGBSurface(SDL_SWSURFACE, WIDTH, HEIGHT, 32,
470                                   0xff0000, 0xff00, 0xff, 0x0);
471    SDL_BlitSurface(tmp, NULL, surface, NULL);
472    pixels = (uint8_t *)surface->pixels;
473    for(y = 0; y < HEIGHT; y++)
474        for(x = 0; x < WIDTH; x++)
475            for(c = 0; c < 3; c++)
476            {
477                src[y * stride + x * 3 + c]
478                    = ctoi(pixels[y * surface->pitch + x * 4 + 2 - c] * 0x101);
479                dst[y * stride + x * 3 + c] = 0;
480            }
481#endif
482
483    /* Let the fun begin */
484    for(y = 0; y < HEIGHT; y++)
485    {
486        uint8_t bg = 0, fg = 7;
487
488        fprintf(stderr, "\rProcessing... %i%%", (y * 100 + 99) / HEIGHT);
489
490        for(x = 0; x < WIDTH; x += 6)
491        {
492            int errvec[3] = { 0, 0, 0 };
493            int dummy, i;
494            uint8_t command;
495
496            depth = (x + DEPTH < WIDTH) ? DEPTH : (WIDTH - x) / 6 - 1;
497            srcl = src + y * stride + x * 3;
498            dstl = dst + y * stride + x * 3;
499
500            /* Recursively compute and apply best command */
501            command = bestmove(srcl, bg, fg, errvec, depth, 0x7fffff,
502                               &dummy, dstl);
503            /* Propagate error */
504            for(c = 0; c < 3; c++)
505            {
506                for(i = 0; i < 6; i++)
507                {
508                    int error = srcl[i * 3 + c] - dstl[i * 3 + c];
509                    srcl[i * 3 + c + 3] =
510                            clamp(srcl[i * 3 + c + 3] + error * FS0 / FSX);
511                    srcl[i * 3 + c + stride - 3] += error * FS1 / FSX;
512                    srcl[i * 3 + c + stride] += error * FS2 / FSX;
513                    srcl[i * 3 + c + stride + 3] += error * FS3 / FSX;
514                }
515
516                for(i = -1; i < 7; i++)
517                    srcl[i * 3 + c + stride] = clamp(srcl[i * 3 + c + stride]);
518            }
519            /* Iterate */
520            domove(command, &bg, &fg);
521            /* Write byte to file */
522            fwrite(&command, 1, 1, f);
523        }
524    }
525
526    fclose(f);
527
528    fprintf(stderr, " done.\n");
529
530    /* Save everything */
531#if defined _WIN32
532    for(y = 0; y < HEIGHT; y++)
533        for(x = 0; x < WIDTH; x++)
534        {
535            uint8_t r = dst[y * stride + x * 3] / 0x100;
536            uint8_t g = dst[y * stride + x * 3 + 1] / 0x100;
537            uint8_t b = dst[y * stride + x * 3 + 2] / 0x100;
538            SetPixel(hdc, x, y, RGB(r, g, b));
539        }
540
541    binfo.bmiHeader.biSize = sizeof(binfo.bmiHeader);
542    binfo.bmiHeader.biBitCount = 0;
543    GetDIBits(hdc, tmp, 0, 0, NULL, &binfo, DIB_RGB_COLORS);
544
545    switch(binfo.bmiHeader.biBitCount)
546    {
547    case 24:
548        bisize = sizeof(BITMAPINFOHEADER);
549        break;
550    case 16:
551    case 32:
552        bisize = sizeof(BITMAPINFOHEADER) + sizeof(DWORD) * 3;
553        break;
554    default:
555        bisize = sizeof(BITMAPINFOHEADER)
556                    + sizeof(RGBQUAD) * (1 << binfo.bmiHeader.biBitCount);
557        break;
558    }
559
560    pbinfo = (PBITMAPINFO)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, bisize);
561    memcpy(pbinfo, &binfo, sizeof(BITMAPINFOHEADER));
562
563    bfheader.bfType = 0x4D42; /* "BM" */
564    bfheader.bfSize = sizeof(BITMAPFILEHEADER)
565                         + bisize + pbinfo->bmiHeader.biSizeImage;
566    bfheader.bfReserved1 = 0;
567    bfheader.bfReserved2 = 0;
568    bfheader.bfOffBits = sizeof(BITMAPFILEHEADER) + bisize;
569
570    pixels = GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT,
571                         binfo.bmiHeader.biSizeImage);
572    GetDIBits(hdc, tmp, 0, pbinfo->bmiHeader.biHeight,
573              pixels, pbinfo, DIB_RGB_COLORS);
574
575    hfile = CreateFile(BMPFILE, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
576                       FILE_ATTRIBUTE_ARCHIVE, NULL);
577    WriteFile(hfile, &bfheader, sizeof(BITMAPFILEHEADER), &ret, NULL);
578    WriteFile(hfile, pbinfo, bisize, &ret, NULL);
579    WriteFile(hfile, pixels, pbinfo->bmiHeader.biSizeImage, &ret, NULL);
580    CloseHandle(hfile);
581
582    GlobalFree(pbinfo);
583    GlobalFree(pixels);
584#else
585    for(y = 0; y < HEIGHT; y++)
586        for(x = 0; x < WIDTH; x++)
587            for(c = 0; c < 3; c++)
588                pixels[y * surface->pitch + x * 4 + 2 - c]
589                    = itoc(dst[y * stride + x * 3 + c]) / 0x100;
590    SDL_SaveBMP(surface, BMPFILE);
591#endif
592
593    return 0;
594}
595
Note: See TracBrowser for help on using the repository browser.