A URL compressor and QR-code generator that runs entirely on the device. It squeezes a link into a short payload with no server and no database — expanding it back is arithmetic, not a lookup — and it draws the QR code that carries it in the smallest symbol the standard allows.
Paste or share a link into LinkLab and it gives you back two things: a short link you can copy, and a QR code you can show or save. Scan or import either one — on this device or someone else's — and it expands back to the original.
The compression is the interesting part, and it is not a shortener. A shortener hands your URL to a server, which stores it under a random key and hands the key back; the short link only works for as long as that server keeps the row. LinkLab has no server and stores nothing. The payload is the link, run through a compressor: a bijective base-N encoding over four Huffman dictionaries of the web's most common domains, TLDs and path fragments. Everything needed to reverse it travels inside the payload, so a link made on an aeroplane resolves in a browser that has never heard of this app.
Two payload alphabets come out of every compression, because the best string and the best QR code are not the same string:
- The link puts the payload in the URL fragment —
https://ha.mr#<payload>— using the full character set a URL permits, for the shortest text. The fragment never reaches the resolver's server, so even if you use the hosted resolver, it never learns which link you shared. - The QR code uses only the 45 symbols of QR's alphanumeric mode and shouts the whole
thing —
HTTP://HA.MR/<PAYLOAD>— because a single lower-case letter would drop the symbol into byte mode, which spends 16 bits per character where alphanumeric spends 11 for two. Same payload, deliberately different encoding, so the printed code is as small as it can be.
- Compress — a link in, a short link and a QR code out, with the size saved shown as a percentage. Copy the link, share it, or save the QR code.
- Scan — a live camera scanner, and decode from an image for a QR code that is already in your gallery or came in over chat. Both feed the same expander.
- Save & share the QR — as a PNG. PNG, not JPEG, because a QR code is nothing but high-frequency detail and JPEG throws exactly that away. Saving uses the system document picker, so the app needs no storage permission and you choose where it lands.
- History — every link you compressed or expanded, newest first, ordered by arrival rather than by the clock so a device correcting its time over NTP cannot shuffle the list. Delete a single entry, with undo; clear the lot behind a confirmation; or turn recording off in Settings.
- Settings — the resolver domain (so a self-hosted resolver works), the minimum QR error-correction level, the theme, and Material You dynamic colour where the platform offers it.
- Share target — LinkLab registers as a share target for plain text, so a link shared
from a browser or any other app arrives ready to compress. It also opens
ha.mrlinks, so tapping a compressed link expands it here.
The format is a port of ha.mr by p2r3, MIT-licensed. The port is not a reimplementation-in-spirit — it is byte-for-byte identical to the reference, and that identity is the whole point: a payload made by this app expands on the reference's own web page, and vice versa. Two things pin it there:
- The four Huffman dictionaries shipped with the app are the exact tables the reference generates from its own source, verified byte-for-byte.
- The codec is checked against 2,760 golden vectors generated by running the reference implementation itself over a mix of curated and deterministically-fuzzed URLs. Every vector asserts the payload matches the reference exactly, in both alphabets, and that decompression reproduces the original. A single byte of divergence fails the build.
The one genuinely hard part is URL parsing. The reference runs on the browser's new URL(),
and java.net.URI does not agree with it — so the engine carries a hand-written WHATWG-subset
parser that replicates the behaviours the vectors exercise: default-port dropping, ./..
path collapsing, IPv4 and IPv6 canonicalisation, IDN-to-punycode, and the exact
percent-encode sets for the path, query and fragment.
The engine is a plain Kotlin/JVM module with no Android dependencies — enforced by
applying only the kotlin-jvm plugin and declaring no dependencies beyond its test
libraries, so reaching for a Context in there is a compile error rather than a
code-review note. It runs its whole test suite on the JVM, no device required.
- No
INTERNETpermission. The app is structurally incapable of reaching the network — compression and expansion are arithmetic that happens on the device. - One runtime permission:
CAMERA, and only for the live scanner. Decoding from an image needs no permission at all, and neither does saving a QR code — that goes through the system document picker. - No account, no analytics, no tracking. History lives in a local database you can clear or switch off. Nothing leaves the device unless you share it yourself.
Android 8.0 (API 26) or newer, any ABI.
Android will ask you to allow installs from your browser or file manager the first time, because the APK is signed with a developer key rather than distributed through a store.
The attached build is the debug variant, signed with Android's standard debug key (the
CN=Android Debug certificate every SDK install shares, not a key unique to this project).
It is the one that installs, because the release variant is built unsigned — see
Build — and an unsigned APK is rejected by the package installer. Everything works
identically; it is larger and marked debuggable, so build and sign a release variant yourself
before deploying this to anyone else.
git clone https://github.com/Dreamucxe/LinkLab.git
cd LinkLab
echo "sdk.dir=$ANDROID_HOME" > local.properties
chmod +x gradlew
./gradlew assembleReleaseThe APK lands in app/build/outputs/apk/release/. Without a keystore.properties it is
unsigned — add one with storeFile, storePassword, keyAlias and keyPassword to have
Gradle sign it.
Run the engine's golden-vector suite — the gate the whole port rests on — with:
./gradlew :core:compression:testKotlin, Jetpack Compose, Material 3, MVVM, Hilt, Room, coroutines and StateFlow
throughout, split so the engine can be tested without a device and the QR arithmetic
without a screen.
core/compression The ha.mr port — codec, dictionaries, WHATWG URL parser. Pure JVM,
no dependencies, 2,760 golden vectors.
core/qr QR encoding and decoding over ZXing. Chooses the smallest version, then
the highest error-correction level that still fits it. Pure JVM.
app/data Room history, DataStore settings, QR rendering, image decode, PNG export
app/domain Compress and resolve use cases
app/ui Four screens — Compress, Scan, History, Settings — the theme and nav
core/qr earns its own module by doing one thing ZXing will not: after encoding, it raises
the error-correction level as high as the chosen symbol version has room for, because that
redundancy lands where padding bits would otherwise go. Free robustness, and unit-testable
without a device.
The instrumented suite covers exactly the seams the JVM tests cannot: that the Huffman
dictionary — a classpath resource in a pure-JVM library — survives packaging into the APK
and loads through Android's class loader; that the Room trimTo correlated subquery runs on
real SQLite; and that a rendered Bitmap decodes back through getPixels, where a
transposed row looks right and does not scan.
The compression format, its dictionaries and the arithmetic that reads them are the work of p2r3, published as ha.mr under the MIT licence. LinkLab is an Android port of that work; see NOTICE for the upstream copyright and permission notice, which its licence requires be carried along.
QR encoding and decoding use ZXing.
MIT — see LICENSE. The bundled ha.mr work is also MIT; its notice is in NOTICE.