Create QR-Code in vector image
Asked Answered
R

5

15

I can successfully create QR Code PNG images with ZXing but there is no easy way to get the output as SVG or EPS.

How can I create a vector image from the BitMatrix object that is created by the QRCodeWriter?

Redeem answered 28/5, 2012 at 18:38 Comment(2)
I would question your need for it in vector format. A QR code is a set of perfectly square black and white pixels. It will scale up infinity without any loss of fidelity. So, I would say, save as PNG.Rafa
there are still devices on this world which can not handle PNG. A laser engraver is one of them. This is why I needed a vector formatRedeem
R
1

The easiest way I found was to create a PDF with iText and then convert the resulting PDF to EPS or SVG. Here is the code to create the PDF:

   @Test
   public void testQRtoPDF() throws WriterException, FileNotFoundException, DocumentException, UnsupportedEncodingException {
      final int s = 600;
      int r = 1;

      Charset charset = Charset.forName( "UTF-8" );
      CharsetEncoder encoder = charset.newEncoder();
      byte[] b = null;
      try {
         // Convert a string to UTF-8 bytes in a ByteBuffer
         ByteBuffer bbuf = encoder.encode( CharBuffer.wrap(
                     "1éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò1" +
                                 "2éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò2" +
                                 "3éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò3" +
                                 "4éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò4" +
                                 "5éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò5" +
                                 "6éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò6" ) );
         b = bbuf.array();
      } catch ( CharacterCodingException e ) {
         System.out.println( e.getMessage() );
      }

      String content = new String( b, "UTF-8" );
      QRCodeWriter qrCodeWriter = new QRCodeWriter();
      Hashtable<EncodeHintType, String> hints = new Hashtable<EncodeHintType, String>( 2 );
      hints.put( EncodeHintType.CHARACTER_SET, "UTF-8" );
      BitMatrix qrCode = qrCodeWriter.encode( content, BarcodeFormat.QR_CODE, s, s, hints );

      Document doc = new Document( new Rectangle( s, s ) );
      PdfWriter pdfWriter = PdfWriter.getInstance( doc, new FileOutputStream( "qr-code.pdf" ) );
      doc.open();
      PdfContentByte contentByte = pdfWriter.getDirectContent();
      contentByte.setColorFill( BaseColor.BLACK );

      boolean d = false;
      for ( int x = 0; x < qrCode.getWidth(); x += r ) {
         for ( int y = 0; y < qrCode.getHeight(); y += r ) {
            if ( qrCode.get( x, y ) ) {
               contentByte.rectangle( x, s - y, r, r );
               contentByte.fill();
               contentByte.stroke();
            }
         }
      }

      doc.close();
   }

I then use image magic for the conversion. Like so:

convert qr-code.pdf qr-code.eps

the same can NOT be done for svg

convert qr-code.pdf qr-code.svg

this does not work

I tested this code with some long content and it worked with up to 600 characters. This is probably down to the precision of either camera on the phone or screen.

I hope this helps someone

Redeem answered 28/5, 2012 at 18:38 Comment(3)
If you don't need UTF-8 you can omit the ByteBuffer and the hints and encode your String directlyRedeem
personally I would use inkscape tracing on the PNG to do this, but if this answer works you can use it.Palermo
stroke isn't needed, and fill can be done once at the end. (I haven't tested this with itext, but that's the way the PDF specification is)Ipswich
W
11

Old question I know, but for anyone that comes along looking for how to do this...it's pretty easy to connect ZXing to JFreeSVG (http://www.jfree.org/jfreesvg), for example:

package org.jfree.demo;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import org.jfree.graphics2d.svg.SVGGraphics2D;
import org.jfree.graphics2d.svg.SVGUtils;

public class QRCodes {

    public static void main(String[] args) throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrCodeWriter.encode("http://www.jfree.org/jfreesvg", 
                BarcodeFormat.QR_CODE, 160, 160);
        int w = bitMatrix.getWidth();
        SVGGraphics2D g2 = new SVGGraphics2D(w, w);
        g2.setColor(Color.BLACK);
    for (int xIndex = 0; xIndex < w; xIndex = xIndex + bitMatrix.getRowSize()) {
        for (int yIndex = 0; yIndex < w; yIndex = yIndex + bitMatrix.getRowSize()) {
            if (bitMatrix.get(xIndex, yIndex)) {
                g2.fillRect(xIndex, yIndex, bitMatrix.getRowSize(), bitMatrix.getRowSize());

            }
        }
    }

