All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.alg.fiducial.qrcode.QrCodeDecoderBits Maven / Gradle / Ivy

/*
 * Copyright (c) 2023, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.alg.fiducial.qrcode;

import boofcv.misc.BoofMiscOps;
import lombok.Getter;
import lombok.Setter;
import org.ddogleg.struct.DogArray_I8;
import org.ddogleg.struct.VerbosePrint;
import org.jetbrains.annotations.Nullable;

import java.io.PrintStream;
import java.util.Objects;
import java.util.Set;

import static boofcv.alg.fiducial.qrcode.EciEncoding.getEciCharacterSet;
import static boofcv.alg.fiducial.qrcode.QrCodeCodecBitsUtils.flipBits8;

/**
 * After the data bits have been read this will decode them and extract a meaningful message.
 *
 * @author Peter Abeles
 */
public class QrCodeDecoderBits implements VerbosePrint {

	// used to compute error correction
	ReedSolomonCodes_U8 rscodes = new ReedSolomonCodes_U8(8, 0b100011101, 0);
	// storage for the data message
	DogArray_I8 message = new DogArray_I8();
	// storage fot the message's ecc
	DogArray_I8 ecc = new DogArray_I8();

	// Specified ECI encoding
	@Nullable String encodingEci;

	final QrCodeCodecBitsUtils utils;

	// Number of errors it found while applying error correction
	int totalErrorBits;

	/**
	 * If true to it won't check the value of padding bytes. Run into an off by one error
	 */
	public @Getter @Setter boolean ignorePaddingBytes = false;

	@Nullable PrintStream verbose = null;

	/**
	 * @param forceEncoding If null then the default byte encoding is used. If not null then the specified
	 * encoding is used.
	 */
	public QrCodeDecoderBits( @Nullable String forceEncoding, String defaultEncoding ) {
		this.utils = new QrCodeCodecBitsUtils(forceEncoding, defaultEncoding);
	}

	/**
	 * Reconstruct the data while applying error correction.
	 */
	public boolean applyErrorCorrection( QrCode qr ) {
//		System.out.println("decoder ver   "+qr.version);
//		System.out.println("decoder mask  "+qr.mask);
//		System.out.println("decoder error "+qr.error);

		QrCode.VersionInfo info = QrCode.VERSION_INFO[qr.version];
		QrCode.BlockInfo block = Objects.requireNonNull(info.levels.get(qr.error));

		int wordsBlockAllA = block.codewords;
		int wordsBlockDataA = block.dataCodewords;
		int wordsEcc = wordsBlockAllA - wordsBlockDataA;
		int numBlocksA = block.blocks;

		int wordsBlockAllB = wordsBlockAllA + 1;
		int wordsBlockDataB = wordsBlockDataA + 1;
		int numBlocksB = (info.codewords - wordsBlockAllA*numBlocksA)/wordsBlockAllB;

		int totalBlocks = numBlocksA + numBlocksB;
		int totalDataBytes = wordsBlockDataA*numBlocksA + wordsBlockDataB*numBlocksB;
		qr.corrected = new byte[totalDataBytes];

		ecc.resize(wordsEcc);
		rscodes.generator(wordsEcc);

		totalErrorBits = 0;
		if (!decodeBlocks(qr, wordsBlockDataA, numBlocksA, 0, 0, totalDataBytes, totalBlocks))
			return false;

		if (!decodeBlocks(qr, wordsBlockDataB, numBlocksB, numBlocksA*wordsBlockDataA, numBlocksA, totalDataBytes, totalBlocks))
			return false;

		qr.totalBitErrors = totalErrorBits;
		return true;
	}

	private boolean decodeBlocks( QrCode qr, int bytesInDataBlock, int numberOfBlocks, int bytesDataRead,
								  int offsetBlock, int offsetEcc, int stride ) {
		message.resize(bytesInDataBlock);

		for (int idxBlock = 0; idxBlock < numberOfBlocks; idxBlock++) {
			copyFromRawData(qr.rawbits, message, ecc, offsetBlock + idxBlock, stride, offsetEcc);

			flipBits8(message);
			flipBits8(ecc);

			if (!rscodes.correct(message, ecc)) {
				return false;
			}
			totalErrorBits += rscodes.getTotalErrors();

			flipBits8(message);
			System.arraycopy(message.data, 0, qr.corrected, bytesDataRead, message.size);
			bytesDataRead += message.size;
		}
		return true;
	}

