
A double-sided QR code contains two different messages. One message shows up when scanning the QR code as-is, while scanning the mirror image of the QR code reveals the second message.
The two images in Figure 1 are identical except for horizontal flipping. However, the left image decodes to the string "Hello" while the right image decodes as "Goodbye".
Double-sided QR codes are not an intended feature of the QR code standard. In fact, the QR codes in this article do not conform to the QR code standard. They contain intentional errors, designed such that the mirror images decodes to a different message. This article describes a method to generate such double-sided QR codes.
You can also use my online double-sided QR-code generator to make your own QR codes.
Overview of QR codes
This section briefly describes how QR codes work according to the QR code standard [1]. It is limited to aspects that are relevant to the discussion of double-sided QR codes later in this article.
A QR code is a square matrix of pixels. The QR code standard uses the word symbol to refer to a QR code instance. QR codes exist in different sizes, referred to as versions in the QR code standard.

A QR code symbol contains three types of regions. Figure 2 shows the locations of these regions in the 25×25 matrix of a version 2 QR code symbol:
- Function patterns (gray) are fixed patterns in the symbol that facilitate QR code detection and alignment. These include the big square finder patterns in three corners of the symbol.
- Format information (blue) represents certain encoding parameters. It consists of 15 bits, of which 2 copies are placed in the symbol.
- Codewords (green) represent the data embedded in the QR code. The codeword region is subdivided into shapes of 8 pixels.
Format information

