|
|
@ -4,26 +4,25 @@ import io.swagger.v3.oas.annotations.Operation; |
|
|
import io.swagger.v3.oas.annotations.tags.Tag; |
|
|
import io.swagger.v3.oas.annotations.tags.Tag; |
|
|
import net.lab1024.sa.admin.module.penalty.domain.entity.PenaltyApplyEntity; |
|
|
import net.lab1024.sa.admin.module.penalty.domain.entity.PenaltyApplyEntity; |
|
|
import net.lab1024.sa.admin.module.penalty.service.PenaltyApplyService; |
|
|
import net.lab1024.sa.admin.module.penalty.service.PenaltyApplyService; |
|
|
import net.lab1024.sa.admin.module.service.domain.form.*; |
|
|
|
|
|
import net.lab1024.sa.admin.module.service.domain.vo.LawyerStatisticsVO; |
|
|
|
|
|
import net.lab1024.sa.admin.module.service.domain.vo.ServiceApplicationsVO; |
|
|
|
|
|
import net.lab1024.sa.admin.module.service.domain.vo.ServiceReportStatisticsVO; |
|
|
|
|
|
import net.lab1024.sa.admin.module.service.service.ServiceApplicationsService; |
|
|
|
|
|
import net.lab1024.sa.admin.module.system.department.domain.vo.DepartmentVO; |
|
|
import net.lab1024.sa.admin.module.system.department.domain.vo.DepartmentVO; |
|
|
import net.lab1024.sa.admin.module.system.department.service.DepartmentService; |
|
|
import net.lab1024.sa.admin.module.system.department.service.DepartmentService; |
|
|
import net.lab1024.sa.admin.module.system.employee.domain.entity.EmployeeEntity; |
|
|
import net.lab1024.sa.admin.module.system.employee.domain.entity.EmployeeEntity; |
|
|
import net.lab1024.sa.admin.module.system.employee.service.EmployeeService; |
|
|
import net.lab1024.sa.admin.module.system.employee.service.EmployeeService; |
|
|
import net.lab1024.sa.base.common.domain.PageResult; |
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
import net.lab1024.sa.base.common.domain.ResponseDTO; |
|
|
|
|
|
import net.lab1024.sa.base.common.domain.ValidateList; |
|
|
|
|
|
import org.springframework.web.bind.annotation.*; |
|
|
import org.springframework.web.bind.annotation.*; |
|
|
import org.springframework.web.multipart.MultipartFile; |
|
|
|
|
|
|
|
|
|
|
|
import javax.annotation.Resource; |
|
|
import org.springframework.core.io.ClassPathResource; |
|
|
|
|
|
import org.springframework.core.io.Resource; |
|
|
|
|
|
import javax.imageio.ImageIO; |
|
|
import javax.servlet.http.HttpServletResponse; |
|
|
import javax.servlet.http.HttpServletResponse; |
|
|
import javax.validation.Valid; |
|
|
import java.awt.*; |
|
|
import java.math.BigDecimal; |
|
|
import java.awt.image.BufferedImage; |
|
|
import java.util.List; |
|
|
import java.io.*; |
|
|
|
|
|
import java.time.LocalDate; |
|
|
|
|
|
import java.time.LocalDateTime; |
|
|
|
|
|
import java.time.format.DateTimeFormatter; |
|
|
|
|
|
import java.util.HashMap; |
|
|
|
|
|
import java.util.Map; |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 无处罚证明开具 Controller |
|
|
* 无处罚证明开具 Controller |
|
|
@ -32,58 +31,489 @@ import java.util.List; |
|
|
* @Date 2025-12-20 14:44:06 |
|
|
* @Date 2025-12-20 14:44:06 |
|
|
* @Copyright 1.0 |
|
|
* @Copyright 1.0 |
|
|
*/ |
|
|
*/ |
|
|
|
|
|
|
|
|
@RestController |
|
|
@RestController |
|
|
@Tag(name = "无处罚证明开具") |
|
|
@Tag(name = "无处罚证明开具") |
|
|
|
|
|
@RequestMapping("/wordCertificate") |
|
|
public class WordApplicationsController { |
|
|
public class WordApplicationsController { |
|
|
@Resource |
|
|
|
|
|
WordCertificateService wordCertificateService; |
|
|
@Autowired |
|
|
@Resource |
|
|
private WordCertificateService wordCertificateService; |
|
|
|
|
|
|
|
|
|
|
|
@Autowired |
|
|
private PenaltyApplyService penaltyApplyService; |
|
|
private PenaltyApplyService penaltyApplyService; |
|
|
@Resource |
|
|
|
|
|
EmployeeService employeeService; |
|
|
@Autowired |
|
|
@Resource |
|
|
private EmployeeService employeeService; |
|
|
DepartmentService departmentService; |
|
|
|
|
|
@Operation(summary = "无处罚证明下载 @author wzh") |
|
|
@Autowired |
|
|
@GetMapping("/wordCertificate/export/{id}") |
|
|
private DepartmentService departmentService; |
|
|
public void wordCertificateExport(HttpServletResponse response, @PathVariable Integer id, |
|
|
|
|
|
@RequestParam(defaultValue = "word") String format) throws Exception { |
|
|
// 字体配置 - 使用文泉驿字体
|
|
|
//查询出当前的开具证明信息
|
|
|
private static final int TITLE_FONT_SIZE = 100; // 标题
|
|
|
|
|
|
private static final int BODY_FONT_SIZE = 78; // 正文
|
|
|
|
|
|
private static final int SIGNATURE_FONT_SIZE = 72; // 落款
|
|
|
|
|
|
|
|
|
|
|
|
// 页面布局配置
|
|
|
|
|
|
private static final int PAGE_WIDTH = 2480; // A4宽度 (300 DPI)
|
|
|
|
|
|
private static final int PAGE_HEIGHT = 3508; // A4高度 (300 DPI)
|
|
|
|
|
|
private static final int LEFT_MARGIN = 380; // 左边距
|
|
|
|
|
|
private static final int RIGHT_MARGIN = 380; // 右边距
|
|
|
|
|
|
private static final int TOP_MARGIN = 500; // 顶部边距
|
|
|
|
|
|
private static final int LINE_SPACING = 40; // 行间距
|
|
|
|
|
|
private static final int PARAGRAPH_SPACING = 100; // 段落间距
|
|
|
|
|
|
|
|
|
|
|
|
// 印章配置
|
|
|
|
|
|
private static final String SEAL_IMAGE_PATH = "templates/official_seal.png"; |
|
|
|
|
|
private static final int SEAL_WIDTH = 580; |
|
|
|
|
|
private static final int SEAL_HEIGHT = 580; |
|
|
|
|
|
|
|
|
|
|
|
// 字体缓存
|
|
|
|
|
|
private static Font titleFont; |
|
|
|
|
|
private static Font bodyFont; |
|
|
|
|
|
private static Font signatureFont; |
|
|
|
|
|
|
|
|
|
|
|
// 文泉驿字体映射
|
|
|
|
|
|
private static final Map<String, String> WENQUANYI_FONTS = new HashMap<>(); |
|
|
|
|
|
static { |
|
|
|
|
|
// 文泉驿字体名称映射
|
|
|
|
|
|
WENQUANYI_FONTS.put("title", "WenQuanYi Zen Hei"); // 标题用正黑(较粗)
|
|
|
|
|
|
WENQUANYI_FONTS.put("body", "WenQuanYi Micro Hei"); // 正文用微米黑
|
|
|
|
|
|
WENQUANYI_FONTS.put("bold", "WenQuanYi Zen Hei Sharp"); // 加粗用点阵正黑
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 静态初始化字体 |
|
|
|
|
|
*/ |
|
|
|
|
|
static { |
|
|
|
|
|
try { |
|
|
|
|
|
initFonts(); |
|
|
|
|
|
System.out.println("字体初始化完成:"); |
|
|
|
|
|
System.out.println("标题字体: " + titleFont.getFontName()); |
|
|
|
|
|
System.out.println("正文字体: " + bodyFont.getFontName()); |
|
|
|
|
|
System.out.println("落款字体: " + signatureFont.getFontName()); |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
System.err.println("字体初始化失败,使用默认字体: " + e.getMessage()); |
|
|
|
|
|
setFallbackFonts(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 初始化文泉驿字体 |
|
|
|
|
|
*/ |
|
|
|
|
|
private static void initFonts() { |
|
|
|
|
|
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); |
|
|
|
|
|
String[] availableFonts = ge.getAvailableFontFamilyNames(); |
|
|
|
|
|
|
|
|
|
|
|
System.out.println("可用字体列表:"); |
|
|
|
|
|
for (String font : availableFonts) { |
|
|
|
|
|
if (font.contains("WenQuanYi") || font.contains("文泉驿")) { |
|
|
|
|
|
System.out.println(" - " + font); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 尝试加载文泉驿正黑作为标题字体
|
|
|
|
|
|
titleFont = findAndCreateFont(availableFonts, |
|
|
|
|
|
new String[]{"WenQuanYi Zen Hei", "文泉驿正黑", "WenQuanYi Zen Hei Sharp"}, |
|
|
|
|
|
Font.BOLD, TITLE_FONT_SIZE); |
|
|
|
|
|
|
|
|
|
|
|
// 尝试加载文泉驿微米黑作为正文字体
|
|
|
|
|
|
bodyFont = findAndCreateFont(availableFonts, |
|
|
|
|
|
new String[]{"WenQuanYi Micro Hei", "文泉驿微米黑", "WenQuanYi Zen Hei"}, |
|
|
|
|
|
Font.PLAIN, BODY_FONT_SIZE); |
|
|
|
|
|
|
|
|
|
|
|
// 落款字体使用正文字体但字号稍小
|
|
|
|
|
|
if (bodyFont != null) { |
|
|
|
|
|
signatureFont = bodyFont.deriveFont((float) SIGNATURE_FONT_SIZE); |
|
|
|
|
|
} else { |
|
|
|
|
|
signatureFont = new Font("SansSerif", Font.PLAIN, SIGNATURE_FONT_SIZE); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 如果字体未找到,使用默认字体
|
|
|
|
|
|
if (titleFont == null) { |
|
|
|
|
|
titleFont = new Font("SansSerif", Font.BOLD, TITLE_FONT_SIZE); |
|
|
|
|
|
} |
|
|
|
|
|
if (bodyFont == null) { |
|
|
|
|
|
bodyFont = new Font("SansSerif", Font.PLAIN, BODY_FONT_SIZE); |
|
|
|
|
|
signatureFont = new Font("SansSerif", Font.PLAIN, SIGNATURE_FONT_SIZE); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 查找并创建字体 |
|
|
|
|
|
*/ |
|
|
|
|
|
private static Font findAndCreateFont(String[] availableFonts, String[] preferredFonts, int style, int size) { |
|
|
|
|
|
for (String preferred : preferredFonts) { |
|
|
|
|
|
for (String available : availableFonts) { |
|
|
|
|
|
if (available.equalsIgnoreCase(preferred) || available.contains(preferred)) { |
|
|
|
|
|
System.out.println("找到字体: " + available + " -> 使用: " + preferred); |
|
|
|
|
|
try { |
|
|
|
|
|
Font font = new Font(available, style, size); |
|
|
|
|
|
// 测试字体是否能显示中文
|
|
|
|
|
|
if (testFontCanDisplayChinese(font)) { |
|
|
|
|
|
return font; |
|
|
|
|
|
} |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
System.err.println("创建字体失败: " + available + ", 错误: " + e.getMessage()); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 测试字体是否能显示中文 |
|
|
|
|
|
*/ |
|
|
|
|
|
private static boolean testFontCanDisplayChinese(Font font) { |
|
|
|
|
|
try { |
|
|
|
|
|
return font.canDisplay('中') && font.canDisplay('文') && font.canDisplay('证'); |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 设置备选字体 |
|
|
|
|
|
*/ |
|
|
|
|
|
private static void setFallbackFonts() { |
|
|
|
|
|
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); |
|
|
|
|
|
String[] fonts = ge.getAvailableFontFamilyNames(); |
|
|
|
|
|
|
|
|
|
|
|
// 寻找任何可用的中文字体
|
|
|
|
|
|
String fallbackFont = "SansSerif"; |
|
|
|
|
|
for (String font : fonts) { |
|
|
|
|
|
if (font.matches(".*[\\u4e00-\\u9fa5].*") || |
|
|
|
|
|
font.contains("Song") || font.contains("Hei") || |
|
|
|
|
|
font.contains("Kai") || font.contains("Fang")) { |
|
|
|
|
|
fallbackFont = font; |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
titleFont = new Font(fallbackFont, Font.BOLD, TITLE_FONT_SIZE); |
|
|
|
|
|
bodyFont = new Font(fallbackFont, Font.PLAIN, BODY_FONT_SIZE); |
|
|
|
|
|
signatureFont = new Font(fallbackFont, Font.PLAIN, SIGNATURE_FONT_SIZE); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 预览无处罚证明(图片格式) |
|
|
|
|
|
*/ |
|
|
|
|
|
@Operation(summary = "无处罚证明预览(图片格式) @author wzh") |
|
|
|
|
|
@GetMapping("/export/{id}") |
|
|
|
|
|
public void previewImage(HttpServletResponse response, @PathVariable Integer id) throws Exception { |
|
|
|
|
|
try { |
|
|
|
|
|
WordCertificateService.CertificateData certificateData = getCertificateData(id); |
|
|
|
|
|
byte[] imageBytes = generateCertificateImage(certificateData); |
|
|
|
|
|
|
|
|
|
|
|
response.setContentType("image/png"); |
|
|
|
|
|
String fileName = LocalDateTime.now()+".png"; |
|
|
|
|
|
response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\""); |
|
|
|
|
|
response.setHeader("Cache-Control", "max-age=3600"); |
|
|
|
|
|
response.setContentLength(imageBytes.length); |
|
|
|
|
|
|
|
|
|
|
|
try (OutputStream out = response.getOutputStream()) { |
|
|
|
|
|
out.write(imageBytes); |
|
|
|
|
|
out.flush(); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
// 生成错误图片
|
|
|
|
|
|
byte[] errorImage = generateErrorImage("生成证明失败: " + e.getMessage()); |
|
|
|
|
|
response.setContentType("image/png"); |
|
|
|
|
|
response.setHeader("Content-Disposition", "inline; filename=\"error.png\""); |
|
|
|
|
|
response.setContentLength(errorImage.length); |
|
|
|
|
|
|
|
|
|
|
|
try (OutputStream out = response.getOutputStream()) { |
|
|
|
|
|
out.write(errorImage); |
|
|
|
|
|
out.flush(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 生成错误提示图片 |
|
|
|
|
|
*/ |
|
|
|
|
|
private byte[] generateErrorImage(String errorMessage) throws IOException { |
|
|
|
|
|
BufferedImage image = new BufferedImage(800, 400, BufferedImage.TYPE_INT_RGB); |
|
|
|
|
|
Graphics2D g2d = image.createGraphics(); |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
setupGraphics(g2d); |
|
|
|
|
|
g2d.setColor(Color.WHITE); |
|
|
|
|
|
g2d.fillRect(0, 0, 800, 400); |
|
|
|
|
|
|
|
|
|
|
|
g2d.setColor(Color.RED); |
|
|
|
|
|
Font errorFont = new Font("SansSerif", Font.BOLD, 20); |
|
|
|
|
|
g2d.setFont(errorFont); |
|
|
|
|
|
|
|
|
|
|
|
// 绘制错误信息
|
|
|
|
|
|
g2d.drawString("错误:无法生成证明", 50, 100); |
|
|
|
|
|
g2d.drawString("原因:" + errorMessage, 50, 140); |
|
|
|
|
|
} finally { |
|
|
|
|
|
g2d.dispose(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
|
|
|
|
|
ImageIO.write(image, "PNG", baos); |
|
|
|
|
|
return baos.toByteArray(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 根据ID获取证书数据 |
|
|
|
|
|
*/ |
|
|
|
|
|
private WordCertificateService.CertificateData getCertificateData(Integer id) { |
|
|
PenaltyApplyEntity penaltyApplyEntity = penaltyApplyService.selectOne(id); |
|
|
PenaltyApplyEntity penaltyApplyEntity = penaltyApplyService.selectOne(id); |
|
|
//查询用户姓名执业证号
|
|
|
EmployeeEntity employee = employeeService.getById(penaltyApplyEntity.getUserId()); |
|
|
EmployeeEntity byId = employeeService.getById(penaltyApplyEntity.getUserId()); |
|
|
DepartmentVO department = departmentService.getById(employee.getDepartmentId()); |
|
|
//机构信息
|
|
|
|
|
|
DepartmentVO departmentVO = departmentService.getById(byId.getDepartmentId()); |
|
|
return new WordCertificateService.CertificateData( |
|
|
WordCertificateService.CertificateData certificateData = new WordCertificateService.CertificateData( |
|
|
department.getDepartmentName(), |
|
|
departmentVO.getDepartmentName(), |
|
|
department.getCreditCode(), |
|
|
departmentVO.getCreditCode(), |
|
|
employee.getActualName(), |
|
|
byId.getActualName(), |
|
|
employee.getCertificateNumber(), |
|
|
byId.getCertificateNumber(), |
|
|
|
|
|
penaltyApplyEntity.getCreateTime() |
|
|
penaltyApplyEntity.getCreateTime() |
|
|
); |
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 生成证书图片 |
|
|
|
|
|
*/ |
|
|
|
|
|
private byte[] generateCertificateImage(WordCertificateService.CertificateData data) throws IOException { |
|
|
|
|
|
BufferedImage image = new BufferedImage(PAGE_WIDTH, PAGE_HEIGHT, BufferedImage.TYPE_INT_RGB); |
|
|
|
|
|
Graphics2D g2d = image.createGraphics(); |
|
|
|
|
|
|
|
|
byte[] fileContent; |
|
|
try { |
|
|
String fileName; |
|
|
setupGraphics(g2d); |
|
|
String contentType; |
|
|
|
|
|
|
|
|
// 填充白色背景
|
|
|
|
|
|
g2d.setColor(Color.WHITE); |
|
|
|
|
|
g2d.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT); |
|
|
|
|
|
g2d.setColor(Color.BLACK); |
|
|
|
|
|
|
|
|
|
|
|
// 绘制证书内容
|
|
|
|
|
|
drawCertificateContent(g2d, data); |
|
|
|
|
|
|
|
|
|
|
|
} finally { |
|
|
|
|
|
g2d.dispose(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return convertImageToBytes(image); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 设置Graphics2D参数 |
|
|
|
|
|
*/ |
|
|
|
|
|
private void setupGraphics(Graphics2D g2d) { |
|
|
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
|
|
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); |
|
|
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); |
|
|
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); |
|
|
|
|
|
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 绘制证书内容 |
|
|
|
|
|
*/ |
|
|
|
|
|
private void drawCertificateContent(Graphics2D g2d, WordCertificateService.CertificateData data) { |
|
|
|
|
|
// 1. 绘制标题"证明" - 居中对齐
|
|
|
|
|
|
g2d.setFont(titleFont); |
|
|
|
|
|
String title = "证 明"; |
|
|
|
|
|
FontMetrics titleMetrics = g2d.getFontMetrics(); |
|
|
|
|
|
|
|
|
|
|
|
// 测试字体是否能显示
|
|
|
|
|
|
if (titleMetrics.stringWidth(title) == 0) { |
|
|
|
|
|
throw new RuntimeException("标题字体不可用,可能显示为方框"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
int titleWidth = titleMetrics.stringWidth(title); |
|
|
|
|
|
int titleX = (PAGE_WIDTH - titleWidth) / 2; |
|
|
|
|
|
int currentY = TOP_MARGIN; |
|
|
|
|
|
|
|
|
|
|
|
g2d.drawString(title, titleX, currentY); |
|
|
|
|
|
currentY += titleMetrics.getHeight() + PARAGRAPH_SPACING; |
|
|
|
|
|
|
|
|
|
|
|
// 2. 绘制正文 - 带首行缩进
|
|
|
|
|
|
g2d.setFont(bodyFont); |
|
|
|
|
|
FontMetrics bodyMetrics = g2d.getFontMetrics(); |
|
|
|
|
|
int lineHeight = bodyMetrics.getHeight(); |
|
|
|
|
|
int maxLineWidth = PAGE_WIDTH - LEFT_MARGIN - RIGHT_MARGIN; |
|
|
|
|
|
|
|
|
|
|
|
// 构建完整内容
|
|
|
|
|
|
String fullContent = "兹证明" + data.getCertificateNo() + "(统一社会信用代码:" + |
|
|
|
|
|
data.getPurpose() + ")" + data.getName() + |
|
|
|
|
|
"律师(执业证号:" + data.getIdCard() + |
|
|
|
|
|
")近五年在我市执业期间未受到律师协会行业处分。"; |
|
|
|
|
|
|
|
|
|
|
|
// 首行缩进(两个全角空格)
|
|
|
|
|
|
String indent = " "; |
|
|
|
|
|
String firstLine = indent + fullContent; |
|
|
|
|
|
|
|
|
|
|
|
// 绘制正文(带自动换行)
|
|
|
|
|
|
currentY = drawWrappedText(g2d, firstLine, LEFT_MARGIN, currentY, maxLineWidth, lineHeight); |
|
|
|
|
|
|
|
|
|
|
|
// 3. 绘制"特此证明。" - 同样缩进
|
|
|
|
|
|
currentY += lineHeight; |
|
|
|
|
|
String herebyText = indent + "特此证明。"; |
|
|
|
|
|
g2d.drawString(herebyText, LEFT_MARGIN, currentY); |
|
|
|
|
|
currentY += lineHeight + PARAGRAPH_SPACING * 2; |
|
|
|
|
|
|
|
|
|
|
|
// 4. 绘制落款和印章
|
|
|
|
|
|
drawSignatureWithSeal(g2d, currentY, data.getLocalDateTime()); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 绘制自动换行文本 |
|
|
|
|
|
*/ |
|
|
|
|
|
private int drawWrappedText(Graphics2D g2d, String text, int x, int y, int maxWidth, int lineHeight) { |
|
|
|
|
|
FontMetrics metrics = g2d.getFontMetrics(); |
|
|
|
|
|
StringBuilder currentLine = new StringBuilder(); |
|
|
|
|
|
int lineIndex = 0; |
|
|
|
|
|
int currentY = y; |
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < text.length(); i++) { |
|
|
|
|
|
char ch = text.charAt(i); |
|
|
|
|
|
currentLine.append(ch); |
|
|
|
|
|
|
|
|
|
|
|
// 检查当前行宽度
|
|
|
|
|
|
int lineWidth = metrics.stringWidth(currentLine.toString()); |
|
|
|
|
|
|
|
|
|
|
|
if (lineWidth > maxWidth) { |
|
|
|
|
|
// 绘制当前行(除了最后一个字符)
|
|
|
|
|
|
String lineToDraw = currentLine.substring(0, currentLine.length() - 1); |
|
|
|
|
|
int drawX = x; |
|
|
|
|
|
g2d.drawString(lineToDraw, drawX, currentY); |
|
|
|
|
|
|
|
|
|
|
|
// 新行开始(从最后一个字符开始)
|
|
|
|
|
|
currentLine = new StringBuilder(String.valueOf(ch)); |
|
|
|
|
|
currentY += lineHeight + LINE_SPACING; |
|
|
|
|
|
lineIndex++; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 绘制最后一行
|
|
|
|
|
|
if (currentLine.length() > 0) { |
|
|
|
|
|
int drawX = x; |
|
|
|
|
|
g2d.drawString(currentLine.toString(), drawX, currentY); |
|
|
|
|
|
currentY += lineHeight + LINE_SPACING; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return currentY; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 绘制落款和印章 |
|
|
|
|
|
*/ |
|
|
|
|
|
private void drawSignatureWithSeal(Graphics2D g2d, int startY, LocalDateTime localDateTime) { |
|
|
|
|
|
g2d.setFont(signatureFont); |
|
|
|
|
|
FontMetrics metrics = g2d.getFontMetrics(); |
|
|
|
|
|
int lineHeight = metrics.getHeight(); |
|
|
|
|
|
|
|
|
|
|
|
String associationName = "合肥市律师协会"; |
|
|
|
|
|
int textWidth = metrics.stringWidth(associationName); |
|
|
|
|
|
|
|
|
|
|
|
// 测试字体是否能显示
|
|
|
|
|
|
if (textWidth == 0) { |
|
|
|
|
|
throw new RuntimeException("落款字体不可用,可能显示为方框"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
int textX = PAGE_WIDTH - RIGHT_MARGIN - textWidth; |
|
|
|
|
|
int textY = startY + lineHeight * 2; |
|
|
|
|
|
|
|
|
|
|
|
// 绘制印章
|
|
|
|
|
|
drawSeal(g2d, textX, textY, textWidth, lineHeight); |
|
|
|
|
|
|
|
|
|
|
|
// 绘制文字
|
|
|
|
|
|
g2d.drawString(associationName, textX, textY); |
|
|
|
|
|
|
|
|
|
|
|
// 绘制日期
|
|
|
|
|
|
String dateText = localDateTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")); |
|
|
|
|
|
int dateWidth = metrics.stringWidth(dateText); |
|
|
|
|
|
int dateX = PAGE_WIDTH - RIGHT_MARGIN - dateWidth; |
|
|
|
|
|
int dateY = textY + lineHeight + LINE_SPACING * 2; |
|
|
|
|
|
|
|
|
|
|
|
g2d.drawString(dateText, dateX, dateY); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 绘制印章 |
|
|
|
|
|
*/ |
|
|
|
|
|
private void drawSeal(Graphics2D g2d, int textX, int textY, int textWidth, int lineHeight) { |
|
|
|
|
|
try { |
|
|
|
|
|
int sealX = textX + (textWidth - SEAL_WIDTH) / 2; |
|
|
|
|
|
int sealY = textY - lineHeight * 2; |
|
|
|
|
|
|
|
|
|
|
|
Resource sealResource = new ClassPathResource(SEAL_IMAGE_PATH); |
|
|
|
|
|
if (sealResource.exists()) { |
|
|
|
|
|
BufferedImage sealImage = ImageIO.read(sealResource.getInputStream()); |
|
|
|
|
|
BufferedImage transparentSeal = removeWhiteBackground(sealImage); |
|
|
|
|
|
g2d.drawImage(transparentSeal, sealX, sealY, SEAL_WIDTH, SEAL_HEIGHT, null); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
// 忽略印章错误
|
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 移除图片白色背景 |
|
|
|
|
|
*/ |
|
|
|
|
|
private BufferedImage removeWhiteBackground(BufferedImage originalImage) { |
|
|
|
|
|
int width = originalImage.getWidth(); |
|
|
|
|
|
int height = originalImage.getHeight(); |
|
|
|
|
|
BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); |
|
|
|
|
|
|
|
|
|
|
|
for (int y = 0; y < height; y++) { |
|
|
|
|
|
for (int x = 0; x < width; x++) { |
|
|
|
|
|
int rgb = originalImage.getRGB(x, y); |
|
|
|
|
|
Color color = new Color(rgb, true); |
|
|
|
|
|
if (color.getRed() > 240 && color.getGreen() > 240 && color.getBlue() > 240) { |
|
|
|
|
|
result.setRGB(x, y, 0x00FFFFFF); |
|
|
|
|
|
} else { |
|
|
|
|
|
result.setRGB(x, y, rgb); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return result; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 将图片转换为字节数组 |
|
|
|
|
|
*/ |
|
|
|
|
|
private byte[] convertImageToBytes(BufferedImage image) throws IOException { |
|
|
|
|
|
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
|
|
|
|
|
ImageIO.write(image, "PNG", baos); |
|
|
|
|
|
return baos.toByteArray(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 字体测试接口(用于调试) |
|
|
|
|
|
*/ |
|
|
|
|
|
@Operation(summary = "字体测试接口 @author wzh") |
|
|
|
|
|
@GetMapping("/test/fonts") |
|
|
|
|
|
public String testFonts() { |
|
|
|
|
|
StringBuilder result = new StringBuilder(); |
|
|
|
|
|
result.append("<h1>字体测试</h1>"); |
|
|
|
|
|
|
|
|
// 生成Word文档
|
|
|
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); |
|
|
fileContent = wordCertificateService.generateCertificate(certificateData); |
|
|
String[] fonts = ge.getAvailableFontFamilyNames(); |
|
|
// fileName = "certificate.docx";
|
|
|
|
|
|
//contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result.append("<h2>可用字体 (").append(fonts.length).append("):</h2>"); |
|
|
|
|
|
result.append("<ul>"); |
|
|
|
|
|
for (String font : fonts) { |
|
|
|
|
|
if (font.contains("WenQuanYi") || font.contains("文泉驿") || |
|
|
|
|
|
font.contains("Song") || font.contains("Hei")) { |
|
|
|
|
|
result.append("<li><b>").append(font).append("</b></li>"); |
|
|
|
|
|
} else { |
|
|
|
|
|
result.append("<li>").append(font).append("</li>"); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
result.append("</ul>"); |
|
|
|
|
|
|
|
|
// 设置文档响应头
|
|
|
result.append("<h2>当前使用的字体:</h2>"); |
|
|
//response.setContentType(contentType);
|
|
|
result.append("<ul>"); |
|
|
//response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + fileName);
|
|
|
result.append("<li>标题字体: ").append(titleFont.getFontName()).append("</li>"); |
|
|
// 设置响应头
|
|
|
result.append("<li>正文字体: ").append(bodyFont.getFontName()).append("</li>"); |
|
|
response.setContentType("image/png"); |
|
|
result.append("<li>落款字体: ").append(signatureFont.getFontName()).append("</li>"); |
|
|
response.setHeader("Content-Disposition", "inline; filename=\"preview.png\""); |
|
|
result.append("</ul>"); |
|
|
response.setHeader("Cache-Control", "max-age=3600"); |
|
|
|
|
|
response.setContentLength(fileContent.length); |
|
|
|
|
|
|
|
|
|
|
|
// 将文件内容写入响应输出流
|
|
|
return result.toString(); |
|
|
response.getOutputStream().write(fileContent); |
|
|
|
|
|
response.getOutputStream().flush(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |