Sfoglia il codice sorgente

Fix scaling issues. Detect screen to print ratios from CSS.

Alan Colon 7 anni fa
parent
commit
6a09429993
6 ha cambiato i file con 1127 aggiunte e 76 eliminazioni
  1. 785 46
      package-lock.json
  2. 5 1
      package.json
  3. 53 14
      public/edit.css
  4. 28 0
      public/edit.html
  5. 197 4
      public/edit.js
  6. 59 11
      server.js

File diff suppressed because it is too large
+ 785 - 46
package-lock.json


+ 5 - 1
package.json

@@ -11,6 +11,10 @@
   "dependencies": {
     "body-parser": "^1.18.3",
     "express": "^4.16.3",
-    "multer": "^1.3.1"
+    "html-pdf-chrome": "^0.5.0",
+    "multer": "^1.3.1",
+    "phantom": "^6.0.3",
+    "phantom-html2pdf": "^4.0.1",
+    "uuid": "^3.3.2"
   }
 }

+ 53 - 14
public/edit.css

@@ -1,16 +1,55 @@
-body {
-    caret-color: black;
-}
+@media screen {
+    body {
+        caret-color: black;
+    }
+
+    .c {
+        pointer-events: none;
+    }
+    .c .t {
+        pointer-events: all;
+    }
+    img {
+        cursor: default;
+        pointer-events: none;
+    }
+    #page-container, #sidebar {
+        padding-top: 44px;
+    }
+    #page-container ::selection {
+        background-color: #08f;
+    }
+
+    .toolbar {
+        position: fixed;
+        top: 0px;
+        left: 0px;
+        right: 0px;
+    }
+
+    .btn svg {
+        fill: currentColor;
+        width: 1.5em;
+        height: 1.5em;
+    }
+
+    /* Whiteout */
+    .whiteout .pc, .whiteout .pc img {
+        cursor: crosshair !important;
+    }
+    .whiteout-box {
+        background-color: rgba(255, 255, 255, 0.75);
+        border: dashed 1px black;
+        position: absolute;
+        pointer-events: none;
+    }
+
+    /* Edit Text */
+    .pf {
+        user-select: none;
+    }
+    .editText .pf {
+        user-select: auto;
+    }
 
-.c {
-    pointer-events: none;
-}
-.c .t {
-    pointer-events: all;
-}
-img {
-    cursor: default;
 }
-#page-container ::selection {
-    background-color: #08f;
-}

+ 28 - 0
public/edit.html

