Build a Full Page Screenshot Chrome Extension: Capture Entire Webpages

21 min read

Build a Full Page Screenshot Chrome Extension: Capture Entire Webpages

Build a Full Page Screenshot Chrome Extension: Capture Entire Webpages

Screenshot functionality is one of the most requested features for Chrome extensions. Whether users need to save entire articles for offline reading, capture long webpages for documentation, or archive e-commerce pages for price tracking, full page screenshot capabilities provide immense value. In this comprehensive guide, we will walk you through building a production-ready Chrome extension that captures complete webpages, not just the visible viewport.

This tutorial leverages the Chrome Desktop Capture API combined with HTML5 Canvas to create a robust screenshot extension. By the end of this guide, you will have a fully functional extension that can capture any webpage in its entirety and save it as a PNG image.


Prerequisites and Development Setup

Before we begin building our full page screenshot Chrome extension, ensure you have the following tools installed on your development machine. You will need a modern code editor like Visual Studio Code, Google Chrome browser, and basic knowledge of HTML, CSS, and JavaScript.

Chrome extension development does not require a complex setup. You can start by creating a new folder for your project and organizing your files according to the standard extension structure. The essential files you will need include manifest.json, popup.html, popup.js, and a content script for handling the page capture.

Your project folder should follow this structure:

full-page-screenshot/
├── manifest.json
├── popup.html
├── popup.js
├── background.js
├── content.js
├── styles.css
└── icons/
    ├── icon16.png
    ├── icon48.png
    └── icon128.png

The manifest file serves as the configuration center for your Chrome extension, defining permissions, background scripts, and UI components. For our screenshot extension, we will request specific permissions that allow capturing the desktop and interacting with web pages.


Creating the Manifest File

The manifest.json file is the foundation of every Chrome extension. For our full page screenshot Chrome extension, we need to declare the appropriate permissions and define the extension’s components. We will use Manifest V3, which is the current standard for Chrome extensions.

{
  "manifest_version": 3,
  "name": "Full Page Screenshot",
  "version": "1.0.0",
  "description": "Capture entire webpages as high-quality PNG images",
  "permissions": [
    "desktopCapture",
    "activeTab",
    "storage"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

The desktopCapture permission is essential for our functionality. It allows the extension to capture the contents of tabs and windows. The activeTab permission ensures we can access the current tab when the user clicks our extension icon. We also include storage permission for saving user preferences.


The popup is what users see when they click your extension icon. It should be simple and intuitive, providing clear options for capturing screenshots. Let’s create a clean, functional popup interface using HTML and CSS.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Full Page Screenshot</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="container">
    <h1>Full Page Screenshot</h1>
    <div class="options">
      <label>
        <input type="checkbox" id="includeScrollbar" checked>
        Include scrollbar
      </label>
      <label>
        <input type="checkbox" id="showCursor" checked>
        Show cursor position
      </label>
    </div>
    <div class="quality-options">
      <label>Image Quality:</label>
      <select id="imageQuality">
        <option value="0.8">Standard (80%)</option>
        <option value="0.92" selected>High (92%)</option>
        <option value="1.0">Maximum (100%)</option>
      </select>
    </div>
    <button id="captureBtn" class="primary-btn">Capture Screenshot</button>
    <div id="status" class="status"></div>
  </div>
  <script src="popup.js"></script>
</body>
</html>

The popup provides options for including the scrollbar, showing cursor position, and selecting image quality. These options give users flexibility while maintaining simplicity. The status element displays progress messages and any errors that occur during capture.

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  width: 320px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #f5f5f5;
  padding: 20px;
}

.container {
  background: white;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

h1 {
  font-size: 18px;
  margin-bottom: 16px;
  color: #333;
}

.options {
  margin-bottom: 16px;
}

.options label {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 14px;
  color: #555;
  cursor: pointer;
}

.quality-options {
  margin-bottom: 16px;
}

.quality-options label {
  display: block;
  font-size: 14px;
  color: #555;
  margin-bottom: 8px;
}

.quality-options select {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

.primary-btn {
  width: 100%;
  padding: 12px;
  background: #4285f4;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s;
}

.primary-btn:hover {
  background: #3367d6;
}

.primary-btn:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.status {
  margin-top: 12px;
  font-size: 12px;
  text-align: center;
  color: #666;
  min-height: 20px;
}

.status.error {
  color: #d32f2f;
}

.status.success {
  color: #388e3c;
}

Implementing the Background Service Worker

The background service worker handles communication between the popup and the content script. It initiates the desktop capture process and coordinates the screenshot workflow. This is where the chrome.desktopCapture API comes into play.

// background.js

// Listen for messages from the popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'captureScreenshot') {
    captureTab(message.options)
      .then(captureId => {
        // Send the capture stream ID back to the popup
        sendResponse({ success: true, captureId: captureId });
      })
      .catch(error => {
        sendResponse({ success: false, error: error.message });
      });
    return true; // Keep the message channel open for async response
  }
});

