export to vault, added pdf settings

This commit is contained in:
zsviczian
2025-01-18 13:20:00 +01:00
parent c35bd385fe
commit 3d3ce73fa1
5 changed files with 203 additions and 53 deletions

View File

@@ -1050,11 +1050,30 @@ FILENAME_HEAD: "Filename",
EXPORTDIALOG_PAGE_ORIENTATION: "Orientation",
EXPORTDIALOG_ORIENTATION_PORTRAIT: "Portrait",
EXPORTDIALOG_ORIENTATION_LANDSCAPE: "Landscape",
EXPORTDIALOG_PDF_FIT_TO_PAGE: "Page Fitting",
EXPORTDIALOG_PDF_FIT_OPTION: "Fit to page",
EXPORTDIALOG_PDF_SCALE_OPTION: "Use image scale (may span multiple pages)",
EXPORTDIALOG_PDF_PAPER_COLOR: "Paper Color",
EXPORTDIALOG_PDF_PAPER_WHITE: "White",
EXPORTDIALOG_PDF_PAPER_SCENE: "Use scene color",
EXPORTDIALOG_PDF_PAPER_CUSTOM: "Custom color",
EXPORTDIALOG_PDF_ALIGNMENT: "Position on Page",
EXPORTDIALOG_PDF_ALIGN_CENTER: "Center",
EXPORTDIALOG_PDF_ALIGN_TOP_LEFT: "Top Left",
EXPORTDIALOG_PDF_ALIGN_TOP_CENTER: "Top Center",
EXPORTDIALOG_PDF_ALIGN_TOP_RIGHT: "Top Right",
EXPORTDIALOG_PDF_ALIGN_BOTTOM_LEFT: "Bottom Left",
EXPORTDIALOG_PDF_ALIGN_BOTTOM_CENTER: "Bottom Center",
EXPORTDIALOG_PDF_ALIGN_BOTTOM_RIGHT: "Bottom Right",
EXPORTDIALOG_PDF_MARGIN: "Margin",
EXPORTDIALOG_PDF_MARGIN_NONE: "None",
EXPORTDIALOG_PDF_MARGIN_TINY: "Small",
EXPORTDIALOG_PDF_MARGIN_NORMAL: "Normal",
// Buttons
EXPORTDIALOG_PNGTOFILE : "PNT to File",
EXPORTDIALOG_SVGTOFILE : "SVG to File",
EXPORTDIALOG_EXCALIDRAW: "Excalidraw",
EXPORTDIALOG_PNGTOCLIPBOARD : "PNG to Clipboard",
EXPORTDIALOG_PDF: "Export PDF",
EXPORTDIALOG_PDFTOVAULT: "Save PDF to Vault",
EXPORTDIALOG_PDF: "Export",
EXPORTDIALOG_PDFTOVAULT: "Save to Vault",
};

View File

