mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a796621f93 | |||
| 3c943c6685 | |||
| 4209774b4e | |||
| b18637f7d0 | |||
| af8a848d14 | |||
| 01e392158d |
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.8.0-beta-1",
|
||||
"version": "2.8.0-beta-3",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -147,6 +147,7 @@ const BUILD_CONFIG = {
|
||||
entryFileNames: 'main.js',
|
||||
format: 'cjs',
|
||||
exports: 'default',
|
||||
inlineDynamicImports: true, // Add this line only
|
||||
},
|
||||
plugins: getRollupPlugins(
|
||||
{
|
||||
|
||||
@@ -801,15 +801,16 @@ const tmpObsidianWYSIWYG = async (
|
||||
if(Boolean(ctx.frontmatter)) {
|
||||
el.empty();
|
||||
} else {
|
||||
const warningEl = el.querySelector("div>h3[data-heading^='Unable to find section #^");
|
||||
//Obsidian changed this at some point from h3 to h5 and also the text...
|
||||
const warningEl = el.querySelector("div>*[data-heading^='Unable to find ");
|
||||
if(warningEl) {
|
||||
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=|clippedframe=)[^ ]*)/)?.[1];
|
||||
const dataHeading = warningEl.getAttr("data-heading");
|
||||
const ref = warningEl.getAttr("data-heading").match(/Unable to find[^^]+(\^(?:group=|area=|frame=|clippedframe=)[^ ”]+)/)?.[1];
|
||||
if(ref) {
|
||||
attr.fname = file.path + ref;
|
||||
attr.fname = file.path + "#" +ref;
|
||||
areaPreview = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if(!isFrontmatterDiv && !areaPreview) {
|
||||
if(el.parentElement === containerEl) containerEl.removeChild(el);
|
||||
|
||||
@@ -502,11 +502,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
pdfSettings: {
|
||||
pageSize: "A4",
|
||||
pageOrientation: "portrait",
|
||||
fitToPage: true,
|
||||
fitToPage: 1,
|
||||
paperColor: "white",
|
||||
customPaperColor: "#ffffff",
|
||||
alignment: "center",
|
||||
margin: "normal"
|
||||
margin: "normal",
|
||||
exportDPI: 300,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2508,6 +2509,9 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.requestReloadDrawings = true;
|
||||
this.plugin.settings.experimentalEnableFourthFont = value;
|
||||
this.applySettingsUpdate();
|
||||
if(value) {
|
||||
this.plugin.initializeFonts();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1048,8 +1048,15 @@ 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",
|
||||
EXPORTDIALOG_PDF_FIT_4_OPTION: "Fit to 4-pages",
|
||||
EXPORTDIALOG_PDF_FIT_6_OPTION: "Fit to 6-pages",
|
||||
EXPORTDIALOG_PDF_FIT_8_OPTION: "Fit to 8-pages",
|
||||
EXPORTDIALOG_PDF_FIT_12_OPTION: "Fit to 12-pages",
|
||||
EXPORTDIALOG_PDF_FIT_16_OPTION: "Fit to 16-pages",
|
||||
EXPORTDIALOG_PDF_SCALE_OPTION: "Use image scale (may span multiple pages)",
|
||||
EXPORTDIALOG_PDF_PAPER_COLOR: "Paper Color",
|
||||
EXPORTDIALOG_PDF_PAPER_WHITE: "White",
|
||||
@@ -1079,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",
|
||||
};
|
||||
|
||||
@@ -386,13 +386,14 @@ FILENAME_HEAD: "文件名",
|
||||
"此设置不会影响您在 Excalidraw 模式下的绘图显示,或者在将绘图嵌入 Markdown 文档时,或在渲染悬停预览时。<br><ul>" +
|
||||
"<li>请参阅下面‘嵌入和导出’部分的 <a href='#"+TAG_PDFEXPORT+"'>PDF 导出</a> 相关设置。</li></ul><br>" +
|
||||
"您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。",
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "在将 Excalidraw 文件导出为 PDF 时将文件渲染为图像",
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME : "在 Obsidian 中导出为 PDF 格式时将 Excalidraw 渲染为图像" ,
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
|
||||
"处于 Markdown 视图模式时,此设置控制 Excalidraw 在使用 Obsidian 的 <b>导出为 PDF</b> 功能时,将 Excalidraw 文件导出为 PDF 的行为。<br>" +
|
||||
"<ul><li>当 <b>启用</b> 时,PDF 将仅显示 Excalidraw 绘图;</li>" +
|
||||
"<li>当 <b>禁用</b> 时,PDF 将显示文档的 Markdown 部分(背景笔记)。</li></ul>" +
|
||||
"请参阅上面‘外观和行为’部分的 <<a href='#"+TAG_MDREADINGMODE+"'>>Markdown 阅读模式</a> 相关设置。" +
|
||||
"⚠️ 注意,您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。⚠️",
|
||||
"此设置控制在使用 Obsidian 内置的<b>导出为 PDF</b>功能,如何将 Excalidraw 文件导出为 PDF。<br>" +
|
||||
"<ul><li><b>启用:</b>PDF 将包含图像格式的 Excalidraw 绘图。</li>" +
|
||||
"<li><b>禁用:</b>PDF 将包含作为文本的 Markdown 内容。</li></ul>" +
|
||||
"注意:此设置不会影响 Excalidraw 本身的 PDF 导出功能。<br>" +
|
||||
"请参阅上方“外观和行为”部分中与<a href='#" + TAG_MDREADINGMODE + "'>Markdown 阅读模式</a>相关的其他设置。<br>" +
|
||||
"⚠️ 您必须关闭并重新打开 Excalidraw/Markdown 文件,设置更改才会生效。⚠️",
|
||||
HOTKEY_OVERRIDE_HEAD: "热键覆盖",
|
||||
HOTKEY_OVERRIDE_DESC: `一些 Excalidraw 的热键,例如 ${labelCTRL()}+Enter 用于编辑文本,或 ${labelCTRL()}+K 用于创建元素链接。` +
|
||||
"与 Obsidian 的热键设置发生冲突。您在下面添加的热键组合将在使用 Excalidraw 时覆盖 Obsidian 的热键设置," +
|
||||
@@ -661,6 +662,7 @@ FILENAME_HEAD: "文件名",
|
||||
EXPORT_EMBED_SCENE_DESC:
|
||||
"在导出的图像中嵌入 Excalidraw 场景。可以通过在文件级别添加 <code>excalidraw-export-embed-scene: true/false</code> frontmatter 元数据键来覆盖此设置。" +
|
||||
"此设置仅在您下次(重新)打开绘图时生效。",
|
||||
PDF_EXPORT_SETTINGS : "PDF 导出设置",
|
||||
EXPORT_HEAD: "导出设置",
|
||||
EXPORT_SYNC_NAME:
|
||||
"保持 SVG/PNG 文件名与绘图文件同步",
|
||||
@@ -1006,4 +1008,75 @@ FILENAME_HEAD: "文件名",
|
||||
LINK_CLICK_POPOUT : "在弹出窗口中打开" ,
|
||||
LINK_CLICK_NEW_TAB : "在新标签页中打开" ,
|
||||
LINK_CLICK_MD_PROPS : "显示 Markdown 图片属性对话框(仅在嵌入 Markdown 文档为图片时适用)" ,
|
||||
|
||||
// 导出对话框
|
||||
// 对话框和标签页
|
||||
EXPORTDIALOG_TITLE : "导出图形",
|
||||
EXPORTDIALOG_TAB_IMAGE : "图像",
|
||||
EXPORTDIALOG_TAB_PDF : "PDF",
|
||||
// 设置持久化
|
||||
EXPORTDIALOG_SAVE_SETTINGS : "将图像设置保存到文件 doc.properties 吗?",
|
||||
EXPORTDIALOG_SAVE_SETTINGS_SAVE : "保存为预设",
|
||||
EXPORTDIALOG_SAVE_SETTINGS_ONETIME : "仅本次使用",
|
||||
// 图像设置
|
||||
EXPORTDIALOG_IMAGE_SETTINGS : "图像",
|
||||
EXPORTDIALOG_IMAGE_DESC : "PNG 支持透明。外部文件可以包含 Excalidraw 场景数据。",
|
||||
EXPORTDIALOG_PADDING : "边距",
|
||||
EXPORTDIALOG_SCALE : "缩放",
|
||||
EXPORTDIALOG_CURRENT_PADDING : "当前边距:",
|
||||
EXPORTDIALOG_SIZE_DESC : "缩放会影响输出大小",
|
||||
EXPORTDIALOG_SCALE_VALUE : "缩放:",
|
||||
EXPORTDIALOG_IMAGE_SIZE : "大小:",
|
||||
// 主题和背景
|
||||
EXPORTDIALOG_EXPORT_THEME : "主题",
|
||||
EXPORTDIALOG_THEME_LIGHT : "浅色",
|
||||
EXPORTDIALOG_THEME_DARK : "深色",
|
||||
EXPORTDIALOG_BACKGROUND : "背景",
|
||||
EXPORTDIALOG_BACKGROUND_TRANSPARENT : "透明",
|
||||
EXPORTDIALOG_BACKGROUND_USE_COLOR : "使用场景颜色",
|
||||
// 选择
|
||||
EXPORTDIALOG_SELECTED_ELEMENTS : "导出",
|
||||
EXPORTDIALOG_SELECTED_ALL : "整个场景",
|
||||
EXPORTDIALOG_SELECTED_SELECTED : "仅选中部分",
|
||||
// 导出选项
|
||||
EXPORTDIALOG_EMBED_SCENE : "包含场景数据吗?",
|
||||
EXPORTDIALOG_EMBED_YES : "是",
|
||||
EXPORTDIALOG_EMBED_NO : "否",
|
||||
// PDF 设置
|
||||
EXPORTDIALOG_PDF_SETTINGS : "PDF",
|
||||
EXPORTDIALOG_PAGE_SIZE : "页面大小",
|
||||
EXPORTDIALOG_PAGE_ORIENTATION : "方向",
|
||||
EXPORTDIALOG_ORIENTATION_PORTRAIT : "纵向",
|
||||
EXPORTDIALOG_ORIENTATION_LANDSCAPE : "横向",
|
||||
EXPORTDIALOG_PDF_FIT_TO_PAGE : "页面适配",
|
||||
EXPORTDIALOG_PDF_FIT_OPTION : "适配页面",
|
||||
EXPORTDIALOG_PDF_SCALE_OPTION : "使用图像缩放(可能跨多页)",
|
||||
EXPORTDIALOG_PDF_PAPER_COLOR : "纸张颜色",
|
||||
EXPORTDIALOG_PDF_PAPER_WHITE : "白色",
|
||||
EXPORTDIALOG_PDF_PAPER_SCENE : "使用场景颜色",
|
||||
EXPORTDIALOG_PDF_PAPER_CUSTOM : "自定义颜色",
|
||||
EXPORTDIALOG_PDF_ALIGNMENT : "页面位置",
|
||||
EXPORTDIALOG_PDF_ALIGN_CENTER : "居中",
|
||||
EXPORTDIALOG_PDF_ALIGN_TOP_LEFT : "左上角",
|
||||
EXPORTDIALOG_PDF_ALIGN_TOP_CENTER : "顶部居中",
|
||||
EXPORTDIALOG_PDF_ALIGN_TOP_RIGHT : "右上角",
|
||||
EXPORTDIALOG_PDF_ALIGN_BOTTOM_LEFT : "左下角",
|
||||
EXPORTDIALOG_PDF_ALIGN_BOTTOM_CENTER : "底部居中",
|
||||
EXPORTDIALOG_PDF_ALIGN_BOTTOM_RIGHT : "右下角",
|
||||
EXPORTDIALOG_PDF_MARGIN : "边距",
|
||||
EXPORTDIALOG_PDF_MARGIN_NONE : "无",
|
||||
EXPORTDIALOG_PDF_MARGIN_TINY : "小",
|
||||
EXPORTDIALOG_PDF_MARGIN_NORMAL : "正常",
|
||||
EXPORTDIALOG_SAVE_PDF_SETTINGS : "保存 PDF 设置",
|
||||
EXPORTDIALOG_SAVE_CONFIRMATION : "PDF 配置已保存为插件默认设置",
|
||||
// 按钮
|
||||
EXPORTDIALOG_PNGTOFILE : "导出 PNG 文件",
|
||||
EXPORTDIALOG_SVGTOFILE : "导出 SVG 文件",
|
||||
EXPORTDIALOG_PNGTOVAULT : "PNG 保存到 Vault",
|
||||
EXPORTDIALOG_SVGTOVAULT : "SVG 保存到 Vault",
|
||||
EXPORTDIALOG_EXCALIDRAW : "Excalidraw",
|
||||
EXPORTDIALOG_PNGTOCLIPBOARD : "PNG 复制到剪贴板",
|
||||
EXPORTDIALOG_SVGTOCLIPBOARD : "SVG 复制到剪贴板",
|
||||
EXPORTDIALOG_PDF : "导出 PDF 文件",
|
||||
EXPORTDIALOG_PDFTOVAULT : "PDF 保存到 Vault",
|
||||
};
|
||||
@@ -38,11 +38,12 @@ export class ExportDialog extends Modal {
|
||||
private contentContainer: HTMLDivElement;
|
||||
private buttonContainerRow1: HTMLDivElement;
|
||||
private buttonContainerRow2: HTMLDivElement;
|
||||
public fitToPage: boolean = true;
|
||||
public fitToPage: number = 1;
|
||||
public paperColor: "white" | "scene" | "custom" = "white";
|
||||
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"));
|
||||
|
||||
@@ -5,11 +5,12 @@ import { t } from "src/lang/helpers";
|
||||
export interface PDFExportSettings {
|
||||
pageSize: PageSize;
|
||||
pageOrientation: PageOrientation;
|
||||
fitToPage: boolean;
|
||||
fitToPage: number;
|
||||
paperColor: "white" | "scene" | "custom";
|
||||
customPaperColor: string;
|
||||
alignment: PDFPageAlignment;
|
||||
margin: PDFPageMarginString;
|
||||
exportDPI: number;
|
||||
}
|
||||
|
||||
export class PDFExportSettingsComponent {
|
||||
@@ -55,17 +56,42 @@ 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 =>
|
||||
dropdown
|
||||
.addOptions({
|
||||
"scale": t("EXPORTDIALOG_PDF_SCALE_OPTION"),
|
||||
"fit": t("EXPORTDIALOG_PDF_FIT_OPTION"),
|
||||
"scale": t("EXPORTDIALOG_PDF_SCALE_OPTION")
|
||||
"fit-2": t("EXPORTDIALOG_PDF_FIT_2_OPTION"),
|
||||
"fit-4": t("EXPORTDIALOG_PDF_FIT_4_OPTION"),
|
||||
"fit-6": t("EXPORTDIALOG_PDF_FIT_6_OPTION"),
|
||||
"fit-8": t("EXPORTDIALOG_PDF_FIT_8_OPTION"),
|
||||
"fit-12": t("EXPORTDIALOG_PDF_FIT_12_OPTION"),
|
||||
"fit-16": t("EXPORTDIALOG_PDF_FIT_16_OPTION")
|
||||
})
|
||||
.setValue(this.settings.fitToPage ? "fit" : "scale")
|
||||
.setValue(this.settings.fitToPage === 1 ? "fit" :
|
||||
(typeof this.settings.fitToPage === "number" ? `fit-${this.settings.fitToPage}` : "scale"))
|
||||
.onChange(value => {
|
||||
this.settings.fitToPage = value === "fit";
|
||||
this.settings.fitToPage = value === "scale" ? 0 :
|
||||
(value === "fit" ? 1 : parseInt(value.split("-")[1]));
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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" +
|
||||
"});",
|
||||
},
|
||||
|
||||
@@ -955,25 +955,26 @@ export class ExcalidrawAutomate {
|
||||
*
|
||||
* @param {Object} params - The parameters for creating the PDF.
|
||||
* @param {SVGSVGElement[]} params.SVG - An array of SVG elements to be included in the PDF.
|
||||
* @param {PDFExportScale} [params.scale={ fitToPage: true, zoom: 1 }] - The scaling options for the SVG elements.
|
||||
* @param {PDFExportScale} [params.scale={ fitToPage: 1, zoom: 1 }] - The scaling options for the SVG elements.
|
||||
* @param {PDFPageProperties} [params.pageProps] - The properties for the PDF pages.
|
||||
* @returns {Promise<ArrayBuffer>} - A promise that resolves to an ArrayBuffer containing the PDF data.
|
||||
*
|
||||
* @example
|
||||
* const pdfData = await createToPDF({
|
||||
* SVG: [svgElement1, svgElement2],
|
||||
* scale: { fitToPage: true },
|
||||
* scale: { fitToPage: 1 },
|
||||
* pageProps: {
|
||||
* dimensions: { width: 595.28, height: 841.89 },
|
||||
* backgroundColor: "#ffffff",
|
||||
* margin: { left: 20, right: 20, top: 20, bottom: 20 },
|
||||
* alignment: "center"
|
||||
* alignment: "center",
|
||||
* exportDPI: 300,
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
async createPDF({
|
||||
SVG,
|
||||
scale = { fitToPage: true, zoom: 1 },
|
||||
scale = { fitToPage: 1, zoom: 1 },
|
||||
pageProps,
|
||||
}: {
|
||||
SVG: SVGSVGElement[];
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+174
-125
@@ -1,12 +1,25 @@
|
||||
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
|
||||
import { Notice } from 'obsidian';
|
||||
import { getEA } from 'src/core';
|
||||
import { t } from 'src/lang/helpers';
|
||||
|
||||
// Define proper types for PDFLib
|
||||
type PDFLibType = typeof import('@cantoo/pdf-lib');
|
||||
let pdfLibPromise: Promise<PDFLibType> | null = null;
|
||||
|
||||
async function getPDFLib(): Promise<PDFLibType> {
|
||||
if (!pdfLibPromise) {
|
||||
pdfLibPromise = import('@cantoo/pdf-lib');
|
||||
}
|
||||
return pdfLibPromise;
|
||||
}
|
||||
|
||||
const PDF_DPI = 72;
|
||||
|
||||
export type PDFPageAlignment = "center" | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
|
||||
export type PDFPageMarginString = "none" | "tiny" | "normal";
|
||||
|
||||
export interface PDFExportScale {
|
||||
fitToPage: boolean;
|
||||
fitToPage: number; // 0 means use zoom, >1 means fit to that many pages exactly
|
||||
zoom?: number;
|
||||
}
|
||||
|
||||
@@ -22,6 +35,7 @@ export interface PDFPageProperties {
|
||||
backgroundColor?: string;
|
||||
margin: PDFMargin;
|
||||
alignment: PDFPageAlignment;
|
||||
exportDPI: number;
|
||||
}
|
||||
|
||||
export interface PageDimensions {
|
||||
@@ -80,28 +94,10 @@ function calculatePosition(
|
||||
pageHeight: number,
|
||||
margin: PDFMargin,
|
||||
alignment: PDFPageAlignment,
|
||||
scale: PDFExportScale
|
||||
): {x: number, y: number} {
|
||||
const availableWidth = pageWidth - margin.left - margin.right;
|
||||
const availableHeight = pageHeight - margin.top - margin.bottom;
|
||||
|
||||
console.log(JSON.stringify({
|
||||
message: 'PDF Position Debug',
|
||||
input: {
|
||||
svgWidth,
|
||||
svgHeight,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
margin,
|
||||
alignment,
|
||||
scale
|
||||
},
|
||||
calculated: {
|
||||
availableWidth,
|
||||
availableHeight
|
||||
}
|
||||
}));
|
||||
|
||||
let x = margin.left;
|
||||
let y = margin.bottom;
|
||||
|
||||
@@ -121,52 +117,70 @@ function calculatePosition(
|
||||
y = pageHeight - margin.top - svgHeight;
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
message: 'PDF Position Intermediate',
|
||||
x,
|
||||
y,
|
||||
alignment,
|
||||
availableHeight,
|
||||
marginTop: margin.top,
|
||||
marginBottom: margin.bottom,
|
||||
svgHeight,
|
||||
pageHeight
|
||||
}));
|
||||
|
||||
console.log(JSON.stringify({
|
||||
message: 'PDF Position Result',
|
||||
x,
|
||||
y,
|
||||
finalPosition: {
|
||||
bottom: y,
|
||||
top: y + svgHeight,
|
||||
left: x,
|
||||
right: x + svgWidth
|
||||
}
|
||||
}));
|
||||
|
||||
return {x, y};
|
||||
}
|
||||
|
||||
function getNumberOfPages(
|
||||
width: number,
|
||||
height: number,
|
||||
availableWidth: number,
|
||||
availableHeight: number
|
||||
): number {
|
||||
const cols = Math.ceil(width / availableWidth);
|
||||
const rows = Math.ceil(height / availableHeight);
|
||||
return cols * rows;
|
||||
}
|
||||
|
||||
function calculateDimensions(
|
||||
svgWidth: number,
|
||||
svgHeight: number,
|
||||
pageDim: PageDimensions,
|
||||
margin: PDFPageProperties['margin'],
|
||||
scale: PDFExportScale,
|
||||
alignment: PDFPageAlignment
|
||||
alignment: PDFPageAlignment,
|
||||
exportDPI: number,
|
||||
): SVGDimensions[] {
|
||||
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;
|
||||
|
||||
let finalWidth: number;
|
||||
let finalHeight: number;
|
||||
// If fitToPage is specified, find optimal zoom using binary search
|
||||
if (scale.fitToPage > 0) {
|
||||
let low = 0;
|
||||
let high = 100; // Start with a reasonable upper bound
|
||||
let bestZoom = 1;
|
||||
const tolerance = 0.000001;
|
||||
|
||||
if (scale.fitToPage) {
|
||||
const ratio = Math.min(availableWidth / svgWidth, availableHeight / svgHeight);
|
||||
finalWidth = svgWidth * ratio;
|
||||
finalHeight = svgHeight * ratio;
|
||||
|
||||
while (high - low > tolerance) {
|
||||
const mid = (low + high) / 2;
|
||||
const scaledWidth = pdfWidth * mid;
|
||||
const scaledHeight = pdfHeight * mid;
|
||||
const pages = getNumberOfPages(scaledWidth, scaledHeight, availableWidth, availableHeight);
|
||||
|
||||
if (pages > scale.fitToPage) {
|
||||
high = mid;
|
||||
} else {
|
||||
bestZoom = mid;
|
||||
low = mid;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a small reduction to prevent floating-point issues
|
||||
scale.zoom = Math.round(bestZoom * 0.99999 * 1000000) / 1000000;
|
||||
}
|
||||
|
||||
// Now handle as regular scale mode
|
||||
const finalWidth = Math.round(pdfWidth * (scale.zoom || 1) * 1000) / 1000;
|
||||
const finalHeight = Math.round(pdfHeight * (scale.zoom || 1) * 1000) / 1000;
|
||||
|
||||
// Round the available dimensions as well for consistent comparison
|
||||
const roundedAvailableWidth = Math.round(availableWidth * 1000) / 1000;
|
||||
const roundedAvailableHeight = Math.round(availableHeight * 1000) / 1000;
|
||||
|
||||
if (finalWidth <= roundedAvailableWidth && finalHeight <= roundedAvailableHeight) {
|
||||
// Content fits on one page
|
||||
const position = calculatePosition(
|
||||
finalWidth,
|
||||
finalHeight,
|
||||
@@ -174,9 +188,8 @@ function calculateDimensions(
|
||||
pageDim.height,
|
||||
margin,
|
||||
alignment,
|
||||
scale
|
||||
);
|
||||
|
||||
|
||||
return [{
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
@@ -184,71 +197,52 @@ function calculateDimensions(
|
||||
y: position.y
|
||||
}];
|
||||
} else {
|
||||
// Scale mode - may need multiple pages
|
||||
finalWidth = svgWidth * (scale.zoom || 1);
|
||||
finalHeight = svgHeight * (scale.zoom || 1);
|
||||
// Content needs to be tiled across multiple pages
|
||||
const dimensions: SVGDimensions[] = [];
|
||||
// Calculate exact number of needed columns and rows
|
||||
const cols = Math.ceil(finalWidth / roundedAvailableWidth);
|
||||
const rows = Math.ceil(finalHeight / roundedAvailableHeight);
|
||||
|
||||
if (finalWidth <= availableWidth && finalHeight <= availableHeight) {
|
||||
// Content fits on one page
|
||||
const position = calculatePosition(
|
||||
finalWidth,
|
||||
finalHeight,
|
||||
pageDim.width,
|
||||
pageDim.height,
|
||||
margin,
|
||||
alignment,
|
||||
scale
|
||||
);
|
||||
|
||||
return [{
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
x: position.x,
|
||||
y: position.y
|
||||
}];
|
||||
} else {
|
||||
// Content needs to be tiled across multiple pages
|
||||
const dimensions: SVGDimensions[] = [];
|
||||
const cols = Math.ceil(finalWidth / availableWidth);
|
||||
const rows = Math.ceil(finalHeight / availableHeight);
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const tileWidth = Math.min(availableWidth, finalWidth - col * availableWidth);
|
||||
const tileHeight = Math.min(availableHeight, finalHeight - row * availableHeight);
|
||||
|
||||
// Calculate y coordinate following the same logic as single-page rendering
|
||||
// We start from the bottom margin and work our way up
|
||||
//const y = margin.bottom + row * availableHeight;
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
// Calculate remaining width and height for this tile
|
||||
const remainingWidth = finalWidth - col * roundedAvailableWidth;
|
||||
const remainingHeight = finalHeight - row * roundedAvailableHeight;
|
||||
|
||||
// Only create tile if there's actual content to show
|
||||
if (remainingWidth > 0 && remainingHeight > 0) {
|
||||
const tileWidth = Math.min(roundedAvailableWidth, remainingWidth);
|
||||
const tileHeight = Math.min(roundedAvailableHeight, remainingHeight);
|
||||
|
||||
dimensions.push({
|
||||
width: tileWidth,
|
||||
height: tileHeight,
|
||||
x: margin.left,
|
||||
y: margin.top,
|
||||
sourceX: col * availableWidth / (scale.zoom || 1),
|
||||
sourceY: row * availableHeight / (scale.zoom || 1),
|
||||
sourceWidth: tileWidth / (scale.zoom || 1),
|
||||
sourceHeight: tileHeight / (scale.zoom || 1)
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
return dimensions;
|
||||
}
|
||||
return dimensions;
|
||||
}
|
||||
}
|
||||
|
||||
async function addSVGToPage(
|
||||
pdfDoc: PDFDocument,
|
||||
pdfDoc: Awaited<ReturnType<typeof import('@cantoo/pdf-lib').PDFDocument.create>>,
|
||||
svg: SVGSVGElement,
|
||||
dimensions: SVGDimensions,
|
||||
pageDim: PageDimensions,
|
||||
backgroundColor?: string
|
||||
pageProps: PDFPageProperties
|
||||
) {
|
||||
const { rgb } = await getPDFLib();
|
||||
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,
|
||||
@@ -258,52 +252,90 @@ 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));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
console.log(JSON.stringify({message: "addSVGToPage", dimensions, html: svgToEmbed.outerHTML}));
|
||||
|
||||
// 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,
|
||||
height: dimensions.height,
|
||||
});
|
||||
|
||||
console.log(JSON.stringify({
|
||||
message: 'PDF Draw SVG',
|
||||
x: dimensions.x,
|
||||
y: adjustedY,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height
|
||||
}));
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
async function renderSVGToCanvas(
|
||||
svg: SVGSVGElement,
|
||||
dimensions: SVGDimensions,
|
||||
exportDPI: number = 300,
|
||||
): Promise<HTMLCanvasElement> {
|
||||
const canvas = document.createElement('canvas');
|
||||
const scale = exportDPI / PDF_DPI;
|
||||
|
||||
canvas.width = dimensions.width * scale;
|
||||
canvas.height = dimensions.height * scale;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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({
|
||||
SVG,
|
||||
scale = { fitToPage: true, zoom: 1 },
|
||||
scale = { fitToPage: 1, zoom: 1 },
|
||||
pageProps,
|
||||
}: {
|
||||
SVG: SVGSVGElement[];
|
||||
scale: PDFExportScale;
|
||||
pageProps: PDFPageProperties;
|
||||
}): Promise<ArrayBuffer> {
|
||||
const { PDFDocument } = await getPDFLib();
|
||||
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);
|
||||
//@ts-ignore
|
||||
let noticeContainerEl = notice.containerEl ?? notice.noticeEl;
|
||||
|
||||
let j=1;
|
||||
for (const svg of SVG) {
|
||||
const svgWidth = parseFloat(svg.getAttribute('width') || '0');
|
||||
const svgHeight = parseFloat(svg.getAttribute('height') || '0');
|
||||
@@ -314,14 +346,31 @@ export async function exportToPDF({
|
||||
pageProps.dimensions,
|
||||
pageProps.margin,
|
||||
scale,
|
||||
pageProps.alignment
|
||||
pageProps.alignment,
|
||||
pageProps.exportDPI
|
||||
);
|
||||
|
||||
for (const dim of dimensions) {
|
||||
await addSVGToPage(pdfDoc, svg, dim, pageProps.dimensions, pageProps.backgroundColor);
|
||||
let i=1;
|
||||
for (const dim of dimensions) {
|
||||
if(noticeContainerEl.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);
|
||||
//@ts-ignore
|
||||
noticeContainerEl = notice.containerEl ?? notice.noticeEl;
|
||||
}
|
||||
await addSVGToPage(pdfDoc, svg, dim, pageProps.dimensions, pageProps);
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
if(noticeContainerEl.parentElement) {
|
||||
notice.setMessage(t('EXPORTDIALOG_PDF_PROGRESS_DONE'));
|
||||
setTimeout(() => notice.hide(), 4000);
|
||||
} else {
|
||||
new Notice(t('EXPORTDIALOG_PDF_PROGRESS_DONE'));
|
||||
}
|
||||
return pdfDoc.save();
|
||||
}
|
||||
|
||||
|
||||
@@ -589,16 +589,16 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
|
||||
const pdfArrayBuffer = await exportToPDF({
|
||||
SVG: [svg],
|
||||
scale: {
|
||||
...this.exportDialog.fitToPage
|
||||
? { fitToPage: true }
|
||||
: { zoom: this.exportDialog.scale, fitToPage: false },
|
||||
scale: {
|
||||
zoom: this.exportDialog.scale,
|
||||
fitToPage: this.exportDialog.fitToPage
|
||||
},
|
||||
pageProps: {
|
||||
dimensions: getPageDimensions(pageSize, orientation),
|
||||
backgroundColor: this.exportDialog.getPaperColor(),
|
||||
margin: getMarginValue(this.exportDialog.margin),
|
||||
alignment: this.exportDialog.alignment,
|
||||
exportDPI: this.exportDialog.exportDPI,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1578,6 +1578,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
this.packages = this.plugin.getPackage(this.ownerWindow);
|
||||
|
||||
if(DEVICE.isDesktop && !apiMissing) {
|
||||
if(this.ownerWindow !== window) {
|
||||
this.plugin.initializeFonts();
|
||||
}
|
||||
this.destroyers.push(
|
||||
//this.containerEl.onWindowMigrated(this.leaf.rebuildView.bind(this))
|
||||
this.containerEl.onWindowMigrated(async() => {
|
||||
|
||||
+11
@@ -116,6 +116,7 @@ li[data-testid] {
|
||||
border: 0 !important;
|
||||
box-shadow: 0 !important;
|
||||
background-color: transparent !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.excalidraw .popover {
|
||||
@@ -336,6 +337,16 @@ label.color-input-container > input {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.excalidraw .App-toolbar-content .dropdown-menu {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.excalidraw .panelColumn {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.excalidraw .panelColumn .buttonList {
|
||||
max-width: 13rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user