New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

pixel-buffer-diff

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pixel-buffer-diff - npm Package Compare versions

Comparing version
1.3.0
to
1.3.1
+2
-2
dist/index.d.ts

@@ -11,4 +11,4 @@ export declare type Result = {

};
export declare const diffImageDatas: (baseline: ImageData, candidate: ImageData, diff: ImageData, options?: Options) => Result;
export declare const diff: (baseline8: Uint8Array | Uint8ClampedArray, candidate8: Uint8Array | Uint8ClampedArray, diff8: Uint8Array | Uint8ClampedArray, width: number, height: number, options?: Options) => Result;
export declare const diffImageDatas: (baseline: ImageData, candidate: ImageData, diff: ImageData, options: Options) => Result;
export declare const diff: (baseline8: Uint8Array | Uint8ClampedArray, candidate8: Uint8Array | Uint8ClampedArray, diff8: Uint8Array | Uint8ClampedArray, width: number, height: number, options: Options) => Result;
//# sourceMappingURL=index.d.ts.map

@@ -1,1 +0,1 @@

Object.defineProperty(exports,"__esModule",{value:!0}),exports.diff=exports.diffImageDatas=void 0;const t={threshold:.03,cumulatedThreshold:.5,enableMinimap:!1};exports.diffImageDatas=(e,a,i,n=t)=>{if(void 0===n.threshold||void 0===n.cumulatedThreshold||void 0===n.enableMinimap)throw new Error("Invalid options");const{width:h,height:r}=e,o=h*r,d=e.data,f=a.data,l=i.data,s=d.length,c=f.length,m=l.length;if(h!==a.width||r!==a.height||4*o!==s||s!==c)throw new Error("Different baseline and candidate ImageData dimensions");const w=i.width/h,u=m/s;if(i.height!==r||w!==u||1!==w&&3!==w)throw new Error("Invalid diff ImageData dimensions");const g=m===s?1:2,M=d.buffer,p=f.buffer,b=l.buffer,x=new Uint32Array(M,0,s>>2),y=new Uint32Array(p,0,s>>2),D=new Uint32Array(b,0,m>>2),v=n.threshold*n.threshold*35215;let A=0,I=0,U=0,E=0,T=0,_=0,j=0,q=0;const C=Math.ceil(Math.sqrt(o)/128),O=s/C&-4;for(let t=0;t<C;t++)q+=(.299*(d[A]+f[A])+.587*(d[A+1]+f[A+1])+.114*(d[A+2]+f[A+2]))/C/2,A+=O;const P=q<128,k=P?1057016832:1056964863,z=P?1056964863:1057016832;A=0;const B=Math.ceil(r/256),F=Math.ceil(h/256),G=new Uint8ClampedArray(F*B),H=2===g?h:0,J=2*H+h,K=Math.max(h,r),L=Math.max(F,B),N=new Uint32Array(K);let Q=0;for(let t=0;t<L;t++)N.fill(t,Q,Math.min(Q+256,K)),Q+=256;for(let t=0;t<r;t++){const e=N[t]*F;H>0&&(D.set(new Uint32Array(M,A,h),U),U+=h,D.set(new Uint32Array(p,A,h),U+h));let a=4034073399*(4034073399^t);for(let t=0;t<h;t++,U++,I++,A+=4,a++){if(x[I]===y[I])continue;const i=f[A]-d[A],n=f[A+1]-d[A+1],h=f[A+2]-d[A+2],r=.29889531*i+.58662247*n+.11448223*h,o=.59597799*i-.2741761*n-.32180189*h,l=.21147017*i-.52261711*n+.31114694*h;if(r*r*.5053+o*o*.299+l*l*.1957>v){G[e+N[t]]++,E++;const i=Math.abs(r);j+=i,D[U]=(r>0?k:z)+(Math.min(192,8*i)<<24),0===T&&(_=a),T+=a}}U+=H}if(T-=_,j/=256,n.enableMinimap)for(let t=0;t<F*B;t++){if(G[t]>0){const e=t/F|0,a=256*(t%F),i=Math.min(a+256,h),n=256*e,o=Math.min(n+256,r);U=a+n*J+H;const d=J-i+a;for(let t=n;t<o;t++){for(let t=a;t<i;t++)D[U++]|=1082064896;U+=d}}}return j>n.cumulatedThreshold?{diff:E,cumulatedDiff:j,hash:T}:{diff:0,cumulatedDiff:0,hash:0}};exports.diff=(e,a,i,n,h,r=t)=>(0,exports.diffImageDatas)({width:n,height:h,data:e},{width:n,height:h,data:a},{width:n*i.length/e.length,height:h,data:i},r);
Object.defineProperty(exports,"__esModule",{value:!0}),exports.diff=exports.diffImageDatas=void 0;exports.diffImageDatas=(t,e,a,i)=>{void 0===i.threshold&&(i.threshold=.3),void 0===i.cumulatedThreshold&&(i.cumulatedThreshold=.5),void 0===i.enableMinimap&&(i.enableMinimap=!1);const{width:n,height:h}=t,r=n*h,d=t.data,o=e.data,f=a.data,l=d.length,s=o.length,c=f.length;if(n!==e.width||h!==e.height||4*r!==l||l!==s)throw new Error("Different baseline and candidate ImageData dimensions");const m=a.width/n,u=c/l;if(a.height!==h||m!==u||1!==m&&3!==m)throw new Error("Invalid diff ImageData dimensions");const w=c===l?1:2,g=d.buffer,M=o.buffer,p=f.buffer,b=new Uint32Array(g,0,l>>2),x=new Uint32Array(M,0,l>>2),y=new Uint32Array(p,0,c>>2),D=i.threshold*i.threshold*35215;let A=0,U=0,v=0,I=0,T=0,E=0,_=0,j=0;const q=Math.ceil(Math.sqrt(r)/128),C=l/q&-4;for(let t=0;t<q;t++)j+=(.299*(d[A]+o[A])+.587*(d[A+1]+o[A+1])+.114*(d[A+2]+o[A+2]))/q/2,A+=C;const O=j<128,P=O?1057016832:1056964863,k=O?1056964863:1057016832;A=0;const z=Math.ceil(h/256),B=Math.ceil(n/256),F=new Uint8ClampedArray(B*z),G=2===w?n:0,H=2*G+n,J=Math.max(n,h),K=Math.max(B,z),L=new Uint32Array(J);let N=0;for(let t=0;t<K;t++)L.fill(t,N,Math.min(N+256,J)),N+=256;for(let t=0;t<h;t++){const e=L[t]*B;G>0&&(y.set(new Uint32Array(g,A,n),v),v+=n,y.set(new Uint32Array(M,A,n),v+n));let a=4034073399*(4034073399^t);for(let t=0;t<n;t++,v++,U++,A+=4,a++){if(b[U]===x[U])continue;const i=o[A]-d[A],n=o[A+1]-d[A+1],h=o[A+2]-d[A+2],r=.29889531*i+.58662247*n+.11448223*h,f=.59597799*i-.2741761*n-.32180189*h,l=.21147017*i-.52261711*n+.31114694*h;if(r*r*.5053+f*f*.299+l*l*.1957>D){F[e+L[t]]++,I++;const i=Math.abs(r);_+=i,y[v]=(r>0?P:k)+(Math.min(192,8*i)<<24),0===T&&(E=a),T+=a}}v+=G}if(T-=E,_/=256,i.enableMinimap)for(let t=0;t<B*z;t++){if(F[t]>0){const e=t/B|0,a=256*(t%B),i=Math.min(a+256,n),r=256*e,d=Math.min(r+256,h);v=a+r*H+G;const o=H-i+a;for(let t=r;t<d;t++){for(let t=a;t<i;t++)y[v++]|=1082064896;v+=o}}}return _>i.cumulatedThreshold?{diff:I,cumulatedDiff:_,hash:T}:{diff:0,cumulatedDiff:0,hash:0}};exports.diff=(t,e,a,i,n,h)=>(0,exports.diffImageDatas)({width:i,height:n,data:t},{width:i,height:n,data:e},{width:i*a.length/t.length,height:n,data:a},h);

