JavaでのQRコード生成と外部フォントファイル読み込み問題の解決策

QRコードの生成

最近、QRコードをエクスポートするAPIを開発しました。その要件の一つは、QRコードの下部にテキストを追加することでした。以下にQRコード生成の実装コードを示します:

コントローラ層


@Operation(summary = "組織QRコードのエクスポート", description = "組織QRコードをエクスポートします")
@GetMapping("/orgCode")
public void exportOrgCode(@RequestParam("url") String url, 
                         @RequestParam("orgIds") List<Long> orgIds, 
                         @RequestParam("channels") List<String> channels, 
                         HttpServletResponse response) throws IOException {
    // QRコード生成データを取得
    byte[] qrData = orgCodeService.generateQRCodePackage(url, orgIds, channels);
    response.reset();
    // ファイルを添付ファイルとして指定、ファイル名を"QRコード.zip"に設定
    response.setHeader(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, 
                      "attachment;filename=" + URLEncoder.encode("QRコード.zip", "UTF-8"));
    // レスポンスデータの長さを設定
    response.addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(qrData.length));
    // レスポンスタイプをバイナリストリームに設定
    response.setContentType("application/octet-stream; charset=UTF-8");
    // QRコードデータをレスポンスの出力ストリームに書き込み、ファイルダウンロードを完了
    IoUtil.write(response.getOutputStream(), Boolean.TRUE, qrData);
}

サービス層


byte[] generateQRCodePackage(String url, List<Long> orgIdList, List<String> channels) throws IOException;

実装クラス


@Slf4j
@Service
public class QRCodeGeneratorServiceImpl implements QRCodeGeneratorService {

    // QRコードのデフォルト幅と高さ
    private static final int QR_WIDTH = 756;
    private static final int QR_HEIGHT = 850;

    // QRコードのスケールとファイルタイプ
    private static final int SCALE_FACTOR = 2;
    private static final String IMAGE_FORMAT = "png";

    // フォントサイズ、角の半径、テキスト位置
    private static final int FONT_SIZE = 50;
    private static final int CORNER_RADIUS = 250;
    private static final int TEXT_MARGIN = 50;
    private static final int LARGE_FONT_SIZE = 23 * SCALE_FACTOR;

    @Resource
    private OrganizationService organizationService;

    /**
     * URL、組織IDリスト、チャネルリストに基づいてQRコードを生成し、
     * 圧縮してフロントエンドに返却
     */
    @Override
    public byte[] generateQRCodePackage(String url, List<Long> orgIdList, List<String> channels) throws IOException {
        // パラメータ検証
        Objects.requireNonNull(url, "URLはnullにできません");
        Objects.requireNonNull(orgIdList, "組織IDリストはnullにできません");
        Objects.requireNonNull(channels, "チャネルリストはnullにできません");
        url = URLDecoder.decode(url, StandardCharsets.UTF_8);
        log.info("URLパラメータ: {}", url);

        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        try (ZipOutputStream zos = new ZipOutputStream(stream)) {
            for (Long orgId : orgIdList) {
                // 組織名を取得
                String orgName = getOrganizationName(orgId);
                for (String channel : channels) {
                    String qrContent = url + "?orgId=" + orgId + "&channel=" + channel;
                    Color color = getChannelColor(channel);
                    byte[] qrCodeBytes = createQRCodeImage(qrContent, 
                            channel.equals(OrgChannelConstant.ON_SITE_CODE), 
                            orgName, color);
                    String fileName = orgName + "_" + OrgCodeConstant.getChannelName(Integer.parseInt(channel)) + ".png";
                    ZipEntry entry = new ZipEntry(fileName);
                    zos.putNextEntry(entry);
                    zos.write(qrCodeBytes);
                    zos.closeEntry();
                }
            }
        }
        return stream.toByteArray();
    }

    // 組織名を取得
    private String getOrganizationName(Long orgId) {
        OrganizationVo organization = organizationService.selectByOrgId(orgId);
        Assert.notNull(organization, "指定された組織情報が存在しません");
        return organization.getName();
    }