The format information represents two parameters: the error correction level and the data mask pattern used in the QR code symbol.
The format information is stored in 15 bits: 2 bits for the error correction level, 3 bits for the data mask pattern, and 10 error correction bits. Error correction is done with a (15, 5) BCH code, which can correct up to 3 bit errors in the 15-bit word.
Two copies of the 15 bits are placed in the QR code symbol. One copy is placed in the upper left corner, folded around the finder pattern. A second copy is split into two halves which are placed alongside the other two finder patterns.
Data encoding
The message represented by a QR code is encoded as a sequence of bits. The QR code standard supports several encoding modes, but I only use the 8-bit mode.
A message encoded in the 8-bit mode consists of the following parts:
- 4-bit mode indicator with the fixed value
0100
; - 8-bit character count indicator;
- the message itself as a sequence of 8-bit groups;
- 4-bit end-of-message indicator with the fixed value
0000
.
For example, the message Hello
is represented in ASCII as
5 bytes 0x48 0x65 0x6c 0x6c 0x6f
.
Encoding these in the 8-bit mode produces the following sequence of bits:
0100 00000101 01001000 01100101 01101100 01101100 01101111 0000
.
This sequence of bits is then reorganized as a sequence of 8-bit codewords:
0x40 0x54 0x86 0x56 0xc6 0xc6 0xf0
.
Finally, the message is padded by appending dummy codewords to fill out the data capacity of the QR code symbol. For example, the data capacity of a version 2-L QR code is 34 codewords.
Error correcting code
Once the data codewords have been determined, a number of error correction codewords are added. These can be used to repair errors, as long as the number of damaged codewords does not exceed the error correction capacity of the code.
The error correcting code is a Reed-Solomon code [2].
At the lowest level, codewords are interpreted as elements of the finite field GF(28).
This is done by interpreting each bit of the codeword as an integer modulo 2, and using the bits as the coefficients of a polynomial.
For example, the codeword 0x86 = 10000110
is interpreted as the polynomial (x7 + x2 + x).
Adding two elements in GF(28) is done by adding coefficients from matching terms of the two polynomials, then taking the remainder modulo 2. This implies that addition in GF(28) is equivalent to bitwise XOR of the underlying codewords. Multiplying two elements in GF(28) amounts to multiplying the polynomials, then taking the remainder modulo a predefined irreducible polynomial.
At a higher level, the complete sequence of codewords is interpreted as a polynomial over GF(28), with each codeword acting as a coefficient in that polynomial. The high-degree coefficients of the polynomial correspond to the data words of the message, while the low degree coefficients correspond to error correction words. The error correction words are chosen such that the final polynomial becomes divisible by a predefined generator polynomial.
The Reed-Solomon code is a linear code. Linear codes have the following property: If A and B are two valid, undamaged, complete codeword sequences, their sum A + B is also a valid, undamaged codeword sequence. Note that codeword sequences are regarded as polynomials, therefore adding them amounts to separately adding each pair of codewords in matching positions. And note that codewords are regarded as elements of GF(28), therefore adding codewords is done by computing a bitwise XOR. It thus follows that the Reed-Solomon code is also a bitwise linear code: the bitwise XOR of any pair of valid, undamaged codeword sequences results in another valid, undamaged codeword sequence.
The QR code standard supports 4 error correction levels, corresponding to different trade-offs between data storage capacity and error correction capacity of the QR code symbol.
I will only use error correction level L
; which has the lowest error capacity.
The number of error correction codewords depends on the QR code version and error correction level, as shown in Table 1.
QR code version | Data codewords | Error correction words | Error correction capacity |
---|---|---|---|
2-L | 34 | 10 | 4 |
3-L | 55 | 15 | 7 |
4-L | 80 | 20 | 10 |
5-L | 108 | 26 | 13 |
Codeword placement
The full codeword sequence consists of the data codewords followed by error correction codewords. These are placed into predefined 8-pixel shapes in the codeword region as shown in Figure 2. Placement starts in the lower right corner of the symbol, then proceeds in an up-and-down zigzag pattern from right to left.
As a final step, a data mask pattern is applied to the pixels in the codeword region. The mask pattern flips certain pixels to the opposite color. This is done to avoid pixel patterns that are difficult to decode, such as large areas of the same color. The standard defines 8 different mask patterns, one of which is chosen by the QR code generator.
QR decoding
A QR decoder performs roughly the following steps:
- Use the function patterns to determine the position, size, rotation and version of the QR code symbol.
- Try to decode the primary format information from the top-left corner of the symbol. If necessary, use the BCH error correcting code to repair up to 3 bit errors in the format bits. If this fails, try to decode the secondary copy of the format information from the other two corners. If this also fails, try to decode the format information from a mirror image of the symbol. If the mirrored symbol contains valid format information, perform the remaining decoding steps on the mirrored symbol.
- Extract the sequence of codewords from the codeword region of the symbol.
- If necessary, use the Reed-Solomon code to repair incorrect codewords up to the error correcting capacity.
- Decode message characters from the data codewords.
Method to make double-sided QR codes
To make a double sided QR code, we construct a matrix of pixels such that both the plain matrix as well as the mirror image of the matrix appear as valid QR codes symbols to a QR decoder.
Transposing the matrix
A QR decoder will automatically rotate its input image such that the three finder patterns appear in the top corners and the bottom left corner. The mirrored symbol therefore corresponds to the transposed matrix, or equivalently, to the matrix mirrorred in its main diagonal.
To make sure that the mirrored symbol is decodeable, we should consider its three types of regions: function patterns, format information and codewords.
It turns out that function patterns are already symmetric with respect to the main diagonal, as can be seen in Figure 2. The only exception is the single black pixel at the side of the bottom left finder, which I will discuss separately.
Double-sided format information
The effect of mirroring on the format information can be seen in Figure 3:
- The format information bits appear reversed in the mirrored symbol.
- The secondary copy of format bit 7 switches places with the fixed black pixel along the bottom left finder in the original symbol.
There are no valid format information words where the reverse bit sequence is also a valid format information word. However, not all is lost. Recall that the format information is protected by an error correcting code which can correct up to 3 bit errors. It thus seems that we need a 15-bit word which differs from a valid format information word in at most 3 places, and also differs from a reversed valid format information word in at most 3 places. There are many pairs of format information words that match this condition.
Unfortunately, it turns out that some popular QR code readers use a non-standard algorithm to decide whether they should decode the symbol as-is, or decode its mirror image. It seems that these decoders consider the format information from the plain image as well as the mirrored image, then prefer the variant that has the fewest number of bit errors in the format information word. This is a problem for double-sided QR codes, where we want the reader to always decode the variant of the symbol that is presented to it, rather than its mirror image.
We can work around this issue by choosing a 15-bit word that meets the following conditions:
- The 15-bit word is a palindrome (identical to its reverse).
- Bit 7 is a
1
bit. - It differs from a valid format information word in at most 3 places.
With these conditions in place, the format information appears identical in both mirror variants of the symbol. This ensures that the format information will not cause any QR code reader to prefer a specific mirror variant.
Several 15-bit words satisfy these conditions.
I like to use the word 111100010001111
.
This word differs in 2 positions from 111100010011101
, which is the valid format information word representing error correction level L and data mask pattern 3.
Placing double-sided data words