@@ -23,3 +23,3 @@ {

],
"version": "1.3.0",
"version": "1.3.1",
"scripts": {

@@ -26,0 +26,0 @@ "build": "tsc && terser ./dist/index.js -c -m --module -o ./dist/index.js"

+16
-19

@@ -81,20 +81,2 @@ # **Pixel-buffer-diff** aka **Pbd**

### The `Result` type
The `Result` type defines the properties resulting from diffing two pixel buffers.
```typescript
type Result = {
diff: number;
cumulatedDiff: number;
hash: number
};
```
* `diff` a number showing the number of pixels that exceeded the `threshold`
* `hash` a numeric hash representing the pixel change between the two images. This hash allows to de-duplicate changes across multiple images to only show unique changes in your visual regression report and approval workflow.
* `cumulatedDiff` a number representing the cumulated difference of every pixel change in the two images. This can used to discard changes that only effect subtle differences like anti-aliasing pixels.
These properties are all set to `0` if the two images are within the cumulatedThreshold.
### The `diff` method

@@ -138,7 +120,22 @@

The `diff` and `diffImageDatas` methods mutate the diff pixel buffer they receive as argument and return an object with the following properties:
The `diff` and `diffImageDatas` methods mutate the diff pixel buffer they receive as argument and return a `Result` object.
### The `Result` type
The `Result` type defines the properties resulting from diffing two pixel buffers.
```typescript
type Result = {
diff: number;
cumulatedDiff: number;
hash: number
};
```
* `diff` a number showing the number of pixels that exceeded the `threshold`
* `hash` a numeric hash representing the pixel change between the two images. This hash allows to de-duplicate changes across multiple images to only show unique changes in your visual regression report and approval workflow.
* `cumulatedDiff` a number representing the cumulated difference of every pixel change in the two images. This can used to discard changes that only effect subtle differences like anti-aliasing pixels.
These properties are all set to `0` if the two images are within the cumulatedThreshold.
## Example usage

@@ -145,0 +142,0 @@

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Noise 🕵️‍♀️ Visual Regression Report</title>
<style>
html, body {
margin: 0;
background-color: #fff;
color: #000;
font-family: sans-serif;
overscroll-behavior: none;
}
* {
box-sizing: border-box;
}
body {
display: flex;
width: 100vw;
height: 100vh;
user-select: none;
}
main {
padding: 0;
height: 100vh;
min-width: 50%;
display: flex;
flex-direction: column;
box-shadow: 0 0 1rem #0008;
z-index: 1;
border: 1rem solid transparent;
transition: border-color .5s;
position: relative;
overflow: hidden;
}
main:before {
content: "🕵️‍♀️";
position:absolute;
right: 0;
bottom: 0;
transform: translate(.4ex, .4ex) rotate(-.1rad);
font-size: 33vmin;
opacity: .1;
}
main.approved {
border-color: #0c3;
}
#filter {
padding: 1rem;
background: #ddd;
border: 0;
}
main > * {
margin: .5rem 1rem;
white-space: pre-line;
}
code {
background: #000;
color: #fff;
padding: .5ex;
}
#instructions {
user-select: none;
opacity: .75;
}
nav {
display: flex;
flex-grow: 1;
align-items: center;
min-height: 4rem;
font: 1.5rem monospace;
}
nav > * {
text-align: center;
padding: 1rem;
cursor: pointer;
}
nav > *.disabled {
opacity: .25;
}
#summary {
flex-grow: 1;
}
#summary::first-letter {
font-size: 4em;
}
#matches {
flex-grow: 1;
flex-shrink: 1;
height: 100%;
padding: 1ex;
align-self: baseline;
word-break: break-all;
font-family: monospace;
overflow: auto;
user-select: text;
}
#view {
background: radial-gradient(circle, #fff4, #0004);
min-width: 50%;
flex-grow: 1;
height: 100vh;
}
#progress {
height: 1rem;
image-rendering: pixelated;
}
</style>
</head>
<body>
<main><h1><code id="overview"></code> visual regressions in <!--between <span id="drop-baseline" class="drop-name">baseline</span> and--><code id="drop-candidate" class="drop-name">user/nelson/sbc5</code></h1>
<!-- <h2 id="overview">? screenshots ➜ ? uniques changes in total</h2>-->
<canvas id="progress"></canvas>
<input id="filter" type="text" placeholder="filter the screenshots"/>
<nav><span id="nav_prev" disabled>🡸</span><tt id="summary" tabindex="0"></tt><span id="nav_next">🡺</span></nav>
<div id="matches"></div>
<div id="instructions">Use the <b>INPUT</b> above to filter the images
<b>🡸</b> and <b>🡺</b> to navigate between matching images
<b>🡹</b> and <b>🡻</b> to change approval rating with
<b>SPACE</b> to change view mode
<b>Click and drag</b> to pan the image use the <b>mouse wheel</b> to zoom in/out
<b>Double click</b> the image to reset the zoom level</div>
</main>
<canvas id="view"></canvas>
</body>
<script>
const xhr = new XMLHttpRequest();
xhr.open("GET", "report.json");
xhr.onload = (e) => {
const report = JSON.parse(e.target.responseText);
const changed = report.changed.sort((a, b)=>(b.hash-a.hash) || (b.diff-a.diff)); //.filter(e=>e.cummulatedDelta>.5);
const progress = document.querySelector("canvas#progress");
const progressContext = progress.getContext("2d");
progress.width = changed.length;
progress.height = 1;
progressContext.imageSmoothingEnabled = false;
const view = document.querySelector("canvas#view");
const viewContext = view.getContext("2d");
const modes = ["👥opacity", "🏄‍♀️swipe"];
const colors = {"rejected": "#f00", "approved": "#0d3"};
const viewState = {
index: 0,
mode: modes[0],
totalChanged: changed.length,
totalUniques: 7, //uniquesTotal,
img: undefined,
zx: 64,
zy: 64,
rzx: 0,
rzy: 0,
x01: .5,
y01: .5,
zoomLevel: 1,
zoomLevelMin: .5,
};
// Create checkboard pattern to show behind the diff
view.width = 32;
view.height = 32;
viewContext.fillStyle = "#707070"
viewContext.fillRect(0,0,16,16);
viewContext.fillRect(16,16,16,16);
viewContext.fillStyle = "#909090"
viewContext.fillRect(16,0,16,16);
viewContext.fillRect(0,16,16,16);
const checkboardPattern = viewContext.createPattern(view, "repeat");
const updateView = () => {
viewState.prevRzx = viewState.rzx;
viewState.prevRzy = viewState.rzy;
requestAnimationFrame(updateView);
if (!viewState.img || !viewState.changed || !viewState.img.complete) {
return;
}
viewState.changed = false;
const cbb = view.getBoundingClientRect();
view.width = cbb.width;
view.height = cbb.height;
const { img, naturalWidth, naturalHeight } = viewState;
if (naturalWidth * naturalHeight === 0) {
console.log({update:"failed-image-is-zero", viewState});
return;
}
const imgWidth = naturalWidth / 3;
const imgHeight = naturalHeight;
const zl = viewState.zoomLevel;
const x = Math.max(0, Math.min( 1, .5 + (viewState.x01-.5) * 2))
const xSmooth = x * x * (3 - 2 * x);
const y = Math.max(0, Math.min( 1, .5 + (viewState.y01-.5) * 2));
const ySmooth = (y * y * (3 - 2 * y)) ** 16;
viewContext.fillStyle = "#808080";
viewContext.fillRect(0,0,view.width,view.height);
viewContext.save();
viewContext.scale(zl, zl);
viewContext.imageSmoothingEnabled = zl<1;
viewContext.translate(viewState.zx, viewState.zy);
viewContext.fillStyle = "#808080";
viewContext.fillRect(0,0,imgWidth,imgHeight);
viewContext.fillStyle = checkboardPattern;
viewContext.fillRect(0,0,imgWidth,imgHeight);
viewContext.fillStyle = "#808080";
viewContext.clearRect(0,0,imgWidth,imgHeight);
viewContext.fillStyle = "#000";
if (viewState.mode === modes[0]) {
// Baseline
viewContext.globalAlpha = (1 - ySmooth);
viewContext.drawImage(img,0,0,imgWidth,imgHeight ,0,0,imgWidth,imgHeight);
// Candidate
viewContext.globalAlpha = xSmooth * ( 1- ySmooth);
viewContext.drawImage(img,imgWidth*2,0,imgWidth,imgHeight ,0,0,imgWidth,imgHeight);
} else if (viewState.mode === modes[1] ) {
// Baseline
viewContext.globalAlpha = ( 1- ySmooth);
const x01 = Math.max(0, Math.min(1, viewState.rzx / imgWidth));
viewContext.drawImage(img,0,0,x01*imgWidth,imgHeight ,0,0,x01*imgWidth,imgHeight);
viewContext.drawImage(img,imgWidth*(2+x01),0,imgWidth*(1-x01),imgHeight ,imgWidth*x01,0,imgWidth*(1-x01),imgHeight);
viewContext.fillRect(imgWidth * x01,0, .5 / zl, imgHeight);
}
// Diff
viewContext.globalAlpha = ySmooth;
viewContext.drawImage(img,imgWidth,0,imgWidth,imgHeight ,0,0,imgWidth,imgHeight);
// Text on the edges
viewContext.restore();
viewContext.fillStyle = colors[viewState.match.rating];
viewContext.globalAlpha = .5;
const fontSize = Math.min(view.width, view.height)/48;
viewContext.fillRect(0,0,view.width,fontSize);
viewContext.fillRect(0,view.height-fontSize,view.width,fontSize);
viewContext.fillRect(0,fontSize,fontSize,view.height-fontSize*2);
viewContext.fillRect(view.width-fontSize,fontSize,fontSize,view.height-fontSize*2);
viewContext.globalAlpha = 1;
viewContext.font = `900 ${fontSize}px monospace`;
//viewContext.globalCompositeOperation = "difference";
viewContext.fillStyle = "#fff";
viewContext.textAlign = "center";
viewContext.textBaseline = "middle";
viewContext.fillText("DIFFERENCE", view.width / 2, view.height - fontSize/2);
viewContext.save();
viewContext.translate(fontSize/2, view.height / 2);
viewContext.rotate(-Math.PI/2);
viewContext.fillText("BASELINE", 0, 0);
viewContext.restore();
viewContext.save();
viewContext.translate(view.width - fontSize/2, view.height / 2);
viewContext.rotate(-Math.PI/2);
viewContext.fillText("CANDIDATE", 0, 0);
viewContext.restore();
};
view.onwheel = e => {
const x = e.offsetX;
const y = e.offsetY;
const rzxBefore = x / viewState.zoomLevel - viewState.zx;
const rzyBefore = y / viewState.zoomLevel - viewState.zy;
viewState.zoomLevel = Math.min(16, Math.max(viewState.zoomLevelMin, viewState.zoomLevel - e.deltaY / 32));
viewState.rzx = x / viewState.zoomLevel - viewState.zx;
viewState.rzy = y / viewState.zoomLevel - viewState.zy;
viewState.zx += viewState.rzx - rzxBefore;
viewState.zy += viewState.rzy - rzyBefore;
viewState.changed = true;
}
view.onmousedown = e => {
viewState.mouseDown = true;
}
view.onmouseup = view.onmouseleave = e => {
viewState.mouseDown = false;
}
view.onmousemove = e => {
const x = e.offsetX;
const y = e.offsetY;
if (viewState.mouseDown || e.ctrlKey) {
viewState.rzx = x / viewState.zoomLevel - viewState.zx;
viewState.rzy = y / viewState.zoomLevel - viewState.zy;
viewState.zx += viewState.rzx - viewState.prevRzx;
viewState.zy += viewState.rzy - viewState.prevRzy;
} else {
viewState.x01 = x / view.width;
viewState.y01 = y / view.height;
}
viewState.rzx = x / viewState.zoomLevel - viewState.zx;
viewState.rzy = y / viewState.zoomLevel - viewState.zy;
viewState.changed = true;
}
const resetSize = e => {
if (e && e.type == "resize") {
const cbb = view.getBoundingClientRect();
view.width = cbb.width;
view.height = cbb.height;
}
viewState.naturalWidth = viewState.img.naturalWidth;
viewState.naturalHeight = viewState.img.naturalHeight;
const imgWidth = viewState.naturalWidth / 3;
const imgHeight = viewState.naturalHeight;
const vw = view.width - 32 * 2;
const vh = view.height - 32 * 2;
viewState.zoomLevelMin = Math.min(vw / imgWidth, vh / imgHeight);
viewState.zoomLevel = viewState.zoomLevelMin;
viewState.zx = (view.width - imgWidth * viewState.zoomLevel) / 2;
viewState.zy = (view.height - imgHeight * viewState.zoomLevel) / 2;
viewState.changed = true;
}
onresize = resetSize;
view.ondblclick = resetSize;
viewState.img = new Image();
viewState.img.onload = resetSize;
// options: InputEvent | { direction?: number }
const applyFilter = (options = {direction: 0}) => {
const value = filter.value.toLowerCase();
let total = 0;
let unique = 0;
let prevHash = NaN;
let prevMatchHash = NaN;
let uniqueMatch = 0;
let rating = undefined;
const matchesList = [];
// ?
for (let i=0; i<changed.length; i++) {
const item = changed[i];
item.matches = item.path.toLowerCase().includes(value);
if (item.matches) {
item.isDuplicate = item.hash == prevHash;
prevHash = item.hash;
if (!item.isDuplicate) {
rating = item.rating || "rejected";
matchesList.push([]);
unique++;
}
item.rating = rating;
matchesList[matchesList.length - 1].push(item);
total++;
}
}
const direction = options?.direction || 0;
viewState.index = Math.max(0, Math.min(matchesList.length - 1, viewState.index + direction));
const matchList = matchesList[viewState.index];
overview.textContent = viewState.totalUniques;
const match = matchList && matchList[0];
if (match) {
nav_prev.classList.toggle("disabled", viewState.index === 0);
nav_next.classList.toggle("disabled", viewState.index === unique - 1);
summary.textContent = `${viewState.mode} view match #${viewState.index + 1} of ${unique}\nΔ ${match.diff} partial and ${match.cummulatedDelta} full pixels`;
matches.textContent = `${matchList.length} copies\n`+ matchList.map((str, index) => `${index!=1?"\u00a0":"•"} ${str.path}`).join("\n");
requestAnimationFrame(() => {
if (viewState.img.getAttribute("src") !== match.path) {
viewState.img.src = match.path;
}
});
} else {
nav_prev.classList.add("disabled");
nav_next.classList.add("disabled");
summary.textContent = `No matches`;
matches.textContent = `N/A`;
}
// Update progress bar
progress.height = 1;
const matchHash = match?.hash;
let zebra = false;
prevHash = NaN;
let allApproved = true;
for (let i=0;i<changed.length;i++) {
const item = changed[i];
progressContext.fillStyle = colors[item.rating];
if (allApproved && item.rating === "rejected") {
allApproved = false;
}
if (item.hash !== prevHash) {
prevHash = item.hash;
zebra = !zebra;
}
const value = zebra ? 1 : .8;
progressContext.globalAlpha = item.hash === matchHash ? 1 : value * (item.matches ? .5 : .1);
progressContext.fillRect(i,0,1,1);
}
document.querySelector("main").classList.toggle("approved", allApproved);
viewState.match = match;
viewState.changed = true;
}
const updateRating = rating => {
const hash = viewState.match.hash;
for (let i=0; i<changed.length; i++) {
if (changed[i].hash === hash) {
changed[i].rating = rating;
}
}
applyFilter();
}
oninput = applyFilter;
onkeyup = e => {
if (document.activeElement === filter) {
return;
}
const code = e.code;
if (code === "ArrowLeft") {
applyFilter({direction: e.shiftKey ? -10 : -1});
} else if (code === "ArrowRight") {
applyFilter({direction: e.shiftKey ? 10 : 1});
} else if (code === "ArrowUp") {
updateRating("approved");
} else if (code === "ArrowDown") {
updateRating("rejected");
} else if (code === "Space") {
const index = modes.indexOf(viewState.mode) || 0;
viewState.mode = modes[(index + 1) % modes.length];
applyFilter();
}
};
updateView();
applyFilter({});
};
xhr.send();
</script>
</html>