diff options
-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) => { |