Генерируем цветной QR-код с логотипом на Java

в 6:22, , рубрики: java, QR code, zxing, метки: , ,

Некоторое время назад мне потребовалось генерировать несколько QR-кодов для каждого пользователя системы. А чтобы было интересно сканировать этот код, было решено добавить в него логотип.
QR code example
Как это сделать читайте дальше.

Предисловие

QR-коды можно встретить везде, но как их отличить друг от друга? QR-коды всё время завоёвывают популярность, и нет-нет, да и встретятся несколько штук рядом. Зрелище это не из приятных — какой сканировать первым? И вообще, зачем сканировать то, от чего начинает рябить в глазах?
Решением такой задачи может служить персонализирование QR-кода: нестандартные цвета, логотип, или пояснительная надпись немного ниже самого кода, по которой можно понять — интересно ли зрителю это или нет.
Навеняка многие видели красивые QR-коды (а кто не видел, может посмотреть), но мне стоит оговориться — чтобы создать такой, нужно вложить либо большие ресурсы в алгоритм генерации картинки, либо нарисовать такой код в фотошопе, но это будет единичный экземпляр, и для большинства из нас он не годится (если, конечно, есть вообще необходимость генерировать их самому).

Как это реализовано?

Создатели QR-кодов не расчитывали, что мы будем вставлять свои картинки в закодированные сообщения, коими являются сами коды, но они предусмотрели возможность высокого объёма восстановительной информации — код может содержать до 30% последней. Чем её больше — тем гуще картинка, но больше шансов, что пользователь раскодирует испорченный код. А портить мы его будем логотипом.
Для генерации кода использовалась библиотека ZXing — это open source библиотека для обработки различных 1D/2D штрихкодов, которая, кроме Java, имеет порты на другие языки.
Особенностью этой библиотеки является то, что она разбита на модули и распространяется в исходных кодах, которые необходимо компилировать. Но, к счастью, она есть в мавен репозитории — модуль core использовался для генерации, и модуль java se использовался для валидации кодов.
Для работы с графикой были использованы стандартные классы из пакета java.awt (JavaSE).

За дело!

Для экспериментов была сделана небольшую консольная программа, которую можно найти на гитхабе — репозиторий опытного образца, которую я и разберу в этом разделе.

Тот, кому просто нужен QR-код, может написать следующее:

BitMatrix matrix = new MultiFormatWriter().encode("text to encode", BarcodeFormat.QR_CODE, width, height);
MatrixToImageWriter.writeToFile(matrix, filename.substring(filename.lastIndexOf('.')+1), new File(filename));

В противном случае так делать не стоит — по умолчанию библиотека добавит мало восстановительной информации, и даже если после вставки логотипа картинка расшифруется у нас на компьютере, то с фотокамеры она уже может считаться неправильно. По-этому будет хорошим тоном добавить максимум восстановительной информации, а раз мы будем изменять цвета и катинку, то не нужно спешить сохранять результат:

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);

Создание картинки из матрицы кода делается в цикле — создаём картинку соответствующего размера и, проходя матрицу кода, отображаем наличие бита в матрице на картинку как информативный пиксель. Во время этого действа можно задать цвет фона и цвет кода:

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);
		}
	}
}

Ну вот, теперь, когда мы оперируем картинкой, а не матицей единиц и нулей, нам очень даже удобно и логотип в центр поместить, предварительно поправив его разрешение, чтобы не перекрывать весь код в случае слишком большого размера:

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()));
}

После нашего надругательства над кодом, обязательно стоит его проверить на правильность — хватит ли восстановительной информации для идеальной фотокамеры? И если хватит, то пора сохранить катинку и отдать её пользователю:

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;
	}
}

Поставленная цель достигнута — QR код сгенерирован. Спасибо за внимание!

Автор: valkiriy

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js