|
|
@@ -100,9 +100,8 @@ export class ReadingPdfComponent implements OnInit {
|
|
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
|
await document.fonts?.ready;
|
|
|
const filename = `${article.title.replace(/[^a-zA-Z0-9一-龥]/g, '_')}_quiz.pdf`;
|
|
|
- let hasBlankTrailingPage = false;
|
|
|
- const { default: html2pdf } = await import('html2pdf.js');
|
|
|
- const worker = html2pdf().set({
|
|
|
+ const [{ default: html2pdf }, { jsPDF }] = await Promise.all([import('html2pdf.js'), import('jspdf')]);
|
|
|
+ const canvas = await html2pdf().set({
|
|
|
margin: [10, 10, 10, 10], filename,
|
|
|
image: { type: 'jpeg', quality: 1 },
|
|
|
html2canvas: { scale: 1.5, logging: false, useCORS: true, backgroundColor: '#ffffff' },
|
|
|
@@ -110,17 +109,36 @@ export class ReadingPdfComponent implements OnInit {
|
|
|
pagebreak: {
|
|
|
mode: ['css', 'legacy'],
|
|
|
before: ['.question-section', '.answer-section'],
|
|
|
- avoid: ['.question-item', '.answer-item'],
|
|
|
+ avoid: ['.reading-content p', '.question-item', '.question-item h3', '.question-item p', '.answer-item', '.answer-item h3', '.answer-item > div', '.answer-item footer'],
|
|
|
},
|
|
|
- }).from(element).toCanvas().get('canvas', (canvas) => {
|
|
|
- const pageHeight = Math.floor(canvas.width * (277 / 190));
|
|
|
- const trailingSlice = canvas.height % pageHeight;
|
|
|
- hasBlankTrailingPage = trailingSlice > 0 && trailingSlice < 32;
|
|
|
- }).toPdf();
|
|
|
- await worker.get('pdf', (pdf) => {
|
|
|
- const pageCount = pdf.internal.getNumberOfPages();
|
|
|
- if (hasBlankTrailingPage && pageCount > 1) pdf.deletePage(pageCount);
|
|
|
- }).save();
|
|
|
+ }).from(element).toCanvas().get('canvas');
|
|
|
+ const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
|
+ if (!context) throw new Error('无法读取打印内容');
|
|
|
+ const pdf = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' });
|
|
|
+ const pageCanvas = document.createElement('canvas');
|
|
|
+ pageCanvas.width = canvas.width;
|
|
|
+ const pageContext = pageCanvas.getContext('2d');
|
|
|
+ if (!pageContext) throw new Error('无法创建 PDF 页面');
|
|
|
+ const pageHeight = Math.floor(canvas.width * (277 / 190));
|
|
|
+ let pageStart = 0;
|
|
|
+ let pageIndex = 0;
|
|
|
+ while (pageStart < canvas.height) {
|
|
|
+ if (pageIndex && canvas.height - pageStart < 120 && !this.hasVisibleContent(context, canvas.width, pageStart, canvas.height)) break;
|
|
|
+ const targetEnd = Math.min(pageStart + pageHeight, canvas.height);
|
|
|
+ let pageEnd = targetEnd === canvas.height ? targetEnd : this.safePageEnd(context, canvas.width, pageStart, targetEnd);
|
|
|
+ if (targetEnd < canvas.height && canvas.height - pageEnd < 120 && this.hasVisibleContent(context, canvas.width, pageEnd, canvas.height)) {
|
|
|
+ pageEnd = this.safePageEnd(context, canvas.width, pageStart, targetEnd - 140);
|
|
|
+ }
|
|
|
+ const sliceHeight = pageEnd - pageStart;
|
|
|
+ pageCanvas.height = sliceHeight;
|
|
|
+ pageContext.fillStyle = '#ffffff';
|
|
|
+ pageContext.fillRect(0, 0, canvas.width, sliceHeight);
|
|
|
+ pageContext.drawImage(canvas, 0, pageStart, canvas.width, sliceHeight, 0, 0, canvas.width, sliceHeight);
|
|
|
+ if (pageIndex++) pdf.addPage();
|
|
|
+ pdf.addImage(pageCanvas.toDataURL('image/jpeg', 1), 'JPEG', 10, 10, 190, sliceHeight * 190 / canvas.width);
|
|
|
+ pageStart = pageEnd;
|
|
|
+ }
|
|
|
+ await pdf.save(filename, { returnPromise: true });
|
|
|
} catch (error) {
|
|
|
const detail = error instanceof Error ? error.message : '';
|
|
|
this.platform.toast(detail ? `PDF 导出失败:${detail}` : 'PDF 导出失败,请重试', 'error');
|
|
|
@@ -128,4 +146,37 @@ export class ReadingPdfComponent implements OnInit {
|
|
|
this.exporting.set(false);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private safePageEnd(context: CanvasRenderingContext2D, width: number, pageStart: number, targetEnd: number): number {
|
|
|
+ // html2pdf normally cuts the canvas at a fixed height, including through text glyphs.
|
|
|
+ // Find the last gap between text lines near that height and cut there instead.
|
|
|
+ const scanStart = Math.max(pageStart + Math.floor((targetEnd - pageStart) * 0.6), targetEnd - 360);
|
|
|
+ const pixels = context.getImageData(0, scanStart, width, targetEnd - scanStart).data;
|
|
|
+ let gapStart = -1;
|
|
|
+ let safeEnd = targetEnd;
|
|
|
+ for (let row = 0; row < targetEnd - scanStart; row++) {
|
|
|
+ let hasText = false;
|
|
|
+ for (let column = 0; column < width; column++) {
|
|
|
+ const pixel = (row * width + column) * 4;
|
|
|
+ if (pixels[pixel] < 190 || pixels[pixel + 1] < 190 || pixels[pixel + 2] < 190) {
|
|
|
+ hasText = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!hasText && gapStart < 0) gapStart = row;
|
|
|
+ if (hasText && gapStart >= 0) {
|
|
|
+ if (row - gapStart >= 4) safeEnd = scanStart + Math.floor((gapStart + row) / 2);
|
|
|
+ gapStart = -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return gapStart >= 0 && targetEnd - scanStart - gapStart >= 4 ? targetEnd : safeEnd;
|
|
|
+ }
|
|
|
+
|
|
|
+ private hasVisibleContent(context: CanvasRenderingContext2D, width: number, start: number, end: number): boolean {
|
|
|
+ const pixels = context.getImageData(0, start, width, end - start).data;
|
|
|
+ for (let pixel = 0; pixel < pixels.length; pixel += 4) {
|
|
|
+ if (pixels[pixel] < 190 || pixels[pixel + 1] < 190 || pixels[pixel + 2] < 190) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
}
|