Last active
September 30, 2024 10:31
-
-
Save tomgidden/a044434d247fb8626f66d2c056babfe8 to your computer and use it in GitHub Desktop.
Emergency A4 graph paper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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