        SVGUtils.writeToSVG(new File("qrtest.svg"), g2.getSVGElement());
    }

}
Weatherspoon answered 3/5, 2016 at 5:13 Comment(2)
This didn't work for me - the SVG created wasn't a valid QR code, and was notably different to the PNG image in the BitMatrix. Close, but damaged.Ketosis
It doesn't work for some sizes, for instance 144x144Palimpsest
F
9

You can even do it only with zxing withou additional libraries:

QRCodeWriter qrCodeWriter = new QRCodeWriter();
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());

BitMatrix bitMatrix = qrCodeWriter.encode(payload, BarcodeFormat.QR_CODE, 543, 543, hints);

StringBuilder sbPath = new StringBuilder();
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
int rowSize = bitMatrix.getRowSize();
BitArray row = new BitArray(width);
for(int y = 0; y < height; ++y) {
    row = bitMatrix.getRow(y, row);
    for(int x = 0; x < width; ++x) {
        if (row.get(x)) {
            sbPath.append(" M"+x+","+y+"h1v1h-1z");
        }
    }
}

StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
sb.append("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 ").append(width).append(" ").append(height).append("\" stroke=\"none\">\n");
sb.append("<style type=\"text/css\">\n");
sb.append(".black {fill:#000000;}\n");
sb.append("</style>\n");
sb.append("<path class=\"black\"  d=\"").append(sbPath.toString()).append("\"/>\n");
sb.append("</svg>\n");

Please note that the solution above is much more efficient in terms of memory consumption than the one using Batik's DOM.

Figwort answered 11/3, 2020 at 14:27 Comment(5)
you saved my day! thank you so much, it's exactly what was needed)Dislimn
This works, but for a QR code the output seems a little... large. 105Kb, vs 359 bytes for the png. An online generator managed the same URL in 2Kb. I think this approach is sound, but needs optimisation.Ketosis
Solved: tweaking the size in 'encode()' helps manage the output, and works well. Thanks for this neat solution.Ketosis
Specifically, passing zeroes as width and height would cause ZXing to produce the matrix that matches the QR code dimensions (including "quiet zone").Callisto
style element is redundant here (fill is black by default for path: see developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill#path). It's unclear why two string builders are used here: it's simpler just to add first part of the XML to a single builder, then loop through array adding pixels, then add the rest of XML. The space before M is completely optional, and only helps making the end result readable.Callisto
P
2

Svg format is meant to be set of rectangles which constructs BitMatrix. The idea behind the format is the resolution of the image not to be affected by any increase in the size. For instance, @siom's solution creates 1x1 rectangles for all pixels(That's why it creates a huge file). I classify that solution as brute force approach to this problem.

I developed a better solution running in O(n^2). It scans whole bitMatrix and first detects the maximum length square in each point, then it tries to extend the square to rectange in each dimension. Finally it draws all possible rectangles and squares in various sizes.

import com.google.zxing.common.BitMatrix;
import java.awt.Color;
import java.awt.Point;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
import org.jfree.graphics2d.svg.SVGGraphics2D;

public class SvgUtils {

  public static byte[] createSvgImage(BitMatrix bitMatrix){
    int width = bitMatrix.getWidth();
    int height = bitMatrix.getHeight();
    Set<Point> visitedPoints = new HashSet<>();
    SVGGraphics2D g2 = new SVGGraphics2D(width, height);
    g2.setColor(Color.BLACK);
    for (int x = 0; x < width; x++) {
      for (int y = 0; y < height; y++) {
        Point maxRectangleLength = getMaxRectangleLength(new Point(x, y), bitMatrix, visitedPoints);
        if(maxRectangleLength != null) {
          g2.fillRect(x, y, maxRectangleLength.x, maxRectangleLength.y);
          y += maxRectangleLength.y-1;
        }
      }
    }
    return g2.getSVGDocument().getBytes(StandardCharsets.UTF_8);
  }

