diff --git a/mockup_package/mockup/__init__.py b/mockup_package/mockup/__init__.py index 66c9d18..e291fc3 100644 --- a/mockup_package/mockup/__init__.py +++ b/mockup_package/mockup/__init__.py @@ -1 +1,2 @@ from .image import mockup as startMockup # noqa: F401 +from .image import previewMockup # noqa: F401 diff --git a/mockup_package/mockup/image.py b/mockup_package/mockup/image.py index 3b788d7..ca68de7 100755 --- a/mockup_package/mockup/image.py +++ b/mockup_package/mockup/image.py @@ -1,49 +1,84 @@ import base64 import sys from pathlib import Path +from typing import Any from pyodide.http import pyfetch from mockup.image_generator import ImageGenerator as IG import os -async def mockup(location, device_id, original_img_path_list, device_info): - device_path_prefix = f"{location.split('/')[0]}//{location.split('/')[2]}" - device_mask_path_prefix = device_path_prefix + "/images/mockup_mask_templates/" +async def generate( + location: str, + original_img_path: str, + spec: dict[str, Any], + ig: IG, +) -> tuple[str, str, str]: + device_path_prefix: str = f"{location.split('/')[0]}//{location.split('/')[2]}" + device_mask_path_prefix: str = device_path_prefix + "/images/mockup_mask_templates/" device_path_prefix += "/images/mockup_templates/" - device_path = "./device.png" - device_mask_path = "./device_mask.png" - output_img_path_list = [] + device_path: str = "./device.png" + device_mask_path: str = "./device_mask.png" + + try: + await process_response( + device_path_prefix + str(spec["image"]), + device_path, + ) + await process_response( + device_mask_path_prefix + str(spec["image"]), + device_mask_path, + ) + except Exception as e: + print(e, file=sys.stderr) + # js.errorBox(e) + raise + ig.create_fit_coord_image(spec) + deviceView = str(spec["image"]).split("-")[-1].split(".")[0] + path = ( + f"{os.path.splitext(os.path.basename(original_img_path))[0]}" + + f"-{deviceView}.png" + ) + ig.create_mockup_image(device_path, device_mask_path, path) + return (path, original_img_path, deviceView) + + +async def mockup( + location: str, + device_id: str, + original_img_path_list: list[str], + device_info: dict[str, Any], +): + output_img_path_list: list[tuple[str, str, str]] = [] for original_img_path in original_img_path_list: ig = IG(original_img_path, device_id, device_info) ig.create_fit_resolution_image() for spec in ig.phone_models.get(device_id).get("mockups").values(): - try: - await process_response( - device_path_prefix + str(spec["image"]), - device_path, - ) - await process_response( - device_mask_path_prefix + str(spec["image"]), - device_mask_path, - ) - except Exception as e: - print(e, file=sys.stderr) - # js.errorBox(e) - raise - ig.create_fit_coord_image(spec) - deviceView = str(spec["image"]).split("-")[-1].split(".")[0] - path = ( - f"{os.path.splitext(os.path.basename(original_img_path))[0]}" - + f"-{deviceView}.png" + output_img_path_list.append( + await generate(location, original_img_path, spec, ig) ) - ig.create_mockup_image(device_path, device_mask_path, path) - output_img_path_list.append([path, original_img_path, deviceView]) original_img_path_list.clear() return output_img_path_list -async def download(url): +async def previewMockup( + location: str, + device_id: str, + original_img_path: str, + device_info: dict[str, Any], + preview_orientation_index: int = 0, +): + ig = IG(original_img_path, device_id, device_info) + ig.create_fit_resolution_image() + spec = list(ig.phone_models.get(device_id).get("mockups").values())[ + preview_orientation_index + ] + output_img_path = await generate(location, original_img_path, spec, ig) + + return output_img_path + + +async def download(url: str): filename = Path(url).name response = await pyfetch(url) if response.status == 200: @@ -57,7 +92,7 @@ async def download(url): return filename, status -async def process_response(url, path): +async def process_response(url: str, path: str): response_content = await download(url) if response_content[1] == 200: data = base64.b64encode(open(response_content[0], "rb").read()) diff --git a/public/image_process.py b/public/image_process.py index 20751d3..62b432e 100644 --- a/public/image_process.py +++ b/public/image_process.py @@ -2,11 +2,13 @@ import io import os -from js import Uint8Array, imageUploadList +from js import Uint8Array, imageUploadList, imageUpload from PIL import Image -async def upload_single_image(origin_image, file_name, original_img_path_list): +async def upload_single_image_and_save_to_list( + origin_image, file_name, original_img_path_list +): array_buf = Uint8Array.new(await origin_image.arrayBuffer()) bytes_list = bytearray(array_buf) origin_bytes = io.BytesIO(bytes_list) @@ -16,17 +18,51 @@ async def upload_single_image(origin_image, file_name, original_img_path_list): my_image.save(filePath) +async def upload_single_image(origin_image, file_name): + array_buf = Uint8Array.new(await origin_image.arrayBuffer()) + bytes_list = bytearray(array_buf) + origin_bytes = io.BytesIO(bytes_list) + my_image = Image.open(origin_bytes) + filePath = f"./{file_name}.png" + my_image.save(filePath) + return filePath + + +async def upload_file(): + basename, ext = os.path.splitext(imageUpload.name) + if ext.lower() not in [".psd", ".jpg", ".jpeg", ".png"]: + return + original_img_path = await upload_single_image(imageUpload, basename) + return original_img_path + + async def upload_files(): original_img_path_list = [] for fileItem in imageUploadList: basename, ext = os.path.splitext(fileItem.name) if ext.lower() not in [".psd", ".jpg", ".jpeg", ".png"]: return - await upload_single_image(fileItem, basename, original_img_path_list) + await upload_single_image_and_save_to_list( + fileItem, basename, original_img_path_list + ) return original_img_path_list -def save_image(imageList): +def save_image(image): + print("image", image) + path = image[0] + my_image = Image.open(path) + my_stream = io.BytesIO() + my_image.save(my_stream, format="PNG") + binary_fc = open(path, "rb").read() + base64_utf8_str = base64.b64encode(binary_fc).decode("utf-8") + basename, ext = os.path.splitext(path) + dataurl = f"data:image/{ext};base64,{base64_utf8_str}" + print(basename) + return [f"img{basename}", dataurl] + + +def save_images(imageList): returnList = [] for image in imageList: path = image[0] diff --git a/public/mockup.zip b/public/mockup.zip index d78ad2d..b7ef8e2 100644 Binary files a/public/mockup.zip and b/public/mockup.zip differ diff --git a/public/scripts/models/image-upload.js b/public/scripts/models/image-upload.js index 513ef0c..24ea536 100644 --- a/public/scripts/models/image-upload.js +++ b/public/scripts/models/image-upload.js @@ -20,6 +20,8 @@ class ImageUpload { signedData = null; readState = ReadState.ReadyForRead; message = null; + ulid = null; + previewUrl = null; loadDimensionPromise = null; diff --git a/public/scripts/preview_worker.js b/public/scripts/preview_worker.js new file mode 100644 index 0000000..37b9c9f --- /dev/null +++ b/public/scripts/preview_worker.js @@ -0,0 +1,69 @@ +importScripts("https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"); + +async function initianPyodide() { + console.log("start startup"); + const pyodide = await loadPyodide(); + await pyodide.loadPackage(["numpy", "opencv-python", "pillow", "micropip"]); + let zipResponse = await fetch("/mockup.zip"); + let zipBinary = await zipResponse.arrayBuffer(); + pyodide.unpackArchive(zipBinary, "zip"); + await pyodide.runPythonAsync( + ` + from pyodide.http import pyfetch + response = await pyfetch("/image_process.py") + with open("./image_process.py", "wb") as f: + f.write(await response.bytes()) + `, + (output) => console.log(output), + (output) => console.log(output), + ); + console.log("end up"); + return pyodide; +} + +// Now only the first orientation model is generated for preview +async function runPreviewMockup(pyodide) { + let pythonNamespace = pyodide.globals.get("dict")(); + await pyodide.runPythonAsync( + ` + import mockup + import image_process + from js import locationKey, imageUpload, deviceInfo, deviceId + origin_image_path = await image_process.upload_file() + print("start preview", origin_image_path) + output_img = await mockup.previewMockup(locationKey, deviceId, origin_image_path, deviceInfo) + `, + { globals: pythonNamespace }, + ); + pyodide.runPython( + ` + temp = image_process.save_image(output_img) + `, + { globals: pythonNamespace }, + ); + return pythonNamespace.get("temp").toJs(); +} + +async function main() { + let pyodideObject = initianPyodide(); + self.onmessage = async (event) => { + pyodideObject = await pyodideObject; + + self["imageUploadList"] = undefined; + self["imageUpload"] = event.data.imageUpload; + self["locationKey"] = event.data.location; + self["deviceId"] = event.data.deviceId; + self["deviceInfo"] = event.data.deviceInfo; + + try { + // TODO: Handle preview loading state in widget + let results = await runPreviewMockup(pyodideObject); + console.log("preview results", results); + self.postMessage(results); + } catch (error) { + self.postMessage({ error: error.message }); + } + }; +} + +main(); diff --git a/public/scripts/ulid.min.js b/public/scripts/ulid.min.js new file mode 100644 index 0000000..8c1fb9d --- /dev/null +++ b/public/scripts/ulid.min.js @@ -0,0 +1,130 @@ +/** + * Minified by jsDelivr using Terser v5.19.2. + * Original file: /npm/ulid@2.3.0/dist/index.umd.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!(function (r, e) { + "object" == typeof exports && "undefined" != typeof module + ? e(exports) + : "function" == typeof define && define.amd + ? define(["exports"], e) + : e((r.ULID = {})); +})(this, function (r) { + "use strict"; + function e(r) { + var e = new Error(r); + return (e.source = "ulid"), e; + } + var t = "0123456789ABCDEFGHJKMNPQRSTVWXYZ", + n = t.length, + o = Math.pow(2, 48) - 1; + function i(r, e, t) { + return e > r.length - 1 ? r : r.substr(0, e) + t + r.substr(e + 1); + } + function u(r) { + for ( + var o = void 0, u = r.length, a = void 0, c = void 0, f = n - 1; + !o && u-- >= 0; + + ) { + if (((a = r[u]), -1 === (c = t.indexOf(a)))) + throw e("incorrectly encoded string"); + c !== f ? (o = i(r, u, t[c + 1])) : (r = i(r, u, t[0])); + } + if ("string" == typeof o) return o; + throw e("cannot increment this string"); + } + function a(r) { + var e = Math.floor(r() * n); + return e === n && (e = n - 1), t.charAt(e); + } + function c(r, i) { + if (isNaN(r)) throw new Error(r + " must be a number"); + if (r > o) throw e("cannot encode time greater than " + o); + if (r < 0) throw e("time must be positive"); + if (!1 === Number.isInteger(r)) throw e("time must be an integer"); + for (var u = void 0, a = ""; i > 0; i--) + (u = r % n), (a = t.charAt(u) + a), (r = (r - u) / n); + return a; + } + function f(r, e) { + for (var t = ""; r > 0; r--) t = a(e) + t; + return t; + } + function d() { + var r = arguments.length > 0 && void 0 !== arguments[0] && arguments[0], + t = arguments[1]; + t || (t = "undefined" != typeof window ? window : null); + var n = t && (t.crypto || t.msCrypto); + if (n) + return function () { + var r = new Uint8Array(1); + return n.getRandomValues(r), r[0] / 255; + }; + try { + var o = require("crypto"); + return function () { + return o.randomBytes(1).readUInt8() / 255; + }; + } catch (r) {} + if (r) { + try { + console.error( + "secure crypto unusable, falling back to insecure Math.random()!", + ); + } catch (r) {} + return function () { + return Math.random(); + }; + } + throw e("secure crypto unusable, insecure Math.random not allowed"); + } + function s(r) { + return ( + r || (r = d()), + function (e) { + return isNaN(e) && (e = Date.now()), c(e, 10) + f(16, r); + } + ); + } + var h = s(); + (r.replaceCharAt = i), + (r.incrementBase32 = u), + (r.randomChar = a), + (r.encodeTime = c), + (r.encodeRandom = f), + (r.decodeTime = function (r) { + if (26 !== r.length) throw e("malformed ulid"); + var i = r + .substr(0, 10) + .split("") + .reverse() + .reduce(function (r, o, i) { + var u = t.indexOf(o); + if (-1 === u) throw e("invalid character found: " + o); + return r + u * Math.pow(n, i); + }, 0); + if (i > o) throw e("malformed ulid, timestamp too large"); + return i; + }), + (r.detectPrng = d), + (r.factory = s), + (r.monotonicFactory = function (r) { + r || (r = d()); + var e = 0, + t = void 0; + return function (n) { + if ((isNaN(n) && (n = Date.now()), n <= e)) { + var o = (t = u(t)); + return c(e, 10) + o; + } + e = n; + var i = (t = f(16, r)); + return c(n, 10) + i; + }; + }), + (r.ulid = h), + Object.defineProperty(r, "__esModule", { value: !0 }); +}); +//# sourceMappingURL=/sm/9461d47ebd40c139f3bc709023a78452e95dfaaa072b412c6fd663f1d02514a4.map diff --git a/public/scripts/upload.js b/public/scripts/upload.js index 7a1f263..4006909 100644 --- a/public/scripts/upload.js +++ b/public/scripts/upload.js @@ -36,6 +36,48 @@ async function runWorker(worker) { .then(function (pictureArray) { window.location.href = "/download/?deviceId=" + window.workerDeviceId; }) + .catch(function (err) { + // TODO: Handle preview error in widget + console.error("Get error while storing images to localforage:", err); + }); + }, + false, + ); +} + +function runPreviewWorker(worker, imageUpload) { + const imageUploadFile = imageUpload.file; + worker.postMessage({ + imageUpload: imageUploadFile, + location: window.location.toString(), + deviceId: window.workerDeviceId, + deviceInfo: window.deviceInfo, + }); + worker.addEventListener( + "message", + function (e) { + window.localforage + .setItem(`previewImage-${imageUpload.ulid}`, e.data[1]) + .then(function () { + const imageContainer = document.querySelector( + ".upload__device-image-rect", + ); + + /* Put first generated mockup to preview area */ + if (!imageContainer.style.backgroundImage) { + imageContainer.style.backgroundImage = `url(${e.data[1]})`; + imageContainer.style.backgroundSize = "cover"; + imageContainer.style.backgroundPosition = "center"; + + const imageUploadHints = document.querySelectorAll( + ".upload__device-hint", + ); + imageUploadHints.forEach((imageUploadHint) => { + imageUploadHint.innerHTML = ""; + imageUploadHint.style.background = "transparent"; + }); + } + }) .catch(function (err) { console.error("Get error while storing images to localforage:", err); }); @@ -88,7 +130,9 @@ class FileListViewModel { for (const file of files) { const imageUpload = new ImageUpload(file, MAX_FILE_SIZE_BYTE); await imageUpload.read(); + imageUpload.ulid = ULID.ulid(); this._imageUploads.push(imageUpload); + window.viewModel.generatePreviewMockup(imageUpload); } } @@ -109,6 +153,7 @@ class RootViewModel { _socket = null; _redirectTimer = null; worker = new Worker("/scripts/web_worker.js"); + previewWorker = new Worker("/scripts/preview_worker.js"); selectedColorId = null; constructor(maxMockupWaitSec, fileListViewModel, selectedColorId) { @@ -129,6 +174,10 @@ class RootViewModel { return this._isGeneratingMockup; } + async generatePreviewMockup(imageUpload) { + runPreviewWorker(this.previewWorker, imageUpload); + } + async generateMockup() { if (!this.fileList.isReadyForMockup) { console.warn("Cannot generate mockup at this moment"); diff --git a/public/scripts/web_worker.js b/public/scripts/web_worker.js index 94c6960..b45df70 100644 --- a/public/scripts/web_worker.js +++ b/public/scripts/web_worker.js @@ -36,7 +36,7 @@ async function runMockup(pyodide) { ); pyodide.runPython( ` - temp = image_process.save_image(output_img_path_list) + temp = image_process.save_images(output_img_path_list) `, { globals: pythonNamespace }, ); @@ -49,6 +49,7 @@ async function main() { pyodideObject = await pyodideObject; self["imageUploadList"] = event.data.imageUploadList; + self["imageUpload"] = undefined; self["locationKey"] = event.data.location; self["deviceId"] = event.data.deviceId; self["deviceInfo"] = event.data.deviceInfo; diff --git a/src/pages/model/[model].astro b/src/pages/model/[model].astro index e52a028..59961ad 100644 --- a/src/pages/model/[model].astro +++ b/src/pages/model/[model].astro @@ -158,6 +158,7 @@ if (deviceDetail.imagePath != null && deviceDetail.imagePath.length >= 1) { +
diff --git a/src/styles/upload.css b/src/styles/upload.css index 79f0441..71be1f2 100644 --- a/src/styles/upload.css +++ b/src/styles/upload.css @@ -147,6 +147,10 @@ main { width: 100%; height: 100%; object-fit: contain; + + /* Overlay the preview image with the color device image */ + position: relative; + z-index: 1; } .upload__device-image-rect-wrapper { @@ -160,10 +164,6 @@ main { justify-content: center; } -.upload__device-image-rect { - position: relative; -} - .upload__device-image-rect__screen-rect { position: absolute; }