Chrome Extension Printing API — Developer Guide

25 min read

Chrome Extension Printing API Guide

Overview

Permissions & Requirements

// manifest.json
{
  "permissions": [
    "printing",
    "printingMetrics"
  ],
  "manifest_version": 3
}

Important Requirements:

Listing Available Printers

// Get all available printers
chrome.printing.getPrinters((printers) => {
  printers.forEach(printer => {
    console.log('Printer:', printer.name);
    console.log('ID:', printer.id);
    console.log('Description:', printer.description);
    console.log('---');
  });
});

Printer Object Structure:

interface Printer {
  id: string;           // Unique printer identifier
  name: string;         // Human-readable name
  description?: string; // Printer description
  printerStatus?: string; // "online", "offline", "error"
}

Typical Response:

[
  {
    id: "00112233-4455-6677-8899-aabbccddeeff",
    name: "HP LaserJet Pro",
    description: "Office Printer - Floor 2",
    printerStatus: "online"
  },
  {
    id: "aabbccdd-eeff-1122-3344-556677889900",
    name: "Google Cloud Print",
    description: "Cloud Printer",
    printerStatus: "online"
  }
]

Understanding Print Tickets

The print ticket defines all job settings. Key components:

const printTicket = {
  jobTitle: "My Print Job",
  printerId: "printer-id-here",
  ticket: {
    version: "1.0",
    print: {
      color: { type: "STANDARD_COLOR" },      // or "STANDARD_MONOCHROME"
      duplex: { type: "NO_DUPLEX" },           // "LONG_EDGE", "SHORT_EDGE"
      copies: { copies: 1 },
      mediaSize: {
        widthMicrons: 215900,                  // A4: 210000
        heightMicrons: 279400                  // A4: 297000
      },
      collation: { collate: false },
      orientation: { type: "PORTRAIT" },       // "LANDSCAPE"
      dpi: { horizontalDpi: 600, verticalDpi: 600 },
      margins: {
        topMicrons: 0,
        bottomMicrons: 0,
        leftMicrons: 0,
        rightMicrons: 0
      }
    }
  },
  content: {
    // URL to print (must be a chrome-extension:// or chrome:// resource)
    // Or use document instead
    documentUrl: chrome.runtime.getURL("content/printable.html")
  }
};

Common Print Ticket Configurations

Basic Black & White Print

function createBWPrintTicket(printerId) {
  return {
    jobTitle: `Print Job ${Date.now()}`,
    printerId: printerId,
    ticket: {
      version: "1.0",
      print: {
        color: { type: "STANDARD_MONOCHROME" },
        duplex: { type: "NO_DUPLEX" },
        copies: { copies: 1 },
        mediaSize: { widthMicrons: 215900, heightMicrons: 279400 },
        orientation: { type: "PORTRAIT" }
      }
    },
    content: { documentUrl: "" }  // Set dynamically
  };
}

Double-Sided (Duplex) Print

function createDuplexPrintTicket(printerId, isLongEdge = true) {
  return {
    jobTitle: "Double-Sided Document",
    printerId: printerId,
    ticket: {
      version: "1.0",
      print: {
        color: { type: "STANDARD_COLOR" },
        duplex: { 
          type: isLongEdge ? "LONG_EDGE" : "SHORT_EDGE" 
        },
        copies: { copies: 1 },
        mediaSize: { widthMicrons: 215900, heightMicrons: 279400 },
        orientation: { type: "PORTRAIT" }
      }
    },
    content: { documentUrl: "" }
  };
}

Multiple Copies with Collating

function createCollatedPrintTicket(printerId, copies = 1) {
  return {
    jobTitle: `Multi-Copy Print (${copies} copies)`,
    printerId: printerId,
    ticket: {
      version: "1.0",
      print: {
        color: { type: "STANDARD_COLOR" },
        duplex: { type: "NO_DUPLEX" },
        copies: { copies: copies },
        collation: { collate: copies > 1 },
        mediaSize: { widthMicrons: 215900, heightMicrons: 279400 }
      }
    },
    content: { documentUrl: "" }
  };
}

Different Paper Sizes

const PAPER_SIZES = {
  A4: { widthMicrons: 210000, heightMicrons: 297000 },
  LETTER: { widthMicrons: 215900, heightMicrons: 279400 },
  LEGAL: { widthMicrons: 215900, heightMicrons: 355600 },
  A3: { widthMicrons: 297000, heightMicrons: 420000 },
  TABLOID: { widthMicrons: 279400, heightMicrons: 431800 }
};

function createPrintTicketWithSize(printerId, paperSize) {
  return {
    jobTitle: `${paperSize} Print Job`,
    printerId: printerId,
    ticket: {
      version: "1.0",
      print: {
        color: { type: "STANDARD_COLOR" },
        duplex: { type: "NO_DUPLEX" },
        copies: { copies: 1 },
        mediaSize: PAPER_SIZES[paperSize] || PAPER_SIZES.A4
      }
    },
    content: { documentUrl: "" }
  };
}

Submitting Print Jobs

function submitPrintJob(printTicket) {
  chrome.printing.submitJob(printTicket, (response) => {
    if (chrome.runtime.lastError) {
      console.error('Error:', chrome.runtime.lastError.message);
      return;
    }
    
    if (response.status === "OK") {
      console.log('Job submitted successfully!');
      console.log('Job ID:', response.jobId);
    } else {
      console.error('Submission failed:', response.status);
    }
  });
}

Submit Job Response:

interface SubmitJobResponse {
  status: "OK" | "USER_NOT_AUTHENTICATED" | "USER_NOT_AUTHORIZED" | 
          "INVALID_PRINTER_ID" | "INVALID_PRINT_TICKET" | 
          "INVALID_JOB_TITLE" | "INVALID_CONTENT_URL" | 
          "PRINTER_NOT_READY" | "TICKET_CONFLICT" | 
          "UNKNOWN_ERROR";
  jobId?: string;  // Only present if status is "OK"
}

Monitoring Job Status

// Listen for job status changes
chrome.printing.onJobStatusChanged.addListener((jobInfo) => {
  console.log('Job ID:', jobInfo.jobId);
  console.log('Status:', jobInfo.status);
  console.log('Status Info:', jobInfo.statusInfo);
  
  // Job statuses: "queued", "pending", "in_progress", 
  //               "canceled", "printed", "error"
});

/*
Job Status Values:
- QUEUED: Job is queued, waiting for processing
- PENDING: Job is pending user confirmation
- IN_PROGRESS: Job is being sent to printer
- CANCELED: Job was canceled
- PRINTED: Job completed successfully
- ERROR: An error occurred

Job Info Object:
{
  jobId: string,
  status: string,
  statusInfo: string,
  printerId: string
}
*/

Complete Print with Status Monitoring

class PrintJobManager {
  constructor() {
    this.activeJobs = new Map();
    this.setupListeners();
  }
  
  setupListeners() {
    chrome.printing.onJobStatusChanged.addListener((jobInfo) => {
      this.handleJobStatusChange(jobInfo);
    });
  }
  
  handleJobStatusChange(jobInfo) {
    const { jobId, status, statusInfo, printerId } = jobInfo;
    
    console.log(`Job ${jobId}: ${status} - ${statusInfo}`);
    
    // Update job tracking
    this.activeJobs.set(jobId, jobInfo);
    
    // Handle completion
    if (status === 'PRINTED') {
      console.log(`✓ Job ${jobId} completed successfully`);
      this.activeJobs.delete(jobId);
    }
    
    // Handle errors
    if (status === 'ERROR') {
      console.error(`✗ Job ${jobId} failed: ${statusInfo}`);
      this.activeJobs.delete(jobId);
    }
    
    // Handle cancellation
    if (status === 'CANCELED') {
      console.log(`Job ${jobId} was canceled`);
      this.activeJobs.delete(jobId);
    }
  }
  
  async submitPrintJob(printTicket) {
    return new Promise((resolve, reject) => {
      chrome.printing.submitJob(printTicket, (response) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
          return;
        }
        
        if (response.status === "OK") {
          resolve(response.jobId);
        } else {
          reject(new Error(`Failed: ${response.status}`));
        }
      });
    });
  }
  
  getActiveJobs() {
    return Array.from(this.activeJobs.values());
  }
}

// Usage
const printManager = new PrintJobManager();

const ticket = createBWPrintTicket("printer-id");
ticket.content.documentUrl = chrome.runtime.getURL("printable/page.html");

printManager.submitPrintJob(ticket)
  .then(jobId => console.log('Job started:', jobId))
  .catch(err => console.error(err));

Chrome Printing Metrics API

For enterprise environments, track printing usage:

// Get printing metrics
chrome.printingMetrics.getPrintJobs((metrics) => {
  metrics.forEach(metric => {
    console.log('Job ID:', metric.jobId);
    console.log('Printer:', metric.printerId);
    console.log('User:', metric.userId);
    console.log('Timestamp:', new Date(metric.timestamp * 1000));
    console.log('Status:', metric.status);
    console.log('Pages Printed:', metric.pagesPrinted);
    console.log('Copies:', metric.copies);
    console.log('---');
  });
});

/*
Print Job Metric Object:
{
  jobId: string,
  printerId: string,
  printerName: string,
  userId: string,
  timestamp: number,      // Unix timestamp
  status: string,         // "success", "failed", "canceled"
  pagesPrinted: number,
  copies: number,
  documentType: string,   // "pdf", "html", etc.
  colorType: string,      // "color", "bw"
  duplex: boolean,
  mediaSize: string       // "letter", "a4", etc.
}
*/

Enterprise Usage Tracking Example

class PrintingAnalytics {
  constructor() {
    this.stats = {
      totalJobs: 0,
      totalPages: 0,
      byPrinter: {},
      byUser: {},
      byDay: {}
    };
  }
  
  async loadMetrics() {
    return new Promise((resolve) => {
      chrome.printingMetrics.getPrintJobs((metrics) => {
        this.processMetrics(metrics);
        resolve(this.stats);
      });
    });
  }
  
  processMetrics(metrics) {
    metrics.forEach(job => {
      if (job.status !== 'success') return;
      
      this.stats.totalJobs++;
      this.stats.totalPages += job.pagesPrinted;
      
      // By printer
      if (!this.stats.byPrinter[job.printerName]) {
        this.stats.byPrinter[job.printerName] = { jobs: 0, pages: 0 };
      }
      this.stats.byPrinter[job.printerName].jobs++;
      this.stats.byPrinter[job.printerName].pages += job.pagesPrinted;
      
      // By user
      if (!this.stats.byUser[job.userId]) {
        this.stats.byUser[job.userId] = { jobs: 0, pages: 0 };
      }
      this.stats.byUser[job.userId].jobs++;
      this.stats.byUser[job.userId].pages += job.pagesPrinted;
      
      // By day
      const day = new Date(job.timestamp * 1000).toDateString();
      if (!this.stats.byDay[day]) {
        this.stats.byDay[day] = { jobs: 0, pages: 0 };
      }
      this.stats.byDay[day].jobs++;
      this.stats.byDay[day].pages += job.pagesPrinted;
    });
  }
  
  generateReport() {
    console.log('=== Printing Usage Report ===');
    console.log(`Total Jobs: ${this.stats.totalJobs}`);
    console.log(`Total Pages: ${this.stats.totalPages}`);
    console.log('\nBy Printer:');
    Object.entries(this.stats.byPrinter).forEach(([printer, data]) => {
      console.log(`  ${printer}: ${data.jobs} jobs, ${data.pages} pages`);
    });
  }
}

// Usage
const analytics = new PrintingAnalytics();
analytics.loadMetrics().then(() => analytics.generateReport());

Building a Print Management Extension

Complete Extension Structure

// background.js - Main service worker
class PrintManager {
  constructor() {
    this.printers = [];
    this.selectedPrinter = null;
    this.initialize();
  }
  
  async initialize() {
    // Load printers on startup
    await this.refreshPrinters();
    
    // Set up listeners
    this.setupMessageListeners();
    this.setupPrintListeners();
  }
  
  async refreshPrinters() {
    return new Promise((resolve) => {
      chrome.printing.getPrinters((printers) => {
        this.printers = printers;
        console.log(`Loaded ${printers.length} printers`);
        resolve(printers);
      });
    });
  }
  
  setupMessageListeners() {
    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
      switch (message.type) {
        case 'GET_PRINTERS':
          sendResponse({ printers: this.printers });
          break;
        case 'REFRESH_PRINTERS':
          this.refreshPrinters().then(printers => {
            sendResponse({ printers: printers });
          });
          return true; // Async response
        case 'SUBMIT_JOB':
          this.submitJob(message.data)
            .then(result => sendResponse(result))
            .catch(err => sendResponse({ error: err.message }));
          return true;
        case 'GET_JOB_STATUS':
          // Implementation for checking specific job
          sendResponse({ jobs: this.getActiveJobs() });
          break;
      }
    });
  }
  
  setupPrintListeners() {
    chrome.printing.onJobStatusChanged.addListener((jobInfo) => {
      // Notify all extension views
      chrome.runtime.sendMessage({
        type: 'JOB_STATUS_UPDATE',
        data: jobInfo
      }).catch(() => {
        // Ignore errors when no listeners
      });
      
      // Store for later reference
      this.updateJobCache(jobInfo);
    });
  }
  
  async submitJob(options) {
    const { printerId, documentUrl, ticket, jobTitle } = options;
    
    const printTicket = {
      jobTitle: jobTitle || `Print Job ${Date.now()}`,
      printerId: printerId,
      ticket: ticket,
      content: { documentUrl: documentUrl }
    };
    
    return new Promise((resolve, reject) => {
      chrome.printing.submitJob(printTicket, (response) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
          return;
        }
        
        if (response.status === "OK") {
          resolve({ jobId: response.jobId, status: response.status });
        } else {
          reject(new Error(response.status));
        }
      });
    });
  }
  
  updateJobCache(jobInfo) {
    // Implementation for job cache management
  }
  
  getActiveJobs() {
    // Return tracked active jobs
    return [];
  }
}

// Initialize
const printManager = new PrintManager();
// popup.js - Handle UI interactions
document.addEventListener('DOMContentLoaded', async () => {
  const printerSelect = document.getElementById('printer-select');
  const printButton = document.getElementById('print-button');
  const statusDiv = document.getElementById('status');
  
  // Load printers
  async function loadPrinters() {
    const response = await chrome.runtime.sendMessage({ 
      type: 'GET_PRINTERS' 
    });
    
    printerSelect.innerHTML = response.printers
      .map(p => `<option value="${p.id}">${p.name}</option>`)
      .join('');
  }
  
  // Submit print job
  printButton.addEventListener('click', async () => {
    const printerId = printerSelect.value;
    const documentUrl = chrome.runtime.getURL('content/printable.html');
    
    const ticket = createBWPrintTicket(printerId);
    ticket.content.documentUrl = documentUrl;
    
    try {
      const result = await chrome.runtime.sendMessage({
        type: 'SUBMIT_JOB',
        data: {
          printerId: printerId,
          documentUrl: documentUrl,
          ticket: ticket,
          jobTitle: 'My Document'
        }
      });
      
      statusDiv.textContent = `Job submitted: ${result.jobId}`;
    } catch (err) {
      statusDiv.textContent = `Error: ${err.message}`;
    }
  });
  
  // Listen for job updates
  chrome.runtime.onMessage.addListener((message) => {
    if (message.type === 'JOB_STATUS_UPDATE') {
      statusDiv.textContent = `Job ${message.data.jobId}: ${message.data.status}`;
    }
  });
  
  loadPrinters();
});

HTML for Print Content

<!-- content/printable.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Printable Document</title>
  <style>
    @media print {
      body { margin: 0; padding: 20px; }
      .no-print { display: none; }
    }
    body { font-family: Arial, sans-serif; padding: 20px; }
    h1 { color: #333; }
  </style>
</head>
<body>
  <h1>My Printable Document</h1>
  <p>This content will be printed.</p>
  <p>Date: <span id="date"></span></p>
  
  <script>
    document.getElementById('date').textContent = new Date().toLocaleString();
  </script>
</body>
</html>

Best Practices & Error Handling

// Robust print job submission with retries
async function submitWithRetry(printTicket, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const result = await submitPrintJob(printTicket);
      return result;
    } catch (error) {
      lastError = error;
      console.log(`Attempt ${attempt} failed: ${error.message}`);
      
      // Wait before retry (exponential backoff)
      if (attempt < maxRetries) {
        await new Promise(r => setTimeout(r, 1000 * attempt));
      }
    }
  }
  
  throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}

// Validate printer before printing
async function validatePrinter(printerId) {
  const printers = await new Promise(resolve => {
    chrome.printing.getPrinters(resolve);
  });
  
  const printer = printers.find(p => p.id === printerId);
  if (!printer) {
    throw new Error(`Printer not found: ${printerId}`);
  }
  
  if (printer.printerStatus === 'offline') {
    throw new Error('Printer is offline');
  }
  
  return printer;
}

// Complete print workflow
async function printDocument(options) {
  const { 
    printerId, 
    documentUrl, 
    copies = 1, 
    color = true, 
    duplex = false 
  } = options;
  
  // Validate printer
  await validatePrinter(printerId);
  
  // Build ticket
  const ticket = {
    jobTitle: `Print Job ${Date.now()}`,
    printerId: printerId,
    ticket: {
      version: "1.0",
      print: {
        color: { type: color ? "STANDARD_COLOR" : "STANDARD_MONOCHROME" },
        duplex: { type: duplex ? "LONG_EDGE" : "NO_DUPLEX" },
        copies: { copies: copies },
        collation: { collate: copies > 1 },
        mediaSize: { widthMicrons: 215900, heightMicrons: 279400 }
      }
    },
    content: { documentUrl: documentUrl }
  };
  
  // Submit with retry
  return submitWithRetry(ticket);
}

Summary

The Chrome Printing API enables powerful print management capabilities:

Key considerations:

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.