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?

Tilman Hausherr
  • 17,731
  • 7
  • 58
  • 97
Micha Roon
  • 3,957
  • 2
  • 30
  • 48
  • 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. – Terence Eden May 29 '12 at 07:49
  • 5
    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 format – Micha Roon May 30 '12 at 08:24

5 Answers5

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

}
Ali Akdurak
  • 3,841
  • 1
  • 18
  • 16
David Gilbert
  • 4,427
  • 14
  • 22
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.

siom
  • 1,610
  • 1
  • 14
  • 21
  • you saved my day! thank you so much, it's exactly what was needed) – dae.eklen Jun 25 '20 at 11:18
  • 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. – antonyh Jan 19 '21 at 16:37
  • Solved: tweaking the size in 'encode()' helps manage the output, and works well. Thanks for this neat solution. – antonyh Jan 19 '21 at 16:59
  • Specifically, passing zeroes as width and height would cause ZXing to produce the matrix that matches the QR code dimensions (including "quiet zone"). – Mike Kaganski Mar 16 '21 at 15:30
  • `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. – Mike Kaganski Mar 17 '21 at 08:29
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

Micha Roon
  • 3,957
  • 2
  • 30
  • 48
  • If you don't need UTF-8 you can omit the ByteBuffer and the hints and encode your String directly – Micha Roon May 28 '12 at 18:39
  • 1
    personally I would use [inkscape tracing](http://inkscape.org/doc/tracing/tutorial-tracing.html) on the PNG to do this, but if this answer works you can use it. – comp500 Apr 20 '14 at 18:55
  • 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) – Tilman Hausherr Oct 22 '21 at 10:39
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);
        }
    }
}
mkdev
  • 972
  • 9
  • 12
0

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);
    }
  }
}
ibrahim demir
  • 421
  • 3
  • 16
  • Deserves some recognition. Wonderful solution. Some minor issues with the variable naming, but who cares, when you get a QR Code svg... Thanks! – theINtoy Jun 28 '21 at 14:02