diff options
author | bobzel <zzzman@gmail.com> | 2023-12-29 01:01:55 -0500 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2023-12-29 01:01:55 -0500 |
commit | 865478933af8166cb699cbe4a2653aa52423dedb (patch) | |
tree | 57dedd5dfe4349432a7c445f408f9e675f1f53df /src | |
parent | df2fc3f11e3b474144db5062620c9f65ca857203 (diff) |
fixed dropping images from web. fixed exif data on images and autorotating exif rotated images. fixed selecting on web pages, and resizing web pages upward so that pointer events aren't grabbed.
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/youtube/YoutubeBox.tsx | 5 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 1 | ||||
-rw-r--r-- | src/client/util/Import & Export/ImageUtils.ts | 41 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 12 | ||||
-rw-r--r-- | src/client/views/PreviewCursor.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 31 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 12 | ||||
-rw-r--r-- | src/client/views/webcam/DashWebRTCVideo.tsx | 5 | ||||
-rw-r--r-- | src/server/DashUploadUtils.ts | 46 |
12 files changed, 91 insertions, 71 deletions
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index ceb80acbc..d3a15cd84 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -11,6 +11,7 @@ import { FieldView, FieldViewProps } from '../../views/nodes/FieldView'; import '../../views/nodes/WebBox.scss'; import './YoutubeBox.scss'; import * as React from 'react'; +import { SnappingManager } from '../../util/SnappingManager'; interface VideoTemplate { thumbnailUrl: string; @@ -355,9 +356,9 @@ export class YoutubeBox extends React.Component<FieldViewProps> { </div> ); - const frozen = !this.props.isSelected() || DocumentView.Interacting; + const frozen = !this.props.isSelected() || SnappingManager.IsResizing; - const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentView.Interacting ? '-interactive' : ''); + const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !SnappingManager.IsResizing ? '-interactive' : ''); return ( <> <div className={classname}>{content}</div> diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 0a4d3a294..79285deb5 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1873,6 +1873,7 @@ export namespace DocUtils { proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); } + proto.data_exif = JSON.stringify(result.exifData?.data); proto.data_contentSize = result.contentSize; // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates const latitude = result.exifData?.data?.GPSLatitude; diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 55d37f544..d99828956 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -4,28 +4,33 @@ import { Cast, StrCast, NumCast } from '../../../fields/Types'; import { Networking } from '../../Network'; import { Id } from '../../../fields/FieldSymbols'; import { Utils } from '../../../Utils'; +import { DocData } from '../../../fields/DocSymbols'; export namespace ImageUtils { - export const ExtractExif = async (document: Doc): Promise<boolean> => { + export type imgInfo = { + contentSize: number; + nativeWidth: number; + nativeHeight: number; + source: string; + exifData: { error: string | undefined; data: string }; + }; + export const ExtractImgInfo = async (document: Doc): Promise<imgInfo | undefined> => { const field = Cast(document.data, ImageField); - if (!field) { - return false; + return field ? await Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined; + }; + + export const AssignImgInfo = (document: Doc, data?: imgInfo) => { + if (data) { + data.nativeWidth && (document._height = (NumCast(document._width) * data.nativeHeight) / data.nativeWidth); + const proto = document[DocData]; + const field = Doc.LayoutFieldKey(document); + proto[`${field}_nativeWidth`] = data.nativeWidth; + proto[`${field}_nativeHeight`] = data.nativeHeight; + proto[`${field}_path`] = data.source; + proto[`${field}_exif`] = JSON.stringify(data.exifData.data); + proto[`${field}_contentSize`] = data.contentSize ? data.contentSize : undefined; } - const source = field.url.href; - const { - contentSize, - nativeWidth, - nativeHeight, - exifData: { error, data }, - } = await Networking.PostToServer('/inspectImage', { source }); - document.exif = error || Doc.Get.FromJson({ data }); - const proto = Doc.GetProto(document); - nativeWidth && (document._height = (NumCast(document._width) * nativeHeight) / nativeWidth); - proto['data_nativeWidth'] = nativeWidth; - proto['data_nativeHeight'] = nativeHeight; - proto['data-path'] = source; - proto.data_contentSize = contentSize ? contentSize : undefined; - return data !== undefined; + return document; }; export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 7003485d2..f3da133c3 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -310,7 +310,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora */ @action onRadiusDown = (e: React.PointerEvent): void => { - this._isRounding = DocumentView.Interacting = true; + SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()); + this._isRounding = true; this._resizeUndo = UndoManager.StartBatch('DocDecs set radius'); setupMoveUpEvents( this, @@ -327,7 +328,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora return false; }, action(e => { - DocumentView.Interacting = this._isRounding = false; + SnappingManager.SetIsResizing(undefined); + this._isRounding = false; this._resizeUndo?.end(); }), // upEvent emptyFunction, @@ -439,10 +441,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora @action onPointerDown = (e: React.PointerEvent): void => { - SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()); + SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); e.stopPropagation(); - DocumentView.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them this._resizeHdlId = e.currentTarget.className; const bounds = e.currentTarget.getBoundingClientRect(); this._offset = { x: this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX, y: this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY }; @@ -595,7 +596,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora onPointerUp = (e: PointerEvent): void => { SnappingManager.SetIsResizing(undefined); SnappingManager.clearSnapLines(); - DocumentView.Interacting = false; this._resizeHdlId = ''; this._resizeUndo?.end(); @@ -766,7 +766,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora top: bounds.y - this._resizeBorderWidth / 2, transformOrigin, background: SnappingManager.ShiftKey ? undefined : 'yellow', - pointerEvents: SnappingManager.ShiftKey || DocumentView.Interacting ? 'none' : 'all', + pointerEvents: SnappingManager.ShiftKey || SnappingManager.IsResizing ? 'none' : 'all', display: SelectionManager.Views.length <= 1 || hideDecorations ? 'none' : undefined, transform: `rotate(${rotation}deg)`, }} diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 5ec9a7d46..166afc0c2 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -102,7 +102,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> { x: newPoint[0], y: newPoint[1], }); - ImageUtils.ExtractExif(doc); + ImageUtils.ExtractImgInfo(doc); this._addDocument?.(doc); })(); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index b56973dc6..2ff435ce2 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -4,11 +4,11 @@ import * as rp from 'request-promise'; import { Utils, returnFalse } from '../../../Utils'; import CursorField from '../../../fields/CursorField'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; -import { AclPrivate } from '../../../fields/DocSymbols'; +import { AclPrivate, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; -import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; @@ -309,20 +309,19 @@ export function CollectionSubView<X>(moreProps?: X) { const cors = img.includes('corsProxy') ? img.match(/http.*corsProxy\//)![0] : ''; img = cors ? img.replace(cors, '') : img; if (img) { - const split = img.split('src="')[1].split('"')[0]; - let source = split; - if (split.startsWith('data:image') && split.includes('base64')) { - const [{ accessPaths }] = await Networking.PostToServer('/uploadRemoteImage', { sources: [split] }); - if (accessPaths.agnostic.client.indexOf('dashblobstore') === -1) { - source = Utils.prepend(accessPaths.agnostic.client); - } else { - source = accessPaths.agnostic.client; - } - } - if (source.startsWith('http')) { - const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 }); - ImageUtils.ExtractExif(doc); - addDocument(doc); + const imgSrc = img.split('src="')[1].split('"')[0]; + const imgOpts = { ...options, _width: 300 }; + if (imgSrc.startsWith('data:image') && imgSrc.includes('base64')) { + const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })).lastElement(); + const newImgSrc = + result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 // + ? Utils.prepend(result.accessPaths.agnostic.client) + : result.accessPaths.agnostic.client; + + addDocument(ImageUtils.AssignImgInfo(Docs.Create.ImageDocument(newImgSrc, imgOpts), result)); + } else if (imgSrc.startsWith('http')) { + const doc = Docs.Create.ImageDocument(imgSrc, imgOpts); + addDocument(ImageUtils.AssignImgInfo(doc, await ImageUtils.ExtractImgInfo(doc))); } return; } else { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 1574deede..645e9cff7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1190,7 +1190,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @computed get childPointerEvents() { const engine = this._props.layoutEngine?.() || StrCast(this._props.Document._layoutEngine); - const pointerevents = DocumentView.Interacting + const pointerevents = SnappingManager.IsResizing ? 'none' : this._props.childPointerEvents?.() ?? (this._props.viewDefDivClick || // @@ -1479,7 +1479,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._disposers.pointerevents = reaction( () => { const engine = this._props.layoutEngine?.() || StrCast(this._props.Document._layoutEngine); - return DocumentView.Interacting + return SnappingManager.IsResizing ? 'none' : this._props.childPointerEvents?.() ?? (this._props.viewDefDivClick || // diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2752fa7f5..823ec885b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1336,7 +1336,6 @@ export class DocumentView extends ObservableReactComponent<DocumentViewProps> { public set SELECTED(val) { runInAction(() => (this._selected = val)); } - @observable public static Interacting = false; @observable public static LongPress = false; @observable public static ExploreMode = false; @observable public static LastPressedSidebarBtn: Opt<Doc>; // bcz: this is a hack to handle highlighting buttons in the leftpanel menu .. need to find a cleaner approach diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index df73dffe4..f205dbd56 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -630,7 +630,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode)) ); this._disposers.youtubeReactionDisposer = reaction( - () => Doc.ActiveTool === InkTool.None && this._props.isSelected() && !SnappingManager.IsDragging && !DocumentView.Interacting, + () => Doc.ActiveTool === InkTool.None && this._props.isSelected() && !SnappingManager.IsDragging && !SnappingManager.IsResizing, interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'), { fireImmediately: true } ); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 2522a674d..758c919d2 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -220,7 +220,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } // else it's an HTMLfield } else if (this.webField && !this.dataDoc.text) { WebRequest.get(Utils.CorsProxy(this.webField.href)) // - .then(result => result && (this.dataDoc.text = htmlToText.convert(result.content))); + .then(result => result && (this.dataDoc.text = htmlToText(result.content))); } this._disposers.scrollReaction = reaction( @@ -491,12 +491,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.layoutDoc.height = Math.min(NumCast(this.layoutDoc._height), (NumCast(this.layoutDoc._width) * NumCast(this.Document.nativeHeight)) / NumCast(this.Document.nativeWidth)); } }; - const swidth = Math.max(NumCast(this.layoutDoc.nativeWidth), iframeContent.body.scrollWidth || 0); + const swidth = Math.max(NumCast(this.Document.nativeWidth), iframeContent.body.scrollWidth || 0); if (swidth) { - const aspectResize = swidth / NumCast(this.Document.nativeWidth); - this.Document.nativeWidth = swidth; - this.Document.nativeHeight = NumCast(this.Document.nativeHeight) * aspectResize; + const aspectResize = swidth / NumCast(this.Document.nativeWidth, swidth); this.layoutDoc.height = NumCast(this.layoutDoc._height) * aspectResize; + this.Document.nativeWidth = swidth; + this.Document.nativeHeight = (swidth * NumCast(this.layoutDoc._height)) / NumCast(this.layoutDoc._width); } initHeights(); this._iframetimeout && clearTimeout(this._iframetimeout); @@ -813,7 +813,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key={this._warning} className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} - style={{ pointerEvents: DocumentView.Interacting ? 'none' : undefined }} + style={{ pointerEvents: SnappingManager.IsResizing ? 'none' : undefined }} src={url} onLoad={this.iframeLoaded} scrolling="no" // ugh.. on windows, I get an inner scroll bar for the iframe's body even though the scrollHeight should be set to the full height of the document. diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index 093806127..f1739a41a 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -12,6 +12,7 @@ import { FieldView, FieldViewProps } from '../nodes/FieldView'; import './DashWebRTCVideo.scss'; import { hangup, initialize, refreshVideos } from './WebCamLogic'; import * as React from 'react'; +import { SnappingManager } from '../../util/SnappingManager'; /** * This models the component that will be rendered, that can be used as a doc that will reflect the video cams. @@ -71,8 +72,8 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV </div> ); - const frozen = !this.props.isSelected() || DocumentView.Interacting; - const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentView.Interacting ? '-interactive' : ''); + const frozen = !this.props.isSelected() || SnappingManager.IsResizing; + const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !SnappingManager.IsResizing ? '-interactive' : ''); return ( <> diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index a8e09818e..e2419e60a 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -26,6 +26,7 @@ import { ffprobe, FfmpegCommand } from 'fluent-ffmpeg'; import * as fs from 'fs'; import * as md5File from 'md5-file'; import * as autorotate from 'jpeg-autorotate'; +const { Duplex } = require('stream'); // Native Node Module export enum SizeSuffix { Small = '_s', @@ -349,7 +350,11 @@ export namespace DashUploadUtils { if (metadata instanceof Error) { return { name: metadata.name, message: metadata.message }; } - return UploadInspectedImage(metadata, filename || metadata.filename, prefix); + const outputFile = filename || metadata.filename; + if (!outputFile) { + return { name: source, message: 'output file not found' }; + } + return UploadInspectedImage(metadata, outputFile, prefix); }; export async function buildFileDirectories() { @@ -399,13 +404,14 @@ export namespace DashUploadUtils { const response = await AzureManager.UploadBase64ImageBlob(resolved, data); source = `${AzureManager.BASE_STRING}/${resolved}`; } else { + source = `${resolvedServerUrl}${clientPathToFile(Directory.images, resolved)}`; + source = serverPathToFile(Directory.images, resolved); const error = await new Promise<Error | null>(resolve => { writeFile(serverPathToFile(Directory.images, resolved), data, 'base64', resolve); }); if (error !== null) { return error; } - source = `${resolvedServerUrl}${clientPathToFile(Directory.images, resolved)}`; } } let resolvedUrl: string; @@ -514,7 +520,7 @@ export namespace DashUploadUtils { * @param cleanUp a boolean indicating if the files should be deleted after upload. True by default. * @returns the accessPaths for the resized files. */ - export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => { + export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => { const { requestable, source, ...remaining } = metadata; const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`; const { images } = Directory; @@ -593,35 +599,43 @@ export namespace DashUploadUtils { force: true, }; + async function correctRotation(imgSourcePath: string) { + const buffer = fs.readFileSync(imgSourcePath); + try { + return (await autorotate.rotate(buffer, { quality: 30 })).buffer; + } catch (e) { + return buffer; + } + } + /** * outputResizedImages takes in a readable stream and resizes the images according to the sizes defined at the top of this file. * * The new images will be saved to the server with the corresponding prefixes. - * @param streamProvider a Stream of the image to process, taken from the /parsed_files location + * @param imgSourcePath file path for image being resized * @param outputFileName the basename (No suffix) of the outputted file. * @param outputDirectory the directory to output to, usually Directory.Images * @returns a map with suffixes as keys and resized filenames as values. */ - export async function outputResizedImages(sourcePath: string, outputFileName: string, outputDirectory: string) { + export async function outputResizedImages(imgSourcePath: string, outputFileName: string, outputDirectory: string) { const writtenFiles: { [suffix: string]: string } = {}; const sizes = imageResampleSizes(path.extname(outputFileName)); + + const imgBuffer = await correctRotation(imgSourcePath); + const imgReadStream = new Duplex(); + imgReadStream.push(imgBuffer); + imgReadStream.push(null); const outputPath = (suffix: SizeSuffix) => path.resolve(outputDirectory, (writtenFiles[suffix] = InjectSize(outputFileName, suffix))); await Promise.all( sizes.filter(({ width }) => !width).map(({ suffix }) => - new Promise<void>(res => createReadStream(sourcePath).pipe(createWriteStream(outputPath(suffix))).on('close', res)) + new Promise<void>(res => imgReadStream.pipe(createWriteStream(outputPath(suffix))).on('close', res)) )); // prettier-ignore - const fileIn = fs.readFileSync(sourcePath); - let buffer: any; - try { - const { buffer2 } = await autorotate.rotate(fileIn, { quality: 30 }); - buffer = buffer2; - } catch (e) {} - return Jimp.read(buffer ?? fileIn) + return Jimp.read(imgBuffer) .then(async (img: any) => { - await Promise.all( sizes.filter(({ width }) => width) .map(({ width, suffix }) => - img = img.resize(width, Jimp.AUTO).write(outputPath(suffix)) - )); // prettier-ignore + await Promise.all( sizes.filter(({ width }) => width).map(({ width, suffix }) => + img = img.resize(width, Jimp.AUTO).write(outputPath(suffix)) + )); // prettier-ignore return writtenFiles; }) .catch((e: any) => { |