async function captureTab(options) {
  // Request desktop capture for the current tab
  const sources = await chrome.desktopCapture.getDesktopSources({
    types: ['tab'],
    thumbnailSize: { width: 1920, height: 1080 }
  });

  // Find the tab source
  const tabSource = sources.find(source => 
    source.id.startsWith('tab:') || source.id.startsWith('web-contents:')
  );

  if (!tabSource) {
    throw new Error('No tab source found for capture');
  }

  return tabSource.id;
}

The background script uses the getDesktopSources method to access available capture sources. We request tab sources specifically, which gives us access to the browser tab content. The thumbnail size parameter determines the resolution of the captured image.


The Core Screenshot Logic

The content script runs in the context of the webpage and handles the actual screenshot capture process. This is where we use the MediaStream from the desktop capture and process it to create a full page screenshot.

// content.js

// Listen for messages from the popup or background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'captureFullPage') {
    captureFullPage(message.options)
      .then(dataUrl => {
        sendResponse({ success: true, dataUrl: dataUrl });
      })
      .catch(error => {
        sendResponse({ success: false, error: error.message });
      });
    return true;
  }
});

async function captureFullPage(options = {}) {
  const { quality = 0.92, includeScrollbar = true } = options;

  // Get the dimensions of the entire page
  const totalWidth = document.documentElement.scrollWidth;
  const totalHeight = document.documentElement.scrollHeight;

  // Store original scroll position
  const originalScrollPosition = window.scrollY;

  // Create a canvas element
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  // Set canvas dimensions to full page size
  canvas.width = totalWidth;
  canvas.height = totalHeight;

  // Method 1: Use html2canvas-like approach with drawWindow
  // This requires the page to be captured via desktopCapture
  
  // Request capture from background
  const captureId = await chrome.runtime.sendMessage({
    action: 'getCaptureStream'
  });

  if (!captureId) {
    throw new Error('Failed to initialize capture');
  }

  // Create stream from the capture
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: {
      mandatory: {
        chromeMediaSource: 'desktop',
        chromeMediaSourceId: captureId,
        minWidth: totalWidth,
        minHeight: totalHeight,
        maxWidth: totalWidth * 2,
        maxHeight: totalHeight * 2
      }
    }
  });

  // Create video element to capture the stream
  const video = document.createElement('video');
  video.srcObject = stream;
  await video.play();

  // Draw the video frame to canvas
  ctx.drawImage(video, 0, 0);

  // Stop the stream
  stream.getTracks().forEach(track => track.stop());

  // Convert canvas to data URL
  const dataUrl = canvas.toDataURL('image/png', quality);

  return dataUrl;
}

// Alternative method using the Page Capture API
async function captureUsingPageCapture() {
  // This method uses chrome.pageCapture API
  const tab = await chrome.tabs.query({ active: true, currentWindow: true });
  
  if (!tab[0].id) {
    throw new Error('No active tab found');
  }

  return new Promise((resolve, reject) => {
    chrome.pageCapture.saveAsMHTML({ tabId: tab[0].id }, (blob) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      
      // Convert blob to appropriate format
      const reader = new FileReader();
      reader.onloadend = () => resolve(reader.result);
      reader.onerror = reject;
      reader.readAsDataURL(blob);
    });
  });
}

This content script handles the complex task of capturing the full page. It calculates the total page dimensions including content that extends beyond the visible viewport, creates a canvas element, and draws the captured content onto it. The script supports multiple capture methods depending on the specific requirements and Chrome permissions available.


The popup JavaScript connects the user interface with the extension’s functionality. It handles button clicks, collects user options, and coordinates the screenshot capture process.

// popup.js

document.addEventListener('DOMContentLoaded', () => {
  const captureBtn = document.getElementById('captureBtn');
  const status = document.getElementById('status');
  const includeScrollbar = document.getElementById('includeScrollbar');
  const showCursor = document.getElementById('showCursor');
  const imageQuality = document.getElementById('imageQuality');

  captureBtn.addEventListener('click', async () => {
    try {
      // Update UI to show progress
      captureBtn.disabled = true;
      status.textContent = 'Preparing capture...';
      status.className = 'status';

      // Get current tab
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      
      if (!tab.id) {
        throw new Error('No active tab found');
      }

      // Collect options
      const options = {
        quality: parseFloat(imageQuality.value),
        includeScrollbar: includeScrollbar.checked,
        showCursor: showCursor.checked
      };

      // Send message to content script to capture
      const response = await chrome.tabs.sendMessage(tab.id, {
        action: 'captureFullPage',
        options: options
      });

      if (!response.success) {
        throw new Error(response.error);
      }

      // Download the screenshot
      await downloadScreenshot(response.dataUrl, tab.title);

      status.textContent = 'Screenshot captured successfully!';
      status.className = 'status success';

    } catch (error) {
      console.error('Capture error:', error);
      status.textContent = error.message || 'Failed to capture screenshot';
      status.className = 'status error';
    } finally {
      captureBtn.disabled = false;
    }
  });

  async function downloadScreenshot(dataUrl, title) {
    // Create a temporary link to trigger download
    const link = document.createElement('a');
    
    // Generate filename from page title
    const filename = title
      .replace(/[^a-z0-9]/gi, '_')
      .substring(0, 50)
      .toLowerCase();
    
    const timestamp = new Date().toISOString().slice(0, 10);
    link.download = `${filename}_${timestamp}.png`;
    link.href = dataUrl;
    
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  }
});

