refac
This commit is contained in:
@@ -186,4 +186,3 @@ export async function highlightCode(code: string, filePath: string): Promise<str
|
||||
defaultColor: 'light'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,25 +12,25 @@ import type { WorkSheet } from 'xlsx';
|
||||
|
||||
/** Convert column index (0-based) to Excel-style letter (A, B, …, Z, AA, AB, …) */
|
||||
const colLetter = (i: number): string => {
|
||||
let s = '';
|
||||
let n = i;
|
||||
while (n >= 0) {
|
||||
s = String.fromCharCode(65 + (n % 26)) + s;
|
||||
n = Math.floor(n / 26) - 1;
|
||||
}
|
||||
return s;
|
||||
let s = '';
|
||||
let n = i;
|
||||
while (n >= 0) {
|
||||
s = String.fromCharCode(65 + (n % 26)) + s;
|
||||
n = Math.floor(n / 26) - 1;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
/** Escape HTML entities */
|
||||
const esc = (v: unknown): string => {
|
||||
if (v === null || v === undefined || v === '') return ' ';
|
||||
return String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
if (v === null || v === undefined || v === '') return ' ';
|
||||
return String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
};
|
||||
|
||||
export interface ExcelTableResult {
|
||||
html: string;
|
||||
rowCount: number;
|
||||
colCount: number;
|
||||
html: string;
|
||||
rowCount: number;
|
||||
colCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,49 +38,51 @@ export interface ExcelTableResult {
|
||||
* Uses sheet_to_json with header:1 for a raw 2D array.
|
||||
*/
|
||||
export async function excelToTable(worksheet: WorkSheet): Promise<ExcelTableResult> {
|
||||
const XLSX = await import('xlsx');
|
||||
const rows: unknown[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' });
|
||||
const XLSX = await import('xlsx');
|
||||
const rows: unknown[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' });
|
||||
|
||||
if (rows.length === 0) {
|
||||
return { html: '<table><tbody><tr><td> </td></tr></tbody></table>', rowCount: 0, colCount: 0 };
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
return {
|
||||
html: '<table><tbody><tr><td> </td></tr></tbody></table>',
|
||||
rowCount: 0,
|
||||
colCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Determine column count from the widest row
|
||||
const colCount = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
||||
const rowCount = rows.length;
|
||||
// Determine column count from the widest row
|
||||
const colCount = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
||||
const rowCount = rows.length;
|
||||
|
||||
const parts: string[] = [];
|
||||
parts.push('<table>');
|
||||
const parts: string[] = [];
|
||||
parts.push('<table>');
|
||||
|
||||
// Column letter header row
|
||||
parts.push('<thead><tr>');
|
||||
parts.push('<th class="excel-row-num"></th>'); // corner cell
|
||||
for (let c = 0; c < colCount; c++) {
|
||||
parts.push(`<th class="excel-col-hdr">${colLetter(c)}</th>`);
|
||||
}
|
||||
parts.push('</tr></thead>');
|
||||
// Column letter header row
|
||||
parts.push('<thead><tr>');
|
||||
parts.push('<th class="excel-row-num"></th>'); // corner cell
|
||||
for (let c = 0; c < colCount; c++) {
|
||||
parts.push(`<th class="excel-col-hdr">${colLetter(c)}</th>`);
|
||||
}
|
||||
parts.push('</tr></thead>');
|
||||
|
||||
// Data rows
|
||||
parts.push('<tbody>');
|
||||
for (let r = 0; r < rowCount; r++) {
|
||||
const row = rows[r];
|
||||
parts.push('<tr>');
|
||||
parts.push(`<td class="excel-row-num">${r + 1}</td>`);
|
||||
for (let c = 0; c < colCount; c++) {
|
||||
const val = c < row.length ? row[c] : '';
|
||||
const isNum = typeof val === 'number';
|
||||
parts.push(
|
||||
`<td${isNum ? ' class="excel-num"' : ''}>${esc(val)}</td>`
|
||||
);
|
||||
}
|
||||
parts.push('</tr>');
|
||||
}
|
||||
parts.push('</tbody></table>');
|
||||
// Data rows
|
||||
parts.push('<tbody>');
|
||||
for (let r = 0; r < rowCount; r++) {
|
||||
const row = rows[r];
|
||||
parts.push('<tr>');
|
||||
parts.push(`<td class="excel-row-num">${r + 1}</td>`);
|
||||
for (let c = 0; c < colCount; c++) {
|
||||
const val = c < row.length ? row[c] : '';
|
||||
const isNum = typeof val === 'number';
|
||||
parts.push(`<td${isNum ? ' class="excel-num"' : ''}>${esc(val)}</td>`);
|
||||
}
|
||||
parts.push('</tr>');
|
||||
}
|
||||
parts.push('</tbody></table>');
|
||||
|
||||
const DOMPurify = (await import('dompurify')).default;
|
||||
return {
|
||||
html: DOMPurify.sanitize(parts.join('')),
|
||||
rowCount,
|
||||
colCount
|
||||
};
|
||||
const DOMPurify = (await import('dompurify')).default;
|
||||
return {
|
||||
html: DOMPurify.sanitize(parts.join('')),
|
||||
rowCount,
|
||||
colCount
|
||||
};
|
||||
}
|
||||
|
||||
+23
-23
@@ -275,7 +275,6 @@ export const canvasPixelTest = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
let resizeImageWarmupDone = false;
|
||||
/**
|
||||
* Draws an image to a canvas at the given dimensions and returns a data URL.
|
||||
@@ -285,7 +284,7 @@ async function resizeImageToDataURL(
|
||||
img: HTMLImageElement,
|
||||
width: number,
|
||||
height: number,
|
||||
mimeType = 'image/jpeg',
|
||||
mimeType = 'image/jpeg'
|
||||
): Promise<string> {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
@@ -294,22 +293,23 @@ async function resizeImageToDataURL(
|
||||
|
||||
const toDataURL = () => canvas.toDataURL(mimeType);
|
||||
|
||||
if (!resizeImageWarmupDone && canvas.toBlob && /android|iphone|ipad|ipod/i.test(navigator?.userAgent)) {
|
||||
if (
|
||||
!resizeImageWarmupDone &&
|
||||
canvas.toBlob &&
|
||||
/android|iphone|ipad|ipod/i.test(navigator?.userAgent)
|
||||
) {
|
||||
resizeImageWarmupDone = true;
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
resolve(toDataURL());
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result));
|
||||
reader.onerror = () => resolve(toDataURL());
|
||||
reader.readAsDataURL(blob);
|
||||
},
|
||||
mimeType,
|
||||
);
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
resolve(toDataURL());
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result));
|
||||
reader.onerror = () => resolve(toDataURL());
|
||||
reader.readAsDataURL(blob);
|
||||
}, mimeType);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(toDataURL());
|
||||
@@ -393,9 +393,9 @@ export const generateInitialsImage = (name) => {
|
||||
const initials =
|
||||
sanitizedName.length > 0
|
||||
? sanitizedName[0] +
|
||||
(sanitizedName.split(' ').length > 1
|
||||
? sanitizedName[sanitizedName.lastIndexOf(' ') + 1]
|
||||
: '')
|
||||
(sanitizedName.split(' ').length > 1
|
||||
? sanitizedName[sanitizedName.lastIndexOf(' ') + 1]
|
||||
: '')
|
||||
: '';
|
||||
|
||||
ctx.fillText(initials.toUpperCase(), canvas.width / 2, canvas.height / 2);
|
||||
@@ -551,10 +551,10 @@ export const compareVersion = (latest, current) => {
|
||||
return current === '0.0.0'
|
||||
? false
|
||||
: current.localeCompare(latest, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'case',
|
||||
caseFirst: 'upper'
|
||||
}) < 0;
|
||||
numeric: true,
|
||||
sensitivity: 'case',
|
||||
caseFirst: 'upper'
|
||||
}) < 0;
|
||||
};
|
||||
|
||||
export const extractCurlyBraceWords = (text) => {
|
||||
|
||||
+202
-204
@@ -11,249 +11,247 @@
|
||||
const EMU_PER_PX = 9525;
|
||||
const emuToPx = (emu: number) => Math.round(emu / EMU_PER_PX);
|
||||
|
||||
const parseEmu = (val: string | null | undefined): number =>
|
||||
val ? parseInt(val, 10) || 0 : 0;
|
||||
const parseEmu = (val: string | null | undefined): number => (val ? parseInt(val, 10) || 0 : 0);
|
||||
|
||||
/** Load a data URI into an Image element and wait for it. */
|
||||
const loadImage = (src: string): Promise<HTMLImageElement> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject(new Error('Failed to load image'));
|
||||
img.src = src;
|
||||
});
|
||||
new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject(new Error('Failed to load image'));
|
||||
img.src = src;
|
||||
});
|
||||
|
||||
/**
|
||||
* Convert PPTX ArrayBuffer → array of PNG data URL strings, one per slide.
|
||||
*/
|
||||
export async function pptxToImages(
|
||||
buffer: ArrayBuffer
|
||||
buffer: ArrayBuffer
|
||||
): Promise<{ images: string[]; width: number; height: number }> {
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
|
||||
// ── Read slide dimensions from presentation.xml ──────────────────
|
||||
let slideW = 960;
|
||||
let slideH = 540;
|
||||
const presXml = zip.file('ppt/presentation.xml');
|
||||
if (presXml) {
|
||||
const presText = await presXml.async('text');
|
||||
const presDoc = new DOMParser().parseFromString(presText, 'application/xml');
|
||||
const sldSz = presDoc.getElementsByTagName('p:sldSz')[0];
|
||||
if (sldSz) {
|
||||
slideW = emuToPx(parseEmu(sldSz.getAttribute('cx')));
|
||||
slideH = emuToPx(parseEmu(sldSz.getAttribute('cy')));
|
||||
}
|
||||
}
|
||||
// ── Read slide dimensions from presentation.xml ──────────────────
|
||||
let slideW = 960;
|
||||
let slideH = 540;
|
||||
const presXml = zip.file('ppt/presentation.xml');
|
||||
if (presXml) {
|
||||
const presText = await presXml.async('text');
|
||||
const presDoc = new DOMParser().parseFromString(presText, 'application/xml');
|
||||
const sldSz = presDoc.getElementsByTagName('p:sldSz')[0];
|
||||
if (sldSz) {
|
||||
slideW = emuToPx(parseEmu(sldSz.getAttribute('cx')));
|
||||
slideH = emuToPx(parseEmu(sldSz.getAttribute('cy')));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Collect media files (images) as base64 data URIs ─────────────
|
||||
const media: Record<string, string> = {};
|
||||
const mediaFiles = Object.keys(zip.files).filter((f) => f.startsWith('ppt/media/'));
|
||||
await Promise.all(
|
||||
mediaFiles.map(async (path) => {
|
||||
const file = zip.file(path);
|
||||
if (!file) return;
|
||||
const base64 = await file.async('base64');
|
||||
const ext = path.split('.').pop()?.toLowerCase() ?? '';
|
||||
const mime =
|
||||
ext === 'png'
|
||||
? 'image/png'
|
||||
: ext === 'gif'
|
||||
? 'image/gif'
|
||||
: ext === 'svg'
|
||||
? 'image/svg+xml'
|
||||
: ext === 'emf' || ext === 'wmf'
|
||||
? 'image/x-emf'
|
||||
: 'image/jpeg';
|
||||
media[path] = `data:${mime};base64,${base64}`;
|
||||
})
|
||||
);
|
||||
// ── Collect media files (images) as base64 data URIs ─────────────
|
||||
const media: Record<string, string> = {};
|
||||
const mediaFiles = Object.keys(zip.files).filter((f) => f.startsWith('ppt/media/'));
|
||||
await Promise.all(
|
||||
mediaFiles.map(async (path) => {
|
||||
const file = zip.file(path);
|
||||
if (!file) return;
|
||||
const base64 = await file.async('base64');
|
||||
const ext = path.split('.').pop()?.toLowerCase() ?? '';
|
||||
const mime =
|
||||
ext === 'png'
|
||||
? 'image/png'
|
||||
: ext === 'gif'
|
||||
? 'image/gif'
|
||||
: ext === 'svg'
|
||||
? 'image/svg+xml'
|
||||
: ext === 'emf' || ext === 'wmf'
|
||||
? 'image/x-emf'
|
||||
: 'image/jpeg';
|
||||
media[path] = `data:${mime};base64,${base64}`;
|
||||
})
|
||||
);
|
||||
|
||||
// ── Discover slide files ─────────────────────────────────────────
|
||||
const slideFiles = Object.keys(zip.files)
|
||||
.filter((f) => /^ppt\/slides\/slide\d+\.xml$/.test(f))
|
||||
.sort((a, b) => {
|
||||
const na = parseInt(a.match(/slide(\d+)/)?.[1] ?? '0');
|
||||
const nb = parseInt(b.match(/slide(\d+)/)?.[1] ?? '0');
|
||||
return na - nb;
|
||||
});
|
||||
// ── Discover slide files ─────────────────────────────────────────
|
||||
const slideFiles = Object.keys(zip.files)
|
||||
.filter((f) => /^ppt\/slides\/slide\d+\.xml$/.test(f))
|
||||
.sort((a, b) => {
|
||||
const na = parseInt(a.match(/slide(\d+)/)?.[1] ?? '0');
|
||||
const nb = parseInt(b.match(/slide(\d+)/)?.[1] ?? '0');
|
||||
return na - nb;
|
||||
});
|
||||
|
||||
const images: string[] = [];
|
||||
const images: string[] = [];
|
||||
|
||||
for (const slidePath of slideFiles) {
|
||||
const slideText = await zip.file(slidePath)!.async('text');
|
||||
const slideDoc = new DOMParser().parseFromString(slideText, 'application/xml');
|
||||
for (const slidePath of slideFiles) {
|
||||
const slideText = await zip.file(slidePath)!.async('text');
|
||||
const slideDoc = new DOMParser().parseFromString(slideText, 'application/xml');
|
||||
|
||||
// Load relationship file for this slide to resolve image references
|
||||
const slideNum = slidePath.match(/slide(\d+)/)?.[1];
|
||||
const relsPath = `ppt/slides/_rels/slide${slideNum}.xml.rels`;
|
||||
const rels: Record<string, string> = {};
|
||||
const relsFile = zip.file(relsPath);
|
||||
if (relsFile) {
|
||||
const relsText = await relsFile.async('text');
|
||||
const relsDoc = new DOMParser().parseFromString(relsText, 'application/xml');
|
||||
const relEls = relsDoc.getElementsByTagName('Relationship');
|
||||
for (let i = 0; i < relEls.length; i++) {
|
||||
const rel = relEls[i];
|
||||
const id = rel.getAttribute('Id') ?? '';
|
||||
const target = rel.getAttribute('Target') ?? '';
|
||||
if (target.startsWith('../')) {
|
||||
rels[id] = 'ppt/' + target.replace('../', '');
|
||||
} else {
|
||||
rels[id] = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load relationship file for this slide to resolve image references
|
||||
const slideNum = slidePath.match(/slide(\d+)/)?.[1];
|
||||
const relsPath = `ppt/slides/_rels/slide${slideNum}.xml.rels`;
|
||||
const rels: Record<string, string> = {};
|
||||
const relsFile = zip.file(relsPath);
|
||||
if (relsFile) {
|
||||
const relsText = await relsFile.async('text');
|
||||
const relsDoc = new DOMParser().parseFromString(relsText, 'application/xml');
|
||||
const relEls = relsDoc.getElementsByTagName('Relationship');
|
||||
for (let i = 0; i < relEls.length; i++) {
|
||||
const rel = relEls[i];
|
||||
const id = rel.getAttribute('Id') ?? '';
|
||||
const target = rel.getAttribute('Target') ?? '';
|
||||
if (target.startsWith('../')) {
|
||||
rels[id] = 'ppt/' + target.replace('../', '');
|
||||
} else {
|
||||
rels[id] = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create canvas and render slide ───────────────────────────
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = slideW;
|
||||
canvas.height = slideH;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
// ── Create canvas and render slide ───────────────────────────
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = slideW;
|
||||
canvas.height = slideH;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
// White background
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, slideW, slideH);
|
||||
// White background
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, slideW, slideH);
|
||||
|
||||
const spTree = slideDoc.getElementsByTagName('p:spTree')[0];
|
||||
if (!spTree) {
|
||||
images.push(canvas.toDataURL('image/png'));
|
||||
continue;
|
||||
}
|
||||
const spTree = slideDoc.getElementsByTagName('p:spTree')[0];
|
||||
if (!spTree) {
|
||||
images.push(canvas.toDataURL('image/png'));
|
||||
continue;
|
||||
}
|
||||
|
||||
const shapes = [
|
||||
...Array.from(spTree.getElementsByTagName('p:sp')),
|
||||
...Array.from(spTree.getElementsByTagName('p:pic'))
|
||||
];
|
||||
const shapes = [
|
||||
...Array.from(spTree.getElementsByTagName('p:sp')),
|
||||
...Array.from(spTree.getElementsByTagName('p:pic'))
|
||||
];
|
||||
|
||||
for (const shape of shapes) {
|
||||
const xfrm =
|
||||
shape.getElementsByTagName('a:xfrm')[0] ??
|
||||
shape.getElementsByTagName('p:xfrm')[0];
|
||||
if (!xfrm) continue;
|
||||
for (const shape of shapes) {
|
||||
const xfrm =
|
||||
shape.getElementsByTagName('a:xfrm')[0] ?? shape.getElementsByTagName('p:xfrm')[0];
|
||||
if (!xfrm) continue;
|
||||
|
||||
const off = xfrm.getElementsByTagName('a:off')[0];
|
||||
const ext = xfrm.getElementsByTagName('a:ext')[0];
|
||||
if (!off || !ext) continue;
|
||||
const off = xfrm.getElementsByTagName('a:off')[0];
|
||||
const ext = xfrm.getElementsByTagName('a:ext')[0];
|
||||
if (!off || !ext) continue;
|
||||
|
||||
const x = emuToPx(parseEmu(off.getAttribute('x')));
|
||||
const y = emuToPx(parseEmu(off.getAttribute('y')));
|
||||
const w = emuToPx(parseEmu(ext.getAttribute('cx')));
|
||||
const h = emuToPx(parseEmu(ext.getAttribute('cy')));
|
||||
const x = emuToPx(parseEmu(off.getAttribute('x')));
|
||||
const y = emuToPx(parseEmu(off.getAttribute('y')));
|
||||
const w = emuToPx(parseEmu(ext.getAttribute('cx')));
|
||||
const h = emuToPx(parseEmu(ext.getAttribute('cy')));
|
||||
|
||||
if (w === 0 && h === 0) continue;
|
||||
if (w === 0 && h === 0) continue;
|
||||
|
||||
// ── Picture ──────────────────────────────────────────────
|
||||
const blipFill = shape.getElementsByTagName('p:blipFill')[0];
|
||||
if (blipFill) {
|
||||
const blip = blipFill.getElementsByTagName('a:blip')[0];
|
||||
if (blip) {
|
||||
const rEmbed = blip.getAttribute('r:embed') ?? '';
|
||||
const mediaPath = rels[rEmbed];
|
||||
const dataUri = mediaPath ? media[mediaPath] : '';
|
||||
if (dataUri && !dataUri.includes('image/x-emf')) {
|
||||
try {
|
||||
const img = await loadImage(dataUri);
|
||||
ctx.drawImage(img, x, y, w, h);
|
||||
} catch {
|
||||
// Skip images that fail to load
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// ── Picture ──────────────────────────────────────────────
|
||||
const blipFill = shape.getElementsByTagName('p:blipFill')[0];
|
||||
if (blipFill) {
|
||||
const blip = blipFill.getElementsByTagName('a:blip')[0];
|
||||
if (blip) {
|
||||
const rEmbed = blip.getAttribute('r:embed') ?? '';
|
||||
const mediaPath = rels[rEmbed];
|
||||
const dataUri = mediaPath ? media[mediaPath] : '';
|
||||
if (dataUri && !dataUri.includes('image/x-emf')) {
|
||||
try {
|
||||
const img = await loadImage(dataUri);
|
||||
ctx.drawImage(img, x, y, w, h);
|
||||
} catch {
|
||||
// Skip images that fail to load
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── Text shape ───────────────────────────────────────────
|
||||
const txBody = shape.getElementsByTagName('p:txBody')[0];
|
||||
if (!txBody) continue;
|
||||
// ── Text shape ───────────────────────────────────────────
|
||||
const txBody = shape.getElementsByTagName('p:txBody')[0];
|
||||
if (!txBody) continue;
|
||||
|
||||
ctx.save();
|
||||
ctx.rect(x, y, w, h);
|
||||
ctx.clip();
|
||||
ctx.save();
|
||||
ctx.rect(x, y, w, h);
|
||||
ctx.clip();
|
||||
|
||||
const paragraphs = txBody.getElementsByTagName('a:p');
|
||||
let cursorY = y;
|
||||
const defaultFontSize = 12;
|
||||
const paragraphs = txBody.getElementsByTagName('a:p');
|
||||
let cursorY = y;
|
||||
const defaultFontSize = 12;
|
||||
|
||||
for (let pi = 0; pi < paragraphs.length; pi++) {
|
||||
const para = paragraphs[pi];
|
||||
const runs = para.getElementsByTagName('a:r');
|
||||
for (let pi = 0; pi < paragraphs.length; pi++) {
|
||||
const para = paragraphs[pi];
|
||||
const runs = para.getElementsByTagName('a:r');
|
||||
|
||||
if (runs.length === 0) {
|
||||
cursorY += defaultFontSize * 1.5;
|
||||
continue;
|
||||
}
|
||||
if (runs.length === 0) {
|
||||
cursorY += defaultFontSize * 1.5;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate max font size in this paragraph for line height
|
||||
let maxFontPt = defaultFontSize;
|
||||
for (let ri = 0; ri < runs.length; ri++) {
|
||||
const rPr = runs[ri].getElementsByTagName('a:rPr')[0];
|
||||
if (rPr) {
|
||||
const sz = rPr.getAttribute('sz');
|
||||
if (sz) {
|
||||
const pt = parseInt(sz, 10) / 100;
|
||||
if (pt > maxFontPt) maxFontPt = pt;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Calculate max font size in this paragraph for line height
|
||||
let maxFontPt = defaultFontSize;
|
||||
for (let ri = 0; ri < runs.length; ri++) {
|
||||
const rPr = runs[ri].getElementsByTagName('a:rPr')[0];
|
||||
if (rPr) {
|
||||
const sz = rPr.getAttribute('sz');
|
||||
if (sz) {
|
||||
const pt = parseInt(sz, 10) / 100;
|
||||
if (pt > maxFontPt) maxFontPt = pt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lineHeight = maxFontPt * 1.4;
|
||||
cursorY += maxFontPt; // baseline offset
|
||||
const lineHeight = maxFontPt * 1.4;
|
||||
cursorY += maxFontPt; // baseline offset
|
||||
|
||||
let cursorX = x + 4; // small left padding
|
||||
let cursorX = x + 4; // small left padding
|
||||
|
||||
for (let ri = 0; ri < runs.length; ri++) {
|
||||
const run = runs[ri];
|
||||
const rPr = run.getElementsByTagName('a:rPr')[0];
|
||||
const text = run.getElementsByTagName('a:t')[0]?.textContent ?? '';
|
||||
if (!text) continue;
|
||||
for (let ri = 0; ri < runs.length; ri++) {
|
||||
const run = runs[ri];
|
||||
const rPr = run.getElementsByTagName('a:rPr')[0];
|
||||
const text = run.getElementsByTagName('a:t')[0]?.textContent ?? '';
|
||||
if (!text) continue;
|
||||
|
||||
let fontPt = defaultFontSize;
|
||||
let bold = false;
|
||||
let italic = false;
|
||||
let color = '#000000';
|
||||
let fontPt = defaultFontSize;
|
||||
let bold = false;
|
||||
let italic = false;
|
||||
let color = '#000000';
|
||||
|
||||
if (rPr) {
|
||||
if (rPr.getAttribute('b') === '1') bold = true;
|
||||
if (rPr.getAttribute('i') === '1') italic = true;
|
||||
const sz = rPr.getAttribute('sz');
|
||||
if (sz) fontPt = parseInt(sz, 10) / 100;
|
||||
const solidFill = rPr.getElementsByTagName('a:solidFill')[0];
|
||||
if (solidFill) {
|
||||
const srgb = solidFill.getElementsByTagName('a:srgbClr')[0];
|
||||
if (srgb) {
|
||||
const val = srgb.getAttribute('val');
|
||||
if (val) color = `#${val}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rPr) {
|
||||
if (rPr.getAttribute('b') === '1') bold = true;
|
||||
if (rPr.getAttribute('i') === '1') italic = true;
|
||||
const sz = rPr.getAttribute('sz');
|
||||
if (sz) fontPt = parseInt(sz, 10) / 100;
|
||||
const solidFill = rPr.getElementsByTagName('a:solidFill')[0];
|
||||
if (solidFill) {
|
||||
const srgb = solidFill.getElementsByTagName('a:srgbClr')[0];
|
||||
if (srgb) {
|
||||
const val = srgb.getAttribute('val');
|
||||
if (val) color = `#${val}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.font = `${italic ? 'italic ' : ''}${bold ? 'bold ' : ''}${fontPt}pt Calibri, Arial, sans-serif`;
|
||||
ctx.fillStyle = color;
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
ctx.font = `${italic ? 'italic ' : ''}${bold ? 'bold ' : ''}${fontPt}pt Calibri, Arial, sans-serif`;
|
||||
ctx.fillStyle = color;
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
|
||||
// Simple word-wrap within the shape bounds
|
||||
const words = text.split(/(\s+)/);
|
||||
for (const word of words) {
|
||||
const metrics = ctx.measureText(word);
|
||||
if (cursorX + metrics.width > x + w && cursorX > x + 4) {
|
||||
cursorX = x + 4;
|
||||
cursorY += lineHeight;
|
||||
}
|
||||
if (cursorY > y + h) break;
|
||||
ctx.fillText(word, cursorX, cursorY);
|
||||
cursorX += metrics.width;
|
||||
}
|
||||
}
|
||||
// Simple word-wrap within the shape bounds
|
||||
const words = text.split(/(\s+)/);
|
||||
for (const word of words) {
|
||||
const metrics = ctx.measureText(word);
|
||||
if (cursorX + metrics.width > x + w && cursorX > x + 4) {
|
||||
cursorX = x + 4;
|
||||
cursorY += lineHeight;
|
||||
}
|
||||
if (cursorY > y + h) break;
|
||||
ctx.fillText(word, cursorX, cursorY);
|
||||
cursorX += metrics.width;
|
||||
}
|
||||
}
|
||||
|
||||
cursorY += lineHeight * 0.4; // paragraph spacing
|
||||
}
|
||||
cursorY += lineHeight * 0.4; // paragraph spacing
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
images.push(canvas.toDataURL('image/png'));
|
||||
}
|
||||
images.push(canvas.toDataURL('image/png'));
|
||||
}
|
||||
|
||||
return { images, width: slideW, height: slideH };
|
||||
return { images, width: slideW, height: slideH };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user