|
|
@@ -719,15 +719,15 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
BaseFont baseFont = getChineseFont(fontName);
|
|
|
|
|
|
// 3. 读取背景图和 logo
|
|
|
- Image bgImage = Image.getInstance(backgroundImageBytes);
|
|
|
- float pageWidth = bgImage.getWidth();
|
|
|
- float pageHeight = bgImage.getHeight();
|
|
|
-
|
|
|
- // 3.1 验证背景图片比例是否为3:2横屏
|
|
|
- float ratio = pageWidth / pageHeight;
|
|
|
- if (Math.abs(ratio - 1.5f) > 0.1f) { // 允许0.1的误差
|
|
|
- throw new IllegalArgumentException("背景图片比例不是3:2横屏比例,当前比例: " + String.format("%.2f", ratio));
|
|
|
- }
|
|
|
+ Image originalBgImage = Image.getInstance(backgroundImageBytes);
|
|
|
+ float originalWidth = originalBgImage.getWidth();
|
|
|
+ float originalHeight = originalBgImage.getHeight();
|
|
|
+
|
|
|
+ // 3.1 获取目标画布尺寸
|
|
|
+ float pageWidth = bibParam.getCanvasWidth() != null ? bibParam.getCanvasWidth().floatValue() : originalWidth;
|
|
|
+ float pageHeight = bibParam.getCanvasHeight() != null ? bibParam.getCanvasHeight().floatValue() : originalHeight;
|
|
|
+
|
|
|
+ log.info("原始背景图片尺寸: {}x{}, 目标画布尺寸: {}x{}", originalWidth, originalHeight, pageWidth, pageHeight);
|
|
|
|
|
|
Integer finalFontSize = fontSize;
|
|
|
// 4. 并行生成所有 PDF
|
|
|
@@ -741,8 +741,9 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
document.open();
|
|
|
PdfContentByte cb = writer.getDirectContent();
|
|
|
|
|
|
- // 添加背景图
|
|
|
+ // 添加背景图(缩放到目标尺寸)
|
|
|
Image bg = Image.getInstance(backgroundImageBytes);
|
|
|
+ bg.scaleToFit(pageWidth, pageHeight);
|
|
|
bg.setAbsolutePosition(0, 0);
|
|
|
document.add(bg);
|
|
|
|
|
|
@@ -750,9 +751,21 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
if (finalLogoImageBytes != null && logoX != null && logoY != null) {
|
|
|
try {
|
|
|
Image img = Image.getInstance(finalLogoImageBytes);
|
|
|
- // 应用缩放参数
|
|
|
- Double logoScale = bibParam.getLogoScale() != null ? bibParam.getLogoScale() : 1.0;
|
|
|
- img.scaleToFit(80 * logoScale.floatValue(), 80 * logoScale.floatValue());
|
|
|
+ // 使用传入的Logo尺寸(前端已计算好最终尺寸)
|
|
|
+ float scaledWidth, scaledHeight;
|
|
|
+ if (bibParam.getLogoWidth() != null && bibParam.getLogoHeight() != null) {
|
|
|
+ // 使用前端传入的Logo尺寸(已包含所有缩放计算)
|
|
|
+ scaledWidth = bibParam.getLogoWidth().floatValue();
|
|
|
+ scaledHeight = bibParam.getLogoHeight().floatValue();
|
|
|
+ log.debug("使用前端计算的Logo尺寸: {}x{}", scaledWidth, scaledHeight);
|
|
|
+ } else {
|
|
|
+ // 回退到原始缩放逻辑(兼容旧版本)
|
|
|
+ Double logoScale = bibParam.getLogoScale() != null ? bibParam.getLogoScale() : 1.0;
|
|
|
+ scaledWidth = img.getWidth() * logoScale.floatValue();
|
|
|
+ scaledHeight = img.getHeight() * logoScale.floatValue();
|
|
|
+ log.warn("使用回退缩放逻辑,Logo尺寸: {}x{}", scaledWidth, scaledHeight);
|
|
|
+ }
|
|
|
+ img.scaleToFit(scaledWidth, scaledHeight);
|
|
|
// 将百分比坐标转换为PDF像素坐标,并翻转Y轴
|
|
|
float logoPositionX = (float) (logoX * pageWidth / 100.0);
|
|
|
float logoPositionY = (float) (pageHeight - (logoY * pageHeight / 100.0));
|
|
|
@@ -820,7 +833,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
try {
|
|
|
String qrDataStr = getQrDataStr(eventName, groupName, teamNameMap, projectMap, athlete);
|
|
|
Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
|
|
|
- int baseQrSize = Math.min(150, (int) (Math.min(pageWidth, pageHeight) * 0.15));
|
|
|
+ int baseQrSize = 100; // 固定基础尺寸
|
|
|
int qrSize = (int) (baseQrSize * barcodeScale.floatValue());
|
|
|
|
|
|
// 将百分比坐标转换为PDF像素坐标,并翻转Y轴
|
|
|
@@ -862,7 +875,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
try {
|
|
|
String qrDataStr = getQrDataStr(eventName, groupName, teamNameMap, projectMap, athlete);
|
|
|
Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
|
|
|
- int baseQrSize = Math.min(150, (int) (Math.min(pageWidth, pageHeight) * 0.15));
|
|
|
+ int baseQrSize = 100; // 固定基础尺寸
|
|
|
int qrSize = (int) (baseQrSize * barcodeScale.floatValue());
|
|
|
|
|
|
// 使用默认位置(右下角)
|
|
|
@@ -1362,9 +1375,15 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
byte[] logoImageBytes = logoImagePath != null ? Files.readAllBytes(Paths.get(logoImagePath)) : null;
|
|
|
|
|
|
// 读取背景图
|
|
|
- Image bgImageObj = Image.getInstance(backgroundImageBytes);
|
|
|
- float pageWidth = bgImageObj.getWidth();
|
|
|
- float pageHeight = bgImageObj.getHeight();
|
|
|
+ Image originalBgImageObj = Image.getInstance(backgroundImageBytes);
|
|
|
+ float originalWidth = originalBgImageObj.getWidth();
|
|
|
+ float originalHeight = originalBgImageObj.getHeight();
|
|
|
+
|
|
|
+ // 获取目标画布尺寸
|
|
|
+ float pageWidth = bibParam.getCanvasWidth() != null ? bibParam.getCanvasWidth().floatValue() : originalWidth;
|
|
|
+ float pageHeight = bibParam.getCanvasHeight() != null ? bibParam.getCanvasHeight().floatValue() : originalHeight;
|
|
|
+
|
|
|
+ log.info("异步生成 - 原始背景图片尺寸: {}x{}, 目标画布尺寸: {}x{}", originalWidth, originalHeight, pageWidth, pageHeight);
|
|
|
|
|
|
// 设置字体和颜色,提供默认值
|
|
|
String fontName = bibParam.getFontName() != null ? bibParam.getFontName() : "yahei";
|
|
|
@@ -1382,8 +1401,9 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
document.open();
|
|
|
PdfContentByte cb = writer.getDirectContent();
|
|
|
|
|
|
- // 添加背景图
|
|
|
+ // 添加背景图(缩放到目标尺寸)
|
|
|
Image bg = Image.getInstance(backgroundImageBytes);
|
|
|
+ bg.scaleToFit(pageWidth, pageHeight);
|
|
|
bg.setAbsolutePosition(0, 0);
|
|
|
document.add(bg);
|
|
|
|
|
|
@@ -1391,9 +1411,21 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
if (logoImageBytes != null && bibParam.getLogoX() != null && bibParam.getLogoY() != null) {
|
|
|
try {
|
|
|
Image img = Image.getInstance(logoImageBytes);
|
|
|
- // 应用缩放参数
|
|
|
- Double logoScale = bibParam.getLogoScale() != null ? bibParam.getLogoScale() : 1.0;
|
|
|
- img.scaleToFit(80 * logoScale.floatValue(), 80 * logoScale.floatValue());
|
|
|
+ // 使用传入的Logo尺寸(前端已计算好最终尺寸)
|
|
|
+ float scaledWidth, scaledHeight;
|
|
|
+ if (bibParam.getLogoWidth() != null && bibParam.getLogoHeight() != null) {
|
|
|
+ // 使用前端传入的Logo尺寸(已包含所有缩放计算)
|
|
|
+ scaledWidth = bibParam.getLogoWidth().floatValue();
|
|
|
+ scaledHeight = bibParam.getLogoHeight().floatValue();
|
|
|
+ log.debug("异步生成 - 使用前端计算的Logo尺寸: {}x{}", scaledWidth, scaledHeight);
|
|
|
+ } else {
|
|
|
+ // 回退到原始缩放逻辑(兼容旧版本)
|
|
|
+ Double logoScale = bibParam.getLogoScale() != null ? bibParam.getLogoScale() : 1.0;
|
|
|
+ scaledWidth = img.getWidth() * logoScale.floatValue();
|
|
|
+ scaledHeight = img.getHeight() * logoScale.floatValue();
|
|
|
+ log.warn("异步生成 - 使用回退缩放逻辑,Logo尺寸: {}x{}", scaledWidth, scaledHeight);
|
|
|
+ }
|
|
|
+ img.scaleToFit(scaledWidth, scaledHeight);
|
|
|
// 将百分比坐标转换为PDF像素坐标,并翻转Y轴
|
|
|
float logoPositionX = (float) (bibParam.getLogoX() * pageWidth / 100.0);
|
|
|
float logoPositionY = (float) (pageHeight - (bibParam.getLogoY() * pageHeight / 100.0));
|
|
|
@@ -1463,7 +1495,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
try {
|
|
|
String qrDataStr = getQrDataStr(bibParam.getEventName(), groupName, teamNameMap, projectMap, athlete);
|
|
|
Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
|
|
|
- int baseQrSize = Math.min(150, (int) (Math.min(pageWidth, pageHeight) * 0.15));
|
|
|
+ int baseQrSize = 100; // 固定基础尺寸
|
|
|
int qrSize = (int) (baseQrSize * barcodeScale.floatValue());
|
|
|
|
|
|
// 将百分比坐标转换为PDF像素坐标,并翻转Y轴
|
|
|
@@ -1506,7 +1538,7 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
try {
|
|
|
String qrDataStr = getQrDataStr(bibParam.getEventName(), groupName, teamNameMap, projectMap, athlete);
|
|
|
Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
|
|
|
- int baseQrSize = Math.min(150, (int) (Math.min(pageWidth, pageHeight) * 0.15));
|
|
|
+ int baseQrSize = 100; // 固定基础尺寸
|
|
|
int qrSize = (int) (baseQrSize * barcodeScale.floatValue());
|
|
|
|
|
|
// 使用默认位置(右下角)
|
|
|
@@ -1644,12 +1676,24 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
private byte[] generateSingleBibFromTemplate(byte[] templateImage, GameAthleteVo athlete, GenerateBibBo bibParam) {
|
|
|
try {
|
|
|
// 使用BufferedImage处理模版图片
|
|
|
- BufferedImage template = ImageIO.read(new ByteArrayInputStream(templateImage));
|
|
|
+ BufferedImage originalTemplate = ImageIO.read(new ByteArrayInputStream(templateImage));
|
|
|
+
|
|
|
+ // 获取目标画布尺寸
|
|
|
+ int targetWidth = bibParam.getCanvasWidth() != null ? bibParam.getCanvasWidth() : originalTemplate.getWidth();
|
|
|
+ int targetHeight = bibParam.getCanvasHeight() != null ? bibParam.getCanvasHeight() : originalTemplate.getHeight();
|
|
|
+
|
|
|
+ // 创建目标尺寸的画布
|
|
|
+ BufferedImage template = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
|
|
|
Graphics2D g2d = template.createGraphics();
|
|
|
|
|
|
// 设置抗锯齿
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
|
+ g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
|
|
+
|
|
|
+ // 将原始模版图片拉伸/缩放到目标画布尺寸
|
|
|
+ g2d.drawImage(originalTemplate, 0, 0, targetWidth, targetHeight, null);
|
|
|
+ log.info("模版图片已调整到目标尺寸: {}x{}", targetWidth, targetHeight);
|
|
|
|
|
|
// 绘制号码
|
|
|
drawNumberOnTemplate(g2d, athlete.getAthleteCode().toString(), bibParam, template.getWidth(), template.getHeight());
|
|
|
@@ -1679,20 +1723,10 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
int x = (int) (canvasWidth * bibParam.getNumberX() / 100);
|
|
|
int y = (int) (canvasHeight * bibParam.getNumberY() / 100);
|
|
|
|
|
|
- // 设置字体(考虑前端预览的相对比例)
|
|
|
- int baseFontSize = bibParam.getFontSize();
|
|
|
- // 前端预览时号码字体被限制为最大56px,相对于预览框600x400的比例
|
|
|
- final int previewMaxFontSize = 56;
|
|
|
-
|
|
|
- // 计算预览时的相对比例(相对于预览框的宽度)
|
|
|
- double previewRelativeFontSize = (double) previewMaxFontSize / previewWidth;
|
|
|
-
|
|
|
- // 计算实际图片中字体应该占的相对比例
|
|
|
- double actualRelativeFontSize = previewRelativeFontSize * canvasWidth;
|
|
|
-
|
|
|
- // 计算字体的实际缩放比例
|
|
|
- double fontScale = Math.min(1.0, actualRelativeFontSize / baseFontSize);
|
|
|
- int fontSize = (int) (baseFontSize * fontScale);
|
|
|
+ // 设置字体(直接使用用户设置的字体大小和缩放)
|
|
|
+ int baseFontSize = bibParam.getFontSize() != null ? bibParam.getFontSize() : 36;
|
|
|
+ Double numberScale = bibParam.getNumberScale() != null ? bibParam.getNumberScale() : 1.0;
|
|
|
+ int fontSize = (int) (baseFontSize * numberScale);
|
|
|
Font font = new Font(bibParam.getFontName(), Font.BOLD, fontSize);
|
|
|
g2d.setFont(font);
|
|
|
|
|
|
@@ -1707,7 +1741,8 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
|
|
|
// 居中绘制
|
|
|
g2d.drawString(number, x - textWidth / 2, y + textHeight / 4);
|
|
|
- log.info("号码绘制完成 - 位置: ({}, {}), 基础字体: {}, 预览相对比例: {}, 实际相对大小: {}, 预览缩放: {}, 最终字体: {}", x, y, baseFontSize, previewRelativeFontSize, actualRelativeFontSize, fontScale, fontSize);
|
|
|
+ log.info("号码绘制完成 - 位置: ({}, {}), 基础字体: {}, 用户缩放: {}, 最终字体: {}, 画布尺寸: {}x{}",
|
|
|
+ x, y, baseFontSize, numberScale, fontSize, canvasWidth, canvasHeight);
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
log.error("绘制号码失败", e);
|
|
|
@@ -1745,25 +1780,10 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
log.info("使用默认二维码位置: ({}, {})", x, y);
|
|
|
}
|
|
|
|
|
|
- // 应用缩放(考虑前端预览的相对比例和用户设置的缩放)
|
|
|
+ // 应用用户设置的缩放
|
|
|
Double barcodeScale = bibParam.getBarcodeScale() != null ? bibParam.getBarcodeScale() : 1.0;
|
|
|
-
|
|
|
- // 前端预览时二维码是32x32的SVG图标,相对于预览框600x400的比例
|
|
|
- final int previewQRSize = 32; // 前端SVG图标大小
|
|
|
-
|
|
|
- // 计算预览时的相对比例(相对于预览框的宽度)
|
|
|
- double previewRelativeSize = (double) previewQRSize / previewWidth;
|
|
|
-
|
|
|
- // 计算实际图片中二维码应该占的相对比例
|
|
|
- double actualRelativeSize = previewRelativeSize * canvasWidth;
|
|
|
-
|
|
|
- // 计算二维码的实际缩放比例
|
|
|
- double previewScale = Math.min(1.0, actualRelativeSize / Math.max(qrImage.getWidth(), qrImage.getHeight()));
|
|
|
-
|
|
|
- // 计算最终缩放:预览缩放 × 用户缩放
|
|
|
- double finalScale = previewScale * barcodeScale;
|
|
|
- int scaledWidth = (int) (qrImage.getWidth() * finalScale);
|
|
|
- int scaledHeight = (int) (qrImage.getHeight() * finalScale);
|
|
|
+ int scaledWidth = (int) (qrImage.getWidth() * barcodeScale);
|
|
|
+ int scaledHeight = (int) (qrImage.getHeight() * barcodeScale);
|
|
|
|
|
|
// 边界检查,确保二维码不会超出图片范围
|
|
|
if (x < 0) x = 0;
|
|
|
@@ -1773,8 +1793,8 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
|
|
|
// 绘制二维码
|
|
|
g2d.drawImage(qrImage, x, y, scaledWidth, scaledHeight, null);
|
|
|
- log.info("二维码绘制完成 - 位置: ({}, {}), 尺寸: {}x{}, 预览相对比例: {}, 实际相对大小: {}, 预览缩放: {}, 用户缩放: {}, 最终缩放: {}, 画布尺寸: {}x{}",
|
|
|
- x, y, scaledWidth, scaledHeight, previewRelativeSize, actualRelativeSize, previewScale, barcodeScale, finalScale, canvasWidth, canvasHeight);
|
|
|
+ log.info("二维码绘制完成 - 位置: ({}, {}), 尺寸: {}x{}, 用户缩放: {}, 画布尺寸: {}x{}",
|
|
|
+ x, y, scaledWidth, scaledHeight, barcodeScale, canvasWidth, canvasHeight);
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
log.error("绘制二维码失败", e);
|
|
|
@@ -1785,10 +1805,72 @@ public class GameEventServiceImpl implements IGameEventService {
|
|
|
* 生成二维码数据
|
|
|
*/
|
|
|
private String generateQRCodeData(GameAthleteVo athlete) {
|
|
|
- return String.format("运动员编号: %s, 姓名: %s, 队伍: %s",
|
|
|
+ StringBuilder joinProject = new StringBuilder();
|
|
|
+ StringJoiner joiner = new StringJoiner(",");
|
|
|
+
|
|
|
+ // 添加调试日志
|
|
|
+ log.debug("生成二维码数据 - 运动员: {}, projectValue: {}, projectList: {}",
|
|
|
+ athlete.getName(), athlete.getProjectValue(), athlete.getProjectList());
|
|
|
+
|
|
|
+ // 检查projectList是否为null或空
|
|
|
+ if (athlete.getProjectList() == null || athlete.getProjectList().isEmpty()) {
|
|
|
+ log.warn("运动员 {} 的参与项目列表为空,projectValue: {}", athlete.getName(), athlete.getProjectValue());
|
|
|
+ return String.format(
|
|
|
+ """
|
|
|
+ {
|
|
|
+ 赛事名称:%s,
|
|
|
+ 号码:%s,
|
|
|
+ 姓名:%s,
|
|
|
+ 性别:%s,
|
|
|
+ 年龄:%d
|
|
|
+ 队伍名称:%s,
|
|
|
+ 参与项目:无项目,
|
|
|
+ }
|
|
|
+ """,
|
|
|
+ athlete.getEventName(),
|
|
|
+ athlete.getAthleteCode(),
|
|
|
+ athlete.getName(),
|
|
|
+ athlete.getGender(),
|
|
|
+ athlete.getAge(),
|
|
|
+ athlete.getTeamName() != null ? athlete.getTeamName() : "未知队伍");
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<Long, String> projectMap = gameEventProjectService.queryNameByEventIdAndProjectIds(athlete.getEventId(), athlete.getProjectList());
|
|
|
+ log.debug("项目映射结果: {}", projectMap);
|
|
|
+
|
|
|
+ // 添加额外的null检查
|
|
|
+ if (projectMap == null) {
|
|
|
+ log.warn("项目映射结果为空,返回空字符串,逻辑上不可能为空,如果有空项目,应该在运动员表中删除");
|
|
|
+ projectMap = new HashMap<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ for (Long projectId : athlete.getProjectList()) {
|
|
|
+ String projectName = projectMap.get(projectId) != null ? projectMap.get(projectId).toString() : "未知项目";
|
|
|
+ joiner.add(projectName);
|
|
|
+ }
|
|
|
+
|
|
|
+ String projectNames = joiner.toString();
|
|
|
+ log.debug("最终项目名称: {}", projectNames);
|
|
|
+
|
|
|
+ return String.format(
|
|
|
+ """
|
|
|
+ {
|
|
|
+ 赛事名称:%s,
|
|
|
+ 号码:%s,
|
|
|
+ 姓名:%s,
|
|
|
+ 性别:%s,
|
|
|
+ 年龄:%d
|
|
|
+ 队伍名称:%s,
|
|
|
+ 参与项目:%s,
|
|
|
+ }
|
|
|
+ """,
|
|
|
+ athlete.getEventName(),
|
|
|
athlete.getAthleteCode(),
|
|
|
athlete.getName(),
|
|
|
- athlete.getTeamName() != null ? athlete.getTeamName() : "未知队伍");
|
|
|
+ athlete.getGender(),
|
|
|
+ athlete.getAge(),
|
|
|
+ athlete.getTeamName() != null ? athlete.getTeamName() : "未知队伍",
|
|
|
+ projectNames);
|
|
|
}
|
|
|
|
|
|
/**
|