Search code examples
javascriptgoogle-chromegoogle-chrome-extension

snipping tool in Chrome Extension not working properly


I have a Chrome extension, that has a snip tool I've been working on. Its almost working as I'd like, the issue is it seems that as soon as I load a webpage it executes the whole process, where I'm intending to have the user click a button and then it starts the process.

So, I've based on this on the CaptureVisibleTab API and I have to communicate between some scripts to do this.

To start my manifest.json (relevant info)

"manifest_version": 3,
permissions": [
      "activeTab",
      "tabs",
      "contextMenus",
      "scripting",
      "storage",
      "identity"
    ],
"background": {
      "service_worker": "scripts/background.js"
},

"content_scripts": [{
      "matches": ["<all_urls>"],
      "js": ["scripts/content.js"],
      "all_frames": false,
      "run_at": "document_end"

I have a button on the popup.HTML that has a id called "snip", pretty self explanitory

then in the popup.js there's a variable for the snip and a event listener to then que the screenshot

var snip = document.getElementById("snip");

snip.addEventListener("click", function() {
  chrome.runtime.sendMessage({ action: "captureVisibleTab" }, function(response) {
    console.log(response.imageUrl);
  });
});

Now in background.js it has a listener for the action and just returns the image result to then be cropped, and sent to the content script

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  if (request.action === "captureVisibleTab") {
    chrome.tabs.captureVisibleTab(null, { format: "png" }, function(imageUrl) {
      sendResponse({ imageUrl: imageUrl });
    });
    return true; // Required to indicate that the response will be sent asynchronously
  }
});

finally in the content.js, I just have the function and the message for the captureVisibleTab where when it gets it, it takes the image of the webapge and passes it as a argument in the function

function cropScreenshot(imageUrl) {
    var canvas = document.createElement("canvas");
    var ctx = canvas.getContext("2d");
    var isMouseDown = false;
    var startX, startY, endX, endY;
  
    var img = new Image();
    img.onload = function() {
      canvas.width = img.width;
      canvas.height = img.height;
      ctx.drawImage(img, 0, 0);
      canvas.style.display = "block";
      var overlay = document.createElement("div");
      overlay.style.position = "absolute";
      overlay.style.top = "0";
      overlay.style.left = "0";
      overlay.style.width = canvas.width + "px";
      overlay.style.height = canvas.height + "px";
      overlay.style.background = "rgba(0,0,0,0.4)";
      document.body.appendChild(overlay);
  
      overlay.addEventListener("mousedown", function(e) {
        isMouseDown = true;
        startX = e.offsetX;
        startY = e.offsetY;
      });
  
      overlay.addEventListener("mouseup", function(e) {
        isMouseDown = false;
        endX = e.offsetX;
        endY = e.offsetY;
        var width = endX - startX;
        var height = endY - startY;
        if (width > 0 && height > 0) {
          var croppedCanvas = document.createElement("canvas");
          var croppedCtx = croppedCanvas.getContext("2d");
          croppedCanvas.width = width;
          croppedCanvas.height = height;
          croppedCtx.drawImage(canvas, startX, startY, width, height, 0, 0, width, height);
          var downloadLink = document.createElement("a");
          downloadLink.href = croppedCanvas.toDataURL("image/png");
          downloadLink.download = "snipped-img.png";
  
          // Attach the anchor to the document and click it
          document.body.appendChild(downloadLink);
          downloadLink.click();
  
          // Remove the anchor from the document
          document.body.removeChild(downloadLink);
        }
        canvas.style.display = "none";
        overlay.remove();
      });
  
      overlay.addEventListener("mousemove", function(e) {
        if (isMouseDown) {
          var width = e.offsetX - startX;
          var height = e.offsetY - startY;
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          ctx.drawImage(img, 0, 0);
          ctx.fillStyle = "rgba(255,255,255,0.5)";
          ctx.fillRect(startX, startY, width, height);
        }
      });
    };
    img.src = imageUrl;
  }

chrome.runtime.sendMessage({ action: "captureVisibleTab" }, function(response) {
    if (response && response.imageUrl) {
      // Call the cropScreenshot function and pass the imageUrl as a parameter
      var imageUrl = response.imageUrl
      cropScreenshot(imageUrl);
    } else {
      console.log("Error capturing visible tab");
    }
  });

that is the gist of it, I have tried putting a action between popup and background, like startSnip, but that doesn't work. This as it sits is currently the closest I've come to getting it right. So I'm sure its something obvious now, and I just can't see it. any help is greatly appreciated


Solution

  • Simplified Messaging

    As others have already pointed out, the main problem is with your content script. The captureVisibleTab method is called immediately on page load and not when a button is clicked. You can fix this and also simplify your code as shown.

    The code below has been tested and works

    background.js

    You can remove the popup completely and instead use the onClicked event. This way users only need to click on the extension icon to capture the current tab. This can be done by clearing the default popup handler and adding your own handler as shown:

    // Allows onClicked to fire when popup was set in manifest
    chrome.action.setPopup({ popup: "" });
    
    chrome.action.onClicked.addListener((tab) => {
      chrome.tabs
        .captureVisibleTab(tab.windowId, { format: "png" })
        .then((dataUrl) => {
          chrome.tabs.sendMessage(tab.id, {
            action: "captureVisibleTab",
            tabId: tab.id,
            length: dataUrl.length,
            dataUrl: dataUrl,
          });
        });
    });
    

    content.js

    And to the content script add a listener to process the captured image:

    chrome.runtime.onMessage.addListener((message) => {
      if (message.action === "captureVisibleTab") {
        // do something...
        // cropScreenshot(message.dataUrl);
      }
    });
    

    manifest.json

    If the background script tries to send a message before the content script has loaded it will fail. There is no handler to receive the message. To avoid this problem you can modify run_at in the manifest to load the content script early. This way your capture button will still work even while the page is still loading ads, etc.

    "content_scripts": [
        {
          "run_at": "document_start",
          "js": ["scripts/content.js"],
          ... etc
    

    Image Cropping

    There are a number of good image cropping libraries available, like ImageCropper, Croppr, and Cropper. Each has different features and complexity. You may want to consider one of these in place of your custom code.