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: { | ||||
|         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
	 Elian Doran
						Elian Doran