You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Clipper: Support clipping screenshots
This commit is contained in:
		| @@ -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", | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 = ( | ||||
| 					<div className="Preview"> | ||||
| 						<input className={"Title"} value={content.title} onChange={this.contentTitle_change}/> | ||||
| 						<div className={"Body"} dangerouslySetInnerHTML={{__html: content.bodyHtml}}></div> | ||||
| 						<div className={"BodyWrapper"}> | ||||
| 							<div className={"Body"} dangerouslySetInnerHTML={{__html: content.bodyHtml}}></div> | ||||
| 						</div> | ||||
| 						<a className={"Confirm Button"} onClick={this.confirm_click}>Confirm</a> | ||||
| 					</div> | ||||
| 				); | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 }; | ||||
| @@ -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) => { | ||||
|   | ||||
| @@ -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'); } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user