aico-image-editor
Version:
Combine multiple image into and create single combined image
1,305 lines (1,102 loc) • 1.85 MB
JavaScript
(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 ***!
\********************************************