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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
-- 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