aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/views/nodes/ImageBox.tsx71
-rw-r--r--src/server/server_Initialization.ts17
-rw-r--r--src/workers/image.worker.ts16
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 });
+ }
+};