mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-29 11:02:28 +08:00
feat(mermaid): enable export as PNG (closes #536)
This commit is contained in:
parent
047c4dc4ca
commit
7cc8dd082d
@ -343,9 +343,8 @@ type EventMappings = {
|
||||
noteContextRemoved: {
|
||||
ntxIds: string[];
|
||||
};
|
||||
exportSvg: {
|
||||
ntxId: string | null | undefined;
|
||||
};
|
||||
exportSvg: { ntxId: string | null | undefined; };
|
||||
exportPng: { ntxId: string | null | undefined; };
|
||||
geoMapCreateChildNote: {
|
||||
ntxId: string | null | undefined; // TODO: deduplicate ntxId
|
||||
};
|
||||
|
@ -91,6 +91,7 @@ import type { AppContext } from "./../components/app_context.js";
|
||||
import type { WidgetsByParent } from "../services/bundle.js";
|
||||
import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js";
|
||||
import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js";
|
||||
import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@ -214,6 +215,7 @@ export default class DesktopLayout {
|
||||
.child(new GeoMapButtons())
|
||||
.child(new CopyImageReferenceButton())
|
||||
.child(new SvgExportButton())
|
||||
.child(new PngExportButton())
|
||||
.child(new BacklinksWidget())
|
||||
.child(new ContextualHelpButton())
|
||||
.child(new HideFloatingButtonsButton())
|
||||
|
@ -609,9 +609,20 @@ function createImageSrcUrl(note: { noteId: string; title: string }) {
|
||||
*/
|
||||
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
||||
const filename = `${nameWithoutExtension}.svg`;
|
||||
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
||||
triggerDownload(filename, dataUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the given data URL on the client device, with a custom file name.
|
||||
*
|
||||
* @param fileName the name to give the downloaded file.
|
||||
* @param dataUrl the data URI to download.
|
||||
*/
|
||||
function triggerDownload(fileName: string, dataUrl: string) {
|
||||
const element = document.createElement("a");
|
||||
element.setAttribute("href", `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`);
|
||||
element.setAttribute("download", filename);
|
||||
element.setAttribute("href", dataUrl);
|
||||
element.setAttribute("download", fileName);
|
||||
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
@ -621,6 +632,56 @@ function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string representation of an SVG, renders the SVG to PNG and triggers a download of the file on the client device.
|
||||
*
|
||||
* Note that the SVG must specify its width and height as attributes in order for it to be rendered.
|
||||
*
|
||||
* @param nameWithoutExtension the name of the file. The .png suffix is automatically added to it.
|
||||
* @param svgContent the content of the SVG file download.
|
||||
* @returns `true` if the operation succeeded (width/height present), or `false` if the download was not triggered.
|
||||
*/
|
||||
function downloadSvgAsPng(nameWithoutExtension: string, svgContent: string) {
|
||||
const mime = "image/svg+xml";
|
||||
|
||||
// First, we need to determine the width and the height from the input SVG.
|
||||
const svgDocument = (new DOMParser()).parseFromString(svgContent, mime);
|
||||
const width = svgDocument.documentElement?.getAttribute("width");
|
||||
const height = svgDocument.documentElement?.getAttribute("height");
|
||||
|
||||
if (!width || !height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert the image to a blob.
|
||||
const svgBlob = new Blob([ svgContent ], {
|
||||
type: mime
|
||||
})
|
||||
|
||||
// Create an image element and load the SVG.
|
||||
const imageEl = new Image();
|
||||
imageEl.width = parseFloat(width);
|
||||
imageEl.height = parseFloat(height);
|
||||
imageEl.src = URL.createObjectURL(svgBlob);
|
||||
imageEl.onload = () => {
|
||||
// Draw the image with a canvas.
|
||||
const canvasEl = document.createElement("canvas");
|
||||
canvasEl.width = imageEl.width;
|
||||
canvasEl.height = imageEl.height;
|
||||
document.body.appendChild(canvasEl);
|
||||
|
||||
const ctx = canvasEl.getContext("2d");
|
||||
ctx?.drawImage(imageEl, 0, 0);
|
||||
URL.revokeObjectURL(imageEl.src);
|
||||
|
||||
const imgUri = canvasEl.toDataURL("image/png")
|
||||
triggerDownload(`${nameWithoutExtension}.png`, imgUri);
|
||||
document.body.removeChild(canvasEl);
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two semantic version strings.
|
||||
* Returns:
|
||||
@ -719,6 +780,7 @@ export default {
|
||||
copyHtmlToClipboard,
|
||||
createImageSrcUrl,
|
||||
downloadSvg,
|
||||
downloadSvgAsPng,
|
||||
compareVersions,
|
||||
isUpdateAvailable,
|
||||
isLaunchBarConfig
|
||||
|
24
src/public/app/widgets/floating_buttons/png_export_button.ts
Normal file
24
src/public/app/widgets/floating_buttons/png_export_button.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<button type="button"
|
||||
class="export-svg-button"
|
||||
title="${t("png_export_button.button_title")}">
|
||||
<span class="bx bxs-file-png"></span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
export default class PngExportButton extends NoteContextAwareWidget {
|
||||
isEnabled() {
|
||||
return super.isEnabled() && ["mermaid"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.on("click", () => this.triggerEvent("exportPng", { ntxId: this.ntxId }));
|
||||
this.contentSized();
|
||||
}
|
||||
}
|
@ -217,4 +217,12 @@ export default abstract class AbstractSvgSplitTypeWidget extends AbstractSplitTy
|
||||
utils.downloadSvg(this.note.title, this.svg);
|
||||
}
|
||||
|
||||
async exportPngEvent({ ntxId }: EventData<"exportPng">) {
|
||||
if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid" || !this.svg) {
|
||||
return;
|
||||
}
|
||||
|
||||
utils.downloadSvgAsPng(this.note.title, this.svg);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1708,5 +1708,8 @@
|
||||
"toggle_read_only_button": {
|
||||
"unlock-editing": "Unlock editing",
|
||||
"lock-editing": "Lock editing"
|
||||
},
|
||||
"png_export_button": {
|
||||
"button_title": "Export diagram as PNG"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user