├── src ├── test │ ├── resources │ │ └── images │ │ │ └── skull_bw.png │ └── java │ │ └── org │ │ └── skrymer │ │ └── qrbuilder │ │ ├── TestHelpers.java │ │ ├── QRCodeColoringTest.java │ │ ├── QRCodeOverlayTest.java │ │ └── QRCodeTest.java └── main │ └── java │ └── org │ └── skrymer │ └── qrbuilder │ ├── decorator │ ├── Decorator.java │ ├── ColoredQRCode.java │ └── ImageOverlay.java │ ├── exception │ ├── InvalidSizeException.java │ ├── CouldNotCreateQRCodeException.java │ ├── CouldNotCreateFileException.java │ └── UnreadableDataException.java │ ├── util │ ├── SyntacticSugar.java │ └── ImageUtils.java │ ├── Main.java │ └── QRCode.java ├── .gitignore ├── pom.xml └── README.md /src/test/resources/images/skull_bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrymer/qrbuilder/HEAD/src/test/resources/images/skull_bw.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | qrCode.png 4 | target/ 5 | 6 | pom*.xml 7 | 8 | .classpath 9 | .project 10 | org* 11 | 12 | .gitignore_* 13 | 14 | .vscode/** -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/decorator/Decorator.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder.decorator; 2 | 3 | /** 4 | * Implement this interface to create custom decorators 5 | */ 6 | public interface Decorator { 7 | 8 | T decorate(T qrcode); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/exception/InvalidSizeException.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder.exception; 2 | 3 | public class InvalidSizeException extends RuntimeException { 4 | 5 | public InvalidSizeException(String message){ 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/exception/CouldNotCreateQRCodeException.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder.exception; 2 | 3 | public class CouldNotCreateQRCodeException extends RuntimeException { 4 | 5 | public CouldNotCreateQRCodeException(String message, Throwable cause){ 6 | super(message, cause); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/exception/CouldNotCreateFileException.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder.exception; 2 | 3 | /** 4 | * {@link CouldNotCreateFileException} 5 | */ 6 | public class CouldNotCreateFileException extends RuntimeException { 7 | public CouldNotCreateFileException(String message, Throwable cause) { 8 | super(message, cause); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/exception/UnreadableDataException.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder.exception; 2 | 3 | public class UnreadableDataException extends RuntimeException { 4 | 5 | public UnreadableDataException(String msg, Throwable cause){ 6 | super(msg, cause); 7 | } 8 | 9 | public UnreadableDataException(String msg) { 10 | super(msg); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/util/SyntacticSugar.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder.util; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | /** 6 | * Some syntactic sugar 7 | */ 8 | public class SyntacticSugar { 9 | 10 | public static void throwIllegalArgumentExceptionIfEmpty(String parameter, String parameterName) { 11 | if (StringUtils.isEmpty(parameter)) { 12 | throw new IllegalArgumentException("Parameter " + parameter + " cannot be empty"); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/util/ImageUtils.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder.util; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | public class ImageUtils { 7 | 8 | public static BufferedImage imageToBufferedImage(Image image) { 9 | BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB); 10 | Graphics2D g2 = bufferedImage.createGraphics(); 11 | g2.drawImage(image, 0, 0, null); 12 | g2.dispose(); 13 | 14 | return bufferedImage; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/org/skrymer/qrbuilder/TestHelpers.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder; 2 | 3 | import com.google.zxing.BinaryBitmap; 4 | import com.google.zxing.DecodeHintType; 5 | import com.google.zxing.Result; 6 | import com.google.zxing.client.j2se.BufferedImageLuminanceSource; 7 | import com.google.zxing.common.HybridBinarizer; 8 | import com.google.zxing.qrcode.QRCodeReader; 9 | 10 | import java.awt.image.BufferedImage; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | /** 15 | * Created by skrymer on 30/04/17. 16 | */ 17 | public class TestHelpers { 18 | public static String decode(BufferedImage qrcode) throws Exception{ 19 | BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(qrcode))); 20 | Map decodeHints = new HashMap(); 21 | decodeHints.put(DecodeHintType.PURE_BARCODE, Boolean.TRUE); 22 | 23 | Result result = new QRCodeReader().decode(binaryBitmap, decodeHints); 24 | 25 | return result.getText(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/decorator/ColoredQRCode.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder.decorator; 2 | 3 | import org.skrymer.qrbuilder.util.ImageUtils; 4 | 5 | import java.awt.*; 6 | import java.awt.image.*; 7 | 8 | /** 9 | * Decorator that colors a qrcode 10 | */ 11 | public class ColoredQRCode implements Decorator { 12 | private Color color; 13 | 14 | /** 15 | * Colors the qrcode with the given color 16 | * @param color the color 17 | * @return this 18 | */ 19 | public static Decorator colorizeQRCode(Color color){ 20 | return new ColoredQRCode(color); 21 | } 22 | 23 | /** 24 | * @param color the color 25 | */ 26 | private ColoredQRCode(Color color) { 27 | this.color = color; 28 | } 29 | 30 | /** 31 | * Colors the given qrcode 32 | * @param qrcode the qrcode to color 33 | * @return 34 | */ 35 | public BufferedImage decorate(BufferedImage qrcode) { 36 | FilteredImageSource prod = new FilteredImageSource(qrcode.getSource(), new QRCodeRGBImageFilter()); 37 | 38 | return ImageUtils.imageToBufferedImage(Toolkit.getDefaultToolkit().createImage(prod)); 39 | } 40 | 41 | private class QRCodeRGBImageFilter extends RGBImageFilter { 42 | public int filterRGB(int x, int y, int rgb) { 43 | if(rgb == Color.black.getRGB()) 44 | return color.getRGB(); 45 | 46 | return rgb; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/org/skrymer/qrbuilder/QRCodeColoringTest.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder; 2 | 3 | import java.awt.Color; 4 | import java.awt.image.BufferedImage; 5 | 6 | import org.skrymer.qrbuilder.decorator.ColoredQRCode; 7 | import org.testng.Assert; 8 | import org.testng.annotations.Test; 9 | 10 | /** 11 | * Created by skrymer on 30/04/17. 12 | */ 13 | @Test 14 | public class QRCodeColoringTest { 15 | 16 | @Test(invocationCount=10) 17 | public void whenColoringAQRCode_thenQRCodeShouldBeReadable() throws Exception { 18 | BufferedImage qrcode = QRCode.ZXingBuilder.build(builder -> 19 | builder.withSize(500, 500) 20 | .and() 21 | .withData("the ninjas are coming") 22 | .and() 23 | .withDecorator(ColoredQRCode.colorizeQRCode(Color.RED)) 24 | ).toImage(); 25 | 26 | Assert.assertEquals("the ninjas are coming", TestHelpers.decode(qrcode)); 27 | } 28 | 29 | @Test(invocationCount=10) 30 | public void whenBuildingRedQRCode_thenQRCodeShouldBeRed() throws Exception { 31 | BufferedImage qrcode = QRCode.ZXingBuilder.build(builder -> 32 | builder.withSize(500, 500) 33 | .and() 34 | .withData("the ninjas are coming") 35 | .and() 36 | .withDecorator(ColoredQRCode.colorizeQRCode(Color.RED)) 37 | ).toImage(); 38 | // qrcode.getColorModel().getGreen() 39 | qrcode.getRGB(0,0); 40 | Assert.assertEquals("the ninjas are coming", TestHelpers.decode(qrcode)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/Main.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder; 2 | 3 | import org.skrymer.qrbuilder.decorator.ColoredQRCode; 4 | import org.skrymer.qrbuilder.decorator.ImageOverlay; 5 | 6 | import javax.imageio.ImageIO; 7 | import java.awt.*; 8 | import java.awt.image.BufferedImage; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.nio.charset.Charset; 12 | 13 | public class Main { 14 | public static final float TRANSPARENCY = 0.20f; 15 | public static final float OVERLAY_RATIO = 1f; 16 | public static final int WIDTH = 250; 17 | public static final int HEIGHT = 250; 18 | 19 | public static void main(String[] args) throws Exception { 20 | QRCode.ZXingBuilder.build(builder -> 21 | builder 22 | .withSize(WIDTH, HEIGHT) 23 | .and() 24 | .withData("One day, lad, all this will be yours. What, the curtains?") 25 | .and() 26 | .withDecorator(ColoredQRCode.colorizeQRCode(Color.green.darker())) 27 | .and() 28 | .withDecorator(ImageOverlay.addImageOverlay(readImage("src/test/resources/images/skull_bw.png"), TRANSPARENCY, OVERLAY_RATIO)) 29 | .and() 30 | .withCharSet(Charset.forName("UTF-8")) 31 | .verify(true) 32 | 33 | ).toFile("./qrCode.png", "PNG"); 34 | } 35 | 36 | public static BufferedImage readImage(String path) { 37 | try { 38 | return ImageIO.read(new File(path)); 39 | } catch (IOException e) { 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | org.skrymer.qrbuilder 5 | qrbuilder 6 | 0.1 7 | jar 8 | qrbuilder 9 | https://github.com/skrymer/qrbuilder 10 | 11 | 12 | 13 | 14 | org.apache.maven.plugins 15 | maven-compiler-plugin 16 | 3.8.0 17 | 18 | 11 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | org.apache.commons 27 | commons-lang3 28 | 3.4 29 | 30 | 31 | 32 | 33 | 34 | com.google.zxing 35 | core 36 | 3.4.1 37 | 38 | 39 | 40 | 41 | com.google.zxing 42 | javase 43 | 3.4.1 44 | 45 | 46 | 47 | 48 | org.testng 49 | testng 50 | 6.8 51 | test 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/test/java/org/skrymer/qrbuilder/QRCodeOverlayTest.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.File; 5 | import java.io.IOException; 6 | 7 | import javax.imageio.ImageIO; 8 | 9 | import org.skrymer.qrbuilder.decorator.ImageOverlay; 10 | import org.skrymer.qrbuilder.exception.UnreadableDataException; 11 | import org.testng.Assert; 12 | import org.testng.annotations.Test; 13 | 14 | /** 15 | * QrCode overlay tests 16 | */ 17 | @Test 18 | public class QRCodeOverlayTest { 19 | 20 | @Test(invocationCount=10) 21 | public void whenBuildingQrCodeWithOverlay_thenQRCodeDataShouldBeReadable() throws Exception { 22 | BufferedImage qrcode = QRCode.ZXingBuilder.build(builder -> 23 | builder.withSize(250, 250) 24 | .and() 25 | .withData("and time began with a bang") 26 | .and() 27 | .withDecorator(ImageOverlay.addImageOverlay(getOverlay(), 1.0f, 0.25f)) 28 | ).toImage(); 29 | 30 | Assert.assertEquals("and time began with a bang", TestHelpers.decode(qrcode)); 31 | } 32 | 33 | @Test(expectedExceptions=UnreadableDataException.class) 34 | public void whenOverlayRatioIsToBig_thenThrowUnreadableDataException() throws Exception { 35 | QRCode.ZXingBuilder.build(builder -> 36 | builder.withSize(250, 250) 37 | .and() 38 | .withData("Some data") 39 | .and() 40 | .withDecorator(ImageOverlay.addImageOverlay(getOverlay(), 1.0f, 0.35f)) 41 | ).toImage(); 42 | } 43 | 44 | private BufferedImage getOverlay() { 45 | try { 46 | return ImageIO.read(new File("src/test/resources/images/skull_bw.png")); 47 | } catch (IOException e) { 48 | throw new RuntimeException(e); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a simple qrcode builder that is build ontop of the awesome ZXing library for barcode generation: https://github.com/zxing/zxing 2 | 3 | QRCodeBuilder 4 | ========= 5 | 6 | The builder is very simple to use, as the following example will shows. 7 | 8 | Create a QRCode with dimensions 250*250, a image overlay and some data: 9 | 10 | ```java 11 | package org.skrymer.qrbuilder; 12 | 13 | import javax.imageio.ImageIO; 14 | import java.awt.*; 15 | import java.awt.image.BufferedImage; 16 | import java.io.File; 17 | import java.io.IOException; 18 | 19 | import static org.skrymer.qrbuilder.decorator.ImageOverlay.*; 20 | import static org.skrymer.qrbuilder.decorator.ColoredQRCode.colorizeQRCode; 21 | 22 | public class Main { 23 | static final float TRANSPARENCY = 0.25f; 24 | static final float OVERLAY_RATIO = 1f; 25 | static final int WIDTH = 250; 26 | static final int HEIGHT = 250; 27 | 28 | public static void main(String[] args) { 29 | QRCode.ZXingBuilder.build(builder -> 30 | builder.withSize(WIDTH, HEIGHT) 31 | .and() 32 | .withData("The answer is 42") 33 | .and() 34 | .withDecorator(colorizeQRCode(Color.green.darker())) 35 | .and() 36 | .withDecorator(addImageOverlay(readImage("src/test/resources/images/skull_bw.png"), TRANSPARENCY, OVERLAY_RATIO)) 37 | .and() 38 | .doVerify(true) 39 | 40 | ).toFile("./qrCode.png", "PNG"); 41 | } 42 | 43 | static BufferedImage readImage(String path) { 44 | try { 45 | return ImageIO.read(new File(path)); 46 | } catch (IOException e) { 47 | throw new RuntimeException(e); 48 | } 49 | } 50 | } 51 | ``` 52 | The following qrCode is then generated: 53 | 54 | ![alt text](https://raw.github.com/wiki/skrymer/qrbuilder/images/qrcode.png "QRCode") 55 | 56 | ## Decorators 57 | 58 | The builder uses the decorators to decorate(obviously) the generated QRCode. 59 | 60 | Decorators currently available: 61 | * ImageOverlay 62 | * ColoredQRCode 63 | 64 | You can create new Decorators by implementing the Decorator interface 65 | -------------------------------------------------------------------------------- /src/test/java/org/skrymer/qrbuilder/QRCodeTest.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder; 2 | 3 | import org.skrymer.qrbuilder.exception.InvalidSizeException; 4 | import org.testng.Assert; 5 | import org.testng.annotations.Test; 6 | 7 | import java.awt.image.BufferedImage; 8 | 9 | import static org.testng.Assert.assertEquals; 10 | 11 | /** 12 | * Tests for class ZXingBuilder 13 | */ 14 | @Test 15 | public class QRCodeTest { 16 | 17 | @Test(invocationCount=10) 18 | public void whenBuildingSimpleQrCode_thenEncodedDataIsAsExpected() throws Exception { 19 | BufferedImage qrcode = QRCode.ZXingBuilder.build(builder -> 20 | builder.withSize(250, 250) 21 | .and() 22 | .withData("the answer to everything is 42") 23 | ).toImage(); 24 | 25 | Assert.assertEquals("the answer to everything is 42", TestHelpers.decode(qrcode)); 26 | } 27 | 28 | @Test(invocationCount=10) 29 | public void whenBuildingSimpleQrCode_thenWidthIsAsExpected() throws Exception { 30 | BufferedImage qrcode = QRCode.ZXingBuilder.build(builder -> 31 | builder.withSize(200, 250) 32 | .and() 33 | .withData("To be or not to be that is...") 34 | ).toImage(); 35 | 36 | assertEquals(200, qrcode.getWidth()); 37 | } 38 | 39 | @Test(invocationCount=10) 40 | public void whenBuildingSimpleQrCode_thenHeightIsAsExpected() throws Exception { 41 | BufferedImage qrcode = QRCode.ZXingBuilder.build(builder -> 42 | builder.withSize(300, 300) 43 | .and() 44 | .withData("Daffy duck is awesome") 45 | ).toImage(); 46 | 47 | assertEquals(300, qrcode.getHeight()); 48 | } 49 | 50 | @Test(expectedExceptions=InvalidSizeException.class) 51 | public void whenWidthIsZero_thenThrowCouldNotCreateQRCodeException(){ 52 | QRCode.ZXingBuilder.build(builder -> 53 | builder.withSize(0, 1) 54 | ).toImage(); 55 | } 56 | 57 | @Test(expectedExceptions=InvalidSizeException.class) 58 | public void whenHeightIsZero_thenThrowCouldNotCreateQRCodeException(){ 59 | QRCode.ZXingBuilder.build(builder -> 60 | builder.withSize(1, 0) 61 | ).toImage(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/decorator/ImageOverlay.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder.decorator; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | /** 7 | * Adds a image overlay to a qrcode 8 | */ 9 | public class ImageOverlay implements Decorator { 10 | public static final Float DEFAULT_OVERLAY_TRANSPARENCY = 1f; 11 | public static final Float DEFAULT_OVERLAY_TO_QRCODE_RATIO = 0.25f; 12 | 13 | private BufferedImage overlay; 14 | private Float overlayToQRCodeRatio, overlayTransparency; 15 | 16 | /** 17 | * @param overlay - the image to be over rendered on top of the qrcode 18 | * 19 | * 20 | * @param overlayTransparency - the overlays transparency from 0..1 where one is no transparency. 21 | * Default is set to 1 22 | * 23 | * 24 | * @param overlayToQRCodeRatio - Specifies the ratio between the overlay image and the QRCode in percentage like 0.20 = 20%. 25 | * Overlays should as a guide not take up more 25% of the QRCode or else the readability of the code could be compromised 26 | * Default is set to 25% 27 | * 28 | * It's safe to add a overlay that takes up more than 25%, if the transparency is less than .20 29 | * 30 | * @throws IllegalArgumentException - if the overlay is null 31 | */ 32 | public static Decorator addImageOverlay(BufferedImage overlay, Float overlayTransparency, Float overlayToQRCodeRatio){ 33 | return new ImageOverlay(overlay, overlayTransparency, overlayToQRCodeRatio); 34 | } 35 | 36 | private ImageOverlay(BufferedImage overlay, Float overlayTransparency, Float overlayToQRCodeRatio){ 37 | if(overlay == null) { 38 | throw new IllegalArgumentException("Overlay is required"); 39 | } 40 | 41 | this.overlay = overlay; 42 | this.overlayTransparency = overlayTransparency == null ? DEFAULT_OVERLAY_TRANSPARENCY : overlayTransparency; 43 | this.overlayToQRCodeRatio = overlayToQRCodeRatio == null ? DEFAULT_OVERLAY_TO_QRCODE_RATIO : overlayToQRCodeRatio; 44 | } 45 | 46 | public BufferedImage decorate(BufferedImage qrcode) { 47 | BufferedImage scaledOverlay = scaleOverlay(qrcode); 48 | 49 | Integer deltaHeight = qrcode.getHeight() - scaledOverlay.getHeight(); 50 | Integer deltaWidth = qrcode.getWidth() - scaledOverlay.getWidth(); 51 | 52 | BufferedImage combined = new BufferedImage(qrcode.getWidth(), qrcode.getHeight(), BufferedImage.TYPE_INT_ARGB); 53 | Graphics2D g2 = (Graphics2D)combined.getGraphics(); 54 | g2.drawImage(qrcode, 0, 0, null); 55 | g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, overlayTransparency)); 56 | g2.drawImage(scaledOverlay, Math.round(deltaWidth/2), Math.round(deltaHeight/2), null); 57 | 58 | return combined; 59 | } 60 | 61 | //----------------- 62 | // private methods 63 | //----------------- 64 | 65 | private BufferedImage scaleOverlay(BufferedImage qrcode){ 66 | Integer scaledWidth = Math.round(qrcode.getWidth() * overlayToQRCodeRatio); 67 | Integer scaledHeight = Math.round(qrcode.getHeight() * overlayToQRCodeRatio); 68 | 69 | BufferedImage imageBuff = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB); 70 | Graphics g = imageBuff.createGraphics(); 71 | g.drawImage(overlay.getScaledInstance(scaledWidth, scaledHeight, BufferedImage.SCALE_SMOOTH), 0, 0, new Color(0,0,0), null); 72 | g.dispose(); 73 | 74 | return imageBuff; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/skrymer/qrbuilder/QRCode.java: -------------------------------------------------------------------------------- 1 | package org.skrymer.qrbuilder; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.nio.charset.Charset; 7 | import java.util.*; 8 | import java.util.function.Consumer; 9 | 10 | import com.google.zxing.*; 11 | import com.google.zxing.client.j2se.BufferedImageLuminanceSource; 12 | import com.google.zxing.client.j2se.MatrixToImageWriter; 13 | import com.google.zxing.common.BitMatrix; 14 | import com.google.zxing.common.HybridBinarizer; 15 | import com.google.zxing.qrcode.QRCodeReader; 16 | import com.google.zxing.qrcode.QRCodeWriter; 17 | import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; 18 | import org.skrymer.qrbuilder.decorator.Decorator; 19 | import org.skrymer.qrbuilder.exception.CouldNotCreateFileException; 20 | import org.skrymer.qrbuilder.exception.CouldNotCreateQRCodeException; 21 | import org.skrymer.qrbuilder.exception.InvalidSizeException; 22 | import org.skrymer.qrbuilder.exception.UnreadableDataException; 23 | import org.skrymer.qrbuilder.util.SyntacticSugar; 24 | 25 | import javax.imageio.ImageIO; 26 | 27 | /** 28 | * QRCode using the ZXing library to generate images and files 29 | *

30 | * see https://github.com/zxing/zxing 31 | */ 32 | public class QRCode { 33 | private String data; 34 | private boolean verify; 35 | private int width, height; 36 | private List> decorators; 37 | private Charset charSet; 38 | 39 | private QRCode(ZXingBuilder builder) { 40 | data = builder.data; 41 | verify = builder.verify; 42 | width = builder.width; 43 | height = builder.height; 44 | decorators = builder.decorators; 45 | charSet = builder.charSet; 46 | } 47 | 48 | public BufferedImage toImage() throws CouldNotCreateQRCodeException, UnreadableDataException { 49 | BufferedImage qrcode = decorate(encode()); 50 | verifyQRCode(qrcode); 51 | 52 | return qrcode; 53 | } 54 | 55 | public File toFile(String fileName, String fileFormat) throws CouldNotCreateQRCodeException, UnreadableDataException { 56 | SyntacticSugar.throwIllegalArgumentExceptionIfEmpty(fileName, "fileName"); 57 | SyntacticSugar.throwIllegalArgumentExceptionIfEmpty(fileFormat, "fileFormat"); 58 | 59 | try { 60 | File imageFile = new File(fileName); 61 | ImageIO.write(toImage(), fileFormat, imageFile); 62 | return imageFile; 63 | } catch (IOException e) { 64 | throw new CouldNotCreateFileException("Could not create file", e); 65 | } 66 | } 67 | 68 | //-------------------- 69 | // private methods 70 | //-------------------- 71 | 72 | private void verifyQRCode(BufferedImage qrCode) { 73 | if (!verify) { 74 | return; 75 | } 76 | 77 | try { 78 | Result actualData = decode(qrCode); 79 | 80 | if (actualData != null && !actualData.getText().equals(this.data)) { 81 | throw new UnreadableDataException("The data contained in the qrCode is not as expected: " + this.data + " actual: " + actualData); 82 | } 83 | } catch (Exception e) { 84 | throw new UnreadableDataException("Verifying qr code failed!", e); 85 | } 86 | } 87 | 88 | private Result decode(BufferedImage qrcode) throws FormatException, ChecksumException, NotFoundException { 89 | return new QRCodeReader().decode(new BinaryBitmap( 90 | new HybridBinarizer( 91 | new BufferedImageLuminanceSource(qrcode)) 92 | ), getDecodeHints() 93 | ); 94 | } 95 | 96 | private BufferedImage encode() { 97 | try { 98 | BitMatrix matrix = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, this.width, this.height, getEncodeHints()); 99 | return MatrixToImageWriter.toBufferedImage(matrix); 100 | } catch (Exception e) { 101 | throw new CouldNotCreateQRCodeException("QRCode could not be generated", e); 102 | } 103 | } 104 | 105 | private Map getEncodeHints() { 106 | Map encodeHints = new HashMap<>(); 107 | encodeHints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); 108 | encodeHints.put(EncodeHintType.CHARACTER_SET, this.charSet.name()); 109 | return encodeHints; 110 | } 111 | 112 | private Map getDecodeHints() { 113 | Map decodeHints = new HashMap<>(); 114 | decodeHints.put(DecodeHintType.PURE_BARCODE, Boolean.TRUE); 115 | decodeHints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); 116 | decodeHints.put(DecodeHintType.CHARACTER_SET, this.charSet.name()); 117 | return decodeHints; 118 | } 119 | 120 | private BufferedImage decorate(BufferedImage qrCodeImage) { 121 | for(Decorator decorator : decorators){ 122 | qrCodeImage = decorator.decorate(qrCodeImage); 123 | } 124 | return qrCodeImage; 125 | } 126 | 127 | //------------------- 128 | // Builder 129 | //------------------- 130 | 131 | public static class ZXingBuilder { 132 | private String data; 133 | private boolean verify; 134 | private int width, height; 135 | private Charset charSet; 136 | private List> decorators; 137 | 138 | private ZXingBuilder(){ 139 | verify = true; 140 | charSet = Charset.defaultCharset(); 141 | decorators = new ArrayList<>(); 142 | } 143 | 144 | public static QRCode build(Consumer block) { 145 | ZXingBuilder builder = new ZXingBuilder(); 146 | block.accept(builder); 147 | return new QRCode(builder); 148 | } 149 | 150 | public ZXingBuilder and() { 151 | return this; 152 | } 153 | 154 | public ZXingBuilder verify(Boolean doVerify) { 155 | this.verify = doVerify; 156 | return this; 157 | } 158 | 159 | public ZXingBuilder withData(String data) { 160 | this.data = data; 161 | return this; 162 | } 163 | 164 | public ZXingBuilder withSize(Integer width, Integer height) { 165 | validateSize(width, height); 166 | this.width = width; 167 | this.height = height; 168 | return this; 169 | } 170 | 171 | public ZXingBuilder withDecorator(Decorator decorator) { 172 | decorators.add(decorator); 173 | return this; 174 | } 175 | 176 | /** 177 | * Defaults to the jvm default char set 178 | * @param charSet 179 | * @return 180 | */ 181 | public ZXingBuilder withCharSet(Charset charSet){ 182 | this.charSet = charSet; 183 | return this; 184 | } 185 | 186 | private void validateSize(Integer width, Integer height) { 187 | if (width <= 0) { 188 | throw new InvalidSizeException("Width is to small should be > 0 is " + width); 189 | } 190 | 191 | if (height <= 0) { 192 | throw new InvalidSizeException("Height is to small should be > 0 is " + height); 193 | } 194 | } 195 | } 196 | } 197 | --------------------------------------------------------------------------------