fix(similar-asin): 货源结果图回WPS DISPIMG并修正协议,WPS打开不再#REF/破损
task-284 曾把图片协议改为 WPS DISPIMG(cellimages.xml),WPS 打开显示 #REF; 9/8 切回 Excel 365 richData 单元格图片后,WPS(用户主环境,12.1 实测)仍不识别, 单元格显示为 #VALUE! 错误值即图片破损。 根因:两套协议都只被单方支持——DISPIMG 是 WPS 私有协议,richData 是 Excel 365 私有协议。 本次回到 WPS DISPIMG(WPS 原生),并修复 WpsCellImageWriter 三处与 WPS 原生产物不一致的 协议细节(此前即 #REF 根因,已在 WPS 12.1.0.28505 实测三图渲染正常): 1. [Content_Types].xml 应为 application/vnd.wps-officedocument.cellimage+xml 2. workbook.xml.rels 关系类型应为 wps.cn/officeDocument/2020/cellImage 3. 公式格需 f t=shared ref + v=公式字符串(POI 输出裸 <f> 且引号转义为 ", patch 阶段统一规范化;保留原格样式) 另 spPr 补 a:xfrm(原图尺寸)/a:noFill/a:ln w=9525(实测样本对齐); patch 幂等(重复 patch 不再 duplicate entry);失败图留空不再写 URL 文本。 新增 WpsCellImageWriterProtocolTest 锁死协议契约防回归。
This commit is contained in:
+2
-3
@@ -229,13 +229,12 @@ class SimilarAsinImageEmbedderTest {
|
||||
var row = workbook.createSheet().createRow(1);
|
||||
|
||||
SimilarAsinImageEmbedder.ImageDim result = failedEmbedder.embedAsExcelCellImage(
|
||||
1, 9, url, row, new HashMap<>(), ExcelCellImageWriter.createSession(), spool);
|
||||
1, 9, url, row, new HashMap<>(), WpsCellImageWriter.createSession(), spool);
|
||||
|
||||
assertEquals(SimilarAsinImageEmbedder.DOWNLOAD_MAX_RETRY + 1, callsAfterPrefetch);
|
||||
assertEquals(callsAfterPrefetch, networkCalls.get());
|
||||
assertNull(result);
|
||||
assertTrue(row.getCell(9) != null, "预取失败应回退为 URL 文本便于人工核对");
|
||||
assertEquals(url, row.getCell(9).getStringCellValue());
|
||||
assertNull(row.getCell(9), "预取失败(WPS DISPIMG 路径)单元格留空,不写 URL 文本");
|
||||
} finally {
|
||||
failedEmbedder.shutdown();
|
||||
}
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package com.nanri.aiimage.modules.similarasin.util;
|
||||
|
||||
import org.apache.poi.ss.usermodel.CellStyle;
|
||||
import org.apache.poi.ss.usermodel.Font;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* WPS DISPIMG 协议回归测试:真实写入流水线(SXSSF 写公式格 → patch)产出的 part 必须与
|
||||
* WPS 12.1 实测通过的原生形态一致。2026-09-09 曾因三处协议细节与 WPS 原生产物不一致
|
||||
* (Content-Type / workbook 关系类型 / 公式格形态)导致 WPS 打开显示 #REF,本测试锁死这些契约。
|
||||
*/
|
||||
class WpsCellImageWriterProtocolTest {
|
||||
|
||||
private static byte[] jpeg(int size) throws Exception {
|
||||
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = image.createGraphics();
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillRect(0, 0, size, size);
|
||||
g.dispose();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
assertTrue(ImageIO.write(image, "jpg", baos), "JPEG writer should be available");
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
@Test
|
||||
void patchedWorkbookMatchesWpsNativeShape() throws Exception {
|
||||
File xlsx = File.createTempFile("wps-dispimg-protocol-", ".xlsx");
|
||||
xlsx.deleteOnExit();
|
||||
try {
|
||||
WpsCellImageWriter.Session session = WpsCellImageWriter.createSession();
|
||||
byte[] jpeg = jpeg(300);
|
||||
String imageId = session.registerImage(jpeg, 300, 300);
|
||||
assertNotNull(imageId, "图片注册应返回 ID");
|
||||
assertTrue(imageId.matches("ID_[0-9A-F]{32}"), "图片 ID 应为 WPS 风格 ID_+32hex, got=" + imageId);
|
||||
|
||||
try (SXSSFWorkbook workbook = new SXSSFWorkbook(200)) {
|
||||
Sheet sheet = workbook.createSheet("相似asin检测");
|
||||
CellStyle headerStyle = workbook.createCellStyle();
|
||||
Font font = workbook.createFont();
|
||||
font.setBold(true);
|
||||
headerStyle.setFont(font);
|
||||
sheet.setColumnWidth(12, 80 * 256);
|
||||
Row header = sheet.createRow(0);
|
||||
header.createCell(0).setCellValue("id");
|
||||
sheet.createRow(1).createCell(0).setCellValue("827665");
|
||||
Row row = sheet.createRow(1);
|
||||
row.setHeightInPoints(409f);
|
||||
row.createCell(12).setCellFormula(WpsCellImageWriter.dispImgFormula(imageId));
|
||||
try (var fos = Files.newOutputStream(xlsx.toPath())) {
|
||||
workbook.write(fos);
|
||||
}
|
||||
workbook.dispose();
|
||||
}
|
||||
WpsCellImageWriter.patchXlsxFile(xlsx, session);
|
||||
|
||||
try (ZipFile zf = new ZipFile(xlsx)) {
|
||||
String sheetXml = readEntry(zf, "xl/worksheets/sheet1.xml");
|
||||
assertTrue(sheetXml.contains("<f t=\"shared\" ref=\"M2\">_xlfn.DISPIMG(\"" + imageId + "\",1)</f>"),
|
||||
"公式格应为 WPS 原生形态(f t=shared/ref+公式字符串 v), got=" + snippet(sheetXml, "M2"));
|
||||
assertTrue(sheetXml.contains("<v>=DISPIMG(\"" + imageId + "\",1)</v>"),
|
||||
"公式格 <v> 应为 =DISPIMG(...) 形态");
|
||||
|
||||
String ct = readEntry(zf, "[Content_Types].xml");
|
||||
assertTrue(ct.contains("application/vnd.wps-officedocument.cellimage+xml"),
|
||||
"cellimages Content-Type 应为 vnd.wps-officedocument.cellimage+xml");
|
||||
|
||||
String rels = readEntry(zf, "xl/_rels/workbook.xml.rels");
|
||||
assertTrue(rels.contains("http://www.wps.cn/officeDocument/2020/cellImage"),
|
||||
"cellimages 关系类型应为 wps.cn/officeDocument/2020/cellImage");
|
||||
|
||||
String cellImages = readEntry(zf, "xl/cellimages.xml");
|
||||
assertTrue(cellImages.contains("r:embed=\"rId1\""), "cellimages.xml 应引用 rId1");
|
||||
assertTrue(cellImages.contains("<a:xfrm>"), "spPr 应含 a:xfrm");
|
||||
assertTrue(cellImages.contains("cx=\"2857500\""), "a:ext 应按 300px×9525EMU 换算");
|
||||
assertTrue(cellImages.contains("name=\"" + imageId + "\""), "cNvPr name 应为图片 ID");
|
||||
|
||||
String cellImagesRels = readEntry(zf, "xl/_rels/cellimages.xml.rels");
|
||||
assertTrue(cellImagesRels.contains("Target=\"media/cellimage1.jpeg\""), "rels 应指向 media/cellimage1.jpeg");
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(xlsx.toPath());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void repatchIsIdempotent() throws Exception {
|
||||
File xlsx = File.createTempFile("wps-dispimg-idempotent-", ".xlsx");
|
||||
xlsx.deleteOnExit();
|
||||
try {
|
||||
WpsCellImageWriter.Session session = WpsCellImageWriter.createSession();
|
||||
String imageId = session.registerImage(jpeg(64), 64, 64);
|
||||
try (SXSSFWorkbook workbook = new SXSSFWorkbook(200)) {
|
||||
Sheet sheet = workbook.createSheet("s");
|
||||
Row row = sheet.createRow(1);
|
||||
row.setHeightInPoints(409f);
|
||||
row.createCell(12).setCellFormula(WpsCellImageWriter.dispImgFormula(imageId));
|
||||
try (var fos = Files.newOutputStream(xlsx.toPath())) {
|
||||
workbook.write(fos);
|
||||
}
|
||||
workbook.dispose();
|
||||
}
|
||||
WpsCellImageWriter.patchXlsxFile(xlsx, session);
|
||||
WpsCellImageWriter.patchXlsxFile(xlsx, session);
|
||||
try (ZipFile zf = new ZipFile(xlsx)) {
|
||||
String sheetXml = readEntry(zf, "xl/worksheets/sheet1.xml");
|
||||
assertTrue(sheetXml.contains("<f t=\"shared\" ref=\"M2\">_xlfn.DISPIMG(\"" + imageId + "\",1)</f>"),
|
||||
"重复 patch 不应破坏公式格");
|
||||
assertTrue(!sheetXml.contains("<f>_xlfn.DISPIMG"),
|
||||
"不应残留未规范化的裸公式格");
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(xlsx.toPath());
|
||||
}
|
||||
}
|
||||
|
||||
private static String readEntry(ZipFile zf, String name) throws Exception {
|
||||
try (var in = zf.getInputStream(zf.getEntry(name))) {
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static String snippet(String xml, String cellRef) {
|
||||
int idx = xml.indexOf("r=\"" + cellRef + "\"");
|
||||
return idx < 0 ? "<not found>" : xml.substring(Math.max(0, idx), Math.min(xml.length(), idx + 160));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user