2.8.0-beta-2

This commit is contained in:
zsviczian
2025-01-19 15:56:39 +01:00
parent b18637f7d0
commit 4209774b4e
9 changed files with 126 additions and 85 deletions

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.8.0-beta-1",
"version": "2.8.0-beta-2",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -506,7 +506,8 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
paperColor: "white",
customPaperColor: "#ffffff",
alignment: "center",
margin: "normal"
margin: "normal",
exportDPI: 300,
},
};

View File

@@ -1048,6 +1048,7 @@ FILENAME_HEAD: "Filename",
EXPORTDIALOG_PAGE_ORIENTATION: "Orientation",
EXPORTDIALOG_ORIENTATION_PORTRAIT: "Portrait",
EXPORTDIALOG_ORIENTATION_LANDSCAPE: "Landscape",
EXPORTDIALOG_PDF_DPI: "Image quality [DPI]",
EXPORTDIALOG_PDF_FIT_TO_PAGE: "Page Fitting",
EXPORTDIALOG_PDF_FIT_OPTION: "Fit to page",
EXPORTDIALOG_PDF_FIT_2_OPTION: "Fit to 2-pages",
@@ -1085,4 +1086,8 @@ FILENAME_HEAD: "Filename",
EXPORTDIALOG_SVGTOCLIPBOARD : "SVG to Clipboard",
EXPORTDIALOG_PDF: "Export PDF",
EXPORTDIALOG_PDFTOVAULT: "PDF to Vault",
EXPORTDIALOG_PDF_PROGRESS_NOTICE: "Exporting page",
EXPORTDIALOG_PDF_PROGRESS_IMAGE: "of image",
EXPORTDIALOG_PDF_PROGRESS_DONE: "Export complete",
};

View File

@@ -43,6 +43,7 @@ export class ExportDialog extends Modal {
public customPaperColor: string = "#ffffff";
public alignment: PDFPageAlignment = "center";
public margin: PDFPageMarginString = "normal";
public exportDPI: number = 300;
constructor(
private plugin: ExcalidrawPlugin,
@@ -68,6 +69,7 @@ export class ExportDialog extends Modal {
this.customPaperColor = plugin.settings.pdfSettings.customPaperColor;
this.alignment = plugin.settings.pdfSettings.alignment;
this.margin = plugin.settings.pdfSettings.margin;
this.exportDPI = plugin.settings.pdfSettings.exportDPI ?? 300;
this.saveSettings = false;
}
@@ -276,7 +278,8 @@ export class ExportDialog extends Modal {
paperColor: this.paperColor,
customPaperColor: this.customPaperColor,
alignment: this.alignment,
margin: this.margin
margin: this.margin,
exportDPI: this.exportDPI,
};
new PDFExportSettingsComponent(
@@ -290,6 +293,7 @@ export class ExportDialog extends Modal {
this.customPaperColor = pdfSettings.customPaperColor;
this.alignment = pdfSettings.alignment;
this.margin = pdfSettings.margin;
this.exportDPI = pdfSettings.exportDPI ?? 300;
}
).render();
}
@@ -378,7 +382,8 @@ export class ExportDialog extends Modal {
paperColor: this.paperColor,
customPaperColor: this.customPaperColor,
alignment: this.alignment,
margin: this.margin
margin: this.margin,
exportDPI: this.exportDPI,
};
await this.plugin.saveSettings();
new Notice(t("EXPORTDIALOG_SAVE_CONFIRMATION"));

View File

@@ -10,6 +10,7 @@ export interface PDFExportSettings {
customPaperColor: string;
alignment: PDFPageAlignment;
margin: PDFPageMarginString;
exportDPI: number;
}
export class PDFExportSettingsComponent {
@@ -55,6 +56,23 @@ export class PDFExportSettingsComponent {
})
);
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PDF_DPI"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"150": "150",
"300": "300",
"600": "600",
"1200": "1200"
})
.setValue(`${this.settings.exportDPI}`)
.onChange(value => {
this.settings.exportDPI = parseInt(value);
this.update();
})
);
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PDF_FIT_TO_PAGE"))
.addDropdown(dropdown =>

View File

@@ -245,6 +245,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
"@property {string} [backgroundColor] - The background color of the PDF pages.\n" +
"@property {PDFMargin} margin - The margins of the PDF pages.\n" +
"@property {PDFPageAlignment} alignment - The alignment of the SVG on the PDF pages.\n" +
"@property {number} exportDPI - The DPI of the exported PDF (150/300/600/1200).\n" +
"\n" +
"@example\n" +
"const pdfData = await createPDF({\n" +
@@ -255,6 +256,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
" backgroundColor: \"#ffffff\",\n" +
" margin: { left: 20, right: 20, top: 20, bottom: 20 },\n" +
" alignment: \"center\"\n" +
" exportDPI: 300\n" +
" }\n" +
"});",
},

View File

