Categories
Development Java

QR Codes in Java

Für ein Projekt soll ich einen QR-Code generieren, der eine URL beinhaltet, die den Zugriff auf eine bestimmte Ressource ermöglicht.

In meinem letzten Post habe ich dokumentiert, wie die URL-Parameter verschlüsselt werden können.

Um mich mich dem Thema der QR Code Generierung zu öffnen habe ich insbesondere diese beiden Artikel verwendet: Generating Barcodes and QR Codes in Java auf Baeldung und Generating QR Code in Java auf Javapoint.

Die Wahl der Library fiel auf ZXing (“zebra crossing”), denn das ist die main library that supports QR codes in Java. und ich habe keine Anhaltspunkte finden können, warum ich eine andere Library nehmen sollte.

ZXing

Here, we need to add two Maven dependencies: the core image library and the Java client:

<dependencies>
  <!-- ZXing for QR-Code Generation -->
  <!-- https://mvnrepository.com/artifact/com.google.zxing/core -->
  <dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.5.1</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/com.google.zxing/javase -->
  <dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>3.5.1</version>
  </dependency>
</dependencies>

QR Code generieren:

package deringo;

import java.awt.Desktop;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.NotFoundException;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

public class TestMain {

    public static void main(String args[]) throws WriterException, IOException, NotFoundException {
        String data = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam";
        Path path = Paths.get("./test.png");
        // Encoding charset to be used
        String charset = "UTF-8";
        Map<EncodeHintType, ErrorCorrectionLevel> hashMap = new HashMap<EncodeHintType, ErrorCorrectionLevel>();
        // generates QR code with Low level(L) error correction capability
        hashMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
        // invoking the user-defined method that creates the QR code
        generateQRcode(data, path, charset, hashMap, 200, 200);// increase or decrease height and width accodingly

        System.out.println("QR Code created successfully.");
        Desktop.getDesktop().open(path.toFile());
    }

    public static void generateQRcode(String data, Path path, String charset, Map<EncodeHintType, ErrorCorrectionLevel> map, int h, int w)
            throws WriterException, IOException {
        BitMatrix matrix = new MultiFormatWriter().encode(new String(data.getBytes(charset), charset),
                BarcodeFormat.QR_CODE, w, h);
        MatrixToImageWriter.writeToPath(matrix, getExtension(path), path);
    }
    
    /**
     * Own function until we have this in JDK<br>
     * Finally, there is a new method Path#getExtension available right in the JDK as of Java 21:<br>
     * https://stackoverflow.com/questions/3571223/how-do-i-get-the-file-extension-of-a-file-in-java/74315488#74315488
     */
    public static String getExtension(Path path) {
        String extension = path.getFileName().toString().substring(path.getFileName().toString().lastIndexOf('.') + 1);
        return extension;
    }
}

ZXing und Docx4J

Der QR Code lässt sich schön als PNG generieren. Allerdings brauche ich den QR Code in einem Word Dokument. Das Word Dokument wird mit Docx4J generiert.

Den Einstieg in Docx4J habe ich bereits vollzogen. Besonders hilfreich ist Introduction To Docx4J auf Baeldung.

package deringo;

import java.awt.Desktop;
import java.io.File;
import java.nio.file.Files;

import org.docx4j.dml.wordprocessingDrawing.Inline;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.Drawing;
import org.docx4j.wml.Jc;
import org.docx4j.wml.JcEnumeration;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.PPr;
import org.docx4j.wml.R;

public class TestMain {

    public static void main(String[] args) throws Exception {
        WordprocessingMLPackage wordPackage = WordprocessingMLPackage.createPackage();
        MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart();
        mainDocumentPart.addStyledParagraphOfText("Title", "Welcome to my QR Code");
        mainDocumentPart.addParagraphOfText("Welcome to my QR Code");
        
        File image = new File("test.png" );
        byte[] fileContent = Files.readAllBytes(image.toPath());
        BinaryPartAbstractImage imagePart = BinaryPartAbstractImage
          .createImagePart(wordPackage, fileContent);
        Inline inline = imagePart.createImageInline(
          "QR Code Image (filename hint)", "Alt Text", 1, 2, false);
        P Imageparagraph = addImageToParagraph(inline);
        mainDocumentPart.getContent().add(Imageparagraph);
        
        File exportFile = new File("welcome.docx");
        wordPackage.save(exportFile);
        
        Desktop.getDesktop().open(exportFile);
    }
    
    private static P addImageToParagraph(Inline inline) {
        ObjectFactory factory = new ObjectFactory();
        P p = factory.createP();
        R r = factory.createR();
        p.getContent().add(r);
        Drawing drawing = factory.createDrawing();
        r.getContent().add(drawing);
        drawing.getAnchorOrInline().add(inline);
        // center image
        PPr paragraphProperties = factory.createPPr();
        Jc justification = factory.createJc();
        justification.setVal(JcEnumeration.CENTER);
        paragraphProperties.setJc(justification);
        p.setPPr(paragraphProperties);
        
        return p;
    }
}