  private static int getMaxSquareLength(Point startPoint, BitMatrix bitMatrix, Set<Point> visitedPoints){
    int width = bitMatrix.getWidth();
    int height = bitMatrix.getHeight();
    int maxLength = 0;
    while(startPoint.x + maxLength < width && startPoint.y + maxLength < height) {
      for (int xOffSett = 0; xOffSett < maxLength; xOffSett++) {
        if (!bitMatrix.get(startPoint.x + xOffSett, startPoint.y + maxLength) || visitedPoints.contains(new Point(startPoint.x + xOffSett, startPoint.y + maxLength))) {
          return maxLength;
        }
      }
      for (int yOffset = 0; yOffset <= maxLength; yOffset++) {
        if (!bitMatrix.get(startPoint.x + maxLength, startPoint.y + yOffset) || visitedPoints.contains(new Point(startPoint.x + maxLength, startPoint.y + yOffset))) {
          return maxLength;
        }
      }
      for (int xOffSett = 0; xOffSett < maxLength; xOffSett++) {
        visitedPoints.add(new Point(startPoint.x + xOffSett, startPoint.y + maxLength));
      }
      for (int yOffset = 0; yOffset <= maxLength; yOffset++) {
        visitedPoints.add(new Point(startPoint.x + maxLength, startPoint.y + yOffset));
      }
      maxLength++;
    }
    return maxLength;
  }

  private static Point getMaxRectangleLength(Point startPoint, BitMatrix bitMatrix, Set<Point> visitedPoints){
    int width = bitMatrix.getWidth();
    int height = bitMatrix.getHeight();
    int maxSquareLength = getMaxSquareLength(startPoint, bitMatrix, visitedPoints);
    if(maxSquareLength == 0)
      return null;
    int maxWidth = maxSquareLength-1;
    int maxHeight = maxSquareLength-1;
    boolean searchFinished = false;
    while(!searchFinished && startPoint.y + ++maxHeight < height) {
      for (int xOffSett = 0; xOffSett < maxSquareLength; xOffSett++) {
        if (!bitMatrix.get(startPoint.x + xOffSett, startPoint.y + maxHeight) ||
            visitedPoints.contains(new Point(startPoint.x + xOffSett, startPoint.y + maxHeight))) {
          searchFinished = true;
          break;
        }
      }
    }
    searchFinished = false;
    while(!searchFinished && startPoint.x + ++maxWidth < width) {
      for (int yOffSett = 0; yOffSett < maxSquareLength; yOffSett++) {
        if (!bitMatrix.get(startPoint.x + maxWidth, startPoint.y + yOffSett) ||
            visitedPoints.contains(new Point(startPoint.x + maxWidth, startPoint.y + yOffSett))) {
          searchFinished = true;
          break;
        }
      }
    }
    if(maxHeight >= maxWidth){
      for(int yOffSet = maxSquareLength; yOffSet < maxHeight; yOffSet++){
        for (int xOffSett = 0; xOffSett < maxSquareLength; xOffSett++) {
          visitedPoints.add(new Point(startPoint.x + xOffSett, startPoint.y + yOffSet));
        }
      }
      return new Point(maxSquareLength, maxHeight);
    } else {
      for(int xOffSett = maxSquareLength; xOffSett < maxWidth; xOffSett++){
        for (int yOffSet = 0; yOffSet < maxSquareLength; yOffSet++) {
          visitedPoints.add(new Point(startPoint.x + xOffSett, startPoint.y + yOffSet));
        }
      }
      return new Point(maxWidth, maxSquareLength);
    }
  }
}
Palimpsest answered 24/4, 2021 at 22:31 Comment(2)
Deserves some recognition. Wonderful solution. Some minor issues with the variable naming, but who cares, when you get a QR Code svg... Thanks!Severity
There's a static method to get the blocks directly: github.com/zxing/zxing/blob/…Tola
R
1

