Optimal character choices for ASCII art in PHP

Problem definition

ASCII art in PHP relies on the selection of a set of characters for the graphic representation. We didn’t find an optimal selection of characters mentioned anywhere. The example character sets seem to be chosen arbitrarily.

Approach

We consider as an acceptable approximation that the number of pixels used to display a font can be used to chose how dark/bright a character is in the ASCII art representation. With this, we use the 5 standard fonts within PHP to count the pixels for each ASCII character which decimal code is within (32-126). The result is the following for example for PHP font 5:

With no surprise, the minimum number of pixels is 0 for the space.

We divide the pixels space by the desired number of characters to find the best spread characters. We do this until we reach a situation where there is no character repetition. This occurs for for all fonts with sets of 9 characters.

PHP code

The following PHP code allowed to reach the desired outcome:

<?php
if(isset($argv[1]) && strlen($argv[1] && $argv[1]>1)) {
        $nbChars = $argv[1];
}else{
        echo "Please Specify a number of characters > 1\n";
        exit(1);
}
//$font must be 1-5
function nbPixelsForCharFont($char,$font){
	//inspired by https://www.php.net/manual/en/function.imagestring.php
	$h=15;
	$w=15;
	// Create a sized image
	$im = imagecreate($h, $w);

	// White background and black text
	$bg = imagecolorallocate($im, 255, 255, 255);
	$textcolor = imagecolorallocate($im, 0, 0, 0);

	// Write the string at the top left
	imagestring($im, $font, 0, 0, $char, $textcolor);
	$colorsSum=0;
	for($y=0;$y<$h;$y++){
		for($x=0;$x<$w;$x++){
			$color=imagecolorat ( $im , $x , $y );
			$colorsSum+=$color;
		}
	}
	imagedestroy($im);
	return($colorsSum);
}

function buildCharIntForNbPxArray($font){
        $charIntForNbPx=array();
        for($i=32;$i<127;$i++){
                $c=chr($i);
                $nbPx=nbPixelsForCharFont($c,$font);
		$charIntForNbPx[$nbPx]=$i;
        }
        ksort($charIntForNbPx);
        return($charIntForNbPx);
}

function findClosestKey($arr,$desiredKey,$reach=0){
	$r=(int)round($desiredKey);
	$high=$r+$reach;
	$last=array_key_last($arr);
	if($last>=$high&&array_key_exists($high,$arr))
		return($high);
	$low=$r-$reach;
	$first=array_key_first($arr);
        if($first<=$low&&array_key_exists($low,$arr))
                return($low);
	if($last>=$high||$first<=$low)
		return(findClosestKey($arr,$r,$reach+1));
	return(null);
}

function findBestChars($font,$nbChars){
	$bestChars=array();
	$charIntForNbPx=buildCharIntForNbPxArray($font);
	$minPx=array_key_first($charIntForNbPx);
	$maxPx=array_key_last($charIntForNbPx);
	$idealStep=($maxPx-$minPx)/($nbChars-1);
	for($i=$minPx;$i<=$maxPx;$i+=$idealStep){
		$key=findClosestKey($charIntForNbPx,$i);
		if(is_null($key))
			echo("Error, char shouldn't be null!\n");
		$bestChars[]=$charIntForNbPx[$key];
	}
	return($bestChars);
}

echo("Best chars for ascii art per PHP font for ".$nbChars." characters:\n");
for($font=1;$font<6;$font++){
	$prev=null;
	echo("font ".$font." best chars:\n");
	foreach(findBestChars($font,$nbChars) as $char){
		echo($char.":".chr($char)."\n");
		if($prev==$char)
			echo("ALERT!!! same as previous!!!\n");
		$prev=$char;
	}
	echo("\n");
}
?>

Outcome

The outcome is the following table of optimized character sets:

