Another method I stumbled upon researching upscaling shaders quite a while back is based on the idea of Signed Distance Functions:
Chris Green. 2007. Improved alpha-tested magnification for vector textures and special effects. In ACM SIGGRAPH 2007 courses (SIGGRAPH '07). Association for Computing Machinery, New York, NY, USA, 9–18. https://doi.org/10.1145/1281500.1281665; available online.
It's a bit specific to 3D environment (specifically: game) texture scaling, but that shouldn't stop you.
Idea is simple: Let's start with the assumption we have a clean bitmap font of the characters used in your scan.
Take your e
shape as bitmap font "realization". The e
rendered to a white rectangular pixel image. Imagine that we have a function mapping positions in continuous pixel space (so, 2D real-valued input) to a real number (so, 1D output). (assume dense square pixels.)
Note that this function maps arbitrary positions values, not just integer pixel positions.
Define the following: on the exact edge of your pixel image (so, where a black pixel ends, and a white pixel begins), the function takes value 0.
On every position that is in front of a black pixel, the value of the function is the distance to the closest edge. So, a strictly positive real number.
On every position that is in front of a white pixel, the value of the function is the negative distance to the closest edge. So, a strictly negative real number.
OK, that converts a black-and-white bitmap image into a continuous-valued field. Not that useful for a computer.
Here's the trick: just calculate the distance field at the scan resolution, meaning that instead of defining the function for every possible position, you just define it for actual pixel positions. Call this signed distance bitmap
Claim that the signed distance function is just an interpolation of that signed distance field.
In other words, no matter at which resolution you render that letter, you just convert the target pixel coordinate to the original pixel coordinate system, evaluate the value of the signed distance function at that point, meaning that you interpolate the neighboring signed distance bitmap values with a smooth function. Positive-valued positions are black, negative-valued positions are white.
Got a lot of freedom picking that function, and it will influence what your upscaled letter will look like. Use a (truncated) sinc
function as interpolator, and you can be lazy and do the interpolation using FFT->zero-padding->IFFT. (if you symmetrically pad, the result should still be real.)
Upsample your, say, 100 dpi scan to 1200 dpi and read the downscaling of that on your screen, using an anti-aliasing downscaler (which hopefully most viewers do).
That sounds a bit stupid, scaling up to scale down to get greyscale edges, right?
You can take this idea one step further: You define some positive threshold for the signed distance, above which all pixels are definitely black, and a negative threshold, below which all pixels are definitely white, and you find a nice S-shaped curve that maps the distance values in between to shades of grey.
That way, single "missing" black pixels (like in the center of the o
in not
in your picture) become less white, and single "extra" black pixels (like the one beneath the serifs of the n
in your not
) become less black.