Writing a tiny program drawing with Postscript
2022-09
Here are the steps I took to draw Swissquote logo in only 3 lines.
%!PS
2.5 2.5 scale /l { lineto } def 1 0.4 0.2 setrgbcolor
(@>CC30TFCF>J>MC30TPCP>T>TJ<J<><NH41HHD0x) { 48 sub 5 mul } forall
arc fill setgray rectfill moveto l l l l l arc l l l arc l fill showpage
Tweet: https://twitter.com/nst021/status/1575046529911787520
Gist: https://gist.github.com/nst/59d02e304142b5a66a2f45c46b6624ee
The main idea is:
I've drawn a grid and tried to figure out the paths and coordinates.
So here is a first version of the program.
2.5 2.5 scale
1 0.4 0.2 setrgbcolor % orange
120 120 100 0 360 arc fill % circle
1 setgray % white
60 150 120 20 rectfill % upper rect
60 70 moveto % start lower path
60 130 lineto % made of lineto ...
180 130 lineto
180 70 lineto
160 70 lineto
160 95 lineto
145 95 15 0 180 arc % right arc
130 70 lineto
110 70 lineto
110 95 lineto
95 95 15 0 180 arc % left arc
80 70 lineto
fill % close and fill path
.1 setlinewidth
0 setgray
0 10 240 { % vertical lines
0 moveto
0 240 rlineto
} for
0 10 240 { % horizontal lines
0 exch moveto
240 0 rlineto
} for
stroke
showpage
The scale 2.5 2.5
is such that coordinates have the following properties:
0..360
rangeIn step 1, operands are pushed right before being consumed by operators.
We can also push them all at the beginning of the program (in reverse order of usage) so that they'll get consumed progessively as needed by operators.
This step doesn't save any characters, but will help to compress them all.
%!PS
2.5 2.5 scale
1 0.4 0.2 setrgbcolor
80 70 95 95 15 0 180 110 95 110 70 130 70 145 95 15 0 180 160 95 160
70 180 70 180 130 60 130 60 70 60 150 120 20 1 120 120 100 0 360
arc fill
% ...
showpage
In order to compress the operands, we'll first put them in a table. From there, we'll be able to perform encoding and decoding operations on each of the table elements. The [1 2 3] {} forall
construction will simply push each element of the array on the stack.
[80 70 95 95 15 0 180 110 95 110 70 130 70 145 95 15 0 180 160 95 160 70
180 70 180 130 60 130 60 70 60 150 120 20 1 120 120 100 0 360] {} forall
The main idea it to leverage the fact that all of our operands are multiples of 5. We can divide them all by 5 in order to reduce their range from 0..360
to 0..72
, and encode them into printable ASCII characters.
Not all operands are multiple of 5 actually. 1, operand of setgray
), is not, so let's change it into 5, so that it will end up as 1 after encoding. It's not a big deal since setgray
will pick white indifferently if its operand is 1 or 5.
A quick look at man ascii
reveals plenty of choice to map the array elements into ASCII. Let's add 48 to them, so that they're now mapped on 48..120
(0..x)
. We conveniently avoid 40 (
and 41 )
and escaping parenthesis, which are the string delimiters in Postscript.
32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 '
40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 /
48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7
56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ?
64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G
72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O
80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W
88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _
96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g
104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o
112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w
120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del
The encoded values are now x/5 + 48
:
[64 62 67 67 51 48 84 70 67 70 62 74 62 77 67 51 48 84 80 67 80 62 84
62 84 74 60 74 60 62 60 78 72 52 49 72 72 68 48 120]
This corresponds to the following ASCII string, that we can embed in the Postscript program:
@>CC30TFCF>J>MC30TPCP>T>TJ<J<><NH41HHD0x
The program is now as follows:
%!PS
2.5 2.5 scale
1 0.4 0.2 setrgbcolor
(@>CC30TFCF>J>MC30TPCP>T>TJ<J<><NH41HHD0x) {
48 sub 5 mul
} forall
arc fill
setgray
rectfill
moveto
lineto
lineto
lineto
lineto
lineto
arc
lineto
lineto
lineto
arc
lineto
fill
showpage
The lineto
operator is used 9 times so it's worth redefining it as l
with /l { lineto } def
. And now the whole code can fit in 3 lines :)
%!PS
2.5 2.5 scale /l { lineto } def 1 0.4 0.2 setrgbcolor
(@>CC30TFCF>J>MC30TPCP>T>TJ<J<><NH41HHD0x) { 48 sub 5 mul } forall
arc fill setgray rectfill moveto l l l l l arc l l l arc l fill showpage
Our custom encoding scheme is more compact than potential alternatives for our specific list of operands.
Here is how a benchmark, along with the encoding and decoding codes, with our operands in a Python list:
l = [int(s) for s in "80 70 95 95 15 0 180 110 95 110 70 130 70 145 95 15 0 180 160
95 160 70 180 70 180 130 60 130 60 70 60 150 120 20 5 120 120 100 0 360".split(' ')]
Hex | Bytes | Code |
---|---|---|
Encode (Python) | '<' + ''.join(["%02x" % (int(x/5)) for x in l]) + '>' |
|
Encoded | 82 | <100e13130300241613160e1a0e1d130300242013200e240e241a0c1a0c0e0c1e1804011818140048> |
Decode (Postscript) | 13 | {5 mul}forall |
ASCII 85 | Bytes | Code |
Encode (Python) | import base64 a = bytes(list([int(x/5) for x in l])) base64.a85encode(a, adobe=True) |
|
Encoded | 54 | <~&.T?e!rsS^',D&r%NQ2b!$i[#+:]Y,,T7(0$k<[e(^'jV(_cs@~> |
Decode (Postscript) | 13 | {5 mul}forall |
Custom | Bytes | Code |
Encode (Python) | '(' + ''.join(["%c" % (int(x/5) + 48) for x in l]) + ')' |
|
Encoded | 42 | (@>CC30TFCF>J>MC30TPCP>T>TJ<J<><NH41HHD0x) |
Decode (Postscript) | 20 | {48 sub 5 mul}forall |