aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/documents/Documents.ts3
-rw-r--r--src/client/views/DocumentDecorations.tsx158
-rw-r--r--src/client/views/nodes/ImageBox.tsx409
-rw-r--r--src/fields/DimensionField.ts29
-rw-r--r--src/fields/Doc.ts25
-rw-r--r--src/server/ApiManagers/FireflyManager.ts319
6 files changed, 829 insertions, 114 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 317bb7feb..a2b55943a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -524,6 +524,9 @@ export class DocumentOptions {
ai?: string; // to mark items as ai generated
ai_firefly_seed?: number;
ai_firefly_prompt?: string;
+
+ _outpaintingMetadata?: STRt = new StrInfo('serialized JSON metadata needed for image outpainting', false);
+
}
export const DocOptions = new DocumentOptions();
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 54e050f9f..d7ff0d06a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,6 +1,7 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
+import { DimensionField } from '../../fields/DimensionField';
import { IconButton } from '@dash/components';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -473,6 +474,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
const tl = docView.screenToContentsTransform().inverse().transformPoint(0, 0);
return project([e.clientX + this._offset.x, e.clientY + this._offset.y], tl, [tl[0] + fixedAspect, tl[1] + 1]);
};
+
+ // Modify the onPointerMove method to handle shift+click during resize
onPointerMove = (e: PointerEvent): boolean => {
const first = DocumentView.Selected()[0];
const effectiveAcl = GetEffectiveAcl(first.Document);
@@ -491,17 +494,135 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
!this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate)
this._interactionLock = true;
this._snapPt = thisPt;
- e.ctrlKey && (DocumentView.Selected().forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions()));
- const hasFixedAspect = DocumentView.Selected().map(dv => dv.Document).some(this.hasFixedAspect);
- const scaleAspect = {x:scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y};
- DocumentView.Selected().forEach(docView =>
- this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey:e.ctrlKey })); // prettier-ignore
+
+ // Special handling for shift+click (outpainting mode)
+ if (e.shiftKey && DocumentView.Selected().some(dv => dv.ComponentView instanceof ImageBox)) {
+ DocumentView.Selected().forEach(docView => {
+ if (docView.ComponentView instanceof ImageBox) {
+ // Set flag for outpainting mode
+ docView.Document._outpaintingResize = true;
+
+ // Adjust only the document dimensions without scaling internal content
+ this.resizeViewForOutpainting(docView, refPt, scale, { dragHdl, shiftKey: e.shiftKey });
+ }
+ else {
+ // Handle regular resize for non-image components
+ e.ctrlKey && !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions();
+ const hasFixedAspect = this.hasFixedAspect(docView.Document);
+ const scaleAspect = {x:scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y};
+ this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey: e.ctrlKey });
+ }
+ });
+ } else {
+ // Regular resize behavior (existing code)
+ e.ctrlKey && (DocumentView.Selected().forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions()));
+ const hasFixedAspect = DocumentView.Selected().map(dv => dv.Document).some(this.hasFixedAspect);
+ const scaleAspect = {x:scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y};
+ DocumentView.Selected().forEach(docView =>
+ this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey:e.ctrlKey }));
+ }
+
await new Promise<void>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
- }); // prettier-ignore
+ });
return false;
};
+ resizeViewForOutpainting = (
+ docView: DocumentView,
+ refPt: number[],
+ scale: { x: number; y: number },
+ opts: { dragHdl: string; shiftKey: boolean }
+ ) => {
+ const doc = docView.Document;
+
+ if (doc.isGroup) {
+ DocListCast(doc.data)
+ .map(member => DocumentView.getDocumentView(member, docView)!)
+ .forEach(member => this.resizeViewForOutpainting(member, refPt, scale, opts));
+ doc.xPadding = NumCast(doc.xPadding) * scale.x;
+ doc.yPadding = NumCast(doc.yPadding) * scale.y;
+ return;
+ }
+
+ if (!doc._outpaintingOriginalWidth || !doc._outpaintingOriginalHeight) {
+ doc._outpaintingOriginalWidth = NumCast(doc._width);
+ doc._outpaintingOriginalHeight = NumCast(doc._height);
+
+ // Initialize or update the _originalDims ObjectField correctly
+ doc._originalDims = new DimensionField(doc._outpaintingOriginalWidth, doc._outpaintingOriginalHeight);
+ }
+
+ // Calculate new boundary dimensions
+ const originalWidth = NumCast(doc._width);
+ const originalHeight = NumCast(doc._height);
+ const newWidth = Math.max(NumCast(doc._width_min, 25), originalWidth * scale.x);
+ const newHeight = Math.max(NumCast(doc._height_min, 10), originalHeight * scale.y);
+
+ // Apply new dimensions
+ doc._width = newWidth;
+ doc._height = newHeight;
+
+ const refCent = docView.screenToViewTransform().transformPoint(refPt[0], refPt[1]);
+ const { deltaX, deltaY } = this.realignRefPt(doc, refCent, originalWidth, originalHeight);
+ doc.x = NumCast(doc.x) + deltaX;
+ doc.y = NumCast(doc.y) + deltaY;
+
+ doc._layout_modificationDate = new DateField();
+
+ // Trigger outpainting
+ doc._needsOutpainting = true;
+
+ // Store metadata needed for outpainting
+ doc._outpaintingMetadata = JSON.stringify({
+ originalWidth: doc._outpaintingOriginalWidth,
+ originalHeight: doc._outpaintingOriginalHeight,
+ newWidth,
+ newHeight,
+ scaleX: scale.x,
+ scaleY: scale.y,
+ anchorHandle: opts.dragHdl
+ });
+ };
+
+ @action
+ onPointerUp = (): void => {
+ SnappingManager.SetIsResizing(undefined);
+ SnappingManager.clearSnapLines();
+
+ // Check if any outpainting needs to be processed
+ DocumentView.Selected().forEach(view => {
+ if (view.Document._needsOutpainting && view.ComponentView instanceof ImageBox) {
+ // Trigger outpainting process in the ImageBox component
+ (view.ComponentView as ImageBox).processOutpainting();
+
+ // Clear flags
+ view.Document._needsOutpainting = false;
+ view.Document._outpaintingResize = false;
+ }
+ });
+
+ this._resizeHdlId = '';
+ this._resizeUndo?.end();
+
+ // detect layout_autoHeight gesture and apply
+ DocumentView.Selected().forEach(view => {
+ NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true);
+ });
+ // need to change points for resize, or else rotation/control points will fail.
+ this._inkDragDocs
+ .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
+ .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
+ doc[DocData].data = new InkField(inkPts.map(
+ (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth)
+ X: NumCast(doc.x) - x + (NumCast(doc._width) * ipt.X) / width,
+ Y: NumCast(doc.y) - y + (NumCast(doc._height) * ipt.Y) / height,
+ }))); // prettier-ignore
+ Doc.SetNativeWidth(doc, undefined);
+ Doc.SetNativeHeight(doc, undefined);
+ });
+ };
+
//
// determines how much to resize, and determines the resize reference point
//
@@ -606,31 +727,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
};
};
- @action
- onPointerUp = (): void => {
- SnappingManager.SetIsResizing(undefined);
- SnappingManager.clearSnapLines();
- this._resizeHdlId = '';
- this._resizeUndo?.end();
-
- // detect layout_autoHeight gesture and apply
- DocumentView.Selected().forEach(view => {
- NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true);
- });
- // need to change points for resize, or else rotation/control points will fail.
- this._inkDragDocs
- .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
- .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
- doc[DocData].data = new InkField(inkPts.map(
- (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth)
- X: NumCast(doc.x) - x + (NumCast(doc.width) * ipt.X) / width,
- Y: NumCast(doc.y) - y + (NumCast(doc.height) * ipt.Y) / height,
- }))); // prettier-ignore
- Doc.SetNativeWidth(doc, undefined);
- Doc.SetNativeHeight(doc, undefined);
- });
- };
-
@computed get selectionTitle(): string {
if (DocumentView.Selected().length === 1) {
const selected = DocumentView.Selected()[0];
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 5b06e9fc5..114d5226b 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,6 +1,7 @@
import { Button, Colors, Size, Type } from '@dash/components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Slider, Tooltip } from '@mui/material';
+import { DimensionField } from '../../../fields/DimensionField';
import axios from 'axios';
import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
import { observer } from 'mobx-react';
@@ -46,6 +47,7 @@ import { FocusViewOptions } from './FocusViewOptions';
import './ImageBox.scss';
import { OpenWhere } from './OpenWhere';
import { RichTextField } from '../../../fields/RichTextField';
+import { List } from '../../../fields/List';
export class ImageEditorData {
// eslint-disable-next-line no-use-before-define
@@ -103,6 +105,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable private _regenerateLoading = false;
@observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : [];
+ // Add these observable properties to the ImageBox class
+ @observable private _outpaintingInProgress = false;
+ @observable private _outpaintingPrompt = '';
+
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
@@ -135,6 +141,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
componentDidMount() {
+ super.componentDidMount?.();
this._disposers.sizer = reaction(
() => ({
forceFull: this._props.renderDepth < 1 || this.layoutDoc._showFullRes,
@@ -148,9 +155,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
);
const { layoutDoc } = this;
this._disposers.path = reaction(
- () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }),
- ({ nativeSize, width, height }) => {
- if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !height) {
+ () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }),
+ ({ nativeSize, width }) => {
+ if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !this.layoutDoc._height) {
this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
}
},
@@ -166,6 +173,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
},
{ fireImmediately: true }
);
+ this._disposers.outpainting = reaction(
+ () => this.Document?._needsOutpainting,
+ needsOutpainting => {
+ if (needsOutpainting && this.Document?._outpaintingResize) {
+ this.processOutpainting();
+ }
+ }
+ );
}
componentWillUnmount() {
@@ -214,7 +229,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}, true);
} else if (hitDropTarget(e.target as HTMLElement, this._regenerateIconRef.current)) {
this._regenerateLoading = true;
- const drag = de.complete.docDragData.draggedDocuments.lastElement();
+ const drag = de.complete.docDragData?.draggedDocuments.lastElement();
const dragField = drag[Doc.LayoutFieldKey(drag)];
const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title));
const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text);
@@ -339,6 +354,202 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
});
+ // Add this method to process outpainting when resize is complete
+ @action
+ processOutpainting = async () => {
+ const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+ if (!field) return;
+
+ const origWidth = NumCast(this.Document._outpaintingOriginalWidth);
+ const origHeight = NumCast(this.Document._outpaintingOriginalHeight);
+
+ if (!origWidth || !origHeight) {
+ console.error("Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.");
+ return;
+ }
+
+ //alert(`Original dimensions: ${origWidth} x ${origHeight}`);
+
+
+ // Set flag that outpainting is in progress
+ this._outpaintingInProgress = true;
+
+ try {
+ // Get the current path to the image
+ const currentPath = this.choosePath(field.url);
+
+ // Get original and new dimensions for calculating mask
+ const newWidth = NumCast(this.Document._width);
+ const newHeight = NumCast(this.Document._height);
+
+ // Optional: Ask user for a prompt to guide the outpainting
+ let prompt = "Extend this image naturally with matching content";
+ const customPrompt = await new Promise<string>((resolve) => {
+ const dialog = document.createElement('div');
+ Object.assign(dialog.style, {
+ position: 'fixed',
+ top: '50%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ background: 'white',
+ padding: '20px',
+ borderRadius: '8px',
+ boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
+ zIndex: '10000'
+ });
+
+ const title = document.createElement('h3');
+ title.style.marginTop = '0';
+ title.textContent = 'Outpaint Image';
+
+ const description = document.createElement('p');
+ description.textContent = 'Enter a prompt for extending the image:';
+
+ const input = document.createElement('input');
+ input.id = 'outpaint-prompt';
+ input.type = 'text';
+ input.value = 'Extend this image naturally with matching content';
+ Object.assign(input.style, {
+ width: '300px',
+ padding: '8px',
+ marginBottom: '10px'
+ });
+
+ const buttonContainer = document.createElement('div');
+ Object.assign(buttonContainer.style, {
+ display: 'flex',
+ justifyContent: 'flex-end',
+ gap: '10px'
+ });
+
+ const cancelButton = document.createElement('button');
+ cancelButton.textContent = 'Cancel';
+
+ const confirmButton = document.createElement('button');
+ confirmButton.textContent = 'Generate';
+ Object.assign(confirmButton.style, {
+ background: '#0078d4',
+ color: 'white',
+ border: 'none',
+ padding: '8px 16px'
+ });
+
+ buttonContainer.appendChild(cancelButton);
+ buttonContainer.appendChild(confirmButton);
+
+ dialog.appendChild(title);
+ dialog.appendChild(description);
+ dialog.appendChild(input);
+ dialog.appendChild(buttonContainer);
+
+ document.body.appendChild(dialog);
+
+ cancelButton.onclick = () => {
+ document.body.removeChild(dialog);
+ resolve("");
+ };
+
+ confirmButton.onclick = () => {
+ const promptValue = input.value;
+ document.body.removeChild(dialog);
+ resolve(promptValue);
+ };
+ });
+
+ // If user cancelled, reset dimensions to original
+ if (!customPrompt) {
+ this.Document._width = origWidth;
+ this.Document._height = origHeight;
+ this._outpaintingInProgress = false;
+ return;
+ }
+
+ // Show loading indicator
+ const loadingOverlay = document.createElement('div');
+ loadingOverlay.style.position = 'absolute';
+ loadingOverlay.style.top = '0';
+ loadingOverlay.style.left = '0';
+ loadingOverlay.style.width = '100%';
+ loadingOverlay.style.height = '100%';
+ loadingOverlay.style.background = 'rgba(0,0,0,0.5)';
+ loadingOverlay.style.display = 'flex';
+ loadingOverlay.style.justifyContent = 'center';
+ loadingOverlay.style.alignItems = 'center';
+ loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>';
+ this._mainCont?.appendChild(loadingOverlay);
+
+ // Call the outpaint API
+ const response = await Networking.PostToServer('/outpaintImageFour', {
+ imageUrl: currentPath,
+ prompt: customPrompt,
+ originalDimensions: {
+ width: origWidth,
+ height: origHeight
+ },
+ newDimensions: {
+ width: newWidth,
+ height: newHeight
+ }
+ });
+ if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') {
+ console.log("Response is valid and contains URL:", response.url);
+
+ } else {
+ console.error("Unexpected API response:", response);
+ alert("Failed to receive a valid image URL from server.");
+ }
+
+
+ if (response && 'url' in response && typeof response.url === 'string') {
+ // Save the original image as an alternate
+ if (!this.dataDoc[this.fieldKey + '_alternates']) {
+ this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>();
+ }
+
+ // Create a copy of the current image as an alternate
+ const originalDoc = Docs.Create.ImageDocument(field.url.href, {
+ title: `Original: ${this.Document.title}`,
+ _nativeWidth: Doc.NativeWidth(this.dataDoc),
+ _nativeHeight: Doc.NativeHeight(this.dataDoc)
+ });
+
+ // Add to alternates
+ Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc);
+
+ // Update the image with the outpainted version
+ this.dataDoc[this.fieldKey] = new ImageField(response.url);
+
+ // Update native dimensions
+ Doc.SetNativeWidth(this.dataDoc, newWidth);
+ Doc.SetNativeHeight(this.dataDoc, newHeight);
+
+ // Add AI metadata
+ this.Document.ai = true;
+ this.Document.ai_outpainted = true;
+ this.Document.ai_outpaint_prompt = customPrompt;
+ } else {
+ // If failed, revert to original dimensions
+ this.Document._width = origWidth;
+ this.Document._height = origHeight;
+ alert("Failed to outpaint image. Please try again.");
+ }
+
+ // Remove loading overlay
+ this._mainCont?.removeChild(loadingOverlay);
+
+ } catch (error) {
+ console.error("Error during outpainting:", error);
+ // Revert to original dimensions on error
+ this.Document._width = origWidth
+ this.Document._height = origHeight
+ alert("An error occurred while outpainting. Please try again.");
+ } finally {
+ // Clear the outpainting flags
+ this._outpaintingInProgress = false;
+ delete this.Document._originalDims;
+ }
+ };
+
specificContextMenu = (): void => {
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
@@ -396,6 +607,27 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down',
});
+ // Add new outpainting option
+ funcs.push({
+ description: 'Outpaint Image',
+ event: () => {
+ this.processOutpainting();
+ },
+ icon: 'brush'
+ });
+ // Add outpainting history option if the image was outpainted
+ this.Document.ai_outpainted &&
+ funcs.push({
+ description: 'View Original Image',
+ event: action(() => {
+ const alternates = DocListCast(this.dataDoc[this.fieldKey + '_alternates']);
+ if (alternates && alternates.length) {
+ // Toggle to show the original image
+ this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate';
+ }
+ }),
+ icon: 'image',
+ });
ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
};
@@ -434,7 +666,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const ext = extname(url.href);
return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
};
- getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
+ getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc.freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
@computed get usingAlternate() {
const usePath = StrCast(this.Document[this.fieldKey + '_usePath']);
@@ -587,7 +819,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
/>
{fadepath === srcpath ? null : (
<div className={`imageBox-fadeBlocker${this.usingAlternate ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
- <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
+ <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={20} />
</div>
)}
</div>
@@ -746,82 +978,89 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']);
const doc = this.usingAlternate ? (alts.lastElement() ?? this.Document) : this.Document;
return (
- <div
- className="imageBox"
- onContextMenu={this.specificContextMenu}
- ref={this.createDropTarget}
- onScroll={action(() => {
- if (!this._forcedScroll) {
- if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) {
- this._ignoreScroll = true;
- this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop;
- this._ignoreScroll = false;
+ <>
+ <div
+ className="imageBox"
+ onContextMenu={this.specificContextMenu}
+ ref={this.createDropTarget}
+ onScroll={action(() => {
+ if (!this._forcedScroll) {
+ if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) {
+ this._ignoreScroll = true;
+ this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop;
+ this._ignoreScroll = false;
+ }
}
- }
- })}
- style={{
- width: this._props.PanelWidth() ? undefined : `100%`,
- height: this._props.PanelHeight() ? undefined : `100%`,
- pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
- borderRadius,
- overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden',
- }}>
- <CollectionFreeFormView
- ref={this._ffref}
- {...this._props}
- Document={doc}
- setContentViewBox={emptyFunction}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- renderDepth={this._props.renderDepth + 1}
- fieldKey={this.annotationKey}
- styleProvider={this._props.styleProvider}
- isAnnotationOverlay
- annotationLayerHostsContent
- PanelWidth={this._props.PanelWidth}
- PanelHeight={this._props.PanelHeight}
- ScreenToLocalTransform={this.screenToLocalTransform}
- select={emptyFunction}
- focus={this.focus}
- getScrollHeight={this.getScrollHeight}
- NativeDimScaling={returnOne}
- isAnyChildContentActive={returnFalse}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}>
- {this.content}
- </CollectionFreeFormView>
- {this.Loading ? (
- <div className="loading-spinner" style={{ position: 'absolute' }}>
- <ReactLoading type="spin" height={50} width={50} color={'blue'} />
+ })}
+ style={{
+ width: this._props.PanelWidth() ? undefined : `100%`,
+ height: this._props.PanelHeight() ? undefined : `100%`,
+ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
+ borderRadius,
+ overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden',
+ }}>
+ <CollectionFreeFormView
+ ref={this._ffref}
+ {...this._props}
+ Document={doc}
+ setContentViewBox={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ renderDepth={this._props.renderDepth + 1}
+ fieldKey={this.annotationKey}
+ styleProvider={this._props.styleProvider}
+ isAnnotationOverlay
+ annotationLayerHostsContent
+ PanelWidth={this._props.PanelWidth}
+ PanelHeight={this._props.PanelHeight}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ select={emptyFunction}
+ focus={this.focus}
+ getScrollHeight={this.getScrollHeight}
+ NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}>
+ {this.content}
+ </CollectionFreeFormView>
+ {this.Loading ? (
+ <div className="loading-spinner" style={{ position: 'absolute' }}>
+ <ReactLoading type="spin" height={50} width={50} color={'blue'} />
+ </div>
+ ) : null}
+ {this.regenerateImageIcon}
+ {this.overlayImageIcon}
+ {this.annotationLayer}
+ {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : (
+ <MarqueeAnnotator
+ Document={this.Document}
+ ref={this.marqueeref}
+ scrollTop={0}
+ annotationLayerScrollTop={0}
+ scaling={returnOne}
+ annotationLayerScaling={this._props.NativeDimScaling}
+ screenTransform={this.DocumentView().screenToViewTransform}
+ docView={this.DocumentView}
+ addDocument={this.addDocument}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this.savedAnnotations}
+ selectionText={returnEmptyString}
+ annotationLayer={this._annotationLayer.current}
+ marqueeContainer={this._mainCont}
+ highlightDragSrcColor=""
+ anchorMenuCrop={this.crop}
+ // anchorMenuFlashcard={() => this.getImageDesc()}
+ />
+ )}
+ </div>
+ {this._outpaintingInProgress && (
+ <div className="imageBox-outpaintingSpinner">
+ <ReactLoading type="spin" color="#666" height={60} width={60} />
</div>
- ) : null}
- {this.regenerateImageIcon}
- {this.overlayImageIcon}
- {this.annotationLayer}
- {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : (
- <MarqueeAnnotator
- Document={this.Document}
- ref={this.marqueeref}
- scrollTop={0}
- annotationLayerScrollTop={0}
- scaling={returnOne}
- annotationLayerScaling={this._props.NativeDimScaling}
- screenTransform={this.DocumentView().screenToViewTransform}
- docView={this.DocumentView}
- addDocument={this.addDocument}
- finishMarquee={this.finishMarquee}
- savedAnnotations={this.savedAnnotations}
- selectionText={returnEmptyString}
- annotationLayer={this._annotationLayer.current}
- marqueeContainer={this._mainCont}
- highlightDragSrcColor=""
- anchorMenuCrop={this.crop}
- // anchorMenuFlashcard={() => this.getImageDesc()}
- />
)}
- </div>
+ </>
);
}
@@ -834,10 +1073,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const file = input.files?.[0];
if (file) {
const disposer = OverlayView.ShowSpinner();
- DocUtils.uploadFileToDoc(file, {}, this.Document).then(doc => {
- disposer();
- doc && (doc.height = undefined);
- });
+ const [{ result }] = await Networking.UploadFilesToServer({ file });
+ if (result instanceof Error) {
+ alert('Error uploading files - possibly due to unsupported file types');
+ } else {
+ this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
+ !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc);
+ }
+ disposer();
} else {
console.log('No file selected');
}
diff --git a/src/fields/DimensionField.ts b/src/fields/DimensionField.ts
new file mode 100644
index 000000000..ffbb9a732
--- /dev/null
+++ b/src/fields/DimensionField.ts
@@ -0,0 +1,29 @@
+import { ObjectField } from "./ObjectField";
+import { Copy, ToJavascriptString, ToScriptString, ToString } from "./FieldSymbols";
+
+export class DimensionField extends ObjectField {
+ width: number;
+ height: number;
+
+ constructor(width: number, height: number) {
+ super();
+ this.width = width;
+ this.height = height;
+ }
+
+ [Copy](): DimensionField {
+ return new DimensionField(this.width, this.height);
+ }
+
+ [ToJavascriptString](): string {
+ return `{ width: ${this.width}, height: ${this.height} }`;
+ }
+
+ [ToScriptString](): string {
+ return `{ width: ${this.width}, height: ${this.height} }`;
+ }
+
+ [ToString](): string {
+ return `${this.width} x ${this.height}`;
+ }
+}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index fc89dcbe7..dded8ce03 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -468,6 +468,10 @@ export class Doc extends RefField {
}
}
export namespace Doc {
+ export let SelectOnLoad: Doc | undefined;
+ export function SetSelectOnLoad(doc: Doc | undefined) {
+ SelectOnLoad = doc;
+ }
export let DocDragDataName: string = '';
export function SetDocDragDataName(name: string) {
DocDragDataName = name;
@@ -1177,6 +1181,27 @@ export namespace Doc {
const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
return NumCast(doc._nativeHeight, nheight || dheight);
}
+
+
+ export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
+ return !doc ? 0 : NumCast(doc._outpaintingWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_outpaintingWidth'], useWidth ? NumCast(doc._width) : 0));
+ }
+
+ export function OutpaintingHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
+ if (!doc) return 0;
+ const oheight = (Doc.OutpaintingWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height);
+ const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_outpaintingHeight'], useHeight ? NumCast(doc._height) : 0);
+ return NumCast(doc._outpaintingHeight, oheight || dheight);
+ }
+
+ export function SetOutpaintingWidth(doc: Doc, width: number | undefined, fieldKey?: string) {
+ doc[(fieldKey || Doc.LayoutFieldKey(doc)) + '_outpaintingWidth'] = width;
+ }
+
+ export function SetOutpaintingHeight(doc: Doc, height: number | undefined, fieldKey?: string) {
+ doc[(fieldKey || Doc.LayoutFieldKey(doc)) + '_outpaintingHeight'] = height;
+ }
+
export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) {
doc[(fieldKey || Doc.LayoutFieldKey(doc)) + '_nativeWidth'] = width;
}
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index e75ede9df..1e0fdb595 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -8,6 +8,7 @@ import { DashUploadUtils } from '../DashUploadUtils';
import { _error, _invalid, _success, Method } from '../RouteManager';
import { Directory, filesDirectory } from '../SocketData';
import ApiManager, { Registration } from './ApiManager';
+import { Upload } from '../SharedMediaTypes';
export default class FireflyManager extends ApiManager {
getBearerToken = () =>
@@ -328,6 +329,324 @@ export default class FireflyManager extends ApiManager {
});
}),
});
+
+ register({
+ method: Method.POST,
+ subscription: '/outpaintImageFour',
+ secureHandler: ({ req, res }) =>
+
+ this.uploadImageToDropbox(req.body.imageUrl, req.user as DashUserModel)
+ .then(uploadUrl => {
+ if (uploadUrl instanceof Error) {
+ _invalid(res, uploadUrl.message);
+ return;
+ }
+ return this.getBearerToken()
+ .then(tokenResponse => tokenResponse?.json())
+ .then((tokenData: { access_token: string }) =>
+ fetch('https://firefly-api.adobe.io/v3/images/expand', {
+ method: 'POST',
+ headers: {
+ //'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'x-api-key': process.env._CLIENT_FIREFLY_CLIENT_ID ?? '',
+ 'Authorization': `Bearer ${tokenData.access_token}`,
+ },
+ body: JSON.stringify({
+ image: {
+ source: { url: uploadUrl },
+ },
+ size: {
+ width: req.body.newDimensions.width,
+ height: req.body.newDimensions.height,
+ },
+ prompt: req.body.prompt ?? '',
+ numVariations: 1,
+ placement: {
+ inset: {
+ left: 0,
+ top: 0,
+ right: req.body.newDimensions.width - req.body.originalDimensions.width,
+ bottom: req.body.newDimensions.height - req.body.originalDimensions.height,
+ },
+ alignment: {
+ horizontal: 'center',
+ vertical: 'center',
+ },
+ },
+ }),
+ })
+ )
+ .then(expandResp => expandResp?.json())
+ .then(expandData => {
+ if (expandData.error_code || !expandData.outputs?.[0]?.image?.url) {
+ console.error('Firefly validation error:', expandData);
+ _error(res, expandData.message ?? 'Failed to generate image');
+ } else {
+ DashUploadUtils.UploadImage(expandData.outputs[0].image.url)
+ .then((info: Upload.ImageInformation | Error) => {
+ if (info instanceof Error) {
+ _invalid(res, info.message);
+ } else {
+ _success(res, { url: info.accessPaths.agnostic.client });
+ }
+ })
+ .catch(uploadErr => {
+ console.error('DashUpload Error:', uploadErr);
+ _error(res, 'Failed to upload generated image.');
+ });
+ }
+ })
+ .catch(err => {
+ console.error('Firefly request error:', err);
+ _error(res, 'Failed to expand image');
+ });
+ }),
+ });
+ register({
+ method: Method.POST,
+ subscription: '/outpaintImageThree',
+ secureHandler: ({ req, res }) =>
+ new Promise<void>(resolver => {
+ this.uploadImageToDropbox(req.body.imageUrl, req.user as DashUserModel)
+ .then(dropboxImgUrl => {
+ if (dropboxImgUrl instanceof Error) {
+ _invalid(res, dropboxImgUrl.message);
+ throw new Error('Error uploading image to dropbox');
+ }
+ return dropboxImgUrl;
+ })
+ .then(dropboxImgUrl =>
+ this.getBearerToken().then(tokenResponse =>
+ tokenResponse?.json().then((tokenData: { access_token: string }) =>
+ fetch('https://firefly-api.adobe.io/v3/images/expand', {
+ method: 'POST',
+ headers: [
+ ['Content-Type', 'application/json'],
+ ['Accept', 'application/json'],
+ ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
+ ['Authorization', `Bearer ${tokenData.access_token}`],
+ ],
+ body: JSON.stringify({
+ image: {
+ source: {
+ url: dropboxImgUrl,
+ },
+ },
+ numVariations: 1,
+ prompt: req.body.prompt,
+ size: {
+ width: req.body.newDimensions.width,
+ height: req.body.newDimensions.height,
+ },
+ placement: {
+ inset: {
+ left: 0,
+ top: 0,
+ right: req.body.newDimensions.width - req.body.originalDimensions.width,
+ bottom: req.body.newDimensions.height - req.body.originalDimensions.height,
+ },
+ alignment: {
+ horizontal: 'center',
+ vertical: 'center',
+ },
+ },
+ }),
+ })
+ .then(resp => resp.json())
+ .then(expandData => {
+ if (expandData.error_code || !expandData.outputs?.[0]?.image?.url) {
+ _invalid(res, expandData.message ?? 'Failed to expand image');
+ resolver();
+ } else {
+ DashUploadUtils.UploadImage(expandData.outputs[0].image.url)
+ .then(info => {
+ if (info instanceof Error) {
+ _invalid(res, info.message);
+ } else {
+ _success(res, { url: info.accessPaths.agnostic.client });
+ }
+ })
+ .then(resolver)
+ .catch(uploadErr => {
+ console.error('DashUpload Error:', uploadErr);
+ _invalid(res, 'Failed to upload generated image.');
+ resolver();
+ });
+ }
+ })
+ .catch(err => {
+ console.error('Firefly API Error:', err);
+ _error(res, 'Failed to expand image');
+ resolver();
+ })
+ )
+ )
+ )
+ .catch(err => {
+ console.error('Dropbox Error:', err);
+ _error(res, 'Failed to upload image to Dropbox');
+ resolver();
+ });
+ }),
+ });
+
+
+ register({
+ method: Method.POST,
+ subscription: '/outpaintImageTwo',
+ secureHandler: async ({ req, res }) => {
+ try {
+ const uploadUrl = await this.uploadImageToDropbox(req.body.imageUrl, req.user as DashUserModel);
+
+ if (uploadUrl instanceof Error) {
+ _invalid(res, uploadUrl.message);
+ return;
+ }
+
+ const tokenResponse = await this.getBearerToken();
+ const tokenData = await tokenResponse?.json();
+
+ const body = JSON.stringify({
+ image: { source: { url: uploadUrl } },
+ prompt: req.body.prompt,
+ numVariations: 1,
+ size: {
+ width: req.body.newDimensions.width,
+ height: req.body.newDimensions.height,
+ },
+ placement: {
+ inset: {
+ left: 0,
+ top: 0,
+ right: req.body.newDimensions.width - req.body.originalDimensions.width,
+ bottom: req.body.newDimensions.height - req.body.originalDimensions.height,
+ },
+ alignment: {
+ horizontal: 'center',
+ vertical: 'center',
+ },
+ },
+ });
+
+ console.log('Sending outpainting request:', body);
+
+ const expandResp = await fetch('https://firefly-api.adobe.io/v3/images/expand', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ 'x-api-key': process.env._CLIENT_FIREFLY_CLIENT_ID ?? '',
+ Authorization: `Bearer ${tokenData.access_token}`,
+ },
+ body,
+ });
+
+ const expandData = await expandResp.json();
+ console.log('Received expandData:', expandData);
+
+ if (expandData.error_code || !expandData.outputs?.[0]?.image?.url) {
+ console.error('Expand API Error:', expandData);
+ _error(res, expandData.message ?? 'Failed to generate image');
+ return;
+ }
+
+ const uploadedInfo = await DashUploadUtils.UploadImage(expandData.outputs[0].image.url);
+
+ if (uploadedInfo instanceof Error) {
+ console.error('Upload Error:', uploadedInfo.message);
+ _invalid(res, uploadedInfo.message);
+ return;
+ }
+
+ console.log('Successfully uploaded image URL:', uploadedInfo.accessPaths.agnostic.client);
+ _success(res, { url: uploadedInfo.accessPaths.agnostic.client });
+ } catch (err) {
+ console.error('Unexpected error during outpainting:', err);
+ _error(res, 'Unexpected error during outpainting');
+ }
+ },
+ });
+
+ register({
+
+
+ method: Method.POST,
+ subscription: '/outpaintImage',
+ secureHandler: ({ req, res }) =>
+ this.uploadImageToDropbox(req.body.imageUrl, req.user as DashUserModel).then(uploadUrl =>
+ uploadUrl instanceof Error
+ ? _invalid(res, uploadUrl.message)
+ : this.getBearerToken()
+ .then(tokenResponse => tokenResponse?.json())
+ .then((tokenData: { access_token: string }) =>
+ fetch('https://firefly-api.adobe.io/v3/images/expand', {
+ method: 'POST',
+ headers: [
+ ['Content-Type', 'application/json'],
+ ['Accept', 'application/json'],
+ ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
+ ['Authorization', `Bearer ${tokenData.access_token}`],
+ ],
+ body: JSON.stringify({
+ image: { source: { url: uploadUrl } },
+ numVariations: 1,
+ size: {
+ width: req.body.newDimensions.width,
+ height: req.body.newDimensions.height,
+ },
+ prompt: req.body.prompt,
+ placement: {
+ inset: {
+ left: 0,
+ top: 0,
+ right: 0, //req.body.newDimensions.width - req.body.originalDimensions.width,
+ bottom: 0 //req.body.newDimensions.height - req.body.originalDimensions.height,
+ },
+ alignment: {
+ horizontal: 'center',
+ vertical: 'center',
+ },
+ },
+ }),
+ })
+ )
+ .then(expandResp => expandResp.json())
+ .then(expandData => {
+ if (expandData.error_code || !expandData.outputs?.[0]?.image?.url) {
+ _error(res, expandData.message ?? "Failed to generate image");
+ } else {
+ DashUploadUtils.UploadImage(expandData.outputs[0].image.url)
+ .then((info: Upload.ImageInformation | Error) => {
+ if (info instanceof Error) {
+ _invalid(res, info.message);
+ } else {
+ // THIS IS CRUCIAL: Respond exactly how your front end expects
+ console.log("Successfully uploaded image. URL:", info.accessPaths.agnostic.client);
+ _success(res, { url: info.accessPaths.agnostic.client });
+ }
+ })
+ .catch(uploadErr => {
+
+ console.error('DashUpload Error:', uploadErr);
+ _error(res, 'Failed to upload generated image.');
+ });
+ }
+ })
+ .catch(err => {
+ console.error(err);
+ _error(res, 'Failed to expand image');
+ })
+ ),
+ });
+
+ /* register({
+ method: Method.POST
+ subscription: '/queryFireflyOutpaint',
+ secureHandler: ({req, res}) =>
+ this.outpaintImage()
+ })*/
+
register({
method: Method.POST,
subscription: '/queryFireflyImage',