Link to the main program: placemat.ps
Links to documentation: ▶︎ Introduction, and a first placemat ▶︎ Fonts and glass decoration ▶︎ Compound Strings and non‑ASCII characters ▶︎ Page‑level controls ▶︎ Arrangement of glasses on the page ▶︎ Non‑Glasses Pages ▶︎ Document‑level controls ▶︎ Type sizes ▶︎ Translations ▷︎ Code injection ▶︎ Bitmap images ▶︎ Debugging
The software allows, even encourages, code injection, of which this document gives some examples.
Mostly, this document describes what may be done with code inside compound strings.
However, other parameters are intended to hold code:
- To
stroke
lines (e.g.,SpiralStrokeCode
,ShapesTitlesStroke
,CrossHatchingOutsideStrokeCode
,TastingNotesColumnStrokeCode
); - To make a more general use of a path (e.g.,
BackgroundTextsGlassesPaintCode
,FlightSeparationPaintCode
,CirclearraysFillBehindCode
); - And without a path being provided, as a generic code-injection point (e.g.,
PrologueCode
,EpilogueCode
,PaintBackgroundCode
,PaintForegroundCode
,PaintBackgroundInsideGlassCircles
).
Further, the majority of the several hundred parameters may hold code.
Much software has a clear specification. If the input meets the specification, the output will behave as promised.
Indeed, that is at least partly true of this software.
But not with code injection.
E.g., assume a tasting is of several vintages of three wines.
The makers of the three wines each have their own branding, each in a different font.
It would be entirely natural to define something like /CircletextFont {Circlearrays WithinTitles get 0 get dup (WineA) eq {pop /FontA} {(WineB) eq {/FontB} {/FontC} ifelse} ifelse} def
.
Except that the default values of NamesFont
and HeaderFont
use {CircletextFont}
.
Typically these are used outside a WithinTitles
loop, so WithinTitles
won’t be defined, and this fails.
Yes, these errors can be located via ⌘F and trial-and-fail.
But the point is that code injection is not always cleanly defined.
It might also be that, occasionally in the software, work is done with a usually-constant parameter outside a loop, when it could be done inside. Multiple instances of this ‘bug’ have been discovered in the past.
Almost any ‘string’ parameter may be a ‘compound string’.
The exception are URLs, as appear in ExternalLinks
and LicensingAgreementLinkPlacemats
.
The code in a compound string may do any of the following.
- Put on the stack ≥0 further compound strings to be painted.
rmoveto
the currentpoint, probably by an amount proportional toCurrentFontSize
(for the horizontal direction of which there is a shortcut:{-0.09 Kern}
being equivalent to{-0.09 CurrentFontSize mul 0 rmoveto}
).- Change the current font or size, probably by
selectfont
, and to a size proportional toCurrentFontSize
, and after storing the original status to permit reversion. - Draw a shape, of a size proportional to
CurrentFontSize
, at the current point, and leaving an extant current point at its lower-right. Such a shape should befill
ed, but non-bind
edly, so thatfill
can be redefined to allow the software to establish the size of the shape.
When user code within compound strings is being executed the current dictionary is UserScratchDict
, which persists between executions of such user code.
UserScratchDict
will therefore hold any variables def
’d.
To avoid name clashes users should not store
anything.
Code inside compound strings may assume that there is a currentpoint
and a current font, and this should still be true after execution.
Any changes made by the code to the current path should be made by rmoveto
, rlineto
, and rcurveto
; code should not alter the dictionary stack; nor alter anything that was on the stack at the start of code execution; nor leave changed the currentmatrix
; nor change any of the many code variables.
But the software does not prevent the user doing any of this: if you don’t want to do these terrible things, just don’t.
Some pairs of letters look better if nudged closer together. This is particularly true with an x-height letter either side of a ‘W’ or a ‘V’, and even more if either of the neighbouring characters is an ‘A’. E.g.:
[(W) {-0.06 Kern} (arre)]
[(T) {-0.06 Kern} (aylor)]
[(Smith W) {-0.06 Kern} (oodhouse)]
[(JDA) {-0.06 Kern} (W)]
Obviously, the optically best kern amount varies by font.
To help with superscripting there are routines SuperscriptOn
and SuperscriptOff
:
A compund string may change the current font:
[
{/Garamond CurrentFontSize selectfont} (This is in Garamond; )
{/TrebuchetMS-Bold CurrentFontSize selectfont} (this is in TrebuchetMS-Bold; )
{/Garamond CurrentFontSize selectfont} (and then back to Garamond.)
]
If a tasting comprises two or three different types of wine, a subtle formatting variation between them can be elegant. However, the variation should not more than slightly change the lightness or darkness, because doing so would impede comparison of wines.
The examples are self explanatory.
/InlineTitlesMaxNumberContours {Belowtitles WithinTitles get (LBV) eq {2} {1} ifelse} def
/ShapesFlowersNumPetalsMin {Circlearrays WithinTitles get 1 get (LBV) eq {5} {6} ifelse} def
/ShapesFlowersNumPetalsMax {ShapesFlowersNumPetalsMin} def
/DecanterLabelsNumCopies
{
Belowtitles WithinTitles get dup
(Tappit Hen) eq
{pop 3}
{(Magnum) eq {2} {1} ifelse}
ifelse
} def % /DecanterLabelsNumCopies
If the string needs kerning, then at the start assign it to a variable with the likes of /TappitHen [(T) {-0.06 Kern} (appit Hen)] def
; use in /Belowtitles [ … () … TappitHen … ] def
; and test for equality to TappitHen
rather than to the string (Tappit Hen)
.
Of course, variation can be purely decorative.
/SpiralCentreFromCentreProportionRadiiInside 0.75 def
/SpiralCentreFromCentreAngle {360 WithinTitles 1 add mul Circlearrays length div} def
FontSizesTitlesNotSmallerIfTitlesNotLonger
causes the font size to be affected by the length of a string, measured in characters.
That means it must be known how many deemed characters is something painted by user code.
This can be set within the code, as /EffectiveNumCharacters 1 def
(or other integer ≥0).
This has been used very rarely, indeed, prior to September 2024, exactly once.
Many parameters may be set to code, and this code may access internal variables. Multiple variables can be available for inspection.
-
At the page level:
-
TypeOfPagesBeingRendered
, possible values including:/Glasses
;/TastingNotes
;/PlaceName
;/PrePour
;/StickyLabels
;/VoteRecorder
;/DecantingNotes
;/Accounts
;/CorkDisplay
;/DecanterLabels
;/BottleWrap
;/OneCircle
; and/Multiple
if doing calculations applicable to multiple types of page.
-
PageWidth
andPageHeight
, from which it might be necessary to subtract some of the current used margins:MgnL
,MgnR
,MgnT
,MgnB
. (UnlessSideBySideGlassesTastingNotes
is true, these will equalMarginL
,MarginR
,MarginT
,MarginB
.) -
On glasses sheets,
SheetNum
, an integer being the item ofGlassesOnSheets
currently being rendered. This also exists on pre-pour, neck-tag, and sticky-label pages. -
Radii
, an array of reals holding the radii of the different glasses sheets. AlsoRadiiCirclearrayBaseline
andRadiiCirclearrayInside
, holding the distance from centre to the baseline and to the top of theCirclearrays
. -
GlassPositions
, a triple-depth array holding the positions of the glasses.GlassPositions SheetNum get WithinPage get
is an array,[x y]
, the position of the centre of the glass placement. -
On tasting-note pages,
TNSheetNum
, an integer being the item ofGlassesOnTastingNotePages
currently being rendered. -
On place-name pages,
PlaceNameSetNum
, an integer being the number of the sub-array ofNamesPlaceNames
currently being rendered. -
On pre-pour pages,
PrePourSheetNum
, an integer ≥ 0 and ≤PrePourNumCopies
−1, being the number of the pre-pour sheet currently being rendered. -
On sticky-label pages, StickyLabelCopyNum, an integer ≥ 0 and ≤
StickyLabelsNumCopies
−1, being the number of the sticky label currently being rendered. -
On vote-recorder pages,
VoteRecorderTopTextNum
andVoteRecorderSheetNum
, less than the length ofGlassesClusteredOnVoteRecorders
. -
On decanting-notes pages,
DecantingNotesCopyNum
, an integer ≥ 0 and ≤DecantingNotesNumCopies
−1, being the number of the copy of the page. AlsoDecantingNotesSheetNum
, less than the length ofGlassesClusteredOnDecantingNotes
. -
Within each neck tag,
NeckTagsCopyNum
. If a tasting as many people and hence multiple bottles of each wine, of which each person tastes only one, the tags could be numbered:/CirclearraysNeckTags [ Circlearrays {[ exch aload pop [(Bottle #) {NeckTagsCopyNum 1 add}] ]} forall ] def
. -
CircletextMaxFontSizes
, an array of reals withCircletextMaxFontSizes SheetNum get
being that page’s usual font size in which theCirclearrays
are rendered (though the font size of any particular circle text might have been shrunk byCircletextsMinCopies
, or altered by code withinCirclearrays
). -
TitleFontSizes
and similar variablesAbovetitleFontSizes
,BelowtitleFontSizes
, andOvertitleFontSizes
, being nested arrays, the same shape asGlassesOnSheets
. They contain the sizes of the font at the start of rendering theTitles
etc. -
NameNum
, an integer being the item of Names the page of which is being rendered, andThisName
, beingNames NameNum get
. Most things may not vary byNameNum
. In particular, not elements ofTitles
,Abovetitles
,Belowtitles
,Overtitles
,FillTexts
, etc, nor their layout or formatting. So if trying to vary something byNameNum
do test, and expect failure.
-
-
At the individual glass level (and thus available to glass-level settings such as
InlineTitlesMaxNumberContours
, as well as to one-glass situations such as the pre-pour pages) there will also be:-
WithinPage
, being number of the glass on this page, thus running from zero to one less than the number of glasses on pageSheetNum
orTNSheetNum
. -
WithinTitles
, being number of item in the arrayTitles
(and hence also ofCirclearrays
,Abovetitles
,Belowtitles
,Overtitles
,Subtitles
, andFillTexts
).
-
-
Special case: if
FlightSeparationPaintSeparately
thenFlightSeparationPaintCode
may readFlightSeparationLineNum
.
Berry Brothers’ name contains an abbreviation, shown as a small ‘s’ above a dot. This entails some PostScript.
The code below starts by saving the current font sizes.
Then it calculates the height of the ‘s’ (using the handy StringHeight
function), of the ‘o’, and the dot, from which it can deduce the font size such that the dot, a gap half height of the dot, and the ‘s’, in the new size, sum to the height of the ‘o’ in the old, and also deduce the vertical offset of the ‘s’ (BrosVerticalOffset
).
Then the code changes the font size, and rmoveto
s up (it would also rmoveto
right if the dot were wider than the ‘s’).
Shows the ‘s’.
Moves back down, and horizontally such that the ‘s’ and the dot will have aligned centres, and shows the dot.
Moves forwards to the end of the ‘s’ (since the dot is narrower), and finally reverts the x and y font sizes, before showing the remainder of string, ( & Rudd Selection)
.
[{
8 dict begin
(Berry Bro)
{
/BrosOrigFontSizeX CurrentFontSizeX def
/BrosOrigFontSizeY CurrentFontSizeY def
/BrosHeight_s (s) StringHeight def
/BrosHeight_dot (.) StringHeight def
/BrosHeight_o (o) StringHeight def
/BrosVerticalOffset BrosHeight_o BrosHeight_s 1.5 div BrosHeight_dot div 1 add div def
CurrentFontName [
[BrosOrigFontSizeX 0 0 BrosOrigFontSizeY 0 0]
{BrosHeight_o mul BrosHeight_dot 1.5 mul BrosHeight_s add div}
forall
] selectfont
/BrosWidth_s (s) StringWidthRecursive def
/BrosWidth_dot (.) StringWidthRecursive def
BrosWidth_dot BrosWidth_s gt {BrosWidth_dot BrosWidth_s sub 2 div} {0} ifelse
BrosVerticalOffset rmoveto
}
(s)
{BrosWidth_s BrosWidth_dot add -2 div BrosVerticalOffset neg rmoveto}
(.)
{
BrosWidth_s BrosWidth_dot gt {BrosWidth_s BrosWidth_dot sub 2 div 0 rmoveto} if
CurrentFontName [BrosOrigFontSizeX 0 0 BrosOrigFontSizeY 0 0] selectfont
}
( & Rudd Selection)
end
}]
The software can cope with this both in a straight line, and typeset around a circle as an element of an element of Circlearrays
.
In fonts /Garamond
and /TrebuchetMS
:
Maybe, the first time, it wasn’t worth the effort. But the code having been written, the effort of a copy-paste is justified by the elegance.
Code can be complicated, especially if fighting against the general flow of the software’s parameters. For instance, the one-line structure of the text can be forced into two lines.
The code below works both for text in a straight line:
and for that painted in a circle:
/TwoLineT (Rebello) def
/TwoLineB [(V) {-0.12 Kern} (alente)] def
/TwoLine [
{//TwoLineB StringWidthRecursive //TwoLineT StringWidthRecursive sub 2 div dup 0 lt {pop 0} if CurrentFontSize 0.4375 mul rmoveto} //TwoLineT
{//TwoLineT StringWidthRecursive //TwoLineB StringWidthRecursive add -2 div CurrentFontSize -0.875 mul rmoveto} //TwoLineB
{//TwoLineT StringWidthRecursive //TwoLineB StringWidthRecursive sub 2 div dup 0 lt {pop 0} if CurrentFontSize 0.4375 mul rmoveto}
] bind def % /TwoLine
/Circlearrays [
[ TwoLine (1945) ]
[ TwoLine (1947) ]
[ TwoLine (1955) ]
[ TwoLine (1960) ]
[ TwoLine (1963) ]
] def % /Circlearrays
Aesthetically, this can look acceptable if the two words are of similar length and lack descenders (‘Rebello Valente’, perhaps ‘Ramos Pinto’), but not if they are of very different lengths or have descenders (‘Smith Woodhouse’, ‘Tuke Holdsworth’; ‘Butler Nephew’, ‘Gonzalez Byass’, ‘Gould Campbell’, ‘Quarles Harris’, ‘Royal Oporto’).
This is fighting against the software’s programgeist, so maybe just don’t at all.
More flamboyant than the usual style, but this was for a tasting on the day of a royal wedding. Of course, if precise hue of colour is important, do not assume that screen and print colours will match: test on the actual printer.
/ShapesInTitles true def
/ShapesToUse [ /Heart ] def
/ShapesTitlesFill
{
[
{0.9 setgray} % Silver
{0.784 0.063 0.18 setrgbcolor} % Union-Jack red
{0.004 0.123 0.412 setrgbcolor} % Union-Jack blue
{1 0.08 0.58 setrgbcolor} % Pink
{1 0.843 0 setrgbcolor} % Gold
] ShapesIntX 2 mul ShapesIntY add WithinTitles add 5 mod 5 add 5 mod get exec
fill
} def % /ShapesTitlesFill
/ShapesTitlesStroke
{
[
{0 setgray} % Black
dup % Black
{0.6 setgray} % Mid-gray
{0 1 0 setrgbcolor} % Green
{0.722 0.451 0.2 setrgbcolor} % Dark gold
] ShapesIntX 2 mul ShapesIntY add WithinTitles add 5 mod 5 add 5 mod get exec
stroke
} def % /ShapesTitlesStroke
/InlineTitlesMaxNumberContours 2 def
Tasting postponed by an extension of covid lockdowns? Multiple links to placemats, so reluctant to remove placemats from web? Nonetheless, wanting to mark “Postponed”? It can be done.
/PaintBackgroundCode {
5 dict begin
/BackgroundText (POSTPONED) def
gsave 0 0.6 0 setrgbcolor
TitlesFont 48 selectfont
BackgroundText StringPathBBox /T exch def /R exch def /B exch def /L exch def
gsave MarginL PageHeight MarginT sub translate % Margin, not Mgn
T SqrtHalf mul L R sub T sub SqrtHalf mul translate 45 rotate L neg 0 moveto
BackgroundText show grestore
PageWidth MarginR sub MarginB translate
L R sub B add SqrtHalf mul B neg SqrtHalf mul translate 45 rotate L neg 0 moveto
BackgroundText show
grestore end
} bind def % /PaintBackgroundCode
Fancier would be a pale background in the PaintBackgroundCode
, and a darker edge in the PaintForegroundCode
.
There are tastings to which everybody brings something, and sometimes some people bring more than one thing.
A standard presentation is to have the name of the person bringing the wine as the last item of the sub-arrays of Circlearrays
.
For such tastings it’s natural to populate Names
from Circlearrays
.
However, there’s sometimes a blank or spare circle, for which the element of Circlearrays resembles [ /dagger ]
.
This code gets the last item of each sub-array of Circlearrays
; discards those of which the type
is /nametype
; removes duplicates; and appends a spare‑guest = ()
.
/Names % Non-duplicate non-nametypes from last items of Circlearrays' sub-arrays
[
Circlearrays
{
dup length 1 sub get dup type /nametype ne
{
dup length 0 gt
{counttomark 1 sub -1 1 {index 1 index eq {pop exit} if} for}
{pop}
ifelse
} {pop} ifelse % /nametype
} forall % Circlearrays
()
] def % /Names
Observe that this code is executed at parameter assignment; it is not injected.
It has two imperfections.
-
In PostScript strings are
eq
ual, but arrays are not. I.e.(Julian) (Julian) eq
returns true, but[(Julian)] [(Julian)] eq
returns false. So names that are compound strings, so all those with kerning or accents, should be assigned to a variable and that variable used. E.g., “João” should be organised via/Joao [(J) {-0.02 Kern} (o) /atilde (o)] def
. -
If
CircletextFont
≠NamesFont
, then presence or quantities ofKern
ing might need to differ. In this case,Names
must be set manually.