From 9305752405807ac736795185ae30c6955f5b483f Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 2 Apr 2025 10:18:57 -0400 Subject: fixed image box overlays to allow for transparent pngs. allowed hover over background for image alternats to be set. --- src/client/views/nodes/ImageBox.scss | 1 - 1 file changed, 1 deletion(-) (limited to 'src/client/views/nodes/ImageBox.scss') diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 3d6942e6f..36abf707c 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -114,7 +114,6 @@ width: 100%; height: 100%; position: absolute; - background: black; display: flex; flex-direction: row; align-items: center; -- cgit v1.2.3-70-g09d2 From ede7aaa19d903a7e55fc60d9bc213988ea602c97 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 3 Apr 2025 14:15:15 -0400 Subject: fixed image box button sizing. made drawing ai regen a little faster by not creating icons when there are no annotqations. --- src/client/apis/gpt/GPT.ts | 5 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/ImageBox.scss | 7 +-- src/client/views/nodes/ImageBox.tsx | 23 ++++--- src/client/views/smartdraw/DrawingFillHandler.tsx | 75 +++++++++++------------ src/server/ApiManagers/FireflyManager.ts | 1 - src/server/ApiManagers/UploadManager.ts | 7 +-- src/server/DashUploadUtils.ts | 14 ++--- 8 files changed, 62 insertions(+), 72 deletions(-) (limited to 'src/client/views/nodes/ImageBox.scss') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 29b6ab989..140aebfe0 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -1,5 +1,6 @@ import { ChatCompletionMessageParam, Image } from 'openai/resources'; import { openai } from './setup'; +import { imageUrlToBase64 } from '../../../ClientUtils'; export enum GPTDocCommand { AssignTags = 1, @@ -310,7 +311,9 @@ const gptHandwriting = async (src: string): Promise => { } }; -const gptDescribeImage = async (image: string): Promise => { +const gptDescribeImage = async (userPrompt: string, url: string): Promise => { + if (userPrompt) return userPrompt; + const image = imageUrlToBase64(url); try { const response = await openai.chat.completions.create({ model: 'gpt-4o', diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 070a13103..37f888ddd 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -702,7 +702,7 @@ export class DocumentViewInternal extends DocComponent() { this._regenerateLoading = true; const drag = de.complete.docDragData.draggedDocuments.lastElement(); const dragField = drag[Doc.LayoutFieldKey(drag)]; + const descText = RTFCast(dragField)?.Text || StrCast(dragField) || RTFCast(drag.text)?.Text || StrCast(drag.text) || StrCast(this.Document.title); const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title)); const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text); - DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(dragField instanceof RichTextField ? dragField.Text : ''), drag)?.then(action(() => (this._regenerateLoading = false))); + DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false))); added = false; } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; @@ -401,13 +401,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }; // updateIcon = () => new Promise(res => res()); - updateIcon = (/* usePanelDimensions?: boolean */) => { - const contentDiv = this._mainCont; - return !contentDiv + updateIcon = (/* usePanelDimensions?: boolean */) => + !this._mainCont || !DocListCast(this.dataDoc[this.annotationKey]).length ? new Promise(res => res()) : UpdateIcon( this.layoutDoc[Id] + '_icon_' + new Date().getTime(), - contentDiv, + this._mainCont, this._props.PanelWidth(), // usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), this._props.PanelHeight(), // usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), this._props.PanelWidth(), @@ -422,7 +421,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this.dataDoc.icon_nativeHeight = nativeHeight; } ); - }; choosePath = (url: URL) => { if (!url?.href) return ''; @@ -453,15 +451,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { /** * How much the content of the view is being scaled based on its nesting and its fit-to-width settings */ - @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * ( this._props.NativeDimScaling?.() || 1); } // prettier-ignore + @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.()??1); } // prettier-ignore /** * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. */ - @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.5 * Math.min(NumCast(this.Document.width)))* this.viewScaling; } // prettier-ignore + @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.2 * this._props.PanelWidth())*this.viewScaling; } // prettier-ignore /** * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content */ - @computed get uiBtnScaling() { return Math.min(this.maxWidgetSize / this._sideBtnWidth, 1); } // prettier-ignore + @computed get uiBtnScaling() { return Math.min(1/(this._props.NativeDimScaling?.()??1), this.maxWidgetSize / this._sideBtnWidth); } // prettier-ignore @computed get overlayImageIcon() { const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; @@ -518,10 +516,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { transform: `scale(${this.uiBtnScaling})`, width: this._sideBtnWidth, height: this._sideBtnWidth, - background: 'transparent', + background: 'black', + color: 'white', // color: SettingsManager.userBackgroundColor, }}> - {this._regenerateLoading ? : } + {this._regenerateLoading ? : } ); diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index d8ef8308d..a91ec23b8 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -1,4 +1,3 @@ -import { imageUrlToBase64 } from '../../../ClientUtils'; import { Doc, StrListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { DocCast, ImageCast, StrCast } from '../../../fields/Types'; @@ -42,44 +41,42 @@ export class DrawingFillHandler { const { href } = ImageCast(imageField).url; const hrefParts = href.split('.'); const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`; - return imageUrlToBase64(structureUrl) - .then(gptDescribeImage) - .then((prompt, newPrompt = user_prompt || prompt) => - Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structureUrl, strength, presets: styles, styleUrl }) - .then(res => { - const error = ('error' in res && (res.error as string)) || ''; - if (error.includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + error.replace(/^[^"]*/, ''))) { - window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus(); - return; - } - const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { title: StrCast(drawing.title) + ' AI Images', _width: 400, _height: 400 }); - drawing.$ai_firefly_generatedDocs = genratedDocs; - (res as Upload.ImageInformation[]).map(info => - Doc.AddDocToList( - genratedDocs, - undefined, - Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { - ai: 'firefly', - tags: new List(['@ai']), - title: newPrompt, - _data_usePath: 'alternate:hover', - data_alternates: new List([drawing]), - ai_firefly_prompt: newPrompt, - _width: 500, - data_nativeWidth: info.nativeWidth, - data_nativeHeight: info.nativeHeight, - }), - undefined, - undefined, - true - ) - ); - if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight); - }) - .catch(e => { - alert(e.toString()); - }) - ); // prettier-ignore:q + return gptDescribeImage(user_prompt, structureUrl).then(newPrompt => + Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structureUrl, strength, presets: styles, styleUrl }) + .then(res => { + const error = ('error' in res && (res.error as string)) || ''; + if (error.includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + error.replace(/^[^"]*/, ''))) { + window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus(); + return; + } + const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { title: StrCast(drawing.title) + ' AI Images', _width: 400, _height: 400 }); + drawing.$ai_firefly_generatedDocs = genratedDocs; + (res as Upload.ImageInformation[]).map(info => + Doc.AddDocToList( + genratedDocs, + undefined, + Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { + ai: 'firefly', + tags: new List(['@ai']), + title: newPrompt, + _data_usePath: 'alternate:hover', + data_alternates: new List([drawing]), + ai_firefly_prompt: newPrompt, + _width: 500, + data_nativeWidth: info.nativeWidth, + data_nativeHeight: info.nativeHeight, + }), + undefined, + undefined, + true + ) + ); + if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight); + }) + .catch(e => { + alert(e.toString()); + }) + ); // prettier-ignore:q } }); }; diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts index e75ede9df..07428798c 100644 --- a/src/server/ApiManagers/FireflyManager.ts +++ b/src/server/ApiManagers/FireflyManager.ts @@ -117,7 +117,6 @@ export default class FireflyManager extends ApiManager { generateImage = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, seed?: number) => { let body = `{ "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}} }`; if (seed) { - console.log('RECEIVED SEED', seed); body = `{ "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}}, "seeds": [${seed}]}`; } const fetched = this.getBearerToken().then(response => diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index c9d5df547..7c55e4a42 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -126,11 +126,8 @@ export default class UploadManager extends ApiManager { secureHandler: async ({ req, res }) => { const { sources } = req.body; if (Array.isArray(sources)) { - const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source))); - res.send(results); - return; - } - res.send(); + res.send(await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)))); + } else res.send(); }, }); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index a2747257a..ed109d8f7 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -450,14 +450,12 @@ export namespace DashUploadUtils { * 3) the size of the image, in bytes (4432130) * 4) the content type of the image, i.e. image/(jpeg | png | ...) */ - export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise => { - const result = await InspectImage(source); - if (result instanceof Error) { - return { name: result.name, message: result.message }; - } - const outputFile = filename || result.filename || ''; - return UploadInspectedImage(result, outputFile, prefix, isLocal().exec(source) || source.startsWith('data:') ? true : false); - }; + export const UploadImage = (source: string, filename?: string, prefix: string = ''): Promise => + InspectImage(source).then(async result => + result instanceof Error + ? ({ name: result.name, message: result.message } as Error) // + : UploadInspectedImage(result, filename || result.filename || '', prefix, isLocal().exec(source) || source.startsWith('data:') ? true : false) + ); type md5 = 'md5'; type falsetype = false; -- cgit v1.2.3-70-g09d2 From 22a40443193320487c27ce02bd3f134d13cb7d65 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Wed, 9 Apr 2025 23:58:46 -0400 Subject: cleaned up code --- src/client/views/nodes/ImageBox.scss | 32 +++ src/client/views/nodes/ImageBox.tsx | 494 ++++++++++++++++++++++------------- 2 files changed, 351 insertions(+), 175 deletions(-) (limited to 'src/client/views/nodes/ImageBox.scss') diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 3d6942e6f..d7a14a9df 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -245,3 +245,35 @@ color: black; } } +.imageBox-regenerate-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + z-index: 10000; + + h3 { margin-top: 0; } + + input { + width: 300px; + padding: 8px; + margin-bottom: 10px; + } + + .buttons { + display: flex; + justify-content: flex-end; + gap: 10px; + + .generate-btn { + background: #0078d4; + color: white; + border: none; + padding: 8px 16px; + } + } + } \ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 14adfaf1e..033b0b5a2 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -353,197 +353,340 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { } }); - // Add this method to process outpainting when resize is complete - @action - processOutpainting = async () => { - const field = Cast(this.dataDoc[this.fieldKey], ImageField); - if (!field) return; + @observable _showOutpaintPrompt: boolean = false; +@observable _outpaintPromptInput: string = 'Extend this image naturally with matching content'; + +@action +openOutpaintPrompt = () => { + this._showOutpaintPrompt = true; +}; + +@action +closeOutpaintPrompt = () => { + this._showOutpaintPrompt = false; +}; + +@action +handlePromptChange = (e: React.ChangeEvent) => { + this._outpaintPromptInput = e.target.value; +}; + +@action +submitOutpaintPrompt = () => { + this.closeOutpaintPrompt(); + this.processOutpaintingWithPrompt(this._outpaintPromptInput); +}; + +@action +processOutpaintingWithPrompt = async (customPrompt: string) => { + 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; + } - const origWidth = NumCast(this.Document._outpaintingOriginalWidth); - const origHeight = NumCast(this.Document._outpaintingOriginalHeight); + // Set flag that outpainting is in progress + this._outpaintingInProgress = true; - if (!origWidth || !origHeight) { - console.error('Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.'); + try { + const currentPath = this.choosePath(field.url); + const newWidth = NumCast(this.Document._width); + const newHeight = NumCast(this.Document._height); + + // Revert dimensions if prompt is blank (acts like Cancel) + if (!customPrompt) { + this.Document._width = origWidth; + this.Document._height = origHeight; + this._outpaintingInProgress = false; 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(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(''); - }; + // Optional: add 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 = '
Generating outpainted image...
'; + this._mainCont?.appendChild(loadingOverlay); + + const response = await Networking.PostToServer('/outpaintImage', { + imageUrl: currentPath, + prompt: customPrompt, + originalDimensions: { width: origWidth, height: origHeight }, + newDimensions: { width: newWidth, height: newHeight }, + }); - confirmButton.onclick = () => { - const promptValue = input.value; - document.body.removeChild(dialog); - resolve(promptValue); - }; - }); + if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') { + console.log('Received outpainted image:', response.url); - // If user cancelled, reset dimensions to original - if (!customPrompt) { - this.Document._width = origWidth; - this.Document._height = origHeight; - this._outpaintingInProgress = false; - return; + if (!this.dataDoc[this.fieldKey + '_alternates']) { + this.dataDoc[this.fieldKey + '_alternates'] = new List(); } - // 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 = '
Generating outpainted image...
'; - this._mainCont?.appendChild(loadingOverlay); - - // Call the outpaint API - const response = await Networking.PostToServer('/outpaintImage', { - imageUrl: currentPath, - prompt: customPrompt, - originalDimensions: { - width: origWidth, - height: origHeight, - }, - newDimensions: { - width: newWidth, - height: newHeight, - }, + const originalDoc = Docs.Create.ImageDocument(field.url.href, { + title: `Original: ${this.Document.title}`, + _nativeWidth: Doc.NativeWidth(this.dataDoc), + _nativeHeight: Doc.NativeHeight(this.dataDoc), }); - 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(); - } - - // 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); + Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc); - // Update native dimensions - Doc.SetNativeWidth(this.dataDoc, newWidth); - Doc.SetNativeHeight(this.dataDoc, newHeight); + // Replace with new outpainted image + this.dataDoc[this.fieldKey] = new ImageField(response.url); - // 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.'); - } + Doc.SetNativeWidth(this.dataDoc, newWidth); + Doc.SetNativeHeight(this.dataDoc, newHeight); - // Remove loading overlay - this._mainCont?.removeChild(loadingOverlay); - } catch (error) { - console.error('Error during outpainting:', error); - // Revert to original dimensions on error + this.Document.ai = true; + this.Document.ai_outpainted = true; + this.Document.ai_outpaint_prompt = customPrompt; + } else { + console.error('Unexpected API response:', response); 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; + alert('Failed to receive a valid image URL from server.'); } - }; + + this._mainCont?.removeChild(loadingOverlay); + } catch (error) { + console.error('Error during outpainting:', error); + this.Document._width = origWidth; + this.Document._height = origHeight; + alert('An error occurred while outpainting. Please try again.'); + } finally { + this._outpaintingInProgress = false; + delete this.Document._originalDims; + } +}; + +processOutpainting = () => { + this.openOutpaintPrompt(); +}; + +componentUI = () => ( + <> + {this._showOutpaintPrompt && ( +
+

