dompeg.js

2 min read Original article ↗
              
                <header>
  <h1>dompeg.js</h1>
</header>
<main>
  <p>The bookmarklet: <a href="javascript:(async%20function%20()%7B%20%0A%20const%20width%20%3D%20document.scrollingElement.scrollWidth%3B%0A%20const%20height%20%3D%20document.scrollingElement.scrollHeight%3B%0A%20const%20doc%20%3D%20document.implementation.createHTMLDocument('')%3B%0A%20doc.write(document.documentElement.outerHTML)%3B%0A%20doc.documentElement.setAttribute('xmlns'%2C%20doc.documentElement.namespaceURI)%3B%0A%20%0A%20const%20styles%20%3D%20%5B%5D%3B%0A%20for(%20let%20i%20%3D%200%3B%20i%20%3C%20document.styleSheets.length%3B%20i%2B%2B%20)%20%7B%0A%20const%20ss%20%3D%20document.styleSheets%5Bi%5D%3B%0A%20if%20(%20ss.cssRules%20)%20%7B%0A%20for(%20let%20j%20%3D%200%3B%20j%20%3C%20ss.cssRules.length%3B%20j%2B%2B%20)%20%7B%0A%20styles.push(%20ss.cssRules%5Bj%5D.cssText%20)%3B%0A%20%7D%0A%20%7D%20else%20%7B%0A%20try%20%7B%0A%20const%20res%20%3D%20await%20fetch(ss.href)%3B%0A%20const%20cssText%20%3D%20await%20res.text()%3B%0A%20styles.push(cssText)%3B%0A%20%7D%20catch(e)%20%7B%0A%20try%20%7B%0A%20const%20res%20%3D%20await%20fetch(%60https%3A%2F%2Fdompeg-proxy-90jseygqo65r.runkit.sh%2F%3Furl%3D%24%7Bbtoa(ss.href)%7D%60)%3B%0A%20const%20cssText%20%3D%20await%20res.text()%3B%0A%20styles.push(cssText)%3B%0A%20%7D%20catch(e)%20%7B%20%0A%20console.warn(%60Exception%20adding%20styles%20from%20%24%7Bss.href%7D%60%2C%20e%2C%20e.stack)%3B%0A%20%7D%0A%20%7D%0A%20%7D%0A%20%7D%0A%20%0A%20%0A%20Array.from(%20doc.querySelectorAll('noscript%2C%20link%2C%20script')).forEach(%20el%20%3D%3E%20el.remove()%20)%3B%0A%20stripComments(doc)%3B%0A%20Array.from(%20doc.querySelectorAll('*%5Bstyle%5D')).forEach(%20el%20%3D%3E%20%7B%0A%20const%20styleText%20%3D%20el.getAttribute('style')%3B%0A%20const%20uniq%20%3D%20(Math.random()%2B''%2Bperformance.now()).replace(%2F%5C.%2Fg%2C'x')%3B%0A%20const%20className%20%3D%20%60class%24%7Buniq%7D%60%3B%0A%20const%20cssText%20%3D%20%60.%24%7BclassName%7D%20%7B%24%7B%20styleText%20%7D%7D%60%3B%0A%20styles.push(%20cssText%20)%3B%0A%20el.classList.add(%20className%20)%3B%0A%20%7D)%3B%0A%20%0A%20%0A%20const%20styleElement%20%3D%20doc.createElement('style')%3B%0A%20styleElement.innerText%20%3D%20styles.join('%5Cn')%3B%0A%20doc.documentElement.appendChild(styleElement)%3B%0A%0A%20%0A%20const%20canvas%20%3D%20document.createElement('canvas')%3B%0A%20Object.assign(%20canvas%2C%20%7Bwidth%2Cheight%7D)%3B%0A%20const%20ctx%20%3D%20canvas.getContext('2d')%3B%0A%0A%20const%20data%20%3D%20%60%0A%20%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22%24%7Bwidth%7D%22%20height%3D%22%24%7Bheight%7D%22%3E%0A%20%3CforeignObject%20width%3D%22100%25%22%20height%3D%22100%25%22%3E%0A%20%24%7B(new%20XMLSerializer).serializeToString(doc).slice(15)%7D%0A%20%3C%2FforeignObject%3E%0A%20%3C%2Fsvg%3E%60%3B%0A%20const%20DOMURL%20%3D%20window.URL%20%7C%7C%20window.webkitURL%20%7C%7C%20window%3B%0A%0A%20const%20img%20%3D%20new%20Image()%3B%0A%20const%20svg%20%3D%20new%20Blob(%5Bdata%5D%2C%20%7Btype%3A%20'image%2Fsvg%2Bxml'%7D)%3B%0A%20%0A%20Object.assign(%20img%2C%20%7Bwidth%2Cheight%7D)%3B%20%0A%20img.crossOrigin%20%3D%20%22Anonymous%22%3B%0A%20img.onload%20%3D%20function()%20%7B%0A%20ctx.fillStyle%20%3D%20'white'%3B%0A%20ctx.fillRect(%200%2C%200%2C%20canvas.width%2C%20canvas.height%20)%3B%0A%20ctx.drawImage(img%2C%200%2C%200)%3B%0A%20const%20datauri%20%3D%20canvas.toDataURL('image%2Fjpeg')%3B%0A%20const%20anchor%20%3D%20document.createElement('a')%3B%0A%20anchor.download%20%3D%20'screen.jpg'%3B%0A%20anchor.href%20%3D%20datauri%3B%0A%20anchor.target%20%3D%20%22_new%22%3B%0A%20anchor.innerText%20%3D%20'download%20screen.jpg'%3B%0A%20anchor.addEventListener('click'%2C%20e%20%3D%3E%20%7Be.stopPropagation()%3Banchor.remove()%3B%7D%2C%20%7B%20capture%3A%20true%20%7D)%3B%0A%20document.body.appendChild(anchor)%3B%0A%20Object.assign(%20anchor.style%2C%20%7B%0A%20position%3A%20'fixed'%2C%0A%20background%3A'white'%2C%0A%20fontSize%3A%20'18px'%2C%0A%20fontFamily%3A%20'monospace'%2C%0A%20color%3A%20'blue'%2C%0A%20top%3A%200%2C%0A%20left%3A%200%2C%0A%20zIndex%3A%20Number.MAX_SAFE_INTEGER%0A%20%7D)%3B%0A%20%7D%0A%20img.src%20%3D%20buildSvgImageUrl(data)%3B%20%0A%20img.style.position%20%3D%20%22absolute%22%3B%0A%20img.style.zIndex%20%3D%20%2210000000%22%3B%0A%20img.style.backgroundColor%20%3D%20%22white%22%3B%0A%20%2F%2Fdocument.body.appendChild(img)%3B%0A%20%0A%20function%20buildSvgImageUrl(svg)%20%7B%0A%20const%20b64%20%3D%20btoa(unescape(encodeURIComponent(svg)))%3B%0A%20return%20%22data%3Aimage%2Fsvg%2Bxml%3Bbase64%2C%22%20%2B%20b64%3B%0A%20%7D%0A%20%0A%20function%20stripComments(docNode)%7B%0A%20const%20commentWalker%20%3D%20docNode.evaluate('%2F%2Fcomment()'%2C%20docNode%2C%20null%2C%20XPathResult.ANY_TYPE%2C%20null)%3B%0A%20let%20comment%20%3D%20commentWalker.iterateNext()%3B%0A%20const%20cuts%20%3D%20%5B%5D%3B%0A%0A%20while%20(comment)%20%7B%0A%20cuts.push(comment)%3B%0A%20comment%20%3D%20commentWalker.iterateNext()%3B%0A%20%7D%0A%20cuts.forEach(%20node%20%3D%3E%20node.remove())%3B%0A%20%7D%0A%7D())%3B">&#128247;DOMPEG</a>
  <p><em>save your google search result pages, and other sites, as jpgs.</em>