@@ -0,0 +1,28 @@
+<script src="edit.js" type="text/javascript"></script>
+<link rel="stylesheet" href="edit.css" />
+<link rel="stylesheet" href="https://toert.github.io/Isolated-Bootstrap/versions/4.1.0/iso_bootstrap4.1.0min.css" />
+<style>
+    @media print {
+        .bootstrap {
+            display: none;
+        }
+    }    
+</style>
+<div class="bootstrap">
+    <div class="toolbar">
+        <div class="btn-group">
+            <button id="saveButton" class="btn btn-secondary" title="Save PDF">
+                <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"><g><g><path d="M38,32.4c0.7-0.1,1.7-0.2,3.1-0.2c1.4,0,2.4,0.3,3.1,0.8c0.6,0.5,1.1,1.3,1.1,2.3s-0.3,1.8-0.9,2.4    c-0.8,0.7-1.9,1.1-3.3,1.1c-0.3,0-0.6,0-0.8,0v3.6H38V32.4z M40.3,37c0.2,0,0.4,0.1,0.8,0.1c1.2,0,2-0.6,2-1.6    c0-0.9-0.6-1.5-1.8-1.5c-0.5,0-0.8,0-0.9,0.1V37z"/><path d="M46.7,32.4c0.8-0.1,1.9-0.2,3.1-0.2c1.9,0,3.2,0.3,4.1,1.1c1,0.8,1.7,2,1.7,3.8c0,1.9-0.7,3.3-1.7,4.1    c-1.1,0.9-2.7,1.3-4.7,1.3c-1.2,0-2-0.1-2.6-0.1V32.4z M49,40.6c0.2,0,0.5,0,0.8,0c2.1,0,3.4-1.1,3.4-3.5c0-2.1-1.2-3.2-3.2-3.2    c-0.5,0-0.8,0-1,0.1V40.6z"/><path d="M57.1,32.2h6.2v1.9h-3.9v2.3h3.6v1.9h-3.6v4.1h-2.3V32.2z"/></g><g><path d="M88.9,25.9L67.4,4.4c-1-1-2.3-1.5-3.7-1.5H16.5c-3.3,0-6,2.7-6,6v82c0,3.3,2.7,6,6,6h68c3.3,0,6-2.7,6-6v-61    C90.5,28.4,89.9,26.9,88.9,25.9z M50.5,83.3L39,71.8h5.9V53.2h11.3v18.6h5.9L50.5,83.3z M77.4,44.7c0,1.2-1,2.1-2.1,2.1H25.8    c-1.2,0-2.1-1-2.1-2.1V31.2c0-1.2,1-2.1,2.1-2.1h49.5c1.2,0,2.1,1,2.1,2.1V44.7z"/></g></g></svg>
+            </button>
+            <button id="whiteoutButton" class="btn btn-secondary" title="Whiteout">
+                <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" preserveAspectRatio="none" x="0px" y="0px" viewBox="0 0 100 100"><defs><g id="a"><path stroke="none" d=" M 53.9 88.85 L 53.9 96.65 67.45 96.65 67.45 88.85 53.9 88.85 M 32.5 88.85 L 32.5 96.65 46.1 96.65 46.1 88.85 32.5 88.85 M 11.15 75.25 L 3.35 75.25 3.35 96.65 24.7 96.65 24.7 88.85 11.15 88.85 11.15 75.25 M 11.15 53.9 L 3.35 53.9 3.35 67.45 11.15 67.45 11.15 53.9 M 3.35 46.1 L 11.15 46.1 11.15 32.5 3.35 32.5 3.35 46.1 M 24.7 11.15 L 24.7 3.35 3.35 3.35 3.35 24.7 11.15 24.7 11.15 11.15 24.7 11.15 M 46.1 11.15 L 46.1 3.35 32.5 3.35 32.5 11.15 46.1 11.15 M 67.45 11.15 L 67.45 3.35 53.9 3.35 53.9 11.15 67.45 11.15 M 96.65 75.25 L 88.85 75.25 88.85 88.85 75.25 88.85 75.25 96.65 96.65 96.65 96.65 75.25 M 96.65 53.9 L 88.85 53.9 88.85 67.45 96.65 67.45 96.65 53.9 M 88.85 24.7 L 96.65 24.7 96.65 3.35 75.25 3.35 75.25 11.15 88.85 11.15 88.85 24.7 M 88.85 32.5 L 88.85 46.1 96.65 46.1 96.65 32.5 88.85 32.5 Z"/></g></defs><g transform="matrix( 1, 0, 0, 1, 0,0) "><use xlink:href="#a"/></g><g transform="matrix( 1, 0, 0, 1, -0.2,0) "></g></svg>
+            </button>
+            <button id="editTextButton" class="btn btn-secondary" title="Edit Text">
+                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" x="0px" y="0px"><g><path d="M15 2h-4c-.553 0-1-.448-1-1s.447-1 1-1h4c.553 0 1 .448 1 1s-.447 1-1 1zM13 16c-.553 0-1-.447-1-1v-14c0-.552.447-1 1-1s1 .448 1 1v14c0 .553-.447 1-1 1zM15 16h-4c-.553 0-1-.447-1-1s.447-1 1-1h4c.553 0 1 .447 1 1s-.447 1-1 1zM9.001 13c-.384 0-.749-.221-.915-.594l-3.086-6.944-3.086 6.944c-.224.504-.815.731-1.32.508-.504-.225-.732-.815-.507-1.32l4-9c.16-.361.518-.594.913-.594s.753.233.914.594l4 9c.224.505-.003 1.096-.508 1.32-.132.059-.269.086-.405.086zM8 11h-6c-.552 0-1-.447-1-1s.448-1 1-1h6c.552 0 1 .447 1 1s-.448 1-1 1z"/></g></svg>
+            </button>
+        </div>
+    </div>
+</div>
+<form style="display: none" action="save" method="POST" id="saveForm">
+    <input type="hidden" name="html" id="htmlInput" />
+</form>

+ 197 - 4
public/edit.js

@@ -1,8 +1,26 @@
 document.addEventListener('DOMContentLoaded', () => {
-    // Make page content editable
-    document.querySelectorAll('.pf').forEach(page => {
-        page.setAttribute('contenteditable', 'true')
-    })
+    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 htmlInput = document.getElementById('htmlInput')
+    const pageContainer = document.getElementById('page-container')
+
+    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', () => {
@@ -14,4 +32,179 @@ document.addEventListener('DOMContentLoaded', () => {
             console.log('selected?')
         })
     })