Das Ergebnis sieht brauchbar aus:

Allerdings habe ich die zuvor gespeicherte Datei verwendet. Ich möchte das aber on the fly machen, also ohne, dass ich den QR Code erst speichere und dann in das Word Dokument übernehme.

package deringo;

import java.awt.Desktop;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.Map;

import javax.imageio.ImageIO;

import org.docx4j.dml.wordprocessingDrawing.Inline;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.Drawing;
import org.docx4j.wml.Jc;
import org.docx4j.wml.JcEnumeration;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.PPr;
import org.docx4j.wml.R;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

public class TestMain {

    public static void main(String[] args) throws Exception {
        WordprocessingMLPackage wordPackage = WordprocessingMLPackage.createPackage();
        MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart();
        mainDocumentPart.addStyledParagraphOfText("Title", "Welcome to my QR Code");
        mainDocumentPart.addParagraphOfText("Welcome to my QR Code");
        
        BinaryPartAbstractImage imagePart = BinaryPartAbstractImage
                .createImagePart(wordPackage, getQRCode());
        
        Inline inline = imagePart.createImageInline(
          "QR Code Image (filename hint)", "Alt Text", 1, 2, false);
        P Imageparagraph = addImageToParagraph(inline);
        mainDocumentPart.getContent().add(Imageparagraph);
        
        File exportFile = new File("welcome.docx");
        wordPackage.save(exportFile);
        
        Desktop.getDesktop().open(exportFile);
    }
    
    private static P addImageToParagraph(Inline inline) {
        ObjectFactory factory = new ObjectFactory();
        P p = factory.createP();
        R r = factory.createR();
        p.getContent().add(r);
        Drawing drawing = factory.createDrawing();
        r.getContent().add(drawing);
        drawing.getAnchorOrInline().add(inline);
        // center image
        PPr paragraphProperties = factory.createPPr();
        Jc justification = factory.createJc();
        justification.setVal(JcEnumeration.CENTER);
        paragraphProperties.setJc(justification);
        p.setPPr(paragraphProperties);
        
        return p;
    }
    
    public static byte[] getQRCode() throws Exception {
        String data = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam";
        // Encoding charset to be used
        String charset = "UTF-8";
        Map<EncodeHintType, ErrorCorrectionLevel> hashMap = new HashMap<EncodeHintType, ErrorCorrectionLevel>();
        // generates QR code with Low level(L) error correction capability
        hashMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
     
        BitMatrix matrix = new MultiFormatWriter().encode(new String(data.getBytes(charset), charset),
                BarcodeFormat.QR_CODE, 200, 200);
        BufferedImage bi = MatrixToImageWriter.toBufferedImage(matrix);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(bi, "png", baos);
        byte[] bytes = baos.toByteArray();
        return bytes;
    }
}

Categories
Development Java

Docx4J

Ein Report soll als Word Dokument (.docx) verfügbar gemacht werden.

Dazu wird zuerst eine Word Datei als Template erstellt mit Platzhaltern an den entsprechenden Stellen. Die Ersetzung einzelner Wörter ist relativ trivial, das Befüllen einer Tabelle mit n Zeilen stellte sich dann etwas kniffliger heraus. Diese Notizen und das dazugehörige Beispielprojekt dienen als Erinnerungsstütze, falls der Zukunftsingo nach dem aktuellen Projekt noch einmal etwas mit docx4j machen darf.

Word Template erstellen

Word mit einem leeren Dokument öffnen, einen Platzhalter für eine Überschrift, einen Text und eine Tabelle hinzufügen.

Die Überschriften der Tabelle sind fix und werden nicht ersetzt.
Die Anzahl der Zeilen in dem Report ist dynamisch.
Die Tabelle erhält keine Zeilen, diese werden später hineingeneriert.

Würde der Report eine Tabelle mit einer fixen Anzahl von Zeilen vorsehen, könnte man die ganze Tabelle in das Dokument eintragen und und später die einzelnen Werte per Wortersetzung hineingenerieren.

Die Tabelle bekommt noch einen Titel, um sie so eindeutig identifizieren zu können. Beispielsweise wenn das Dokument mehrere Tabellen enthält.

Abschließend unter dem Namen template.docx als Word-Dokument speichern, nicht als Strict Open XML-Dokument!

Projekt einrichten

Ein neues Maven Projekt anlegen.

Die Datei template.docx nach src/main/resources kopieren.

Java Klasse anlegen, die das Template läd und eine neue Zieldatei im tmpdir mit teilgeneriertem Name anlegt. Dadurch erhalten wir bei jedem Durchlauf eine neue Datei, um sie ggf. noch einmal miteinander vergleichen zu können, und das Aufräumen übernimmt das Betriebssystem für uns.

Als Dummyaction wird das Template in die Zieldatei kopiert. An dieser Stelle kommt im nächsten Schritt Docx4J ins Spiel um das Template zu befüllen.

