mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 13:01:31 +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: { |     noteContextRemoved: { | ||||||
|         ntxIds: string[]; |         ntxIds: string[]; | ||||||
|     }; |     }; | ||||||
|     exportSvg: { |     exportSvg: { ntxId: string | null | undefined; }; | ||||||
|         ntxId: string | null | undefined; |     exportPng: { ntxId: string | null | undefined; }; | ||||||
|     }; |  | ||||||
|     geoMapCreateChildNote: { |     geoMapCreateChildNote: { | ||||||
|         ntxId: string | null | undefined; // TODO: deduplicate ntxId
 |         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 type { WidgetsByParent } from "../services/bundle.js"; | ||||||
| import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js"; | import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js"; | ||||||
| import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_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 { | export default class DesktopLayout { | ||||||
| 
 | 
 | ||||||
| @ -214,6 +215,7 @@ export default class DesktopLayout { | |||||||
|                                                                 .child(new GeoMapButtons()) |                                                                 .child(new GeoMapButtons()) | ||||||
|                                                                 .child(new CopyImageReferenceButton()) |                                                                 .child(new CopyImageReferenceButton()) | ||||||
|                                                                 .child(new SvgExportButton()) |                                                                 .child(new SvgExportButton()) | ||||||
|  |                                                                 .child(new PngExportButton()) | ||||||
|                                                                 .child(new BacklinksWidget()) |                                                                 .child(new BacklinksWidget()) | ||||||
|                                                                 .child(new ContextualHelpButton()) |                                                                 .child(new ContextualHelpButton()) | ||||||
|                                                                 .child(new HideFloatingButtonsButton()) |                                                                 .child(new HideFloatingButtonsButton()) | ||||||
|  | |||||||
| @ -609,9 +609,20 @@ function createImageSrcUrl(note: { noteId: string; title: string }) { | |||||||
|  */ |  */ | ||||||
| function downloadSvg(nameWithoutExtension: string, svgContent: string) { | function downloadSvg(nameWithoutExtension: string, svgContent: string) { | ||||||
|     const filename = `${nameWithoutExtension}.svg`; |     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"); |     const element = document.createElement("a"); | ||||||
|     element.setAttribute("href", `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`); |     element.setAttribute("href", dataUrl); | ||||||
|     element.setAttribute("download", filename); |     element.setAttribute("download", fileName); | ||||||
| 
 | 
 | ||||||
|     element.style.display = "none"; |     element.style.display = "none"; | ||||||
|     document.body.appendChild(element); |     document.body.appendChild(element); | ||||||
| @ -621,6 +632,56 @@ function downloadSvg(nameWithoutExtension: string, svgContent: string) { | |||||||
|     document.body.removeChild(element); |     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. |  * Compares two semantic version strings. | ||||||
|  * Returns: |  * Returns: | ||||||
| @ -719,6 +780,7 @@ export default { | |||||||
|     copyHtmlToClipboard, |     copyHtmlToClipboard, | ||||||
|     createImageSrcUrl, |     createImageSrcUrl, | ||||||
|     downloadSvg, |     downloadSvg, | ||||||
|  |     downloadSvgAsPng, | ||||||
|     compareVersions, |     compareVersions, | ||||||
|     isUpdateAvailable, |     isUpdateAvailable, | ||||||
|     isLaunchBarConfig |     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); |         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": { |   "toggle_read_only_button": { | ||||||
|     "unlock-editing": "Unlock editing", |     "unlock-editing": "Unlock editing", | ||||||
|     "lock-editing": "Lock editing" |     "lock-editing": "Lock editing" | ||||||
|  |   }, | ||||||
|  |   "png_export_button": { | ||||||
|  |     "button_title": "Export diagram as PNG" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran