From 2ea993a381da63b5c66957236b090820a312345a Mon Sep 17 00:00:00 2001 From: Project Nayuki Date: Thu, 23 Nov 2017 06:58:16 +0000 Subject: [PATCH] Implemented principal QR Code class, added demo program. --- src/io/nayuki/fastqrcodegen/QrCode.java | 515 ++++++++++++++++++ .../fastqrcodegen/QrCodeGeneratorDemo.java | 190 +++++++ 2 files changed, 705 insertions(+) create mode 100644 src/io/nayuki/fastqrcodegen/QrCode.java create mode 100644 src/io/nayuki/fastqrcodegen/QrCodeGeneratorDemo.java diff --git a/src/io/nayuki/fastqrcodegen/QrCode.java b/src/io/nayuki/fastqrcodegen/QrCode.java new file mode 100644 index 0000000..750d4f9 --- /dev/null +++ b/src/io/nayuki/fastqrcodegen/QrCode.java @@ -0,0 +1,515 @@ +/* + * Fast QR Code generator library + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +package io.nayuki.fastqrcodegen; + +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + + +public final class QrCode { + + /*---- Public static factory functions ----*/ + + public static QrCode encodeText(String text, Ecc ecl) { + Objects.requireNonNull(text); + Objects.requireNonNull(ecl); + List segs = QrSegment.makeSegments(text); + return encodeSegments(segs, ecl); + } + + + public static QrCode encodeBinary(byte[] data, Ecc ecl) { + Objects.requireNonNull(data); + Objects.requireNonNull(ecl); + QrSegment seg = QrSegment.makeBytes(data); + return encodeSegments(Arrays.asList(seg), ecl); + } + + + public static QrCode encodeSegments(List segs, Ecc ecl) { + return encodeSegments(segs, ecl, MIN_VERSION, MAX_VERSION, -1, true); + } + + + public static QrCode encodeSegments(List segs, Ecc ecl, int minVersion, int maxVersion, int mask, boolean boostEcl) { + Objects.requireNonNull(segs); + Objects.requireNonNull(ecl); + if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) + throw new IllegalArgumentException("Invalid value"); + + // Find the minimal version number to use + int version, dataUsedBits; + for (version = minVersion; ; version++) { + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = QrSegment.getTotalBits(segs, version); + if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) + break; // This version number is found to be suitable + if (version >= maxVersion) // All versions in the range could not fit the given data + throw new IllegalArgumentException("Data too long"); + } + if (dataUsedBits == -1) + throw new AssertionError(); + + // Increase the error correction level while the data still fits in the current version number + for (Ecc newEcl : Ecc.values()) { + if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8) + ecl = newEcl; + } + + // Create the data bit string by concatenating all segments + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; + BitBuffer bb = new BitBuffer(); + for (QrSegment seg : segs) { + bb.appendBits(seg.mode.modeBits, 4); + bb.appendBits(seg.numChars, seg.mode.numCharCountBits(version)); + bb.appendBits(seg.data, seg.bitLength); + } + + // Add terminator and pad up to a byte if applicable + bb.appendBits(0, Math.min(4, dataCapacityBits - bb.bitLength)); + bb.appendBits(0, (8 - bb.bitLength % 8) % 8); + + // Pad with alternate bytes until data capacity is reached + for (int padByte = 0xEC; bb.bitLength < dataCapacityBits; padByte ^= 0xEC ^ 0x11) + bb.appendBits(padByte, 8); + if (bb.bitLength % 8 != 0) + throw new AssertionError(); + + // Create the QR Code symbol + return new QrCode(version, ecl, bb.getBytes(), mask); + } + + + + /*---- Public constants ----*/ + + public static final int MIN_VERSION = 1; + public static final int MAX_VERSION = 40; + + + + /*---- Instance fields ----*/ + + public final int version; + + public final int size; + + public final Ecc errorCorrectionLevel; + + public final int mask; + + private final int[] modules; + + + + /*---- Constructors ----*/ + + public QrCode(int ver, Ecc ecl, byte[] dataCodewords, int mask) { + // Check arguments + Objects.requireNonNull(ecl); + if (ver < MIN_VERSION || ver > MAX_VERSION || mask < -1 || mask > 7) + throw new IllegalArgumentException("Value out of range"); + Objects.requireNonNull(dataCodewords); + + // Initialize fields + version = ver; + size = ver * 4 + 17; + errorCorrectionLevel = ecl; + + QrTemplate tpl = QrTemplate.getInstance(ver); + modules = tpl.template.clone(); + + // Draw function patterns, draw all codewords, do masking + byte[] allCodewords = appendErrorCorrection(dataCodewords); + drawCodewords(tpl.dataOutputBitIndexes, allCodewords); + this.mask = handleConstructorMasking(tpl.masks, mask); + } + + + + /*---- Public instance methods ----*/ + + /** + * Returns the color of the module (pixel) at the specified coordinates, which is either + * false for white or true for black. The top left corner has the coordinates (x=0, y=0). + * If the specified coordinates are out of bounds, then false (white) is returned. + * @param x the x coordinate, where 0 is the left edge and size−1 is the right edge + * @param y the y coordinate, where 0 is the top edge and size−1 is the bottom edge + * @return the module's color, which is either false (white) or true (black) + */ + public boolean getModule(int x, int y) { + return 0 <= x && x < size && 0 <= y && y < size + && getModuleUnchecked(x, y) != 0; + } + + + /** + * Returns a new image object representing this QR Code, with the specified module scale and number + * of border modules. For example, the arguments scale=10, border=4 means to pad the QR Code symbol + * with 4 white border modules on all four edges, then use 10*10 pixels to represent each module. + * The resulting image only contains the hex colors 000000 and FFFFFF. + * @param scale the module scale factor, which must be positive + * @param border the number of border modules to add, which must be non-negative + * @return an image representing this QR Code, with padding and scaling + * @throws IllegalArgumentException if the scale or border is out of range + */ + public BufferedImage toImage(int scale, int border) { + if (scale <= 0 || border < 0) + throw new IllegalArgumentException("Value out of range"); + BufferedImage result = new BufferedImage((size + border * 2) * scale, (size + border * 2) * scale, BufferedImage.TYPE_INT_RGB); + for (int y = 0; y < result.getHeight(); y++) { + for (int x = 0; x < result.getWidth(); x++) { + boolean val = getModule(x / scale - border, y / scale - border); + result.setRGB(x, y, val ? 0x000000 : 0xFFFFFF); + } + } + return result; + } + + + /** + * Based on the specified number of border modules to add as padding, this returns a + * string whose contents represents an SVG XML file that depicts this QR Code symbol. + * Note that Unix newlines (\n) are always used, regardless of the platform. + * @param border the number of border modules to add, which must be non-negative + * @return a string representing this QR Code as an SVG document + */ + public String toSvgString(int border) { + if (border < 0) + throw new IllegalArgumentException("Border must be non-negative"); + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append("\n"); + sb.append(String.format( + "\n", + size + border * 2)); + sb.append("\t\n"); + sb.append("\t\n"); + sb.append("\n"); + return sb.toString(); + } + + + + /*---- Private helper methods for constructor: Drawing function modules ----*/ + + // Draws two copies of the format bits (with its own error correction code) + // based on the given mask and this object's error correction level field. + private void drawFormatBits(int mask) { + // Calculate error correction code and pack bits + int data = errorCorrectionLevel.formatBits << 3 | mask; // errCorrLvl is uint2, mask is uint3 + int rem = data; + for (int i = 0; i < 10; i++) + rem = (rem << 1) ^ ((rem >>> 9) * 0x537); + data = data << 10 | rem; + data ^= 0x5412; // uint15 + if (data >>> 15 != 0) + throw new AssertionError(); + + // Draw first copy + for (int i = 0; i <= 5; i++) + setModule(8, i, (data >>> i) & 1); + setModule(8, 7, (data >>> 6) & 1); + setModule(8, 8, (data >>> 7) & 1); + setModule(7, 8, (data >>> 8) & 1); + for (int i = 9; i < 15; i++) + setModule(14 - i, 8, (data >>> i) & 1); + + // Draw second copy + for (int i = 0; i <= 7; i++) + setModule(size - 1 - i, 8, (data >>> i) & 1); + for (int i = 8; i < 15; i++) + setModule(8, size - 15 + i, (data >>> i) & 1); + setModule(8, size - 8, 1); + } + + + private void setModule(int x, int y, int black) { + assert 0 <= x && x < size; + assert 0 <= y && y < size; + int i = y * size + x; + if (black == 0) + modules[i >>> 5] &= ~(1 << i); + else if (black == 1) + modules[i >>> 5] |= 1 << i; + else + throw new IllegalArgumentException(); + } + + + private int getModuleUnchecked(int x, int y) { + int i = y * size + x; + return (modules[i >>> 5] >>> i) & 1; + } + + + /*---- Private helper methods for constructor: Codewords and masking ----*/ + + // Returns a new byte string representing the given data with the appropriate error correction + // codewords appended to it, based on this object's version and error correction level. + private byte[] appendErrorCorrection(byte[] data) { + if (data.length != getNumDataCodewords(version, errorCorrectionLevel)) + throw new IllegalArgumentException(); + + // Calculate parameter numbers + int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[errorCorrectionLevel.ordinal()][version]; + int blockEccLen = ECC_CODEWORDS_PER_BLOCK[errorCorrectionLevel.ordinal()][version]; + int rawCodewords = QrTemplate.getNumRawDataModules(version) / 8; + int numShortBlocks = numBlocks - rawCodewords % numBlocks; + int shortBlockLen = rawCodewords / numBlocks; + + // Split data into blocks and append ECC to each block + byte[][] blocks = new byte[numBlocks][]; + ReedSolomonGenerator rs = ReedSolomonGenerator.getInstance(blockEccLen); + byte[] ecc = new byte[blockEccLen]; + for (int i = 0, k = 0; i < numBlocks; i++) { + byte[] dat = Arrays.copyOfRange(data, k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1)); + byte[] block = Arrays.copyOf(dat, shortBlockLen + 1); + k += dat.length; + rs.getRemainder(dat, ecc); + System.arraycopy(ecc, 0, block, block.length - blockEccLen, ecc.length); + blocks[i] = block; + } + + // Interleave (not concatenate) the bytes from every block into a single sequence + byte[] result = new byte[rawCodewords]; + for (int i = 0, k = 0; i < blocks[0].length; i++) { + for (int j = 0; j < blocks.length; j++) { + // Skip the padding byte in short blocks + if (i != shortBlockLen - blockEccLen || j >= numShortBlocks) { + result[k] = blocks[j][i]; + k++; + } + } + } + return result; + } + + + // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire + // data area of this QR Code symbol. Function modules need to be marked off before this is called. + private void drawCodewords(int[] dataOutputBitIndexes, byte[] allCodewords) { + Objects.requireNonNull(dataOutputBitIndexes); + Objects.requireNonNull(allCodewords); + if (allCodewords.length * 8 != dataOutputBitIndexes.length) + throw new IllegalArgumentException(); + for (int i = 0; i < dataOutputBitIndexes.length; i++) { + int j = dataOutputBitIndexes[i]; + int bit = (allCodewords[i >>> 3] >>> (~i & 7)) & 1; + modules[j >>> 5] |= bit << j; + } + } + + + // XORs the data modules in this QR Code with the given mask pattern. Due to XOR's mathematical + // properties, calling applyMask(m) twice with the same value is equivalent to no change at all. + // This means it is possible to apply a mask, undo it, and try another mask. Note that a final + // well-formed QR Code symbol needs exactly one mask applied (not zero, not two, etc.). + private void applyMask(int[] mask) { + if (mask.length != modules.length) + throw new IllegalArgumentException(); + for (int i = 0; i < mask.length; i++) + modules[i] ^= mask[i]; + } + + + // A messy helper function for the constructors. This QR Code must be in an unmasked state when this + // method is called. The given argument is the requested mask, which is -1 for auto or 0 to 7 for fixed. + // This method applies and returns the actual mask chosen, from 0 to 7. + private int handleConstructorMasking(int[][] masks, int mask) { + if (mask == -1) { // Automatically choose best mask + int minPenalty = Integer.MAX_VALUE; + for (int i = 0; i < 8; i++) { + drawFormatBits(i); + applyMask(masks[i]); + int penalty = getPenaltyScore(); + if (penalty < minPenalty) { + mask = i; + minPenalty = penalty; + } + applyMask(masks[i]); // Undoes the mask due to XOR + } + } + if (mask < 0 || mask > 7) + throw new AssertionError(); + drawFormatBits(mask); // Overwrite old format bits + applyMask(masks[mask]); // Apply the final choice of mask + return mask; // The caller shall assign this value to the final-declared field + } + + + // Calculates and returns the penalty score based on state of this QR Code's current modules. + // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. + private int getPenaltyScore() { + int result = 0; + + // Adjacent modules in row having same color + for (int y = 0; y < size; y++) { + int colorX = 0; + for (int x = 0, runX = 0; x < size; x++) { + if (x == 0 || getModuleUnchecked(x, y) != colorX) { + colorX = getModuleUnchecked(x, y); + runX = 1; + } else { + runX++; + if (runX == 5) + result += PENALTY_N1; + else if (runX > 5) + result++; + } + } + } + // Adjacent modules in column having same color + for (int x = 0; x < size; x++) { + int colorY = 0; + for (int y = 0, runY = 0; y < size; y++) { + if (y == 0 || getModuleUnchecked(x, y) != colorY) { + colorY = getModuleUnchecked(x, y); + runY = 1; + } else { + runY++; + if (runY == 5) + result += PENALTY_N1; + else if (runY > 5) + result++; + } + } + } + + // 2*2 blocks of modules having same color + for (int y = 0; y < size - 1; y++) { + for (int x = 0; x < size - 1; x++) { + int color = getModuleUnchecked(x, y); + if ( color == getModuleUnchecked(x + 1, y) && + color == getModuleUnchecked(x, y + 1) && + color == getModuleUnchecked(x + 1, y + 1)) + result += PENALTY_N2; + } + } + + // Finder-like pattern in rows + for (int y = 0; y < size; y++) { + for (int x = 0, bits = 0; x < size; x++) { + bits = ((bits << 1) & 0x7FF) | getModuleUnchecked(x, y); + if (x >= 10 && (bits == 0x05D || bits == 0x5D0)) // Needs 11 bits accumulated + result += PENALTY_N3; + } + } + // Finder-like pattern in columns + for (int x = 0; x < size; x++) { + for (int y = 0, bits = 0; y < size; y++) { + bits = ((bits << 1) & 0x7FF) | getModuleUnchecked(x, y); + if (y >= 10 && (bits == 0x05D || bits == 0x5D0)) // Needs 11 bits accumulated + result += PENALTY_N3; + } + } + + // Balance of black and white modules + int black = 0; + for (int x : modules) + black += Integer.bitCount(x); + int total = size * size; + // Find smallest k such that (45-5k)% <= dark/total <= (55+5k)% + for (int k = 0; black*20 < (9-k)*total || black*20 > (11+k)*total; k++) + result += PENALTY_N4; + return result; + } + + + + /*---- Private static helper functions ----*/ + + // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any + // QR Code of the given version number and error correction level, with remainder bits discarded. + // This stateless pure function could be implemented as a (40*4)-cell lookup table. + static int getNumDataCodewords(int ver, Ecc ecl) { + if (ver < MIN_VERSION || ver > MAX_VERSION) + throw new IllegalArgumentException("Version number out of range"); + return QrTemplate.getNumRawDataModules(ver) / 8 + - ECC_CODEWORDS_PER_BLOCK[ecl.ordinal()][ver] + * NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal()][ver]; + } + + + /*---- Private tables of constants ----*/ + + // For use in getPenaltyScore(), when evaluating which mask is best. + private static final int PENALTY_N1 = 3; + private static final int PENALTY_N2 = 3; + private static final int PENALTY_N3 = 40; + private static final int PENALTY_N4 = 10; + + + private static final byte[][] ECC_CODEWORDS_PER_BLOCK = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low + {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium + {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile + {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High + }; + + private static final byte[][] NUM_ERROR_CORRECTION_BLOCKS = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low + {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium + {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile + {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High + }; + + + + /*---- Public helper enumeration ----*/ + + public enum Ecc { + // These enum constants must be declared in ascending order of error protection, + // for the sake of the implicit ordinal() method and values() function. + LOW(1), MEDIUM(0), QUARTILE(3), HIGH(2); + + // In the range 0 to 3 (unsigned 2-bit integer). + final int formatBits; + + // Constructor. + private Ecc(int fb) { + formatBits = fb; + } + } + +} diff --git a/src/io/nayuki/fastqrcodegen/QrCodeGeneratorDemo.java b/src/io/nayuki/fastqrcodegen/QrCodeGeneratorDemo.java new file mode 100644 index 0000000..4d4bff5 --- /dev/null +++ b/src/io/nayuki/fastqrcodegen/QrCodeGeneratorDemo.java @@ -0,0 +1,190 @@ +/* + * Fast QR Code generator demo + * + * Run this command-line program with no arguments. The program creates/overwrites a bunch of + * PNG and SVG files in the current working directory to demonstrate the creation of QR Codes. + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +package io.nayuki.fastqrcodegen; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import javax.imageio.ImageIO; + + +public final class QrCodeGeneratorDemo { + + // The main application program. + public static void main(String[] args) throws IOException { + doBasicDemo(); + doVarietyDemo(); + doSegmentDemo(); + doMaskDemo(); + } + + + + /*---- Demo suite ----*/ + + // Creates a single QR Code, then writes it to a PNG file and an SVG file. + private static void doBasicDemo() throws IOException { + String text = "Hello, world!"; // User-supplied Unicode text + QrCode.Ecc errCorLvl = QrCode.Ecc.LOW; // Error correction level + + QrCode qr = QrCode.encodeText(text, errCorLvl); // Make the QR Code symbol + + BufferedImage img = qr.toImage(10, 4); // Convert to bitmap image + File imgFile = new File("hello-world-QR.png"); // File path for output + ImageIO.write(img, "png", imgFile); // Write image to file + + String svg = qr.toSvgString(4); // Convert to SVG XML code + try (Writer out = new OutputStreamWriter( + new FileOutputStream("hello-world-QR.svg"), + StandardCharsets.UTF_8)) { + out.write(svg); // Create/overwrite file and write SVG data + } + } + + + // Creates a variety of QR Codes that exercise different features of the library, and writes each one to file. + private static void doVarietyDemo() throws IOException { + QrCode qr; + + // Numeric mode encoding (3.33 bits per digit) + qr = QrCode.encodeText("314159265358979323846264338327950288419716939937510", QrCode.Ecc.MEDIUM); + writePng(qr.toImage(13, 1), "pi-digits-QR.png"); + + // Alphanumeric mode encoding (5.5 bits per character) + qr = QrCode.encodeText("DOLLAR-AMOUNT:$39.87 PERCENTAGE:100.00% OPERATIONS:+-*/", QrCode.Ecc.HIGH); + writePng(qr.toImage(10, 2), "alphanumeric-QR.png"); + + // Unicode text as UTF-8 + qr = QrCode.encodeText("こんにちwa、世界! αβγδ", QrCode.Ecc.QUARTILE); + writePng(qr.toImage(10, 3), "unicode-QR.png"); + + // Moderately large QR Code using longer text (from Lewis Carroll's Alice in Wonderland) + qr = QrCode.encodeText( + "Alice was beginning to get very tired of sitting by her sister on the bank, " + + "and of having nothing to do: once or twice she had peeped into the book her sister was reading, " + + "but it had no pictures or conversations in it, 'and what is the use of a book,' thought Alice " + + "'without pictures or conversations?' So she was considering in her own mind (as well as she could, " + + "for the hot day made her feel very sleepy and stupid), whether the pleasure of making a " + + "daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly " + + "a White Rabbit with pink eyes ran close by her.", QrCode.Ecc.HIGH); + writePng(qr.toImage(6, 10), "alice-wonderland-QR.png"); + } + + + // Creates QR Codes with manually specified segments for better compactness. + private static void doSegmentDemo() throws IOException { + QrCode qr; + List segs; + + // Illustration "silver" + String silver0 = "THE SQUARE ROOT OF 2 IS 1."; + String silver1 = "41421356237309504880168872420969807856967187537694807317667973799"; + qr = QrCode.encodeText(silver0 + silver1, QrCode.Ecc.LOW); + writePng(qr.toImage(10, 3), "sqrt2-monolithic-QR.png"); + + segs = Arrays.asList( + QrSegment.makeAlphanumeric(silver0), + QrSegment.makeNumeric(silver1)); + qr = QrCode.encodeSegments(segs, QrCode.Ecc.LOW); + writePng(qr.toImage(10, 3), "sqrt2-segmented-QR.png"); + + // Illustration "golden" + String golden0 = "Golden ratio φ = 1."; + String golden1 = "6180339887498948482045868343656381177203091798057628621354486227052604628189024497072072041893911374"; + String golden2 = "......"; + qr = QrCode.encodeText(golden0 + golden1 + golden2, QrCode.Ecc.LOW); + writePng(qr.toImage(8, 5), "phi-monolithic-QR.png"); + + segs = Arrays.asList( + QrSegment.makeBytes(golden0.getBytes(StandardCharsets.UTF_8)), + QrSegment.makeNumeric(golden1), + QrSegment.makeAlphanumeric(golden2)); + qr = QrCode.encodeSegments(segs, QrCode.Ecc.LOW); + writePng(qr.toImage(8, 5), "phi-segmented-QR.png"); + + // Illustration "Madoka": kanji, kana, Greek, Cyrillic, full-width Latin characters + String madoka = "「魔法少女まどか☆マギカ」って、 ИАИ desu κα?"; + qr = QrCode.encodeText(madoka, QrCode.Ecc.LOW); + writePng(qr.toImage(9, 4), "madoka-utf8-QR.png"); + + int[] kanjiChars = { // Kanji mode encoding (13 bits per character) + 0x0035, 0x1002, 0x0FC0, 0x0AED, 0x0AD7, + 0x015C, 0x0147, 0x0129, 0x0059, 0x01BD, + 0x018D, 0x018A, 0x0036, 0x0141, 0x0144, + 0x0001, 0x0000, 0x0249, 0x0240, 0x0249, + 0x0000, 0x0104, 0x0105, 0x0113, 0x0115, + 0x0000, 0x0208, 0x01FF, 0x0008, + }; + BitBuffer bb = new BitBuffer(); + for (int c : kanjiChars) + bb.appendBits(c, 13); + segs = Arrays.asList(new QrSegment(QrSegment.Mode.KANJI, kanjiChars.length, bb.data, bb.bitLength)); + qr = QrCode.encodeSegments(segs, QrCode.Ecc.LOW); + writePng(qr.toImage(9, 4), "madoka-kanji-QR.png"); + } + + + // Creates QR Codes with the same size and contents but different mask patterns. + private static void doMaskDemo() throws IOException { + QrCode qr; + List segs; + + // Project Nayuki URL + segs = QrSegment.makeSegments("https://www.nayuki.io/"); + qr = QrCode.encodeSegments(segs, QrCode.Ecc.HIGH, QrCode.MIN_VERSION, QrCode.MAX_VERSION, -1, true); // Automatic mask + writePng(qr.toImage(8, 6), "project-nayuki-automask-QR.png"); + qr = QrCode.encodeSegments(segs, QrCode.Ecc.HIGH, QrCode.MIN_VERSION, QrCode.MAX_VERSION, 3, true); // Force mask 3 + writePng(qr.toImage(8, 6), "project-nayuki-mask3-QR.png"); + + // Chinese text as UTF-8 + segs = QrSegment.makeSegments("維基百科(Wikipedia,聆聽i/ˌwɪkᵻˈpiːdi.ə/)是一個自由內容、公開編輯且多語言的網路百科全書協作計畫"); + qr = QrCode.encodeSegments(segs, QrCode.Ecc.MEDIUM, QrCode.MIN_VERSION, QrCode.MAX_VERSION, 0, true); // Force mask 0 + writePng(qr.toImage(10, 3), "unicode-mask0-QR.png"); + qr = QrCode.encodeSegments(segs, QrCode.Ecc.MEDIUM, QrCode.MIN_VERSION, QrCode.MAX_VERSION, 1, true); // Force mask 1 + writePng(qr.toImage(10, 3), "unicode-mask1-QR.png"); + qr = QrCode.encodeSegments(segs, QrCode.Ecc.MEDIUM, QrCode.MIN_VERSION, QrCode.MAX_VERSION, 5, true); // Force mask 5 + writePng(qr.toImage(10, 3), "unicode-mask5-QR.png"); + qr = QrCode.encodeSegments(segs, QrCode.Ecc.MEDIUM, QrCode.MIN_VERSION, QrCode.MAX_VERSION, 7, true); // Force mask 7 + writePng(qr.toImage(10, 3), "unicode-mask7-QR.png"); + } + + + + /*---- Utilities ----*/ + + // Helper function to reduce code duplication. + private static void writePng(BufferedImage img, String filepath) throws IOException { + ImageIO.write(img, "png", new File(filepath)); + } + +}