diff --git a/test/renderdiff/src/viewer_html/app.js b/test/renderdiff/src/viewer_html/app.js
index 677ea9a7f9..b78088cfbb 100644
--- a/test/renderdiff/src/viewer_html/app.js
+++ b/test/renderdiff/src/viewer_html/app.js
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js";
+import { LitElement, html, css, repeat } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js";
import './tools.js';
import './tiff-viewer.js';
@@ -32,6 +32,12 @@ const DIFF_VIEW = 'diff'
const GOLDEN_VIEW = 'golden'
const RENDERED_VIEW = 'rendered'
+const RES_EQUAL = "mismatched dimensions";
+const RES_MISMATCHED_DIMENSIONS = "mismatched dimensions";
+const RES_DIFFERENT_PIXELS = "different pixels";
+const RES_NOT_READY = "not ready";
+const FAILED_POSTIFX = ".failed";
+
class ExpandedPassedResult extends LitElement {
static get properties() {
return {
@@ -58,67 +64,264 @@ class ExpandedFailedResult extends LitElement {
static get properties() {
return {
content: { type: Object, attribute: 'content' },
- leftViewSelected: { type: String },
- rightViewSelected: { type: String },
+ tests: { type: Array },
};
}
- static styles = css`
- .viewer-container {
- display: flex;
- flex-direction: row;
- }
- .viewer {
- margin: 0 5px;
- }
- `;
-
- constructor() {
- super();
- this.content = null;
- this.leftViewSelected = GOLDEN_VIEW;
- this.rightViewSelected = DIFF_VIEW;
- this.addEventListener('radio-change', (ev) => {
- if (ev.detail.radioId == 'right') {
- this.rightViewSelected = ev.detail.value;
- }
- if (ev.detail.radioId == 'left') {
- this.leftViewSelected = ev.detail.value;
- }
- });
- }
-
- _viewer(name, choices, current) {
- const url = {
- [GOLDEN_VIEW]: getGoldenUrl,
- [RENDERED_VIEW]: getCompUrl,
- [DIFF_VIEW]: getDiffUrl,
- }[current](this.content);
- return html`
-
-
-
-
- `;
- }
-
render() {
if (!this.content) {
return html``;
}
- const v1 = this._viewer("left", [GOLDEN_VIEW, RENDERED_VIEW], this.leftViewSelected);
- const v2 = this._viewer("right", [RENDERED_VIEW, DIFF_VIEW], this.rightViewSelected);
return html`
-
- ${v1}
- ${v2}
-
+
+
`;
- return html``;
}
}
customElements.define('expanded-failed-result', ExpandedFailedResult);
+class ExpandedComparisonResult extends LitElement {
+ static get properties() {
+ return {
+ left: { type: Object },
+ right: { type: Object },
+ tests: { type: Array },
+ diffResult: { type: Object },
+ showDiff: {type: Boolean },
+ leftViewType: { type: String },
+ rightViewType: { type: String },
+ disableDropdowns: { type: Boolean },
+ };
+ }
+
+ static styles = css`
+ .main-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+ .viewer-container {
+ display: flex;
+ flex-direction: row;
+ }
+ #diffCanvas {
+ width: 100%;
+ height: 100%;
+ margin-top: 35px;
+ margin-bottom: 5px;
+ }
+ .selector {
+ margin: 8px 0;
+ }
+ `;
+
+ constructor() {
+ super();
+ this.left = null;
+ this.right = null;
+ this.tests = [];
+ this.showDiff = false;
+ this.leftImageLoaded = false;
+ this.rightImageLoaded = false;
+ this.leftViewType = 'golden';
+ this.rightViewType = 'golden';
+ this.disableDropdowns = false;
+
+ this.addEventListener(
+ 'image-loaded',
+ (ev) => {
+ if (ev.detail.name == this.left.name) {
+ this.leftImageLoaded = true;
+ }
+ if (ev.detail.name == this.right.name) {
+ this.rightImageLoaded = true;
+ }
+ if (this.leftImageLoaded && this.rightImageLoaded) {
+ this._triggerDiff();
+ }
+ }
+ );
+ }
+
+ _viewer(name, choices, current, viewType) {
+ const url = viewType == 'rendered' ? getCompUrl(current) : getGoldenUrl(current);
+ const onSelect = (ev) => {
+ const testName = ev.target.value;
+ const test = this.tests.find((t) => t.name == testName);
+ if (name == 'left') {
+ this.left = test;
+ this.leftImageLoaded = false;
+ } else {
+ this.right = test;
+ this.rightImageLoaded = false;
+ }
+ this.showDiff = false;
+ };
+
+ const dropdown = () => {
+ if (this.disableDropdowns) {
+ return html`${current.name} (${viewType})
`;
+ }
+ return html`
+
+ `;
+ }
+
+ return html`
+
+ ${dropdown()}
+
+
+ `;
+ }
+
+ _computeDiff() {
+ const tiffViewerLeft = this.shadowRoot.querySelector('#viewer-left');
+ const tiffViewerRight = this.shadowRoot.querySelector('#viewer-right');
+
+ if (!tiffViewerLeft || !tiffViewerRight) {
+ return {
+ "result": RES_NOT_READY,
+ };
+ }
+
+ const canvasLeft = tiffViewerLeft.shadowRoot.querySelector('canvas');
+ const canvasRight = tiffViewerRight.shadowRoot.querySelector('canvas');
+
+ if (!canvasLeft || !canvasRight) {
+ return {
+ "result": RES_NOT_READY,
+ };
+ }
+
+ const ctxLeft = canvasLeft.getContext('2d');
+ const ctxRight = canvasRight.getContext('2d');
+ const imgLeft = ctxLeft.getImageData(0, 0, canvasLeft.width, canvasLeft.height);
+ const imgRight = ctxRight.getImageData(0, 0, canvasRight.width, canvasRight.height);
+
+ if (imgLeft.width !== imgRight.width || imgLeft.height !== imgRight.height) {
+ console.error("Images have different dimensions");
+ return {
+ "result": RES_MISMATCHED_DIMENSIONS,
+ "explanation": "Images have different dimensions " +
+ "left=(" + imgLeft.width + ", " + imgLeft.height + ") " +
+ "right=(" + imgRight.width + ", " + imgRight.height + ")",
+ };
+ }
+
+ const imgDiff = new Uint8ClampedArray(imgLeft.width * imgLeft.height * 4);
+ const maxDiff = [0, 0, 0, 0];
+ for (let i = 0; i < imgLeft.data.length; i += 4) {
+ for (let j = 0; j < 4; j++) {
+ maxDiff[j] = Math.max(
+ maxDiff[j],
+ Math.min(255, Math.abs(imgLeft.data[i + j] - imgRight.data[i + j]))
+ );
+ imgDiff[i + j] = Math.abs(imgLeft.data[i + j] - imgRight.data[i + j]);
+ }
+ imgDiff[i + 3] = 255;
+ }
+
+ if (maxDiff[0] == 0 &&
+ maxDiff[1] == 0 &&
+ maxDiff[2] == 0 &&
+ maxDiff[3] == 0) {
+ return {
+ "result": RES_EQUAL,
+ "explanation": "Equal",
+ "dim": {"width": imgLeft.width, "height": imgLeft.height },
+ }
+ }
+
+ return {
+ "result": RES_DIFFERENT_PIXELS,
+ "explanation": "Images are different",
+ "dim": {"width": imgLeft.width, "height": imgLeft.height },
+ "maxDiff": maxDiff,
+ "diffImg": imgDiff,
+ };
+ }
+
+ _triggerDiff() {
+ const diff = this._computeDiff();
+ if (diff.result == RES_DIFFERENT_PIXELS) {
+ this.diffResult = diff;
+ this.showDiff = true;
+ }
+ }
+
+ updated(props) {
+ if (this.showDiff && this.diffResult) {
+ const mult = this.shadowRoot.querySelector('#diffMultiplier').value;
+ this._updateDiffCanvas(this.diffResult, mult);
+ }
+ }
+
+ _updateDiffCanvas(diffResult, mult) {
+ const diffCanvas = this.shadowRoot.querySelector('#diffCanvas');
+ const diffCtx = diffCanvas.getContext('2d');
+ diffCanvas.width = diffResult.dim.width;
+ diffCanvas.height = diffResult.dim.height;
+
+ // Create a fresh copy of the original diff data to avoid mutation.
+ const diffImgCopy = diffResult.diffImg.slice();
+
+ // Modify the copy, not the original.
+ for (let i = 0; i < diffImgCopy.length; i += 4) {
+ for (let j = 0; j < 3; j++) {
+ diffImgCopy[i + j] = Math.min(255, mult * diffImgCopy[i + j]);
+ }
+ diffImgCopy[i + 3] = 255; // Ensure alpha is always 255
+ }
+
+ // Create the ImageData from the modified copy.
+ const imgData = new ImageData(diffImgCopy, diffResult.dim.width, diffResult.dim.height);
+ diffCtx.putImageData(imgData, 0, 0);
+ }
+
+ render() {
+ if (!this.left || !this.right) {
+ return html``;
+ }
+
+ const diffStyle = !this.showDiff ? "display:none;" : "display:flex; flex-direction: column;";
+ const v1 = this._viewer("left", this.tests, this.left, this.leftViewType);
+ const v2 = this._viewer("right", this.tests, this.right, this.rightViewType);
+ const onMultiplierChange = (ev) => {
+ this._updateDiffCanvas(this.diffResult, ev.target.value);
+ };
+ return html`
+
+
+
+
+
Difference Multiplier
+
+
+
+
+ `;
+ }
+}
+customElements.define('expanded-comparison-result', ExpandedComparisonResult);
+
+
class App extends LitElement {
static styles = css`
.test-label {
@@ -146,8 +349,9 @@ class App extends LitElement {
padding-top: 20px;
}
.results {
- display: inline-flex;
+ display: flex;
flex-direction: row;
+ flex-wrap: wrap;
}
.results-container {
display: inline-flex;
@@ -157,7 +361,7 @@ class App extends LitElement {
background: #cbf1c9;
padding: 5px;
align-items: center;
- margin: 10px 0;
+ margin: 10px 10px;
}
.container-label {
margin-bottom: 10px;
@@ -180,11 +384,27 @@ class App extends LitElement {
font-size: 18px;
margin-bottom: 10px;
}
+ .compare-button {
+ margin: 10px;
+ padding: 10px;
+ border-radius: 5px;
+ background: #4285F4;
+ color: white;
+ cursor: pointer;
+ }
+ .compare-button[disabled] {
+ background: #ccc;
+ cursor: not-allowed;
+ margin: 10px 0;
+ }
`;
static properties = {
tests: {type: Array},
dialogContent: {type: Object},
+ selectedTests: {type: Array},
+ comparisonContent: {type: Object},
+ compareMode: {type: Boolean},
};
async _init() {
@@ -197,42 +417,76 @@ class App extends LitElement {
super();
this.tests = [];
this.dialogContent = null;
+ this.selectedTests = [];
+ this.comparisonContent = null;
+ this.compareMode = false;
this._init();
this.addEventListener('dialog-closed', () => {
this.dialogContent = null;
+ this.comparisonContent = null;
});
}
updated(props) {
- if (props.has('dialogContent')) {
+ if (props.has('dialogContent') || props.has('comparisonContent')) {
let dialog = this.shadowRoot.querySelector("#dialog");
- dialog.open = !!this.dialogContent;
+ dialog.open = !!this.dialogContent || !!this.comparisonContent;
}
}
- _onClick(testDetail) {
+ _onClick(testDetail, ev) {
+ if (this.compareMode) {
+ const index = this.selectedTests.indexOf(testDetail);
+ if (index > -1) {
+ this.selectedTests.splice(index, 1);
+ } else {
+ if (this.selectedTests.length >= 2) {
+ this.selectedTests.shift();
+ }
+ this.selectedTests.push(testDetail);
+ }
+ this.requestUpdate();
+ return;
+ }
this.dialogContent = testDetail;
this.shadowRoot.querySelector("#dialog").open = true;
}
+ _onCompare() {
+ if (this.selectedTests.length == 2) {
+ this.comparisonContent = {
+ left: this.selectedTests[0],
+ right: this.selectedTests[1],
+ };
+ }
+ }
+
+ _onCompareModeChange(e) {
+ this.compareMode = e.target.checked;
+ if (!this.compareMode) {
+ this.selectedTests = [];
+ }
+ }
+
render() {
let passed = this.tests.filter((t) => t.result == 'ok');
let failed = this.tests.filter((t) => t.result != 'ok');
const singleTiff = (url) => {
return html``;
};
- const buildTiffs = (ts) => ts.map((t) => {
+ const buildTiffs = (ts) => repeat(ts, (t) => t.name, (t) => {
const printName = t.name.replace('.tif', '');
const tiff = singleTiff(getGoldenUrl(t));
+ const selected = this.selectedTests.includes(t);
return html`
- this._onClick(t)}">
+
this._onClick(t, e)}" style=${selected ? "background: #eee" : ""}>
${printName}
${tiff}
`;
});
- const buildFailedTiffs = (ts) => ts.map((t) => {
+ const buildFailedTiffs = (ts) => repeat(ts, (t) => t.name, (t) => {
const printName = t.name.replace('.tif', '');
const goldenUrl = getGoldenUrl(t);
const compUrl = getCompUrl(t);
@@ -251,8 +505,8 @@ class App extends LitElement {
].map((a) => [singleTiff(a[0]), a[1]])
.map((a) => wrap(...a));
return html`
-
this._onClick(t)}">
-
${printName}
+
this._onClick(t, e)}" >
+
${printName}
${tiffs}
@@ -269,14 +523,30 @@ class App extends LitElement {
if (this.dialogContent.result == 'ok') {
return html`
`;
}
- return html`
`;
+ return html`
`;
})();
dialogHeader = this.dialogContent.name;
+ } else if (this.comparisonContent) {
+ dialogSlot = html`
+
+
+ `;
+ dialogHeader = 'Comparison';
+ }
+
+ const compareButton = () => {
+ if (!this.compareMode) {
+ return html``;
+ }
+ return html`
`;
}
return html`
-
+
Failed
${failedTiffs}
@@ -288,6 +558,11 @@ class App extends LitElement {
${passedTiffs}
+
+ ${compareButton()}
diff --git a/test/renderdiff/src/viewer_html/tiff-viewer.js b/test/renderdiff/src/viewer_html/tiff-viewer.js
index f56bd5cf96..6c46082594 100644
--- a/test/renderdiff/src/viewer_html/tiff-viewer.js
+++ b/test/renderdiff/src/viewer_html/tiff-viewer.js
@@ -29,11 +29,13 @@ export class TiffViewer extends LitElement {
static properties = {
fileurl: {type: String, attribute: 'fileurl'},
+ name: {type: String, attribute: 'name'},
};
constructor() {
super();
this.fileurl = null;
+ this.name = null;
}
render() {
@@ -78,6 +80,15 @@ export class TiffViewer extends LitElement {
const imageData = new ImageData(new Uint8ClampedArray(rgba), firstImage.width, firstImage.height);
ctx.putImageData(imageData, 0, 0);
+
+ this.dispatchEvent(new CustomEvent('image-loaded', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ name: this.name,
+ img: imageData,
+ }
+ }));
} catch (error) {
console.error('Error processing TIFF file:', error);
this._clearCanvas();