javascriptmergehtml5-canvaspdf-lib.js

How can I prevent loss of quality and darkening when merging two canvases that load PDFs in JavaScript?


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: Example images

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:

Result after merge process

White background is grey and color are dark.


Solution

  • 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>