    // QRコード画像を生成
    private byte[] createQRCodeImage(String qrContent, boolean isSharpCorner, String orgName, Color color) {
        try {
            // QRコードパラメータ設定
            QRCodeWriter qrWriter = new QRCodeWriter();
            BitMatrix bitMatrix = qrWriter.encode(qrContent, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);

            // BufferedImageオブジェクトを作成し、背景色を設定
            BufferedImage qrImage = new BufferedImage(QR_WIDTH, QR_HEIGHT, BufferedImage.TYPE_INT_RGB);
            Graphics2D graphics = qrImage.createGraphics();
            graphics.setColor(Color.WHITE);
            graphics.fillRect(0, 0, QR_WIDTH, QR_HEIGHT);
            graphics.setColor(color);

            // BitMatrixをBufferedImageに変換
            for (int i = 0; i < QR_WIDTH; i++) {
                for (int j = 0; j < QR_HEIGHT; j++) {
                    if (bitMatrix.get(i, j)) {
                        graphics.fillRect(i, j, 1, 1);
                    }
                }
            }

            Font font;
            FontResource fontResource = new FontResource("/usr/share/fonts/SIMSUN.TTC");
            try {
                if (fontResource.exists()) {
                    font = Font.createFont(Font.TRUETYPE_FONT, fontResource.getFile());
                    font = font.deriveFont(Font.BOLD, LARGE_FONT_SIZE);
                    log.info("フォントファイルの読み込みに成功しました");
                } else {
                    font = new Font("SimSun", Font.BOLD, LARGE_FONT_SIZE);
                }
            } catch (IOException | FontFormatException e) {
                log.error("フォントファイルの読み込みに失敗: {}", e.getMessage());
                font = new Font("SimSun", Font.BOLD, LARGE_FONT_SIZE); // 読み込み失敗時はデフォルトフォントを使用
            }

            graphics.setFont(font);
            FontMetrics metrics = graphics.getFontMetrics(font);
            int textWidth = metrics.stringWidth(orgName);
            // 中央揃えのX座標を計算
            int textX = (QR_WIDTH - textWidth) / 2;
            // ピクセル調整、テキストを下部に配置
            int textY = QR_HEIGHT - TEXT_MARGIN;
            // 組織名を中央に描画
            graphics.drawString(orgName, textX, textY);

            // QRコードの角を丸くする
            if (!isSharpCorner) {
                BufferedImage roundedImage = applyRoundedCorners(qrImage, CORNER_RADIUS);
                qrImage = roundedImage;
            }

            // QRコード画像をバイト配列に保存
            try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                ImageIO.write(qrImage, IMAGE_FORMAT, outputStream);
                return outputStream.toByteArray();
            } catch (IOException e) {
                log.error("QRコード画像の保存に失敗: {}", e.getMessage());
                throw new RuntimeException("QRコード画像の保存に失敗しました", e);
            }

        } catch (WriterException e) {
            log.error("QRコードの生成に失敗: {}", e.getMessage());
            throw new RuntimeException("QRコードの生成に失敗しました", e);
        }
    }

    // 丸い角を持つ画像を作成
    private static BufferedImage applyRoundedCorners(BufferedImage image, int radius) {
        int width = image.getWidth();
        int height = image.getHeight();
        // 新しい透明背景のBufferedImageを作成
        BufferedImage roundedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2 = roundedImage.createGraphics();
        // 合成モードをSrcに設定
        g2.setComposite(AlphaComposite.Src);
        // アンチエイリアシングを有効に
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        // 背景色を白に設定
        g2.setColor(Color.WHITE);
        // 丸い角の矩形を描画
        g2.fill(new RoundRectangle2D.Float(0, 0, width, height, radius, radius));
        // 合成モードをSrcAtopに設定
        g2.setComposite(AlphaComposite.SrcAtop);
        // 元の画像を描画
        g2.drawImage(image, 0, 0, null);
        // リソースを解放
        g2.dispose();
        return roundedImage;
    }

    // チャネルに基づいて色を設定
    private Color getChannelColor(String channel) {
        switch (channel) {
            case "1":
                return Color.BLACK;
            case "2":
                return new Color(37, 82, 151); // #255297
            default:
                return Color.BLACK;
        }
    }
}

外部フォントファイル読み込み時のエラー原因と解決策

上記のコードでは、ローカル環境では問題なく動作します。しかし、本番サーバー上ではフォントファイルが存在しない場合に文字化けが発生する可能性があります。この問題を解決する方法を以下に示します。

1. フォントファイルの破損を確認

まず、使用しているフォントファイルが破損していないか確認します。最も良いテスト方法は、ローカル環境でPostmanを使用してテストすることです。生成されるテキストがシステムのデフォルトフォントの場合、ファイルが破損しています。希望するフォント(コード例のSimSunなど)が表示される場合は、ファイルは正常です。

2. Maven設定の修正

フォントファイルが破損していないにもかかわらず読み込みに失敗する場合、pom.xmlファイルでフォントファイルがjarパッケージング時に除外されないように設定する必要があります。


<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <configuration>
            <!-- 転換不要なファイル拡張子 >
            <nonFilteredFileExtensions>
                <nonFilteredFileExtension>ttf</nonFilteredFileExtension>
                <nonFilteredFileExtension>xlsx</nonFilteredFileExtension>
                <nonFilteredFileExtension>xls</nonFilteredFileExtension>
                <nonFilteredFileExtension>zip</nonFilteredFileExtension>
                <nonFilteredFileExtension>cer</nonFilteredFileExtension>
                <nonFilteredFileExtension>pfx</nonFilteredFileExtension>
                <nonFilteredFileExtension>py</nonFilteredFileExtension>
            </nonFilteredFileExtensions>
        </configuration>
    </plugin>
</plugins>

3. サーバー上のフォントファイルの配置

上記の設定を行っても問題が解決しない場合、サーバー上にフォントファイルが存在するか確認する必要があります。Dockerfileに外部ファイルをコピーするコマンドを追加します。


COPY ./SIMSUN.TTC /usr/share/fonts/SIMSUN.TTC

フォントファイルは、サーバーの指定されたディレクトリに配置されていることを確認してください。この問題に遭遇した場合は、必ず上記の手順を順番に確認してください。

タグ: QRコード Java フォント 二次元マトリックス Docker

5月18日 16:08 投稿