diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 71 | ||||
-rw-r--r-- | src/server/server_Initialization.ts | 17 | ||||
-rw-r--r-- | src/workers/image.worker.ts | 16 |
3 files changed, 90 insertions, 14 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 617a09ed5..cc747eb32 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -8,7 +8,7 @@ import { extname } from 'path'; import * as React from 'react'; import { AiOutlineSend } from 'react-icons/ai'; import ReactLoading from 'react-loading'; -import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon, returnTrue } from '../../../ClientUtils'; +import { ClientUtils, DashColor, imageUrlToBase64, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -16,10 +16,11 @@ import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast, ImageCastWithSuffix } from '../../../fields/Types'; +import { Cast, DocCast, ImageCast, ImageCastWithSuffix, NumCast, RTFCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; +import { gptImageLabel } from '../../apis/gpt/GPT'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; @@ -45,7 +46,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; -import { gptImageLabel } from '../../apis/gpt/GPT'; const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { @@ -389,7 +389,59 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return cropping; }; - docEditorView = action(() => { + static _worker?: Worker; + static removeImgBackground = (doc: Doc, addDoc: (doc: Doc | Doc[], annotationKey?: string) => boolean, docImgPath: string) => { + ImageEditorData.AddDoc = addDoc; + ImageEditorData.RootDoc = doc; + if (ImageBox._worker) return ImageBox._worker; + const worker = new Worker('/image.worker.js', { type: 'module' }); + worker.onmessage = async (event: MessageEvent) => { + const { success, result, error } = event.data; + if (success) { + const blobToDataURL = (blob: any) => { + return new Promise<string | ArrayBuffer | null>((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = error => reject(error); + reader.readAsDataURL(blob); + }); + }; + blobToDataURL(result).then(durl => { + ClientUtils.convertDataUri(durl as string, doc[Id] + '_noBgd').then(url => { + const width = NumCast(doc._width) || 1; + const height = NumCast(doc._height); + const imageSnapshot = Docs.Create.ImageDocument(url, { + _nativeWidth: Doc.NativeWidth(doc), + _nativeHeight: Doc.NativeHeight(doc), + x: NumCast(doc.x) + width, + y: NumCast(doc.y), + _width: 150, + _height: (height / width) * 150, + title: 'bgremoved:' + doc.title, + }); + Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(doc)); + Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(doc)); + addDoc?.(imageSnapshot); + }); + }); + } else { + console.error('Error in background removal:', error); + } + // worker.terminate(); + }; + worker.onerror = (e: ErrorEvent) => console.error('Worker failed:', e); // worker.terminate(); + + worker.postMessage({ imagePath: docImgPath }); + return worker; + }; + removeBackground = () => { + const field = ImageCast(this.dataDoc[this.fieldKey]); + if (field && this._props.addDocument) { + ImageBox.removeImgBackground(this.rootDoc, this._props.addDocument, this.choosePath(field.url)); + } + }; + + docEditorView = () => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { ImageEditorData.Open = true; @@ -397,7 +449,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ImageEditorData.AddDoc = this._props.addDocument; ImageEditorData.RootDoc = this.Document; } - }); + }; @observable _showOutpaintPrompt: boolean = false; @observable _outpaintPromptInput: string = 'Extend this image naturally with matching content'; @@ -433,7 +485,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action processOutpaintingWithPrompt = async (customPrompt: string) => { - const field = Cast(this.dataDoc[this.fieldKey], ImageField); + const field = ImageCast(this.dataDoc[this.fieldKey]); if (!field) return; // Set flag that outpainting is in progress @@ -618,7 +670,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ); specificContextMenu = (): void => { - const field = Cast(this.dataDoc[this.fieldKey], ImageField); + const field = ImageCast(this.dataDoc[this.fieldKey]); if (field) { const funcs: ContextMenuProps[] = []; funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' }); @@ -629,13 +681,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { file: (file => { const ext = file ? extname(file) : ''; return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href), + })(ImageCast(this.Document[this.fieldKey])?.url.href), }).then(text => alert(text)); }, icon: 'expand-arrows-alt', }); // prettier-ignore funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' }); funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' }); + funcs.push({ description: 'Remove Background', event: this.removeBackground, icon: 'pencil-alt' }); this.layoutDoc.ai && funcs.push({ description: 'Regenerate AI Image', @@ -800,7 +853,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } @computed get paths() { - const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc + const field = ImageCast(this.dataDoc[this.fieldKey], new ImageField(StrCast(this.dataDoc[this.fieldKey]))); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images const defaultUrl = new URL(ClientUtils.prepend(DefaultPath)); const altpaths = diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index 5deb66caf..7915938b7 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -1,3 +1,4 @@ +import axios from 'axios'; import * as bodyParser from 'body-parser'; import { blue, yellow } from 'colors'; import * as flash from 'connect-flash'; @@ -6,22 +7,22 @@ import * as express from 'express'; import * as expressFlash from 'express-flash'; import * as session from 'express-session'; import { createServer } from 'https'; +import { JSDOM } from 'jsdom'; import * as passport from 'passport'; import * as webpack from 'webpack'; import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; import * as config from '../../webpack.config'; +import * as workerConfig from '../../webpack.worker.config'; import { logPort } from './ActionUtilities'; import RouteManager from './RouteManager'; import RouteSubscriber from './RouteSubscriber'; import { publicDirectory, resolvedPorts } from './SocketData'; +import { setupDynamicToolsAPI } from './api/dynamicTools'; import { SSL } from './apis/google/CredentialsLoader'; import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager'; import { Database } from './database'; import { WebSocket } from './websocket'; -import axios from 'axios'; -import { JSDOM } from 'jsdom'; -import { setupDynamicToolsAPI } from './api/dynamicTools'; /* RouteSetter is a wrapper around the server that prevents the server from being exposed. */ @@ -126,13 +127,12 @@ function registerCorsProxy(server: express.Express) { //res.status(400).json({ error: 'Invalid URL format' }); return; } - const isBinary = /\.(gif|png|jpe?g|bmp|webp|ico|pdf|zip|mp3|mp4|wav|ogg)$/i.test(targetUrl as string); const responseType = isBinary ? 'arraybuffer' : 'text'; const response = await axios.get(targetUrl as string, { headers: { 'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0' }, - responseType: responseType + responseType: responseType, }); const baseUrl = new URL(targetUrl as string); @@ -200,8 +200,13 @@ function registerAuthenticationRoutes(server: express.Express) { export default async function InitializeServer(routeSetter: RouteSetter) { const isRelease = determineEnvironment(); const app = buildWithMiddleware(express()); + const workerCompiler = webpack(workerConfig as webpack.Configuration); const compiler = webpack(config as webpack.Configuration); + if (!compiler) throw new Error('Webpack compiler is not defined. Please check your webpack configuration.'); + if (!workerCompiler) throw new Error('Webpack worker compiler is not defined. Please check your webpack worker configuration.'); + // print out contents of virtual output filesystem used in development + // compiler.outputFileSystem?.readdir?.(config.output.path, (err, files) => (err ? console.error('Error reading virtual output path:', err) : console.log('Files in virtual output path:', files))); // Default route app.get('/', (req, res) => { res.redirect(req.user ? '/home' : '/login'); //res.send('This is the default route.'); @@ -209,6 +214,8 @@ export default async function InitializeServer(routeSetter: RouteSetter) { // route table managed by express. routes are tested sequentially against each of these map rules. when a match is found, the handler is called to process the request app.use(wdm(compiler, { publicPath: config.output.publicPath })); app.use(whm(compiler)); + app.use(wdm(workerCompiler, { publicPath: workerConfig.output.publicPath })); + app.use(whm(workerCompiler)); app.get(/^\/+$/, (req, res) => res.redirect(req.user ? '/home' : '/login')); // target urls that consist of one or more '/'s with nothing in between app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader('Access-Control-Allow-Origin', '*') })); // all urls that start with dash's public directory: /files/ (e.g., /files/images, /files/audio, etc) // app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) })); diff --git a/src/workers/image.worker.ts b/src/workers/image.worker.ts new file mode 100644 index 000000000..d069742f3 --- /dev/null +++ b/src/workers/image.worker.ts @@ -0,0 +1,16 @@ +import { removeBackground } from '@imgly/background-removal'; + +self.onmessage = async (event: MessageEvent) => { + const { imagePath, doc, addDoc } = event.data; + + try { + // Perform the background removal + const result = await removeBackground(imagePath); + + // Send the result back to the main thread + self.postMessage({ success: true, result, doc, addDoc }); + } catch (error) { + // Send the error back to the main thread + self.postMessage({ success: false, error: (error as any).message }); + } +}; |