document.addEventListener('DOMContentLoaded', () => { const viewerMode = document.getElementById('viewerMode').value const filename = document.getElementById('filename').value const zoom = 0.25// From the pdf2htmlex parameter --zoom const zoomRatio = 1 / zoom const saveButton = document.getElementById('saveButton') const whiteoutButton = document.getElementById('whiteoutButton') const editTextButton = document.getElementById('editTextButton') const saveForm = document.getElementById('saveForm') const filenameInput = document.getElementById('filenameInput') const htmlInput = document.getElementById('htmlInput') const pageContainer = document.getElementById('page-container') let host = null let mode = null const toggleMode = newMode => { if (mode === newMode) newMode = null switch (mode) { case 'whiteout': disableWhiteout(); break; case 'editText': disableEditText(); break; } switch (newMode) { case 'whiteout': enableWhiteout(); break; case 'editText': enableEditText(); break; } mode = newMode } document.querySelectorAll('#page-container img').forEach(image => { image.addEventListener('click', () => { const selection = getSelection() selection.removeAllRanges() const range = document.createRange() range.selectNode(image) selection.addRange(range) }) }) saveButton.addEventListener('click', () => { htmlInput.value = '' htmlInput.value = document.documentElement.outerHTML filenameInput.value = filename saveForm.submit() htmlInput.value = '' }) const save = () => new Promise((resolve, reject) => { document.documentElement.classList.add('saving') const req = new XMLHttpRequest() req.open('POST', 'save', true) req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') req.responseType = 'arraybuffer' htmlInput.value = '' const body = `filename=${encodeURIComponent(filename)}&html=${encodeURIComponent(document.documentElement.outerHTML)}` req.onload = () => { if (req.status > 299) { reject(new Error(req.statusText)) } else { resolve(req.response) } } req.onerror = (ev) => { reject(ev.error) } req.send(body) }).finally(() => { document.documentElement.classList.remove('saving') }) // Get screen to print ratio /* The editing process deals with documents in several different dimensions and units of distance: - Paper size (A4, Legal, etc) - Print size (Font size in points, applied using the '@media print' rule) - Screen size (Font size in pixels, applied using the '@media screen' rule) - Zoom (Display transformation of the document preview, applied using transform-matrix) - Browser size (1:1 ratio for the browser itself) The following code reads evidence of these different values from the CSS, and determines how to transform from browser vector space, to preview vector space, to print vector space, so our edits and the preview are accurately represented in the resulting PDF. */ const localStyleSheets = Array.from(document.styleSheets).filter(x => !x.href) const pageOnePixelWidth = localStyleSheets.map(ss => Array.from(ss.rules) .find(rule => rule.selectorText === '.w0' && rule.style.width.endsWith('px')) ).find(x => x).style.width.split('px')[0] const pageOnePointWidth = localStyleSheets.map(ss => Array.from(ss.rules) .filter(rule => rule instanceof CSSMediaRule && rule.conditionText === 'print') .map(mediaRule => Array.from(mediaRule.cssRules) .filter(rule => rule.selectorText === '.w0' && rule.style.width.endsWith('pt')) .find(x => x) ).find(x => x) ).find(x => x).style.width.split('pt')[0] const pixelsPerPoint = 1.3333333333333333 const pageOnePrintPixelWidth = pageOnePointWidth * pixelsPerPoint const screenToPrintRatio = pageOnePrintPixelWidth / pageOnePixelWidth const printToScreenRatio = pageOnePixelWidth / pageOnePrintPixelWidth // Replacements are constructed in print vector space, so they must be scaled down in screen. const myStyle = document.createElement('style') myStyle.type = 'text/css' myStyle.innerHTML = ` @media screen { .replacement { zoom: ${printToScreenRatio}; } } ` document.head.appendChild(myStyle) // Whiteout let start = null const box = document.createElement('div') box.className = 'whiteout-box' whiteoutButton.addEventListener('click', () => toggleMode('whiteout')) const enableWhiteout = () => { whiteoutButton.classList.add('active') pageContainer.classList.add('whiteout') pageContainer.addEventListener('mousedown', whiteoutMouseDown) } const disableWhiteout = () => { whiteoutButton.classList.remove('active') pageContainer.classList.remove('whiteout') pageContainer.removeEventListener('mousedown', whiteoutMouseDown) } const whiteoutMouseDown = event => { start = {x: event.clientX, y: event.clientY} document.body.appendChild(box) drawBox(start, start) window.addEventListener('mousemove', whiteoutMouseMove) window.addEventListener('mouseup', whiteoutMouseUp) } const whiteoutMouseMove = event => { let end = {x: event.clientX, y: event.clientY} drawBox(rect(start, end)) } const whiteoutMouseUp = event => { let end = {x: event.clientX, y: event.clientY} document.body.removeChild(box) const selection = rect(start, end) console.log('whiteout', selection) whiteout(selection) window.removeEventListener('mousemove', whiteoutMouseMove) window.removeEventListener('mouseup', whiteoutMouseUp) } const rect = (a, b) => { const left = Math.min(a.x, b.x) const top = Math.min(a.y, b.y) const right = Math.max(a.x, b.x) const bottom = Math.max(a.y, b.y) return new DOMRect(left, top, right - left, bottom - top) } const drawBox = rect => { box.style.cssText = ` top: ${rect.top - 1}px; left: ${rect.left - 1}px; width: ${rect.width}px; height: ${rect.height}px; ` } const intersects = (r1, r2) => !(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top) /* The common whiteout approach of drawing an opaque white box over sensitive content is quite ineffective. A user can simply select the text from behind the whiteout, and copy & paste it into another program. This method finds every text character within the selected rectangle, and replaces it with a placeholder. It also modifies the background image by drawing a white box, replacing the pixels within the rectangle. */ const whiteout = rect => { const elements = [] const walk = (element) => { let elementRect if (element instanceof HTMLImageElement) { elementRect = element.getBoundingClientRect() } else { const range = document.createRange() range.selectNodeContents(element) elementRect = range.getBoundingClientRect() } if (intersects(rect, elementRect)) { if (element.childNodes && element.childNodes.length) { Array.from(element.childNodes).forEach(walk) } else if (element instanceof Text && element.textContent.length > 1) { while (element.textContent.length) { const next = element.splitText(1) walk(element) element = next } } else { elements.push(element) } } } walk(pageContainer) elements.forEach(element => { if (element instanceof Text) { const range = document.createRange() range.selectNodeContents(element) const elementRect = range.getBoundingClientRect() const replacement = document.createElement('span') replacement.className = 'replacement' replacement.style.cssText = ` width: ${elementRect.width * zoomRatio * screenToPrintRatio}px; display: inline-block; position: relative; ` element.replaceWith(replacement) } else if (element instanceof HTMLImageElement) { const canvas = document.createElement('canvas') canvas.width = element.offsetWidth * zoomRatio canvas.height = element.offsetHeight * zoomRatio const ctx = canvas.getContext('2d') ctx.fillStyle = 'white' ctx.drawImage(element, 0, 0, element.offsetWidth * zoomRatio, element.offsetHeight * zoomRatio) const elementRect = element.getBoundingClientRect() ctx.fillRect( (rect.x - elementRect.x) * zoomRatio, (rect.y - elementRect.y) * zoomRatio, rect.width * zoomRatio, rect.height * zoomRatio ) element.src = canvas.toDataURL() } }) window.editor.changed = true } // Edit Text editTextButton.addEventListener('click', () => toggleMode('editText')) const enableEditText = () => { editTextButton.classList.add('active') pageContainer.classList.add('editText') document.querySelectorAll('.pc').forEach(page => { page.setAttribute('contenteditable', 'true') }) window.editor.changed = true } const disableEditText = () => { editTextButton.classList.remove('active') pageContainer.classList.remove('editText') document.querySelectorAll('.pc').forEach(page => { page.setAttribute('contenteditable', 'false') }) } const attach = hostController => { host = hostController } window.editor = { attach, save, changed: false } // Debugging purposes: Display page CSS rules const rules = Array.from(document.styleSheets).filter(x => !x.href).map(x => Array.from(x.cssRules)).reduce((a, b) => [...a, ...b], []) const pageRules = rules.filter(x => x.constructor.name == 'CSSPageRule').map(x => x.cssText + rules.filter(y => y.parentRule === x).map(x => x.cssText).join('\n')).join('\n') console.log(pageRules) console.log(rules.filter(x => x.cssText.length < 2000).map(x => x.cssText).join('\n')) })