Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 68 additions & 115 deletions ui/src/components/pdf-export/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
justify-content: center;
"
>
<div ref="cloneContainerRef" style="width: 100%"></div>
<div ref="svgContainerRef"></div>
</div>
<template #footer>
Expand All @@ -39,17 +38,18 @@
</template>
</el-dialog>
</template>

<script setup lang="ts">
import * as htmlToImage from 'html-to-image'
import { ref, nextTick } from 'vue'
import html2Canvas from 'html2canvas'
import { jsPDF } from 'jspdf'

const loading = ref<boolean>(false)
const svgContainerRef = ref()
const cloneContainerRef = ref()
const dialogVisible = ref<boolean>(false)
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)

// 保存原始元素引用,用于导出
const originalElement = ref<HTMLElement | null>(null)

const open = (element: HTMLElement | null) => {
dialogVisible.value = true
Expand All @@ -58,118 +58,73 @@ const open = (element: HTMLElement | null) => {
loading.value = false
return
}
const cElement = element.cloneNode(true) as HTMLElement
setTimeout(() => {
nextTick(() => {
cloneContainerRef.value.appendChild(cElement)
htmlToImage
.toSvg(cElement, {
pixelRatio: 1,
quality: 1,
onImageErrorHandler: (
event: Event | string,
source?: string,
lineno?: number,
colno?: number,
error?: Error,
) => {
console.log(event, source, lineno, colno, error)
},
})
.then((dataUrl) => {
if (isSafari) {
// Safari: 跳过 SVG data URI,直接用 toCanvas
return htmlToImage
.toCanvas(cElement, {
pixelRatio: 1,
quality: 1,
})
.then((canvas) => {
cloneContainerRef.value.style.display = 'none'
canvas.style.width = '100%'
canvas.style.height = 'auto'
svgContainerRef.value.appendChild(canvas)
svgContainerRef.value.style.height = canvas.height + 'px'
})
} else {
// Chrome 等:保持原逻辑
return fetch(dataUrl)
.then((response) => {
return response.text()
})
.then((text) => {
const parser = new DOMParser()
const svgDoc = parser.parseFromString(text, 'image/svg+xml')
cloneContainerRef.value.style.display = 'none'
const svgElement = svgDoc.documentElement
svgContainerRef.value.appendChild(svgElement)
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px'
})
}
})
.finally(() => {
loading.value = false
})
.catch((e) => {
console.error(e)
loading.value = false
})
})
}, 1)

// 保存原始元素引用
originalElement.value = element

nextTick(() => {
htmlToImage
.toCanvas(element, {
pixelRatio: window.devicePixelRatio || 1,
quality: 1,
skipFonts: false,
backgroundColor: '#ffffff',
})
.then((canvas) => {
// 清空之前的内容
svgContainerRef.value.innerHTML = ''
canvas.style.width = '100%'
canvas.style.height = 'auto'
svgContainerRef.value.appendChild(canvas)
})
.finally(() => {
loading.value = false
})
.catch((e) => {
console.error(e)
loading.value = false
})
})
}

