refac
This commit is contained in:
1 parent
b3a5fd3875
commit
f96b717566
3 files changed
+110
-47
No files matched your search
@@ -161,7 +161,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mr-1 flex flex-none items-center gap-2 self-center">
|
<div class="lg:mr-1 flex flex-none items-center gap-2 self-center">
|
||||||
<!-- <div class="md:hidden flex self-center w-[0.0625rem] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
|
<!-- <div class="md:hidden flex self-center w-[0.0625rem] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
|
||||||
|
|
||||||
{#if $user?.role === 'user' ? ($user?.permissions?.chat?.temporary ?? true) && !($user?.permissions?.chat?.temporary_enforced ?? false) : true}
|
{#if $user?.role === 'user' ? ($user?.permissions?.chat?.temporary ?? true) && !($user?.permissions?.chat?.temporary_enforced ?? false) : true}
|
||||||
|
|||||||
@@ -261,7 +261,7 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute bottom-3 {hideThumbs
|
class="absolute bottom-3 {hideThumbs
|
||||||
? 'left-1/2'
|
? 'left-1/2'
|
||||||
: 'left-[calc(160px+(100%-160px)/2)]'} -translate-x-1/2 z-10 flex items-center gap-0.5 rounded-lg bg-white/90 dark:bg-gray-850/90 backdrop-blur-sm shadow-lg border border-gray-200/60 dark:border-gray-700/60 px-1 py-0.5"
|
: 'left-[calc(144px+(100%-144px)/2)]'} -translate-x-1/2 z-10 flex items-center gap-0.5 rounded-lg bg-white/90 dark:bg-gray-850/90 backdrop-blur-sm shadow-lg border border-gray-200/60 dark:border-gray-700/60 px-1 py-0.5"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
+108
-45
@@ -38,6 +38,7 @@ type Placeholder = {
|
|||||||
|
|
||||||
type TextToken = {
|
type TextToken = {
|
||||||
text: string;
|
text: string;
|
||||||
|
fontFace: string;
|
||||||
fontPt: number;
|
fontPt: number;
|
||||||
bold: boolean;
|
bold: boolean;
|
||||||
italic: boolean;
|
italic: boolean;
|
||||||
@@ -45,6 +46,8 @@ type TextToken = {
|
|||||||
width: number;
|
width: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TextStyle = Omit<TextToken, 'text' | 'width'>;
|
||||||
|
|
||||||
const getTag = (el: Element) => el.tagName.split(':').pop();
|
const getTag = (el: Element) => el.tagName.split(':').pop();
|
||||||
|
|
||||||
const normalizePath = (path: string) => {
|
const normalizePath = (path: string) => {
|
||||||
@@ -263,25 +266,70 @@ const renderBackground = (
|
|||||||
ctx.fillRect(0, 0, slideW, slideH);
|
ctx.fillRect(0, 0, slideW, slideH);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fontString = ({ italic, bold, fontPt }: Pick<TextToken, 'italic' | 'bold' | 'fontPt'>) =>
|
const directChild = (el: Element, tag: string) =>
|
||||||
`${italic ? 'italic ' : ''}${bold ? 'bold ' : ''}${fontPt}pt Calibri, Arial, sans-serif`;
|
Array.from(el.children).find((child) => getTag(child) === tag);
|
||||||
|
|
||||||
const readRunStyle = (run: Element, defaultFontSize: number) => {
|
const defaultTextStyle = (fontPt: number): TextStyle => ({
|
||||||
const rPr = run.getElementsByTagName('a:rPr')[0];
|
fontFace: 'Calibri',
|
||||||
let fontPt = defaultFontSize;
|
fontPt,
|
||||||
let bold = false;
|
bold: false,
|
||||||
let italic = false;
|
italic: false,
|
||||||
let color = '#000000';
|
color: '#000000'
|
||||||
|
});
|
||||||
|
|
||||||
if (rPr) {
|
const fontString = ({ italic, bold, fontPt, fontFace }: TextStyle) => {
|
||||||
if (rPr.getAttribute('b') === '1') bold = true;
|
const family = fontFace.includes(' ') ? `"${fontFace}"` : fontFace;
|
||||||
if (rPr.getAttribute('i') === '1') italic = true;
|
return `${italic ? 'italic ' : ''}${bold ? 'bold ' : ''}${fontPt}pt ${family}, Calibri, Arial, sans-serif`;
|
||||||
const sz = rPr.getAttribute('sz');
|
};
|
||||||
if (sz) fontPt = parseInt(sz, 10) / 100;
|
|
||||||
color = solidFillColor(rPr) ?? color;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { fontPt, bold, italic, color };
|
const readTextStyle = (rPr: Element | undefined, base: TextStyle): TextStyle => {
|
||||||
|
if (!rPr) return base;
|
||||||
|
|
||||||
|
const latin = rPr.getElementsByTagName('a:latin')[0];
|
||||||
|
const typeface = latin?.getAttribute('typeface');
|
||||||
|
const sz = rPr.getAttribute('sz');
|
||||||
|
|
||||||
|
return {
|
||||||
|
fontFace: typeface && !typeface.startsWith('+') ? typeface : base.fontFace,
|
||||||
|
fontPt: sz ? parseInt(sz, 10) / 100 : base.fontPt,
|
||||||
|
bold: rPr.getAttribute('b') === '1' ? true : rPr.getAttribute('b') === '0' ? false : base.bold,
|
||||||
|
italic:
|
||||||
|
rPr.getAttribute('i') === '1' ? true : rPr.getAttribute('i') === '0' ? false : base.italic,
|
||||||
|
color: solidFillColor(rPr) ?? base.color
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const readParagraphStyle = (para: Element, defaultFontSize: number) => {
|
||||||
|
const pPr = directChild(para, 'pPr');
|
||||||
|
const defRPr = pPr?.getElementsByTagName('a:defRPr')[0];
|
||||||
|
const endParaRPr = para.getElementsByTagName('a:endParaRPr')[0];
|
||||||
|
return readTextStyle(defRPr ?? endParaRPr, defaultTextStyle(defaultFontSize));
|
||||||
|
};
|
||||||
|
|
||||||
|
const readRunStyle = (run: Element, base: TextStyle) => {
|
||||||
|
const rPr = directChild(run, 'rPr') as Element | undefined;
|
||||||
|
return readTextStyle(rPr, base);
|
||||||
|
};
|
||||||
|
|
||||||
|
const textBodyInsets = (txBody: Element) => {
|
||||||
|
const bodyPr = txBody.getElementsByTagName('a:bodyPr')[0];
|
||||||
|
return {
|
||||||
|
left: emuToPx(parseEmu(bodyPr?.getAttribute('lIns') ?? '91440')),
|
||||||
|
right: emuToPx(parseEmu(bodyPr?.getAttribute('rIns') ?? '91440')),
|
||||||
|
top: emuToPx(parseEmu(bodyPr?.getAttribute('tIns') ?? '45720')),
|
||||||
|
bottom: emuToPx(parseEmu(bodyPr?.getAttribute('bIns') ?? '45720'))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const paragraphBullet = (para: Element) => {
|
||||||
|
const pPr = directChild(para, 'pPr');
|
||||||
|
if (!pPr || pPr.getElementsByTagName('a:buNone')[0]) return '';
|
||||||
|
|
||||||
|
const buChar = pPr.getElementsByTagName('a:buChar')[0]?.getAttribute('char');
|
||||||
|
if (buChar) return `${buChar} `;
|
||||||
|
|
||||||
|
if (pPr.getElementsByTagName('a:buAutoNum')[0]) return '1. ';
|
||||||
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawTextLine = (
|
const drawTextLine = (
|
||||||
@@ -293,9 +341,9 @@ const drawTextLine = (
|
|||||||
w: number
|
w: number
|
||||||
) => {
|
) => {
|
||||||
const lineWidth = line.reduce((sum, token) => sum + token.width, 0);
|
const lineWidth = line.reduce((sum, token) => sum + token.width, 0);
|
||||||
let cursorX = x + 4;
|
let cursorX = x;
|
||||||
if (align === 'center') cursorX = x + Math.max(0, (w - lineWidth) / 2);
|
if (align === 'center') cursorX = x + Math.max(0, (w - lineWidth) / 2);
|
||||||
if (align === 'right') cursorX = x + w - lineWidth - 4;
|
if (align === 'right') cursorX = x + w - lineWidth;
|
||||||
|
|
||||||
for (const token of line) {
|
for (const token of line) {
|
||||||
ctx.font = fontString(token);
|
ctx.font = fontString(token);
|
||||||
@@ -452,32 +500,42 @@ export async function pptxToImages(
|
|||||||
ctx.clip();
|
ctx.clip();
|
||||||
|
|
||||||
const paragraphs = txBody.getElementsByTagName('a:p');
|
const paragraphs = txBody.getElementsByTagName('a:p');
|
||||||
let cursorY = y;
|
const insets = textBodyInsets(txBody);
|
||||||
|
const textX = x + insets.left;
|
||||||
|
const textY = y + insets.top;
|
||||||
|
const textW = Math.max(1, w - insets.left - insets.right);
|
||||||
|
const textBottom = y + h - insets.bottom;
|
||||||
|
let cursorY = textY;
|
||||||
const defaultFontSize = placeholderFontSize(ph ?? placeholder ?? null);
|
const defaultFontSize = placeholderFontSize(ph ?? placeholder ?? null);
|
||||||
|
|
||||||
for (let pi = 0; pi < paragraphs.length; pi++) {
|
for (let pi = 0; pi < paragraphs.length; pi++) {
|
||||||
const para = paragraphs[pi];
|
const para = paragraphs[pi];
|
||||||
const runs = para.getElementsByTagName('a:r');
|
|
||||||
const align = paragraphAlign(para, ph ?? placeholder ?? null);
|
const align = paragraphAlign(para, ph ?? placeholder ?? null);
|
||||||
|
const paragraphStyle = readParagraphStyle(para, defaultFontSize);
|
||||||
|
const textParts: Array<{ text: string; style: TextStyle } | { newline: true }> = [];
|
||||||
|
const bullet = paragraphBullet(para);
|
||||||
|
|
||||||
if (runs.length === 0) {
|
for (const child of Array.from(para.children)) {
|
||||||
cursorY += defaultFontSize * 1.5;
|
const tag = getTag(child);
|
||||||
continue;
|
if (tag === 'br') {
|
||||||
}
|
textParts.push({ newline: true });
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (tag !== 'r' && tag !== 'fld') continue;
|
||||||
|
|
||||||
|
const style = tag === 'r' ? readRunStyle(child, paragraphStyle) : paragraphStyle;
|
||||||
|
const text = child.getElementsByTagName('a:t')[0]?.textContent ?? '';
|
||||||
|
if (text) textParts.push({ text, style });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bullet && textParts.some((part) => 'text' in part && part.text.trim())) {
|
||||||
|
textParts.unshift({ text: bullet, style: paragraphStyle });
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxFontPt = textParts.reduce(
|
||||||
|
(max, part) => ('text' in part ? Math.max(max, part.style.fontPt) : max),
|
||||||
|
paragraphStyle.fontPt
|
||||||
|
);
|
||||||
const lineHeight = maxFontPt * 1.4;
|
const lineHeight = maxFontPt * 1.4;
|
||||||
cursorY += maxFontPt; // baseline offset
|
cursorY += maxFontPt; // baseline offset
|
||||||
|
|
||||||
@@ -485,22 +543,27 @@ export async function pptxToImages(
|
|||||||
let lineWidth = 0;
|
let lineWidth = 0;
|
||||||
const flushLine = () => {
|
const flushLine = () => {
|
||||||
if (line.length === 0) return;
|
if (line.length === 0) return;
|
||||||
if (cursorY <= y + h) {
|
if (cursorY <= textBottom) {
|
||||||
drawTextLine(ctx, line, align, x, cursorY, w);
|
drawTextLine(ctx, line, align, textX, cursorY, textW);
|
||||||
}
|
}
|
||||||
line = [];
|
line = [];
|
||||||
lineWidth = 0;
|
lineWidth = 0;
|
||||||
cursorY += lineHeight;
|
cursorY += lineHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let ri = 0; ri < runs.length; ri++) {
|
if (textParts.length === 0) {
|
||||||
const run = runs[ri];
|
cursorY += lineHeight;
|
||||||
const text = run.getElementsByTagName('a:t')[0]?.textContent ?? '';
|
continue;
|
||||||
if (!text) continue;
|
}
|
||||||
|
|
||||||
const style = readRunStyle(run, defaultFontSize);
|
for (const part of textParts) {
|
||||||
|
if ('newline' in part) {
|
||||||
|
if (line.length > 0) flushLine();
|
||||||
|
else cursorY += lineHeight;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { text, style } = part;
|
||||||
ctx.font = fontString(style);
|
ctx.font = fontString(style);
|
||||||
|
|
||||||
ctx.textBaseline = 'alphabetic';
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
|
||||||
// Simple word-wrap within the shape bounds
|
// Simple word-wrap within the shape bounds
|
||||||
@@ -511,10 +574,10 @@ export async function pptxToImages(
|
|||||||
flushLine();
|
flushLine();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (cursorY > y + h) break;
|
if (cursorY > textBottom) break;
|
||||||
|
|
||||||
const width = ctx.measureText(word).width;
|
const width = ctx.measureText(word).width;
|
||||||
if (lineWidth + width > w - 8 && line.length > 0) {
|
if (lineWidth + width > textW && line.length > 0) {
|
||||||
flushLine();
|
flushLine();
|
||||||
if (word.trim() === '') continue;
|
if (word.trim() === '') continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user