	private void copyFromRawData( byte[] input, DogArray_I8 message, DogArray_I8 ecc,
								  int offsetBlock, int stride, int offsetEcc ) {
		for (int i = 0; i < message.size; i++) {
			message.data[i] = input[i*stride + offsetBlock];
		}
		for (int i = 0; i < ecc.size; i++) {
			ecc.data[i] = input[i*stride + offsetBlock + offsetEcc];
		}
	}

	/**
	 * Decodes the message encoded in the corrects bits inside the QR. Results are written to the input QR.
	 *
	 * @return true if no error occurred
	 */
	public boolean decodeMessage( QrCode qr ) {
		if (verbose != null) verbose.println("decodeMessage: qr.corner=" + qr.ppCorner.get(0));
		encodingEci = null;

		utils.workString.setLength(0);

		// if there isn't enough bits left to read the mode it must be done
		try {
			int location = decideMessageBits(qr);
			if (location < 0)
				return false;

			// ensure the length is byte aligned
			location = alignToBytes(location);
			int lengthBytes = location/8;

			// At this point, even if fails, return whatever it was able to decode
			qr.message = utils.workString.toString();

			// sanity check padding
			if (!ignorePaddingBytes && !checkPaddingBytes(qr, lengthBytes)) {
				qr.failureCause = QrCode.Failure.READING_PADDING;
				return false;
			}

			return true;
		} catch (RuntimeException e) {
			if (verbose != null) {
				e.printStackTrace(verbose);
			}
			qr.failureCause = QrCode.Failure.DECODING_MESSAGE;
			qr.message = utils.workString.toString();
			return false;
		}
	}

	/**
	 * Decodes the bits and converts into a message and returns the number of bits processed or -1 if it failed
	 */
	private int decideMessageBits( QrCode qr ) {
		qr.byteEncoding = "";

		var bits = new PackedBits8();
		bits.data = qr.corrected;
		bits.size = qr.corrected.length*8;

		int location = 0;
		while (location + 4 <= bits.size) {
			int modeBits = bits.read(location, 4, true);
			location += 4;
			if (modeBits == 0) { // escape indicator
				if (verbose != null) verbose.printf("bit_loc=%d/%d escape mode\n", location, bits.size);
				break;
			}
			QrCode.Mode mode = QrCode.Mode.lookup(modeBits);

			if (verbose != null) verbose.printf("bit_loc=%d/%d mode=%s\n", location, bits.size, mode);

			qr.mode = updateModeLogic(qr.mode, mode);
			switch (mode) {
				case NUMERIC -> location = decodeNumeric(qr, bits, location);
				case ALPHANUMERIC -> location = decodeAlphanumeric(qr, bits, location);
				case BYTE -> {
					location = decodeByte(qr, bits, location);
					// Only record the encoding of the first BYTE segment encountered
					if (qr.byteEncoding.isEmpty()) {
						qr.byteEncoding = utils.selectedByteEncoding;
					}
				}
				case KANJI -> location = decodeKanji(qr, bits, location);
				case ECI -> location = decodeEci(bits, location);
				case FNC1_FIRST, FNC1_SECOND -> {
					// This isn't the proper way to handle this mode, but it
					// should still parse the data
				}
				default -> {
					qr.failureCause = QrCode.Failure.UNKNOWN_MODE;
					return -1;
				}
			}

			if (location < 0) {
				qr.failureCause = utils.failureCause;
				return -1;
			}
		}
		return location;
	}

	/**
	 * If only one mode then that mode is used. If more than one mode is used then set to multiple
	 */
	private QrCode.Mode updateModeLogic( QrCode.Mode current, QrCode.Mode candidate ) {
		if (current == candidate)
			return current;
		else if (current == QrCode.Mode.UNKNOWN) {
			return candidate;
		} else {
			return QrCode.Mode.MIXED;
		}
	}

	public static int alignToBytes( int lengthBits ) {
		return lengthBits + (8 - lengthBits%8)%8;
	}

