ChartServlet.java

/*
 * Copyright 2013 ZXing authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.zxing.web;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.CharStreams;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import jakarta.servlet.ServletRequest;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;

/**
 * A reimplementation of the
 * <a href="https://google-developers.appspot.com/chart/infographics/docs/qr_codes">
 * Google Chart Server's QR code encoder</a>, which is now deprecated. See
 * <a href="https://github.com/zxing/zxing/wiki/Chart-Server-Parameters">the chart server
 * parameters wiki page</a> for docs.
 *
 * @author Sean Owen
 */
@WebServlet({"/w/chart", "/w/chart.png", "/w/chart.gif", "/w/chart.jpg", "/w/chart.jpeg"})
public final class ChartServlet extends HttpServlet {

  private static final int MAX_DIMENSION = 4096;
  private static final Collection<Charset> SUPPORTED_OUTPUT_ENCODINGS = ImmutableSet.<Charset>builder()
      .add(StandardCharsets.UTF_8).add(StandardCharsets.ISO_8859_1).add(Charset.forName("Shift_JIS")).build();

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    doEncode(request, response, false);
  }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
    doEncode(request, response, true);
  }

  private static void doEncode(HttpServletRequest request, HttpServletResponse response, boolean isPost)
      throws IOException {

    ChartServletRequestParameters parameters;
    try {
      parameters = doParseParameters(request, isPost);
    } catch (IllegalArgumentException | NullPointerException e) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.toString());
      return;
    }

    Map<EncodeHintType,Object> hints = new EnumMap<>(EncodeHintType.class);
    hints.put(EncodeHintType.MARGIN, parameters.getMargin());
    if (!StandardCharsets.ISO_8859_1.equals(parameters.getOutputEncoding())) {
      // Only set if not QR code default
      hints.put(EncodeHintType.CHARACTER_SET, parameters.getOutputEncoding().name());
    }
    hints.put(EncodeHintType.ERROR_CORRECTION, parameters.getEcLevel());

    BitMatrix matrix;
    try {
      matrix = new QRCodeWriter().encode(parameters.getText(),
                                         BarcodeFormat.QR_CODE,
                                         parameters.getWidth(),
                                         parameters.getHeight(),
                                         hints);
    } catch (WriterException we) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST, we.toString());
      return;
    }

    String requestURI = request.getRequestURI();
    if (requestURI == null) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
      return;
    }
    int lastDot = requestURI.lastIndexOf('.');
    String imageFormat;
    if (lastDot > 0) {
      imageFormat = requestURI.substring(lastDot + 1).toUpperCase(Locale.ROOT);
      // Special-case jpg -> JPEG
      if ("JPG".equals(imageFormat)) {
        imageFormat = "JPEG";
      }
    } else {
      imageFormat = "PNG";
    }

    String contentType;
    switch (imageFormat) {
      case "PNG":
        contentType = "image/png";
        break;
      case "JPEG":
        contentType = "image/jpeg";
        break;
      case "GIF":
        contentType = "image/gif";
        break;
      default:
        throw new IllegalArgumentException("Unknown format " + imageFormat);
    }

    ByteArrayOutputStream imageOut = new ByteArrayOutputStream(1024);
    MatrixToImageWriter.writeToStream(matrix, imageFormat, imageOut);
    byte[] imageData = imageOut.toByteArray();

    response.setContentType(contentType);
    response.setContentLength(imageData.length);
    response.setHeader("Cache-Control", "public");
    response.getOutputStream().write(imageData);
  }

  private static ChartServletRequestParameters doParseParameters(ServletRequest request, boolean readBody)
      throws IOException {

    String chartType = request.getParameter("cht");
    Preconditions.checkArgument(chartType == null || "qr".equals(chartType), "Bad type");

    String widthXHeight = request.getParameter("chs");
    Preconditions.checkNotNull(widthXHeight, "No size");
    int xIndex = widthXHeight.indexOf('x');
    Preconditions.checkArgument(xIndex >= 0, "Bad size");

    int width = Integer.parseInt(widthXHeight.substring(0, xIndex));
    int height = Integer.parseInt(widthXHeight.substring(xIndex + 1));
    Preconditions.checkArgument(width > 0 && height > 0, "Bad size");
    Preconditions.checkArgument(width <= MAX_DIMENSION && height <= MAX_DIMENSION, "Bad size");

    String outputEncodingName = request.getParameter("choe");
    Charset outputEncoding;
    if (outputEncodingName == null) {
      outputEncoding = StandardCharsets.UTF_8;
    } else {
      outputEncoding = Charset.forName(outputEncodingName);
      Preconditions.checkArgument(SUPPORTED_OUTPUT_ENCODINGS.contains(outputEncoding), "Bad output encoding");
    }

    ErrorCorrectionLevel ecLevel = ErrorCorrectionLevel.L;
    int margin = 4;

    String ldString = request.getParameter("chld");
    if (ldString != null) {
      int pipeIndex = ldString.indexOf('|');
      if (pipeIndex < 0) {
        // Only an EC level
        ecLevel = ErrorCorrectionLevel.valueOf(ldString);
      } else {
        ecLevel = ErrorCorrectionLevel.valueOf(ldString.substring(0, pipeIndex));
        margin = Integer.parseInt(ldString.substring(pipeIndex + 1));
        Preconditions.checkArgument(margin > 0, "Bad margin");
      }
    }

    String text;
    if (readBody) {
      text = CharStreams.toString(request.getReader());
    } else {
      text = request.getParameter("chl");
    }
    Preconditions.checkArgument(text != null && !text.isEmpty(), "No input");

    return new ChartServletRequestParameters(width, height, outputEncoding, ecLevel, margin, text);
  }

}