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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user