Generate a color QR code with a logo in Java

Some time ago, I needed to generate several QR codes for each user of the system. And to make it interesting to scan this code, it was decided to add a logo to it.
QR code example
How to do this, read on.


Foreword

QR codes can be found everywhere, but how to distinguish them from each other? QR codes are gaining popularity all the time, and no, no, and there are several pieces nearby. This is not a pleasant sight - which one to scan first? And anyway, why scan something that starts to ripple in your eyes?
Personalization of the QR code can serve as a solution to this problem: non-standard colors, a logo, or an explanatory inscription a little lower than the code itself, by which you can understand whether the viewer is interested or not.
Many have seen Navenyak beautiful QR codes (and whoever hasn’t seen it, can look at the Habr or on a third-party resource ), but I should make a reservation - to create one, you need to invest either large resources in the algorithm for generating the image, or draw such a code in Photoshop, but this will be a single instance, and for most of us it will not work (unless, of course, there is a need generate them yourself).

How is this implemented?

The creators of QR codes did not expect that we would insert our pictures in encoded messages, which are the codes themselves, but they provided for the possibility of a high volume of recovery information - the code can contain up to 30% of the latter. The larger it is, the thicker the picture, but more likely that the user will decode the damaged code. And we will spoil it with a logo.
To generate the code, the ZXing library was used - it is an open source library for processing various 1D / 2D barcodes, which, in addition to Java, has ports for other languages.
A feature of this library is that it is divided into modules and distributed in the source codes that need to be compiled. But, fortunately, it is in the maven repository - the core module was used for generation, and the java se module was used for code validation.
To work with graphics, standard classes from the java.awt package (JavaSE) were used.

To the cause!

For experiments, a small console program was made, which can be found on the github - a prototype repository , which I will discuss in this section.

Anyone who just needs a QR code can write the following:
BitMatrix matrix = new MultiFormatWriter().encode("text to encode", BarcodeFormat.QR_CODE, width, height);
MatrixToImageWriter.writeToFile(matrix, filename.substring(filename.lastIndexOf('.')+1), new File(filename));

Otherwise, you should not do this - by default, the library will add a little restorative information, and even if after inserting the logo the picture is decrypted on our computer, then it may already be considered incorrect from the camera. Therefore, it will be a good form to add a maximum of recovery information, and since we will change the colors and the picture, we don’t need to rush to save the result:
Hashtable<EncodeHintType, ErrorCorrectionLevel> hintMap = new Hashtable<EncodeHintType, ErrorCorrectionLevel>();
hintMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);

QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, qrCodeSize, qrCodeSize, hintMap);

Creating a picture from the code matrix is ​​done in a cycle - we create a picture of the appropriate size and, passing the code matrix, display the presence of a bit in the matrix on the picture as an informative pixel. During this action, you can set the background color and code color:
int matrixWidth = bitMatrix.getWidth();
BufferedImage image = new BufferedImage(matrixWidth, matrixWidth, BufferedImage.TYPE_INT_RGB);
image.createGraphics();
Graphics2D graphics = (Graphics2D) image.getGraphics();

graphics.setColor(Color.white);
graphics.fillRect(0, 0, matrixWidth, matrixWidth);

Color mainColor = new Color(51, 102, 153);
graphics.setColor(mainColor);
 
//Write Bit Matrix as image
for (int i = 0; i < matrixWidth; i++) {
	for (int j = 0; j < matrixWidth; j++) {
		if (bitMatrix.get(i, j)) {
			graphics.fillRect(i, j, 1, 1);
		}
	}
}

Well, now that we are operating with a picture, and not a matrix of ones and zeros, it’s very convenient for us to put the logo in the center, having previously adjusted its resolution so as not to overlap the entire code in case of too large a size:
BufferedImage logo = ImageIO.read( this.getLogoFile());
double scale = calcScaleRate(image, logo);
logo = getScaledImage( logo,
		(int)( logo.getWidth() * scale),
		(int)( logo.getHeight() * scale) );
graphics.drawImage( logo,
		image.getWidth()/2 - logo.getWidth()/2,
		image.getHeight()/2 - logo.getHeight()/2,
		image.getWidth()/2 + logo.getWidth()/2,
		image.getHeight()/2 + logo.getHeight()/2,
		0, 0, logo.getWidth(), logo.getHeight(), null);

private BufferedImage getScaledImage(BufferedImage image, int width, int height) throws IOException {
	int imageWidth  = image.getWidth();
	int imageHeight = image.getHeight();

	double scaleX = (double)width/imageWidth;
	double scaleY = (double)height/imageHeight;
	AffineTransform scaleTransform = AffineTransform.getScaleInstance(scaleX, scaleY);
	AffineTransformOp bilinearScaleOp = new AffineTransformOp( scaleTransform, AffineTransformOp.TYPE_BILINEAR);

	return bilinearScaleOp.filter( image, new BufferedImage(width, height, image.getType()));
}

After our abuse of the code, you should definitely check it for correctness - is there enough recovery information for an ideal camera? And if that's enough, then it's time to save the katinka and give it to the user:
if ( isQRCodeCorrect(content, image)) {
	ImageIO.write(image, imageFormat, this.getGeneratedFileStream());
}

private boolean isQRCodeCorrect(String content, BufferedImage image){
	boolean result = false;
	Result qrResult = decode(image);
	if (qrResult != null && content != null && content.equals(qrResult.getText())){
		result = true;
	}		
	return result;
}

private Result decode(BufferedImage image){
	if (image == null) {
		return null;
	}
	try {
		LuminanceSource source = new BufferedImageLuminanceSource(image);	      
		BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));	      
		Result result = new MultiFormatReader().decode(bitmap, Collections.EMPTY_MAP);	      
		return result;
	} catch (NotFoundException nfe) {
		return null;
	}
}


The goal is achieved - a QR code is generated. Thanks for your attention!