Outpaint Image

+

Enter a prompt for extending the image:

+ +
+ + +
+
+ )} + +); + + // // 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(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 = '
Generating outpainted image...
'; + // this._mainCont?.appendChild(loadingOverlay); + + // // Call the outpaint API + // const response = await Networking.PostToServer('/outpaintImage', { + // 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(); + // } + + // // 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); @@ -586,10 +729,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { funcs.push({ description: 'Outpaint Image', event: () => { - this.processOutpainting(); + this.openOutpaintPrompt(); }, icon: 'brush', }); + // Add outpainting history option if the image was outpainted this.Document.ai_outpainted && funcs.push({ -- cgit v1.2.3-70-g09d2 From 0694277e33c4a5c4f66b1cebef38f9d86c38cf34 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 29 Apr 2025 12:29:02 -0400 Subject: clean up outpaint code. fix image sizing when outpainting. --- src/client/views/DocumentDecorations.tsx | 25 ++++++------- src/client/views/nodes/ImageBox.scss | 42 +++++++++++----------- src/client/views/nodes/ImageBox.tsx | 24 ++++++------- .../views/nodes/formattedText/FormattedTextBox.tsx | 1 - .../imageMeshTool/imageMeshToolButton.tsx | 15 ++------ 5 files changed, 46 insertions(+), 61 deletions(-) (limited to 'src/client/views/nodes/ImageBox.scss') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 2d39b827d..ab665e984 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -448,8 +448,8 @@ export class DocumentDecorations extends ObservableReactComponent e.shiftKey && dv.ComponentView instanceof ImageBox) .forEach(dv => { - dv.Document._outpaintingOriginalWidth = NumCast(dv.Document._width); - dv.Document._outpaintingOriginalHeight = NumCast(dv.Document._height); + dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalWidth'] = NumCast(dv.Document._width); + dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalHeight'] = NumCast(dv.Document._height); }); setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); e.stopPropagation(); @@ -502,14 +502,15 @@ export class DocumentDecorations extends ObservableReactComponent !e.shiftKey || !(dv.ComponentView instanceof ImageBox)); - // Special handling for shift-drag resize (outpainting of Images) - DocumentView.Selected() - .filter(dv => !notOutpainted.includes(dv)) - .forEach(dv => this.resizeViewForOutpainting(dv, refPt, scale, { dragHdl, shiftKey: e.shiftKey })); // Adjust only the document dimensions without scaling internal content + const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView instanceof ImageBox) : []; + const notOutpainted = e.shiftKey ? DocumentView.Selected().filter(dv => !outpainted.includes(dv)) : DocumentView.Selected(); - // Regular resize behavior for docs not being outpainted + // Special handling for shift-drag resize (outpainting of Images by resizing without scaling content - fill in with firefly GAI) + e.shiftKey && outpainted.forEach(dv => this.resizeViewForOutpainting(dv, refPt, scale, { dragHdl, shiftKey: e.shiftKey })); + + // Special handling for not outpainted Docs when ctrl-resizing (setup native dimesions for modification) e.ctrlKey && notOutpainted.forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions()); + const hasFixedAspect = notOutpainted.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 }; notOutpainted.forEach(docView => this.resizeView(docView, refPt, scaleAspect, { dragHdl, freezeNativeDims: e.ctrlKey })); @@ -546,20 +547,14 @@ export class DocumentDecorations extends ObservableReactComponent { + onPointerUp = () => { SnappingManager.SetIsResizing(undefined); SnappingManager.clearSnapLines(); - // Check if any outpainting needs to be processed - DocumentView.Selected() - .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox) - .forEach(view => (view.ComponentView as ImageBox).processOutpainting()); - this._resizeHdlId = ''; this._resizeUndo?.end(); diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 9fc20ffd4..ac1a6ece9 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -106,7 +106,7 @@ height: 100%; img { object-fit: contain; - height: 100%; + height: fit-content; } .imageBox-fadeBlocker, @@ -249,27 +249,29 @@ background: white; padding: 20px; border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); z-index: 10000; - - h3 { margin-top: 0; } - + + h3 { + margin-top: 0; + } + input { - width: 300px; - padding: 8px; - margin-bottom: 10px; + width: 300px; + padding: 8px; + margin-bottom: 10px; } - + .buttons { - display: flex; - justify-content: flex-end; - gap: 10px; - - .generate-btn { - background: #0078d4; - color: white; - border: none; - padding: 8px 16px; - } + display: flex; + justify-content: flex-end; + gap: 10px; + + .generate-btn { + background: #0078d4; + color: white; + border: none; + padding: 8px 16px; + } } - } \ No newline at end of file +} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 010028af7..31a135fa7 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -170,6 +170,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }, { fireImmediately: true } ); + this._disposers.outpaint = reaction( + () => this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined && !SnappingManager.ShiftKey, + complete => complete && this.openOutpaintPrompt(), + { fireImmediately: true } + ); } componentWillUnmount() { @@ -372,13 +377,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { 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; - } + const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']); + const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']); // Set flag that outpainting is in progress this._outpaintingInProgress = true; @@ -444,8 +444,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this.Document.$ai = true; this.Document.$ai_outpainted = true; this.Document.$ai_outpaint_prompt = customPrompt; - this.Document._outpaintingOriginalWidth = undefined; - this.Document._outpaintingOriginalHeight = undefined; + this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined; + this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined; } else { this.Document._width = origWidth; this.Document._height = origHeight; @@ -465,11 +465,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { } }; - processOutpainting = () => this.openOutpaintPrompt(); - componentUI = () => !this._showOutpaintPrompt ? null : ( -
+

