UNPKG

aico-image-editor

Version:

Combine multiple image into and create single combined image

1,305 lines (1,102 loc) 1.85 MB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["aicoEditor"] = factory(); else root["aicoEditor"] = factory(); })(self, () => { return /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ "./src/bgshapecroppermodalstore.js": /*!*****************************************!*\ !*** ./src/bgshapecroppermodalstore.js ***! \*****************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var cropperjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! cropperjs */ "../node_modules/cropperjs/dist/cropper.js"); /* harmony import */ var cropperjs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cropperjs__WEBPACK_IMPORTED_MODULE_0__); const elementStore = Alpine.store('elements'); Alpine.store('uploadStore', { uploadFilesToServer(uploadObj) { const {files,action, type, imageType, originalFile,cropperData} = uploadObj; const canvasStore = Alpine.store('canvas'); const formData = new FormData(); formData.append('configuratorId', canvasStore.configuratorId) // append productId when configuratorId is null to first time save as create configurator endpoint if(!canvasStore.configuratorId){ formData.append('productId', canvasStore.productId) } imageType && formData.append('imageType', imageType) cropperData && formData.append('cropperData[]', JSON.stringify(cropperData)); originalFile && formData.append('originalFiles[]', originalFile); for (const file of files) { formData.append(type, file) // appending every file to formdata } //console.log(formData.get('backgrounds')) if(type === 'hexCode') { canvasStore.isColorBlockLoaderVisible = true; } else { canvasStore.isServerLoaderTabVisible = true; } return fetch(action, { method: "POST", body: formData, // "timeout": 0, headers: { "Authorization": `Bearer ${canvasStore.apiConfig.apiToken}`, }, redirect: 'follow' }) .then((response) => { return response.json() }) .then((data) => { canvasStore.isServerLoaderTabVisible = canvasStore.isColorBlockLoaderVisible = false; canvasStore.configuratorId = data.data.configuratorId; //console.log(data); return data; }) .catch((error) => { canvasStore.isServerLoaderTabVisible = canvasStore.isColorBlockLoaderVisible = false; console.error(error); }); }, updateImageInServer(editUploadObj) { const {files, action, type, cropperData} = editUploadObj; const canvasStore = Alpine.store('canvas'); const formData = new FormData(); cropperData && formData.append('cropperData', JSON.stringify(cropperData)); for (const file of files) { formData.append(type, file) // appending every file to formdata } canvasStore.isServerLoaderTabVisible = true; return fetch(action, { method: "POST", body: formData, // "timeout": 0, headers: { "Authorization": `Bearer ${canvasStore.apiConfig.apiToken}`, }, redirect: 'follow' }) .then((response) => { return response.json() }) .then((data) => { canvasStore.isServerLoaderTabVisible = false; //console.log(data); return data; }) .catch((error) => { canvasStore.isServerLoaderTabVisible = false; console.error(error); }); }, uploadImageWithoutConfigurator(uploadObj) { const canvasStore = Alpine.store('canvas'); const formData = new FormData(); const {files, action} = uploadObj; for (const file of files) { formData.append('layerFile', file) } canvasStore.isServerLoaderCanvasVisible = true; return fetch(action, { method: "POST", body: formData, // "timeout": 0, headers: { "Authorization": `Bearer ${canvasStore.apiConfig.apiToken}`, }, redirect: 'follow' }) .then((response) => { if (!response.ok) { throw new Error('Network response was not OK'); } return response.json() }) .then((data) => { canvasStore.isServerLoaderCanvasVisible = false; //console.log(data); return data; }) .catch((error) => { canvasStore.isServerLoaderCanvasVisible = false; console.error(error); }); }, deleteFilesFromServer(id, type,fileName) { const canvasStore = Alpine.store('canvas'); const formData = new FormData(); formData.append('productId', this.$store.canvas.buganoProductId) formData.append('fileID', id) formData.append('fileName', fileName) formData.append('type', type) canvasStore.isServerLoaderTabVisible = true; return fetch('http://localhost:6288/delete', { method: "DELETE", body: formData, }) .then((response) => { return response.json() }) .then((data) => { canvasStore.isServerLoaderTabVisible = false; //console.log(data); return data; }) .catch((error) => { console.error(error); }); } }) Alpine.store('bgshapeCropperModalStore', { cropBgImage: null, cropperData: null, originalBackgroundFile: null, editingBackgroundId: null, shouldPushRounded: false, dataURLtoBlob(dataurl) { var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--){ u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], {type:mime}); }, async getFileFromUrl(url, name, defaultType = 'image/jpeg') { const response = await fetch(url); const data = await response.blob(); return new File([data], name, { type: data.type || defaultType, }); }, async editExistingBackground(background) { //const originalBackgroundFile = await this.getFileFromUrl(background.originalUrl); this.editingBackgroundId = background.id; const cropperInitObj = { fileName: background.name, url: background.cropperData.originalFileDataUrl, //url: background.originalUrl, cropperData: background.cropperData } this.displaySelectedBackground(cropperInitObj); }, async displaySelectedBackground(cropperInitObj) { const input = elementStore.backgroundUploadEL; const img = document.createElement('img'); img.id = 'js-bgimg-cropper'; img.src = cropperInitObj.url; img.alt = cropperInitObj.fileName; const imgbgcrop = elementStore.imgBgCropEL; imgbgcrop.innerHTML = ''; imgbgcrop.appendChild(img); // this represents current background image element which was created from file above this.cropBgImage = img; this.cropperData = cropperInitObj.cropperData; if(cropperInitObj.originalBackgroundFile) { this.originalBackgroundFile = cropperInitObj.originalBackgroundFile; } // open modal if not open and if alreddy open then init background cropper if (elementStore.cropbgModalEL.classList.contains('show')) { this.initBackgroundCropper(img) } else { bootstrap.Modal.getOrCreateInstance(elementStore.cropbgModalEL).show() } }, initBackgroundCropper(img) { let self = this; const input = elementStore.backgroundUploadEL; input.value = ''; this.aspectRatio = '1'; window.dispatchEvent(new CustomEvent('backgrounds-cleard')) //destroy old cropper before initializing new one this.destroyCropper() new (cropperjs__WEBPACK_IMPORTED_MODULE_0___default())(img, { crop: function (event) { self.cropBoxWidth = Math.round(event.detail.width); self.cropBoxHeight = Math.round(event.detail.height); }, aspectRatio: 1, autoCropArea: 1, preview: elementStore.cropperBgPreviewEL, ready: function () { if(self.cropperData) { this.cropper.setData(self.cropperData); } }, }); }, cropBoxWidth: 0, cropBoxHeight: 0, aspectRatio: '1', clear() { const cropperInstance = this.cropBgImage.cropper; cropperInstance.clear() }, destroyCropper() { this.cropBgImage?.cropper?.destroy(); }, async crop() { return new Promise((resolve, reject) => { const cropperInstance = this.cropBgImage.cropper; const input = elementStore.backgroundUploadEL; let self = this; const croppedCanvas = cropperInstance.getCroppedCanvas(); let finalCanvas = croppedCanvas; if (self.shouldPushRounded) { finalCanvas = self.getRoundedCanvas(croppedCanvas); } finalCanvas.toBlob(blob => { if (blob) { const dt = new DataTransfer(); dt.items.add(new File([blob], self.cropBgImage.alt, { type: blob.type })); input.files = dt.files; window.dispatchEvent(new CustomEvent('background-uploaded', { detail: { name: self.cropBgImage.alt, label: self.cropBgImage.alt, url: URL.createObjectURL(blob), class: self.shouldPushRounded ? 'rounded' : '', type: blob.type, } })); resolve(dt.files); } else { reject("Failed to generate blob"); } }); }); }, async processUpload($store) { let self = this; await this.crop(); const input = elementStore.backgroundUploadEL; const cropperData = Object.assign({originalFileDataUrl: self.cropBgImage.src} ,self.cropBgImage?.cropper.getData()); if(input.files.length) { const uploadObj = { files: input.files, action: `${$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/${$store.canvas.configuratorId || null}/images`, type: 'backgrounds[]', imageType: 'BACKGROUND', originalFile: this.originalBackgroundFile, cropperData: cropperData } const data = await $store.uploadStore.uploadFilesToServer(uploadObj); data?.data?.images?.forEach((background) => { window.dispatchEvent(new CustomEvent('backgrounds-added-from-api', { detail: { backgrounds: [background] } })); }); //productBlockVisibleMobile = true; } }, async processUpdate($store) { let self = this; await this.crop(); const input = elementStore.backgroundUploadEL; const cropperData = Object.assign({originalFileDataUrl: self.cropBgImage.src} ,self.cropBgImage?.cropper.getData()); if(input.files.length) { const editUploadObj = { files: input.files, action: `${$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/images/${self.editingBackgroundId}`, type: 'image', cropperData: cropperData } const data = await $store.uploadStore.updateImageInServer(editUploadObj); //here for update case background is directly returned as data.data and no further background property under that if(data?.data) { window.dispatchEvent(new CustomEvent('backgrounds-upated-from-api', { detail: { backgrounds: [data?.data] } })); } //productBlockVisibleMobile = true; } } }) /***/ }), /***/ "./src/components/aiModal/aiModal.js": /*!*******************************************!*\ !*** ./src/components/aiModal/aiModal.js ***! \*******************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var i18next__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! i18next */ "../node_modules/i18next/dist/esm/i18next.js"); /* harmony import */ var fabric__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fabric */ "fabric"); /* harmony import */ var fabric__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fabric__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var _initStyles__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../initStyles */ "./src/initStyles.js"); /* harmony import */ var _initHTML__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../initHTML */ "./src/initHTML.js"); /* harmony import */ var _infoPopover_infoPopover__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../infoPopover/infoPopover */ "./src/components/infoPopover/infoPopover.js"); /* harmony import */ var _dropdownMenu_dropdownMenu__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../dropdownMenu/dropdownMenu */ "./src/components/dropdownMenu/dropdownMenu.js"); /* harmony import */ var _utilities_bubbltPosition__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../utilities/bubbltPosition */ "./src/utilities/bubbltPosition.js"); //import Alpine from 'alpinejs'; const loadAIModalHTML = () => Promise.resolve(/*! import() eager */).then(__webpack_require__.t.bind(__webpack_require__, /*! ./aiModal.html */ "./src/components/aiModal/aiModal.html", 17)); // initialize custom made styles and html loading modules// // inspired by alpine js component// (0,_initHTML__WEBPACK_IMPORTED_MODULE_3__["default"])('ai-modal', loadAIModalHTML) ; const apiKey = document.currentScript.dataset.openaiApiKey; document.addEventListener('alpine:init', function() { Alpine.data('infoPopover',_infoPopover_infoPopover__WEBPACK_IMPORTED_MODULE_4__["default"]) Alpine.data('dropdownMenu', _dropdownMenu_dropdownMenu__WEBPACK_IMPORTED_MODULE_5__["default"]); }) // for adding objects as we drag let isMouseDown, mouseDownPointer, selectionRect, dashOffset = 0, animationId; let frameCounter = 0, framesPerUpdate = 10; let animate = function() { frameCounter++; if(selectionRect) { // Only update dashOffset every 'framesPerUpdate' frames if (frameCounter % framesPerUpdate === 0) { // Increment the offset dashOffset++; if (dashOffset > 100) { // Reset after reaching a certain value dashOffset = 0; } selectionRect.set({strokeDashOffset: -dashOffset}); // Move the dashes __aiCanvas.renderAll() } // Request the next animation frame and update the animationId animationId = requestAnimationFrame(animate); } else { cancelAnimationFrame(animationId); } } /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (() => ({ init() { ;(0,_initStyles__WEBPACK_IMPORTED_MODULE_2__["default"])(this.$el.shadowRoot); }, get addToType() { return (this.aIImageUploadMode === 'mainPicture') ? i18next__WEBPACK_IMPORTED_MODULE_0__["default"].t('pictures', {lng: this.$store.canvas.currentLocale}) : (this.aIImageUploadMode === 'motive') ? i18next__WEBPACK_IMPORTED_MODULE_0__["default"].t('motiveSubTabTitle', {lng: this.$store.canvas.currentLocale}) : i18next__WEBPACK_IMPORTED_MODULE_0__["default"].t('background', {lng: this.$store.canvas.currentLocale}) }, apiKey: apiKey, maxFileSize: 4, imageSize: '512x512', response_format: 'b64_json', aiWelcomeText: "Welcome to the enhancement provided using the computational power of AI", freeDrawingBrushWidth: 10, setBubblePosition: _utilities_bubbltPosition__WEBPACK_IMPORTED_MODULE_6__.setBubblePosition, initAICanvas(el) { const aiCanvas = window.__aiCanvas = new (fabric__WEBPACK_IMPORTED_MODULE_1___default().Canvas)(el, { preserveObjectStacking: true, uniformScaling: true, isDrawingMode: true, //stateful: true, uniScaleTransform: true, backgroundColor: 'white' }); this.setAICanvasDimensions(aiCanvas); if(this.$store.canvas.aiImageSrc) { this.replaceImageInAICanvas(this.$store.canvas.aiImageSrc) } __canvas.getActiveObject() && this.$dispatch('object-added-to-ai-canvas', { object: __canvas.getActiveObject() }) //initialize free drawing brush once aiCanvas.freeDrawingBrush = new (fabric__WEBPACK_IMPORTED_MODULE_1___default().EraserBrush)(aiCanvas); // set brush size and it;s icon accordingly this.setupFreeDrawingBrush(aiCanvas); }, setupFreeDrawingBrush(aiCanvas) { this.freeDrawingBrushWidth = parseInt(this.freeDrawingBrushWidth) aiCanvas.freeDrawingBrush.width = this.freeDrawingBrushWidth; const cursorUrl = this.getCursorCircleImageUrl() aiCanvas.freeDrawingCursor = `url(${cursorUrl}) ${this.freeDrawingBrushWidth/2} ${this.freeDrawingBrushWidth/2}, auto`; aiCanvas.hoverCursor = this.activeDrawMode === 'create' ? 'crosshair' : 'move'; }, getCursorCircleImageUrl() { const canvas = document.createElement("canvas"); //canvas width will be always +2 more than brush width so that circle can pass through first and last coordinate without clipping canvas.width = canvas.height = this.freeDrawingBrushWidth + 2; const ctx = canvas.getContext("2d"); ctx.beginPath(); ctx.arc(this.freeDrawingBrushWidth/2 + 1, this.freeDrawingBrushWidth/2 + 1, this.freeDrawingBrushWidth/2, 0, 2 * Math.PI); ctx.stroke(); return canvas.toDataURL(); }, activeDrawMode: 'erase', activeOperationID: 'aiOperation-0', get activeOperationLabel() { return this.aiOperations.find(aiOperation => aiOperation.id === this.activeOperationID).label }, aiOperations: [ { id: 'aiOperation-0', label: i18next__WEBPACK_IMPORTED_MODULE_0__["default"].t('generateNewImage'), },{ id: 'aiOperation-1', label: i18next__WEBPACK_IMPORTED_MODULE_0__["default"].t('editImage') }, { id: 'aiOperation-2', label: i18next__WEBPACK_IMPORTED_MODULE_0__["default"].t('generateVariantImage') } ], prompt: '', hideErrorMesssages() { this.showPromptError = this.showEmptyCanvasError = this.showFileSizeValidationError = this.showServerError = false; }, get showAllErrors() { return this.showPromptError || this.showEmptyCanvasError || this.showFileSizeValidationError || this.showServerError; }, showPromptError: false, showEmptyCanvasError: false, showFileSizeValidationError: false, showServerError: false, setAICanvasDimensions(aiCanvas) { const aiCanvasResizeEL = Alpine.store('elements').aiCanvasResizeEL; const offsetWidth = aiCanvasResizeEL.offsetWidth - 2; const offsetHeight = aiCanvasResizeEL.offsetWidth - 2; const containerSize = { width: offsetWidth, height: offsetHeight, } aiCanvas.setDimensions(containerSize); aiCanvas.renderAll(); }, updateActiveAIOperation(activeOperationID) { this.activeOperationID = activeOperationID; }, averageBrightness: 255, async getAverageBrightness(url) { return new Promise(resolve => { const img = document.createElement('img'); img.crossOrigin = 'anonymous'; img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, img.width, img.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); let totalBrightness = 0; let count = 0; for(let i = 0; i < imageData.data.length; i += 4) { const r = imageData.data[i]; const g = imageData.data[i + 1]; const b = imageData.data[i + 2]; const a = imageData.data[i + 3]; // Skip transparent pixels if(a === 0) continue; totalBrightness += (r * 299 + g * 587 + b * 114) / 1000; count++; } resolve(totalBrightness / count); }; img.src = url; }); }, decideBorderColorBasedOnBrightness() { return this.averageBrightness > 128 ? '#000000' : '#FFFFFF'; }, aiCanvasImageUrl: '', aiTriggerEL: { ['@object-added-to-ai-canvas.window'](event) { if(event.detail.object.name === 'motive' || event.detail.object.name === 'mainPicture') { // get url of object which is selected on main canvas const url = event.detail.object.getSrc(); this.replaceImageInAICanvas(url); } } }, replaceImageInAICanvas(url) { // here url can be absolute url incase of adding image from main canvas or // dataUrl incase of adding image from openai api response const aiCanvas = window.__aiCanvas; if(aiCanvas) { //clear aiCanvas before replacing it with a new image aiCanvas.getObjects().forEach(function(object) { aiCanvas.remove(object) }) //aiCanvas.clear(); // and then add image from provided url fabric__WEBPACK_IMPORTED_MODULE_1___default().Image.fromURL(url,async function(img) { img.set({ hasControls: false, selectable: false, evented: true, }) this.$store.canvas.fitImageInCanvas(img, aiCanvas) aiCanvas.add(img); //calculate averageBrightness of image after each adding it on aiCanvas this.averageBrightness = await this.getAverageBrightness(url); aiCanvas.renderAll() }.bind(this), {crossOrigin: 'anonymous'}) } // also update the image url which is currently present in canvas this.aiCanvasImageUrl = url; }, setDrawMode(mode) { this.activeDrawMode = mode; const aiCanvas = window.__aiCanvas; // turn off drawing mode (erase/bringback) if it selection is enabled using 3rd button aiCanvas.isDrawingMode = this.activeDrawMode !== 'create'; // setup drawing brush for drawing mode (erase/bringback) or selection create mode this.setupFreeDrawingBrush(aiCanvas); // set inverted to true if it is draw mode to bring back aiCanvas.freeDrawingBrush.inverted = (mode === 'draw'); // register/unregister mouse up/move/down operations when toggling draw mode this.toggleObjectAddEventsViaDrag(); }, generateAIImage() { if (this.activeOperationID === 'aiOperation-0') { this.createImage() } else if (this.activeOperationID === 'aiOperation-1') { this.editExistingImage() } else { this.generateVariantImage() } }, validateCanvas() { const aiCanvas = window.__aiCanvas; // image presence only needs to be checked if it is create image api request this.showEmptyCanvasError = (this.activeOperationID !== 'aiOperation-0' && aiCanvas.getObjects().length === 0); return !this.showEmptyCanvasError; }, checkFileSize(maxSize, file) { this.maxFileSize = maxSize; const MAX_SIZE_BYTES = maxSize * 1024 * 1024; // Convert MB to bytes if (file && typeof file.size === 'number' && !isNaN(file.size)) { const fileSize = file.size; return !(fileSize > MAX_SIZE_BYTES); // File size is within limits } throw new Error('Invalid file object or missing file size'); }, validateFields(originalImageFile, maskedImageFile) { // prompt is not needed if it is generate variant api request this.showPromptError = (this.activeOperationID !== 'aiOperation-2' && this.prompt === ''); //file size does not needed to be checked if it is reate image api request // for edit and variant requests, check if image sizes dont exceed 4mb if(this.activeOperationID !== 'aiOperation-0') { if(originalImageFile) { this.showFileSizeValidationError = !(this.checkFileSize(4, originalImageFile)) } if(maskedImageFile) { this.showFileSizeValidationError = !(this.checkFileSize(4, maskedImageFile)); } } return !this.showPromptError && !this.showFileSizeValidationError; }, isAiModalLoaderVisible: false, startLoader() { this.isAiModalLoaderVisible = true; }, stopLoader() { this.isAiModalLoaderVisible = false; }, base64ToDataUrl(b64_json) { return 'data:image/png;base64,' + b64_json; }, async createImage() { let self = this; if(this.validateFields()) { this.startLoader(); fetch('https://api.openai.com/v1/images/generations',{ method: 'POST', body: JSON.stringify({ 'prompt': self.prompt, 'n': 1, 'size': self.imageSize, 'response_format': self.response_format }), headers: { 'Content-Type': 'application/json', "Authorization": `Bearer ${self.apiKey}`, }, redirect: 'follow' }).then(response => { if (response.ok) { return response.json() } else { throw new Error('Network response was not OK'); } }).then(data => { if(data.error) { this.showServerError = true; } if(data?.data[0]?.b64_json) { const dataUrl = self.base64ToDataUrl(data.data[0].b64_json); this.replaceImageInAICanvas(dataUrl) this.$store.canvas.aiImageSrc = dataUrl; } this.stopLoader(); }).catch(error => { self.showServerError = true; this.stopLoader(); }) } }, logDataUrl(file) { const reader = new FileReader(); reader.onloadend = () => { console.log(reader.result); }; reader.readAsDataURL(file); }, formatFileSize(bytes,decimalPoint = 2) { if(bytes == 0) return '0 Bytes'; let k = 1000, dm = decimalPoint || 2, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; }, async editExistingImage() { let self = this; if(this.validateCanvas()) { const maskedImageFile = await this.getMaskedImageFile(); const originalImageFile = await this.getOriginalImageFile(); if(this.validateFields(originalImageFile,maskedImageFile)) { this.startLoader(); const formdata = new FormData(); formdata.append('response_format', self.response_format); formdata.append('prompt', this.prompt); formdata.append('image', originalImageFile ,'original-image.png'); formdata.append('mask', maskedImageFile ,'masked-image.png'); formdata.append('size', self.imageSize); fetch('https://api.openai.com/v1/images/edits',{ method: 'POST', body: formdata, headers: { "Authorization": `Bearer ${self.apiKey}`, }, redirect: 'follow' }).then(response => { if (response.ok) { return response.json() } else { throw new Error('Network response was not OK'); } }).then(data => { if(data.error) { this.showServerError = true; } if(data?.data[0]?.b64_json) { const dataUrl = self.base64ToDataUrl(data.data[0].b64_json); this.replaceImageInAICanvas(dataUrl) this.$store.canvas.aiImageSrc = dataUrl; } this.stopLoader(); }).catch(error => { self.showServerError = true; this.stopLoader(); }) } } }, async generateVariantImage() { let self = this; const aiCanvas = window.__aiCanvas; if(this.validateCanvas()) { const originalImageFile = await this.getOriginalImageFile(); if(this.validateFields(originalImageFile)) { this.startLoader(); const formdata = new FormData(); formdata.append('response_format', self.response_format); formdata.append('image', originalImageFile ,'original-image.png'); formdata.append('size', self.imageSize); fetch('https://api.openai.com/v1/images/variations',{ method: 'POST', body: formdata, headers: { "Authorization": `Bearer ${self.apiKey}`, }, redirect: 'follow' }).then(response => { if (response.ok) { return response.json() } else { throw new Error('Network response was not OK'); } }).then(data => { if(data.error) { this.showServerError = true; } if(data?.data[0]?.b64_json) { const dataUrl = self.base64ToDataUrl(data.data[0].b64_json); this.replaceImageInAICanvas(dataUrl) this.$store.canvas.aiImageSrc = dataUrl; } this.stopLoader(); }).catch(error => { this.showServerError = true; this.stopLoader(); }) } } }, removeObjectEraser(obj) { delete obj.eraser; obj.dirty = true; }, getBase64(canvas) { return canvas.toDataURL({ format: 'png', multiplier: 2, //multiplier: 10, // for size checking keep this on enableRetinaScaling: true }); }, async getMaskedImageFile() { const aiCanvas = window.__aiCanvas; // as rect composite operation was initallly default (which is source-over), // change it now to "destination-out" to generate masked image by subtracting selection rect from underlying image // and thus making rectangle part pixels transparent when generating base64 using toDataUrl this.subtractSelectionFromImage() const base64Masked = this.getBase64(aiCanvas) return fetch(base64Masked).then(response => response.blob()).then((blob) => { const maskedImageFile = new File([blob],`masked-image.png`, { type: 'image/png', lastModified: Date.now() }) return maskedImageFile }).catch((error) => { console.error(error); }); }, async getOriginalImageFile() { const aiCanvas = window.__aiCanvas; //remove rectangle which was added for indicating masked areas using globalCompositeOperation technique // so that image again reveals itself at those pixels where rectangle was there earlier if(selectionRect) { aiCanvas.remove(selectionRect) } const activeObject = aiCanvas.getObjects()[0]; //store original eraser object in a variable before emptying it const oldErasor = activeObject.eraser; // and then remove object's eraser to get original image and file this.removeObjectEraser(activeObject); const base64Original = this.getBase64(aiCanvas); const originalImageFile = await fetch(base64Original).then(response => response.blob()).then((blob) => { return new File([blob],`original-image.png`, { type: 'image/png', lastModified: Date.now() }) }).catch((error) => { console.error(error); }); activeObject.eraser = oldErasor; activeObject.dirty = true; return originalImageFile; }, isShiftPressed: false, updateShiftKeyState(state) { this.isShiftPressed = state; }, createMouseDownHandler() { return (options) => this.startAddingObject(options); }, createMouseMoveHandler() { return (options) => this.startDrawingObject(options); }, createMouseUpHandler() { return () => this.stopDrawingObject(); }, toggleObjectAddEventsViaDrag() { const aiCanvas = window.__aiCanvas; if(this.activeDrawMode === 'create') { this.mouseDownHandler = this.createMouseDownHandler(); this.mouseMoveHandler = this.createMouseMoveHandler(); this.mouseUpHandler = this.createMouseUpHandler(); aiCanvas.on('mouse:down', this.mouseDownHandler); aiCanvas.on('mouse:move', this.mouseMoveHandler); aiCanvas.on('mouse:up', this.mouseUpHandler); } else { aiCanvas.off('mouse:down', this.mouseDownHandler); aiCanvas.off('mouse:move', this.mouseMoveHandler); aiCanvas.off('mouse:up', this.mouseUpHandler); } }, subtractSelectionFromImage() { if(selectionRect) { selectionRect.set({ fill: 'white', globalCompositeOperation: 'destination-out' }) } __aiCanvas.renderAll(); }, startAddingObject(options) { const aiCanvas = window.__aiCanvas; // these two are common for all objects whether line(arrow), rect, ellipse or text isMouseDown = true; // mouseDownPointer is the cooridinates of the point when mouse is down mouseDownPointer = aiCanvas.getPointer(options.e) // if selectionRect already exists, remove it before creting a new if(selectionRect) { aiCanvas.remove(selectionRect) } const borderColor = this.decideBorderColorBasedOnBrightness(); selectionRect = new (fabric__WEBPACK_IMPORTED_MODULE_1___default().Rect)({ left: mouseDownPointer.x, top: mouseDownPointer.y, width: 0, height: 0, selectable: false, evented: false, erasable: false, fill: 'transparent', stroke: borderColor, strokeDashArray: [3,4], strokeWidth: 2, strokeDashOffset: 1 }) aiCanvas.add(selectionRect); aiCanvas.renderAll(); }, startDrawingObject(options) { const aiCanvas = window.__aiCanvas; if(isMouseDown) { //mouseMovePointer represents the point where mouse is constantly moving // and it is getting updated as it keeps moving const mouseMovePointer = aiCanvas.getPointer(options.e) let width = Math.abs(mouseMovePointer.x - mouseDownPointer.x); let height = Math.abs(mouseMovePointer.y - mouseDownPointer.y); // contraint width and height if shift is pressed // which will be maximum of width and height if(this.isShiftPressed) { width = height = Math.max(width, height); } selectionRect.set({ width: width, height: height, }) aiCanvas.renderAll(); } }, stopDrawingObject() { const aiCanvas = window.__aiCanvas; selectionRect.setCoords(); isMouseDown = false; aiCanvas.renderAll(); // cancel out the previous animation if it already exists before running new loop if(animationId) { cancelAnimationFrame(animationId); } // Start the marching ants animation loop animationId = requestAnimationFrame(animate); }, aIImageUploadMode: 'mainPicture', setAIImageUploadMode(mode) { this.aIImageUploadMode = mode; }, async upoloadAndUse() { let uploadObj, data; const uploadFile = await this.getOriginalImageFile() const dt = new DataTransfer(); dt.items.add(uploadFile); this.startLoader(); switch(this.aIImageUploadMode) { case 'mainPicture': uploadObj = { files: dt.files, action: `${this.$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/${this.$store.canvas.configuratorId || null}/images`, type: 'mainPictures[]', imageType: 'MAIN_IMAGE', originalFile: uploadFile, } data = await this.$store.uploadStore.uploadFilesToServer(uploadObj); data?.data?.images?.forEach((mainPicture) => { window.dispatchEvent(new CustomEvent('main-pictures-added-from-api', { detail: { mainPictures: [mainPicture] } })); this.$store.canvas.addPictureToCanvas(mainPicture.url, 'mainPicture'); }); this.stopLoader(); this.$store.modal.closeModal(this.$store.elements.aiModalEL); this.$dispatch('go-to-tab', {tab: 'picture'}) break; case 'motive': uploadObj = { files: dt.files, action: `${this.$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/${this.$store.canvas.configuratorId || null}/images`, type: 'shapes[]', imageType: 'SHAPE', originalFile: uploadFile, } data = await this.$store.uploadStore.uploadFilesToServer(uploadObj); data?.data?.images?.forEach((shape) => { window.dispatchEvent(new CustomEvent('shapes-added-from-api', { detail: { shapes: [shape] } })); this.$store.canvas.addShapeToCanvas(shape.url, 'motive'); }); this.stopLoader(); this.$store.modal.closeModal(this.$store.elements.aiModalEL); this.$dispatch('go-to-tab', {tab: 'motive'}) this.$dispatch('go-to-subtab', {tab: 'motive'}) break; case 'background': uploadObj = { files: dt.files, action: `${this.$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/${this.$store.canvas.configuratorId || null}/images`, type: 'backgrounds[]', imageType: 'BACKGROUND', originalFile: uploadFile, } data = await this.$store.uploadStore.uploadFilesToServer(uploadObj); data?.data?.images?.forEach((background) => { window.dispatchEvent(new CustomEvent('backgrounds-added-from-api', { detail: { backgrounds: [background] } })); this.$store.canvas.setCanvasBackground(background.url); }); this.stopLoader(); this.$store.modal.closeModal(this.$store.elements.aiModalEL); this.$dispatch('go-to-tab', {tab: 'motive'}); this.$dispatch('go-to-subtab', {tab: 'backgrounds'}) break; default: } }, // background removal api settings backgroundRemovalFormat: 'png', backgroundRemovalSize: 'preview', shouldBackgroundRemovalCrop: false, photoroomAPIMode: 'edit', setPhotoroomAPIMode(mode) { this.photoroomAPIMode = mode; }, async processBackground() { // as this remove endpoint is completely different, // we have two methods here for each of them if(this.photoroomAPIMode === 'remove') { this.removeBackground() } else { this.editBackground() } }, async removeBackground() { if(__aiCanvas.getObjects().length === 0) { this.showEmptyCanvasError = true; return; } const originalImageFile = await this.getOriginalImageFile(); if(!originalImageFile) { return; } this.startLoader(); try { const form = new FormData(); form.append('image_file', originalImageFile); this.backgroundRemovalFormat && form.append('format', this.backgroundRemovalFormat); this.backgroundRemovalSize && form.append('size', this.backgroundRemovalSize); form.append('crop', this.shouldBackgroundRemovalCrop); const response = await fetch('https://sdk.photoroom.com/v1/segment', { method: 'POST', headers: { 'X-Api-Key': 'sandbox_fd16193f5e4337cbafd63e6b6b04171119c4700a' }, body: form }) if (!response.ok) { console.error(response.json()) console.log(response); throw new Error('Network response was not OK'); } // api response from photoroom is blob, so blob can be obtained using response.blob() const imageBlob = await response.blob(); // convert blob to dataurl const dataUrl = await new Promise(r => {let a=new FileReader(); a.onload=r; a.readAsDataURL(imageBlob)}).then(e => e.target.result); this.replaceImageInAICanvas(dataUrl) this.$store.canvas.aiImageSrc = dataUrl; this.stopLoader(); } catch (error) { console.error('Error:', error); self.showServerError = true; this.stopLoader(); } }, prRemoveBackground: true, expandMode: false, referenceBox: 'subjectBox', backgroundPrompt: '', backgroundScaling: 'fill', subjectScaling: 'fit', subjectShadow: '', outputWidth: 1000, outputHeight: 1000, outputMaxWidth: '', outputMaxHeight: '', outputSize: 'originalImage', async editBackground() { if(__aiCanvas.getObjects().length === 0) { this.showEmptyCanvasError = true; return; } const originalImageFile = await this.getOriginalImageFile(); if(!originalImageFile) { return; } if(!this.checkFileSize(30, originalImageFile)) { this.showFileSizeValidationError = true return; } this.startLoader(); const url = 'https://image-api.photoroom.com/v2/edit'; const form = new FormData(); form.append('removeBackground', this.prRemoveBackground); if(this.prRemoveBackground) { this.backgroundPrompt && form.append('background.prompt', this.backgroundPrompt); this.backgroundScaling && form.append('background.scaling', this.backgroundScaling); this.subjectScaling && form.append('scaling', this.subjectScaling); } else { //this.expandMode && form.append('expand.mode', 'ai.auto'); } form.append('imageFile', originalImageFile); form.append('referenceBox', this.referenceBox); form.append('shadow.mode', this.subjectShadow); if(this.outputSize === 'custom' && this.outputWidth && this.outputHeight) { form.append('outputSize', `${this.outputWidth}x${this.outputHeight}`); } if((this.outputSize === 'originalImage') || (this.outputSize === 'croppedSubject')) { form.append('outputSize', this.outputSize) this.outputMaxHeight && form.append('maxHeight', this.outputMaxHeight); this.outputMaxWidth && form.append('maxWidth', this.outputMaxWidth); } const options = { method: 'POST', headers: {Accept: 'image/png, application/json', 'x-api-key': 'sandbox_fd16193f5e4337cbafd63e6b6b04171119c4700a'}, body: form }; try { const response = await fetch(url, options); if (!response.ok) { console.error(response.json()) throw new Error('Network response was not OK'); } // api response from photoroom is blob, so blob can be obtained using response.blob() const imageBlob = await response.blob(); // convert blob to dataurl const dataUrl = await new Promise(r => {let a=new FileReader(); a.onload=r; a.readAsDataURL(imageBlob)}).then(e => e.target.result); this.replaceImageInAICanvas(dataUrl) this.$store.canvas.aiImageSrc = dataUrl; this.stopLoader(); } catch (error) { console.error(error); this.showServerError = true; this.stopLoader(); } } })); /***/ }), /***/ "./src/components/backgroundCropModal/backgroundCropModal.js": /*!*******************************************************************!*\ !*** ./src/components/backgroundCropModal/backgroundCropModal.js ***! \********************************************