|
|
@@ -28,7 +28,8 @@ import type {
|
|
|
Gradient,
|
|
|
} from '@/types/slides'
|
|
|
|
|
|
-const convertFontSizePtToPx = (html: string, ratio: number) => {
|
|
|
+const convertFontSizePtToPx = (html: string, ratio: number, autoFit: any) => {
|
|
|
+ if (autoFit?.fontScale && autoFit?.type == "text") { ratio = ratio * autoFit.fontScale / 100; }
|
|
|
// return html;
|
|
|
return html.replace(/\s*([\d.]+)pt/g, (match, p1) => {
|
|
|
return `${(parseFloat(p1) * ratio - 1) | 0}px `
|
|
|
@@ -403,6 +404,7 @@ export default () => {
|
|
|
/**
|
|
|
* 将 base64 字符串或 Blob 转换为 File 对象
|
|
|
*/
|
|
|
+
|
|
|
const dataToFile = async (data: string | Blob, filename: string, videoMimeType: string): File => {
|
|
|
if (typeof data === 'string') {
|
|
|
// 1. 通过 fetch 获取 Blob 数据
|
|
|
@@ -422,6 +424,62 @@ export default () => {
|
|
|
}
|
|
|
throw new Error('Unsupported data type')
|
|
|
}
|
|
|
+
|
|
|
+/*
|
|
|
+ // 你原有的 dataToFile 函数保持不变
|
|
|
+ const dataToFile = async (data: string | Blob, filename: string, videoMimeType: string): Promise<File> => {
|
|
|
+ if (typeof data === 'string') {
|
|
|
+ const response = await fetch(data);
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`Failed to fetch blob: ${response.statusText}`);
|
|
|
+ }
|
|
|
+ const blob = await response.blob();
|
|
|
+ const mime = videoMimeType || blob.type;
|
|
|
+ return new File([blob], filename, { type: mime });
|
|
|
+ } else if (data instanceof Blob) {
|
|
|
+ return new File([data], filename, { type: data.type });
|
|
|
+ }
|
|
|
+ throw new Error('Unsupported data type');
|
|
|
+ };
|
|
|
+
|
|
|
+
|
|
|
+ const convertVideoToMP4 = async (
|
|
|
+ videoSource: string | Blob,
|
|
|
+ outputFilename: string = `video_${Date.now()}.mp4`
|
|
|
+ ): Promise<File> => {
|
|
|
+ // 1. 检查浏览器支持
|
|
|
+ const supported = await canEncode();
|
|
|
+ if (!supported) {
|
|
|
+ throw new Error('当前浏览器不支持 WebCodecs,请使用最新 Chrome/Edge 并确保 HTTPS');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 转为 File
|
|
|
+ const inputFile = await dataToFile(videoSource, 'input.mp4', 'video/mp4');
|
|
|
+
|
|
|
+ // 3. 创建 VideoFile 对象(webcodecs-encoder 的输入包装)
|
|
|
+ //const videoFile = new VideoFile(inputFile);
|
|
|
+ const videoFile = {
|
|
|
+ file: inputFile,
|
|
|
+ type: 'video/mp4'
|
|
|
+ };
|
|
|
+ // 4. 执行编码
|
|
|
+ const encodedData = await encode(videoFile, {
|
|
|
+ quality: 'high',
|
|
|
+ video: {
|
|
|
+ codec: 'av1',
|
|
|
+ bitrate: 2_000_000,
|
|
|
+ hardwareAcceleration: 'prefer-hardware'
|
|
|
+ },
|
|
|
+ audio: false, // 显式禁用音频编码
|
|
|
+ container: 'mp4',
|
|
|
+ onProgress: (progress) => console.log(progress)
|
|
|
+ });
|
|
|
+
|
|
|
+ // 5. 返回 File
|
|
|
+ return new File([encodedData], outputFilename, { type: 'video/mp4' });
|
|
|
+ };
|
|
|
+*/
|
|
|
+
|
|
|
|
|
|
/*
|
|
|
const makeWhiteTransparent = async (
|
|
|
@@ -514,14 +572,14 @@ export default () => {
|
|
|
options?: { tolerance?: number }
|
|
|
): Promise<File> => {
|
|
|
const tolerance = options?.tolerance ?? 15
|
|
|
-
|
|
|
+
|
|
|
// ----- 辅助函数:将输入统一转换为 { blob, mime } -----
|
|
|
async function getBlobAndMime(input: string | Blob): Promise<{ blob: Blob; mime: string }> {
|
|
|
// 1. 已经是 Blob
|
|
|
if (input instanceof Blob) {
|
|
|
return { blob: input, mime: input.type }
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 2. 处理字符串
|
|
|
if (input.startsWith('data:')) {
|
|
|
// data URL → 通过 fetch 获取 Blob(自动获得正确的 MIME 类型)
|
|
|
@@ -540,15 +598,15 @@ export default () => {
|
|
|
return { blob, mime: 'image/png' }
|
|
|
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 获取统一的 blob 和实际 MIME 类型
|
|
|
const { blob, mime } = await getBlobAndMime(data)
|
|
|
-
|
|
|
+
|
|
|
// ----- 非 PNG 格式:直接返回原始文件(不处理透明)-----
|
|
|
if (mime !== 'image/png') {
|
|
|
return new File([blob], filename, { type: mime })
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// ----- PNG 格式:执行白色变透明处理 -----
|
|
|
// 1. 创建对象 URL 用于加载图片
|
|
|
const imageUrl = URL.createObjectURL(blob)
|
|
|
@@ -562,33 +620,33 @@ export default () => {
|
|
|
// Blob URL 不需要设置 crossOrigin
|
|
|
image.src = imageUrl
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
const canvas = document.createElement('canvas')
|
|
|
try {
|
|
|
canvas.width = img.width
|
|
|
canvas.height = img.height
|
|
|
const ctx = canvas.getContext('2d')!
|
|
|
ctx.drawImage(img, 0, 0)
|
|
|
-
|
|
|
+
|
|
|
// 3. 获取像素数据,将接近白色的像素设为透明
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
|
const dataArray = imageData.data
|
|
|
-
|
|
|
+
|
|
|
for (let i = 0; i < dataArray.length; i += 4) {
|
|
|
const r = dataArray[i]
|
|
|
const g = dataArray[i + 1]
|
|
|
const b = dataArray[i + 2]
|
|
|
-
|
|
|
+
|
|
|
const dr = r - 255
|
|
|
const dg = g - 255
|
|
|
const db = b - 255
|
|
|
const dist = Math.sqrt(dr * dr + dg * dg + db * db)
|
|
|
-
|
|
|
+
|
|
|
if (dist <= tolerance) {
|
|
|
dataArray[i + 3] = 0 // 完全透明
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
ctx.putImageData(imageData, 0, 0)
|
|
|
}
|
|
|
finally {
|
|
|
@@ -596,7 +654,7 @@ export default () => {
|
|
|
URL.revokeObjectURL(imageUrl)
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 4. 导出为 PNG Blob
|
|
|
const outputBlob = await new Promise<Blob>((resolve, reject) => {
|
|
|
canvas.toBlob((blob) => {
|
|
|
@@ -604,7 +662,7 @@ export default () => {
|
|
|
else reject(new Error('Canvas toBlob failed'))
|
|
|
}, 'image/png')
|
|
|
})
|
|
|
-
|
|
|
+
|
|
|
return new File([outputBlob], filename, { type: 'image/png' })
|
|
|
}
|
|
|
|
|
|
@@ -1338,8 +1396,8 @@ export default () => {
|
|
|
rotate: el.rotate,
|
|
|
defaultFontName: theme.value.fontName,
|
|
|
defaultColor: theme.value.fontColor,
|
|
|
- content: convertFontSizePtToPx(el.content, ratio),
|
|
|
- style: getStyle(convertFontSizePtToPx(el.content, ratio)),
|
|
|
+ content: convertFontSizePtToPx(el.content, ratio, el.autoFit),
|
|
|
+ style: getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)),
|
|
|
lineHeight: 1.15,
|
|
|
outline: {
|
|
|
color: el.borderColor,
|
|
|
@@ -1401,27 +1459,27 @@ export default () => {
|
|
|
range: [[0, 0], [100, 100]],
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // 如果 src 是 base64,触发上传
|
|
|
- if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
|
|
|
- const uploadTask = (async () => {
|
|
|
- try {
|
|
|
- const file = await makeWhiteTransparent(el.src, `image_${Date.now()}.png`)
|
|
|
- if (file) {
|
|
|
- const url = await uploadFileToS3(file)
|
|
|
- element.src = url // 替换为远程 URL
|
|
|
- const slidesStore = useSlidesStore()
|
|
|
- slidesStore.updateElement({ id: element.id, props: { src: url } })
|
|
|
- }
|
|
|
- }
|
|
|
- catch (error) {
|
|
|
- console.error('Image upload failed:', error)
|
|
|
- // 失败时保留原 base64(或可置空)
|
|
|
- }
|
|
|
- })()
|
|
|
- uploadTasks.push(uploadTask)
|
|
|
- }
|
|
|
-
|
|
|
+ /*
|
|
|
+ // 如果 src 是 base64,触发上传
|
|
|
+ if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ const file = await makeWhiteTransparent(el.src, `image_${Date.now()}.png`)
|
|
|
+ if (file) {
|
|
|
+ const url = await uploadFileToS3(file)
|
|
|
+ element.src = url // 替换为远程 URL
|
|
|
+ const slidesStore = useSlidesStore()
|
|
|
+ slidesStore.updateElement({ id: element.id, props: { src: url } })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('Image upload failed:', error)
|
|
|
+ // 失败时保留原 base64(或可置空)
|
|
|
+ }
|
|
|
+ })()
|
|
|
+ uploadTasks.push(uploadTask)
|
|
|
+ }
|
|
|
+ */
|
|
|
slide.elements.push(element)
|
|
|
}
|
|
|
else if (el.type === 'math') {
|
|
|
@@ -1518,7 +1576,7 @@ export default () => {
|
|
|
if (localData) {
|
|
|
const uploadTask = (async () => {
|
|
|
try {
|
|
|
- const file = await dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4')
|
|
|
+ const file = await convertVideoToMP4(localData, `video_${Date.now()}.mp4`)
|
|
|
if (file) {
|
|
|
const url = await uploadFileToS3(file)
|
|
|
element.src = url
|
|
|
@@ -1566,7 +1624,7 @@ export default () => {
|
|
|
|
|
|
const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
|
|
|
const fill = el.fill?.type === 'color' ? el.fill.value : ''
|
|
|
- const style = getStyle(convertFontSizePtToPx(el.content, ratio)) + (el.pathBBox.pWidth ? ';width:' + (el.pathBBox.pWidth * ratio) + 'px;height:' + (el.pathBBox.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
|
|
|
+ const style = getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)) + (el.pathBBox.pWidth ? ';width:' + (el.pathBBox.pWidth * ratio) + 'px;height:' + (el.pathBBox.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
|
|
|
const element: PPTShapeElement = {
|
|
|
type: 'shape',
|
|
|
id: nanoid(10),
|
|
|
@@ -1588,7 +1646,7 @@ export default () => {
|
|
|
style: el.borderType,
|
|
|
},
|
|
|
text: {
|
|
|
- content: convertFontSizePtToPx(el.content, ratio),
|
|
|
+ content: convertFontSizePtToPx(el.content, ratio, el.autoFit),
|
|
|
style: style,
|
|
|
defaultFontName: theme.value.fontName,
|
|
|
defaultColor: theme.value.fontColor,
|