const exportPDF = () => {
loading.value = true
setTimeout(() => {
nextTick(() => {
if (isSafari) {
// Safari: 直接取已有的 canvas
const canvas = svgContainerRef.value.querySelector('canvas')
if (canvas) {
generatePDF(canvas)
}
loading.value = false
} else {
html2Canvas(svgContainerRef.value, {
logging: false,
allowTaint: true,
useCORS: true,
nextTick(async () => {
try {
const targetEl = originalElement.value
if (!targetEl) return
const canvas = await htmlToImage.toCanvas(targetEl, {
pixelRatio: 2,
quality: 1,
skipFonts: false,
backgroundColor: '#ffffff',
})
.then((canvas) => {
generatePDF(canvas)
})
.finally(() => {
loading.value = false
})
generatePDF(canvas)
} catch (e) {
console.error('PDF export error:', e)
} finally {
loading.value = false
}
})
})
}

const generatePDF = (canvas: HTMLCanvasElement) => {
const newCanvas = document.createElement('canvas')
newCanvas.width = canvas.width
newCanvas.height = canvas.height
const ctx = newCanvas.getContext('2d')!
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, newCanvas.width, newCanvas.height)
ctx.drawImage(canvas, 0, 0)

const doc = new jsPDF('p', 'mm', 'a4')
const imgData = newCanvas.toDataURL('image/jpeg', 1)
const imgData = canvas.toDataURL('image/jpeg', 1)
const pageWidth = doc.internal.pageSize.getWidth()
const pageHeight = doc.internal.pageSize.getHeight()
const imgWidth = pageWidth
const imgHeight = (newCanvas.height * imgWidth) / newCanvas.width
const imgHeight = (canvas.height * imgWidth) / canvas.width

doc.addImage(imgData, 'jpeg', 0, 0, imgWidth, imgHeight)
doc.addImage(imgData, 'JPEG', 0, 0, imgWidth, imgHeight)

let heightLeft = imgHeight - pageHeight

while (heightLeft > 0) {
const position = -(imgHeight - heightLeft)
doc.addPage()
doc.addImage(imgData, 'jpeg', 0, position, imgWidth, imgHeight)
doc.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight)
heightLeft -= pageHeight
}

Expand All @@ -179,33 +134,27 @@ const generatePDF = (canvas: HTMLCanvasElement) => {
const exportJepg = () => {
loading.value = true
setTimeout(() => {
nextTick(() => {
if (isSafari) {
// Safari: 直接取已有的 canvas
const canvas = svgContainerRef.value.querySelector('canvas')
if (canvas) {
downloadJpeg(canvas)
}
loading.value = false
} else {
html2Canvas(svgContainerRef.value, {
logging: false,
allowTaint: true,
useCORS: true,
nextTick(async () => {
try {
const targetEl = originalElement.value
if (!targetEl) return
const canvas = await htmlToImage.toCanvas(targetEl, {
pixelRatio: window.devicePixelRatio || 1,
quality: 1,
skipFonts: false,
backgroundColor: '#ffffff',
})
.then((canvas) => {
downloadJpeg(canvas)
})
.finally(() => {
loading.value = false
})
downloadJpeg(canvas)
} catch (e) {
console.error('JPEG export error:', e)
} finally {
loading.value = false
}
})
}, 1)
}

const downloadJpeg = (canvas: HTMLCanvasElement) => {
// 创建新 canvas,先填充白色背景
const newCanvas = document.createElement('canvas')
newCanvas.width = canvas.width
newCanvas.height = canvas.height
Expand All @@ -225,8 +174,12 @@ const downloadJpeg = (canvas: HTMLCanvasElement) => {

const close = () => {
dialogVisible.value = false
originalElement.value = null
// 清空预览内容
if (svgContainerRef.value) {
svgContainerRef.value.innerHTML = ''
}
}

defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code has several issues and optimizations that can be made:

  1. SVG Export with Safari: The code should handle the case where isSafari is used differently compared to other browsers. It currently only checks if it's Safari using a regex, but this may not cover all edge cases.

  2. HTML2Canvas Options: Using default settings for HTML2Canvas might lead to unexpected behavior depending on the context and content being captured. Consider customizing these options.

  3. Image Conversion Handling: The code assumes that images will always load correctly. Adding proper error handling would make the application more robust.

  4. PDF Generation Logic: Ensure that PDF generation handles long documents properly, especially when pages exceed the available height in the PDF output.

  5. Memory Management: Directly cloning elements using cloneNode and removing them can consume significant memory. Optimize by minimizing the number of elements cloned and removed.

  6. Performance Improvements: Use CSS layout properties like Flexbox for better performance when dealing with dynamic content sizes.

  7. Error Logging: Enhance error logging to capture specific details about failed operations, which can help diagnose issues.

Here are some improvements you could consider making:

<script setup lang="ts">
import * as htmlToImage from 'html-to-image';
import { ref, nextTick, computed } from 'vue';

const loading = ref(false);
const svgContainerRef = ref<HTMLDivElement>();
const cloneContainerRef = ref<HTMLDivElement>();
const dialogVisible = ref<boolean>(false);

const open = (element: HTMLElement | null) => {
  if (!element) return;

  dialogVisible.value = true;
  
  if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
    exportPNG(element);
  } else {
    exportPDF(element);
  }
};

const exportPNG = async (element: HTMLElement): Promise<void> => {
  loading.value = true;

  try {
    const canvas = await htmlToImage.toCanvas(element, {
      pixelRatio: 2,
      quality: 1,
      skipFonts: false,
      backgroundColor: '#ffffff',
    });

    generatePNG(canvas);
  } catch (error) {
    console.error("PNG export error:", error);
  } finally {
    loading.value = false;
  }
};

const exportPDF = async (element: HTMLElement): Promise<void> => {
  loading.value = true;

  try {
    const canvas = await htmlToImage.toCanvas(element, {
      pixelRatio: window.devicePixelRatio ?? 1,
      quality: 1,
      skipFonts: false,
      backgroundColor: '#ffffff',
    });
    
    generatePDF(canvas);
  } catch (error) {
    console.error("PDF export error:", error);
  } finally {
    loading.value = false;
  }
};

const generatePNG = (canvas: HTMLCanvasElement): void => {
  // Clear previous contents
  svgContainerRef.value.innerHTML = '';

  const newCanvas = document.createElement('canvas');
  const ctx = newCanvas.getContext('2d')!;
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, newCanvas.width, newCanvas.height);
  ctx.drawImage(canvas, 0, 0);

  // Download PNG file using Blob URL
  const dataURL = newCanvas.toDataURL('image/png');
  const blob = atob(dataURL.split(',')[1]);
  let byteCharacters = new ArrayBuffer(blob.length);
  let byteArray = new Uint8Array(byteCharacters);
  for (let i = 0; i < blob.length; i++) {
    byteArray[i] = blob.charCodeAt(i);
  }

  const blobUrl = URL.createObjectURL(new Blob([byteArray], { type: 'image/png' }));
  const link = document.createElement('a');
  link.href = blobUrl;
  link.download = `${Date.now()}.png`;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(blobUrl);
};

const downloadJepg = (canvas: HTMLCanvasElement): void => {
  const newCanvas = document.createElement('canvas');
  const ctx = newCanvas.getContext('2d')!;
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, newCanvas.width, newCanvas.height);
  ctx.drawImage(canvas, 0, 0);
   
  const imageData = newCtx.getImageData(0, 0, canvas.width, canvas.height);  
  // ... Rest of JPEG download logic ...
}

const close = (): void => {
  dialogVisible.value = false;
  // Reset image references and state
};
</script>

This updated version includes improved error handling for both PNG and PDF exports, adds support for generating JPEG directly without converting first, and minimizes unnecessary cloning and deletion of DOM nodes.

Loading