Skip to content

Instantly share code, notes, and snippets.

@tomgidden
Last active September 30, 2024 10:31
Show Gist options
  • Save tomgidden/a044434d247fb8626f66d2c056babfe8 to your computer and use it in GitHub Desktop.
Save tomgidden/a044434d247fb8626f66d2c056babfe8 to your computer and use it in GitHub Desktop.
Emergency A4 graph paper
Display the source blob
Display the rendered blob
Raw
%PDF-1.4
1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<<
/Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/Resources 4 0 R
/MediaBox [0 0 595.2755905511812 841.8897637795277]
/Contents 5 0 R
>>
endobj
4 0 obj
<<
/ProcSet [/PDF]
/ColorSpace <</CS1 [/DeviceCMYK]>>
>>
endobj
5 0 obj
<<
/Length 9246
>>
stream
/CS1 CS
2.834645669291339 0 0 2.834645669291339 0 0 cm
1 0 0 1 15 8.5 cm
2 J
0 j
0.5 0 0 0 SC
0.05 w
0 0 m 0 280 l
1 0 m 1 280 l
2 0 m 2 280 l
3 0 m 3 280 l
4 0 m 4 280 l
5 0 m 5 280 l
6 0 m 6 280 l
7 0 m 7 280 l
8 0 m 8 280 l
9 0 m 9 280 l
10 0 m 10 280 l
11 0 m 11 280 l
12 0 m 12 280 l
13 0 m 13 280 l
14 0 m 14 280 l
15 0 m 15 280 l
16 0 m 16 280 l
17 0 m 17 280 l
18 0 m 18 280 l
19 0 m 19 280 l
20 0 m 20 280 l
21 0 m 21 280 l
22 0 m 22 280 l
23 0 m 23 280 l
24 0 m 24 280 l
25 0 m 25 280 l
26 0 m 26 280 l
27 0 m 27 280 l
28 0 m 28 280 l
29 0 m 29 280 l
30 0 m 30 280 l
31 0 m 31 280 l
32 0 m 32 280 l
33 0 m 33 280 l
34 0 m 34 280 l
35 0 m 35 280 l
36 0 m 36 280 l
37 0 m 37 280 l
38 0 m 38 280 l
39 0 m 39 280 l
40 0 m 40 280 l
41 0 m 41 280 l
42 0 m 42 280 l
43 0 m 43 280 l
44 0 m 44 280 l
45 0 m 45 280 l
46 0 m 46 280 l
47 0 m 47 280 l
48 0 m 48 280 l
49 0 m 49 280 l
50 0 m 50 280 l
51 0 m 51 280 l
52 0 m 52 280 l
53 0 m 53 280 l
54 0 m 54 280 l
55 0 m 55 280 l
56 0 m 56 280 l
57 0 m 57 280 l
58 0 m 58 280 l
59 0 m 59 280 l
60 0 m 60 280 l
61 0 m 61 280 l
62 0 m 62 280 l
63 0 m 63 280 l
64 0 m 64 280 l
65 0 m 65 280 l
66 0 m 66 280 l
67 0 m 67 280 l
68 0 m 68 280 l
69 0 m 69 280 l
70 0 m 70 280 l
71 0 m 71 280 l
72 0 m 72 280 l
73 0 m 73 280 l
74 0 m 74 280 l
75 0 m 75 280 l
76 0 m 76 280 l
77 0 m 77 280 l
78 0 m 78 280 l
79 0 m 79 280 l
80 0 m 80 280 l
81 0 m 81 280 l
82 0 m 82 280 l
83 0 m 83 280 l
84 0 m 84 280 l
85 0 m 85 280 l
86 0 m 86 280 l
87 0 m 87 280 l
88 0 m 88 280 l
89 0 m 89 280 l
90 0 m 90 280 l
91 0 m 91 280 l
92 0 m 92 280 l
93 0 m 93 280 l
94 0 m 94 280 l
95 0 m 95 280 l
96 0 m 96 280 l
97 0 m 97 280 l
98 0 m 98 280 l
99 0 m 99 280 l
100 0 m 100 280 l
101 0 m 101 280 l
102 0 m 102 280 l
103 0 m 103 280 l
104 0 m 104 280 l
105 0 m 105 280 l
106 0 m 106 280 l
107 0 m 107 280 l
108 0 m 108 280 l
109 0 m 109 280 l
110 0 m 110 280 l
111 0 m 111 280 l
112 0 m 112 280 l
113 0 m 113 280 l
114 0 m 114 280 l
115 0 m 115 280 l
116 0 m 116 280 l
117 0 m 117 280 l
118 0 m 118 280 l
119 0 m 119 280 l
120 0 m 120 280 l
121 0 m 121 280 l
122 0 m 122 280 l
123 0 m 123 280 l
124 0 m 124 280 l
125 0 m 125 280 l
126 0 m 126 280 l
127 0 m 127 280 l
128 0 m 128 280 l
129 0 m 129 280 l
130 0 m 130 280 l
131 0 m 131 280 l
132 0 m 132 280 l
133 0 m 133 280 l
134 0 m 134 280 l
135 0 m 135 280 l
136 0 m 136 280 l
137 0 m 137 280 l
138 0 m 138 280 l
139 0 m 139 280 l
140 0 m 140 280 l
141 0 m 141 280 l
142 0 m 142 280 l
143 0 m 143 280 l
144 0 m 144 280 l
145 0 m 145 280 l
146 0 m 146 280 l
147 0 m 147 280 l
148 0 m 148 280 l
149 0 m 149 280 l
150 0 m 150 280 l
151 0 m 151 280 l
152 0 m 152 280 l
153 0 m 153 280 l
154 0 m 154 280 l
155 0 m 155 280 l
156 0 m 156 280 l
157 0 m 157 280 l
158 0 m 158 280 l
159 0 m 159 280 l
160 0 m 160 280 l
161 0 m 161 280 l
162 0 m 162 280 l
163 0 m 163 280 l
164 0 m 164 280 l
165 0 m 165 280 l
166 0 m 166 280 l
167 0 m 167 280 l
168 0 m 168 280 l
169 0 m 169 280 l
170 0 m 170 280 l
171 0 m 171 280 l
172 0 m 172 280 l
173 0 m 173 280 l
174 0 m 174 280 l
175 0 m 175 280 l
176 0 m 176 280 l
177 0 m 177 280 l
178 0 m 178 280 l
179 0 m 179 280 l
180 0 m 180 280 l
S
0 0 m 180 0 l
0 1 m 180 1 l
0 2 m 180 2 l
0 3 m 180 3 l
0 4 m 180 4 l
0 5 m 180 5 l
0 6 m 180 6 l
0 7 m 180 7 l
0 8 m 180 8 l
0 9 m 180 9 l
0 10 m 180 10 l
0 11 m 180 11 l
0 12 m 180 12 l
0 13 m 180 13 l
0 14 m 180 14 l
0 15 m 180 15 l
0 16 m 180 16 l
0 17 m 180 17 l
0 18 m 180 18 l
0 19 m 180 19 l
0 20 m 180 20 l
0 21 m 180 21 l
0 22 m 180 22 l
0 23 m 180 23 l
0 24 m 180 24 l
0 25 m 180 25 l
0 26 m 180 26 l
0 27 m 180 27 l
0 28 m 180 28 l
0 29 m 180 29 l
0 30 m 180 30 l
0 31 m 180 31 l
0 32 m 180 32 l
0 33 m 180 33 l
0 34 m 180 34 l
0 35 m 180 35 l
0 36 m 180 36 l
0 37 m 180 37 l
0 38 m 180 38 l
0 39 m 180 39 l
0 40 m 180 40 l
0 41 m 180 41 l
0 42 m 180 42 l
0 43 m 180 43 l
0 44 m 180 44 l
0 45 m 180 45 l
0 46 m 180 46 l
0 47 m 180 47 l
0 48 m 180 48 l
0 49 m 180 49 l
0 50 m 180 50 l
0 51 m 180 51 l
0 52 m 180 52 l
0 53 m 180 53 l
0 54 m 180 54 l
0 55 m 180 55 l
0 56 m 180 56 l
0 57 m 180 57 l
0 58 m 180 58 l
0 59 m 180 59 l
0 60 m 180 60 l
0 61 m 180 61 l
0 62 m 180 62 l
0 63 m 180 63 l
0 64 m 180 64 l
0 65 m 180 65 l
0 66 m 180 66 l
0 67 m 180 67 l
0 68 m 180 68 l
0 69 m 180 69 l
0 70 m 180 70 l
0 71 m 180 71 l
0 72 m 180 72 l
0 73 m 180 73 l
0 74 m 180 74 l
0 75 m 180 75 l
0 76 m 180 76 l
0 77 m 180 77 l
0 78 m 180 78 l
0 79 m 180 79 l
0 80 m 180 80 l
0 81 m 180 81 l
0 82 m 180 82 l
0 83 m 180 83 l
0 84 m 180 84 l
0 85 m 180 85 l
0 86 m 180 86 l
0 87 m 180 87 l
0 88 m 180 88 l
0 89 m 180 89 l
0 90 m 180 90 l
0 91 m 180 91 l
0 92 m 180 92 l
0 93 m 180 93 l
0 94 m 180 94 l
0 95 m 180 95 l
0 96 m 180 96 l
0 97 m 180 97 l
0 98 m 180 98 l
0 99 m 180 99 l
0 100 m 180 100 l
0 101 m 180 101 l
0 102 m 180 102 l
0 103 m 180 103 l
0 104 m 180 104 l
0 105 m 180 105 l
0 106 m 180 106 l
0 107 m 180 107 l
0 108 m 180 108 l
0 109 m 180 109 l
0 110 m 180 110 l
0 111 m 180 111 l
0 112 m 180 112 l
0 113 m 180 113 l
0 114 m 180 114 l
0 115 m 180 115 l
0 116 m 180 116 l
0 117 m 180 117 l
0 118 m 180 118 l
0 119 m 180 119 l
0 120 m 180 120 l
0 121 m 180 121 l
0 122 m 180 122 l
0 123 m 180 123 l
0 124 m 180 124 l
0 125 m 180 125 l
0 126 m 180 126 l
0 127 m 180 127 l
0 128 m 180 128 l
0 129 m 180 129 l
0 130 m 180 130 l
0 131 m 180 131 l
0 132 m 180 132 l
0 133 m 180 133 l
0 134 m 180 134 l
0 135 m 180 135 l
0 136 m 180 136 l
0 137 m 180 137 l
0 138 m 180 138 l
0 139 m 180 139 l
0 140 m 180 140 l
0 141 m 180 141 l
0 142 m 180 142 l
0 143 m 180 143 l
0 144 m 180 144 l
0 145 m 180 145 l
0 146 m 180 146 l
0 147 m 180 147 l
0 148 m 180 148 l
0 149 m 180 149 l
0 150 m 180 150 l
0 151 m 180 151 l
0 152 m 180 152 l
0 153 m 180 153 l
0 154 m 180 154 l
0 155 m 180 155 l
0 156 m 180 156 l
0 157 m 180 157 l
0 158 m 180 158 l
0 159 m 180 159 l
0 160 m 180 160 l
0 161 m 180 161 l
0 162 m 180 162 l
0 163 m 180 163 l
0 164 m 180 164 l
0 165 m 180 165 l
0 166 m 180 166 l
0 167 m 180 167 l
0 168 m 180 168 l
0 169 m 180 169 l
0 170 m 180 170 l
0 171 m 180 171 l
0 172 m 180 172 l
0 173 m 180 173 l
0 174 m 180 174 l
0 175 m 180 175 l
0 176 m 180 176 l
0 177 m 180 177 l
0 178 m 180 178 l
0 179 m 180 179 l
0 180 m 180 180 l
0 181 m 180 181 l
0 182 m 180 182 l
0 183 m 180 183 l
0 184 m 180 184 l
0 185 m 180 185 l
0 186 m 180 186 l
0 187 m 180 187 l
0 188 m 180 188 l
0 189 m 180 189 l
0 190 m 180 190 l
0 191 m 180 191 l
0 192 m 180 192 l
0 193 m 180 193 l
0 194 m 180 194 l
0 195 m 180 195 l
0 196 m 180 196 l
0 197 m 180 197 l
0 198 m 180 198 l
0 199 m 180 199 l
0 200 m 180 200 l
0 201 m 180 201 l
0 202 m 180 202 l
0 203 m 180 203 l
0 204 m 180 204 l
0 205 m 180 205 l
0 206 m 180 206 l
0 207 m 180 207 l
0 208 m 180 208 l
0 209 m 180 209 l
0 210 m 180 210 l
0 211 m 180 211 l
0 212 m 180 212 l
0 213 m 180 213 l
0 214 m 180 214 l
0 215 m 180 215 l
0 216 m 180 216 l
0 217 m 180 217 l
0 218 m 180 218 l
0 219 m 180 219 l
0 220 m 180 220 l
0 221 m 180 221 l
0 222 m 180 222 l
0 223 m 180 223 l
0 224 m 180 224 l
0 225 m 180 225 l
0 226 m 180 226 l
0 227 m 180 227 l
0 228 m 180 228 l
0 229 m 180 229 l
0 230 m 180 230 l
0 231 m 180 231 l
0 232 m 180 232 l
0 233 m 180 233 l
0 234 m 180 234 l
0 235 m 180 235 l
0 236 m 180 236 l
0 237 m 180 237 l
0 238 m 180 238 l
0 239 m 180 239 l
0 240 m 180 240 l
0 241 m 180 241 l
0 242 m 180 242 l
0 243 m 180 243 l
0 244 m 180 244 l
0 245 m 180 245 l
0 246 m 180 246 l
0 247 m 180 247 l
0 248 m 180 248 l
0 249 m 180 249 l
0 250 m 180 250 l
0 251 m 180 251 l
0 252 m 180 252 l
0 253 m 180 253 l
0 254 m 180 254 l
0 255 m 180 255 l
0 256 m 180 256 l
0 257 m 180 257 l
0 258 m 180 258 l
0 259 m 180 259 l
0 260 m 180 260 l
0 261 m 180 261 l
0 262 m 180 262 l
0 263 m 180 263 l
0 264 m 180 264 l
0 265 m 180 265 l
0 266 m 180 266 l
0 267 m 180 267 l
0 268 m 180 268 l
0 269 m 180 269 l
0 270 m 180 270 l
0 271 m 180 271 l
0 272 m 180 272 l
0 273 m 180 273 l
0 274 m 180 274 l
0 275 m 180 275 l
0 276 m 180 276 l
0 277 m 180 277 l
0 278 m 180 278 l
0 279 m 180 279 l
0 280 m 180 280 l
S
0.2 w
0 0 m 0 280 l
10 0 m 10 280 l
20 0 m 20 280 l
30 0 m 30 280 l
40 0 m 40 280 l
50 0 m 50 280 l
60 0 m 60 280 l
70 0 m 70 280 l
80 0 m 80 280 l
90 0 m 90 280 l
100 0 m 100 280 l
110 0 m 110 280 l
120 0 m 120 280 l
130 0 m 130 280 l
140 0 m 140 280 l
150 0 m 150 280 l
160 0 m 160 280 l
170 0 m 170 280 l
180 0 m 180 280 l
S
0 0 m 180 0 l
0 10 m 180 10 l
0 20 m 180 20 l
0 30 m 180 30 l
0 40 m 180 40 l
0 50 m 180 50 l
0 60 m 180 60 l
0 70 m 180 70 l
0 80 m 180 80 l
0 90 m 180 90 l
0 100 m 180 100 l
0 110 m 180 110 l
0 120 m 180 120 l
0 130 m 180 130 l
0 140 m 180 140 l
0 150 m 180 150 l
0 160 m 180 160 l
0 170 m 180 170 l
0 180 m 180 180 l
0 190 m 180 190 l
0 200 m 180 200 l
0 210 m 180 210 l
0 220 m 180 220 l
0 230 m 180 230 l
0 240 m 180 240 l
0 250 m 180 250 l
0 260 m 180 260 l
0 270 m 180 270 l
0 280 m 180 280 l
S
0.4 w
0 0 m 0 280 l
20 0 m 20 280 l
40 0 m 40 280 l
60 0 m 60 280 l
80 0 m 80 280 l
100 0 m 100 280 l
120 0 m 120 280 l
140 0 m 140 280 l
160 0 m 160 280 l
180 0 m 180 280 l
S
0 0 m 180 0 l
0 20 m 180 20 l
0 40 m 180 40 l
0 60 m 180 60 l
0 80 m 180 80 l
0 100 m 180 100 l
0 120 m 180 120 l
0 140 m 180 140 l
0 160 m 180 160 l
0 180 m 180 180 l
0 200 m 180 200 l
0 220 m 180 220 l
0 240 m 180 240 l
0 260 m 180 260 l
0 280 m 180 280 l
S
endstream
endobj
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000062 00000 n
0000000125 00000 n
0000000267 00000 n
0000000343 00000 n
trailer
<<
/Size 6
/Root 1 0 R
>>
startxref
9643
%%EOF
-- ghc generate_graph_paper.hs
-- Install ghcup first!
-- Tom Gidden <[email protected]>
module Main where
import Data.List (intercalate)
import System.IO ()
import Text.Printf (printf)
-- 'intercalate "\n" [...]' is like 'buf.join("\n")' in other languages
joinLines :: [[Char]] -> [Char]
joinLines = intercalate "\n"
pdfHeader :: String
pdfHeader = "%PDF-1.4"
pdfFooter :: String
pdfFooter = "%%EOF"
-- Conversion factor (points per mm)
pointsPerMM :: Double
pointsPerMM = 72 / 25.4
-- A4 Page dimensions in millimetres
pageWidth, pageHeight :: Double
pageWidth = 210
pageHeight = 297
-- Minimum page margin in millimetres
margin :: Double
margin = 8
-- Number of millimetres per line (for small, medium and large divisions)
-- so if smallGridMM = 1, this is a small division every millimetre; mediumGridMM = 10
-- is a medium division every 10 millimetres.
smallGridMM, mediumGridMM, largeGridMM :: Double
smallGridMM = 1
mediumGridMM = 10
largeGridMM = 20
-- Line widths for each grid (in millimetres)
smallLineWidth, mediumLineWidth, largeLineWidth :: Double
smallLineWidth = 0.05
mediumLineWidth = 0.2
largeLineWidth = 0.4
-- Calculate number of complete large squares possible
numSquaresX, numSquaresY :: Int
numSquaresX = floor ((pageWidth - 2 * margin) / largeGridMM)
numSquaresY = floor ((pageHeight - 2 * margin) / largeGridMM)
-- Resulting grid dimensions (in mm)
gridWidth, gridHeight :: Double
gridWidth = largeGridMM * fromIntegral numSquaresX
gridHeight = largeGridMM * fromIntegral numSquaresY
-- Calculate translation of bottom left to centre the grid on the page
x0, y0 :: Double
x0 = (pageWidth - gridWidth) / 2
y0 = (pageHeight - gridHeight) / 2
-- The transformation of scaling of points to mm, and then shifting the
-- origin to the bottom-left of the grid to be drawn
matrix :: String
matrix = printf "%f 0 0 %f %f %f cm" pointsPerMM pointsPerMM (x0 * pointsPerMM) (y0 * pointsPerMM)
-- The actual graphics on the page
stream :: String
stream =
joinLines
[ matrix,
"/CS1 CS", -- CMYK mode
"2 J", -- Line caps square
"0 j", -- Mitre join
"0.5 0 0 0 SC", -- Set stroke color to cyan in CMYK
drawGrid -- Draw the grid
]
-- Draw the grid for small, medium and large divisions
drawGrid :: String
drawGrid =
joinLines
[ drawLines 0 gridWidth 0 gridHeight smallGridMM smallLineWidth,
drawLines 0 gridWidth 0 gridHeight mediumGridMM mediumLineWidth,
drawLines 0 gridWidth 0 gridHeight largeGridMM largeLineWidth
]
-- Draw a grid of lines: each grid is a series of horizontal lines as one stroke,
-- and a series of vertical lines.
drawLines :: Double -> Double -> Double -> Double -> Double -> Double -> String
drawLines startX endX startY endY step lineWidth =
joinLines
[ printf "%f w" lineWidth, -- Set line width
unlines [drawLine x startY x endY | x <- [startX, startX + step .. endX]], -- Vertical lines
unlines [drawLine startX y endX y | y <- [startY, startY + step .. endY]], -- Horizontal lines
"S" -- Stroke the path
]
-- Draw a single line
drawLine :: Double -> Double -> Double -> Double -> String
drawLine = printf "%f %f m %f %f l"
-- PDF objects that make up our "document"
pdfObjects :: [String]
pdfObjects =
[ joinLines
[ "1 0 obj",
"<<",
" /Type /Catalog",
" /Pages 2 0 R",
">>",
"endobj"
],
joinLines
[ "2 0 obj",
"<<",
" /Type /Pages",
" /Kids [3 0 R]",
" /Count 1",
">>",
"endobj"
],
joinLines
[ "3 0 obj",
"<<",
" /Type /Page",
" /Parent 2 0 R",
" /Resources 4 0 R",
printf " /MediaBox [0 0 %f %f]" (pageWidth * pointsPerMM) (pageHeight * pointsPerMM),
" /Contents 5 0 R",
">>",
"endobj"
],
joinLines
[ "4 0 obj",
"<<",
" /ProcSet [/PDF]",
" /ColorSpace <</CS1 [/DeviceCMYK]>> ",
">>",
"endobj"
],
joinLines
[ "5 0 obj",
"<<",
printf " /Length %d" (length stream),
">>",
"stream",
stream,
"endstream",
"endobj"
]
]
-- For each string, work out the cumulative start position.
-- `scanl fn initialValue objects` is like `objects.reduce(fn, initialValue)`
findOffsets :: Int -> [String] -> [Int]
findOffsets = scanl (\acc obj -> acc + length obj + 1)
-- Build the xref table, which gives the byte-offsets in the file for each
-- PDF object.
xref :: String
xref =
joinLines
[ "xref",
printf "0 %d" (length pdfObjects + 1),
"0000000000 65535 f",
xrefTable
]
where
offsets = init $ findOffsets (length pdfHeader + 1) pdfObjects
makeXrefEntry (i, offset) = printf "%010d 00000 n" offset
xrefTable = joinLines xrefs
xrefs = map makeXrefEntry (zip [1 ..] offsets)
-- Everything up to the xref table
startOfPdf :: String
startOfPdf =
joinLines
[ pdfHeader,
joinLines pdfObjects
]
-- Trailer, which indicates the number of objects, the root of the document
-- and the location of the xref table.
trailer :: String
trailer =
joinLines
[ "trailer",
"<<",
printf " /Size %d" (length pdfObjects + 1),
" /Root 1 0 R",
">>",
"startxref",
show (1 + length startOfPdf)
]
-- Build the entire PDF buffer.
pdf :: String
pdf =
joinLines
[ startOfPdf,
xref,
trailer,
pdfFooter
]
-- Output the PDF buffer to stdout.
main :: IO ()
main = putStr pdf
#!python
# by Tom Gidden <[email protected]>, 2024
import math
# We'll do everything in millimetres
# Conversion factor (points per mm)
pointsPerMM = 72 / 25.4
# A4 Page dimensions in millimetres
pageWidth = 210
pageHeight = 297
# Minimum page margin in millimetres
margin = 8
# Cyan in CMYK (0-1 scale)
color_cmyk = [0.5, 0, 0, 0]
# Number of millimetres per line (for small, medium and large divisions)
line_widths = {
'S': 0.05,
'M': 0.2,
'L': 0.4
}
# Line widths for each grid (in millimetres)
line_spacing = {
'S': 1,
'M': 10,
'L': 20
}
# Round to nearest large division and add margins.
gridWidth = math.floor((pageWidth - 2 * margin) / line_spacing['L']) * line_spacing['L']
gridHeight = math.floor((pageHeight - 2 * margin) / line_spacing['L']) * line_spacing['L']
# Calculate translation of bottom left to centre the grid on the page
x0 = (pageWidth - gridWidth) / 2
y0 = (pageHeight - gridHeight) / 2
# PDF content
objects = []
# File header
objects.append("%PDF-1.4")
# Object 1: Catalog
objects.append("""
1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj""")
# Object 2: Pages
objects.append("""
2 0 obj
<<
/Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj""")
# Object 3: Page
objects.append(f"""
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/Resources 4 0 R
/MediaBox [0 0 {pageWidth * pointsPerMM} {pageHeight * pointsPerMM}]
/Contents 5 0 R
>>
endobj""")
# Object 4: Resources; define CS1 to switch to CMYK
objects.append("""
4 0 obj
<<
/ProcSet [/PDF]
/ColorSpace <</CS1 [/DeviceCMYK]>>
>>
endobj""")
# Object 5: Contents
def vertical_lines(p0, p1, dp):
p = p0
while p <= p1:
yield f"{p} 0 m {p} {gridHeight} l"
p += dp
def horizontal_lines(p0, p1, dp):
p = p0
while p <= p1:
yield f"0 {p} m {gridWidth} {p} l"
p += dp
def lines(dp):
return "\n".join(list(vertical_lines(0, gridWidth, dp)) + list(horizontal_lines(0, gridHeight, dp)))
# The graphics command stream for the page content object
stream = f"""
/CS1 CS
{pointsPerMM} 0 0 {pointsPerMM} 0 0 cm
1 0 0 1 {x0} {y0} cm
2 J
0 j
{color_cmyk[0]} {color_cmyk[1]} {color_cmyk[2]} {color_cmyk[3]} SC
{line_widths['S']} w
{lines(line_spacing['S'])}
S
{line_widths['M']} w
{lines(line_spacing['M'])}
S
{line_widths['L']} w
{lines(line_spacing['L'])}
S
"""
# Object 5: the actual page content!
objects.append(f"""
5 0 obj
<</Length {len(stream)} >>
stream
{stream}
endstream
endobj""")
# Generate xref table
xref = []
buf = ""
for object in objects:
# Add object to page buffer
buf += object
# Add object to xref table, with start position (len(buf) + 1)
xref.append(f"{str(len(buf) + 1).zfill(10)} 00000 n")
# We scrap the last entry because it's the xref itself.
xref.pop()
# Build the pdf
pdf = f"""{buf}
xref
0 {len(objects)}
0000000000 65535 f
{chr(10).join(xref)}
trailer
<<
/Size {len(objects)}
/Root 1 0 R
>>
startxref
{len(buf) + 1}
%%EOF"""
# Output
print(pdf)
#!/usr/bin/env bun run --
// by Tom Gidden <[email protected]>, 2024
const pdfHeader = '%PDF-1.4';
const pdfFooter = '%%EOF';
// We'll do everything in millimetres.
// Conversion factor (points per mm)
const pointsPerMM = 72 / 25.4;
// A4 Page dimensions in millimetres
const pageWidth = 210;
const pageHeight = 297;
// Minimum page margin in millimetres
const margin = 8;
// Cyan in CMYK (0-1 scale)
const color_cmyk = [0.5, 0, 0, 0]
// Number of millimetres per line (for small, medium and large divisions)
const smallGridMM = 1;
const mediumGridMM = 10;
const largeGridMM = 20;
// Line widths for each grid (in millimetres)
const smallLineWidth = 0.05;
const mediumLineWidth = 0.2;
const largeLineWidth = 0.4;
// Decimal places for grid lines. Should not be necessary, but
// rounding errors make for big files.
const precision = 0;
// Calculate number of complete large squares possible
const numSquaresX = Math.floor((pageWidth - 2 * margin) / largeGridMM);
const numSquaresY = Math.floor((pageHeight - 2 * margin) / largeGridMM);
// Resulting grid dimensions (in mm)
const gridWidth = largeGridMM * numSquaresX;
const gridHeight = largeGridMM * numSquaresY;
// Calculate translation of bottom left to centre the grid on the page
const x0 = (pageWidth - gridWidth) / 2;
const y0 = (pageHeight - gridHeight) / 2;
// CS: change to CMYK mode
// cm (1): set scaling factor to millimetres by changing current transformation matrix
// cm (2): move origin to x0,y0 by changing current transformation matrix
// J: set to square end caps
// j: set to mitre joins
// SC: set stroke colour
// w: set line width
// S: stroke path
// m: move-to
// l: line-to
const drawLine = (x1: number, y1: number, x2: number, y2: number): string =>
`${x1.toFixed(precision)} ${y1.toFixed(precision)} m ${x2.toFixed(precision)} ${y2.toFixed(precision)} l`;
// Draw a grid of lines
function* verticalLines(p0, p1, dp) {
for (let p = p0; p <= p1; p += dp) {
yield drawLine(p, 0, p, gridHeight);
}
}
function* horizontalLines(p0, p1, dp) {
for (let p = p0; p <= p1; p += dp) {
yield drawLine(0, p, gridWidth, p);
}
}
function drawLines(x0: number, x1: number, y0: number, y1: number, step: number, lineWidth: number): string {
return [
`${lineWidth} w`,
...verticalLines(x0, x1, step),
`S`,
...horizontalLines(y0, y1, step),
`S`
].join("\n");
}
// Draw the grid for small, medium and large divisions
function drawGrid(): string {
return [
drawLines(0, gridWidth, 0, gridHeight, smallGridMM, smallLineWidth),
drawLines(0, gridWidth, 0, gridHeight, mediumGridMM, mediumLineWidth),
drawLines(0, gridWidth, 0, gridHeight, largeGridMM, largeLineWidth)
].join('\n');
}
// The actual graphics on the page
const stream: string = `/CS1 CS
${pointsPerMM} 0 0 ${pointsPerMM} 0 0 cm
1 0 0 1 ${x0} ${y0} cm
2 J
0 j
${color_cmyk.join(" ")} SC
${drawGrid()}`;
const pdfObjects: string[] = [
`1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj`,
`2 0 obj
<<
/Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj`,
`3 0 obj
<<
/Type /Page
/Parent 2 0 R
/Resources 4 0 R
/MediaBox [0 0 ${pageWidth * pointsPerMM} ${pageHeight * pointsPerMM}]
/Contents 5 0 R
>>
endobj`,
`4 0 obj
<<
/ProcSet [/PDF]
/ColorSpace <</CS1 [/DeviceCMYK]>>
>>
endobj`,
`5 0 obj
<<
/Length ${stream.length}
>>
stream
${stream}
endstream
endobj`
];
// Generate the 'xref' table, cross-referencing byte-offsets in the
// file to PDF objects.
function generateXref(pdfObjects: string[]): string {
let xref = [
`xref
0 ${pdfObjects.length + 1}
0000000000 65535 f`
];
let offset = pdfHeader.length + 1;
for (let i = 0; i < pdfObjects.length; i++) {
xref.push(`${offset.toString().padStart(10, '0')} 00000 n`);
offset += pdfObjects[i].length + 1;
}
return xref.join("\n");
}
function generatePDF(): string {
const startOfPdf = `${pdfHeader}
${pdfObjects.join("\n")}`;
const xref = generateXref(pdfObjects);
return `${startOfPdf}
${xref}
trailer
<<
/Size ${pdfObjects.length + 1}
/Root 1 0 R
>>
startxref
${startOfPdf.length + 1}
${pdfFooter}`;
}
// Generate and save the PDF
const pdf = generatePDF();
console.log(pdf);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment