feat: add Java JRXML-to-PNG rendering pipeline with pixel-level SSIM comparison

- lib/java/: Java renderer (JrxmlRenderer) using JasperReports 6.21.0
  - JrxmlDebug for diagnostics, JrxmlGen for format reference
  - download_jars.sh for one-time dependency setup
- agent/nodes.py: _render_jrxml_to_png() and _compute_pixel_similarity()
  - Pixel comparison integrates into validate node (SSIM < 0.4 fails)
  - Pixel fidelity context injected into correct_jrxml for targeted fixes
- tests/test_pixel_comparison.py: 15 unit tests (render, SSIM, integration)
- .gitignore: exclude lib/java/*.jar, lib/java/*.class, tmp/
- CLAUDE.md: v11 changelog documenting the rendering pipeline
- All non-LLM tests pass (97/97)
This commit is contained in:
2026-05-23 15:09:55 +08:00
parent 9de75d2f25
commit bb6cc6e241
16 changed files with 837 additions and 8 deletions
+47
View File
@@ -0,0 +1,47 @@
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.design.*;
import net.sf.jasperreports.engine.xml.*;
import java.io.*;
public class JrxmlDebug {
public static void main(String[] args) throws Exception {
String path = args.length > 0 ? args[0] : "D:/Idea Project/jaspersoft/tmp/test_simple.jrxml";
File f = new File(path);
System.out.println("File: " + path + " (exists=" + f.exists() + ", len=" + f.length() + ")");
// Test 1: JRXmlLoader.load()
System.out.println("\n=== JRXmlLoader.load() ===");
try {
JasperDesign design = JRXmlLoader.load(f);
System.out.println("PASS: " + design.getName()
+ " pages=" + design.getPageWidth() + "x" + design.getPageHeight());
System.out.println(" Title: " + (design.getTitle() != null ? design.getTitle().getHeight() + "px" : "null"));
System.out.println(" Detail: " + (design.getDetailSection() != null ? "present" : "null"));
} catch (Throwable t) {
System.out.println("FAIL: " + t.getMessage());
Throwable c = t;
int d = 0;
while (c != null) {
System.out.println(" [" + d + "] " + c.getClass().getName() + ": " + c.getMessage());
for (int i = 0; i < Math.min(5, c.getStackTrace().length); i++)
System.out.println(" at " + c.getStackTrace()[i]);
c = c.getCause();
d++;
}
}
// Test 2: JasperCompileManager.compileReport()
System.out.println("\n=== JasperCompileManager.compileReport() ===");
try {
JasperReport report = JasperCompileManager.compileReport(path);
System.out.println("PASS: " + report.getName());
} catch (Throwable t) {
System.out.println("FAIL: " + t.getMessage());
Throwable c = t;
while (c != null) {
System.out.println(" -> " + c.getClass().getName() + ": " + c.getMessage());
c = c.getCause();
}
}
}
}
+58
View File
@@ -0,0 +1,58 @@
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.design.*;
import net.sf.jasperreports.jackson.util.*;
import java.io.*;
/**
* Generate a minimal JasperDesign programmatically, then serialize it
* via JacksonUtil to show the correct 7.x XML format.
*/
public class JrxmlGen {
public static void main(String[] args) throws Exception {
JasperDesign design = new JasperDesign();
design.setName("TestReport");
design.setPageWidth(595);
design.setPageHeight(842);
design.setColumnWidth(555);
design.setLeftMargin(20);
design.setRightMargin(20);
design.setTopMargin(20);
design.setBottomMargin(20);
design.setWhenNoDataType(com.fasterxml.jackson.databind.node.TextNode.valueOf("AllSectionsNoDetail"));
design.setQuery("SELECT 1");
JRDesignBand titleBand = new JRDesignBand();
titleBand.setHeight(50);
JRDesignStaticText st = new JRDesignStaticText();
st.setX(0);
st.setY(0);
st.setWidth(555);
st.setHeight(30);
st.setText("HELLO WORLD");
titleBand.addElement(st);
design.setTitle(titleBand);
JRDesignBand detailBand = new JRDesignBand();
detailBand.setHeight(20);
JRDesignStaticText dt = new JRDesignStaticText();
dt.setX(0);
dt.setY(0);
dt.setWidth(555);
dt.setHeight(20);
dt.setText("test row");
detailBand.addElement(dt);
design.setDetail(detailBand);
JacksonUtil util = JacksonUtil.getInstance(DefaultJasperReportsContext.getInstance());
String xml = util.saveXml(design);
System.out.println("=== Serialized 7.x JRXML ===");
System.out.println(xml);
String outPath = "D:/Idea Project/jaspersoft/tmp/test_reference.jrxml";
try (FileWriter fw = new FileWriter(outPath)) {
fw.write(xml);
}
System.out.println("\nSaved to: " + outPath);
}
}
+110
View File
@@ -0,0 +1,110 @@
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.export.*;
import net.sf.jasperreports.export.*;
import java.io.*;
import java.util.*;
import java.awt.image.*;
import javax.imageio.*;
/**
* Minimal JRXML → PNG renderer for pixel-level fidelity comparison.
* Usage: java JrxmlRenderer input.jrxml output.png [scale]
* scale: optional zoom factor (default 2.0 for readable text)
*
* Uses JasperReports 7.x exporter API (SimpleExporterInput / SimpleGraphics2DExporterOutput).
*/
public class JrxmlRenderer {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.err.println("Usage: java JrxmlRenderer <input.jrxml> <output.png> [scale]");
System.exit(1);
}
String jrxmlPath = args[0];
String outputPath = args[1];
float scale = args.length >= 3 ? Float.parseFloat(args[2]) : 2.0f;
File jrxmlFile = new File(jrxmlPath);
if (!jrxmlFile.exists()) {
System.err.println("ERROR: JRXML file not found: " + jrxmlPath);
System.exit(2);
}
try {
// 1. Compile JRXML → JasperReport
JasperReport report = JasperCompileManager.compileReport(jrxmlPath);
// 2. Fill with empty data source
Map<String, Object> params = new HashMap<>();
JasperPrint print = JasperFillManager.fillReport(report, params, new JREmptyDataSource());
int pages = print.getPages().size();
if (pages == 0) {
System.err.println("ERROR: Report has 0 pages after filling");
System.exit(4);
}
// 3. Calculate image dimensions from page size
// JasperReports uses 72 DPI, convert to pixels with scale
int pageWidth = (int) Math.ceil(report.getPageWidth() * scale);
int pageHeight = (int) Math.ceil(report.getPageHeight() * scale);
// 4. Render each page to a BufferedImage
java.util.List<BufferedImage> pageImages = new ArrayList<>();
for (int i = 0; i < pages; i++) {
BufferedImage pageImage = new BufferedImage(pageWidth, pageHeight, BufferedImage.TYPE_INT_RGB);
java.awt.Graphics2D g2d = pageImage.createGraphics();
g2d.setColor(java.awt.Color.WHITE);
g2d.fillRect(0, 0, pageWidth, pageHeight);
// Scale the graphics context
g2d.scale(scale, scale);
JRGraphics2DExporter exporter = new JRGraphics2DExporter();
exporter.setExporterInput(new SimpleExporterInput(print));
SimpleGraphics2DExporterOutput output = new SimpleGraphics2DExporterOutput();
output.setGraphics2D(g2d);
exporter.setExporterOutput(output);
SimpleGraphics2DReportConfiguration config = new SimpleGraphics2DReportConfiguration();
config.setPageIndex(i);
exporter.setConfiguration(config);
exporter.exportReport();
g2d.dispose();
pageImages.add(pageImage);
}
// 5. Combine pages into single tall image
int totalHeight = 0;
for (BufferedImage img : pageImages) {
totalHeight += img.getHeight();
}
BufferedImage combined = new BufferedImage(pageWidth, totalHeight, BufferedImage.TYPE_INT_RGB);
java.awt.Graphics2D g = combined.createGraphics();
g.setColor(java.awt.Color.WHITE);
g.fillRect(0, 0, pageWidth, totalHeight);
int yOffset = 0;
for (BufferedImage img : pageImages) {
g.drawImage(img, 0, yOffset, null);
yOffset += img.getHeight();
}
g.dispose();
// 6. Write PNG
ImageIO.write(combined, "png", new File(outputPath));
System.out.println("OK: " + outputPath
+ " (pages=" + pages
+ ", size=" + pageWidth + "x" + totalHeight
+ ", scale=" + scale + ")");
} catch (JRException e) {
System.err.println("JASPER_ERROR: " + e.getMessage());
e.printStackTrace();
System.exit(3);
}
}
}
+36
View File
@@ -0,0 +1,36 @@
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.design.*;
import net.sf.jasperreports.engine.xml.*;
import java.io.*;
public class JrxmlRendererTest {
public static void main(String[] args) throws Exception {
String path = args.length > 0 ? args[0] : "/tmp/test_render.jrxml";
File f = new File(path);
System.out.println("File: " + f.getAbsolutePath() + " exists=" + f.exists() + " len=" + f.length());
// Read content to check XML validity
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
String line;
int lineNum = 0;
while ((line = br.readLine()) != null && lineNum < 5) {
System.out.println(" L" + (++lineNum) + ": " + line);
}
}
try {
System.out.println("Step 1: Loading JRXML...");
JasperDesign design = JRXmlLoader.load(f);
System.out.println(" OK - name=" + design.getName());
} catch (JRException e) {
System.err.println("JR ERROR: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("GENERIC ERROR: " + e.getClass().getName() + ": " + e.getMessage());
e.printStackTrace();
} catch (Error e) {
System.err.println("FATAL ERROR: " + e.getClass().getName() + ": " + e.getMessage());
e.printStackTrace();
}
}
}
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
# Download JasperReports 6.21.0 and dependencies for JRXML-to-PNG rendering.
# Run this once after cloning the repo.
set -e
BASE="https://repo1.maven.org/maven2"
JARS=(
"net/sf/jasperreports/jasperreports/6.21.0/jasperreports-6.21.0.jar"
"commons-logging/commons-logging/1.3.5/commons-logging-1.3.5.jar"
"org/apache/commons/commons-collections4/4.5.0/commons-collections4-4.5.0.jar"
"commons-beanutils/commons-beanutils/1.10.1/commons-beanutils-1.10.1.jar"
"org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar"
"commons-digester/commons-digester/2.1/commons-digester-2.1.jar"
"com/lowagie/itext/2.1.7/itext-2.1.7.jar"
"org/jfree/jfreechart/1.5.5/jfreechart-1.5.5.jar"
"org/eclipse/jdt/ecj/3.38.0/ecj-3.38.0.jar"
)
for jar in "${JARS[@]}"; do
fname=$(basename "$jar")
if [ -f "$fname" ]; then
echo "SKIP: $fname (exists)"
else
echo "DOWNLOAD: $fname"
curl -sL -o "$fname" "$BASE/$jar"
fi
done
echo ""
echo "All JARs ready. Compile with:"
echo " javac -cp \"jasperreports-6.21.0.jar;...\" JrxmlRenderer.java"
echo " java -cp \".;jasperreports-6.21.0.jar;...\" JrxmlRenderer input.jrxml output.png 2.0"