@@ -967,7 +967,8 @@ export class ExcalidrawAutomate {
* dimensions: { width: 595.28, height: 841.89 },
* backgroundColor: "#ffffff",
* margin: { left: 20, right: 20, top: 20, bottom: 20 },
* alignment: "center"
* alignment: "center",
* exportDPI: 300,
* }
* });
*/
@@ -984,6 +985,7 @@ export class ExcalidrawAutomate {
pageProps = {
alignment: this.plugin.settings.pdfSettings.alignment,
margin: getMarginValue(this.plugin.settings.pdfSettings.margin),
exportDPI: this.plugin.settings.pdfSettings.exportDPI ?? 300,
};
}

View File

@@ -1,9 +1,10 @@
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
import exp from 'constants';
import { Notice } from 'obsidian';
import { getEA } from 'src/core';
import { t } from 'src/lang/helpers';
const SVG_DPI = 300;
const PDF_DPI = 72;
const SVG_TO_PDF_SCALE = PDF_DPI / SVG_DPI;
export type PDFPageAlignment = "center" | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
export type PDFPageMarginString = "none" | "tiny" | "normal";
@@ -25,6 +26,7 @@ export interface PDFPageProperties {
backgroundColor?: string;
margin: PDFMargin;
alignment: PDFPageAlignment;
exportDPI: number;
}
export interface PageDimensions {
@@ -126,10 +128,12 @@ function calculateDimensions(
pageDim: PageDimensions,
margin: PDFPageProperties['margin'],
scale: PDFExportScale,
alignment: PDFPageAlignment
alignment: PDFPageAlignment,
exportDPI: number,
): SVGDimensions[] {
const pdfWidth = svgWidth * SVG_TO_PDF_SCALE;
const pdfHeight = svgHeight * SVG_TO_PDF_SCALE;
const svg_to_pdf_scale = PDF_DPI / exportDPI;
const pdfWidth = svgWidth * svg_to_pdf_scale;
const pdfHeight = svgHeight * svg_to_pdf_scale;
const availableWidth = pageDim.width - margin.left - margin.right;
const availableHeight = pageDim.height - margin.top - margin.bottom;
@@ -206,10 +210,10 @@ function calculateDimensions(
height: tileHeight,
x: margin.left,
y: margin.top,
sourceX: (col * roundedAvailableWidth) / ((scale.zoom || 1) * SVG_TO_PDF_SCALE),
sourceY: (row * roundedAvailableHeight) / ((scale.zoom || 1) * SVG_TO_PDF_SCALE),
sourceWidth: tileWidth / ((scale.zoom || 1) * SVG_TO_PDF_SCALE),
sourceHeight: tileHeight / ((scale.zoom || 1) * SVG_TO_PDF_SCALE)
sourceX: (col * roundedAvailableWidth) / ((scale.zoom || 1) * svg_to_pdf_scale),
sourceY: (row * roundedAvailableHeight) / ((scale.zoom || 1) * svg_to_pdf_scale),
sourceWidth: tileWidth / ((scale.zoom || 1) * svg_to_pdf_scale),
sourceHeight: tileHeight / ((scale.zoom || 1) * svg_to_pdf_scale)
});
}
}
@@ -223,12 +227,12 @@ async function addSVGToPage(
svg: SVGSVGElement,
dimensions: SVGDimensions,
pageDim: PageDimensions,
backgroundColor?: string
pageProps: PDFPageProperties
) {
const page = pdfDoc.addPage([pageDim.width, pageDim.height]);
if (backgroundColor && backgroundColor !== '#ffffff') {
const { r, g, b } = hexToRGB(backgroundColor);
if (pageProps.backgroundColor && pageProps.backgroundColor !== '#ffffff') {
const { r, g, b } = hexToRGB(pageProps.backgroundColor);
page.drawRectangle({
x: 0,
y: 0,
@@ -238,23 +242,20 @@ async function addSVGToPage(
});
}
// Clone and modify SVG for tiling if needed
let svgToEmbed = svg;
if (dimensions.sourceX !== undefined) {
svgToEmbed = svg.cloneNode(true) as SVGSVGElement;
const viewBox = `${dimensions.sourceX} ${dimensions.sourceY} ${dimensions.sourceWidth} ${dimensions.sourceHeight}`;
svgToEmbed.setAttribute('viewBox', viewBox);
svgToEmbed.setAttribute('width', String(dimensions.sourceWidth));
svgToEmbed.setAttribute('height', String(dimensions.sourceHeight));
}
svgToEmbed = await preprocessSVGForPDFLib(svgToEmbed);
const svgImage = await pdfDoc.embedSvg(svgToEmbed.outerHTML);
// Render SVG to canvas with specified DPI
const canvas = await renderSVGToCanvas(svg, dimensions, pageProps.exportDPI);
// Convert canvas to PNG
const pngData = canvas.toDataURL('image/png');
// Embed the PNG in the PDF
const image = await pdfDoc.embedPng(pngData);
// Adjust y-coordinate to account for PDF coordinate system
const adjustedY = pageDim.height - dimensions.y;
const adjustedY = pageDim.height - dimensions.y - dimensions.height;
page.drawSvg(svgImage, {
// Draw the image
page.drawImage(image, {
x: dimensions.x,
y: adjustedY,
width: dimensions.width,
@@ -264,62 +265,45 @@ async function addSVGToPage(
return page;
}
async function convertImageToDataURL(src: string): Promise<string> {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL('image/png'));
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = src;
});
}
async function preprocessSVGForPDFLib(svg: SVGSVGElement): Promise<SVGSVGElement> {
const clone = svg.cloneNode(true) as SVGSVGElement;
async function renderSVGToCanvas(
svg: SVGSVGElement,
dimensions: SVGDimensions,
exportDPI: number = 300,
): Promise<HTMLCanvasElement> {
const canvas = document.createElement('canvas');
const scale = exportDPI / PDF_DPI;
// Convert all images to PNG format
const images = clone.querySelectorAll('image');
await Promise.all(Array.from(images).map(async (img) => {
const href = img.getAttribute('href') || img.getAttribute('xlink:href');
if (href && href.startsWith('data:image/')) {
try {
const pngDataUrl = await convertImageToDataURL(href);
img.setAttribute('href', pngDataUrl);
if (img.hasAttribute('xlink:href')) {
img.setAttribute('xlink:href', pngDataUrl);
}
} catch (error) {
console.error('Failed to convert image:', error);
}
}
}));
canvas.width = dimensions.width * scale;
canvas.height = dimensions.height * scale;
// Convert symbols with images
const symbols = clone.querySelectorAll('symbol');
for (const symbol of Array.from(symbols)) {
const image = symbol.querySelector('image');
if (image) {
const uses = clone.querySelectorAll(`use[href="#${symbol.id}"]`);
for (const use of Array.from(uses)) {
const newImage = image.cloneNode(true) as SVGImageElement;
newImage.setAttribute('width', use.getAttribute('width') || '100%');
newImage.setAttribute('height', use.getAttribute('height') || '100%');
newImage.setAttribute('x', use.getAttribute('x') || '0');
newImage.setAttribute('y', use.getAttribute('y') || '0');
use.parentNode.replaceChild(newImage, use);
}
}
symbol.remove();
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Failed to get canvas context');
let svgToRender = svg;
if (dimensions.sourceX !== undefined) {
svgToRender = svg.cloneNode(true) as SVGSVGElement;
const viewBox = `${dimensions.sourceX} ${dimensions.sourceY} ${dimensions.sourceWidth} ${dimensions.sourceHeight}`;
svgToRender.setAttribute('viewBox', viewBox);
svgToRender.setAttribute('width', String(dimensions.sourceWidth));
svgToRender.setAttribute('height', String(dimensions.sourceHeight));
}
return clone;
const svgBlob = new Blob([svgToRender.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
const blobUrl = URL.createObjectURL(svgBlob);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
URL.revokeObjectURL(blobUrl);
resolve(canvas);
};
img.onerror = () => {
URL.revokeObjectURL(blobUrl);
reject(new Error('Failed to load SVG'));
};
img.src = blobUrl;
});
}
export async function exportToPDF({
@@ -331,8 +315,15 @@ export async function exportToPDF({
scale: PDFExportScale;
pageProps: PDFPageProperties;
}): Promise<ArrayBuffer> {
const pdfDoc = await PDFDocument.create();
const msg = t('EXPORTDIALOG_PDF_PROGRESS_NOTICE');
const imgmsg = t('EXPORTDIALOG_PDF_PROGRESS_IMAGE');
let notice = new Notice(msg, 0);
let j=1;
for (const svg of SVG) {
const svgWidth = parseFloat(svg.getAttribute('width') || '0');
const svgHeight = parseFloat(svg.getAttribute('height') || '0');
@@ -343,14 +334,30 @@ export async function exportToPDF({
pageProps.dimensions,
pageProps.margin,
scale,
pageProps.alignment
pageProps.alignment,
pageProps.exportDPI
);
let i=1;
for (const dim of dimensions) {
await addSVGToPage(pdfDoc, svg, dim, pageProps.dimensions, pageProps.backgroundColor);
//@ts-ignore
if(notice.containerEl.parentElement) {
notice.setMessage(`${msg} ${i++}/${dimensions.length}${SVG.length>1?` ${imgmsg} ${j}`:""}`);
} else {
notice = new Notice(`${msg} ${i++}/${dimensions.length}${SVG.length>1?` ${imgmsg} ${j}`:""}`, 0);
}
await addSVGToPage(pdfDoc, svg, dim, pageProps.dimensions, pageProps);
}
j++;
}
//@ts-ignore
if(notice.containerEl.parentElement) {
notice.setMessage(t('EXPORTDIALOG_PDF_PROGRESS_DONE'));
setTimeout(() => notice.hide(), 4000);
} else {
new Notice(t('EXPORTDIALOG_PDF_PROGRESS_DONE'));
}
return pdfDoc.save();
}

View File

@@ -598,6 +598,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
backgroundColor: this.exportDialog.getPaperColor(),
margin: getMarginValue(this.exportDialog.margin),
alignment: this.exportDialog.alignment,
exportDPI: this.exportDialog.exportDPI,
}
});