	/**
	 * Makes sure the used bytes have the expected values
	 *
	 * @param lengthBytes Number of bytes that data should be been written to and not filled with padding.
	 */
	boolean checkPaddingBytes( QrCode qr, int lengthBytes ) {
		boolean a = true;

		for (int i = lengthBytes; i < qr.corrected.length; i++) {
			if (a) {
				if (0b00110111 != (qr.corrected[i] & 0xFF))
					return false;
			} else {
				if (0b10001000 != (qr.corrected[i] & 0xFF)) {
					// the pattern starts over at the beginning of a block. Strictly enforcing the standard
					// requires knowing size of a data chunk and where it starts. Possible but
					// probably not worth the effort the implement as a strict requirement.
					if (0b00110111 == (qr.corrected[i] & 0xFF)) {
						a = true;
					} else {
						return false;
					}
				}
			}
			a = !a;
		}

		return true;
	}

	/**
	 * Decodes a numeric message
	 *
	 * @param qr QR code
	 * @param data encoded data
	 * @return Location it has read up to in bits
	 */
	private int decodeNumeric( QrCode qr, PackedBits8 data, int bitLocation ) {
		int lengthBits = QrCodeEncoder.getLengthBitsNumeric(qr.version);
		return utils.decodeNumeric(data, bitLocation, lengthBits);
	}

	/**
	 * Decodes alphanumeric messages
	 *
	 * @param qr QR code
	 * @param data encoded data
	 * @return Location it has read up to in bits
	 */
	private int decodeAlphanumeric( QrCode qr, PackedBits8 data, int bitLocation ) {
		int lengthBits = QrCodeEncoder.getLengthBitsAlphanumeric(qr.version);
		return utils.decodeAlphanumeric(data, bitLocation, lengthBits);
	}

	/**
	 * Decodes byte messages
	 *
	 * @param qr QR code
	 * @param data encoded data
	 * @return Location it has read up to in bits
	 */
	private int decodeByte( QrCode qr, PackedBits8 data, int bitLocation ) {
		int lengthBits = QrCodeEncoder.getLengthBitsBytes(qr.version);
		utils.encodingEci = this.encodingEci;
		return utils.decodeByte(data, bitLocation, lengthBits);
	}

	/**
	 * Decodes Kanji messages
	 *
	 * @param qr QR code
	 * @param data encoded data
	 * @return Location it has read up to in bits
	 */
	private int decodeKanji( QrCode qr, PackedBits8 data, int bitLocation ) {
		int lengthBits = QrCodeEncoder.getLengthBitsKanji(qr.version);
		return utils.decodeKanji(data, bitLocation, lengthBits);
	}

	/**
	 * Decodes Extended Channel Interpretation (ECI) Mode. Allows character set to be changed
	 *
	 * @param data encoded data
	 * @return Location it has read up to in bits
	 */
	int decodeEci( PackedBits8 data, int bitLocation ) {
		// NOTE: I'm having trouble testing this code. Just finding an encoding which will do ECI is difficult
		//       almost all use UTF-8 by default and that supports a lot of characters

		// number of 1 bits before first 0 define number of additional codewords
		int firstByte = data.read(bitLocation, 8, true);
		bitLocation += 8;

		int numCodeWords = 1;
		while ((firstByte & (1 << (7 - numCodeWords))) != 0) {
			numCodeWords++;
		}
		// trip the bits that indicate the number of code words
		if (numCodeWords > 1) {
			firstByte <<= numCodeWords - 1;
			firstByte >>= numCodeWords - 1;
		}

		// read the 6-digit designator
		int assignmentValue = firstByte;
		for (int i = 1; i < numCodeWords; i++) {
			assignmentValue <<= 8;
			assignmentValue |= data.read(bitLocation, 8, true);
			bitLocation += 8;
		}

		encodingEci = getEciCharacterSet(assignmentValue);

		return bitLocation;
	}

	@Override public void setVerbose( @Nullable PrintStream out, @Nullable Set configuration ) {
		this.verbose = BoofMiscOps.addPrefix(this, out);
		BoofMiscOps.verboseChildren(verbose, configuration, utils);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy