PDF Viewer Embedder

Embeds PDFs directly in the page instead of downloading

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         PDF Viewer Embedder
// @namespace    *
// @version      1.9
// @author       zinchaiku
// @description  Embeds PDFs directly in the page instead of downloading
// @match        *://*/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    let currentBlobUrl = null;

    function showPDF(srcUrl, isBlob = false) {
        const existing = document.getElementById('tampermonkey-pdf-viewer');
        if (existing) existing.remove();

        const overlay = document.createElement('div');
        overlay.id = 'tampermonkey-pdf-viewer';
        overlay.style.position = 'fixed';
        overlay.style.top = 0;
        overlay.style.left = 0;
        overlay.style.width = '100vw';
        overlay.style.height = '100vh';
        overlay.style.background = 'rgba(0,0,0,0.8)';
        overlay.style.zIndex = 99999;
        overlay.style.display = 'flex';
        overlay.style.alignItems = 'center';
        overlay.style.justifyContent = 'center';

        const iframe = document.createElement('iframe');
        iframe.src = srcUrl;
        iframe.style.width = '80vw';
        iframe.style.height = '90vh';
        iframe.style.border = 'none';
        iframe.style.background = '#fff';
        overlay.appendChild(iframe);

        // Fallback if iframe fails to load (e.g. X-Frame-Options)
        iframe.onerror = () => {
            overlay.remove();
            if (isBlob && currentBlobUrl) {
                URL.revokeObjectURL(currentBlobUrl);
                currentBlobUrl = null;
            }
            window.open(srcUrl, '_blank');
        };

        // Close button
        const closeBtn = document.createElement('button');
        closeBtn.textContent = 'Close PDF';
        closeBtn.style.position = 'absolute';
        closeBtn.style.top = '20px';
        closeBtn.style.right = '200px';
        closeBtn.style.zIndex = 100000;
        closeBtn.style.padding = '2px 6px';
        closeBtn.style.fontSize = '1.2em';
        closeBtn.onclick = () => {
            overlay.remove();
            if (isBlob && currentBlobUrl) {
                URL.revokeObjectURL(currentBlobUrl);
                currentBlobUrl = null;
            }
        };
        overlay.appendChild(closeBtn);

        // "Open in New Tab" button
        const newTabBtn = document.createElement('button');
        newTabBtn.textContent = 'Open in New Tab';
        newTabBtn.style.position = 'absolute';
        newTabBtn.style.top = '20px';
        newTabBtn.style.right = '40px';
        newTabBtn.style.zIndex = 100000;
        newTabBtn.style.padding = '2px 6px';
        newTabBtn.style.fontSize = '1.2em';
        newTabBtn.onclick = () => {
            window.open(srcUrl, '_blank');
        };
        overlay.appendChild(newTabBtn);

        document.body.appendChild(overlay);

        // Track for cleanup
        if (isBlob) {
            currentBlobUrl = srcUrl;
        } else {
            currentBlobUrl = null;
        }
    }

    function isPDFLink(href) {
        try {
            const url = new URL(href, window.location.href);
            return url.pathname.toLowerCase().endsWith('.pdf');
        } catch {
            return false;
        }
    }

    function isSameOrigin(href) {
        try {
            const url = new URL(href, window.location.href);
            return url.origin === window.location.origin;
        } catch {
            return false;
        }
    }

    // Links we should never intercept - anchors, JS handlers, mail/tel links,
    // and links explicitly meant to open in a new tab.
    function isInterceptable(link) {
        const href = link.getAttribute('href');
        if (!href) return false;
        if (href.startsWith('#')) return false;
        if (href.startsWith('javascript:')) return false;
        if (href.startsWith('mailto:')) return false;
        if (href.startsWith('tel:')) return false;
        if (link.target === '_blank') return false;
        if (link.hasAttribute('download')) return false;

        try {
            const url = new URL(href, window.location.href);
            // Only intercept http(s) links
            if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
        } catch {
            return false;
        }

        return true;
    }

    function attachPDFInterceptors() {
        document.querySelectorAll('a[href]').forEach(link => {
            if (link.dataset.tmPdfBound) return;
            if (!isInterceptable(link)) return;

            link.dataset.tmPdfBound = "1";

            link.addEventListener('click', function (e) {
                // Let modified clicks (open in new tab/window, etc.) behave normally
                if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
                    return;
                }

                // Cross-origin: fetch() can't read Content-Type due to CORS
                // (most sites don't send Access-Control-Allow-Origin), so fall
                // back to an extension check instead of fetching. If it looks
                // like a PDF, load it straight into the iframe; otherwise let
                // the browser navigate there normally.
                if (!isSameOrigin(link.href)) {
                    if (isPDFLink(link.href)) {
                        e.preventDefault();
                        showPDF(link.href);
                    }
                    return;
                }

                e.preventDefault();

                fetch(link.href, { credentials: 'include' })
                    .then(res => {
                        const contentType = res.headers.get('Content-Type') || '';
                        if (contentType.includes('pdf')) {
                            return res.blob().then(blob => {
                                const blobUrl = URL.createObjectURL(blob);
                                showPDF(blobUrl, true);
                            });
                        } else {
                            window.location.href = link.href;
                        }
                    })
                    .catch(err => {
                        console.warn("PDF Viewer Embedder: fetch failed, navigating normally.", err);
                        window.location.href = link.href;
                    });
            });
        });
    }

    window.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
            const viewer = document.getElementById('tampermonkey-pdf-viewer');
            if (viewer) {
                viewer.remove();
                if (currentBlobUrl) {
                    URL.revokeObjectURL(currentBlobUrl);
                    currentBlobUrl = null;
                }
            }
        }
    });

    attachPDFInterceptors();
    const observer = new MutationObserver(attachPDFInterceptors);
    observer.observe(document.body, { childList: true, subtree: true });
})();