Font 1Font 2Font 3Font 4Font 5
32:  32:  32:  32:  32:  
94:^96:`46:.46:.96:`
95:_34:”96:`58::94:^
120:x59:;33:!114:r124:|
125:}118:v63:?108:l49:1
71:G122:z120:x70:F97:a
81:Q121:y121:y109:m85:U
64:@72:H87:W82:R66:B
35:#87:W78:N81:Q78:N

Comparison

To compare the sets, we used the following PHP code inspired by this page:

<?php
if(isset($argv[1]) && strlen($argv[1])) {
	$file = $argv[1];
}else{
	echo "Please Specify a file\n";
	exit(1);
}

$img = imagecreatefromstring(file_get_contents($file));
list($width, $height) = getimagesize($file);

$scale = 10;

//characters list from http://paulbourke.net/dataformats/asciiart/
$chars=[
'@',
'%',
'#',
'*',
'+',
'=',
'-',
':',
'.',
' '
];

$c_count = count($chars);

//algorithm from https://gist.github.com/donatj/1353237
for($y = 0; $y <= $height - $scale - 1; $y += $scale) {
	for($x = 0; $x <= $width - ($scale / 2) - 1; $x += ($scale / 2)) {
		$rgb = imagecolorat($img, $x, $y);
		$r = (($rgb >> 16) & 0xFF);
		$g = (($rgb >> 8) & 0xFF);
		$b = ($rgb & 0xFF);
		$sat = ($r + $g + $b) / (255 * 3);
		echo $chars[ (int)( $sat * ($c_count - 1) ) ];
	}
	echo PHP_EOL;
}
?>

Here is the original image for the comparison:

The resulting ASCII art based on the short character set on this page is:

$chars=['@','%','#','*','+','=','-',':','.',' '];

++==+=+++++++++++++++++=--::::==++****+#**+*+********+***++**+******#
=++++++++++++++++++++++:::::--.:--+++++*+++++++++++++++++++++++++++**
==+====++==++========+++=+=::::-=+++++#%+++++++++++++++++++++++++++++
=====+====--......:-==---:-:...:=++++*#%%+++++++++++++++++++++++++++*
. :=====-:  ........::::::-:..::-+++#%#%%%#++++++++++++++++++++++++++
.....====-.....::-:.:. .....:-:=+=+#%%%%%%%#+++++++++++++++++++++++++
......-==:.. ..:-=:.........:. ..-#%%@@@@%%%%*+++++++++++++++++++++++
:.....:===......::.    ....... .:#%@@@%@@@@@%%#=+++++++++++++++++++++
:......===...           ..   ..:#@@@%%%@@@@@@@%%=++++++++++++++++++++
.......:..  .             ..-%@@%%##*#%@@@@@@@@@@@%%*++++++++++++++++
:::--:..       .......... .=%%%**#####%@@@@@@@@@@@%#+++++++++++++++++
====-:....   .....--::::...:*##*+#*####@@@@@@@@@@@@#+++++++++++++++++
======-:..........:::......:*#%**###**#@@@@@@@@@@@@%=-==+++++++++++++
=======-:::................=%%#+***%*##@@@@@@@@@@@%%+=++++++=.:.....:
+=========-:.::.:.:::+%#%@@@%@%++*##*##@@@@%@@@@@@%%+==+==++=-:::::::
===-=-======---::+###++*@@@@@@%++++**##@@@@@%@@@@@@%=:.:---====---:::
=----========+#*%+=+++++@@@@%%%***#**##%@@@@@@@@@@%%+:::---=++====---
=-----=--=###*++++++*+*#@@@@@@%******##@@@@@@@@%@@@%*================
-:...:##%*+++*****=#+++#@@@@@@%#*#%%%##%@@@%%@%%@%@%#-=======+=======
-..*#++*+***+#=+#******#@@@@@@@%***#*##%@@@%@@%@@%@%%-==-==========-=
:..-***#*++*+**********#@@@@@@%%*+***##%@@@@@@%@@@@%%+---=-===--=====
:..#*+****+********###*#@@@@@@@%*+*#*##%@@@%@@@%%@@%%*-------------==
#@@@@@@@@@@#*#***#%@%**#@@@@@@@#*@%**###@@@@@@@@@@@@%*-------------==
#@@@@@@@@@@#@%**+*@@%*##@@@@@@@@@@@@*###@@@@@@@@@@@@%#::::::::-------
##*+**#@@@@#@%+=+*@@%*#%@@@@@@@@@@@@*###@@@@@@@@@@@@@%-:--::---------
#%#**+*%%#@%@%+++*%%%*#%@@@@@@@@@@@@####@@@@@@%@@@@@%%--:-----------=
#%#%++*%%%@@%%***#%#%##%@@@@@@@%@@@@*###@@@@@@@@@@@@@%=+-----------==
#%#%+**%%@@%*##########@@@@@@@@@@@@@###*@@@@@@@@@@@@@@@#%#---+=======
++=+***#*@@#+**##**####@@@@@@@@@%%%%#***@@@@@@@@@@@@@@@@@@%#**%%#**+*
+*+*+*********####**%%%@@@@@##**+*#%*#*#@@@@@@@@%@@@@@@@%%%%%%@@@@%##
********+++++*+++++++=+=====++++*#######@@@@@@@#%%%%@@@@%@%%%#%%@@@@#
*********+****+*+++====--===+=--===+++++++=++++**#####**##%@@@@@@@@@@
*+**+*+****+++*+*++++++++*+****+*******+++******+*********+****+*****
***********+++++++*+*====+=+++++=+++*+++++*+++****+*+****##**********

With php font 1 character set, we get this:

$chars=['#','@','Q','G','}','x','_','^',' '];
G}}}}}}}G}G}GGGGGGGGGGGxx_____x}}GGGGGGQGGGGGGGGGGGGGGGGGGGGGGGGGGGQQ
x}}}}}}}}}}}}}}}}}}}}}}___^^_x^__x}}}}GG}}}}}}GGGGGG}GGGGGGGGGGGGGGGG
}x}}}}}}}}}}}}}}}}}}}}}}}}}____xx}}}}}Q@G}}}}}G}}}}GGGGGGGGGGGGGGGGGG
x}}}}}}}}}x_^^^^^^_x}}xx__x_^^^_}}}}}G@@@G}}}}}}}}}}GGGGGGGGGGGGGGGGG
^ _}}}}}x^  ^^^^^^^^____^___^^__x}}}Q@@@@@Q}}}}}}}}}}}}}}GGGGGGGGGGGG
^^^^^}}}}x^^^^^__x_^_^ ^^^^^___x}}}@@@@@#@@Q}}}}}}}}}}}}G}GGGG}GGGGGG
^^^^^^x}}_^^ ^^_xx_^^^^^^^^^_^ ^^_@@@####@@@@G}}}}}}}}}}}}}}}}}}}GGGG
^^^^^^_}}}^^^^^^_^^    ^^^^^^^ ^_@@###@#####@@Q}}}}}}}}}}}}}}}}}GG}GG
^^^^^^^x}}^^^           ^^   ^^_@###@@@#######@@}}}}}}}}}}}G}}}}}}GGG
^^^^^^^_^^  ^             ^^x@###@QQQ@@############@Q}}}}}}}}}}G}}GG}
____x_^^       ^^^^^^^^^^ ^}@@@QG@QQQQ@############Q}}}}}}}}}}}}}}}}}
x}}}x_^^^^   ^^^^^xx____^^^^QQQGG@QQQQ@############@}}}}}}}}}}}}}}}}}
}}}}}}x_^^^^^^^^^^___^^^^^^_QQ@GGQQQQQQ############@xx}}}}}}}}}}}}}}}
}}}}}}xx___^^^^^^^^^^^^^^^^}@#@GGGQ@QQQ###########@@}}}}}}}}}^_^^^^^^
}}}xx}x}}xx_^^_^^^^__G@Q@###@#@GGG@QGQQ############@}}}}}}}}}x_______
}}}x}xx}}x}}xx___}@Q@G}G######@GG}GQGQQ############@}_^_x_x}}}xxx____
}xxxxxx}}x}x}GQG@G}}GG}G######@GGGQGGQQ@##########@@G____xx}}}}}}}xxx
}xxxxxxxx}QQ@QG}}}GGQ}GQ######@QQGGGQQ@############@Q}}}}}}}}}}}xxxx}
_^^^^_Q@@GGGGGQGGG}QG}GQ#######QQ@@@@QQ@###@@#@@#@#@@x}}}}x}x}}}xxxxx
x^^G@GGGGGGGGQ}}QGGQGGQQ#######@GGQQGQQ@#########@#@@x}xx}xx}xxxx}xxx
_^^xGGGQG}GGGGGGGGGGGGQQ#######@GGGGQQQ@###########@@}xxxxxx}}xxxxxxx
_^^QGGGGGQGQGQQGQGQQQQQQ#######@GGQQQQQ@########@##@@Gxxxxxxxxxxxxxxx
Q##########QQQGQQQ##@QGQ#######@G#@QQQQ@############@Q___xx_xxx_xxxxx
Q##########Q#@QGGG##@QQQ############G@QQ############@Q_________xxxxxx
QQQGGGQ####@#@G}GQ##@QQ@############QQQQ#############@_______xxxxxxxx
Q@QGQ}G@@@#@#@GGGQ##@QQ@############QQQQ######@#####@@x_______xxxxxxx
Q@Q@}GG@@@##@@QGQQ@Q@QQ@############GQQQ#############@}}x___xxxxxxxxx
Q@Q@}GQ@@##@GQQQQQQQQQQ#############QQQQ###############@@@xxx}}}xxxx}
G}}}GQQQG##QGQQQQGQQQQQ#########@@##QGQQ###################QQG@@QQGGG
GGGGGGGGGGQGGQQQQQQQ@@######QQGGGQQ@GQQQ################@@@@@@####@Q@
GGGGGGGGGGG}GGGGGGGG}}}}xx}}}}GGQQQQQQQQ#######@@@#@####@##@@Q@@####Q
GQQQQQGGGGGGGQGGGG}}}}xxxx}}}}xxx}}}}}GG}G}GGGGGGQQQQQGGQ@@##########
GGGGGGGGGGGGGGGGGGGGGGG}GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGGGGGGGGG}GGGGGGG}}}}}}}}GG}}GGGGGGGGGGGGGGGGGGGGQGQQQQGGGQGGGGGG

With php font 2 character set, we get this:

$chars=['W','H','y','z','v',';','"','`',' '];
zvvvvvvvzvzvzzzzzzzzzzz;;""""";vvzzzzzzyzzzzzzzzzzzzzzzzzzzzzzzzzzzyy
;vvvvvvvvvvvvvvvvvvvvvv"""``";`"";vvvvzzvvvvvvzzzzzzvzzzzzzzzzzzzzzzz
v;vvvvvvvvvvvvvvvvvvvvvvvvv"""";;vvvvvyHzvvvvvzvvvvzzzzzzzzzzzzzzzzzz
;vvvvvvvvv;"``````";vv;;"";"```"vvvvvzHHHzvvvvvvvvvvzzzzzzzzzzzzzzzzz
` "vvvvv;`  ````````""""`"""``"";vvvyHHHHHyvvvvvvvvvvvvvvzzzzzzzzzzzz
`````vvvv;`````"";"`"` `````""";vvvHHHHHWHHyvvvvvvvvvvvvzvzzzzvzzzzzz
``````;vv"`` ``";;"`````````"` ``"HHHWWWWHHHHzvvvvvvvvvvvvvvvvvvvzzzz
``````"vvv``````"``    ``````` `"HHWWWHWWWWWHHyvvvvvvvvvvvvvvvvvzzvzz
```````;vv```           ``   ``"HWWWHHHWWWWWWWHHvvvvvvvvvvvzvvvvvvzzz
```````"``  `             ``;HWWWHyyyHHWWWWWWWWWWWWHyvvvvvvvvvvzvvzzv
"""";"``       `````````` `vHHHyzHyyyyHWWWWWWWWWWWWyvvvvvvvvvvvvvvvvv
;vvv;"````   `````;;""""````yyyzzHyyyyHWWWWWWWWWWWWHvvvvvvvvvvvvvvvvv
vvvvvv;"``````````"""``````"yyHzzyyyyyyWWWWWWWWWWWWH;;vvvvvvvvvvvvvvv
vvvvvv;;"""````````````````vHWHzzzyHyyyWWWWWWWWWWWHHvvvvvvvvv`"``````
vvv;;v;vv;;"``"````""zHyHWWWHWHzzzHyzyyWWWWWWWWWWWWHvvvvvvvvv;"""""""
vvv;v;;vv;vv;;"""vHyHzvzWWWWWWHzzvzyzyyWWWWWWWWWWWWHv"`";";vvv;;;""""
v;;;;;;vv;v;vzyzHzvvzzvzWWWWWWHzzzyzzyyHWWWWWWWWWWHHz"""";;vvvvvvv;;;
v;;;;;;;;vyyHyzvvvzzyvzyWWWWWWHyyzzzyyHWWWWWWWWWWWWHyvvvvvvvvvvv;;;;v
"````"yHHzzzzzyzzzvyzvzyWWWWWWWyyHHHHyyHWWWHHWHHWHWHH;vvvv;v;vvv;;;;;
;``zHzzzzzzzzyvvyzzyzzyyWWWWWWWHzzyyzyyHWWWWWWWWWHWHH;v;;v;;v;;;;v;;;
"``;zzzyzvzzzzzzzzzzzzyyWWWWWWWHzzzzyyyHWWWWWWWWWWWHHv;;;;;;vv;;;;;;;
"``yzzzzzyzyzyyzyzyyyyyyWWWWWWWHzzyyyyyHWWWWWWWWHWWHHz;;;;;;;;;;;;;;;
yWWWWWWWWWWyyyzyyyWWHyzyWWWWWWWHzWHyyyyHWWWWWWWWWWWWHy""";;";;;";;;;;
yWWWWWWWWWWyWHyzzzWWHyyyWWWWWWWWWWWWzHyyWWWWWWWWWWWWHy""""""""";;;;;;
yyyzzzyWWWWHWHzvzyWWHyyHWWWWWWWWWWWWyyyyWWWWWWWWWWWWWH""""""";;;;;;;;
yHyzyvzHHHWHWHzzzyWWHyyHWWWWWWWWWWWWyyyyWWWWWWHWWWWWHH;""""""";;;;;;;
yHyHvzzHHHWWHHyzyyHyHyyHWWWWWWWWWWWWzyyyWWWWWWWWWWWWWHvv;""";;;;;;;;;
yHyHvzyHHWWHzyyyyyyyyyyWWWWWWWWWWWWWyyyyWWWWWWWWWWWWWWWHHH;;;vvv;;;;v
zvvvzyyyzWWyzyyyyzyyyyyWWWWWWWWWHHWWyzyyWWWWWWWWWWWWWWWWWWWyyzHHyyzzz
zzzzzzzzzzyzzyyyyyyyHHWWWWWWyyzzzyyHzyyyWWWWWWWWWWWWWWWWHHHHHHWWWWHyH
zzzzzzzzzzzvzzzzzzzzvvvv;;vvvvzzyyyyyyyyWWWWWWWHHHWHWWWWHWWHHyHHWWWWy
zyyyyyzzzzzzzyzzzzvvvv;;;;vvvv;;;vvvvvzzvzvzzzzzzyyyyyzzyHHWWWWWWWWWW
zzzzzzzzzzzzzzzzzzzzzzzvzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
zzzzzzzzzzzzzvzzzzzzzvvvvvvvvzzvvzzzzzzzzzzzzzzzzzzzzyzyyyyzzzyzzzzzz

With php font 3 character set, we get this:

$chars=['N','W','y','x','?','!','`','.',' '];
x???????x?x?xxxxxxxxxxx!!`````!??xxxxxxyxxxxxxxxxxxxxxxxxxxxxxxxxxxyy
!??????????????????????```..`!.``!????xx??????xxxxxx?xxxxxxxxxxxxxxxx
?!?????????????????????????````!!?????yWx?????x????xxxxxxxxxxxxxxxxxx
!?????????!`......`!??!!``!`...`?????xWWWx??????????xxxxxxxxxxxxxxxxx
. `?????!.  ........````.```..``!???yWWWWWy??????????????xxxxxxxxxxxx
.....????!.....``!`.`. .....```!???WWWWWNWWy????????????x?xxxx?xxxxxx
......!??`.. ..`!!`.........`. ..`WWWNNNNWWWWx???????????????????xxxx
......`???......`..    ....... .`WWNNNWNNNNNWWy?????????????????xx?xx
.......!??...           ..   ..`WNNNWWWNNNNNNNWW???????????x??????xxx
.......`..  .             ..!WNNNWyyyWWNNNNNNNNNNNNWy??????????x??xx?
````!`..       .......... .?WWWyxWyyyyWNNNNNNNNNNNNy?????????????????
!???!`....   .....!!````....yyyxxWyyyyWNNNNNNNNNNNNW?????????????????
??????!`..........```......`yyWxxyyyyyyNNNNNNNNNNNNW!!???????????????
??????!!```................?WNWxxxyWyyyNNNNNNNNNNNWW?????????.`......
???!!?!??!!`..`....``xWyWNNNWNWxxxWyxyyNNNNNNNNNNNNW?????????!```````
???!?!!??!??!!```?WyWx?xNNNNNNWxx?xyxyyNNNNNNNNNNNNW?`.`!`!???!!!````
?!!!!!!??!?!?xyxWx??xx?xNNNNNNWxxxyxxyyWNNNNNNNNNNWWx````!!???????!!!
?!!!!!!!!?yyWyx???xxy?xyNNNNNNWyyxxxyyWNNNNNNNNNNNNWy???????????!!!!?
`....`yWWxxxxxyxxx?yx?xyNNNNNNNyyWWWWyyWNNNWWNWWNWNWW!????!?!???!!!!!
!..xWxxxxxxxxy??yxxyxxyyNNNNNNNWxxyyxyyWNNNNNNNNNWNWW!?!!?!!?!!!!?!!!
`..!xxxyx?xxxxxxxxxxxxyyNNNNNNNWxxxxyyyWNNNNNNNNNNNWW?!!!!!!??!!!!!!!
`..yxxxxxyxyxyyxyxyyyyyyNNNNNNNWxxyyyyyWNNNNNNNNWNNWWx!!!!!!!!!!!!!!!
yNNNNNNNNNNyyyxyyyNNWyxyNNNNNNNWxNWyyyyWNNNNNNNNNNNNWy```!!`!!!`!!!!!
yNNNNNNNNNNyNWyxxxNNWyyyNNNNNNNNNNNNxWyyNNNNNNNNNNNNWy`````````!!!!!!
yyyxxxyNNNNWNWx?xyNNWyyWNNNNNNNNNNNNyyyyNNNNNNNNNNNNNW```````!!!!!!!!
yWyxy?xWWWNWNWxxxyNNWyyWNNNNNNNNNNNNyyyyNNNNNNWNNNNNWW!```````!!!!!!!
yWyW?xxWWWNNWWyxyyWyWyyWNNNNNNNNNNNNxyyyNNNNNNNNNNNNNW??!```!!!!!!!!!
yWyW?xyWWNNWxyyyyyyyyyyNNNNNNNNNNNNNyyyyNNNNNNNNNNNNNNNWWW!!!???!!!!?
x???xyyyxNNyxyyyyxyyyyyNNNNNNNNNWWNNyxyyNNNNNNNNNNNNNNNNNNNyyxWWyyxxx
xxxxxxxxxxyxxyyyyyyyWWNNNNNNyyxxxyyWxyyyNNNNNNNNNNNNNNNNWWWWWWNNNNWyW
xxxxxxxxxxx?xxxxxxxx????!!????xxyyyyyyyyNNNNNNNWWWNWNNNNWNNWWyWWNNNNy
xyyyyyxxxxxxxyxxxx????!!!!????!!!?????xx?x?xxxxxxyyyyyxxyWWNNNNNNNNNN
xxxxxxxxxxxxxxxxxxxxxxx?xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxx?xxxxxxx????????xx??xxxxxxxxxxxxxxxxxxxxyxyyyyxxxyxxxxxx

With php font 4 character set, we get this:

$chars=['Q','R','m','F','l','r',':','.',' '];
FlllllllFlFlFFFFFFFFFFFrr:::::rllFFFFFFmFFFFFFFFFFFFFFFFFFFFFFFFFFFmm
rllllllllllllllllllllll:::..:r.::rllllFFllllllFFFFFFlFFFFFFFFFFFFFFFF
lrlllllllllllllllllllllllll::::rrlllllmRFlllllFllllFFFFFFFFFFFFFFFFFF
rlllllllllr:......:rllrr::r:...:lllllFRRRFllllllllllFFFFFFFFFFFFFFFFF
. :lllllr.  ........::::.:::..::rlllmRRRRRmllllllllllllllFFFFFFFFFFFF
.....llllr.....::r:.:. .....:::rlllRRRRRQRRmllllllllllllFlFFFFlFFFFFF
......rll:.. ..:rr:.........:. ..:RRRQQQQRRRRFlllllllllllllllllllFFFF
......:lll......:..    ....... .:RRQQQRQQQQQRRmlllllllllllllllllFFlFF
.......rll...           ..   ..:RQQQRRRQQQQQQQRRlllllllllllFllllllFFF
.......:..  .             ..rRQQQRmmmRRQQQQQQQQQQQQRmllllllllllFllFFl
::::r:..       .......... .lRRRmFRmmmmRQQQQQQQQQQQQmlllllllllllllllll
rlllr:....   .....rr::::....mmmFFRmmmmRQQQQQQQQQQQQRlllllllllllllllll
llllllr:..........:::......:mmRFFmmmmmmQQQQQQQQQQQQRrrlllllllllllllll
llllllrr:::................lRQRFFFmRmmmQQQQQQQQQQQRRlllllllll.:......
lllrrlrllrr:..:....::FRmRQQQRQRFFFRmFmmQQQQQQQQQQQQRlllllllllr:::::::
lllrlrrllrllrr:::lRmRFlFQQQQQQRFFlFmFmmQQQQQQQQQQQQRl:.:r:rlllrrr::::
lrrrrrrllrlrlFmFRFllFFlFQQQQQQRFFFmFFmmRQQQQQQQQQQRRF::::rrlllllllrrr
lrrrrrrrrlmmRmFlllFFmlFmQQQQQQRmmFFFmmRQQQQQQQQQQQQRmlllllllllllrrrrl
:....:mRRFFFFFmFFFlmFlFmQQQQQQQmmRRRRmmRQQQRRQRRQRQRRrllllrlrlllrrrrr
r..FRFFFFFFFFmllmFFmFFmmQQQQQQQRFFmmFmmRQQQQQQQQQRQRRrlrrlrrlrrrrlrrr
:..rFFFmFlFFFFFFFFFFFFmmQQQQQQQRFFFFmmmRQQQQQQQQQQQRRlrrrrrrllrrrrrrr
:..mFFFFFmFmFmmFmFmmmmmmQQQQQQQRFFmmmmmRQQQQQQQQRQQRRFrrrrrrrrrrrrrrr
mQQQQQQQQQQmmmFmmmQQRmFmQQQQQQQRFQRmmmmRQQQQQQQQQQQQRm:::rr:rrr:rrrrr
mQQQQQQQQQQmQRmFFFQQRmmmQQQQQQQQQQQQFRmmQQQQQQQQQQQQRm:::::::::rrrrrr
mmmFFFmQQQQRQRFlFmQQRmmRQQQQQQQQQQQQmmmmQQQQQQQQQQQQQR:::::::rrrrrrrr
mRmFmlFRRRQRQRFFFmQQRmmRQQQQQQQQQQQQmmmmQQQQQQRQQQQQRRr:::::::rrrrrrr
mRmRlFFRRRQQRRmFmmRmRmmRQQQQQQQQQQQQFmmmQQQQQQQQQQQQQRllr:::rrrrrrrrr
mRmRlFmRRQQRFmmmmmmmmmmQQQQQQQQQQQQQmmmmQQQQQQQQQQQQQQQRRRrrrlllrrrrl
FlllFmmmFQQmFmmmmFmmmmmQQQQQQQQQRRQQmFmmQQQQQQQQQQQQQQQQQQQmmFRRmmFFF
FFFFFFFFFFmFFmmmmmmmRRQQQQQQmmFFFmmRFmmmQQQQQQQQQQQQQQQQRRRRRRQQQQRmR
FFFFFFFFFFFlFFFFFFFFllllrrllllFFmmmmmmmmQQQQQQQRRRQRQQQQRQQRRmRRQQQQm
FmmmmmFFFFFFFmFFFFllllrrrrllllrrrlllllFFlFlFFFFFFmmmmmFFmRRQQQQQQQQQQ
FFFFFFFFFFFFFFFFFFFFFFFlFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFlFFFFFFFllllllllFFllFFFFFFFFFFFFFFFFFFFFmFmmmmFFFmFFFFFF

With php font 5 character set, we get this:

$chars=['N','B','U','a','1','|','^','`',' '];
a1111111a1a1aaaaaaaaaaa||^^^^^|11aaaaaaUaaaaaaaaaaaaaaaaaaaaaaaaaaaUU
|1111111111111111111111^^^``^|`^^|1111aa111111aaaaaa1aaaaaaaaaaaaaaaa
1|1111111111111111111111111^^^^||11111UBa11111a1111aaaaaaaaaaaaaaaaaa
|111111111|^``````^|11||^^|^```^11111aBBBa1111111111aaaaaaaaaaaaaaaaa
` ^11111|`  ````````^^^^`^^^``^^|111UBBBBBU11111111111111aaaaaaaaaaaa
`````1111|`````^^|^`^` `````^^^|111BBBBBNBBU111111111111a1aaaa1aaaaaa
``````|11^`` ``^||^`````````^` ``^BBBNNNNBBBBa1111111111111111111aaaa
``````^111``````^``    ``````` `^BBNNNBNNNNNBBU11111111111111111aa1aa
```````|11```           ``   ``^BNNNBBBNNNNNNNBB11111111111a111111aaa
```````^``  `             ``|BNNNBUUUBBNNNNNNNNNNNNBU1111111111a11aa1
^^^^|^``       `````````` `1BBBUaBUUUUBNNNNNNNNNNNNU11111111111111111
|111|^````   `````||^^^^````UUUaaBUUUUBNNNNNNNNNNNNB11111111111111111
111111|^``````````^^^``````^UUBaaUUUUUUNNNNNNNNNNNNB||111111111111111
111111||^^^````````````````1BNBaaaUBUUUNNNNNNNNNNNBB111111111`^``````
111||1|11||^``^````^^aBUBNNNBNBaaaBUaUUNNNNNNNNNNNNB111111111|^^^^^^^
111|1||11|11||^^^1BUBa1aNNNNNNBaa1aUaUUNNNNNNNNNNNNB1^`^|^|111|||^^^^
1||||||11|1|1aUaBa11aa1aNNNNNNBaaaUaaUUBNNNNNNNNNNBBa^^^^||1111111|||
1||||||||1UUBUa111aaU1aUNNNNNNBUUaaaUUBNNNNNNNNNNNNBU11111111111||||1
^````^UBBaaaaaUaaa1Ua1aUNNNNNNNUUBBBBUUBNNNBBNBBNBNBB|1111|1|111|||||
|``aBaaaaaaaaU11UaaUaaUUNNNNNNNBaaUUaUUBNNNNNNNNNBNBB|1||1||1||||1|||
^``|aaaUa1aaaaaaaaaaaaUUNNNNNNNBaaaaUUUBNNNNNNNNNNNBB1||||||11|||||||
^``UaaaaaUaUaUUaUaUUUUUUNNNNNNNBaaUUUUUBNNNNNNNNBNNBBa|||||||||||||||
UNNNNNNNNNNUUUaUUUNNBUaUNNNNNNNBaNBUUUUBNNNNNNNNNNNNBU^^^||^|||^|||||
UNNNNNNNNNNUNBUaaaNNBUUUNNNNNNNNNNNNaBUUNNNNNNNNNNNNBU^^^^^^^^^||||||
UUUaaaUNNNNBNBa1aUNNBUUBNNNNNNNNNNNNUUUUNNNNNNNNNNNNNB^^^^^^^||||||||
UBUaU1aBBBNBNBaaaUNNBUUBNNNNNNNNNNNNUUUUNNNNNNBNNNNNBB|^^^^^^^|||||||
UBUB1aaBBBNNBBUaUUBUBUUBNNNNNNNNNNNNaUUUNNNNNNNNNNNNNB11|^^^|||||||||
UBUB1aUBBNNBaUUUUUUUUUUNNNNNNNNNNNNNUUUUNNNNNNNNNNNNNNNBBB|||111||||1
a111aUUUaNNUaUUUUaUUUUUNNNNNNNNNBBNNUaUUNNNNNNNNNNNNNNNNNNNUUaBBUUaaa
aaaaaaaaaaUaaUUUUUUUBBNNNNNNUUaaaUUBaUUUNNNNNNNNNNNNNNNNBBBBBBNNNNBUB
aaaaaaaaaaa1aaaaaaaa1111||1111aaUUUUUUUUNNNNNNNBBBNBNNNNBNNBBUBBNNNNU
aUUUUUaaaaaaaUaaaa1111||||1111|||11111aa1a1aaaaaaUUUUUaaUBBNNNNNNNNNN
aaaaaaaaaaaaaaaaaaaaaaa1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaa1aaaaaaa11111111aa11aaaaaaaaaaaaaaaaaaaaUaUUUUaaaUaaaaaa

Here are the same as images:

Example set
Font 1 set
Font 2 set
Font 3 set
Font 4 set
Font 5 set

Our favorite is the font 5 set.

Adaptations

As the ASCII art representation depends on the selected font, the above explained approach can be adapted to the situation. Also, the number of characters can easily be either increased or decreased.

Our Flutter app is so big on iOS, it is 5 times Google Chrome (and some Apple App Store Connect publication tips)

Flutter

At jod.li, we started developing Flutter apps in June 2019 (9 months ago). We needed a cross-platform mobile development solution. We chose Flutter because it is evolving fast, yet, it is stable enough for robust development. There is a large community. Also, with Google as the underlying force behind, things are less likely to go wrong. Finally, we tried several other solutions without being convinced and sometimes even being very disappointed (several months of development on a non-Flutter solution and after an update, we ended up with an unrecoverable amount and complexity of compilation errors).

So, Flutter seemed like a good choice.

An example app

We have grown enough knowledge in Flutter to develop side projects in a few days as we need it. During the covid-19 pandemic, the cryptocurrencies are going crazy. Thus, not having a notification on Binance when a price reaches a certain threshold appeared like a problem easy to fix with a Flutter app.

We have thus developed an app that does just that relying on the Binance API.

App size

Came the time of publication on the Google Play Store. The Android App ends up with a very reasonable size smaller than 7 MB.

However, the iOS app once installed on an iPhone takes 407.1 MB. This is 5 times the 84.2 MB the Google Chrome app takes on the same iPhone. This is of course not reasonable.

This is a time when we realise that we may have taken – again – the wrong road for several months with Flutter. All of this because of this final app size on iOS.

What does internet say about this?

The most interesting we found is here:

https://github.com/flutter/flutter/issues/47101#issuecomment-567522077

It says that the embedded Flutter framework file (the vast majority of the bytes in the app) is going to be reduced in the app before reaching the user mobile devices. Well, we’d like to see this happening before we pay the 100 bucks to Apple for the publication.

Preparing the app for the store

So to get closer to the store reality, we follow the iOS Flutter publication guidelines:

https://flutter.dev/docs/deployment/ios#review-xcode-project-settings

We clean Xcode and Flutter, close Xcode and then:

flutter build ios

Open Xcode and select:

  • Runner scheme
  • Generic iOS Device Destination
  • Runner target
  • Optimisation for size
  • Product > Archive
  • Show archive in Finder

The archive size is even bigger than the debug app size on the iPhone: 577.4 MB

Let’s go further anyway.

Deploy to Apple app store

We pay the 100 bucks for a deployment in the Apple app store.

Few hours later, we receive the confirmation email and create the app in the app store.

Small trick, the store icon cannot contain any alpha layer both in the app store and in the app archive (1024×1024 px).

Back in Xcode, at the archive validation step, there is indeed a mention that the bitcode will not be included and that Swift symbols can be striped.

Since the archive is extremely big, the uploading takes ages with an annoying forever 100% progress bar.

Permission and other issues

After all this time uploading, the store sends a rejection email with the following permission related issues:

ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSContactsUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (https://developer.apple.com/documentation/uikit/core_app/protecting_the_user_s_privacy).

ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSCalendarsUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (https://developer.apple.com/documentation/uikit/core_app/protecting_the_user_s_privacy).

ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSAppleMusicUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (https://developer.apple.com/documentation/uikit/core_app/protecting_the_user_s_privacy).

ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSMotionUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (https://developer.apple.com/documentation/uikit/core_app/protecting_the_user_s_privacy).

ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSSpeechRecognitionUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (https://developer.apple.com/documentation/uikit/core_app/protecting_the_user_s_privacy).

Though you are not required to fix the following issues, we wanted to make you aware of them:

ITMS-90078: Missing Push Notification Entitlement - Your app appears to register with the Apple Push Notification service, but the app signature's entitlements do not include the "aps-environment" entitlement. If your app uses the Apple Push Notification service, make sure your App ID is enabled for Push Notification in the Provisioning Portal, and resubmit after signing your app with a Distribution provisioning profile that includes the "aps-environment" entitlement. Xcode does not automatically copy the aps-environment entitlement from provisioning profiles at build time. This behavior is intentional. To use this entitlement, either enable Push Notifications in the project editor's Capabilities pane, or manually add the entitlement to your entitlements file. For more information, see https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1.

ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSLocationAlwaysUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (https://developer.apple.com/documentation/uikit/core_app/protecting_the_user_s_privacy).

ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSLocationWhenInUseUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (https://developer.apple.com/documentation/uikit/core_app/protecting_the_user_s_privacy).

Here is an interesting link on the permissions topic.

To summarise, The app requires permissions that are not needed for its execution. For Apple not to reject the build, we need to mention those permissions as not needed in the Podfile:

  • NSContactsUsageDescription
  • NSCalendarsUsageDescription
  • NSAppleMusicUsageDescription
  • NSMotionUsageDescription
  • NSSpeechRecognitionUsageDescription
  • NSLocationAlwaysUsageDescription
  • NSLocationWhenInUseUsageDescription

Therefore, we added the following to the Podfile:

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      # Here are some configurations automatically generated by flutter

      # You can remove unused permissions here
      # for more infomation: https://github.com/BaseflowIT/flutter-permission-handler/blob/develop/ios/Classes/PermissionHandlerEnums.h
      # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',

        ## dart: PermissionGroup.calendar
         'PERMISSION_EVENTS=0',

        ## dart: PermissionGroup.reminders
         'PERMISSION_REMINDERS=0',

        ## dart: PermissionGroup.contacts
         'PERMISSION_CONTACTS=0',

        ## dart: PermissionGroup.camera
         'PERMISSION_CAMERA=0',

        ## dart: PermissionGroup.microphone
         'PERMISSION_MICROPHONE=0',

        ## dart: PermissionGroup.speech
         'PERMISSION_SPEECH_RECOGNIZER=0',

        ## dart: PermissionGroup.photos
         'PERMISSION_PHOTOS=0',

        ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
         'PERMISSION_LOCATION=0',

        ## dart: PermissionGroup.notification
         'PERMISSION_NOTIFICATIONS=0',

        ## dart: PermissionGroup.mediaLibrary
         'PERMISSION_MEDIA_LIBRARY=0',

        ## dart: PermissionGroup.sensors
         'PERMISSION_SENSORS=0'
      ]

    end
  end
end

We also need to find a solution regarding the push notifications which we don’t use either. Add file ios/Runner/Runner.entitlements with this content:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>aps-environment</key>
	<string>Write here your reason for using push notifications.</string>
</dict>
</plist>

Clean, build, archive, validate, upload,… again.

After some complaints because iPad requires all orientations for multitasking, we adapt configuration in Xcode to force fullscreen (and disable multitasking) and start again:

Invalid Bundle. iPad Multitasking support requires these orientations: 'UIInterfaceOrientationPortrait,UIInterfaceOrientationPortraitUpsideDown,UIInterfaceOrientationLandscapeLeft,UIInterfaceOrientationLandscapeRight'. Found 'UIInterfaceOrientationPortrait' in bundle ...

We also got the build number error although no app was actually successfully published yet (we just increase the build number to 1.0.1):

ERROR ITMS-90189: "Redundant Binary Upload. You've already uploaded a build with build number '1.0.0' for version number '1.0.0'. Make sure you increment the build string before you upload your app to App Store Connect. Learn more in Xcode Help (http://help.apple.com/xcode/mac/current/#/devba7f53ad4)."

Nearly 2 hours of upload later, finally some good news:

We complete the submission on the Apple App Store Connect site.

Publication

6 hours later, the app is in review and 30 minutes later, the app is ready for sale.

Few hours later, the app is published on the app store and the resulting app size is a very reassuring 25.2 MB.

Conclusion

We should not worry too much about the app size before it is published. Once published, the app size is reduced considerably (archive 577 MB -> iPhone from app store 25 MB).

This first Flutter iOS Apple App Store Connect publishing made us aware of the following traps:

  • App store icon cannot contain transparency layer (1024×1024).
  • The archive is so big that the publication takes a very long time. Thus, automating this step is highly desirable.
  • When using the permission handler, include all the non-needed permissions in the Podfile.
  • With Flutter, always include a ios/Runner/Runner.entitlements file for the push notification permissions even if not used.
  • If not all orientations are supported on iPad, disable multitasking by supporting only fullscreen.
  • Always increase build number even if no application version was published.

Can an interlaced PNG image be smaller than the equivalent non-interlaced image?

Purpose

The purpose of those experiments was to determine whether an interlaced PNG image could be smaller in size than the equivalent non-interlaced PNG image.

It was already explained here why interlaced PNG tend to be larger in size than the non-interlaced PNG equivalent image. Here was the conclusion:

“The conclusion is that the uncompressed data length of identical PNG images (with pixel size >= 8) will almost always be greater in the interlaced case because more filter types must be described in the file. Though, the compression (deflate) applied to the PNG data can lead to some particular cases if we compare the resulting PNG file sizes.”

So, the purpose here is to find those particular cases.

For the experiments to be valid, here is what must be identical between the interlaced and the non-interlaced images (based on this StackOverflow response which is based on the PNG RFC2083):

  1. Non-pixel encoding related content embedded in the file such as palette (in the case of color type =! 3) and non-critical chunks such as chromaticities, gamma, number of significant bits, default background color, histogram, transparency, physical pixel dimensions, time, text, compressed text.
  2. Different pixel encoding related content such as bit depth, color type (and thus the use of palette or not with color type = 3), image size,… .
  3. Pixels, compression level.

Method used

The method used consisted in evolving through the auto optimised distributed https://oga.jod.li  genetic algorithm with niche mechanism with the chromosome defining the shape and pixels of an image. Creating the interlaced PNG and the non-interlaced versions with ImageMagick. Comparing the file sizes in bytes. The image depth chosen is 8. The chromosome fitness is defined by the non-interlaced file size minus the interlaced file size offset by 200 to avoid reaching 0.

The following oga.jod.li parameters were constants for all the experiments (see this page for an explanation on those parameters):

  • The auto optimisation: true (more details on this here)
  • The mutation rate: 0.3 (initial value only as it is auto evolved)
  • The crossover rate: 0.2 (initial value only as it is auto evolved)
  • The elitism rate: 0.5 (initial value only as it is auto evolved)
  • The niche distance ratio (ndr): 10 (initial value only as it is auto evolved) (more details on niche mechanism here)
  • The niche power sigma (psi): 1 (initial value only as it is auto evolved) (more details on niche mechanism here)
  • The minimum length: 2
  • The maximum length: 3000
  • The encoding: xdigit (hexadecimal)
  • The envelope: CSV
  • The number of chromosomes per batch: 40

Experiments

General shape

General shape search with no constraint except the maximum size due to the maximum length of the chromosome (3000). This is the same experiment as the one mentioned in the oga.jod.li auto optimisation parameter page.

Here are the results:

General shape batch fitness plot on oga.jod.li

The above batch fitness plot shows that:

  1. The 1 pixel height plateau with a maximum fitness of 197 lasts 27 batches (1080 chromosomes). Indeed, it has been noticed, during those experiments that the evolution starts with a local optima for the PNG images with a height of 1 that leads to a maximum fitness of 197 (meaning that the non-interlaced PNG file is larger than the interlace PNG file as the fitness is offset by 200).
  2. The maximum fitness is 222 after 168 batches (6720 chromosomes).
  3. The maximum fitness did not improve for the next 754 batches (30160 chromosomes). Though, under the right circumstances, it is possible to reach a fitness of 230 as shown below.

As expected and shown below, the genetic algorithm parameters evolved over time due to the auto parameter being enabled:

General shape batch parameters plot on oga.jod.li

Not pattern or trend seems to be readable from the above plot. To the human eye, the parameter choices seem random.

The PNG image corresponding to the best chromosome is the following one (width 1 x height 505, 2036 bytes interlaced, 2058 bytes non-interlaced):

The shape of the image seems to be important for the fitness as the best chromosome converts to a tall image. Thus, the next experiment will constraint the image shape to a one pixel width.

One pixel width

As the best shape for a smaller interlaced PNG image file size compared to the same image non-interlaced is a tall image from previous experiment, this experiment constraints the shape to a one pixel width image.

The files for this experiment can be found here: 1px .

The outcome of the experiment are the following ones:

One pixel width batch fitness plot on oga.jod.li

The above plot shows that:

  1. No one pixel width case had a fitness lower than 180.
  2. Maximum fitness is 230 reached after batch 105 (4200 chromosomes). This means that for the best chromosome, the interlace PNG file size is 30 bytes larger than the same image non-interlaced with the constraint on the maximum chromosome length (3000).
  3. The beginning of the evolution shows a fitness around 200. Which means that the 1 pixel width images interlace PNG files tend to have the same size as their interlaced version.

As expected and shown below, the genetic algorithm parameters evolved over time due to the auto parameter being enabled:

One pixel width batch parameter plot on oga.jod.li

Not pattern or trend seems to be readable from the above plot. To the human eye, the parameter choices seem random.

The PNG image corresponding to the best chromosome is the following one (width 1 x height 996, 3757 bytes interlaced, 3787 bytes non-interlaced):

For the sake of observation, the above image has been split in chunks of 8 pixels high (size of the PNG Adam7 interlace pattern) put side by side and scaled up below:

No particular pattern is visible on the above image. The pixels chosen by the evolution seem to be random to the human eye.

Random one pixel width 996 pixels height

In order to know whether the fitness is due only to the shape or whether the pixels content matter, several PNG images have been generated using random pixels and the interlaced PNG size has been compared to the non-interlaced PNG size. The script is available here: random_1px_band_png.sh .

Here are the results:

Comparing 1×996 random pixels PNG sizes

One can see from the above screenshot the following corresponding fitnesses (offset 200):

  1. 199
  2. 208
  3. 208
  4. 200
  5. 197
  6. 199
  7. 189
  8. 209
  9. 189
  10. 191
  11. 197

This shows a good repartition around 200 but always below 230, which means that the fitness is not just a matter of image shape but also a matter of pixels content.

Why is the fitness higher for tall images?

As explained here, the main reason why the interlaced images are usually larger than the non-interlaced ones is because of the number of filters that need to be encoded. The number of filters for the non-interlaced case is the number of lines while for the interlaced case it can be calculated as follows:

nb_pass1_lines = CEIL(height/8)
nb_pass2_lines = (width>4?CEIL(height/8):0)
nb_pass3_lines = CEIL((height-4)/8)
nb_pass4_lines = (width>2?CEIL(height/4):0)
nb_pass5_lines = CEIL((height-2)/4)
nb_pass6_lines = (width>1?CEIL(height/2):0)
nb_pass7_lines = FLOOR(height/2)

So, in the case of a 1×996 image, we end up with the following number of filter encodings:

nb_pass1_lines = 125
nb_pass2_lines = 0
nb_pass3_lines = 124
nb_pass4_lines = 0
nb_pass5_lines = 249
nb_pass6_lines = 0
nb_pass7_lines = 498

This leads to a total number of filters encoding in the interlaced case of: 996

This is the same number for the non-interlaced case.

Therefore, the cost for filters encoding can be the same for 1 pixel width images and the pixel content can be rearranged by the interlacing passes so that the filtering and compression lead to a smaller size interlaced than non-interlaced.

Conclusion

To minimise the interlaced PNG image size compared to the non-interlaced PNG image size, the oga.jod.li genetic algorithm used a 1 pixel width tall image. One pixel width tall image is a reasonable way to maximise the size difference between non-interlaced and interlaced as the number of filter encoding can be the same while the number of pixels can be significant enough to lead to a rearrangement by interlacing passes so that the filtering and compression lead to a smaller size interlaced than non-interlaced. Once constrained to this shape, the evolution tends to choose the longest images. If chosen randomly, the pixels content of such image lead to a lower fitness than the best chromosome found through evolution. This means that both, the shape and the pixels content are important to have an interlaced PNG image file size smaller than the non-interlaced equivalent image.

Future work

Confirm those findings with another tool than ImageMagick as the filtering and compression choices depend on the PNG image generator.

Size of interlaced against non-interlaced PNG uncompressed data

Goal

I would like to know by advance the length of the uncompressed data within a PNG image whether it is encoded interlaced or not. Knowing and comparing the size of interlaced against non-interlaced PNG uncompressed (inflated) data can allow a wiser choice when encoding a PNG image and can also help while decoding. This can also explain why PNG images are usually greater when interlaced. All the following explanations are based on the PNG RFC. I focus on the cases when the pixel size is >= 8 bits.

Interlaced and non-interlaced: what changes in the PNG file?

Let’s assume 2 identical PNG images, one non-interlaced and one interlaced.
The differences between the files describing those images will be:
– In IHDR chunk: Interlace method (0 for non-interlaced, 1 (Adam7) for interlaced). This doesn’t change the file size by itself.
– In IDAT chunks: Compressed (deflated) scanlines starting with filter type containing the encoded image data. This usually changes the file size as explained below.

Uncompressed data size

The uncompressed (inflated) data length can be calculated as follows for the cases when the pixel size is >= 8 bits.

Non-interlaced case

uncompressed_data_length = height * width * nb_bits_per_pixel / 8 + height
Where nb_bits_per_pixel = nb_sample_per_pixel * bit_depth
Where nb_sample_per_pixel depends on the color_type:
color_type to nb_sample_per_pixel:
0 (grayscale) -> 1
2 (RGB) -> 3
3 (palette based) -> 1
4 (grayscale with alpha) -> 2
6 (RGB with alpha) -> 4

So, for example, if we assume a non-interlaced image with the following characteristics, we get an uncompressed data size of 1695885:
height: 677
width: 626
color_type: 6
=> nb_sample_per_pixel: 4
bit_depth: 8
=> nb_bits_per_pixel: 32

Note the “+ height” at the end of the formula giving the uncompressed_data_length. This is due to the fact that for each scanline, a leading filter type byte is necessary and there are as many scanlines as there are lines in a non-interlaced image.

Interlaced case

uncompressed_data_length = height * width * nb_bits_per_pixel / 8 + sum_lines_per_pass
Where sum_lines_per_pass = nb_pass1_lines + nb_pass2_lines + nb_pass3_lines + nb_pass4_lines + nb_pass5_lines + nb_pass6_lines + nb_pass7_lines
Where (calculation due to Adam7 interlace method):
nb_pass1_lines = CEIL(height/8)
nb_pass2_lines = (width>4?CEIL(height/8):0)
nb_pass3_lines = CEIL((height-4)/8)
nb_pass4_lines = (width>2?CEIL(height/4):0)
nb_pass5_lines = CEIL((height-2)/4)
nb_pass6_lines = (width>1?CEIL(height/2):0)
nb_pass7_lines = FLOOR(height/2)

So, for example, if we assume an interlaced image with the following characteristics, we get an uncompressed data size of 1696479:
height: 677
width: 626
color_type: 6
=> nb_sample_per_pixel: 4
bit_depth: 8
=> nb_bits_per_pixel: 32
nb_pass1_lines = 85
nb_pass2_lines = 85
nb_pass3_lines = 85
nb_pass4_lines = 170
nb_pass5_lines = 169
nb_pass6_lines = 339
nb_pass7_lines = 338
=> sum_lines_per_pass = 1271 (which is greater than height 677 for the non-interlaced case)

Note the “+ sum_lines_per_pass” at the end of the formula giving the uncompressed_data_length. This is because the number of filter type needs to be added. Each scanline has a leading filter type. Having passes for the interlaced case leads to a different number of filter types.

Comparison of uncompressed data length formula whether interlaced or not

Is the number of filter types for the interlaced case always going to be greater than the one for the non-interlaced case because of the passes?
It is easy to show that the number of lines for passes 1, 2, 4 and 6 contribute already for at least a number equivalent to height when width > 4. Indeed:
CEIL(height/8) + CEIL(height/8) + CEIL(height/4) + CEIL(height/2) >= height
The number of lines for passes 3, 5 and 7 can contribute to increase the number of filters for the interlaced case above the number of filters for the non-interlaced case.
Thus, the number of filters for the interlaced case will almost always be greater than the one for the non-interlaced case.

Why is this only “almost” always greater?

Let’s take the following particular example:
height: 1
width: 1
color_type: 6
=> nb_sample_per_pixel: 4
bit_depth: 8
=> nb_bits_per_pixel: 32
nb_pass1_lines = 1
nb_pass2_lines = 0
nb_pass3_lines = 0
nb_pass4_lines = 0
nb_pass5_lines = 0
nb_pass6_lines = 0
nb_pass7_lines = 0
=> sum_lines_per_pass = 1 (which is equal to height 1)
This means that the uncompressed data length should be the same for a 1 pixel PNG image whether interlaced or not (particular case).

Conclusion

The conclusion is that the uncompressed data length of identical PNG images (with pixel size >= 8) will almost always be greater in the interlaced case because more filter types must be described in the file. Though, the compression (deflate) applied to the PNG data can lead to some particular cases if we compare the resulting PNG file sizes.