From 264ee4f3194339ec5f29ef950a23f1ecbdcbb2dc Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Fri, 25 May 2018 08:51:54 +0100 Subject: [PATCH] Clipper: Support clipping screenshots --- Clipper/joplin-webclipper/background.js | 10 +-- .../content_scripts/index.js | 68 +++++++++++++------ Clipper/joplin-webclipper/popup/src/App.css | 19 ++++-- Clipper/joplin-webclipper/popup/src/App.js | 6 +- ReactNativeClient/lib/ClipperServer.js | 19 ++++++ ReactNativeClient/lib/mime-utils.js | 11 +++ ReactNativeClient/lib/shim-init-node.js | 42 ++++++++---- ReactNativeClient/lib/shim.js | 1 + 8 files changed, 132 insertions(+), 44 deletions(-) diff --git a/Clipper/joplin-webclipper/background.js b/Clipper/joplin-webclipper/background.js index 8f5fc1f619..63636f1f10 100644 --- a/Clipper/joplin-webclipper/background.js +++ b/Clipper/joplin-webclipper/background.js @@ -8,10 +8,10 @@ if (typeof browser !== 'undefined') { } async function browserCaptureVisibleTabs(windowId, options) { - return new Promise((resolve, reject) => { - if (browserSupportsPromises_) return browser_.tabs.captureVisibleTab(null, { format: 'jpeg' }); + if (browserSupportsPromises_) return browser_.tabs.captureVisibleTab(windowId, { format: 'jpeg' }); - browser_.tabs.captureVisibleTab(null, { format: 'jpeg' }, (image) => { + return new Promise((resolve, reject) => { + browser_.tabs.captureVisibleTab(windowId, { format: 'jpeg' }, (image) => { resolve(image); }); }); @@ -23,9 +23,9 @@ chrome.runtime.onInstalled.addListener(function() { browser_.runtime.onMessage.addListener((command) => { if (command.name === 'screenshotArea') { - browserCaptureVisibleTabs(null, { format: 'jpeg' }).then((image) => { + browserCaptureVisibleTabs(null, { format: 'jpeg' }).then((imageDataUrl) => { content = Object.assign({}, command.content); - content.imageBase64 = image; + content.imageDataUrl = imageDataUrl; fetch(command.apiBaseUrl + "/notes", { method: "POST", diff --git a/Clipper/joplin-webclipper/content_scripts/index.js b/Clipper/joplin-webclipper/content_scripts/index.js index 5562d8fba9..0e43b98220 100644 --- a/Clipper/joplin-webclipper/content_scripts/index.js +++ b/Clipper/joplin-webclipper/content_scripts/index.js @@ -112,7 +112,7 @@ } else if (command.name === 'screenshot') { const overlay = document.createElement('div'); - overlay.style.opacity = '0.5'; + overlay.style.opacity = '0.4'; overlay.style.background = 'black'; overlay.style.width = '100%'; overlay.style.height = '100%'; @@ -123,9 +123,30 @@ document.body.appendChild(overlay); + const messageComp = document.createElement('div'); + + const messageCompWidth = 300; + messageComp.style.position = 'fixed' + messageComp.style.opacity = '0.9' + messageComp.style.width = messageCompWidth + 'px' + messageComp.style.maxWidth = messageCompWidth + 'px' + messageComp.style.border = '1px solid black' + messageComp.style.background = 'white' + messageComp.style.top = '10px' + messageComp.style.textAlign = 'center'; + messageComp.style.padding = '6px' + messageComp.style.left = Math.round(document.body.clientWidth / 2 - messageCompWidth / 2) + 'px' + messageComp.style.zIndex = overlay.style.zIndex + 1 + + messageComp.textContent = 'Drag and release to capture a screenshot'; + + document.body.appendChild(messageComp); + const selection = document.createElement('div'); - selection.style.opacity = '0.5'; - selection.style.background = 'blue'; + selection.style.opacity = '0.4'; + selection.style.border = '1px solid red'; + selection.style.background = 'white'; + selection.style.border = '2px solid black'; selection.style.zIndex = overlay.style.zIndex - 1; selection.style.top = 0; selection.style.left = 0; @@ -138,21 +159,21 @@ let selectionArea = {}; function updateSelection() { - selection.style.left = selectionArea.x; - selection.style.top = selectionArea.y; - selection.style.width = selectionArea.width; - selection.style.height = selectionArea.height; + selection.style.left = selectionArea.x + 'px'; + selection.style.top = selectionArea.y + 'px'; + selection.style.width = selectionArea.width + 'px'; + selection.style.height = selectionArea.height + 'px'; } function setSelectionSizeFromMouse(event) { - selectionArea.width = Math.max(1, event.pageX - draggingStartPos.x); - selectionArea.height = Math.max(1, event.pageY - draggingStartPos.y); + selectionArea.width = Math.max(1, event.clientX - draggingStartPos.x); + selectionArea.height = Math.max(1, event.clientY - draggingStartPos.y); updateSelection(); } function selection_mouseDown(event) { - selectionArea = { x: event.pageX - document.body.scrollLeft, y: event.pageY - document.body.scrollTop, width: 0, height: 0 } - draggingStartPos = { x: event.pageX, y: event.pageY }; + selectionArea = { x: event.clientX, y: event.clientY, width: 0, height: 0 } + draggingStartPos = { x: event.clientX, y: event.clientY }; isDragging = true; updateSelection(); } @@ -173,18 +194,23 @@ document.body.removeChild(overlay); document.body.removeChild(selection); + document.body.removeChild(messageComp); - const content = { - title: pageTitle(), - area: selectionArea, - url: location.origin + location.pathname, - }; + if (!selectionArea || !selectionArea.width || !selectionArea.height) return; - browser_.runtime.sendMessage({ - name: 'screenshotArea', - content: content, - apiBaseUrl: command.apiBaseUrl, - }); + setTimeout(() => { + const content = { + title: pageTitle(), + cropRect: selectionArea, + url: location.origin + location.pathname, + }; + + browser_.runtime.sendMessage({ + name: 'screenshotArea', + content: content, + apiBaseUrl: command.apiBaseUrl, + }); + }, 10); } overlay.addEventListener('mousedown', selection_mouseDown); diff --git a/Clipper/joplin-webclipper/popup/src/App.css b/Clipper/joplin-webclipper/popup/src/App.css index ab7644a6f8..4edd5aac39 100644 --- a/Clipper/joplin-webclipper/popup/src/App.css +++ b/Clipper/joplin-webclipper/popup/src/App.css @@ -77,17 +77,26 @@ margin-bottom: 10px; } -.App .Preview .Body { +.App .Preview .BodyWrapper { flex: 1; + overflow: hidden; + flex-shrink: 1; + min-width: auto; +} + +.App .Preview .Body { + /*flex: 1;*/ font-size: .5em; overflow-x: hidden; overflow-y: scroll; overflow-wrap: break-word; background-color: #ffffff; - flex-shrink: 1; - min-width: auto; - padding: 10px; - margin-bottom: 10px; + /*flex-shrink: 1;*/ + /*min-width: auto;*/ + /*padding: 10px;*/ + /*margin-bottom: 10px;*/ + width: 100%; + height: 100%; } .App .Preview .Confirm { diff --git a/Clipper/joplin-webclipper/popup/src/App.js b/Clipper/joplin-webclipper/popup/src/App.js index ac21f4f851..fb8e50c086 100644 --- a/Clipper/joplin-webclipper/popup/src/App.js +++ b/Clipper/joplin-webclipper/popup/src/App.js @@ -43,6 +43,8 @@ class AppComponent extends Component { name: 'screenshot', apiBaseUrl: 'http://127.0.0.1:9967', }); + + window.close(); } async loadContentScripts() { @@ -91,7 +93,9 @@ class AppComponent extends Component { previewComponent = (
-
+
+
+
Confirm
); diff --git a/ReactNativeClient/lib/ClipperServer.js b/ReactNativeClient/lib/ClipperServer.js index a12b04a1f1..e42050c876 100644 --- a/ReactNativeClient/lib/ClipperServer.js +++ b/ReactNativeClient/lib/ClipperServer.js @@ -10,6 +10,7 @@ const { fileExtension, safeFileExtension, safeFilename, filename } = require('li const HtmlToMd = require('lib/HtmlToMd'); const { Logger } = require('lib/logger.js'); const markdownUtils = require('lib/markdownUtils'); +const mimeUtils = require('lib/mime-utils.js').mime; class ClipperServer { @@ -59,6 +60,19 @@ class ClipperServer { return output; } + // Note must have been saved first + async attachImageFromDataUrl_(note, imageDataUrl, cropRect) { + const tempDir = Setting.value('tempDir'); + const mime = mimeUtils.fromDataUrl(imageDataUrl); + let ext = mimeUtils.toFileExtension(mime) || ''; + if (ext) ext = '.' + ext; + const tempFilePath = tempDir + '/' + md5(Math.random() + '_' + Date.now()) + ext; + const imageConvOptions = {}; + if (cropRect) imageConvOptions.cropRect = cropRect; + await shim.imageFromDataUrl(imageDataUrl, tempFilePath, imageConvOptions); + return await shim.attachFileToNote(note, tempFilePath); + } + async downloadImage_(url) { const tempDir = Setting.value('tempDir'); const name = filename(url); @@ -189,6 +203,11 @@ class ClipperServer { note.body = this.replaceImageUrlsByResources_(note.body, result); note = await Note.save(note); + + if (requestNote.imageDataUrl) { + await this.attachImageFromDataUrl_(note, requestNote.imageDataUrl, requestNote.cropRect); + } + this.logger().info('Request (' + requestId + '): Created note ' + note.id); return writeResponseJson(200, note); } catch (error) { diff --git a/ReactNativeClient/lib/mime-utils.js b/ReactNativeClient/lib/mime-utils.js index ca4c8395e7..95339448ab 100644 --- a/ReactNativeClient/lib/mime-utils.js +++ b/ReactNativeClient/lib/mime-utils.js @@ -29,6 +29,17 @@ const mime = { return null; }, + fromDataUrl(dataUrl) { + // Example: data:image/jpeg;base64,/9j/4AAQSkZJR..... + const defaultMime = 'text/plain'; + let p = dataUrl.substr(0, dataUrl.indexOf(',')).split(';'); + let s = p[0]; + s = s.split(':'); + if (s.length <= 1) return defaultMime; + s = s[1]; + return s.indexOf('/') >= 0 ? s : defaultMime; // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs + }, + } module.exports = { mime }; \ No newline at end of file diff --git a/ReactNativeClient/lib/shim-init-node.js b/ReactNativeClient/lib/shim-init-node.js index 5047df1c0f..c1c0ab8525 100644 --- a/ReactNativeClient/lib/shim-init-node.js +++ b/ReactNativeClient/lib/shim-init-node.js @@ -5,6 +5,7 @@ const { FileApiDriverLocal } = require('lib/file-api-driver-local.js'); const { time } = require('lib/time-utils.js'); const { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js'); const { FsDriverNode } = require('lib/fs-driver-node.js'); +const mimeUtils = require('lib/mime-utils.js').mime; const urlValidator = require('valid-url'); function shimInit() { @@ -35,21 +36,24 @@ function shimInit() { return locale; } - // For Electron only shim.writeImageToFile = async function(nativeImage, mime, targetPath) { - let buffer = null; + if (shim.isElectron()) { // For Electron + let buffer = null; - mime = mime.toLowerCase(); + mime = mime.toLowerCase(); - if (mime === 'image/png') { - buffer = nativeImage.toPNG(); - } else if (mime === 'image/jpg' || mime === 'image/jpeg') { - buffer = nativeImage.toJPEG(90); + if (mime === 'image/png') { + buffer = nativeImage.toPNG(); + } else if (mime === 'image/jpg' || mime === 'image/jpeg') { + buffer = nativeImage.toJPEG(90); + } + + if (!buffer) throw new Error('Cannot resize image because mime type "' + mime + '" is not supported: ' + targetPath); + + await shim.fsDriver().writeFile(targetPath, buffer, 'buffer'); + } else { + throw new Error('Node support not implemented'); } - - if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath); - - await shim.fsDriver().writeFile(targetPath, buffer, 'buffer'); } const resizeImage_ = async function(filePath, targetPath, mime) { @@ -146,7 +150,7 @@ function shimInit() { } shim.attachFileToNote = async function(note, filePath, position = null) { - const resource = shim.createResourceFromPath(filePath); + const resource = await shim.createResourceFromPath(filePath); const newBody = []; @@ -164,6 +168,20 @@ function shimInit() { return await Note.save(newNote); } + shim.imageFromDataUrl = async function(imageDataUrl, filePath, options = null) { + if (options === null) options = {}; + + if (shim.isElectron()) { + const nativeImage = require('electron').nativeImage; + let image = nativeImage.createFromDataURL(imageDataUrl); + if (options.cropRect) image = image.crop(options.cropRect); + const mime = mimeUtils.fromDataUrl(imageDataUrl); + await shim.writeImageToFile(image, mime, filePath); + } else { + throw new Error('Node support not implemented'); + } + } + const nodeFetch = require('node-fetch'); shim.readLocalFileBase64 = (path) => { diff --git a/ReactNativeClient/lib/shim.js b/ReactNativeClient/lib/shim.js index 130ed5d856..90a5a1fc93 100644 --- a/ReactNativeClient/lib/shim.js +++ b/ReactNativeClient/lib/shim.js @@ -136,6 +136,7 @@ shim.clearInterval = function(id) { shim.stringByteLength = function(string) { throw new Error('Not implemented'); } shim.detectAndSetLocale = null; shim.attachFileToNote = async (note, filePath) => {} +shim.imageFromDataUrl = async function(imageDataUrl, filePath, options = null) { throw new Error('Not implemented') } shim.Buffer = null; shim.openUrl = () => { throw new Error('Not implemented'); }