The popup controller provides a smooth user experience by disabling the capture button during processing, displaying status messages, and automatically generating descriptive filenames based on the page title. It handles errors gracefully and provides clear feedback to users.


Advanced Features and Optimizations

A production-ready screenshot extension should include additional features that enhance usability and handle edge cases. Let’s explore some advanced implementations that will make your extension stand out in the Chrome Web Store.

Handling Large Pages

Large webpages with extensive content can cause memory issues during capture. Implement a chunked capture approach that processes the page in sections and stitches them together:

async function captureLargePage(chunkHeight = 4000) {
  const totalHeight = document.documentElement.scrollHeight;
  const viewportHeight = window.innerHeight;
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  
  canvas.width = document.documentElement.scrollWidth;
  canvas.height = totalHeight;
  
  let currentPosition = 0;
  
  while (currentPosition < totalHeight) {
    // Scroll to position
    window.scrollTo(0, currentPosition);
    
    // Wait for rendering
    await new Promise(resolve => setTimeout(resolve, 100));
    
    // Capture viewport
    const chunkCanvas = document.createElement('canvas');
    chunkCanvas.width = window.innerWidth;
    chunkCanvas.height = Math.min(chunkHeight, totalHeight - currentPosition);
    
    const chunkCtx = chunkCanvas.getContext('2d');
    chunkCtx.drawWindow(
      window,
      0,
      currentPosition,
      window.innerWidth,
      chunkCanvas.height,
      'rgb(255, 255, 255)'
    );
    
    // Draw chunk to main canvas
    ctx.drawImage(chunkCanvas, 0, currentPosition);
    
    currentPosition += chunkHeight;
  }
  
  // Restore original position
  window.scrollTo(0, 0);
  
  return canvas.toDataURL('image/png', 0.92);
}

Adding Watermarks and Annotations

Many users want to add watermarks or annotations to their screenshots. Implement a post-processing function that adds these elements:

function addWatermark(dataUrl, watermarkText) {
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => {
      const canvas = document.createElement('canvas');
      canvas.width = img.width;
      canvas.height = img.height;
      
      const ctx = canvas.getContext('2d');
      
      // Draw original image
      ctx.drawImage(img, 0, 0);
      
      // Add watermark
      ctx.font = '14px Arial';
      ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
      ctx.textAlign = 'right';
      ctx.fillText(
        watermarkText,
        canvas.width - 10,
        canvas.height - 10
      );
      
      resolve(canvas.toDataURL('image/png'));
    };
    img.src = dataUrl;
  });
}

Testing Your Extension

Before publishing your extension, thorough testing is essential. Chrome provides built-in tools for testing extensions that make the development process straightforward. Load your extension in developer mode by navigating to chrome://extensions, enabling developer mode, and clicking “Load unpacked.” Select your extension folder to install it temporarily.

Test your extension on various types of websites including those with lazy-loaded images, infinite scroll, dynamic content, and complex layouts. Pay special attention to pages with frames or embedded content, as these can present unique challenges for screenshot capture.

Use Chrome DevTools to debug any issues. The Console panel will show any JavaScript errors, while the Network panel helps identify problems with resource loading. For more detailed debugging, you can inspect the background service worker by clicking the “service worker” link in the extensions page.


Publishing to the Chrome Web Store

Once your extension is thoroughly tested and working correctly, you can publish it to the Chrome Web Store. Create a developer account if you do not already have one, and prepare your store listing with compelling screenshots, a detailed description, and appropriate category tags.

The review process typically takes a few days. Google checks for policy compliance, security issues, and proper functionality. Ensure your extension follows Chrome’s policies, particularly regarding user data handling and permissions usage. Be clear about why you need each permission you request, as this helps with the review process.


Conclusion

Building a full page screenshot Chrome extension is a rewarding project that teaches you important concepts about Chrome extension architecture, the desktop capture API, and canvas-based image processing. The extension we built in this guide provides a solid foundation that you can extend with additional features like annotation tools, cloud storage integration, or automatic sharing to social media.

The key to a successful screenshot extension lies in handling edge cases gracefully and providing a smooth user experience. Users expect their screenshots to accurately represent the webpage they are capturing, including all content beyond the visible viewport. By following the patterns and best practices outlined in this guide, you can create a reliable and professional screenshot tool that serves users well.

Remember to maintain and update your extension regularly to ensure compatibility with Chrome updates and evolving web technologies. With the right approach, your full page screenshot extension can become a valuable tool for millions of Chrome users who need to capture and save webpage content.

No previous article
No next article