<h1>The code</h1>
<code><pre>
(async function (){ 
  const width = document.scrollingElement.scrollWidth;
  const height = document.scrollingElement.scrollHeight;
  const doc = document.implementation.createHTMLDocument('');
  doc.write(document.documentElement.outerHTML);
  doc.documentElement.setAttribute('xmlns', doc.documentElement.namespaceURI);
  
  const styles = [];
  for( let i = 0; i < document.styleSheets.length; i++ ) {
    const ss = document.styleSheets[i];
    if ( ss.cssRules ) {
      for( let j = 0; j < ss.cssRules.length; j++ ) {
         styles.push( ss.cssRules[j].cssText );
      }
    } else {
      try {
        const res = await fetch(ss.href);
        const cssText = await res.text();
        styles.push(cssText);
      } catch(e) {
        try {
          const res = await fetch(`https://dompeg-proxy-90jseygqo65r.runkit.sh/?url=${btoa(ss.href)}`);
          const cssText = await res.text();
          styles.push(cssText);
        } catch(e) { 
          console.warn(`Exception adding styles from ${ss.href}`, e, e.stack);
        }
      }
    }
  }
  
  
  Array.from( doc.querySelectorAll('noscript, link, script')).forEach( el => el.remove() );
  stripComments(doc);
  Array.from( doc.querySelectorAll('*[style]')).forEach( el => {
    const styleText = el.getAttribute('style');
    const uniq = (Math.random()+''+performance.now()).replace(/\./g,'x');
    const className = `class${uniq}`;
    const cssText = `.${className} {${ styleText }}`;
    styles.push( cssText );
    el.classList.add( className );
  });
  
  
  const styleElement = doc.createElement('style');
  styleElement.innerText = styles.join('\n');
  doc.documentElement.appendChild(styleElement);

  
  const canvas = document.createElement('canvas');
  Object.assign( canvas, {width,height});
  const ctx = canvas.getContext('2d');

  const data = `
  <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
    <foreignObject width="100%" height="100%">
     ${(new XMLSerializer).serializeToString(doc).slice(15)}
    </foreignObject>
  </svg>`;
  const DOMURL = window.URL || window.webkitURL || window;

  const img = new Image();
  const svg = new Blob([data], {type: 'image/svg+xml'});
  
  Object.assign( img, {width,height});  
  img.crossOrigin = "Anonymous";
  img.onload = function() {
    ctx.fillStyle = 'white';
    ctx.fillRect( 0, 0, canvas.width, canvas.height );
    ctx.drawImage(img, 0, 0);
    const datauri = canvas.toDataURL('image/jpeg');
    const anchor = document.createElement('a');
    anchor.download = 'screen.jpg';
    anchor.href = datauri;
    anchor.target = "_new";
    anchor.innerText = 'download screen.jpg';
    anchor.addEventListener('click', e => {e.stopPropagation();anchor.remove();}, { capture: true });
    document.body.appendChild(anchor);
    Object.assign( anchor.style, {
      position: 'fixed',
      background:'white',
      fontSize: '18px',
      fontFamily: 'monospace',
      color: 'blue',
      top: 0,
      left: 0,
      zIndex: Number.MAX_SAFE_INTEGER
    });
  }
  img.src = buildSvgImageUrl(data);  
  img.style.position = "absolute";
  img.style.zIndex = "10000000";
  img.style.backgroundColor = "white";
  //document.body.appendChild(img);
  
  function buildSvgImageUrl(svg) {
    const b64 = btoa(unescape(encodeURIComponent(svg)));
    return "data:image/svg+xml;base64," + b64;
  }
  
  function stripComments(docNode){
    const commentWalker = docNode.evaluate('//comment()', docNode, null, XPathResult.ANY_TYPE, null);
    let comment = commentWalker.iterateNext();
    const cuts = [];

    while (comment) {
      cuts.push(comment);
      comment = commentWalker.iterateNext();
    }
    cuts.forEach( node => node.remove());
  }
}());
</code></pre>
              
            

!