Two messages are placed in the codeword region of a double-sided QR code.
As a first step, the messages are separately converted to codeword sequences via the usual data encoding procedure. However, we do not add padding after the end of the message. Each message ends on the codeword that contains the end-of-message indicator.
As an example, let's consider the two messages "Hello" and "Goodbye". Message "Hello" contains 5 characters. Encoding this message with its mode indicator, length indicator and end-of-message indicator, results in 7 codewords. Similarly, the message "Goodbye" is encoded in 9 codewords.
Figure 4 shows how these messages are placed in a version 2-L QR code symbol. Message "Hello" (green) is placed in the normal orientation, starting in the lower right corner, then moving up, then back down in the next column. Message "Goodbye" (red) is placed in the transposed orientation, starting in the lower right corner, then moving to the left, then back to the right in the next row, then back to the left again.
This placement obviously causes conflicts between overlapping codewords. We resolve these conflicts somewhat arbitrarily: Green gets priority on its first codeword, thereby damaging two red codewords, while red gets priority on its last codeword, thereby damaging two green codewords. Both messages thus end up with two damaged codewords.
We will set up the Reed-Solomon codes such that the damaged codewords are repaired during decoding. Note that the error correction capacity of 2-L symbols can handle up to 4 errors per message. So it should be fine!
Double-sided Reed-Solomon codes
Both mirror variants of the symbol must represent a decodable Reed-Solomon code. Without it, QR decoders will refuse to decode the symbol at all. Furthermore, we actually need the error correcting properties of the Reed-Solomon code to repair the codewords that got damaged by conflicts between the messages.
We obtain valid Reed-Solomon codes by choosing suitable values for the unused pixels in the codeword region (the gray dots in Figure 4). The values of these pixels will not affect the contents of the encoded messages. Although the pixels may affect data codewords, those codewords are located after the end-of-message indicator, and therefore ignored by QR decoders.
Bit values for the unused pixels must be chosen such that the following two conditions are satisfied:
- The complete codeword sequence in the normal orientation, modified such that the green codewords take precedence over the red codewords in places of conflict, is a valid Reed-Solomon code.
- The complete codeword sequence in the transposed orientation, modified such that the red codewords take precedence over the green codewords in places of conflict, is a valid Reed-Solomon code.
These conditions ensure that both mirror variants of the symbol look like correctable Reed-Solomon codes to a QR decoder. The decoder will detect two damaged codewords in the lower right corner. After correcting these errors, it will recover either the undamaged green message or the undamaged red message.
Recall that the Reed-Solomon code is a bitwise linear code. This implies that the two conditions described above, can be stated as a system of linear equations over integers modulo 2. I use the following approach:
- Initialize the unused pixels to arbitrary values. You could make them all 0 or all 1 or anything you feel like. I prefer to initialize them to a checkerboard pattern for reasons discussed later.
- Create a variable xi for each unused pixel. If xi = 0, it means that the unused pixel keeps its initial value; xi = 1 means that the pixel value will be flipped.
- Create a linear equation for each bit of each syndrome of both messages.
(See [2] for an introduction to syndrome decoding.)
- The right-hand side of each equation is a single bit: the initial value of a specific bit in a specific syndrome for one of the two messages.
- The left-hand side of each equation is the sum of a subset of variables. These are the variables corresponding to the unused pixels that affect this equation's syndrome bit.
- To determine the right-hand side values for a particular message, extract the codeword sequence from the initial pixel values in the matrix, but substitute the undamaged data codewords in places where the messages overlap. Then calculate the syndromes for the resulting codeword sequence. Extract 8 bits from each syndrome and put them in the right-hand sides of the equations. For example, when calculating the syndromes for the green message, start with the undamaged green codewords, then continue extracting codewords from the pixel values in the symbol. Some of these pixels are part of red codewords, others are unused pixels. Similarly, when calculating syndromes for the red message, start with the undamaged red codewords, then continue extracting codewords from the transposed symbol.
- To determine which syndrome bits are affected by a specific unused pixel, consider a hypothetical symbol where all pixels have value
0
except this specific pixel which has value1
. Extract codewords from this hypothetical symbol and calculate the syndromes. The syndrome bits with value1
are precisely the bits that are affected by this specific pixel. The variable for this unused pixel appears only in the equations for those syndrome bits.
- Solve the system of linear equations. This can be done by Gauss elimination [3].
- Use the solution of the equations to determine the final values of the unused pixels.
If a pixel's variable has value
1
in the solution, the pixel must be flipped, otherwise it keeps its initial value.
For example, the 2-L symbol in Figure 4 has 247 unused pixels. Error correcting codes for 2-L symbols use 10 error correction words and therefore have 10 syndromes. Together, the two messages have 2 × 10 × 8 = 160 syndrome bits. We thus need to solve a system of 160 linear equations with 247 variables.
Note that the Reed-Solomon codes for the two messages are solved simultaneously in a shared system of equations. This is necessary because most pixels affect both messages. In contrast, separately solving the code for a single message would almost certainly cause the code in the other message to become invalid.
In general, the equations may have many solutions.
When this happens, my solver assigns value 0 to the free variables.
As a result, it is biased towards letting unused pixels keep their initially assigned value.
If the initial values of unused pixels are all-0
, it may cause large white regions in the final symbol, making the symbol difficult to decode.
To avoid this, I prefer to initialize the unused pixels to a checkerboard pattern.
Resolving practical issues
In some cases, the linear equations for the Reed-Solomon codes turn out to have no solution. This can happen even if there are more variables than equations, since variables can be redundant. So far, I have only seen this happen with version 5-L codes. When this happens, it can often be resolved by "sacrificing" a codeword. I do this by setting up 8 extra variables to describe the bits of a specific codeword in one of the messages. These variables appear only in the equations for one of the messages and are ignored for the other message. I ignore the solution of these variables. The extra variables effectively allow the solver to damage this selected codeword. The QR decoder will interpret this as just another codeword error, which it will correct without any issue as long as we don't exceed the error correction capacity of the code.
We have already seen that some QR code readers prefer to decode the mirrored symbol if it has fewer bit errors in the format information. I suspect that some QR code readers may also prefer the mirrored symbol if it has fewer errors in the Reed-Solomon code. To avoid such issues, I make sure that both mirror variants of the symbol have the same number of codeword errors.
Depending on the contents of messages, conflicts during codeword placement may produce different numbers of damaged codewords in the two messages. I fix this by intentionally damaging codewords (located past the end of the actual message content) until the number of errors is balanced. To selectively damage a codeword in one message, I feed an incorrect value for the initial value of that codeword into the procedure that sets up the linear equations. As a result, that codeword will not match the rest of the Reed-Solomon code, so it will look like a damaged codeword to the decoder. Another method to damage one codeword in one message is to flip some of the remaining pixels: the pixels, at most 7, which remain in the lower left corner of the codeword region after placing the last codeword. These pixels are ignored by one message, and can thus be flipped to selectively damage only the other message.
I have not yet discussed the role of the data mask pattern in double-sided QR codes. It turns out to play a fairly small role. Although the data mask pattern must be dealt with when codewords are placed in, or extracted from the codeword region, it does not otherwise affect the basic technique of placing conflicting messages and solving Reed-Solomon codes. Handling of the data mask pattern can be further simplified by making use of the fact that mask pattern 3 is symmetric under transposition.
Results
I implemented the method described above for QR code versions 2 to 5. Table 2 lists the maximum message length I achieved for double-sided QR codes of each version. There is still some margin in these numbers; the message lengths could be cranked up slightly at the cost of increased errors.
QR code version | max message length (bytes) |
errors per message |
---|---|---|
2-L | 10 | 3 |
3-L | 15 | 3 |
4-L | 32 | 6 |
5-L | 38 | 6 |
Whether double-sided QR codes work in practice depends on the algorithm used by the QR code reader to detect mirrored input images. I successfully tested double-sided QR codes with the following QR code readers:
- ZXing 3.5.3
- zxing-cpp 2.3.0
- ZBar 0.10
- ZBar 0.23.92
- iPhone iOS 18.5
Contributions
The very idea of a double-sided QR code was suggested to me by Sidney Cadot. He also provided important insights into methods for generating such codes.
The notion of double-sided QR codes came up while Sidney and I were discussing some ambiguous statements in the QR code standard about the handling of mirrored QR code symbols. I initially didn't believe it was possible to make double-sided codes. After pondering the issue for a while, I changed my mind and developed a proof-of-concept method to make them.
My first approach painstakingly avoided conflicts between the two messages. As a result, I was limited to very short messages of 4 or 5 characters. Sidney then suggested to use the error correcting capability of the QR code to deal with conflicts between the messages. This significantly increased the length of messages that can be encoded.
Related work
After developing the double-sided QR code generator, I discovered that Aleksey Tikhonov had already done similar work many years earlier [4] [5] . It looks like we independently converged on similar approaches.
References
-
ISO/IEC 18004:2024, QR code bar code symbology specification, 2024.
The document is behind a paywall, but older versions of this standard are easy to find on the public internet. -
Aleksey Tikhonov, "Double-sided QR-code", 2018.
https://medium.com/altsoph/double-sided-qr-code-c946468f05d4 -
Alexey Tikhonov, "On Double-Sided QR-Codes", SIGBOVIK 2019.
https://arxiv.org/abs/1902.05722