Extending qnop
qnop-spi is the seam between the community edition and anything built on top
of it, commercial add-ons included. It is published to GitHub Packages as
io.qnop:qnop-spi under AGPL-3.0 and has no runtime dependencies — not even
Spring — so an implementation is a plain library.
The surface is deliberately small. Two extension points ship today.
Storage providers
Section titled “Storage providers”io.qnop.spi.storage.StorageProvider is where document binaries go.
public interface StorageProvider { void put(String key, InputStream content, long contentLength, String contentType); Optional<StorageContent> get(String key); boolean exists(String key); boolean delete(String key);
default Stream<StorageListing> list(String prefix) { throw new UnsupportedOperationException(); }}StorageContent is an AutoCloseable record of stream, length and content
type; StorageListing reports key, size and last-modified. Failures surface as
the unchecked StorageException.
list is optional because not every backing store can enumerate cheaply. The
storage-consistency scan uses it when available.
The community edition ships an S3-compatible implementation registered so that
any StorageProvider bean you supply replaces it. Providing your own is
therefore a matter of putting one on the classpath.
Document extractors
Section titled “Document extractors”io.qnop.spi.extract.DocumentExtractor turns an uploaded file into the geometry
the annotation layer anchors against.
public interface DocumentExtractor { boolean supports(String contentType); RenderedDocument extract(InputStream content) throws ExtractionException;}The returned model is what makes line-precise anchoring possible:
| Type | Carries |
|---|---|
RenderedDocument | one or more Surfaces — pages |
Surface | index, width, height, and its text spans |
TextSpan | the text, its start and end offsets, its box, and per-character advances |
NormalizedBox | x, y, width, height, all 0..1, origin top-left |
Coordinates are normalised rather than absolute, so an annotation survives being rendered at a different scale. Per-character advances are what let a selection land between two characters instead of on a whole span.
The community edition ships a PDF extractor. It is the reason PDF is the format that works end to end today.
What is not there yet
Section titled “What is not there yet”Today the reliable way to extend qnop is a JVM implementation of one of the two interfaces above, on the classpath of your own build.
Licensing
Section titled “Licensing”qnop-spi is AGPL-3.0 like the rest of the community edition. The architecture
decision that defines this seam also defines it as the licence boundary — which
is what allows commercial add-ons to exist against a published contract instead
of as a fork. The reasoning is public in the repository’s ADRs.