DEV Community

Cover image for Effective QR-code compression
Sergey Royz
Sergey Royz

Posted on

1

Effective QR-code compression

Intro

In the previous article, I described how to extract a QR code from the following message:

{
    "n0": 7,
    "x": 21,
    "c": "AcO9w7fDuCDCoMOdKXbDqkvCt11dwoLCqg/DtV/DgCwBF1/CksKVwqfCiwvDrALDtcKfeQcAw6vDv8O0E8Kgwp/Dg3Vyw6vCpMKmw503Mghkw7/Dk8Kl"
}
Enter fullscreen mode Exit fullscreen mode

The Origin

This message is generated by the service qrshare.io, which serves well in the following cases:

  1. Transferring a file from a desktop to a mobile.
  2. Sharing a file with multiple recipients (e.g., presentations, lecture materials).
  3. Sharing a file during an online meeting, especially on Google Meet calls, using this Chrome Extension.

Explanation of the Format

But why is the QR code data transferred in such a strange format in the first place? This is the logic behind using such a format:

  • A QR code is a square matrix of zeroes and ones, which can be represented as a one-dimensional array.
  • The array of 0s and 1s can be split into groups of 8 elements, each representing one byte. This significantly reduces the amount of data to be sent over the network.
  • The array should be padded with leading zeros so that its length is divisible by 8.
  • Since JSON is a text-based format and the string obtained in the previous steps is binary, it has to be base64 encoded.

The Code

The code below takes a string as input and produces that magically ciphered QR code, where:

  • n0 - the number of leading zeroes that should be removed before restoring the matrix.
  • x - the side length of the square matrix.
  • c - the base64-encoded binary string representing the sequence of 0s and 1s.
import com.google.zxing.EncodeHintType;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.google.zxing.qrcode.encoder.Encoder;
import lombok.SneakyThrows;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;

public class QRCodeGenerator {

    public static int[] toBinary(int c) {
        if (c < 0 || c > 255) {
            throw new IllegalArgumentException("Input should be withing the range [0, 255], got: " + c);
        }

        int[] bits = new int[8];
        int i = 7;
        while (c > 0) {
            bits[i--] = c & 0x1;
            c = c >> 1;
        }
        return bits;
    }

    private static char oneCharFromBinary(int[] eightBits) {
        int code = 0;
        for (int i = 7; i >= 0; i--) {
            code += eightBits[i] * (1 << (7 - i));
        }
        return (char) code;
    }

    private static String stringFromBinary(int[] bits) {
        if (bits.length % 8 != 0) {
            throw new IllegalArgumentException("Number of bits should be divisible by 8");
        }
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < bits.length; i += 8) {
            int[] charBits = new int[8];
            System.arraycopy(bits, i, charBits, 0, 8);
            str.append(oneCharFromBinary(charBits));
        }
        return str.toString();
    }

    @SneakyThrows
    public static QR encode(String data) {
        if (data == null || data.length() == 0) {
            throw new IllegalArgumentException("Data is empty");
        }
        var hints = Map.of(EncodeHintType.CHARACTER_SET, "UTF-8");
        var qrCode = Encoder.encode(data, ErrorCorrectionLevel.M, hints);
        var m = qrCode.getMatrix();
        var size = m.getWidth() * m.getHeight();
        var zeroesCount = size % 8 == 0 ? 0 : (8 - (size % 8));

        var bits = new int[size + zeroesCount];

        int i = 0;
        for (int y = 0; y < m.getHeight(); y++) {
            for (int x = 0; x < m.getWidth(); x++) {
                bits[(i++) + zeroesCount] = m.get(x, y);
            }
        }

        var rawCode = stringFromBinary(bits);
        var code = Base64.getEncoder().encodeToString(rawCode.getBytes(StandardCharsets.UTF_8));

        return new QR(zeroesCount, m.getWidth(), code);
    }

    // used for testing encoded
    public static int[][] decode(QR qr) {
        var decoded = new String(Base64.getDecoder().decode(qr.code), StandardCharsets.UTF_8);
        var data = decoded.toCharArray();

        var bits = new int[data.length * 8];
        for (int i = 0; i < data.length; i++) {
            var byteBits = toBinary(data[i]);
            System.arraycopy(byteBits, 0, bits, i * 8, 8);
        }

        var matrix = new int[qr.size][qr.size];
        int i = qr.zeroesCount;
        for (int y = 0; y < qr.size; y++) {
            for (int x = 0; x < qr.size; x++) {
                matrix[y][x] = bits[i++];
            }
        }
        return matrix;
    }

    public record QR(int zeroesCount, int size, String code) {
    }
}
Enter fullscreen mode Exit fullscreen mode

The code uses the zxing library for generating QR codes.

Thanks for reading this far!
Try sharing a file with qrshare.io or use the Chrome Extension!

Next time, we'll discuss how to create that beautiful animation when the code is generated. Stay tuned!

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more