blob: 5a289df60ed9e1cb0a06d37b0a474e04d983ee4d [file] [edit]
/**
* @license
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html, css, LitElement, PropertyValues} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {ImageInfo} from '../gr-opacity-diff-mode/gr-opacity-diff-mode';
const DEFAULT_SETTING = {
errorType: 'flat',
largeImageThreshold: 1200,
};
@customElement('gr-resemble-diff-mode')
export class ResembleDiffMode extends LitElement {
static override get styles() {
return [
css`
:host {
display: block;
}
h2 {
display: none;
}
:host([loading]) #imageDiff {
display: none;
}
:host([loading]) h2 {
display: inline;
padding: 1em 0;
}
.toggle {
padding: 0.5em;
}
#controlsContainer {
align-items: center;
border-top: 1px solid var(--border-color, #ddd);
display: flex;
justify-content: space-between;
padding: 1em;
white-space: nowrap;
width: 100%;
}
#diffContainer {
display: block;
width: 100%;
}
#color {
border: 1px solid var(--border-color, #ddd);
border-radius: 2px;
}
#fullscreen {
border: 1px solid var(--border-color, #ddd);
border-radius: 2px;
color: var(--primary-text-color, #000);
padding: 0.5em;
}
#controlsContainer,
#color,
#fullscreen {
background-color: var(--table-header-background-color, #fafafa);
}
#color:hover,
#fullscreen:hover {
background-color: var(--header-background-color, #eeeeee);
}
#imageDiff {
display: block;
height: auto;
margin: auto;
max-width: 50em;
}
#modeContainer {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
display: block;
margin: 1em 0em;
width: 50em;
}
`
];
}
@property({type: Object})
baseImage?: ImageInfo;
@property({type: Object})
revisionImage?: ImageInfo;
@state()
protected colorValue = '#00ffff';
@state()
protected difference = 0;
@state()
protected ignoreColors = false;
@state()
protected transparent = false;
@state()
protected diffImageSrc = '';
@property({type: Boolean, reflect: true})
loading = false;
override render() {
return html`
<div id="modeContainer">
<div id="diffContainer">
<h2>Loading...</h2>
<img id="imageDiff" src=${this.diffImageSrc || ''} />
</div>
<div id="controlsContainer">
<div>${this.difference}% changed</div>
<label class="toggle">
<input
id="ignoreColorsToggle"
type="checkbox"
.checked=${this.ignoreColors}
@click=${this.handleIgnoreColorsToggle}
/>
Ignore colors
</label>
<label class="toggle">
<input
id="transparentToggle"
type="checkbox"
.checked=${this.transparent}
@click=${this.handleTransparentToggle}
/>
Transparent
</label>
<input
id="color"
type="color"
.value=${this.colorValue}
@change=${this.handleColorChange}
/>
<button id="fullscreen" @click=${this.handleFullScreen}>
View full sized
</button>
</div>
</div>
`;
}
override updated(changedProperties: PropertyValues) {
if (
changedProperties.has('baseImage') ||
changedProperties.has('revisionImage')
) {
this.handleImageDiff(this.baseImage, this.revisionImage);
}
}
override connectedCallback() {
super.connectedCallback();
window.resemble.outputSettings(DEFAULT_SETTING);
}
protected handleImageDiff(baseImage?: ImageInfo, revisionImage?: ImageInfo) {
if (baseImage === undefined || revisionImage === undefined) {
return;
}
this.reload();
}
protected setImageDiffSrc(src: string) {
this.diffImageSrc = src;
}
protected setDifferenceValue(percentage: number) {
this.difference = percentage;
}
protected getDataUrl(image: ImageInfo) {
return 'data:' + image.type + ';base64,' + image.body;
}
protected maybeIgnoreColors(diffProcess: any, ignoreColors: boolean) {
ignoreColors ? diffProcess.ignoreColors() : diffProcess.ignoreNothing();
return diffProcess;
}
protected createDiffProcess(base: string, rev: string, ignoreColors: boolean) {
window.resemble.outputSettings(this.setOutputSetting());
const process = window.resemble(base).compareTo(rev);
return this.maybeIgnoreColors(process, ignoreColors);
}
protected setOutputSetting() {
const rgb = this.hexToRGB(this.colorValue);
return {
transparency: this.transparent ? 0.1 : 1,
errorColor: {
red: rgb?.r ?? 0,
green: rgb?.g ?? 255,
blue: rgb?.b ?? 255,
},
};
}
/**
* Reloads the diff. Resemble 1.2.1 seems to have an issue with successive
* reloads via the repaint() function, so this implementation creates a
* fresh diff each time it is called.
*
* @return resolves if and when the reload succeeds.
*/
reload(): Promise<void> | void {
this.loading = true;
if (this.baseImage && this.revisionImage) {
const base = this.getDataUrl(this.baseImage);
const rev = this.getDataUrl(this.revisionImage);
return new Promise((resolve) => {
this.createDiffProcess(base, rev, this.ignoreColors).onComplete(
(data: any) => {
this.setImageDiffSrc(data.getImageDataUrl());
this.setDifferenceValue(data.misMatchPercentage);
this.loading = false;
resolve();
}
);
});
}
this.loading = false;
}
protected handleIgnoreColorsToggle(e: Event) {
this.ignoreColors = (e.target as HTMLInputElement).checked;
this.reload();
}
protected handleTransparentToggle(e: Event) {
this.transparent = (e.target as HTMLInputElement).checked;
this.reload();
}
protected handleColorChange(e: Event) {
this.colorValue = (e.target as HTMLInputElement).value;
this.reload();
}
protected hexToRGB(hex: string) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
protected handleFullScreen() {
const w = window.open('about:blank', '_blank');
const imageDiff = this.shadowRoot?.getElementById('imageDiff');
if (imageDiff && w) {
w.document.body.appendChild(imageDiff.cloneNode(true));
}
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-resemble-diff-mode': ResembleDiffMode;
}
interface Window {
resemble: any;
}
}