This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Public Go SDK for generating Ncode-overlaid PDFs that the NeoLAB Neo smartpen can read, on Linux, macOS, and Windows. Community complement to the official NeoSmartpen/Ncode-SDK2.0.
The two contributions over upstream are load-bearing for the project's identity:
-
Linux / macOS support via Mono. NeoLAB's
NeoLABNcodeSDK.dllships only as a Windows-targeted .NET Framework 4.5 binary, and itsGetImage()/GetPostscript()paths concatenate `workingFolder + "\"- filename
with a literal backslash that breaks under Mono. The C# wrapper intools/ncode-clipatches around this by passing only the basename to the SDK call and then relocating the produced file. The Go side (pattern/neolab) prefixes the wrapper withmonoon non-Windows hosts. **Do not** "simplify" either branch without re-verifying on the Linux side — seedocs/linux-support.mdandtools/ncode-cli/MONO_PATH_WORKAROUND.md` for the disassembly evidence.
- filename
-
3-pixel triangle and 4-pixel custom dilation kernels for the dot ink footprint. The official SDK only exposes
isBold ∈ {false, true};pattern/kernelsaddsTri3Up,Tri3Down,Tri4Up,Tri4Down,Diamond4, plus theApplyKernel/ApplyAlternating/ApplySquare/ApplyDisc/ReduceClusterTopLeftstransforms that shape the bitmap before it is rendered into a PDF. Every kernel is centroid-anchored on the original SDK pixel position so the pen IR camera reads the same Ncode coordinate regardless of kernel choice (seedocs/kernels.md).
If a contribution to this repo doesn't move one of those two needles or the supporting Go API, ask before adding it — the SDK is intentionally narrow.
ncode/ Pure domain types: NcodeType, Ticket, PageID,
Page, PaperSize, DotMode. No I/O, no SDK calls.
Mirrors the public surface of NcodeSDK 2.0.
pattern/ Generator interface + 1-bpp Bitmap (0 = dot, 1 =
background, MSB-first byte packing).
├── neolab/ Production Generator. Spawns ncode-cli once per
│ page; prefixes "mono" on non-Windows; decodes the
│ produced PNG into a Bitmap.
├── stub/ Deterministic non-pen-readable Generator
│ used by the SDK's own test suite (and exported
│ so downstream test suites can use it too) to
│ exercise the PDF pipeline without NeoLAB
│ credentials. Logs slog.Warn on every Generate.
│ NEVER call from an example or production path.
└── kernels/ Tri3 / Tri4 / Diamond4 kernels and dilation
transforms. All consume a pattern.Bitmap and
return a new pattern.Bitmap.
pdf/ K-removal compositor + two-layer PDF assembly.
Publish() ties everything together via
go-fitz (MuPDF) for source rendering, pdfcpu
for assembly, and hhrutter/tiff for the
lossless artwork layer.
tools/ncode-cli/ C# console (.NET Framework 4.5) wrapping
NeoLABNcodeSDK.dll. Builds on Windows
(msbuild) and on Linux/macOS (mono-csc). Holds
the path-handling workaround.
examples/publish/ Minimal end-to-end Go program: env-var
configured, drives Publish() with the
production Tri3 alternating kernel.
docs/ linux-support.md (Mono workaround), kernels.md
(kernel rationale + density measurements),
pipeline.md (two-layer PDF structure).
pdf.Publish deliberately keeps artwork and dots in separate PDF objects:
- A DeviceCMYK artwork image with K forced to 0 everywhere
(
(C, M, Y, 0) = (255-R, 255-G, 255-B, 0)). - A 1-bpp PDF ImageMask drawn in pure K (
0 0 0 1 k) on top.
Pure K from the mask is the only K ink on the page. Flattening the dots
into the artwork image lets printer colour management dilute them — that's
the failure mode the two-layer split exists to prevent. If you ever feel
tempted to merge them for "simplicity", read docs/pipeline.md first.
The pixel-composition rule is verbatim from
cpp/sampleApp(mupdf)/main.cpp:148-176 of the upstream SDK; deviating from
it produces PDFs the pen cannot decode.
Every dilation kernel in pattern/kernels keeps its footprint within
±1 pixel of the original SDK pixel — well inside the Anoto cell pitch
(~9 px at 600 DPI) — so the pen camera reads the same Ncode coordinate
regardless of kernel choice. The Tri3Up/Tri3Down alternating pair and
Diamond4 (alone) additionally satisfy exact centroid invariance
(average pixel position equals the original SDK pixel); the Tri4 pair
does not (its alternating centroid is offset by 0.5 px in y).
TestKernelFootprintWithinOneCell and TestExactCentroidInvariance
in pattern/kernels/kernels_test.go pin both properties — new kernels
added later must keep them passing.
go build ./... # build all Go packages
go vet ./... # vet gate
go test ./... # full test suite (no NeoLAB creds needed)
go test ./pattern/kernels -run TestApply -v # subset by name
go test ./pdf/ -run TestPublishStub -v # integration test against stub generator
gofmt -l . | tee /dev/stderr | (! read) # format gate
# C# wrapper (run from tools/ncode-cli, requires NeoLABNcodeSDK.dll)
msbuild NcodeCli.csproj /p:Configuration=Release /p:Platform=x64 # Windows
mono-csc -r:NeoLABNcodeSDK.dll -target:exe -out:ncode-cli.exe Program.cs # Linux/macOS
The full go test ./... suite is self-contained — it uses
pattern/stub as a Generator and pdfcpu to synthesise sample input
PDFs at runtime, so no NeoLAB credentials, no DLL, and no Mono runtime
are required. The pdf/ integration tests run the full Publish pipeline
at 600 DPI; total wall time on a developer laptop is ~50 s.
The Go side requires CGo (go-fitz statically links MuPDF). On a fresh
Linux box, install build-essential and libgl1-mesa-dev before the
first go build.
go-fitz and pdfcpu are heavy dependencies; do not add or remove them
without checking the impact on the cross-compile matrix the SDK targets
(linux/amd64, linux/arm64, darwin/arm64, windows/amd64).
Both examples are production-only (NeoLAB-required) by design. There
is no example-level offline fallback — offline coverage lives in the
test suite (go test ./... with stub generator). Two distinct
production flows:
examples/discover-tickets — enumerate which (section, owner,
book, page) ranges the app key is provisioned for. This shells out
to ncode-cli tickets, parses the JSON, and prints each grant +
the first PageID it resolves to. Run this before the publish
example so you know what to put in NCODE_OWNER / NCODE_BOOK /
NCODE_PAGE_*.
export NCODE_APPKEY=...
export NCODE_CLI_EXE=/opt/ncode-cli/ncode-cli.exe
go run ./examples/discover-ticketsexamples/publish — full publishing pipeline. Pen-readable
output. Requires the proprietary NeoLABNcodeSDK.dll (obtained
through NeoLAB's technical agreement program), a built
ncode-cli.exe, and a NeoLAB application key.
export NCODE_APPKEY=...
export NCODE_CLI_EXE=/opt/ncode-cli/ncode-cli.exe
export NCODE_INPUT_PDF=./input.pdf
export NCODE_OUTPUT_PDF=./output.pdf
export NCODE_TYPE=N3C6
export NCODE_SECTION=3
export NCODE_OWNER=100 # from discover-tickets output
export NCODE_BOOK=0 # from discover-tickets output
export NCODE_PAGE_START=0 # from discover-tickets output
export NCODE_PAGE_SIZE=64 # from discover-tickets output
go run ./examples/publishThe proprietary DLL is not committed and cannot be redistributed
under NeoLAB's licensing terms — see docs/linux-support.md.
go test ./... runs end-to-end without any NeoLAB asset, by
swapping pattern.Generator to pattern/stub and synthesising a
sample input PDF at runtime via pdfcpu. This is the canonical
offline development loop. The stub is exported so downstream
codebases can use the same trick when writing tests around their
own SDK consumers.
Do not introduce stub usage into examples or production code.
The package doc explicitly forbids it; slog.Warn is emitted on
every Generate call as a tripwire if it ever happens.
This SDK is intentionally narrow. The following kinds of features were considered and left out — do not add them without explicit user direction:
- Ticket persistence:
ncode.Ticketis a plain value the caller constructs. Applications choose their own storage (SQLite, KV, config file, etc.); the SDK does not impose one. - Application UIs / desktop shells: out of scope. SDK consumers wire their own.
- ICC OutputIntent / PDF/X embedding: trivial to add downstream of
Publishwith any PDF library; not worth the dependency surface here. - Vector glyph dot mode (Type 3 font): the raster ImageMask path with the Tri3 kernel matches it on pen recognition in our tests while being significantly simpler to ship and audit.
- Upload / sync clients to specific note-management backends: those belong in their own repos against their own APIs.
If a downstream user asks "where is X?", first check whether X is in this list before assuming it was an oversight.
- Public API stability matters here — this is a published SDK, not an
app. Treat exported names in
ncode,pattern,pattern/kernels,pattern/neolab, andpdfas load-bearing. Removing or renaming an exported symbol is a breaking change; surface it explicitly. - Cross-platform parity matters: every Go change must build on linux/amd64, linux/arm64, darwin/arm64, and windows/amd64. The C# wrapper must build on both Windows (msbuild) and Linux (mono-csc).
- Kernel additions must come with: a comment block showing the dot
layout, a centroid calculation, and an
examples/-level demonstration of how to invoke them viapdf.PublishRequest.DilationKernel. - The Apache 2.0 LICENSE in this repo covers only the source code. The Anoto pattern algorithm, NeoLAB SDK binary, and NeoSmartpen trademarks are not relicensed by this repo. Any documentation that could be read as relicensing them must be edited.
- User's global policy (
/home/dev/.claude/CLAUDE.md) prohibits invokingpsqldirectly. Not relevant to this repo today (no database) but applies if one is ever added.