The official Ncode SDK exposes one knob for ink-footprint shaping:
isBold ∈ {false, true}, which produces either a single ink pixel per
Ncode dot (false) or a 2×2 cluster (true). On real laser printers
neither of these is universally optimal:
- The 1 px dot beats the printer's halftone screen at 600 DPI and can be partially or fully dropped, depending on the engine.
- The 2×2 cluster is a ~4× denser ink area than physically necessary, which on color RIPs drives K up enough to occlude background artwork and sometimes triggers the engine's dot-gain compensation curve in a way that shifts the centroid.
Both effects show up as drops in pen recognition rate.
This SDK adds five new dilation kernels that replace the cluster shape after the SDK emits its bitmap, leaving the dot algorithm itself untouched. Every kernel keeps its footprint within ±1 pixel of the original SDK pixel, so the dilated cluster stays inside the same Anoto cell (~9 px pitch at 600 DPI) and the pen's IR camera reads the same Ncode coordinate regardless of which kernel was used.
Tri3Up (▲) Tri3Down (▽) Tri4Up Tri4Down Diamond4
. X . X . X . X . X X X . X .
X . X . X . X X X . X . X . X
. X .
Anchor convention: every kernel includes (0, 0) (Diamond4 surrounds
it). The center pixel is the position the SDK would have set under
isBold=false.
The recommended starting point is the alternating triangle pattern:
pdf.PublishRequest{
Bold: false, // SDK emits 1 px per dot
DilationKernel: kernels.Tri3Up,
DilationKernelB: kernels.Tri3Down,
}Why alternating shapes:
- Both triangles have the same 3-pixel ink area, so total K density is predictable.
- Alternating ▲ ▽ ▲ ▽ … in row-major discovery order means any halftone screen interaction averages across two orientations rather than resonating with one. In testing this consistently eliminated the zoom-level moiré fringe a single-orientation Tri3 sometimes produces.
- 3 px is a single LPI cell at the typical 150 lpi office screen, so the cluster either sits inside a halftone cell (printed solid) or straddles two (printed solid+solid). Either way it survives.
Tri4Up/Tri4Down: marginal printers where Tri3 occasionally drops dots. Adds one ink pixel; same alternating logic applies.Diamond4: same ink count asisBold=true(4 px) but distributed along the cardinal directions, so the printed footprint reads visually lighter even though the K coverage is identical. Useful when artwork needs to remain legible behind the dots.
Four building blocks in pattern/kernels:
| Function | Use case |
|---|---|
ApplyKernel(bm, k) |
Custom shape — supply your own offsets. |
ApplyAlternating(bm, kA, kB) |
Two shapes, row-major alternated. |
ApplySquare(bm, n) |
Bulk up to an n×n cluster (n=2 reproduces SDK isBold). |
ApplyDisc(bm, r) |
Circular blob of pixel radius r. Round footprint reads softer than squares. |
ReduceClusterTopLefts(bm) collapses every connected zero-pixel cluster
to its top-left pixel, so a Bold=true bitmap can be normalized back to
1 px/dot before applying a custom kernel.
Pen-recognition outcomes are printer-specific: they depend on the engine's halftone screen, dot-gain curve, and toner. Treat any single data point in this document — including the recommendation to default to alternating Tri3 — as a starting hypothesis to verify on your own hardware. A practical workflow:
- Print one A4 page each of
isBold=falseraw,isBold=true(raw 2×2 cluster),Tri3Up+Tri3Downalternating, andDiamond4. - Measure each page's mean K density (any imaging tool that can threshold and count black pixels works).
- Run pen recognition over a fixed stroke pattern on each page and record the recognition rate.
- Pick the kernel with the lowest density that still gives stable recognition.
NeoLAB's own reference Ncoded PDF lands around the upper end of the density range; their SDK samples are calibrated against it. Below the pen's synchronisation threshold the recognition rate falls off sharply, so the goal is to find the kernel that reaches that threshold with the least ink.
The pen does not care which pixels under a dot are inked; it computes each cluster's centroid in the IR image and quantises that to the Anoto grid (~9 px pitch at 600 DPI). Two facts make these kernels safe:
- Footprint locality. Every kernel offset is in
[-1, 1] × [-1, 1]. The whole cluster lives within one Anoto cell of the original SDK pixel, so even an off-centre centroid quantises to the correct Ncode coordinate. - Exact centroid invariance for two of the kernels:
Tri3Up: centroid (0, 0.667). Anti-symmetric withTri3Down's (0, -0.667). The alternating pair averages exactly to (0, 0).Diamond4: cardinal neighbours, centroid exactly (0, 0).
Tri4Up/Tri4Down are not perfect mirrors — their alternating pair
centroid is (0, 0.5), a half-pixel offset that is still well inside
the Anoto cell but is not exact. Use them when ink density is the
priority and the half-pixel y-bias is acceptable on the target
printer; otherwise prefer the alternating Tri3 pair.
TestExactCentroidInvariance and TestKernelFootprintWithinOneCell
in pattern/kernels pin both properties so a future kernel addition
that violates either is caught at PR time.