The easiest way I found was to create a PDF with iText and then convert the resulting PDF to EPS or SVG. Here is the code to create the PDF:

   @Test
   public void testQRtoPDF() throws WriterException, FileNotFoundException, DocumentException, UnsupportedEncodingException {
      final int s = 600;
      int r = 1;

      Charset charset = Charset.forName( "UTF-8" );
      CharsetEncoder encoder = charset.newEncoder();
      byte[] b = null;
      try {
         // Convert a string to UTF-8 bytes in a ByteBuffer
         ByteBuffer bbuf = encoder.encode( CharBuffer.wrap(
                     "1éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò1" +
                                 "2éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò2" +
                                 "3éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò3" +
                                 "4éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò4" +
                                 "5éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò5" +
                                 "6éöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùòïëéöàäèüùò6" ) );
         b = bbuf.array();
      } catch ( CharacterCodingException e ) {
         System.out.println( e.getMessage() );
      }

      String content = new String( b, "UTF-8" );
      QRCodeWriter qrCodeWriter = new QRCodeWriter();
      Hashtable<EncodeHintType, String> hints = new Hashtable<EncodeHintType, String>( 2 );
      hints.put( EncodeHintType.CHARACTER_SET, "UTF-8" );
      BitMatrix qrCode = qrCodeWriter.encode( content, BarcodeFormat.QR_CODE, s, s, hints );

      Document doc = new Document( new Rectangle( s, s ) );
      PdfWriter pdfWriter = PdfWriter.getInstance( doc, new FileOutputStream( "qr-code.pdf" ) );
      doc.open();
      PdfContentByte contentByte = pdfWriter.getDirectContent();
      contentByte.setColorFill( BaseColor.BLACK );

      boolean d = false;
      for ( int x = 0; x < qrCode.getWidth(); x += r ) {
         for ( int y = 0; y < qrCode.getHeight(); y += r ) {
            if ( qrCode.get( x, y ) ) {
               contentByte.rectangle( x, s - y, r, r );
               contentByte.fill();
               contentByte.stroke();
            }
         }
      }

      doc.close();
   }

I then use image magic for the conversion. Like so:

convert qr-code.pdf qr-code.eps

the same can NOT be done for svg

convert qr-code.pdf qr-code.svg

this does not work

I tested this code with some long content and it worked with up to 600 characters. This is probably down to the precision of either camera on the phone or screen.

I hope this helps someone

Redeem answered 28/5, 2012 at 18:38 Comment(3)
If you don't need UTF-8 you can omit the ByteBuffer and the hints and encode your String directlyRedeem
personally I would use inkscape tracing on the PNG to do this, but if this answer works you can use it.Palermo
stroke isn't needed, and fill can be done once at the end. (I haven't tested this with itext, but that's the way the PDF specification is)Ipswich
P
1

You can use Apache Batik instead of JFreeSVG if you want a more liberate license. That makes especially sense when you use Apache FOP that includes Apache Batik as a transient dependency.

Here a tweaked version. Credits for the idea and the original code goes to David Gilbert.

import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import org.apache.batik.anim.dom.SVGDOMImplementation;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.svg.SVGDocument;

import java.awt.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

public class QRCodesBatik {

    public static void main(String[] args) throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrCodeWriter.encode("https://xmlgraphics.apache.org/batik/",
                BarcodeFormat.QR_CODE, 800, 800);

        // Create a document with the appropriate namespace
        DOMImplementation domImpl = SVGDOMImplementation.getDOMImplementation();
        SVGDocument document = (SVGDocument) domImpl.createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, "svg", null);
        // Create an instance of the SVG Generator
        org.apache.batik.svggen.SVGGraphics2D g2 = new org.apache.batik.svggen.SVGGraphics2D(document);

        // draw onto the SVG Graphics object
        g2.setColor(Color.BLACK);

        for (int xIndex = 0; xIndex < bitMatrix.getWidth(); xIndex = xIndex + bitMatrix.getRowSize()) {
            for (int yIndex = 0; yIndex < bitMatrix.getWidth(); yIndex = yIndex + bitMatrix.getRowSize()) {
                if (bitMatrix.get(xIndex, yIndex)) {
                    g2.fillRect(xIndex, yIndex, bitMatrix.getRowSize(), bitMatrix.getRowSize());
                }
            }
        }

        try (Writer out = new OutputStreamWriter(new FileOutputStream(new File("qrtest.svg")), "UTF-8")) {
            g2.stream(out, true);
        }
    }
}
Propellant answered 26/8, 2019 at 16:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.