cad pdf转换模块提取独立 优化多线程转换方法

This commit is contained in:
高雄
2026-01-09 15:44:02 +08:00
parent ea35da6694
commit 9a3ec88390
6 changed files with 34 additions and 323 deletions

View File

@@ -24,7 +24,7 @@
<jodconverter.version>4.4.11</jodconverter.version>
<poi.version>5.2.5</poi.version>
<xdocreport.version>1.0.6</xdocreport.version>
<aspose-cad.version>24.8</aspose-cad.version>
<aspose-cad.version>25.10</aspose-cad.version>
<!-- ========== PDF 处理 ========== -->
<pdfbox.version>3.0.6</pdfbox.version>

View File

@@ -4,32 +4,18 @@ import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.model.FileType;
import cn.keking.service.cache.CacheService;
import cn.keking.service.cache.NotResourceCache;
import cn.keking.utils.*;
import cn.keking.web.filter.BaseUrlFilter;
import com.aspose.cad.*;
import com.aspose.cad.fileformats.cad.CadDrawTypeMode;
import com.aspose.cad.fileformats.tiff.enums.TiffExpectedFormat;
import com.aspose.cad.imageoptions.*;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.tools.imageio.ImageIOUtil;
import org.apache.poi.EncryptedDocumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLDecoder;
import java.net.URLEncoder;
@@ -38,7 +24,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.stream.IntStream;
/**
@@ -47,10 +32,10 @@ import java.util.stream.IntStream;
*/
@Component
@DependsOn(ConfigConstants.BEAN_NAME)
public class FileHandlerService implements InitializingBean {
public class FileHandlerService {
private static final String PDF2JPG_IMAGE_FORMAT = ".jpg";
private static final String PDF_PASSWORD_MSG = "password";
private final Logger logger = LoggerFactory.getLogger(FileHandlerService.class);
private final String fileDir = ConfigConstants.getFileDir();
private final CacheService cacheService;
@@ -144,15 +129,6 @@ public class FileHandlerService implements InitializingBean {
cacheService.putImgCache(fileKey, imgs);
}
/**
* cad定义线程池
*/
private ExecutorService pool = null;
@Override
public void afterPropertiesSet() throws Exception {
pool = Executors.newFixedThreadPool(ConfigConstants.getCadThread());
}
/**
* 对转换后的文件进行操作(改变编码方式)
@@ -192,7 +168,7 @@ public class FileHandlerService implements InitializingBean {
* @param index 图片索引
* @return 图片访问地址
*/
private String getPdf2jpgUrl(String pdfFilePath, int index) {
public String getPdf2jpgUrl(String pdfFilePath, int index) {
String baseUrl = BaseUrlFilter.getBaseUrl();
pdfFilePath = pdfFilePath.replace(fileDir, "");
String pdfFolder = pdfFilePath.substring(0, pdfFilePath.length() - 4);
@@ -214,230 +190,20 @@ public class FileHandlerService implements InitializingBean {
* @param pdfFilePath pdf文件路径
* @return 图片访问集合
*/
private List<String> loadPdf2jpgCache(String pdfFilePath) {
public List<String> loadPdf2jpgCache(String pdfFilePath) { // 移除 static 修饰符
List<String> imageUrls = new ArrayList<>();
Integer imageCount = this.getPdf2jpgCache(pdfFilePath);
Integer imageCount = this.getPdf2jpgCache(pdfFilePath); // 使用 this. 调用
if (Objects.isNull(imageCount)) {
return imageUrls;
}
IntStream.range(0, imageCount).forEach(i -> {
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, i);
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, i); // 使用 this. 调用
imageUrls.add(imageUrl);
});
return imageUrls;
}
/**
* pdf文件转换成jpg图片集
* fileNameFilePath pdf文件路径
* pdfFilePath pdf输出文件路径
* pdfName pdf文件名称
* loadPdf2jpgCache 图片访问集合
*/
public List<String> pdf2jpg(String fileNameFilePath, String pdfFilePath, String pdfName, FileAttribute fileAttribute) throws Exception {
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
String filePassword = fileAttribute.getFilePassword();
PDDocument doc;
final String[] pdfPassword = {null};
final int[] pageCount = new int[1];
if (!forceUpdatedCache) {
List<String> cacheResult = this.loadPdf2jpgCache(pdfFilePath);
if (!CollectionUtils.isEmpty(cacheResult)) {
return cacheResult;
}
}
List<String> imageUrls = new ArrayList<>();
File pdfFile = new File(fileNameFilePath);
if (!pdfFile.exists()) {
return null;
}
int index = pdfFilePath.lastIndexOf(".");
String folder = pdfFilePath.substring(0, index);
File path = new File(folder);
if (!path.exists() && !path.mkdirs()) {
logger.error("创建转换文件【{}】目录失败,请检查目录权限!", folder);
}
try {
doc = Loader.loadPDF(pdfFile, filePassword);
doc.setResourceCache(new NotResourceCache());
pageCount[0] = doc.getNumberOfPages();
} catch (IOException e) {
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) {
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
pdfPassword[0] = PDF_PASSWORD_MSG; //查询到该文件是密码文件 输出带密码的值
}
}
}
if (!PDF_PASSWORD_MSG.equals(pdfPassword[0])) { //该文件异常 错误原因非密码原因输出错误
logger.error("Convert pdf exception, pdfFilePath{}", pdfFilePath, e);
}
throw new Exception(e);
}
Callable <List<String>> call = () -> {
try {
String imageFilePath;
BufferedImage image = null;
PDFRenderer pdfRenderer = new PDFRenderer(doc);
pdfRenderer.setSubsamplingAllowed(true);
for (int pageIndex = 0; pageIndex < pageCount[0]; pageIndex++) {
imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT;
image = pdfRenderer.renderImageWithDPI(pageIndex, ConfigConstants.getPdf2JpgDpi(), ImageType.RGB);
ImageIOUtil.writeImage(image, imageFilePath, ConfigConstants.getPdf2JpgDpi());
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, pageIndex);
imageUrls.add(imageUrl);
}
image.flush();
} catch (IOException e) {
throw new Exception(e);
} finally {
doc.close();
}
return imageUrls;
};
Future<List<String>> result = pool.submit(call);
int pdftimeout;
if(pageCount[0] <=50){
pdftimeout = ConfigConstants.getPdfTimeout();
}else if(pageCount[0] <=200){
pdftimeout = ConfigConstants.getPdfTimeout80();
}else {
pdftimeout = ConfigConstants.getPdfTimeout200();
}
try {
result.get(pdftimeout, TimeUnit.SECONDS);
// 如果在超时时间内没有数据返回则抛出TimeoutException异常
} catch (InterruptedException | ExecutionException e) {
throw new Exception(e);
} catch (TimeoutException e) {
throw new Exception("overtime");
} finally {
//关闭
doc.close();
}
if (usePasswordCache || ObjectUtils.isEmpty(filePassword)) { //加密文件 判断是否启用缓存命令
this.addPdf2jpgCache(pdfFilePath, pageCount[0]);
}
return imageUrls;
}
/**
* cad文件转pdf
*
* @param inputFilePath cad文件路径
* @param outputFilePath pdf输出文件路径
* @return 转换是否成功
*/
public String cadToPdf(String inputFilePath, String outputFilePath, String cadPreviewType, FileAttribute fileAttribute) throws Exception {
final InterruptionTokenSource source = new InterruptionTokenSource();//CAD延时
final SvgOptions SvgOptions = new SvgOptions();
final PdfOptions pdfOptions = new PdfOptions();
final TiffOptions TiffOptions = new TiffOptions(TiffExpectedFormat.TiffJpegRgb);
if (fileAttribute.isCompressFile()) { //判断 是压缩包的创建新的目录
int index = outputFilePath.lastIndexOf("/"); //截取最后一个斜杠的前面的内容
String folder = outputFilePath.substring(0, index);
File path = new File(folder);
//目录不存在 创建新的目录
if (!path.exists()) {
path.mkdirs();
}
}
File outputFile = new File(outputFilePath);
try {
LoadOptions opts = new LoadOptions();
opts.setSpecifiedEncoding(CodePages.SimpChinese);
final Image cadImage = Image.load(inputFilePath, opts);
try {
RasterizationQuality rasterizationQuality = new RasterizationQuality();
rasterizationQuality.setArc(RasterizationQualityValue.High);
rasterizationQuality.setHatch(RasterizationQualityValue.High);
rasterizationQuality.setText(RasterizationQualityValue.High);
rasterizationQuality.setOle(RasterizationQualityValue.High);
rasterizationQuality.setObjectsPrecision(RasterizationQualityValue.High);
rasterizationQuality.setTextThicknessNormalization(true);
CadRasterizationOptions cadRasterizationOptions = new CadRasterizationOptions();
cadRasterizationOptions.setBackgroundColor(Color.getWhite());
cadRasterizationOptions.setPageWidth(cadImage.getWidth());
cadRasterizationOptions.setPageHeight(cadImage.getHeight());
cadRasterizationOptions.setUnitType(cadImage.getUnitType());
cadRasterizationOptions.setAutomaticLayoutsScaling(false);
cadRasterizationOptions.setNoScaling(false);
cadRasterizationOptions.setQuality(rasterizationQuality);
cadRasterizationOptions.setDrawType(CadDrawTypeMode.UseObjectColor);
cadRasterizationOptions.setExportAllLayoutContent(true);
cadRasterizationOptions.setVisibilityMode(VisibilityMode.AsScreen);
switch (cadPreviewType) { //新增格式方法
case "svg":
SvgOptions.setVectorRasterizationOptions(cadRasterizationOptions);
SvgOptions.setInterruptionToken(source.getToken());
break;
case "pdf":
pdfOptions.setVectorRasterizationOptions(cadRasterizationOptions);
pdfOptions.setInterruptionToken(source.getToken());
break;
case "tif":
TiffOptions.setVectorRasterizationOptions(cadRasterizationOptions);
TiffOptions.setInterruptionToken(source.getToken());
break;
}
Callable<String> call = () -> {
try (OutputStream stream = new FileOutputStream(outputFile)) {
switch (cadPreviewType) {
case "svg":
cadImage.save(stream, SvgOptions);
break;
case "pdf":
cadImage.save(stream, pdfOptions);
break;
case "tif":
cadImage.save(stream, TiffOptions);
break;
}
} catch (IOException e) {
logger.error("CADFileNotFoundExceptioninputFilePath{}", inputFilePath, e);
return null;
} finally {
cadImage.dispose();
source.interrupt(); //结束任务
source.dispose();
}
return "true";
};
Future<String> result = pool.submit(call);
try {
result.get(Long.parseLong(ConfigConstants.getCadTimeout()), TimeUnit.SECONDS);
// 如果在超时时间内没有数据返回则抛出TimeoutException异常
} catch (InterruptedException e) {
logger.error("CAD转换文件异常", e);
return null;
} catch (ExecutionException e) {
logger.error("CAD转换在尝试取得任务结果时出错", e);
return null;
} catch (TimeoutException e) {
logger.error("CAD转换时间超时", e);
return null;
} finally {
source.interrupt(); //结束任务
source.dispose();
cadImage.dispose();
// pool.shutdownNow();
}
} finally {
source.dispose();
cadImage.dispose();
}
} finally {
source.dispose();
}
if(cadPreviewType.equals("svg")){
System.out.println(" This is a new line.");
System.out.println(outputFilePath);
RemoveSvgAdSimple.removeSvgAdFromFile(outputFilePath);
}
return "true";
}
/**
* @param str 原字符串(待截取原串)

View File

@@ -3,6 +3,7 @@ package cn.keking.service.impl;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import cn.keking.service.CadToPdfService;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.utils.DownloadUtils;
@@ -15,7 +16,6 @@ import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import static cn.keking.service.impl.OfficeFilePreviewImpl.getPreviewType;
/**
* @author chenjh
@@ -29,11 +29,15 @@ public class CadFilePreviewImpl implements FilePreview {
private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
private final FileHandlerService fileHandlerService;
private final CadToPdfService cadtopdfservice;
private final OtherFilePreviewImpl otherFilePreview;
private final OfficeFilePreviewImpl officefilepreviewimpl;
public CadFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
public CadFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview, CadToPdfService cadtopdfservice,OfficeFilePreviewImpl officefilepreviewimpl ) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
this.cadtopdfservice = cadtopdfservice;
this.officefilepreviewimpl = officefilepreviewimpl;
}
@Override
@@ -53,14 +57,14 @@ public class CadFilePreviewImpl implements FilePreview {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
String filePath = response.getContent();
String imageUrls = null;
boolean imageUrls = false;
if (StringUtils.hasText(outFilePath)) {
try {
imageUrls = fileHandlerService.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute);
imageUrls = cadtopdfservice.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute);
} catch (Exception e) {
logger.error("Failed to convert CAD file: {}", filePath, e);
}
if (imageUrls == null) {
if (!imageUrls) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常请联系管理员");
}
//是否保留CAD源文件
@@ -82,7 +86,7 @@ public class CadFilePreviewImpl implements FilePreview {
return SVG_FILE_PREVIEW_PAGE;
}
if (baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) {
return getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath, fileHandlerService, OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview);
return officefilepreviewimpl.getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath, fileHandlerService, OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview);
}
model.addAttribute("pdfUrl", cacheName);
return PDF_FILE_PREVIEW_PAGE;

View File

@@ -6,6 +6,7 @@ import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.OfficeToPdfService;
import cn.keking.service.PdfToJpgService;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.OfficeUtils;
@@ -35,11 +36,13 @@ public class OfficeFilePreviewImpl implements FilePreview {
private final FileHandlerService fileHandlerService;
private final OfficeToPdfService officeToPdfService;
private final OtherFilePreviewImpl otherFilePreview;
private final PdfToJpgService pdftojpgservice;
public OfficeFilePreviewImpl(FileHandlerService fileHandlerService, OfficeToPdfService officeToPdfService, OtherFilePreviewImpl otherFilePreview) {
public OfficeFilePreviewImpl(FileHandlerService fileHandlerService, OfficeToPdfService officeToPdfService, OtherFilePreviewImpl otherFilePreview, PdfToJpgService pdftojpgservice) {
this.fileHandlerService = fileHandlerService;
this.officeToPdfService = officeToPdfService;
this.otherFilePreview = otherFilePreview;
this.pdftojpgservice = pdftojpgservice;
}
@Override
@@ -115,12 +118,12 @@ public class OfficeFilePreviewImpl implements FilePreview {
return isHtmlView ? EXEL_FILE_PREVIEW_PAGE : PDF_FILE_PREVIEW_PAGE;
}
static String getPreviewType(Model model, FileAttribute fileAttribute, String officePreviewType, String pdfName, String outFilePath, FileHandlerService fileHandlerService, String officePreviewTypeImage, OtherFilePreviewImpl otherFilePreview) {
String getPreviewType(Model model, FileAttribute fileAttribute, String officePreviewType, String pdfName, String outFilePath, FileHandlerService fileHandlerService, String officePreviewTypeImage, OtherFilePreviewImpl otherFilePreview) {
String suffix = fileAttribute.getSuffix();
boolean isPPT = suffix.equalsIgnoreCase("ppt") || suffix.equalsIgnoreCase("pptx");
List<String> imageUrls = null;
try {
imageUrls = fileHandlerService.pdf2jpg(outFilePath,outFilePath, pdfName, fileAttribute);
imageUrls = pdftojpgservice.pdf2jpg(outFilePath,outFilePath, pdfName, fileAttribute);
} catch (Exception e) {
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) {

View File

@@ -5,6 +5,7 @@ import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.PdfToJpgService;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.WebUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -24,10 +25,12 @@ public class PdfFilePreviewImpl implements FilePreview {
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
private final PdfToJpgService pdftojpgservice;
private static final String PDF_PASSWORD_MSG = "password";
public PdfFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
public PdfFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview, PdfToJpgService pdftojpgservice) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
this.pdftojpgservice = pdftojpgservice;
}
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
@@ -51,7 +54,7 @@ public class PdfFilePreviewImpl implements FilePreview {
}
List<String> imageUrls;
try {
imageUrls = fileHandlerService.pdf2jpg(originFilePath,outFilePath, pdfName, fileAttribute);
imageUrls = pdftojpgservice.pdf2jpg(originFilePath,outFilePath, pdfName, fileAttribute);
} catch (Exception e) {
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) {

View File

@@ -4,74 +4,9 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class RemoveSvgAdSimple {
/**
* 改进版本直接处理SVG内容保留包含transform的<g>元素及其内容
* @param svgContent SVG内容字符串
* @return 清理后的SVG内容
*/
public static String removeSvgAdPrecisely(String svgContent) {
// 使用非贪婪模式匹配包含transform的<g>元素及其完整内容
String preservePattern = "<g\\s+[^>]*transform\\s*=\\s*\"[^\"]*\"[^>]*>.*?</g>";
// 查找所有包含transform的<g>元素
Pattern pattern = Pattern.compile(preservePattern, Pattern.DOTALL);
Matcher matcher = pattern.matcher(svgContent);
StringBuilder result = new StringBuilder();
// 添加XML声明和SVG根元素
if (svgContent.contains("<?xml")) {
result.append("<?xml version=\"1.0\" standalone=\"no\"?>");
}
// 找到SVG开始标签
int svgStart = svgContent.indexOf("<svg");
int svgEnd = svgContent.indexOf(">", svgStart) + 1;
if (svgStart != -1) {
// 添加SVG开始标签
result.append(svgContent.substring(svgStart, svgEnd));
// 收集所有包含transform的<g>元素
while (matcher.find()) {
result.append("\n").append(matcher.group());
}
// 添加SVG结束标签
result.append("\n</svg>");
} else {
// 如果没有找到SVG标签返回空或原始内容
return svgContent;
}
return result.toString();
}
/**
* 简单暴力版本:直接删除特定广告内容
* @param svgContent SVG内容字符串
* @return 清理后的SVG内容
*/
public static String removeSvgAdSimple(String svgContent) {
// 查找包含广告的<g>元素根据你的示例广告通常包含stroke="#FFFFFF"等特征)
String adPattern1 = "<g>\\s*<path[^>]*stroke=\"#FFFFFF\"[^>]*>.*?</path>\\s*<path[^>]*fill=\"#FFFFFF\"[^>]*>.*?</path>\\s*</g>";
String adPattern2 = "<g>\\s*<path[^>]*M0 0L[^>]*stroke=\"#FFFFFF\"[^>]*>.*?</path>.*?</g>";
String result = svgContent;
result = result.replaceAll(adPattern1, "");
result = result.replaceAll(adPattern2, "");
// 也可以直接删除所有不含transform属性的顶级<g>元素
// 这个正则会删除不带transform的顶级<g>,但保留嵌套的<g>
result = result.replaceAll("(?s)<g>(?:(?!<g>).)*?</g>", "");
return result;
}
/**
* 更可靠的版本使用DOM解析思路但用正则实现
@@ -84,7 +19,7 @@ public class RemoveSvgAdSimple {
// 找到XML声明
if (svgContent.contains("<?xml")) {
int xmlEnd = svgContent.indexOf("?>") + 2;
cleaned.append(svgContent.substring(0, xmlEnd)).append("\n");
cleaned.append(svgContent, 0, xmlEnd).append("\n");
}
// 找到SVG开始标签
@@ -92,7 +27,7 @@ public class RemoveSvgAdSimple {
if (svgStart == -1) return svgContent;
int svgEnd = svgContent.indexOf(">", svgStart) + 1;
cleaned.append(svgContent.substring(svgStart, svgEnd)).append("\n");
cleaned.append(svgContent, svgStart, svgEnd).append("\n");
// 解析剩余内容
String remaining = svgContent.substring(svgEnd);
@@ -133,7 +68,7 @@ public class RemoveSvgAdSimple {
}
if (gClose != -1) {
cleaned.append(remaining.substring(gStart, gClose)).append("\n");
cleaned.append(remaining, gStart, gClose).append("\n");
pos = gClose;
} else {
pos = gTagEnd + 1;
@@ -166,14 +101,14 @@ public class RemoveSvgAdSimple {
public static void removeSvgAdFromFile(String sourceFilePath, String targetFilePath) throws IOException {
// 读取文件内容
Path sourcePath = Paths.get(sourceFilePath);
String svgContent = new String(Files.readAllBytes(sourcePath), StandardCharsets.UTF_8);
String svgContent = Files.readString(sourcePath);
// 清理SVG广告
String cleanedContent = removeSvgAdReliable(svgContent);
// 写入目标文件
Path targetPath = Paths.get(targetFilePath);
Files.write(targetPath, cleanedContent.getBytes(StandardCharsets.UTF_8));
Files.writeString(targetPath, cleanedContent);
System.out.println("SVG广告清理完成");
}