Im letzten Schritt lassen wir uns die Zieldatei mit Word öffnen um so direkt das Ergebnis überprüfen zu können.

public class Main {

    public static void main(String[] args) throws Exception {
        File template = getTemplateFile();
        File output   = getOutputFile();
        
        Files.copy(Paths.get(template.toURI()), Paths.get(output.toURI()), StandardCopyOption.REPLACE_EXISTING);

        Desktop.getDesktop().open(output);
    }
    
    private static File getTemplateFile() throws Exception {
        File template = new File( Main.class.getClassLoader().getResource("template.docx").toURI() );
        return template;
    }

    private static File getOutputFile() throws Exception {
        String tmpdir = System.getProperty("java.io.tmpdir");
        Path tmpfile = Files.createTempFile(Paths.get(tmpdir), "MyReport" + "-", ".docx");
        return tmpfile.toFile();
    }
}

Um das Template befüllen zu können legen wir einen Report mit Beispieldaten an:

public class Report {

    public String ueberschrift = "Sample Report";
    public String text = "Lorem Ipsum";
    public List<String[]> tabelle;
    
    public Report() {
        tabelle = new ArrayList<>();
        tabelle.add(new String[] {"Montag", "Regen", "4°C"});
        tabelle.add(new String[] {"Dienstag", "Wolkig", "6°C"});
        tabelle.add(new String[] {"Mittwoch", "Sonne", "10°C"});
    }
}

Docx4J

Projekt Homepage: Link

Projekt GitHub Repository: Link

Artikel zum Einstieg: Baeldung

Dem Projekt dank Maven einfach die notwendigen Abhängigkeiten in der pom.xml hinzufügen:

<dependencies>
  <!-- https://mvnrepository.com/artifact/org.docx4j/docx4j-JAXB-ReferenceImpl -->
  <dependency>
    <groupId>org.docx4j</groupId>
    <artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
    <version>11.4.9</version>
  </dependency>
</dependencies>

Template befüllen

Template laden

WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(template);

Text ersetzen

Wir suchen alle Texte und wenn ein Platzhalter gefunden wird, wird der Text mit dem Wert aus dem Report gefüllt:

private static void textErsetzen(WordprocessingMLPackage wordPackage, Report report) throws Exception {
  List<Object> texts = wordPackage.getMainDocumentPart().getJAXBNodesViaXPath(XPATH_TO_SELECT_TEXT_NODES, true);
  for (Object obj : texts) {  
    Text text = (Text) ((JAXBElement<?>) obj).getValue();

    String textValue;
    if ("ÜBERSCHRIFT1".equals(text.getValue())) {
      textValue = report.ueberschrift;
    } else if ("TEXT1".equals(text.getValue())) {
      textValue = report.text;
    } else {
      textValue = text.getValue();
    }

    text.setValue(textValue);
  } 
}

Resultat:

Tabellenzeilen erzeugen

Wir laufen über alle Elemente und wenn eine Tabelle gefunden wird, wird deren Titel überprüft und dann die Tabelle befüllt.

private static void tabellenzeilenErzeugen(WordprocessingMLPackage wordPackage, Report report) {
  for (Object o : wordPackage.getMainDocumentPart().getContent()) {
    if (o instanceof JAXBElement) {
      @SuppressWarnings("unchecked")
      Tbl tbl = ((JAXBElement<Tbl>)o).getValue();
      String tblCaption = tbl.getTblPr().getTblCaption() != null ? tbl.getTblPr().getTblCaption().getVal() : null;
      if ("MyTabelle1".equals(tblCaption)) {
        fillTabelle1(tbl, report);
      } else {
        System.out.println("tblCaption: " + tblCaption);
      }
    }
  }
}

private static void fillTabelle1(Tbl tbl, Report report) {
  for (String[] zeile : report.tabelle) {
    addRow(tbl, zeile[0], zeile[1], zeile[2]);
  }
}

Bei den Einträgen in der Tabelle setzen wir noch den gewünschten Font.

private static void addRow(Tbl tbl, String... cells) {
  ObjectFactory factory = new ObjectFactory();

  // setup Font
  String fontName = "Arial";
  RFonts fonts = factory.createRFonts();
  fonts.setAscii(fontName);
  fonts.setCs(fontName);
  fonts.setHAnsi(fontName);
  RPr rpr = factory.createRPr();
  rpr.setRFonts(fonts);

  // new Row
  Tr tr = factory.createTr();
  for (String cell : cells) {
    Tc tc = factory.createTc();

    P p = factory.createP();
    R r = factory.createR();
    r.setRPr(rpr);

    Text text = factory.createText();

    text.setValue(cell);
    r.getContent().add(text);
    p.getContent().add(r);

    tc.getContent().add(p);

    tr.getContent().add(tc);
  }
  tbl.getContent().add(tr);
}

Resultat:

Report speichern

wordPackage.save(output);