Outpaint Image

() { ref={action((r: HTMLImageElement | null) => (this.imageRef = r))} key="paths" src={srcpath} - style={{ transform, transformOrigin, height: 'fit-content' }} + style={{ transform, transformOrigin, height: this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined ? '100%' : undefined }} onError={action(e => (this._error = e.toString()))} draggable={false} width={nativeWidth} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 98e461a52..d6fa3172d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1349,7 +1349,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const html = data?.getData('text/html'); - const text = data?.getData('text/plain'); const pdfAnchorId = data?.getData('dash/pdfAnchor'); if (html && !pdfAnchorId) { const replaceDivsWithParagraphs = (expr: string) => { diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx index c02a1eb94..e580c7070 100644 --- a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx @@ -13,20 +13,11 @@ interface ButtonContainerProps { btnText: string; imageWidth: number; imageHeight: number; - gridXSize: number; // X subdivisions - gridYSize: number; // Y subdivisions + gridXSize: number; // X subdivisions + gridYSize: number; // Y subdivisions } -export function MeshTransformButton({ - loading, - onClick, - onReset, - btnText, - imageWidth, - imageHeight, - gridXSize, - gridYSize -}: ButtonContainerProps) { +export function MeshTransformButton({ loading, onClick, onReset, btnText, imageWidth, imageHeight, gridXSize, gridYSize }: ButtonContainerProps) { const [showGrid, setShowGrid] = React.useState(false); const [isGridInteractive, setIsGridInteractive] = React.useState(false); // Controls the dragging of control points const imageRef = React.useRef(null); // Reference to the image element -- cgit v1.2.3-70-g09d2 From 95db4029fdfa8b1951c29144fdb9e589b77fd77a Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 30 Apr 2025 22:13:18 -0400 Subject: changing images back to use 100% to work in Safari. --- src/client/views/nodes/ImageBox.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/views/nodes/ImageBox.scss') diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index ac1a6ece9..3adc16879 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -106,7 +106,7 @@ height: 100%; img { object-fit: contain; - height: fit-content; + height: 100%; } .imageBox-fadeBlocker, -- cgit v1.2.3-70-g09d2 From d4659e2bd3ddb947683948083232c26fb1227f39 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 2 May 2025 13:09:24 -0400 Subject: fixed up image outpaint dialog and added options for placementt. --- src/client/views/nodes/ImageBox.scss | 7 +- src/client/views/nodes/ImageBox.tsx | 156 +++++++++++++++++++++++++------ src/server/ApiManagers/FireflyManager.ts | 12 +-- 3 files changed, 135 insertions(+), 40 deletions(-) (limited to 'src/client/views/nodes/ImageBox.scss') diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 3adc16879..9f7a5d03f 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -242,12 +242,9 @@ } } .imageBox-regenerate-dialog { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + position: absolute; background: white; - padding: 20px; + padding: 10px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); z-index: 10000; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 31a135fa7..d16baada6 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,4 +1,4 @@ -import { Button, Colors, EditableText, Size, Type } from '@dash/components'; +import { Button, Colors, EditableText, IconButton, Size, Toggle, ToggleType, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Slider, Tooltip } from '@mui/material'; import axios from 'axios'; @@ -353,6 +353,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { @action openOutpaintPrompt = () => { + this._outpaintVAlign = ''; + this._outpaintAlign = ''; this._showOutpaintPrompt = true; }; @@ -361,6 +363,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this._showOutpaintPrompt = false; }; + @action + cancelOutpaintPrompt = () => { + const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']); + const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']); + this.Document._width = origWidth; + this.Document._height = origHeight; + this._outpaintingInProgress = false; + this.closeOutpaintPrompt(); + }; + @action handlePromptChange = (val: string | number) => { this._outpaintPromptInput = '' + val; @@ -377,17 +389,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (!field) return; - const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']); - const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']); - // Set flag that outpainting is in progress this._outpaintingInProgress = true; // Revert dimensions if prompt is blank (acts like Cancel) if (!customPrompt) { - this.Document._width = origWidth; - this.Document._height = origHeight; - this._outpaintingInProgress = false; + this.cancelOutpaintPrompt(); return; } @@ -410,11 +417,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { loadingOverlay.innerHTML = '
Generating outpainted image...
'; this._mainCont?.appendChild(loadingOverlay); + const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']); + const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']); const response = await Networking.PostToServer('/outpaintImage', { imageUrl: currentPath, prompt: customPrompt, originalDimensions: { width: Math.min(newWidth, origWidth), height: Math.min(newHeight, origHeight) }, newDimensions: { width: newWidth, height: newHeight }, + halignment: this._outpaintAlign, + valignment: this._outpaintVAlign, }); const error = ('error' in response && (response.error as string)) || ''; @@ -447,8 +458,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined; this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined; } else { - this.Document._width = origWidth; - this.Document._height = origHeight; + this.cancelOutpaintPrompt(); alert('Failed to receive a valid image URL from server.'); } batch.end(); @@ -456,30 +466,106 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this._mainCont?.removeChild(loadingOverlay); } catch (error) { - console.error('Error during outpainting:', error); - this.Document._width = origWidth; - this.Document._height = origHeight; - alert('An error occurred while outpainting. Please try again.'); + this.cancelOutpaintPrompt(); + alert('An error occurred while outpainting.' + error); } finally { runInAction(() => (this._outpaintingInProgress = false)); } }; - componentUI = () => + @observable _outpaintAlign = ''; + @observable _outpaintVAlign = ''; + @computed get outpaintVertical() { + return this._props.PanelWidth() / this._props.PanelHeight() < this.nativeSize.nativeWidth / this.nativeSize.nativeHeight; + } + + componentUI = (/* boundsLeft: number, boundsTop: number*/) => !this._showOutpaintPrompt ? null : ( -
-

Outpaint Image

- this.handlePromptChange(val)} - val={this._outpaintPromptInput} - type={Type.TERT} - color={SettingsManager.userColor} - background={SettingsManager.userBackgroundColor} - /> -
-