PHP + gd2 - 86 bytes
<?imagestring($i=imagecreatetruecolor(97,16),4,2,0,'Hello world!',65535);imagepng($i);
imagecreatetruecolor
is used instead of the shorter imagecreate
, because colors can used directly without having to allocate them with imagecolorallocate
. 65535
corresponds to hex color #00FFFF
, a.k.a. cyan. I could have used 255
for blue, but it's fairly hard to see on a black canvas.
If the requirement that the background must be white or transparent is to be strictly enforced, I think the best that can be done is 98 bytes:
<?imagestring($i=imagecreatetruecolor(97,16),4,2,0,'Hello world!',imagefilter($i,0));imagepng($i);
The 0
sent to imagefilter
is the value of the constant IMG_FILTER_NEGATE
, which of course negates the image. The result, 1
, is then used as the paint color (#000001
):
Another option at 108 bytes:
<?imagestring($i=imagecreatetruecolor(97,16),4,2,imagecolortransparent($i,0),'Hello world!',1);imagepng($i);
Setting black to be transparent, and drawing with #000001
instead.
PHP + No Library - 790+ bytes
<?
echo pack('CA3N2',137,'PNG',218765834,13);
echo $ihdr = pack('A4N2C5','IHDR',45,7,1,0,0,0,0);
echo hash('crc32b',$ihdr,true);
$data =
'--------0 0 0 0 0 0 0---'.
'--------0 0 0 0 0 0 0---'.
'--------0 0 00 0 0 00 0 0 00 0 0 0 000 0---'.
'--------0000 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0---'.
'--------0 0 0000 0 0 0 0 0 0 0 0 0 0 0 0 0 0---'.
'--------0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ---'.
'--------0 0 000 0 0 00 0 0 00 0 0 000 0---';
$bytes = join(array_map(chr,array_map(bindec,str_split(strtr($data,' -',10),8))));
$cmp = gzcompress($bytes);
echo pack('N',strlen($cmp));
echo $idat = 'IDAT'.$cmp;
echo hash('crc32b',$idat,true);
echo pack('NA4N',0,'IEND',2923585666);
Ahh, that's better. No bloat; exactly as much as required, and not a chunk more.
The result is this 109 byte png:
Or, URI encoded (which seems to be trending...) at 168 bytes:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAHAQAAAAC0VvlnAAAANElEQVR4nGPIv/7//+8LDFAq51r15mgBBu7Q3Wu1LjBkhO5aW3WBIfsqkLrBkBNWW1wtAACw2RlgLInRogAAAABJRU5ErkJggg
Supposing we wanted to cut that down a bit more, let's say we replace the data string with this:
$data =
'--------0 0 0 0 0 0 0'.
'--------0 0 00 0 0 000 0 0 000 00 0 000 0'.
'--------0000 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0'.
'--------0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 '.
'--------0 0 000 0 0 000 00 00 000 0 0 000 0';
(and update the header to the new dimensions, 40x5), the output would be this 96 byte png:
Which URI encodes to 150 bytes:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAFAQAAAAAft5MoAAAAJ0lEQVR4nGPIv/7//y6GnCvlLosYuEJLQ1cxZF4tDV3NkBNS5LoIANqBDTt5Av0NAAAAAElFTkSuQmCC
I think that's about as small as you're going to be able to get, and still be considered "human readable".
Further Analysis
You may have noticed that we've been toting along an extra byte at the beginning of each scanline (denoted by --------
). This isn't solely for decoration. Each byte specifies the filtering used by each scanline. According to the PNG specification, "Filtering transforms the PNG image with the goal of improving compression." So let's try that.
The are five different filtering operations which can be applied independently to each scanline. The PHP implementation that I used for each can be seen here: http://codepad.org/xCQpBPC3 where $bytes
represents the raw bytes for the current scanline, and $prior
represents the raw, unfiltered bytes for the scanline above the current.
Let's start with the first 45x7 image. Seven scanlines each with 5 different filterings makes 78125 different possibilities to grind through. The initial encoding of the data block was 52 bytes in length, and after a bit of grinding zlib found a one byte improvement using filtering pattern [1, 1, 1, 1, 0, 0, 0] (that is, the first four scanlines with Sub filtering, and the last three unfiltered). The result is this 108 byte png:
Which of course looks identical to the last. But I'm not convinced that zlib is producing the best possible encoding, and I think i have good reason to be skeptical. I decided to try AdvanceComp (which uses the same DEFLATE engine used for 7-zip), and Zopfli, an implementation which claims to "find a low bit cost path through the graph of all possible deflate representations." Sure enough, Zopfli mananged to compress the same data data pattern [1, 1, 1, 1, 0, 0, 0] down to 50 bytes, producing this 107 byte png:
Once again, visually identical. (As a point of interest, it should probably be mentioned at this point that AdvanceComp with the setting -z3
(compress-extra (7z)) didn't manage to find anything shorter than 60 bytes - the data was left uncompressed. It seems it refuses to compress anything this short). The above URI encodes to 165 bytes:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAHAQAAAAC0VvlnAAAAMklEQVR4AWPMz9Bg+HMVRuVkLbVYsZWR2yvtU+0yhozQXWurLjBkXwVSNxhywmqLqwUA+IMVKa7QjrYAAAAASUVORK5CYII
Fully 11 bytes shorter than squeamish ossifrage's attempt at more or less an identical image.
Onwards to the 40x5 image. Five lines with 5 filterings each means we only have 3125 possibilities this time. The original encoding was 39 bytes in length, and with a bit of grinding, zlib found quite a few 38s. The one I've chosen is [1, 0, 0, 2, 0], which contains the largest number of unfiltered lines, and Sub and Up filters on lines 0 and 4, which are the simplest. Zopfli wasn't able to improve this result any further. The result is this 95 byte png:
Which URI encodes to 149 bytes:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAFAQAAAAAft5MoAAAAJklEQVR4nGPMz9Bg2M2Qc6XcZREDV2hp6Cqm+AYGBkaGnJAi10UAju4JJ/1zkEIAAAAASUVORK5CYII
You might be tempted to think that the last 18 or so bytes of this aren't necessary. After all, this 121 byte URI will still display correctly, at least in Chromium:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAFAQAAAAAft5MoAAAAIUlEQVR4nGPMz9Bg2M2Qc6XcZREDV2hp6Cqm+AYGBkaGnJAi10U
But if you save it to a file, it will break in very many image viewers. In fact, any compliant decoder is required to report an error. So what have we chopped off?
From end towards beginning:
- 4 bytes - CRC32 for
IEND
chunk (always 0xAE426082
)
- 4 bytes -
IEND
chunk marker (always IEND
)
- 4 bytes -
IEND
chunk length (always 0x00000000
)
- 4 bytes - CRC32 for
IDAT
chunk
- 4 bytes - Adler32 for zlib data
- 1 byte - Stop marker for zlib data
Additionally adjusting the IDAT
length marker down by 5 (to compensate for the bytes we deleted) seems to "fix" the image in Windows Previewer.
Does it have to output
Hello World
orHello World!
? – The Guy with The Hat – 2014-02-02T12:55:44.5033@ryan Hello World! is the right one. – figgycity50 – 2014-02-02T14:52:28.823
3A lot of answers could save one character by replacing the double 'l' in 'hello' with the medieval Welsh ligatures 'Ỻ' or 'ỻ': once the chars have been transformed to pixels it shouldn't matter what their origin was as long as people still perceive 'heỻo world!' as 'hello world!'. – timxyz – 2014-02-03T11:27:47.367
@timxyz Clearly not if you want the result to appear correctly on my browser! – bye – 2014-02-03T21:56:29.957
9@poldie Your browser changes PNG based on how the pixels got in there? – Christopher Creutzig – 2014-02-04T02:22:04.853
@timxyz: Arguably the extra stroke in
ỻ
violates the "nothing else" requirement. – Mechanical snail – 2014-02-04T03:55:43.8232@timxyz For me, ỻ is rendered as a box containing 1EFB. I tried to use it in my program anyway, and the result was a square. So, this is probably not very portable. – Victor Stafusa – 2014-02-04T05:24:05.537
@timxyz No, it changes the characters you want changed into a PNG. – bye – 2014-02-04T08:43:36.557