8086 MS-DOS .COM file / BMP, output file size = 2192 bytes
Encoder
The encoder is written in C. It takes two arguments: Input file and output file. The input file is a 64x64 RAW RGB image (meaning it is simply 4096 RGB triplets). Number of colours is limited to 4, so that the palette can be as short as possible. It is very straight-forward in its doings; it merely builds a palette, packs pixels pairs into bytes and glues it together with pre-made headers and the decoder program.
#include <stdio.h>
#include <stdlib.h>
#define MAXPAL 4
#define IMAGESIZE 64 * 64
int main(int argc, char **argv)
{
FILE *fin, *fout;
unsigned char *imgdata = malloc(IMAGESIZE * 3), *outdata = calloc(IMAGESIZE / 2, 1);
unsigned palette[MAXPAL] = {0};
int pal_size = 0;
if (!(fin = fopen(argv[1], "rb")))
{
fprintf(stderr, "Could not open \"%s\".\n", argv[1]);
exit(1);
}
if (!(fout = fopen(argv[2], "wb")))
{
fprintf(stderr, "Could not open \"%s\".\n", argv[2]);
exit(2);
}
fread(imgdata, 1, IMAGESIZE * 3, fin);
for (int i = 0; i < IMAGESIZE; i++)
{
// BMP saves the palette in BGR order
unsigned col = (imgdata[i * 3] << 16) | (imgdata[i * 3 + 1] << 8) | (imgdata[i * 3 + 2]), palindex;
int is_in_pal = 0;
for (int j = 0; j < pal_size; j++)
{
if (palette[j] == col)
{
palindex = j;
is_in_pal = 1;
}
}
if (!is_in_pal)
{
if (pal_size == MAXPAL)
{
fprintf(stderr, "Too many unique colours in input image.\n");
exit(3);
}
palindex = pal_size;
palette[pal_size++] = col;
}
// High nibble is left-most pixel of the pair
outdata[i / 2] |= (palindex << !(i & 1) * 4);
}
char BITMAPFILEHEADER[14] = {
0x42, 0x4D, // "BM" magic marker
0x90, 0x08, 0x00, 0x00, // FileSize
0x00, 0x00, // Reserved1
0x00, 0x00, // Reserved2
0x90, 0x00, 0x00, 0x00 // ImageOffset
};
char BITMAPINFOHEADER[40] = {
0x28, 0x00, 0x00, 0x00, // StructSize
0x40, 0x00, 0x00, 0x00, // ImageWidth
0x40, 0x00, 0x00, 0x00, // ImageHeight
0x01, 0x00, // Planes
0x04, 0x00, // BitsPerPixel
0x00, 0x00, 0x00, 0x00, // CompressionType (0 = none)
0x00, 0x00, 0x00, 0x00, // RawImagDataSize (0 is fine for non-compressed,)
0x00, 0x00, 0x00, 0x90, // HorizontalRes
// db 0, 0, 0
// nop
0xEB, 0x1A, 0x90, 0x90, // VerticalRes
// jmp Decoder
// nop
// nop
0x04, 0x00, 0x00, 0x00, // NumPaletteColours
0x00, 0x00, 0x00, 0x00, // NumImportantColours (0 = all)
};
char DECODER[74] = {
0xB8, 0x13, 0x00, 0xCD, 0x10, 0xBA, 0x00, 0xA0, 0x8E, 0xC2, 0xBA,
0xC8, 0x03, 0x31, 0xC0, 0xEE, 0x42, 0xBE, 0x38, 0x01, 0xB1, 0x04,
0xFD, 0x51, 0xB1, 0x03, 0xAC, 0xD0, 0xE8, 0xD0, 0xE8, 0xEE, 0xE2,
0xF8, 0x83, 0xC6, 0x07, 0x59, 0xE2, 0xEF, 0xFC, 0xB9, 0x00, 0x08,
0xBE, 0x90, 0x01, 0xBF, 0xC0, 0x4E, 0xAC, 0xD4, 0x10, 0x86, 0xC4,
0xAB, 0xF7, 0xC7, 0x3F, 0x00, 0x75, 0x04, 0x81, 0xEF, 0x80, 0x01,
0xE2, 0xEE, 0x31, 0xC0, 0xCD, 0x16, 0xCD, 0x20,
};
fwrite(BITMAPFILEHEADER, 1, 14, fout);
fwrite(BITMAPINFOHEADER, 1, 40, fout);
fwrite(palette, 4, 4, fout);
fwrite(DECODER, 1, 74, fout);
// BMPs are stored upside-down, because why not
for (int i = 64; i--; )
fwrite(outdata + i * 32, 1, 32, fout);
fclose(fin);
fclose(fout);
return 0;
}
Output file
The output file is a BMP file which can be renamed .COM and run in a DOS environment. Upon execution, it will change to video mode 13h and display the image.
A BMP file has a first header BITMAPFILEHEADER, which contains among other things the field ImageOffset, which denotes where in the file the image data begins. After this comes BITMAPINFOHEADER with various de-/encoding information, followed by a palette, if one is used. ImageOffset can have a value that points beyond the end of any headers, allowing us to make a gap for the decoder to reside in. Roughly:
BITMAPFILEHEADER
BITMAPINFOHEADER
PALETTE
<gap>
IMAGE DATA
Another problem is to enter the decoder. BITMAPFILEHEADER and BITMAPINFOHEADER can be tinkered with to make sure they are legal machine code (which does not produce a non-recoverable state), but the palette is trickier. We could of course have made the palette artificially longer, and put the machine code there, but I opted to instead use the fields biXPelsPerMeter and biYPelsPerMeter, the former to make the code aligned properly, and the latter to jump into the decoder. These fields will of course then have garbage in them, but any image viewer I have tested with displays the image fine. Printing it might produce peculiar results, though.
It is, as far as I know, standard-compliant.
One could make a shorter file if the JMP
instruction was put in one of the reserved fields in BITMAPFILEHEADER. This would allow us to store the image height as -64 instead of 64, which in the magical wonderland of BMP files means that the image data is stored the right way up, which in turn would allow for a simplified decoder.
Decoder
No particular tricks in the decoder. The palette is populated by the encoder, and shown here with dummy-values. It could be slightly shorter if it did not return to DOS upon a keypress, but it was not fun testing without that. If you feel you must, you can replace the last three instructions with jmp $
to save a few bytes. (Don't forget to update the file headers if you do!)
BMP stores palettes as BGR (not RGB) triplets, padded with zeroes. This makes setting up the VGA palette more annoying than usual. The fact that BMPs are stored upside-down only adds to the flavour (and size).
Listed here in NASM style:
Palette:
db 0, 0, 0, 0
db 0, 0, 0, 0
db 0, 0, 0, 0
db 0, 0, 0, 0
Decoder:
; Set screen mode
mov ax, 0x13
int 0x10
mov dx, 0xa000
mov es, dx
; Prepare to set palette
mov dx, 0x3c8
xor ax, ax
out dx, al
inc dx
mov si, Palette + 2
mov cl, 4
std
pal_loop:
push cx
mov cl, 3
pal_inner:
lodsb
shr al, 1
shr al, 1
out dx, al
loop pal_inner
add si, 7
pop cx
loop pal_loop
cld
; Copy image data to video memory
mov cx, 64 * 64 / 2
mov si, ImageData
mov di, 20160
img_loop:
lodsb
aam 16
xchg al, ah
stosw
test di, 63
jnz skip
sub di, 384
skip:
loop img_loop
; Eat a keypress
xor ax, ax
int 0x16
; Return to DOS
int 0x20
ImageData:
I've added the winning condition tags (code-challenge in combination with metagolf - shortest output). As for the input 64x64 image, do you have some example images? Also, does the image itself has to be the same when viewed? Or can the output image and input image differ? To be more concrete: let's say we add some kind of code for the
.exe
part of the challenge, and when viewing it as a.png
there are modified pixels based on this.exe
-code. Is this allowed as long as it's still a.png
we can view? Does the output image also have to have at least 4 colors? – Kevin Cruijssen – 2019-06-05T07:14:46.4902How do you define "common image viewer"? For example, does an internet browser with HTML "code" count? – Jo King – 2019-06-05T07:32:00.483
@KevinCruijssen When interpreted as image file, the output file shall represent the same image as the input file: Same width and height in pixels and each pixel shall have the same color. If the file formats do not support exactly the same color palette, the colors of each pixel shall be as close as possible. The same is true for the file interpreted as executable file. If the output file represents a "full screen" program, it may display the image anywhere on the screen (centered, top-left edge, ...) or stretch it to full screen size. – Martin Rosenau – 2019-06-05T07:53:05.413
1@JoKing "Recognized by common image viewers" means that the file format can either be read by most computers with pre-installed software (such as HTML) or that a lot of user download a free-of-cost tool to view the file (such as PDF). I would say that HTML+JavaScript can be seen as code, however, the "image viewer" should not execute the code! So it would be allowed to say a web browser is an "image viewer", but in this case HTML is not "code". Or you can say that HTML+JS is "code", but in this case the web browser is not an "image viewer". – Martin Rosenau – 2019-06-05T08:09:45.413
@JoKing I would also say that something like "colored ASCII art" is allowed, so one character represents one "pixel". – Martin Rosenau – 2019-06-05T08:11:20.727
Do you mean 4 colors (say black, white, two kinds of gray), or 4 channels (rgba)? – jimmy23013 – 2019-06-06T07:03:45.300
@jimmy23013 At least 4 colors means black, white and two kinds of gray. However, I would call "two kinds of gray" "grayscale" and not "colors", so black, white, red and green would be a better example. – Martin Rosenau – 2019-06-06T07:07:04.647
@MartinRosenau So, is grayscale disallowed? That's a bit arbitrary. But if you want to disallowed it anyway please edit that into the question. – jimmy23013 – 2019-06-06T07:35:04.997
@jimmy23013 I would allow grayscale. – Martin Rosenau – 2019-06-06T13:22:45.600
2It is sad to see such an interesting question closed. As far as I understand, any concerns should be addressed before reopening a question. The main things in the comments is the "common image viewer" term, which is sufficiently hazy to be ambiguous, and the image being displayed in a state (as per @KevinCruijssen's concern) unaltered by the presence of the executable code is worthy of clarification. Would an edit addressing those concerns be enough? (I confess to not understanding the "is four colours four colours" ambiguity.) – gastropner – 2019-06-11T19:16:21.083