Scanned drawings: size, scale, align, crop and trace
Places a scanned picture, scales and turns it from picked points, crops it, and turns its traced ink into lines, arcs and circles in drawing coordinates.
How the precision is kept
Placement arithmetic is 64-bit: a pixel is mapped to the drawing through the picture's corner and pixel vectors and the reference's matrix. What sets the accuracy is the picture: traced geometry is as good as the pixel size and the trace tolerance, 1.5 pixels unless changed. The first size of a scan comes from the file's stated dots per inch as millimeters per pixel.
Open item. Convert the first size of a scan from millimeters per pixel into the drawing's own unit.
Calculations under this policy
9 calculations in 2 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
scan.jsCadCraft 0.8.264-bit3 calculationsReviewed
Tests bin/test-raster.mjs
Pixels to drawing coordinates and back
64-bit
Maps a pixel of a placed picture to the drawing, a drawing point back to a pixel, and gives the size of one pixel.
Pixel y counts down from the top row, so the height is taken off first. The way back divides by the reference matrix's determinant (below 1e-300 it returns pixel 0, 0) and by the determinant of the two pixel vectors, which is not checked.
scan.js
lines 35 to 54
CadCraft 0.8.2
/** Everything the commands need to turn pixels into drawing coordinates for one picture reference. */
export function scanGeom(doc, ref) {
if (!ref || ref.type !== 'insert') return null;
const block = doc.blocks.get(ref.block), frame = pictureFrame(block);
if (!frame || frame.ole) return null;
const pic = frame.pic, m = insertMatrix(ref, block), W = (x, y) => mApply(m, { x, y });
const bp = (px, py) => ({ x: pic.x + pic.ux * px + pic.vx * (pic.h - py), y: pic.y + pic.uy * px + pic.vy * (pic.h - py) });
const det = pic.ux * pic.vy - pic.uy * pic.vx, dm = m[0] * m[3] - m[1] * m[2];
const toDraw = (px, py) => { const b = bp(px, py); return W(b.x, b.y); };
const inv = (x, y) => { // drawing → block → pixel
const dd = m[0] * m[3] - m[1] * m[2];
if (Math.abs(dd) < 1e-300) return { x: 0, y: 0 };
const bx = (m[3] * (x - m[4]) - m[2] * (y - m[5])) / dd, by = (-m[1] * (x - m[4]) + m[0] * (y - m[5])) / dd;
const dx = bx - pic.x, dy = by - pic.y;
const a = (dx * pic.vy - dy * pic.vx) / det, b = (pic.ux * dy - pic.uy * dx) / det; // along the width, up (pixels)
return { x: a, y: pic.h - b };
};
const pixel = Math.hypot(m[0] * pic.ux + m[2] * pic.uy, m[1] * pic.ux + m[3] * pic.uy);
return { ref, block, frame, pic, rec: findRecord(doc, pic.file), toDraw, toPixel: inv, pixel, mirrored: (det * dm) < 0 };
}
Arc through three points
64-bit
The counterclockwise arc through a start, a middle and an end point.
d = 2(aₓ(b_y − c_y) + bₓ(c_y − a_y) + cₓ(a_y − b_y)); center from |a|², |b|², |c|² ÷ d; r = |a − center|
The center comes from absolute drawing coordinates and their squares, so three points close together and far from the origin subtract large nearly equal numbers. Points are called collinear when |d| < 1e-12, an absolute value that does not scale with the coordinates. The radius is measured to the first point only.
Turns a trace result in pixels into lines, arcs and circles in drawing coordinates.
circle r = r(px) × pixel size; arc through start, middle and end
A circle's radius is the traced radius times the size of one pixel along the picture's width, which assumes square pixels and no shear. Arcs are rebuilt from three mapped points; a sweep within 1e-6 rad of a full turn becomes a circle.
scan.js
lines 157 to 178
CadCraft 0.8.2
/** A trace result (pixels) → drawing objects. Lines and arcs on `layer`; with `leftovers`, the boxes left as raster on `leftLayer`. */
export function traceEntities(g, result, { layer, leftLayer = null, leftovers = false } = {}) {
const out = [], P = (x, y) => g.toDraw(x, y);
for (const l of result.lines) { const a = P(l.x1, l.y1), b = P(l.x2, l.y2); out.push({ type: 'line', layer, color: null, x1: a.x, y1: a.y, x2: b.x, y2: b.y }); }
for (const a of result.arcs) {
const TAU = Math.PI * 2;
const c = P(a.cx, a.cy), r = a.r * g.pixel;
if (a.a1 - a.a0 >= TAU - 1e-6) { out.push({ type: 'circle', layer, color: null, cx: c.x, cy: c.y, r }); continue; }
const m = (a.a0 + a.a1) / 2;
const s = P(a.sx, a.sy), e = P(a.ex, a.ey), md = P(a.cx + a.r * Math.cos(m), a.cy + a.r * Math.sin(m));
const arc = arcThrough(s, md, e);
if (arc) out.push({ type: 'arc', layer, color: null, cx: arc.cx, cy: arc.cy, r: arc.r, a0: arc.a0, a1: arc.a1 });
}
const left = [];
if (leftovers && leftLayer) {
for (const b of [...result.left.text, ...result.left.hatch]) {
const ps = [P(b.x0, b.y0), P(b.x1, b.y0), P(b.x1, b.y1), P(b.x0, b.y1)];
left.push({ type: 'poly', layer: leftLayer, color: null, closed: true, pts: ps.map((p) => ({ x: p.x, y: p.y, b: 0 })) });
}
}
return { entities: out, left };
}
per pixel = 25.4 ÷ dpi when the file states it and the drawing has units, else 1; typed width ÷ pixel width
25.4 ÷ dpi is millimeters per pixel, and it is used as drawing units per pixel for any drawing that has units, so the offered size is true only when the unit is the millimeter. Only the horizontal dpi is read. A width typed at the prompt replaces it.
Rescales a scan so two picked points are a stated distance apart.
f = wanted ÷ |b − a|; insertion′ = a + f·(insertion − a); sx′ = sx·f
Points closer than 1e-9 drawing units, an absolute limit, are refused. One division for f; the reference is scaled about the first picked point so that point stays put. The distances printed are rounded for the message only.
scan-cmd.js
lines 100 to 105
CadCraft 0.8.2
const cur = Math.hypot(b.x - a.x, b.y - a.y);
if (!(cur > 1e-9)) { c.print(t('scan.scale.same')); return; }
const want = await c.distance(t('scan.prompt.scale3', { d: fmt(cur, 4) }), { base: a });
if (typeof want !== 'number' || !(want > 0)) return;
const f = want / cur, r = g.ref;
c.doc.change('SCANSCALE', () => c.doc.update(r.id, { ...r, x: a.x + f * (r.x - a.x), y: a.y + f * (r.y - a.y), sx: r.sx * f, sy: r.sy * f }));
Align by two pairs of points
64-bit
Moves, turns and optionally scales a scan so two points on it land on two points of the drawing.
The turn is the difference of two atan2 results, then one cosine and one sine. The first pair lands exactly; the second lands exactly only when scaling is on. The new rotation is wrapped with one added turn.
Turns the picture back by the skew angle that was found or typed.
An angle under 0.005° is not applied. The angle found by the raster library is shown to 2 decimals but applied unrounded; the picture's pixels are resampled by the library's rotateImage.
scan-cmd.js
lines 141 to 142
CadCraft 0.8.2
if (Math.abs(deg) < 0.005) { c.print(t('scan.skew.none')); return; }
const out = rotateImage(img, -deg, { fill: 255 }); // lines that rise by `deg` are turned back down by `deg`
Crop window in pixels
64-bit
The whole-pixel window of a crop and the new corner of the cropped picture.
The picked window is widened outward to whole pixels and held inside the picture. The new corner is built from whole pixel counts, so the pixels that remain keep their place and size.
scan-cmd.js
lines 193 to 200
CadCraft 0.8.2
const [x0, y0, x1, y1] = windowBox(g, a, b).map((v, i) => (i < 2 ? Math.max(0, Math.floor(v)) : Math.ceil(v)));
const img = await guarded(c, () => decodePicture(g.rec, { decodeJpeg: c.host.decodeJpeg }));
if (!img) return;
const X1 = Math.min(img.w, x1), Y1 = Math.min(img.h, y1);
if (X1 - x0 < 2 || Y1 - y0 < 2) { c.print(t('scan.crop.empty')); return; }
const out = crop(img, x0, y0, X1, Y1);
// the cropped picture's lower-left corner, in the block's coordinates; one pixel keeps its size
const p = g.pic, bx = p.x + p.ux * x0 + p.vx * (p.h - Y1), by = p.y + p.uy * x0 + p.vy * (p.h - Y1);
Trace tolerance
64-bit
The tolerance of a trace, typed in drawing units and used in pixels.
tolerance (px) = typed ÷ pixel size, held to 0.5 … 20
One division by the pixel size; the result is held between 0.5 and 20 pixels, and 1.5 pixels is used when nothing is typed. The trace is run with this many pixels.
scan-cmd.js
line 252
CadCraft 0.8.2
if (o === 'T') { const v = await c.distance(t('scan.prompt.tol'), { default: tolUnits() }); if (typeof v === 'number' && v > 0) tolPx = Math.max(0.5, Math.min(20, v / g.pixel)); }
In detail
Method
Affine pixel-to-drawing mapping and its inverse; scale and rotation from two picked pairs; circle through three points for traced arcs; tracing, thresholding, deskew and rotation by the shared raster library.
Number types
64-bit numbers for geometry; pictures are 8-bit arrays.
Tolerances
Picked points closer than 1e-9 drawing units (absolute) are refused. Skew under 0.005° is not applied. Trace tolerance 0.5 to 20 pixels. Collinear test |d| < 1e-12 (absolute). A traced sweep within 1e-6 rad of a full turn is a circle. Inverse returns pixel (0, 0) when the reference determinant is under 1e-300.
Limits
The first size uses 25.4 ÷ dpi as drawing units per pixel whatever the drawing's unit is, so it is true size only in a millimeter drawing. Circle radii assume square pixels.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.