1- import { Command , CommandExecutionContext , CommandReturn , IVNodePostprocessor , ModelRenderer , TYPES , ViewRegistration , ViewRegistry } from "sprotty" ;
1+ import {
2+ Command ,
3+ CommandExecutionContext ,
4+ CommandReturn ,
5+ IVNodePostprocessor ,
6+ ModelRenderer ,
7+ TYPES ,
8+ ViewRegistry ,
9+ } from "sprotty" ;
210import themeCss from "../assets/theme.css?raw" ;
311import elementCss from "../diagram/style.css?raw" ;
4- import toHTML from "snabbdom-to-html"
12+ import toHTML from "snabbdom-to-html" ;
13+ import { classModule , eventListenersModule , h , init , propsModule , styleModule , VNode , VNodeStyle } from "snabbdom" ;
514import { Action } from "sprotty-protocol" ;
615import { inject , multiInject } from "inversify" ;
716import { FileName } from "../fileName/fileName" ;
817
18+ const patch = init ( [
19+ // Init patch function with chosen modules
20+ classModule , // makes it easy to toggle classes
21+ propsModule , // for setting properties on DOM elements
22+ styleModule , // handles styling on elements with support for animations
23+ eventListenersModule , // attaches event listeners
24+ ] ) ;
25+
926export namespace SaveImageAction {
1027 export const KIND = "save-image" ;
1128
@@ -18,41 +35,30 @@ export namespace SaveImageAction {
1835
1936export class SaveImageCommand extends Command {
2037 static readonly KIND = SaveImageAction . KIND ;
38+ private static readonly PADDING = 5 ;
2139
2240 constructor (
2341 @inject ( TYPES . Action ) _ : Action ,
2442 @inject ( FileName ) private readonly fileName : FileName ,
2543 @inject ( TYPES . ViewRegistry ) private readonly viewRegistry : ViewRegistry ,
26- @multiInject ( TYPES . IVNodePostprocessor ) private readonly postProcessors : IVNodePostprocessor [ ]
44+ @multiInject ( TYPES . IVNodePostprocessor ) private readonly postProcessors : IVNodePostprocessor [ ] ,
2745 ) {
2846 super ( ) ;
2947 }
3048
3149 execute ( context : CommandExecutionContext ) : CommandReturn {
32- const renderer = new ModelRenderer ( this . viewRegistry , 'main' , this . postProcessors )
33- const svg = renderer . renderElement ( context . root )
34- if ( ! svg ) return context . root
35- console . debug ( toHTML ( svg ) )
36-
37-
38- /* The result svg will render (0,0) as the top left corner of the svg.
39- * We calculate the minimum translation of all children.
40- * We then offset the whole svg by this opposite of this amount.
41- */
42- /*const minTranslate = { x: Infinity, y: Infinity };
43- for (const child of firstChild.children) {
44- const childTranslate = this.getMinTranslate(child as HTMLElement);
45- minTranslate.x = Math.min(minTranslate.x, childTranslate.x);
46- minTranslate.y = Math.min(minTranslate.y, childTranslate.y);
47- }
48- const svg = `<svg xmlns="http://www.w3.org/2000/svg"><defs><style type="text/css">${themeCss}\n${elementCss}</style></defs><g transform="translate(${-minTranslate.x}, ${-minTranslate.y})">${innerSvg}</g></svg>`;
50+ const dummyRoot = document . createElement ( "div" ) ;
51+ dummyRoot . style . position = "absolute" ;
52+ dummyRoot . style . left = "-100000px" ;
53+ dummyRoot . style . top = "-100000px" ;
54+ dummyRoot . style . visibility = "hidden" ;
4955
50- const blob = new Blob([svg], { type: "image/svg+xml" } );
51- const url = URL.createObjectURL(blob);
52- const link = document.createElement("a" );
53- link.href = url;
54- link.download = this.fileName.getName() + ".svg";*/
55- //link.click();
56+ document . body . appendChild ( dummyRoot ) ;
57+ try {
58+ this . makeImage ( context , dummyRoot ) ;
59+ } finally {
60+ document . body . removeChild ( dummyRoot ) ;
61+ }
5662
5763 return context . root ;
5864 }
@@ -63,6 +69,67 @@ export class SaveImageCommand extends Command {
6369 return context . root ;
6470 }
6571
72+ makeImage ( context : CommandExecutionContext , dom : HTMLElement ) {
73+ // render diagram virtually
74+ const renderer = new ModelRenderer ( this . viewRegistry , "hidden" , this . postProcessors ) ;
75+ const svg = renderer . renderElement ( context . root ) ;
76+ if ( ! svg ) return ;
77+
78+ // add stylesheets
79+ const styleHolder = document . createElement ( "style" ) ;
80+ styleHolder . innerHTML = `${ themeCss } \n${ elementCss } ` ;
81+ dom . appendChild ( styleHolder ) ;
82+
83+ // render svg into dom
84+ const dummyDom = h ( "div" , { } , [ svg ] ) ;
85+ patch ( dom , dummyDom ) ;
86+ // apply style and clean attributes
87+ transformStyleToAttributes ( dummyDom ) ;
88+ removeUnusedAttributes ( dummyDom ) ;
89+
90+ // compute diagram offset and size
91+ const holderG = svg . children ?. [ 0 ] ;
92+ if ( ! holderG || typeof holderG == "string" ) return ;
93+ const actualElements = holderG . children ?? [ ] ;
94+ const minTranslate = { x : Infinity , y : Infinity } ;
95+ const maxSize = { x : 0 , y : 0 } ;
96+ for ( const child of actualElements ) {
97+ if ( typeof child == "string" ) continue ;
98+ const childTranslate = this . getMinTranslate ( child ) ;
99+ minTranslate . x = Math . min ( minTranslate . x , childTranslate . x ) ;
100+ minTranslate . y = Math . min ( minTranslate . y , childTranslate . y ) ;
101+
102+ const childSize = this . getMaxRequieredCanvasSize ( child ) ;
103+ maxSize . x = Math . max ( maxSize . x , childSize . x ) ;
104+ maxSize . y = Math . max ( maxSize . y , childSize . y ) ;
105+ }
106+
107+ // correct offset and set size
108+ if ( ! holderG . data ) holderG . data = { } ;
109+ if ( ! holderG . data . attrs ) holderG . data . attrs = { } ;
110+ holderG . data . attrs [ "transform" ] =
111+ `translate(${ - minTranslate . x + SaveImageCommand . PADDING } ,${ - minTranslate . y + SaveImageCommand . PADDING } )` ;
112+ if ( ! svg . data ) svg . data = { } ;
113+ if ( ! svg . data . attrs ) svg . data . attrs = { } ;
114+ const width = maxSize . x - minTranslate . x + 2 * SaveImageCommand . PADDING ;
115+ const height = maxSize . y - minTranslate . y + 2 * SaveImageCommand . PADDING ;
116+ svg . data . attrs . width = width ;
117+ svg . data . attrs . height = height ;
118+ svg . data . attrs . viewBox = `0 0 ${ width } ${ height } ` ;
119+
120+ // make sure element is seen as svg by all users
121+ svg . data . attrs . version = "1.0" ;
122+ svg . data . attrs . xmlns = "http://www.w3.org/2000/svg" ;
123+
124+ // download file
125+ const blob = new Blob ( [ toHTML ( svg ) ] , { type : "image/svg+xml" } ) ;
126+ const url = URL . createObjectURL ( blob ) ;
127+ const link = document . createElement ( "a" ) ;
128+ link . href = url ;
129+ link . download = this . fileName . getName ( ) + ".svg" ;
130+ link . click ( ) ;
131+ }
132+
66133 /**
67134 * Gets the minimum translation of an element relative to the svg.
68135 * This is done by recursively getting the translation of all child elements
@@ -71,15 +138,16 @@ export class SaveImageCommand extends Command {
71138 * @returns Minimum absolute offset of any child element relative to the svg
72139 */
73140 private getMinTranslate (
74- e : HTMLElement ,
141+ e : VNode ,
75142 parentOffset : { x : number ; y : number } = { x : 0 , y : 0 } ,
76143 ) : { x : number ; y : number } {
77144 const myTranslate = this . getTranslate ( e , parentOffset ) ;
78- const minTranslate = myTranslate ;
145+ const minTranslate = myTranslate ?? { x : Infinity , y : Infinity } ;
79146
80- const children = e . children ;
147+ const children = e . children ?? [ ] ;
81148 for ( const child of children ) {
82- const childTranslate = this . getMinTranslate ( child as HTMLElement , myTranslate ) ;
149+ if ( typeof child == "string" ) continue ;
150+ const childTranslate = this . getMinTranslate ( child , myTranslate ) ;
83151 minTranslate . x = Math . min ( minTranslate . x , childTranslate . x ) ;
84152 minTranslate . y = Math . min ( minTranslate . y , childTranslate . y ) ;
85153 }
@@ -94,11 +162,11 @@ export class SaveImageCommand extends Command {
94162 * @returns Offset of the child relative to the svg
95163 */
96164 private getTranslate (
97- e : HTMLElement ,
165+ e : VNode ,
98166 parentOffset : { x : number ; y : number } = { x : 0 , y : 0 } ,
99- ) : { x : number ; y : number } {
100- const transform = e . getAttribute ( "transform" ) ;
101- if ( ! transform ) return parentOffset ;
167+ ) : { x : number ; y : number } | undefined {
168+ const transform = e . data ?. attrs ?. [ "transform" ] as string | undefined ;
169+ if ( ! transform ) return undefined ;
102170 const translateMatch = transform . match ( / t r a n s l a t e \( ( [ ^ ) ] + ) \) / ) ;
103171 if ( ! translateMatch ) return parentOffset ;
104172 const translate = translateMatch [ 1 ] . match ( / ( - ? [ 0 - 9 . ] + ) (?: , | | , ) ( - ? [ 0 - 9 . ] + ) / ) ;
@@ -109,4 +177,143 @@ export class SaveImageCommand extends Command {
109177 const newY = y + parentOffset . y ;
110178 return { x : newX , y : newY } ;
111179 }
180+
181+ private getMaxRequieredCanvasSize (
182+ e : VNode ,
183+ parentOffset : { x : number ; y : number } = { x : 0 , y : 0 } ,
184+ ) : { x : number ; y : number } {
185+ const myTranslate = this . getTranslate ( e , parentOffset ) ;
186+ const maxSize = this . getRequieredCanvasSize ( e , parentOffset ) ;
187+
188+ const children = e . children ?? [ ] ;
189+ for ( const child of children ) {
190+ if ( typeof child == "string" ) continue ;
191+ const childTranslate = this . getMaxRequieredCanvasSize ( child , myTranslate ) ;
192+ maxSize . x = Math . max ( maxSize . x , childTranslate . x ) ;
193+ maxSize . y = Math . max ( maxSize . y , childTranslate . y ) ;
194+ }
195+ return maxSize ;
196+ }
197+
198+ private getRequieredCanvasSize (
199+ e : VNode ,
200+ parentOffset : { x : number ; y : number } = { x : 0 , y : 0 } ,
201+ ) : { x : number ; y : number } {
202+ const width = ( e . data ?. attrs ?. [ "width" ] as number | undefined ) ?? 0 ;
203+ const height = ( e . data ?. attrs ?. [ "height" ] as number | undefined ) ?? 0 ;
204+ const translate = this . getTranslate ( e , parentOffset ) ?? parentOffset ;
205+
206+ const x = translate . x + width ;
207+ const y = translate . y + height ;
208+ return { x : x , y : y } ;
209+ }
210+ }
211+
212+ function transformStyleToAttributes ( v : VNode ) {
213+ if ( ! v . elm ) return ;
214+
215+ if ( ! v . data ) v . data = { } ;
216+ if ( ! v . data . style ) v . data . style = { } ;
217+ if ( ! v . data . attrs ) v . data . attrs = { } ;
218+
219+ const computedStyle = getComputedStyle ( v . elm as Element ) as VNodeStyle ;
220+ for ( const key of getRelevantStyleProps ( v ) ) {
221+ let value = v . data . style [ key ] ?? computedStyle [ key ] ;
222+ if ( key == "fill" && value . startsWith ( "color(srgb" ) ) {
223+ const srgb = / c o l o r \( s r g b ( [ ^ ] + ) ( [ ^ ] + ) ( [ ^ ] + ) (?: ? \/ ? ( [ ^ ] + ) ) ? \) / . exec ( value ) ;
224+ if ( srgb ) {
225+ const r = Math . round ( Number ( srgb [ 1 ] ) * 255 ) ;
226+ const g = Math . round ( Number ( srgb [ 2 ] ) * 255 ) ;
227+ const b = Math . round ( Number ( srgb [ 3 ] ) * 255 ) ;
228+ const a = srgb [ 4 ] ? Number ( srgb [ 4 ] ) : 1 ;
229+ value = `rgb(${ r } ,${ g } ,${ b } )` ;
230+
231+ v . data . attrs [ "fill-opacity" ] = a ;
232+ }
233+ }
234+ if ( key == "font-family" ) {
235+ value = "sans-serif" ;
236+ }
237+
238+ if ( value . endsWith ( "px" ) ) {
239+ value = value . substring ( 0 , value . length - 2 ) ;
240+ }
241+ if ( value != getDefaultValues ( key ) ) {
242+ v . data . attrs [ key ] = value ;
243+ }
244+ }
245+
246+ if ( getVNodeSVGType ( v ) == "text" ) {
247+ const oldY = ( v . data . attrs . y as number | undefined ) ?? 0 ;
248+ const fontSize = computedStyle . fontSize
249+ ? Number ( computedStyle . fontSize . substring ( 0 , computedStyle . fontSize . length - 2 ) )
250+ : 12 ;
251+ const newY = oldY + 0.35 * fontSize ;
252+ v . data . attrs . y = newY ;
253+ }
254+
255+ if ( ! v . children ) return ;
256+ for ( const child of v . children ) {
257+ if ( typeof child === "string" ) continue ;
258+ transformStyleToAttributes ( child ) ;
259+ }
260+ }
261+
262+ function removeUnusedAttributes ( v : VNode ) {
263+ if ( ! v . data ) v . data = { } ;
264+ if ( v . data . attrs ) {
265+ delete v . data . attrs [ "id" ] ;
266+ delete v . data . attrs [ "tabindex" ] ;
267+ }
268+ if ( v . data . class ) {
269+ for ( const clas in v . data . class ) {
270+ v . data . class [ clas ] = false ;
271+ }
272+ }
273+
274+ if ( ! v . children ) return ;
275+ for ( const child of v . children ) {
276+ if ( typeof child === "string" ) continue ;
277+ removeUnusedAttributes ( child ) ;
278+ }
279+ }
280+
281+ function getVNodeSVGType ( v : VNode ) : string | undefined {
282+ return v . sel ?. split ( / # | \. / ) [ 0 ] ;
283+ }
284+
285+ function getRelevantStyleProps ( v : VNode ) : string [ ] {
286+ const type = getVNodeSVGType ( v ) ;
287+ switch ( type ) {
288+ case "g" :
289+ case "svg" :
290+ return [ ] ;
291+ case "text" :
292+ return [ "font-size" , "font-family" , "font-weight" , "text-anchor" , "opacity" ] ;
293+ default :
294+ return [
295+ "fill" ,
296+ "stroke" ,
297+ "stroke-width" ,
298+ "stroke-dasharray" ,
299+ "stroke-linecap" ,
300+ "stroke-linejoin" ,
301+ "opacity" ,
302+ ] ;
303+ }
304+ }
305+
306+ function getDefaultValues ( key : string ) {
307+ switch ( key ) {
308+ case "stroke-dasharray" :
309+ return "none" ;
310+ case "stroke-linecap" :
311+ return "butt" ;
312+ case "stroke-linejoin" :
313+ return "miter" ;
314+ case "opacity" :
315+ return 1 ;
316+ default :
317+ return undefined ;
318+ }
112319}
0 commit comments