#316·qrcodejs

FTA: hack to get around bugs with utf8/unicode string length computations

Author: getifyCreated Dec 22, 2025Updated Dec 24, 2025

For posterity sake, I want to document these bugs -- though the library is clearly not being maintained! -- so maybe it saves others debugging time.

But rather than posting a PR to fix these, since it almost certainly won't be accepted, I'm going to do something different later in this message: provide a "hack" (a run-time patch of the instance!) that not only gets around all these utf8/unicode bugs, but also provides something feature-wise this library arguably always should have had: a way to encode arbitrary binary data into the QR without needing to go through JS strings (utf16).

The Bug(s)

There are several bugs in this library relating to how it inconsistently/incorrectly computes data payload length if there are unicode characters in it (when it has to prepend the 3 byte BOM, etc).

For example, this function:

javascript
function _getUTF8Length(sText) {
   var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
   return replacedText.length;
}

(from chatgpt):

It’s trying to estimate UTF-8 byte length by encodeURI(..) and then replacing each %XX with one character. That approach is fragile (and it doesn’t account cleanly for all cases, including surrogate pairs / certain characters)

Moreover, this function isn't even used consistently internally.

The result is, as the library tries to determine what QR code version (1-40) to use, based on the chosen error correction level -- by iterating through its internal table here (QRCodeLimitLength) -- it's improperly estimated how much payload size it actually needs for your arbitrary unicode-containing string input, then it selects a version (with your specified correction level) that it thinks will fit... and it MIGHT or MIGHT NOT actually fit... and if it doesn't fit, you get a thrown exception.

Ugh.

I think there are probably other bugs like this, but this is the main identified issue.

The real fixes would be to correct that -getUTF8Length() implementation and to make sure it's consistently used throughout like in its version determination, etc.

The hack (+ the feature that should have been there)

javascript
function patchQRCodeInstance(qr) {
	// keep original
	qr.makeCodeText = qr.makeCode;

	// per-instance bytes API
	qr.makeCodeBytes = function makeCodeBytes(u8, titleText = ""){
		if (!(u8 instanceof Uint8Array)) {
			throw new TypeError("makeCodeBytes(..) expects a Uint8Array");
		}

		// 1) get a correct typeNumber/version chosen by the library (ASCII => 1 byte per char)
		// build dummy string safely (avoid huge arg to repeat if ever larger)
		let dummy = "";
		for (let i = 0; i < u8.length; i++) dummy += "A";

		this.makeCodeText(dummy);

		// 2) reuse the existing model; just replace its payload with raw bytes
		let mode = this._oQRCode.dataList[0].mode; // MODE_8BIT_BYTE constant, sourced from lib
		let parsed = Array.from(u8);

		this._oQRCode.dataList = [{
			mode,
			parsedData: parsed,
			getLength: function getLength(){ return this.parsedData.length; },
			write: function write(buffer){
				for (let i = 0, l = this.parsedData.length; i < l; i++) {
					buffer.put(this.parsedData[i], 8);
				}
			}
		}];

		this._oQRCode.dataCache = null;
		this._oQRCode.make();

		// don’t set binary as title
		this._el.title = titleText;

		this._oDrawing.draw(this._oQRCode);
		this.makeImage();
	};
	
	return qr;
}

What you do is instantiate your QRCode() instance as normal, like:

javascript
var qrcode = new QRCode(document.getElementById("qrcode"), {
	text: "http://jindo.dev.naver.com/collie",
	width: 128,
	height: 128,
	colorDark : "#000000",
	colorLight : "#ffffff",
	correctLevel : QRCode.CorrectLevel.H
});

Then you pass it in to this helper hack:

qrcode = patchQRCodeInstance(qrcode);

Then instead of calling qrcode.makeCode(..) with a string, you call:

javascript
qrcode.makeCodeBytes(byteData);

The byteData here should be a raw binary data representation of what you want to encode, as a Uint8Array instance.

For example, you could do:

javascript
qrcode.makeCodeBytes( (new TextEncoder()).encode(myUnicodeString) );

That bypasses all the problematic string length computation machinery. As you can see above, it "fakes" that by passing a string like "AAAAAA...." of the number of characters equal to your actual byte length. "A" is fully unicode safe, so the buggy length computations don't happen, and you get a proper selection of version+correctionLevel to hold your byte data.

This feature is useful if you want to use QR codes to transmit non-text (or non-printable) data. It's a feature that would have been nice in the library itself, to bypass the string handling and just provide the raw bytes.

But that new feature now has the added benefit of giving us a way around the buggy unicode text length computations.

(note: this is all provided public domain, as-is, no warranties or guarantees of any kind. use it if you like, adapt it if you need to, or whatever.)