|
|
@@ -1,5 +1,6 @@
|
|
|
import fabric from '../PictureEditor/js/fabric-adapter'
|
|
|
import { buildRenderPlans, normalizeGoods } from './generateImagesPlan'
|
|
|
+import fontConfig from '../PictureEditor/mixin/edit/module/font.json'
|
|
|
|
|
|
/**
|
|
|
* 根据渲染计划和拍平后的货号数据,真正生成图片(dataURL),按 canvasIndex 返回。
|
|
|
@@ -250,7 +251,7 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
// 工具函数:只检查并压缩图片,返回压缩后的图片URL
|
|
|
const compressImageIfNeeded = (url, maxWidth = 2048) => {
|
|
|
return new Promise((resolve) => {
|
|
|
-
|
|
|
+
|
|
|
if (!url) {
|
|
|
resolve(null)
|
|
|
return
|
|
|
@@ -258,9 +259,9 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
|
|
|
const img = new Image()
|
|
|
img.crossOrigin = 'anonymous'
|
|
|
-
|
|
|
+
|
|
|
img.onload = () => {
|
|
|
-
|
|
|
+
|
|
|
// 检查图片是否需要压缩
|
|
|
if (img.width <= maxWidth) {
|
|
|
resolve(url)
|
|
|
@@ -275,43 +276,43 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
const canvas = document.createElement('canvas')
|
|
|
canvas.width = newWidth
|
|
|
canvas.height = newHeight
|
|
|
-
|
|
|
+
|
|
|
const ctx = canvas.getContext('2d')
|
|
|
if (!ctx) {
|
|
|
console.error('无法获取canvas 2d上下文')
|
|
|
resolve(url)
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 重要:设置canvas背景为透明
|
|
|
// 方法1:清除画布,设置为透明
|
|
|
// ctx.clearRect(0, 0, newWidth, newHeight)
|
|
|
-
|
|
|
+
|
|
|
// 方法2:或者使用透明矩形填充
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0)'
|
|
|
ctx.fillRect(0, 0, newWidth, newHeight)
|
|
|
-
|
|
|
+
|
|
|
// 设置高质量压缩参数
|
|
|
ctx.imageSmoothingEnabled = true
|
|
|
ctx.imageSmoothingQuality = 'high'
|
|
|
-
|
|
|
+
|
|
|
// 绘制压缩后的图片
|
|
|
ctx.drawImage(img, 0, 0, newWidth, newHeight)
|
|
|
-
|
|
|
+
|
|
|
// 返回压缩后的dataURL,使用PNG格式保持透明通道(如果原图有透明背景)
|
|
|
// 但如果是JPG商品图,通常没有透明通道,也可以用PNG
|
|
|
const dataUrl = canvas.toDataURL('image/png') // 使用PNG确保无黑色背景
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
canvas.remove()
|
|
|
resolve(dataUrl)
|
|
|
}
|
|
|
img.onerror = (error) => {
|
|
|
resolve(url) // 返回原始URL作为fallback
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
img.src = url
|
|
|
})
|
|
|
}
|
|
|
@@ -319,7 +320,7 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
// 工具:从 canvas_json 创建一个离屏 fabric.Canvas,并应用一次性的渲染逻辑
|
|
|
const renderOne = async (canvasItem, planForCanvas, imagePlan) => {
|
|
|
|
|
|
-
|
|
|
+
|
|
|
return new Promise(async (resolve) => {
|
|
|
if (!canvasItem) {
|
|
|
return resolve(null)
|
|
|
@@ -327,7 +328,7 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
|
|
|
const { canvasIndex } = planForCanvas
|
|
|
const { skuIndexes } = imagePlan
|
|
|
-
|
|
|
+
|
|
|
|
|
|
// 模特/场景占位:直接返回类型,不渲染
|
|
|
if (canvasItem.canvas_json === 'model') {
|
|
|
@@ -357,13 +358,13 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
const width = Number(canvasItem.width) || 395
|
|
|
const height = Number(canvasItem.height) || 600
|
|
|
const bgColor = canvasItem.bg_color || '#fff'
|
|
|
-
|
|
|
+
|
|
|
|
|
|
try {
|
|
|
const mode = canvasItem.multi_goods_mode || ''
|
|
|
const perCanvasSlots = planForCanvas.perCanvasSlots || 1
|
|
|
const usedSkus = (skuIndexes || []).map((idx) => (idx != null ? skus[idx] : null))
|
|
|
-
|
|
|
+
|
|
|
|
|
|
|
|
|
// 获取画布对象
|
|
|
@@ -371,30 +372,30 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
|
|
|
// 1) 处理图片占位(data-type = img)
|
|
|
const imgPlaceholders = objs.filter((o) => o && o['data-type'] === 'img')
|
|
|
-
|
|
|
+
|
|
|
|
|
|
|
|
|
// 为每个图片占位生成压缩后的URL
|
|
|
const imageUrlMap = new Map()
|
|
|
-
|
|
|
+
|
|
|
// 收集所有需要压缩的图片URL
|
|
|
const urlsToCompress = []
|
|
|
-
|
|
|
+
|
|
|
if (mode === 'multiple') {
|
|
|
imgPlaceholders.forEach((obj, idx) => {
|
|
|
const slotIndex = idx % perCanvasSlots
|
|
|
const sku = usedSkus[slotIndex]
|
|
|
const angleKey = obj['data-key']
|
|
|
-
|
|
|
+
|
|
|
if (!sku) {
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const url = (sku.pics && sku.pics[angleKey]) || ''
|
|
|
if (!url) {
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
urlsToCompress.push({ url, objId: `${idx}_${angleKey}`, originalObj: obj, angleKey, skuIndex: slotIndex })
|
|
|
})
|
|
|
} else {
|
|
|
@@ -402,21 +403,21 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
const sku = usedSkus[0]
|
|
|
if (!sku) {
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
imgPlaceholders.forEach((obj, idx) => {
|
|
|
if (!sku) return
|
|
|
-
|
|
|
+
|
|
|
const angleKey = obj['data-key']
|
|
|
const url = (sku.pics && sku.pics[angleKey]) || ''
|
|
|
if (!url) {
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
urlsToCompress.push({ url, objId: `${idx}_${angleKey}`, originalObj: obj, angleKey })
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-
|
|
|
+
|
|
|
// 并发压缩所有图片
|
|
|
const compressResults = await Promise.all(
|
|
|
urlsToCompress.map(async ({ url, objId, originalObj, angleKey }) => {
|
|
|
@@ -443,15 +444,15 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
const slotIndex = idx % perCanvasSlots
|
|
|
const sku = usedSkus[slotIndex]
|
|
|
const angleKey = obj['data-key']
|
|
|
-
|
|
|
+
|
|
|
if (!sku) {
|
|
|
obj.visible = false
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const objId = `${idx}_${angleKey}`
|
|
|
const compressedUrl = imageUrlMap.get(objId)
|
|
|
-
|
|
|
+
|
|
|
if (compressedUrl) {
|
|
|
obj.visible = true
|
|
|
obj['data-value'] = compressedUrl
|
|
|
@@ -475,11 +476,11 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
obj.visible = false
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const angleKey = obj['data-key']
|
|
|
const objId = `${idx}_${angleKey}`
|
|
|
const compressedUrl = imageUrlMap.get(objId)
|
|
|
-
|
|
|
+
|
|
|
if (compressedUrl) {
|
|
|
obj.visible = true
|
|
|
obj['data-value'] = compressedUrl
|
|
|
@@ -501,7 +502,74 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
const textPlaceholders = objs.filter((o) => o && o['data-key'] && (o['data-type'] === 'text' || o['type'] === "textbox"))
|
|
|
console.log('=====textPlaceholders========', textPlaceholders)
|
|
|
|
|
|
+ // 字体加载工具函数
|
|
|
+ const loadFont = (_fontName, _fontUrl) => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ if (checkFont(_fontName)) {
|
|
|
+ console.log('已有字体:', _fontName)
|
|
|
+ return resolve(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ let prefont = new FontFace(
|
|
|
+ _fontName,
|
|
|
+ 'url(' + _fontUrl + ')'
|
|
|
+ );
|
|
|
+
|
|
|
+ prefont.load().then(function (loaded_face) {
|
|
|
+ document.fonts.add(loaded_face);
|
|
|
+ console.log('字体加载成功', loaded_face, document.fonts)
|
|
|
+ return resolve(true)
|
|
|
+ }).catch(function (error) {
|
|
|
+ console.log('字体加载失败', error)
|
|
|
+ return reject(error)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const checkFont = (name) => {
|
|
|
+ let values = document.fonts.values();
|
|
|
+ let isHave = false;
|
|
|
+ let item = values.next();
|
|
|
+ while(!item.done && !isHave) {
|
|
|
+ let fontFace = item.value;
|
|
|
+ if(fontFace.family == name) {
|
|
|
+ isHave = true;
|
|
|
+ }
|
|
|
+ item = values.next();
|
|
|
+ }
|
|
|
+ return isHave;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据fontFamily查找字体URL
|
|
|
+ const findFontUrl = (fontFamily) => {
|
|
|
+ const font = fontConfig.find(f => f.fontFamily === fontFamily || f.name === fontFamily)
|
|
|
+ return font ? font.src : null
|
|
|
+ }
|
|
|
+
|
|
|
if (textPlaceholders.length) {
|
|
|
+ // 收集所有需要加载的字体
|
|
|
+ const fontsToLoad = new Map()
|
|
|
+
|
|
|
+ // 首先收集字体信息
|
|
|
+ textPlaceholders.forEach((obj) => {
|
|
|
+ const fontFamily = obj.fontFamily
|
|
|
+ if (fontFamily && fontFamily !== 'Arial') {
|
|
|
+ const fontUrl = findFontUrl(fontFamily)
|
|
|
+ if (fontUrl) {
|
|
|
+ fontsToLoad.set(fontFamily, fontUrl)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 加载所有需要的字体
|
|
|
+ const fontLoadPromises = Array.from(fontsToLoad.entries()).map(([fontName, fontUrl]) => {
|
|
|
+ console.log('需要加载字体:', fontName, fontUrl)
|
|
|
+ return loadFont(fontName, fontUrl)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 等待所有字体加载完成
|
|
|
+ await Promise.all(fontLoadPromises)
|
|
|
+
|
|
|
// 通用的 key -> 文本 映射函数,
|
|
|
const mapKeyToText = (sku, key, defaultVal) => {
|
|
|
if (!sku) return defaultVal
|
|
|
@@ -553,6 +621,17 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
obj.visible = true
|
|
|
obj.text = textVal || ''
|
|
|
obj['data-value'] = textVal || ''
|
|
|
+
|
|
|
+ // 确保字体属性被正确应用
|
|
|
+ // 保留原始的字体设置,如果没有则设置默认值
|
|
|
+ if (!obj.fontFamily) obj.fontFamily = 'Arial'
|
|
|
+ if (!obj.fontSize || obj.fontSize <= 0) obj.fontSize = 16
|
|
|
+ if (!obj.fill) obj.fill = '#000000'
|
|
|
+
|
|
|
+ // 保留其他字体属性
|
|
|
+ if (obj.fontWeight === undefined) obj.fontWeight = 'normal'
|
|
|
+ if (obj.fontStyle === undefined) obj.fontStyle = 'normal'
|
|
|
+ if (obj.textAlign === undefined) obj.textAlign = 'left'
|
|
|
})
|
|
|
} else {
|
|
|
// 默认 / single:全部文字都使用同一个货号(默认模式只生成 1 张,用第一个货号)
|
|
|
@@ -567,6 +646,17 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
obj.visible = true
|
|
|
obj.text = textVal || ''
|
|
|
obj['data-value'] = textVal || ''
|
|
|
+
|
|
|
+ // 确保字体属性被正确应用
|
|
|
+ // 保留原始的字体设置,如果没有则设置默认值
|
|
|
+ if (!obj.fontFamily) obj.fontFamily = 'Arial'
|
|
|
+ if (!obj.fontSize || obj.fontSize <= 0) obj.fontSize = 16
|
|
|
+ if (!obj.fill) obj.fill = '#000000'
|
|
|
+
|
|
|
+ // 保留其他字体属性
|
|
|
+ if (obj.fontWeight === undefined) obj.fontWeight = 'normal'
|
|
|
+ if (obj.fontStyle === undefined) obj.fontStyle = 'normal'
|
|
|
+ if (obj.textAlign === undefined) obj.textAlign = 'left'
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
@@ -587,7 +677,7 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
// 添加fabric事件监听器来调试图片加载
|
|
|
let loadedImageCount = 0
|
|
|
const totalImagePlaceholders = imgPlaceholders.filter(obj => obj.visible !== false).length
|
|
|
-
|
|
|
+
|
|
|
|
|
|
// 重要:使用reviver函数确保dataURL图片正确加载
|
|
|
const reviver = (key, value, object) => {
|
|
|
@@ -596,28 +686,28 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
// 对于dataURL,确保fabric正确处理
|
|
|
return value
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 记录图片加载状态
|
|
|
if (key === 'type' && value === 'image' && object && object.src) {
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return value
|
|
|
}
|
|
|
|
|
|
-
|
|
|
+
|
|
|
// 把已经替换好动态数据的 JSON 加载进 fabric;
|
|
|
// loadFromJSON 会等所有图片资源加载完之后才调用回调函数
|
|
|
fcanvas.loadFromJSON(json, () => {
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
-
|
|
|
+
|
|
|
// 检查所有加载的对象
|
|
|
const allObjects = fcanvas.getObjects()
|
|
|
-
|
|
|
+
|
|
|
// 检查图片对象
|
|
|
const imageObjects = allObjects.filter(obj => obj.type === 'image')
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
+
|
|
|
// 确保所有图片对象都正确渲染
|
|
|
fcanvas.getObjects().forEach(obj => {
|
|
|
if (obj.type === 'image') {
|
|
|
@@ -629,7 +719,7 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
obj.setCoords()
|
|
|
}
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
fcanvas.renderAll()
|
|
|
|
|
|
// 获取商品图宽高,并在导出时根据宽度做压缩控制
|
|
|
@@ -639,7 +729,7 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
|
|
|
// 计算按当前导出倍数后的宽度
|
|
|
const expectedWidth = width * exportMultiplier
|
|
|
-
|
|
|
+
|
|
|
if (expectedWidth > MAX_WIDTH) {
|
|
|
// 根据最大宽度反推需要的缩放倍数(<= 1)
|
|
|
exportMultiplier = MAX_WIDTH / width
|
|
|
@@ -648,13 +738,13 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
const finalWidth = Math.round(width * exportMultiplier)
|
|
|
const finalHeight = Math.round(height * exportMultiplier)
|
|
|
|
|
|
-
|
|
|
+
|
|
|
const dataUrl = fcanvas.toDataURL({
|
|
|
format: 'jpeg',
|
|
|
multiplier: exportMultiplier,
|
|
|
enableRetinaScaling: true,
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
|
|
|
fcanvas.dispose()
|
|
|
|
|
|
@@ -672,11 +762,11 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
resolve(null)
|
|
|
}
|
|
|
}, reviver)
|
|
|
-
|
|
|
+
|
|
|
// 添加fabric错误处理
|
|
|
fcanvas.on('object:added', (e) => {
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
} catch (e) {
|
|
|
resolve(null)
|
|
|
}
|
|
|
@@ -687,7 +777,7 @@ export async function renderImagesByPlans(plans, canvasList, skus) {
|
|
|
const canvasItem = canvasList[plan.canvasIndex]
|
|
|
if (!canvasItem) continue
|
|
|
|
|
|
-
|
|
|
+
|
|
|
for (const imgPlan of plan.images || []) {
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
const res = await renderOne(canvasItem, plan, imgPlan)
|
|
|
@@ -787,7 +877,7 @@ const composeCombinedImage = (images) =>
|
|
|
// 确保有canvas元素
|
|
|
const el = document.createElement('canvas')
|
|
|
if (!el) return resolve(null)
|
|
|
-
|
|
|
+
|
|
|
const ctx = el.getContext('2d')
|
|
|
if (!ctx) {
|
|
|
el.remove()
|