I am trying to merge two canvases, each of which contains an identical PDF document except for a signature pad. My goal is to combine both canvases to merge the signature pads into one document. However, when I combine the canvases, I notice that the resulting document loses quality and the background becomes darker.
Here is the code I am using to merge the canvases:
async function combinePages(numPages) {
console.log("Combining ..."+numPages);
let canvas1, canvas2; // Declare canvas1 and canvas2 variables here
// Iterate through each page
for (let i = 1; i <= numPages; i++) {
let element = document.getElementById(`canvasContainer3_${i}`);
if (!element) {
// Create a new canvas container element
let container = document.createElement("div");
container.id = `canvasContainer3_${i}`;
// Append the container to the document body
document.body.appendChild(container);
}
// Get the canvas elements for the current page
canvas1 = document.getElementById('canvas1_'+i);
canvas2 = document.getElementById('canvas2_'+i);
// Create a new canvas element for the combined page
let combinedCanvas = "";
combinedCanvas = document.createElement("canvas");
combinedCanvas.setAttribute('id', 'canvas3_'+i);
let canvas1Width = canvas1.width;
let canvas1Height = canvas1.height;
let canvas2Width = canvas2.width;
let canvas2Height = canvas2.height;
// Set the size of the combined canvas
combinedCanvas.width = Math.max(canvas1Width, canvas2Width);
combinedCanvas.height = Math.max(canvas1Height, canvas2Height);
let context = combinedCanvas.getContext("2d");
context.drawImage(canvas1, 0, 0);
// Draw the contents of the second canvas onto the combined canvas
context.globalCompositeOperation = 'multiply';
context.drawImage(canvas2, 0, 0);
// Add the combined canvas to the page
let container = document.getElementById(`canvasContainer3_${i}`);
container.appendChild(combinedCanvas);
document.getElementById("wrapper").appendChild(container);
// Hide canvas1 and canvas2
//canvas1.style.display = "none";
// canvas2.style.display = "none";
}
return 'finish combination';
}
Here the example of the PDF's that I'm using:
PDF 1 is merged with PDF 2, then the result is merged with PDF 3, and then that result is merged with PDF 4.
Here the result I'm getting:
White background is grey and color are dark.
As you realized yourself, the CanvasRenderingContext2D
composite operations won't bring you too far. For your particular use-case though there's a nifty workaround - given that the difference in-between two images does not overlap.
The trick here is instead of directly trying to merge the content of e.g. canvas A:
with canvas B:
you first create an offscreen canvas without any signature written onto and keep it's pixeldata obtained using the .getImageData()
function.
Now if we want to merge the aforementioned canvases A + B, we need to go over each image's pixeldata and compare it with every pixel from the 'empty' image. If it's different pick it, if it's the same keep it. The result of this comparisons has to be stored inside an Uint8ClampedArray
sized the width * height of the canvas and multiplied by four because we need to store a red, green, blue and an alpha value for each pixel.
This array can then be used to create a new ImageData
object which we can ultimately draw onto another canvas as the result of our merging efforts.
I've made a quick interactive example which illustrates the process. Just use the dropdown boxes (and the 'merge' button) to successively merge:
PDF #1 with PDF #2
Result with PDF #3
Result with PDF #4
let emptyPDF, emptyPDFImageData;
(async() => {
let pdfs = ["https://i.sstatic.net/bvfme.png", "https://i.sstatic.net/x9Cam.png", "https://i.sstatic.net/M6V2p.png", "https://i.sstatic.net/YWxJF.png"];
for (let a = 0; a < pdfs.length; a++) {
document.getElementById("pdf" + (a + 1)).getContext("2d").drawImage(await loadImage(pdfs[a]), 0, 0)
}
emptyPDF = document.createElement("canvas");
emptyPDF.width = 590
emptyPDF.height = 180
emptyPDF.getContext("2d").drawImage(await loadImage("https://i.sstatic.net/QUT3B.png"), 0, 0);
emptyPDFImageData = emptyPDF.getContext("2d").getImageData(0, 0, emptyPDF.width, emptyPDF.height).data;
})();
function loadImage(src) {
return new Promise((resolve, reject) => {
let img = new Image();
img.crossOrigin = ""
img.onload = () => resolve(img);
img.src = "https://api.codetabs.com/v1/proxy/?quest=" + src;
});
}
function merge() {
let canvas = document.getElementById(document.getElementById("sourceA").value);
let imageDataA = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data;
canvas = document.getElementById(document.getElementById("sourceB").value);
let imageDataB = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data;
let colors = new Uint8ClampedArray(new Array(emptyPDF.width * emptyPDF.height * 4).fill(255));
let colA, colB, colC;
for (let a = 0; a < colors.length; a += 4) {
colA = (emptyPDFImageData[a] << 16 | emptyPDFImageData[a + 1] << 8 | emptyPDFImageData[a + 2]);
colB = (imageDataA[a] << 16 | imageDataA[a + 1] << 8 | imageDataA[a + 2]);
colC = (imageDataB[a] << 16 | imageDataB[a + 1] << 8 | imageDataB[a + 2]);
if (colA != colB) {
colors[a] = imageDataA[a];
colors[a + 1] = imageDataA[a + 1];
colors[a + 2] = imageDataA[a + 2];
} else if (colC != colA) {
colors[a] = imageDataB[a];
colors[a + 1] = imageDataB[a + 1];
colors[a + 2] = imageDataB[a + 2];
} else {
colors[a] = emptyPDFImageData[a];
colors[a + 1] = emptyPDFImageData[a + 1];
colors[a + 2] = emptyPDFImageData[a + 2];
}
}
let imageData = new ImageData(colors, emptyPDF.width, emptyPDF.height);
document.getElementById("result").getContext("2d").putImageData(imageData, 0, 0);
}
.container {
display: flex;
align-items: center;
}
canvas {
width: 295px;
}
<div>
<div class="container"><canvas id="pdf1" width="590" height="180"></canvas><span>PDF #1</span></div>
<hr>
<div class="container"><canvas id="pdf2" width="590" height="180"></canvas><span>PDF #2</span></div>
<hr>
<div class="container"><canvas id="pdf3" width="590" height="180"></canvas><span>PDF #3</span></div>
<hr>
<div class="container"><canvas id="pdf4" width="590" height="180"></canvas><span>PDF #4</span></div>
<hr>
<div class="container"><canvas id="result" width="590" height="180"></canvas><span>Result</span></div>
</div>
<label>Source A:</label>
<select id="sourceA">
<option value="pdf1">PDF #1</option>
<option value="pdf2">PDF #2</option>
<option value="pdf3">PDF #3</option>
<option value="pdf4">PDF #4</option>
<option value="result">Result</option>
</select>
<span>+</span>
<label>Source B:</label>
<select id="sourceB">
<option value="pdf1">PDF #1</option>
<option value="pdf2">PDF #2</option>
<option value="pdf3">PDF #3</option>
<option value="pdf4">PDF #4</option>
<option value="result">Result</option>
</select>
<button id="mergeButton" onclick="merge()">Merge</button>