+
+    saveButton.addEventListener('click', () => {
+        htmlInput.value = document.documentElement.outerHTML
+        saveForm.submit()
+    })
+
+    // Get screen to print ratio
+    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
+
+    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)
+
+    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()
+            }
+        })
+    }
+
+    // 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')
+        })
+    }
+
+    const disableEditText = () => {
+        editTextButton.classList.remove('active')
+        pageContainer.classList.remove('editText')
+        document.querySelectorAll('.pc').forEach(page => {
+            page.setAttribute('contenteditable', 'false')
+        })
+    }
 })

+ 59 - 11
server.js

@@ -2,34 +2,82 @@ const fs = require('fs')
 const express = require('express')
 const multer = require('multer')
 const childProcess = require('child_process')
+const bodyParser = require('body-parser')
 const PDF2HTMLEX_PATH = 'C:/Users/alan.colon/Downloads/pdf2htmlEX-win32-0.14.6-upx-with-poppler-data/pdf2htmlEX.exe'
+const phantom = require('phantom')
+const uuid = require('uuid')
 
 const upload = multer({
     dest: 'temp/'
 })
 
 const app = express()
-
+app.use(bodyParser.urlencoded({extended: false, limit: '100mb'}))
 app.use(express.static('./public'))
 
 app.post('/edit', upload.single('document'), (req, res) => {
-    childProcess.exec(`"${PDF2HTMLEX_PATH}" --no-drm 1 "${req.file.path}" "${req.file.path}.html"`, (err, stdout, stderr) => {
+    const pdfFile = req.file.path
+    const htmlFile = `${req.file.path}.html`
+    childProcess.exec(`"${PDF2HTMLEX_PATH}" --hdpi 200 --vdpi 200 --font-format ttf --no-drm 1 "${pdfFile}" "${htmlFile}"`, (err, stdout, stderr) => {
         if (err) {
             res.status(500).send(`<pre>${err}\n\n${stdout}\n\n${stderr}</pre>`)
         } else {
-            fs.readFile(`${req.file.path}.html`, 'utf8', (err, data) => {
-                if (err) {
-                    res.status(500).send(`<pre>${err}\n\n${stdout}\n\n${stderr}</pre>`)
-                } else {
-                    res.status(200).send(data.replace('</head>', `
-                        <script src="edit.js" type="text/javascript"></script>
-                        <link rel="stylesheet" href="edit.css" />
-                    </head>`))
-                }
+            fs.readFile('public/edit.html', (err, editHtml) => {
+                fs.readFile(`${req.file.path}.html`, 'utf8', (err, data) => {
+                    if (err) {
+                        res.status(500).send(`<pre>${err}\n\n${stdout}\n\n${stderr}</pre>`)
+                    } else {
+                        res.status(200).send(data.replace('</body>', `${editHtml}</body>`))
+                    }
+                    fs.unlink(pdfFile)
+                    fs.unlink(htmlFile)
+                })
             })
         }
     })
 })
 
+
+
+app.post('/save', (req, res) => {
+    console.log(req.body.html)
+
+    const tmpUuid = uuid()
+    const htmlPath = `temp/${tmpUuid}.html`
+    const pdfPath = `temp/${tmpUuid}.pdf`
+
+    const pageWidth = /\.w0{width:([\d\.]*)pt/.exec(req.body.html)[1]
+    const pageHeight = /\.h0{height:([\d\.]*)pt/.exec(req.body.html)[1]
+
+
+    fs.writeFile(htmlPath, req.body.html, 'utf8', async (err) => {
+        const instance = await phantom.create()
+
+        const page = await instance.createPage()
+        page.property('paperSize', {
+            width: pageWidth,
+            height: pageHeight,
+            margin: '0px'
+        })
+        page.open(htmlPath)
+        page.on('onLoadFinished', function(status) {
+            if (status === 'success') {
+                page.render(pdfPath, {format: 'pdf'})
+                setTimeout(() => {
+                    res.download(pdfPath, 'todo-preserve-filename.pdf', (err) => {
+                        if (err) {
+                            console.error(err)
+                            res.status(500).send(err)
+                        }
+                        fs.unlink(htmlPath)
+                        //fs.unlink(pdfPath)
+                    })
+                }, 5000)
+            } else {
+                res.status(500).send('Failed: ' + status)
+            }
+        })
+    })
+})
 app.listen('3003')
 console.log('http://localhost:3003')

Some files were not shown because too many files changed in this diff