| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- 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'))
- })
|