diff options
author | bobzel <zzzman@gmail.com> | 2024-12-11 13:11:43 -0500 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-12-11 13:11:43 -0500 |
commit | a5a7769e0c400f0a58a9b53ae13b338a26eaa919 (patch) | |
tree | c6705b180714b0cbe03a23b36f4b6014d34aca9a | |
parent | 48c3b802a3c8fd446ecbd33747fe702b11170dfe (diff) |
fixes for uploading remote images to sample and save them properly. extnesions fo firefly manager to query text in images, expand images, and tweaks to generate images.
-rw-r--r-- | package-lock.json | 23 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 31 | ||||
-rw-r--r-- | src/server/ApiManagers/FireflyManager.ts | 114 | ||||
-rw-r--r-- | src/server/ApiManagers/UploadManager.ts | 8 | ||||
-rw-r--r-- | src/server/DashUploadUtils.ts | 37 |
6 files changed, 183 insertions, 33 deletions
diff --git a/package-lock.json b/package-lock.json index 05f6d540f..46057f615 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,6 +100,7 @@ "depcheck": "^1.4.7", "dompurify": "^3.1.7", "dotenv": "^16.4.5", + "dropbox": "^10.34.0", "eslint-webpack-plugin": "^4.1.0", "exif": "^0.6.0", "exifr": "^7.1.3", @@ -128,7 +129,7 @@ "google-auth-library": "^9.4.1", "googleapis": "^144.0.0", "googlephotos": "^0.3.5", - "got": "^14.0.0", + "got": "^14.4.5", "howler": "^2.2.4", "html-to-image": "^1.11.11", "html-to-text": "^9.0.5", @@ -19701,6 +19702,20 @@ "resolved": "https://registry.npmjs.org/double-bits/-/double-bits-1.1.1.tgz", "integrity": "sha512-BCLEIBq0O/DWoA7BsCu/R+RP0ZXiowP8BhtJT3qeuuQEBpnS8LK/Wo6UTJQv6v8mK1fj8n90YziHLwGdM5whSg==" }, + "node_modules/dropbox": { + "version": "10.34.0", + "resolved": "https://registry.npmjs.org/dropbox/-/dropbox-10.34.0.tgz", + "integrity": "sha512-5jb5/XzU0fSnq36/hEpwT5/QIep7MgqKuxghEG44xCu7HruOAjPdOb3x0geXv5O/hd0nHpQpWO+r5MjYTpMvJg==", + "dependencies": { + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=0.10.3" + }, + "peerDependencies": { + "@types/node-fetch": "^2.5.7" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -22149,9 +22164,9 @@ } }, "node_modules/got": { - "version": "14.4.4", - "resolved": "https://registry.npmjs.org/got/-/got-14.4.4.tgz", - "integrity": "sha512-tqiF7eSgTBwQkxb1LxsEpva8TaMYVisbhplrFVmw9GQE3855Z+MH/mnsXLLOkDxR6hZJRFMj5VTAZ8lmTF8ZOA==", + "version": "14.4.5", + "resolved": "https://registry.npmjs.org/got/-/got-14.4.5.tgz", + "integrity": "sha512-sq+uET8TnNKRNnjEOPJzMcxeI0irT8BBNmf+GtZcJpmhYsQM1DSKmCROUjPWKsXZ5HzwD5Cf5/RV+QD9BSTxJg==", "dependencies": { "@sindresorhus/is": "^7.0.1", "@szmarczak/http-timer": "^5.0.1", diff --git a/package.json b/package.json index 949f76599..7ca0c8b38 100644 --- a/package.json +++ b/package.json @@ -183,6 +183,7 @@ "depcheck": "^1.4.7", "dompurify": "^3.1.7", "dotenv": "^16.4.5", + "dropbox": "^10.34.0", "eslint-webpack-plugin": "^4.1.0", "exif": "^0.6.0", "exifr": "^7.1.3", @@ -211,7 +212,7 @@ "google-auth-library": "^9.4.1", "googleapis": "^144.0.0", "googlephotos": "^0.3.5", - "got": "^14.0.0", + "got": "^14.4.5", "howler": "^2.2.4", "html-to-image": "^1.11.11", "html-to-text": "^9.0.5", diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index ff879a2ab..8c7ec959e 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -38,6 +38,8 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; +import { Upload } from '../../../server/SharedMediaTypes'; +import { ImageUtils } from '../../util/Import & Export/ImageUtils'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define @@ -309,6 +311,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' }); funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' }); funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' }); + funcs.push({ + description: 'GetImageText', + event: () => { + Networking.PostToServer('/queryFireflyImageText', { + file: (file => { + const ext = extname(file); + return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + }).then(text => alert(text)); + }, + icon: 'expand-arrows-alt', + }); + funcs.push({ + description: 'Expand Image', + event: () => { + Networking.PostToServer('/expandImage', { + prompt: 'sunny skies', + file: (file => { + const ext = extname(file); + return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + }).then((info: Upload.ImageInformation) => { + const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title }); + DocUtils.assignImageInfo(info, img); + this._props.addDocTab(img, OpenWhere.addRight); + }); + }, + icon: 'expand-arrows-alt', + }); funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' }); funcs.push({ description: 'Open Image Editor', diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts index cc4c218bf..d757a23fe 100644 --- a/src/server/ApiManagers/FireflyManager.ts +++ b/src/server/ApiManagers/FireflyManager.ts @@ -1,7 +1,11 @@ +import { Dropbox, files } from 'dropbox'; +import * as fs from 'fs'; +import * as multipart from 'parse-multipart-data'; +import * as path from 'path'; import { DashUploadUtils } from '../DashUploadUtils'; -import { _invalid, _success, Method } from '../RouteManager'; +import { _error, _invalid, _success, Method } from '../RouteManager'; +import { Directory, filesDirectory } from '../SocketData'; import ApiManager, { Registration } from './ApiManager'; -import * as multipart from 'parse-multipart-data'; export default class FireflyManager extends ApiManager { getBearerToken = () => @@ -13,11 +17,11 @@ export default class FireflyManager extends ApiManager { body: `grant_type=client_credentials&client_id=${process.env._CLIENT_FIREFLY_CLIENT_ID}&client_secret=${process.env._CLIENT_FIREFLY_SECRET}&scope=openid,AdobeID,session,additional_info,read_organizations,firefly_api,ff_apis`, }).catch(error => { console.error('Error:', error); - return ''; + return undefined; }); - askFirefly = (prompt: string = 'a realistic illustration of a cat coding') => { + generateImage = (prompt: string = 'a realistic illustration of a cat coding') => { const fetched = this.getBearerToken().then(response => - (response as Response).json().then((data: { access_token: string }) => + response?.json().then((data: { access_token: string }) => fetch('https://firefly-api.adobe.io/v3/images/generate', { method: 'POST', headers: [ @@ -28,20 +32,66 @@ export default class FireflyManager extends ApiManager { ], body: `{ "prompt": "${prompt}" }`, }) - .then(response2 => response2.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image))) + .then(response2 => response2.json().then(json => (json.outputs?.[0] as { image: { url: string } })?.image.url)) .catch(error => { console.error('Error:', error); - return ''; + return undefined; }) ) ); return fetched; }; - getImageText = (testshotpng: Blob) => { + expandImage = (imgUrl: string, prompt?: string) => { + const dropboxImgUrl = imgUrl; + const fetched = this.getBearerToken().then(response => + response + ?.json() + .then((data: { access_token: string }) => { + return 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 ${data.access_token}`], + ], + body: JSON.stringify({ + image: { + source: { + url: dropboxImgUrl, + }, + }, + numVariations: 1, + seeds: [0], + size: { + width: 3048, + height: 2048, + }, + prompt: prompt ?? 'cloudy skies', + placement: { + inset: { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + alignment: { + horizontal: 'center', + vertical: 'center', + }, + }, + }), + }); + }) + .then(resp => resp.json()) + ); + return fetched; + }; + getImageText = (imageBlob: Blob) => { const inputFileVarName = 'infile'; const outputVarName = 'result'; const fetched = this.getBearerToken().then(response => - (response as Response).json().then((data: { access_token: string }) => { + response?.json().then((data: { access_token: string }) => { return fetch('https://sensei.adobe.io/services/v2/predict', { method: 'POST', headers: [ @@ -51,7 +101,7 @@ export default class FireflyManager extends ApiManager { ['Authorization', `Bearer ${data.access_token}`], ], body: ((form: FormData) => { - form.set(inputFileVarName, testshotpng); + form.set(inputFileVarName, imageBlob); form.set( 'contentAnalyzerRequests', JSON.stringify({ @@ -98,7 +148,7 @@ export default class FireflyManager extends ApiManager { multipart .parse(Buffer.from(arrayBuffer), 'Boundary' + (response2.headers.get('content-type')?.match(/=Boundary(.*);/)?.[1] ?? '')) .filter(part => part.name === outputVarName) - .map(part => JSON.parse(part.data.toString()[0])) + .map(part => JSON.parse(part.data.toString())[0]) .reduce((text, json) => text + (json?.is_text_present ? json.tags.map((tag: { text: string }) => tag.text).join(' ') : ''), '') ) .catch(error => { @@ -117,10 +167,10 @@ export default class FireflyManager extends ApiManager { method: Method.POST, subscription: '/queryFireflyImage', secureHandler: ({ req, res }) => - this.askFirefly(req.body.prompt).then(fire => - DashUploadUtils.UploadImage(JSON.parse(fire).url).then(info => { + this.generateImage(req.body.prompt).then(url => + DashUploadUtils.UploadImage(url ?? '').then(info => { if (info instanceof Error) _invalid(res, info.message); - else _success(res, info.accessPaths.agnostic.client); + else _success(res, info); }) ), }); @@ -128,6 +178,7 @@ export default class FireflyManager extends ApiManager { register({ method: Method.POST, subscription: '/queryFireflyImageText', + // eslint-disable-next-line @typescript-eslint/no-unused-vars secureHandler: ({ req, res }) => fetch('http://localhost:1050/files/images/testshot.png').then(json => json.blob().then(file => @@ -137,5 +188,40 @@ export default class FireflyManager extends ApiManager { ) ), }); + register({ + method: Method.POST, + subscription: '/expandImage', + secureHandler: ({ req, res }) => + new Promise<void>((resolve, reject) => { + const dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN }); + fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(req.body.file)}`), undefined, (err, contents) => { + if (err) { + console.log('Error: ', err); + reject(); + } else { + dbx.filesUpload({ path: `/Apps/browndash/${path.basename(req.body.file)}`, contents }) + .then(response => { + dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' }).then(link => { + console.log(link.result); + this.expandImage(link.result.link, req.body.prompt).then(text => { + if (text.error_code) _error(res, text.message); + else + DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => { + if (info instanceof Error) _invalid(res, info.message); + else _success(res, info); + resolve(); + }); + }); + }); + }) + .catch(uploadErr => { + console.log(uploadErr); + _error(res, 'upload to dropbox failed'); + reject(); + }); + } + }); + }), + }); } } diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 7bfdd5aec..5a880901b 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -70,10 +70,16 @@ export default class UploadManager extends ApiManager { ]); } else { fileguids.split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, `resampling images`)); + // original filenames with '.'s, such as a Macbook screenshot, can be a problem - their extension is not kept in formidable's newFilename. + // This makes sure that the extension is preserved in the newFilename. + const fixNewFilename = (f: formidable.File) => { + if (path.extname(f.originalFilename ?? '') !== path.extname(f.newFilename)) f.newFilename = f.newFilename + path.extname(f.originalFilename ?? ''); + return f; + }; const results = ( await Promise.all( Array.from(Object.keys(files)).map( - async key => (!files[key] ? undefined : DashUploadUtils.upload(files[key]![0] /* , key */)) // key is the guid used by the client to track upload progress. + async key => (!files[key] ? undefined : DashUploadUtils.upload(fixNewFilename(files[key][0]) /* , key */)) // key is the guid used by the client to track upload progress. ) ) ).filter(result => result && !(result.result instanceof Error)); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 028116779..a06fa3b6e 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -22,7 +22,6 @@ import { AzureManager } from './ApiManagers/AzureManager'; import { AcceptableMedia, Upload } from './SharedMediaTypes'; import { Directory, clientPathToFile, filesDirectory, pathToDirectory, publicDirectory, serverPathToFile } from './SocketData'; import { resolvedServerUrl } from './server_Initialization'; - import { Worker, isMainThread, parentPort } from 'worker_threads'; // Create an array to store worker threads @@ -55,7 +54,8 @@ if (isMainThread) { .then(img => sizes.forEach(({ width, suffix }) => img.resize({ w: width || img.bitmap.width }) - .write(InjectSize(outputPath, suffix) as `${string}.${string}`) + .write(InjectSize(outputPath, suffix) as `${string}.${string}`) + .catch(e => console.log("Jimp error:", e)) )) .catch(e => console.log('Error Jimp:', e)) .finally(() => unlinkSource && unlinkSync(imgSourcePath)); @@ -343,15 +343,24 @@ export namespace DashUploadUtils { const outputPath = path.resolve(pathToDirectory(Directory.images), outputFileName); const sizes = imageResampleSizes(path.extname(outputFileName)); - const imgReadStream = new Duplex(); - imgReadStream.push(fs.readFileSync(imgSourcePath)); - imgReadStream.push(null); - await Promise.all( - sizes.map(({ suffix }) => - new Promise<unknown>(res => - imgReadStream.pipe(createWriteStream(writtenFiles[suffix] = InjectSize(outputPath, suffix))).on('close', res) - ) - )); // prettier-ignore + if (unlinkSource) { + const imgReadStream = new Duplex(); + imgReadStream.push(fs.readFileSync(imgSourcePath)); + imgReadStream.push(null); + await Promise.all( + sizes.map(({ suffix }) => + new Promise<unknown>(res => + imgReadStream.pipe(createWriteStream(writtenFiles[suffix] = InjectSize(outputPath, suffix))).on('close', res) + ) + )); // prettier-ignore + } else { + await Promise.all( + sizes.map(({ suffix }) => + new Promise<unknown>(res => + request.get(imgSourcePath).pipe(createWriteStream(writtenFiles[suffix] = InjectSize(outputPath, suffix))).on('close', res) + ) + )); // prettier-ignore + } workerResample(imgSourcePath, outputPath, SizeSuffix.Original, unlinkSource); return writtenFiles; @@ -450,7 +459,7 @@ export namespace DashUploadUtils { } const outputFile = filename || result.filename || ''; - return UploadInspectedImage(result, outputFile, prefix); + return UploadInspectedImage(result, outputFile, prefix, isLocal().exec(source) ? true : false); }; type md5 = 'md5'; @@ -568,7 +577,9 @@ export namespace DashUploadUtils { switch (category) { case 'image': if (imageFormats.includes(format)) { - const result = await UploadImage(filepath, basename(filepath)); + const outputName = basename(filepath); + const extname = path.extname(originalFilename ?? ''); + const result = await UploadImage(filepath, outputName.endsWith(extname) ? outputName : outputName + extname, undefined); return { source: file, result }; } fs.unlink(filepath, () => {}); |