| 庫 | Maven 坐標 | 特點 | 適用場景 |
|---|---|---|---|
| ZXing (Zebra Crossing) | com.google.zxing:core:3.5.1com.google.zxing:javase:3.5.1 | Google 維護、功能最全、支持 1D/2D 多種碼制、可讀性強、活躍社區 | 首選:生產環境生成/解析、Android、多格式需求 |
| QRGen | net.glxn:qrgen:2.6.0 | ZXing 封裝、API 極簡 (鏈式調用)、內建圖片輸出 | 快速原型、簡單生成需求、不想依賴 javase |
| Swetake QRCode | jp.sourceforge.qrcode:qrcode:1.0 | 日系老牌、純 Java、無額外依賴、維護停滯 | 舊項目維護、極簡依賴環境 |
| Google Chart API (已停止) | HTTP API | 無需本地庫、URL 即生成 | 已棄用,**嚴禁新項目使用** |
public final class QrCodeGenerator {
private static final Map DEFAULT_HINTS = Map.of(
EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M, // L(7%) M(15%) Q(25%) H(30%)
EncodeHintType.CHARACTER_SET, "UTF-8",
EncodeHintType.MARGIN, 2 // 靜默區模塊數
);
private QrCodeGenerator() {}
/**
* 生成帶 Logo 的二維碼
* @param content 內容
* @param width 寬度 (px)
* @param height 高度 (px)
* @param logoPath Logo 文件路徑 (可選)
* @param logoRatio Logo 佔比 (0.15-0.3 建議)
* @return BufferedImage
*/
public static BufferedImage generate(String content, int width, int height,
String logoPath, double logoRatio) throws WriterException, IOException {
BitMatrix matrix = new MultiFormatWriter()
.encode(content, BarcodeFormat.QR_CODE, width, height, DEFAULT_HINTS);
BufferedImage image = MatrixToImageWriter.toBufferedImage(matrix);
if (StringUtils.isNotBlank(logoPath)) {
embedLogo(image, logoPath, logoRatio);
}
return image;
}
private static void embedLogo(BufferedImage qrImage, String logoPath, double ratio) throws IOException {
BufferedImage logo = ImageIO.read(new File(logoPath));
int qrWidth = qrImage.getWidth();
int qrHeight = qrImage.getHeight();
int logoWidth = (int) (qrWidth * ratio);
int logoHeight = (int) (qrHeight * ratio);
int x = (qrWidth - logoWidth) / 2;
int y = (qrHeight - logoHeight) / 2;
Graphics2D g = qrImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(logo, x, y, logoWidth, logoHeight, null);
// 繪製白色圓角矩形背景,提高識別率
g.setColor(Color.WHITE);
g.fillRoundRect(x - 2, y - 2, logoWidth + 4, logoHeight + 4, 8, 8);
g.drawImage(logo, x, y, logoWidth, logoHeight, null);
g.dispose();
}
/** 寫入文件 */
public static void writeToFile(BufferedImage image, String filePath, String format) throws IOException {
ImageIO.write(image, format, new File(filePath));
}
/** 寫入 OutputStream (Web 直接輸出) */
public static void writeToStream(BufferedImage image, OutputStream out, String format) throws IOException {
ImageIO.write(image, format, out);
}
/** 生成 Base64 Data URI (前端直接
) */
public static String toBase64DataUri(BufferedImage image, String format) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, format, baos);
return "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(baos.toByteArray());
}
}
}
public final class QrCodeParser {
private static final Map DEFAULT_HINTS = Map.of(
DecodeHintType.CHARACTER_SET, "UTF-8",
DecodeHintType.TRY_HARDER, Boolean.TRUE, // 更積極嘗試
DecodeHintType.PURE_BARCODE, Boolean.FALSE, // 非純碼圖片
DecodeHintType.POSSIBLE_FORMATS, List.of(BarcodeFormat.QR_CODE)
);
private QrCodeParser() {}
public static String parse(File imageFile) throws NotFoundException, IOException {
return parse(ImageIO.read(imageFile));
}
public static String parse(InputStream inputStream) throws NotFoundException, IOException {
return parse(ImageIO.read(inputStream));
}
public static String parse(byte[] imageBytes) throws NotFoundException, IOException {
try (ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes)) {
return parse(ImageIO.read(bis));
}
}
public static String parse(Base64String base64) throws NotFoundException, IOException {
byte[] bytes = Base64.getDecoder().decode(base64.getValue());
return parse(bytes);
}
private static String parse(BufferedImage image) throws NotFoundException {
// 嘗試多種旋轉角度 (0, 90, 180, 270) 提高識別率
for (int angle : new int[]{0, 90, 180, 270}) {
BufferedImage rotated = angle == 0 ? image : rotateImage(image, angle);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(rotated)));
try {
Result result = new MultiFormatReader().decode(bitmap, DEFAULT_HINTS);
return result.getText();
} catch (NotFoundException ignored) {}
}
throw new NotFoundException("所有角度均無法識別");
}
private static BufferedImage rotateImage(BufferedImage src, int angle) {
double rad = Math.toRadians(angle);
int w = src.getWidth(), h = src.getHeight();
int newW = (int) Math.round(Math.abs(w * Math.cos(rad)) + Math.abs(h * Math.sin(rad)));
int newH = (int) Math.round(Math.abs(w * Math.sin(rad)) + Math.abs(h * Math.cos(rad)));
BufferedImage dest = new BufferedImage(newW, newH, src.getType());
Graphics2D g = dest.createGraphics();
g.translate(newW / 2, newH / 2);
g.rotate(rad);
g.translate(-w / 2, -h / 2);
g.drawImage(src, 0, 0, null);
g.dispose();
return dest;
}
}
@RestController
@RequestMapping("/api/qrcode")
@RequiredArgsConstructor
public class QrCodeController {
@GetMapping("/generate")
public void generate(@RequestParam String content,
@RequestParam(defaultValue = "300") int size,
@RequestParam(required = false) String logo,
HttpServletResponse response) throws Exception {
BufferedImage image = QrCodeGenerator.generate(content, size, size, logo, 0.2);
response.setContentType("image/png");
QrCodeGenerator.writeToStream(image, response.getOutputStream(), "PNG");
}
@PostMapping("/parse")
public Result parse(@RequestParam("file") MultipartFile file) {
try {
String text = QrCodeParser.parse(file.getInputStream());
return Result.success(text);
} catch (Exception e) {
return Result.error("解析失敗: " + e.getMessage());
}
}
@PostMapping("/parse-base64")
public Result parseBase64(@RequestBody Map body) {
try {
String text = QrCodeParser.parse(new Base64String(body.get("base64")));
return Result.success(text);
} catch (Exception e) {
return Result.error("解析失敗: " + e.getMessage());
}
}
}
嵌入 Logo 會佔用糾錯容量,建議級別至少 M,Logo 面積 ≤ 20%,並留白邊緣。
| 現象 | 原因 | 解決 |
|---|---|---|
| 生成圖片模糊/掃不出 | 尺寸過小、邊距為 0、錯誤級別過低 | 最小 200x200px、MARGIN≥2、級別 M+ |
| 中文內容亂碼 | 未指定 CHARACTER_SET=UTF-8 | Hints 必須設置 EncodeHintType.CHARACTER_SET |
| 解析失敗 (NotFoundException) | 圖片噪點大、旋轉、光照不均、Logo 遮擋過多 | TRY_HARDER=true、多角度嘗試、預處理(銳化/二值化) |
| ZXing 依賴衝突 | 傳遞依賴引入舊版 core | mvn dependency:tree 排查、統一版本、排除舊傳遞 |
ZXing 是 Java 二維碼領域的事實標準。**封裝統一生成/解析入口、預設合理 Hints、支持 Logo 嵌入與多角度識別、提供 Base64/流/文件多種輸出**,即可滿足 99% 業務需求。選型時堅持:新項目只用 ZXing,舊項目遷移 ZXing,拒絕已停維的 Swetake/Google Chart API。