The most useful thing a software partner can show you is not a capability list — it is what they have actually built. Below is a selection of our own application and library work: mobile platforms, developer tooling, identity components, and licensing infrastructure. Status is stated for each, because “we built this” and “this is a finished product” are different claims and we keep them apart.
PDF Essentials Professional
Our most substantial application: an Android enterprise document platform, built in Kotlin with Jetpack Compose, Material 3, Room, and WorkManager, on top of a Pdfium rendering core.
On the document side it handles direct PDF editing, annotations, page organisation, OCR and searchable scans, form filling, handwritten and certificate-based signatures, irreversible redaction, password protection, watermarking, document comparison, review threads, voice comments, and secure export. The redaction is worth calling out specifically — irreversible means the content is removed, not covered with a black rectangle that survives a copy-paste.
Its AI assistant provides document summaries, question answering, action-item extraction, semantic search, multi-document workspaces, and voice interaction — with citations tied back to specific pages and regions. That last detail is the difference between an assistant you can trust with a contract and one you cannot: every answer points at where in the document it came from. It is the same retrieval-grounded approach our AI practice applies at enterprise scale.
The enterprise layer covers tenant configuration, policy-controlled AI and sharing, WebDAV and S3-compatible connectors, offline synchronisation, migration and recovery, and accessibility support. The engineering around it — automated tests, CI release gates, SBOM generation, and production build validation — is the part that decides whether a mobile platform can actually be maintained.
Status: working prototype, actively refined.
Mobile Engineering Foundations
- Android PDF Viewer — the Pdfium-based rendering library and viewer foundation used to work through page rendering, gestures, and document display. It is the base layer beneath PDF Essentials Professional, not the same thing as it. In progress.
- File Preloader Library — a reusable Android library that preloads and caches files before they are needed, cutting perceived load time. Library project.
- Privacy Dashboard — an Android utility that surfaces which applications are accessing the camera, microphone, and location, presented in a standard Android or OneUI-style interface. Android development project.
This is what Android and cross-platform work looks like underneath the surface: rendering internals, caching strategy, and platform permission behaviour.
Identity and Biometric Login
A facial-recognition login system, with a liveness-detection variant that adds presentation-attack protection — distinguishing a live person from a photograph or a replayed video. This work is built on and evaluated against established open-source components including face-api.js and CompreFace; the recognition engines are theirs, our work is the login flow, the liveness handling, and the integration.
Biometric authentication is one of those areas where the security design matters more than the demo. If you are considering it, the questions worth asking are about fallback, spoofing, and what happens to the biometric template — which is properly secure-development territory. Status: in progress.
Voice Interaction API
A reusable service for adding spoken interaction to an application: speech recognition, AI response generation, voice synthesis, interruption handling, conversation state, and command execution. The intent is one voice layer usable across mobile, desktop, web, and agent applications rather than rebuilding it per project. Interruption and conversation state are the hard parts — they are what separate a voice interface from a dictation box. Status: in progress.
Software Licensing and Entitlement
A consolidated body of work around protecting and licensing software: licence and serial-key generation, activation validation, machine and host binding, expiration, feature-level entitlements, API-based key validation, licence management, and Java application integration. Parts of it build on existing open-source licensing libraries rather than reinventing the cryptography.
We describe this as one licensing and entitlement capability rather than as several separate products, because that is what it actually is — components of a single problem solved across a few repositories. Status: developed project.
Developer Utilities
HTML Live Editor and Live Code Editor — browser-based editors that render or execute as you type, without a build-and-refresh cycle. Useful for prototyping, testing snippets, teaching, and quickly building interface components. Status: developed utilities.
What This Is Meant to Show
Not that we sell these as products — most are internal builds, prototypes, and libraries. What they demonstrate is the range the team works across: a full enterprise mobile platform with its own release engineering, rendering and caching internals, biometric and identity flows, voice interfaces, and licensing infrastructure. That is the spread you want in a partner for custom software, because most real projects touch several of those at once.
Where a project needs specialist depth — AI agents, building automation, security engineering, business systems, or games — it routes to the relevant specialist practice rather than being forced through a generalist lens.
Tell us what you are building and we will tell you honestly which of this is relevant, what we would reuse, and what we would build from scratch.
What a Page Costs in Memory, and Why Editing Is Not Rendering
Document rendering is bounded by bitmap arithmetic long before it is bounded by parsing speed. A letter page rasterized at 300 dpi is 2,550 by 3,300 pixels, about 8.4 million pixels, which at four bytes each for a 32-bit ARGB bitmap is roughly 34 MB for one page. Hold the current page, the next one, and a zoomed tile, and a mid-range Android device is being asked for something it will not always give. Since API 26 the pixel data for a bitmap lives on the native heap rather than the Java heap, which means the failure is less likely to arrive as a catchable OutOfMemoryError and more likely to arrive as the process being killed with no stack trace. The practical answers are to rasterize at display density rather than print density, tile large pages instead of rendering them whole, and key the cache by page and zoom bucket so that a pinch gesture does not fill memory with near-duplicate bitmaps.
Threading is the second constraint. A Pdfium-class C API is not internally synchronized, so callers are expected to serialize access to a document handle rather than render two pages from it in parallel. That pushes the architecture toward one render thread with a work queue, or toward opening the file more than once and paying the parse cost again per handle. It also makes cancelation a first-class feature: when a user flings through fifty pages, most of the render requests already queued are for pages that will never be seen, and a pipeline that cannot drop them will spend its time producing stale output.
Editing is a different problem from rendering, and the page format is the reason. A PDF stores glyph runs with positions, not paragraphs, so there is no reflow model to fall back on. Changing a sentence means re-laying out a text object and deciding what happens when the embedded font is a subset containing only the glyphs the original author used, which is normal, and the new characters are not in it. Redaction has the mirror version of the same problem: content has to be removed from the content stream rather than covered, and it can also survive in extracted text layers, embedded thumbnails, XObjects, metadata, and in earlier revisions of the file, because a PDF saved as an incremental update appends new objects and leaves the old ones in place. Removing content safely means writing a new file, not appending to the old one.
Barge-In Is an Audio Problem Before It Is a Conversation Problem
Interruption fails in the audio path first. If the microphone hears the synthesized voice, recognition transcribes the application's own speech and the turn-taking logic reacts to it, which looks like a conversation bug and is not one. Echo cancellation needs a reference of what is being played, aligned in time with what is captured. On Android the platform canceler is tied to the voice communication capture path, and AcousticEchoCanceler.isAvailable() reports only that the device implements the effect, not that it performs well on the current route; a Bluetooth headset and a speakerphone present very different delays for it to model. In a browser, the getUserMedia echoCancellation constraint applies to what the system renders through the paired output, so audio played out a different device is simply not covered.
Once the echo is handled, the remaining question is when the speaker stopped. Voice activity detection on energy alone clips people who pause mid-sentence and holds the turn open in a noisy room. A fixed end-of-speech silence window trades directly against latency: shorten it and you cut speakers off, lengthen it and every single turn carries that delay before the response begins. Streaming recognition helps because partial hypotheses let downstream work start early, but partials are revised as more audio arrives, so anything started from a partial has to be revocable rather than committed.
Because recognition is probabilistic and an interruption can land in the middle of execution, command handling needs a taxonomy before it needs a grammar: sort actions into cancelable, confirmable, and not exposed to voice at all. Interruption also has to unwind the turn that was in flight. Playback stops, the pending generation is canceled rather than left streaming into a buffer no one will hear, and conversation state records that the last response was only partly delivered, because the user heard part of it and will refer to that part in the next thing they say.
- Recognition keeps transcribing the assistant's own words: echo cancellation is not covering the active output route. Compare speakerphone against a wired headset before touching the dialog logic.
- Users are clipped mid-sentence: the end-of-speech window is shorter than the pauses in natural speech for that population and language.
- Barge-in works on a headset and fails on speaker: the canceler is dealing with a longer acoustic delay and more nonlinear speaker distortion than it was tuned for.
- The assistant acts on something the user did not finish saying: an action was triggered from a partial hypothesis that was later revised.
The Line Between a Build and Something Another Team Can Depend On
The difference between a feature inside an application and a library is the contract, and the contract is more expensive than the code. Once another team compiles against it, every public symbol is a promise with a removal cost. Kotlin makes some of this easy to get wrong quietly: explicit API mode forces visibility and return types to be stated rather than inferred, and adding a parameter with a default value is source compatible while still changing the JVM signature, so callers compiled against the previous signature fail at runtime rather than at build time. Deciding what is public is the design work; the implementation behind it can be replaced later, and the surface cannot.
On Android, an AAR that declares a dependency on a widely used library does not get its own private copy: Gradle resolves one version for the whole build, so a library that assumes an exact version has to tolerate whatever the host resolves to. The default resolution picks the highest requested version, which means the library can find itself running against something newer than it was compiled against, and only breaks when that newer version is not compatible. Keeping the declared dependency set small reduces the surface for that, and the distinction between api and implementation matters because implementation keeps a transitive dependency off the consumer's compile classpath entirely. Testing against the oldest supported version of a key dependency, not only the newest, is what catches the rest.
The remaining differences are packaging, and they are where an otherwise finished component stops being usable by someone else.
- A library's minSdk is a floor for every consumer. The manifest merger fails outright when an application declares a lower minSdk than a library it includes, and raising the application's floor to accommodate one dependency drops real devices.
- Keep rules for classes reached by reflection have to ship inside the AAR through consumerProguardFiles. Otherwise the host's R8 build strips or renames them and the failure appears only in release builds, which is why a minified build belongs in the first integration rather than the last.
- Library resources merge into the consuming application's namespace, so a resource prefix keeps generically named strings, colors, and IDs from colliding with the host's.
- Native code multiplies per ABI in the packaged output, and recent Android devices can use 16 KB memory pages, which shared objects have to be aligned for. A native core is a packaging and distribution decision, not only a performance one.
Face Recognition Has No Accuracy Number Without a Threshold
A face matcher produces an embedding, a vector derived from an aligned crop, and compares it to an enrolled vector by distance. Nothing in that is a yes or a no until a threshold is chosen, and the threshold is the security posture: lower it and false accepts rise, raise it and legitimate users are locked out, and the point where those cross moves with lighting, camera, and the composition of the enrolled population. One-to-one verification and one-to-many identification are also different problems. With a gallery of N identities, a per-comparison false accept rate accumulates across every comparison in the search, so a threshold that is defensible for verifying a claimed identity is not defensible for picking one out of a directory.
Presentation attack detection has its own vocabulary and its own test standard. ISO/IEC 30107-3 defines how PAD is evaluated and gives the terms that make claims comparable: APCER for attacks accepted as genuine, BPCER for genuine attempts rejected. Attack classes are not interchangeable. A printed photo, a screen replay, a recorded video, and a mask defeat different checks, and challenge-response prompts such as a blink or a head turn raise the cost of a print attack while doing little against a replay that already contains the requested motion. The harder issue is where the decision is made: an attacker who controls the client can feed frames directly into the pipeline and never involve a camera, so a liveness verdict computed only on the client is a verdict the attacker gets to compute.
Template custody is the third decision and the one with legal weight. An embedding is not a photograph, but it is still biometric data, treated as special category data under GDPR Article 9 and named specifically by several US state biometric privacy laws. The design questions are where the template is stored, whether enrollment can be revoked and redone, what happens when a device is lost, and what the fallback path is, because the fallback sets the real strength of the authentication. On Android there is a hard architectural line here: platform biometrics reached through BiometricPrompt keep the template in secure hardware and never expose it to the application, and a custom camera-based matcher is a separate system that cannot unlock Keystore keys configured to require user authentication.
Frequently Asked Questions
Is a PDF rendering engine like Pdfium a PDF editor?
No. It is a parsing and rasterization engine, originally the PDF component of Chromium, and its job is to turn page content into pixels and expose the document's objects, text runs, and form fields through a C API. Annotation models, OCR, signing, redaction, and any form of writing back to the file are layers built above it, each with its own rules from the file format specification. A viewer that renders a document faithfully tells you very little about whether saving changes to that document will be correct.
Where do the face recognition components actually run?
On opposite sides of the client boundary, which is the main practical difference between them. face-api.js is a JavaScript library that runs model inference in the page through TensorFlow.js, so camera frames never leave the device, but the models and the matching logic are delivered to the browser and can be read and modified by whoever controls it. CompreFace is a self-hosted service you deploy and call over a REST API, so images or embeddings cross a network boundary to a server you operate, which gives you a trust boundary the client cannot alter in exchange for a service to run, scale, and secure. That choice usually decides more of the architecture than recognition quality does: it sets where templates are stored, whether the deployment needs a GPU, and which side an attacker has to compromise to inject frames.
What does integrating an Android library into an existing application actually involve?
The first pass is compatibility rather than features: the application's minSdk, its Kotlin and Android Gradle Plugin versions, whether it already resolves a conflicting version of something the library declares, and whether release builds are minified. The second pass is ownership, meaning which side controls threading, storage locations, background scheduling, and how errors are surfaced to the user. The step that surprises people is the release build, because code that works in debug can fail after R8 removes or renames something reached by reflection, so a minified build belongs in the first integration cycle rather than in hardening at the end.
How does offline synchronization resolve conflicts?
Conflict resolution is a policy decision rather than something a sync layer can settle for you: last writer wins, per-field merge, or keeping both versions for the user to resolve. Which one fits depends on the shape of the data, since structured records with independent fields merge cleanly while a document body is a single opaque blob where merging two divergent edits is not defined. Wall-clock timestamps are a weak arbiter because device clocks drift and skew, so ordering is safer with version counters or version vectors carried alongside each record. The decision that belongs to the customer is what a lost edit costs, because every policy except keep-both can discard one.
What has to be in place for a document assistant to cite a specific page and region?
Coordinates have to survive the entire pipeline. Text extraction from a PDF yields glyph runs with bounding boxes in page space rather than sentences, so chunking must carry the page index and rectangle through segmentation, embedding, storage, and retrieval, and the reader has to map page space back to the rendered view at the current zoom and rotation. Scanned pages have no text layer at all, so OCR supplies both the characters and their boxes, and OCR confidence becomes part of what any answer rests on. If a single stage drops coordinates, the citation quietly degrades to a page number.
Does machine binding stop someone from copying licensed software?
No, and the useful framing is what it does instead: binding raises the cost of casual copying and makes it visible, but any check that executes on hardware the user controls can eventually be patched out. Fingerprints also break honestly, since replacing a disk or network adapter, migrating a virtual machine, or MAC address randomization can invalidate a binding for a legitimate user, which makes a reactivation path part of the design rather than an exception. Offline activation relies on signed license files verified against a public key embedded in the application, moving the problem to protecting the signing key, while online validation trades that for an availability dependency and requires a defined behavior when the validation endpoint cannot be reached.
When is building custom software the wrong answer for one of these problems?
When a platform capability or an existing product already meets the requirement and the gap is preference rather than function. The platform's own document and print stack, a hosted signature service, or the built-in biometric prompt cover common cases with far less to maintain, and every line you do not own is a line you do not have to keep compatible with next year's OS release. Custom work earns its place when the requirement crosses those boundaries: behavior a vendor does not expose, data that cannot leave a specific environment, or an integration inside an existing application where a separate product would break the workflow. The test worth applying is whether you can name a specific thing the off-the-shelf option cannot do.