At first this was a huge question but I solved the problem myself by discovering the -dEPSFitPage option. The so now it's a short question, why do I need to use this option? What is wrong with my maths here?
Here is my original scanned image (bzipped to a tiny size). I converted it to postscript with:
pnmtops -dpi=300 -width 142.5 -height 190 <image-0005.pnm >correctly-sized.ps
The result of that conversion is
here.
So now I want a PDF. I try this, with the points and pixels specified as I think they should be (pnmtops accepts sizes in mm):
gs -dCompatibilityLevel=1.4 -dSAFER -dFIXEDMEDIA -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dDEVICEWIDTHPOINTS=403.93700797 -dDEVICEHEIGHTPOINTS=538.58267730 -dDEVICEWIDTHPIXELS=1683 -dDEVICEHEIGHTPIXELS=2244 -sOutputFile=test.pdf -f correctly-sized.ps
But that doesn't work. I have to add the -dEPSFitPage option and then I get the result I expect.
As one final sanity check, I go back to my xsane batch settings and check:
Size of images is 19.00 cm by 14.25 cm. Scan was taken at 300 dpi and then forced to 300 dpi with pnmtops anyway, which is also the default dpi for pnmtops.
This is 190 x 142.5 mm. I reversed width and height to pnmtops, it's clever enough to realise it should rotate the image if I do that, it wasn't a mistake.
DPI = Dots Per Inch = Pixels. If I google "mm in inch" it says:
1 inch = 25.4 millimeters
( 190 / 25.4 ) * 300 = 2244 pixels high.
( 142.5 / 25.4 ) * 300 = 1683 pixels wide, so it seems I gave the right numbers. Since I had to tell ghostscript to resize I suppose I didn't.
What went wrong?