@@ -6,9 +6,11 @@ import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import ExcalidrawView from "src/view/ExcalidrawView";
import ExcalidrawPlugin from "src/core/main";
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/utils";
import { PageOrientation, PageSize, STANDARD_PAGE_SIZES } from "src/utils/exportUtils";
import { PageOrientation, PageSize, PDFMargin, PDFPageAlignment, PDFPageMarginString, STANDARD_PAGE_SIZES } from "src/utils/exportUtils";
import { t } from "src/lang/helpers";
export class ExportDialog extends Modal {
private ea: ExcalidrawAutomate;
private api: ExcalidrawImperativeAPI;
@@ -34,6 +36,11 @@ export class ExportDialog extends Modal {
private activeTab: "image" | "pdf" = "image";
private contentContainer: HTMLDivElement;
private buttonContainer: HTMLDivElement;
public fitToPage: boolean = true;
public paperColor: "white" | "scene" | "custom" = "white";
public customPaperColor: string = "#ffffff";
public alignment: PDFPageAlignment = "center";
public margin: PDFPageMarginString = "normal";
constructor(
private plugin: ExcalidrawPlugin,
@@ -296,6 +303,82 @@ export class ExportDialog extends Modal {
this.pageOrientation = value as PageOrientation;
})
);
new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_PDF_FIT_TO_PAGE"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"fit": t("EXPORTDIALOG_PDF_FIT_OPTION"),
"scale": t("EXPORTDIALOG_PDF_SCALE_OPTION")
})
.setValue(this.fitToPage ? "fit" : "scale")
.onChange(value => {
this.fitToPage = value === "fit";
})
);
new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_PDF_MARGIN"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"none": t("EXPORTDIALOG_PDF_MARGIN_NONE"),
"tiny": t("EXPORTDIALOG_PDF_MARGIN_TINY"),
"normal": t("EXPORTDIALOG_PDF_MARGIN_NORMAL")
})
.setValue(this.margin)
.onChange(value => {
this.margin = value as typeof this.margin;
})
);
const paperColorSetting = new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_PDF_PAPER_COLOR"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"white": t("EXPORTDIALOG_PDF_PAPER_WHITE"),
"scene": t("EXPORTDIALOG_PDF_PAPER_SCENE"),
"custom": t("EXPORTDIALOG_PDF_PAPER_CUSTOM")
})
.setValue(this.paperColor)
.onChange(value => {
this.paperColor = value as typeof this.paperColor;
colorInput.style.display = (value === "custom") ? "block" : "none";
})
);
const colorInput = paperColorSetting.controlEl.createEl("input", {
type: "color",
value: this.customPaperColor
});
colorInput.style.width = "50px";
colorInput.style.marginLeft = "10px";
colorInput.style.display = this.paperColor === "custom" ? "block" : "none";
colorInput.addEventListener("change", (e) => {
this.customPaperColor = (e.target as HTMLInputElement).value;
});
new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_PDF_ALIGNMENT"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"center": t("EXPORTDIALOG_PDF_ALIGN_CENTER"),
"top-left": t("EXPORTDIALOG_PDF_ALIGN_TOP_LEFT"),
"top-center": t("EXPORTDIALOG_PDF_ALIGN_TOP_CENTER"),
"top-right": t("EXPORTDIALOG_PDF_ALIGN_TOP_RIGHT"),
"bottom-left": t("EXPORTDIALOG_PDF_ALIGN_BOTTOM_LEFT"),
"bottom-center": t("EXPORTDIALOG_PDF_ALIGN_BOTTOM_CENTER"),
"bottom-right": t("EXPORTDIALOG_PDF_ALIGN_BOTTOM_RIGHT")
})
.setValue(this.alignment)
.onChange(value => {
this.alignment = value as typeof this.alignment;
})
);
}
private createImageButtons() {
@@ -328,13 +411,28 @@ export class ExportDialog extends Modal {
}
private createPDFButton() {
const bPDFVault = this.buttonContainer.createEl("button", {
text: t("EXPORTDIALOG_PDFTOVAULT"),
cls: "excalidraw-prompt-button"
});
bPDFVault.onclick = () => {
this.view.exportPDF(
true,
this.hasSelectedElements && this.exportSelectedOnly,
this.pageSize,
this.pageOrientation
);
this.close();
};
if (!DEVICE.isDesktop) return;
const bPDF = this.buttonContainer.createEl("button", {
const bPDFExport = this.buttonContainer.createEl("button", {
text: t("EXPORTDIALOG_PDF"),
cls: "excalidraw-prompt-button"
});
bPDF.onclick = () => {
bPDFExport.onclick = () => {
this.view.exportPDF(
false,
this.hasSelectedElements && this.exportSelectedOnly,
this.pageSize,
this.pageOrientation
@@ -342,4 +440,13 @@ export class ExportDialog extends Modal {
this.close();
};
}
public getPaperColor(): string {
switch (this.paperColor) {
case "white": return "#ffffff";
case "scene": return this.api.getAppState().viewBackgroundColor;
case "custom": return this.customPaperColor;
default: return "#ffffff";
}
}
}

View File

@@ -1,21 +1,27 @@
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
import { getEA } from 'src/core';
import { svgToBase64 } from './utils';
export type PDFPageAlignment = "center" | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
export type PDFPageMarginString = "none" | "tiny" | "normal";
interface PDFExportScale {
fitToPage: boolean;
zoom?: number;
}
export interface PDFMargin {
left: number;
right: number;
top: number;
bottom: number;
}
interface PDFPageProperties {
dimensions?: {width: number; height: number};
backgroundColor?: string;
margin?: {
left: number;
right: number;
top: number;
bottom: number;
};
margin: PDFMargin;
alignment: PDFPageAlignment;
}
interface PageDimensions {
@@ -40,6 +46,15 @@ export const STANDARD_PAGE_SIZES = {
export type PageSize = keyof typeof STANDARD_PAGE_SIZES;
export function getMarginValue(margin:PDFPageMarginString): PDFMargin {
switch(margin) {
case "none": return { left: 0, right: 0, top: 0, bottom: 0 };
case "tiny": return { left: 5, right: 5, top: 5, bottom: 5 };
case "normal": return { left: 20, right: 20, top: 20, bottom: 20 };
default: return { left: 20, right: 20, top: 20, bottom: 20 };
}
}
export function getPageDimensions(pageSize: PageSize, orientation: PageOrientation): PageDimensions {
const dimensions = STANDARD_PAGE_SIZES[pageSize];
return orientation === "portrait"
@@ -47,8 +62,6 @@ export function getPageDimensions(pageSize: PageSize, orientation: PageOrientati
: { width: dimensions.height, height: dimensions.width };
}
const DEFAULT_MARGIN = 20; // 20pt margins
interface SVGDimensions {
width: number;
height: number;
@@ -117,20 +130,15 @@ async function addSVGToPage(
export async function exportToPDF({
SVG,
scale = { fitToPage: true, zoom: 1 },
pageProps = {}
pageProps,
}: {
SVG: SVGSVGElement[];
scale?: PDFExportScale;
pageProps?: PDFPageProperties;
scale: PDFExportScale;
pageProps: PDFPageProperties;
}): Promise<ArrayBuffer> {
const margin = pageProps.margin ?? {
left: DEFAULT_MARGIN,
right: DEFAULT_MARGIN,
top: DEFAULT_MARGIN,
bottom: DEFAULT_MARGIN
};
const pageDim = pageProps.dimensions ?? getPageDimensions("A4", "portrait");
const margin = pageProps.margin;
const pageDim = pageProps.dimensions;
const pdfDoc = await PDFDocument.create();
for (const svg of SVG) {

View File

@@ -493,3 +493,22 @@ export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime:
const fileList = getExcalidrawEmbeddedFilesFiletree(sourceFile, plugin);
return fileList.some(f=>f.stat.mtime > mtime);
}
export async function createOrOverwriteFile(app: App, path: string, content: string | ArrayBuffer): Promise<TFile> {
const file = app.vault.getAbstractFileByPath(normalizePath(path));
if(content instanceof ArrayBuffer) {
if(file && file instanceof TFile) {
await app.vault.modifyBinary(file, content);
return file;
} else {
return await app.vault.createBinary(path, content);
}
}
if (file && file instanceof TFile) {
await app.vault.modify(file, content);
return file;
} else {
return await app.vault.create(path, content);
}
}

View File

@@ -78,6 +78,7 @@ import {
import {
arrayBufferToBase64,
checkAndCreateFolder,
createOrOverwriteFile,
download,
getDataURLFromURL,
getIMGFilename,
@@ -150,7 +151,8 @@ import { getPDFCropRect } from "../utils/PDFUtils";
import { Position, ViewSemaphores } from "../types/excalidrawViewTypes";
import { DropManager } from "./managers/DropManager";
import { ImageInfo } from "src/types/excalidrawAutomateTypes";
import { exportToPDF, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils";
import { exportToPDF, getMarginValue, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils";
import { create } from "domain";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -517,19 +519,13 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
const exportImage = async (filepath:string, theme?:string) => {
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const svg = await this.svg(scene,theme, embedScene, true);
if (!svg) {
return;
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026
const svgString = svg.outerHTML;
if (file && file instanceof TFile) {
await this.app.vault.modify(file, svgString);
} else {
await this.app.vault.create(filepath, svgString);
}
await createOrOverwriteFile(this.app, filepath, svgString);
}
if(this.plugin.settings.autoExportLightAndDark) {
@@ -558,6 +554,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
public async exportPDF(
toVault: boolean,
selectedOnly?: boolean,
pageSize: PageSize = "A4",
orientation: PageOrientation = "portrait"
@@ -579,16 +576,16 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const pdfArrayBuffer = await exportToPDF({
SVG: [svg],
scale: { fitToPage: true },
scale: {
...this.exportDialog.fitToPage
? { fitToPage: true }
: { zoom: this.exportDialog.scale, fitToPage: false },
},
pageProps: {
dimensions: getPageDimensions(pageSize, orientation),
backgroundColor: this.exportDialog.transparent ? undefined : "#ffffff",
margin: {
top: 20,
left: 20,
right: 20,
bottom: 20
}
backgroundColor: this.exportDialog.getPaperColor(),
margin: getMarginValue(this.exportDialog.margin),
alignment: this.exportDialog.alignment,
}
});
@@ -596,11 +593,17 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
return;
}
download(
"data:application/pdf;base64",
arrayBufferToBase64(pdfArrayBuffer),
`${this.file.basename}.pdf`
);
if(toVault) {
const filepath = getIMGFilename(this.file.path, "pdf");
const file = await createOrOverwriteFile(this.app, filepath, pdfArrayBuffer);
this.app.workspace.getLeaf("split").openFile(file);
} else {
download(
"data:application/pdf;base64",
arrayBufferToBase64(pdfArrayBuffer),
`${this.file.basename}.pdf`
);
}
}
public async png(scene: any, theme?:string, embedScene?: boolean): Promise<Blob> {
@@ -646,17 +649,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
const exportImage = async (filepath:string, theme?:string) => {
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const png = await this.png(scene, theme, embedScene);
if (!png) {
return;
}
if (file && file instanceof TFile) {
await this.app.vault.modifyBinary(file, await png.arrayBuffer());
} else {
await this.app.vault.createBinary(filepath, await png.arrayBuffer());
}
await createOrOverwriteFile(this.app, filepath, await png.arrayBuffer());
}
if(this.plugin.settings.autoExportLightAndDark) {