Skip to content

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.

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.

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:

TypeCarries
RenderedDocumentone or more Surfaces — pages
Surfaceindex, width, height, and its text spans
TextSpanthe text, its start and end offsets, its box, and per-character advances
NormalizedBoxx, 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.

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.

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.