seriot.ch

About > Projects > Golfing with Postscript

Golfing with Postscript

Writing a tiny program drawing with Postscript

2022-09

Swissquote Logo

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:

  1. draw the logo
  2. compress the operands
  3. shorten the remaining code

Step 1 - Draw the logo

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:

  1. remain in the 0..360 range
  2. are multiple of 5

Step 2 - Compress the operands

In 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

Step 3 - Shorten the remaining code

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

Alternative operands encoding schemes

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