在矢量图像中创建QR码

15

我能够成功地使用ZXing创建QR Code PNG图像,但是没有简单的方法可以将输出作为SVG或EPS格式获取。

如何从由QRCodeWriter创建的BitMatrix对象中创建矢量图像?


我会质疑您需要矢量格式的必要性。QR码是一组完美的正方形黑白像素。它可以无限缩放而不会失去保真度。因此,我建议保存为PNG格式。 - Terence Eden
5
世界上仍有一些设备无法处理PNG格式,激光雕刻机就是其中之一。这就是为什么我需要矢量格式的原因。 - Micha Roon
5个回答

11

虽然这是一个老问题,但是对于任何需要了解如何做到这一点的人来说,将 ZXingJFreeSVG (http://www.jfree.org/jfreesvg) 连接起来非常容易,例如:

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

}

1
这对我没用 - 创建的SVG不是有效的QR码,并且与BitMatrix中的PNG图像明显不同。接近,但有损坏。 - antonyh
对于某些尺寸,比如144x144,它无法正常工作。 - ibrahim demir

9

您甚至可以仅使用zxing而无需其他库来完成此操作:

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

请注意,上述解决方案在内存消耗方面比使用Batik的DOM的解决方案更加高效。

你救了我的一天!非常感谢,这正是所需的。 - dae.eklen
这个可以运行,但是对于QR码来说输出似乎有点...大。105Kb,而png只有359字节。一个在线生成器用2Kb处理了相同的URL。我认为这种方法是可行的,但需要优化。 - antonyh
已解决:在“encode()”中调整大小有助于管理输出,并且效果很好。感谢这个简洁的解决方案。 - antonyh
具体来说,将宽度和高度设为零会导致ZXing生成与QR码尺寸(包括“安静区域”)相匹配的矩阵。 - Mike Kaganski
这里的 style 元素是多余的(对于路径来说,填充默认为黑色:请参见 developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill#path)。不清楚为什么要使用两个字符串生成器:更简单的方法是将 XML 的第一部分添加到单个生成器中,然后循环遍历数组添加像素,最后添加其余的 XML 部分。M 前面的空格完全是可选的,并且只有帮助使最终结果可读性更强。 - Mike Kaganski

1
我发现最简单的方法是使用iText创建PDF,然后将生成的PDF转换为EPS或SVG。以下是创建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();
   }

然后我使用ImageMagick进行转换。就像这样:

convert qr-code.pdf qr-code.eps

对于 SVG,无法做到同样的效果。

convert qr-code.pdf qr-code.svg

这个不起作用

我使用一些长内容测试了这段代码,它可以处理多达600个字符。这可能取决于手机相机或屏幕的精度。

希望这能帮助到某个人。


如果您不需要UTF-8,可以省略ByteBuffer和hints,直接对字符串进行编码。 - Micha Roon
1
个人而言,我会使用 inkscape 追踪 来处理这个 PNG 图像,但如果这个答案可行,你也可以使用它。 - comp500
不需要描边,填充可以在最后一次完成。(我没有用itext测试过,但PDF规范是这样的) - Tilman Hausherr

1
你可以使用 Apache Batik 替代 JFreeSVG,如果你想要更自由的许可证。当你使用包含 Apache Batik 的瞬态依赖项的 Apache FOP 时,这尤其有意义。

这是一个调整过的版本。感谢 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);
        }
    }
}

0

Svg格式是一组构建BitMatrix的矩形。该格式的理念是图像的分辨率不受尺寸增加的影响。例如,@siom的解决方案为每个像素创建1x1的矩形(这就是为什么它创建了一个巨大的文件)。我将这个解决方案归类为对这个问题的蛮力方法。

我开发了一个更好的解决方案,运行时间为O(n^2)。它扫描整个BitMatrix,并首先在每个点检测最大长度的正方形,然后尝试在每个维度上扩展正方形为矩形。最后,它以各种尺寸绘制所有可能的矩形和正方形。

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

值得一提。很棒的解决方案。变量命名有一些小问题,但是当你得到一个QR码svg时,谁会在意呢...谢谢! - theINtoy
有一个静态方法可以直接获取区块:https://github.com/zxing/zxing/blob/735079ea7fd1bef60dfa7528caf08af3c27a8f3e/core/src/main/java/com/google/zxing/qrcode/encoder/Encoder.java#L77 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接