source: www/img2oric/img2oric.c @ 2393

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