From 3a776f8795f2a2f3fcd42b84d9b165b620210d7f Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Sun, 18 Sep 2022 05:02:13 +0800 Subject: [PATCH] fix: image-mirroring in export preview and in exported svg (#5700) Co-authored-by: dwelle --- src/element/newElement.ts | 9 ++- src/renderer/renderElement.ts | 30 ++++++- src/tests/__snapshots__/export.test.tsx.snap | 20 +++++ src/tests/export.test.tsx | 73 ++++++++++++++++++ src/tests/fixtures/deer.png | Bin 0 -> 12468 bytes .../svg-image-exporting-reference.svg | 16 ++++ src/tests/helpers/api.ts | 24 +++++- 7 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 src/tests/__snapshots__/export.test.tsx.snap create mode 100644 src/tests/fixtures/deer.png create mode 100644 src/tests/fixtures/svg-image-exporting-reference.svg diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 2d6a12af..ad5161b7 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -308,6 +308,9 @@ export const newLinearElement = ( export const newImageElement = ( opts: { type: ExcalidrawImageElement["type"]; + status?: ExcalidrawImageElement["status"]; + fileId?: ExcalidrawImageElement["fileId"]; + scale?: ExcalidrawImageElement["scale"]; } & ElementConstructorOpts, ): NonDeleted => { return { @@ -315,9 +318,9 @@ export const newImageElement = ( // in the future we'll support changing stroke color for some SVG elements, // and `transparent` will likely mean "use original colors of the image" strokeColor: "transparent", - status: "pending", - fileId: null, - scale: [1, 1], + status: opts.status ?? "pending", + fileId: opts.fileId ?? null, + scale: opts.scale ?? [1, 1], }; }; diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index e53dd698..1265aebc 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -790,6 +790,9 @@ export const renderElement = ( context.save(); context.translate(cx, cy); context.rotate(element.angle); + if (element.type === "image") { + context.scale(element.scale[0], element.scale[1]); + } context.translate(-shiftX, -shiftY); if (shouldResetImageFilter(element, renderConfig)) { @@ -950,6 +953,8 @@ export const renderElementToSvg = ( break; } case "image": { + const width = Math.round(element.width); + const height = Math.round(element.height); const fileData = isInitializedImageElement(element) && files[element.fileId]; if (fileData) { @@ -978,17 +983,34 @@ export const renderElementToSvg = ( use.setAttribute("filter", IMAGE_INVERT_FILTER); } - use.setAttribute("width", `${Math.round(element.width)}`); - use.setAttribute("height", `${Math.round(element.height)}`); + use.setAttribute("width", `${width}`); + use.setAttribute("height", `${height}`); - use.setAttribute( + // We first apply `scale` transforms (horizontal/vertical mirroring) + // on the element, then apply translation and rotation + // on the element which wraps the . + // Doing this separately is a quick hack to to work around compositing + // the transformations correctly (the transform-origin was not being + // applied correctly). + if (element.scale[0] !== 1 || element.scale[1] !== 1) { + const translateX = element.scale[0] !== 1 ? -width : 0; + const translateY = element.scale[1] !== 1 ? -height : 0; + use.setAttribute( + "transform", + `scale(${element.scale[0]}, ${element.scale[1]}) translate(${translateX} ${translateY})`, + ); + } + + const g = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g"); + g.appendChild(use); + g.setAttribute( "transform", `translate(${offsetX || 0} ${ offsetY || 0 }) rotate(${degree} ${cx} ${cy})`, ); - root.appendChild(use); + root.appendChild(g); } break; } diff --git a/src/tests/__snapshots__/export.test.tsx.snap b/src/tests/__snapshots__/export.test.tsx.snap new file mode 100644 index 00000000..857a1e59 --- /dev/null +++ b/src/tests/__snapshots__/export.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`export exporting svg containing transformed images: svg export output 1`] = ` +" + + + + + + " +`; diff --git a/src/tests/export.test.tsx b/src/tests/export.test.tsx index e6bd2182..02e6a13a 100644 --- a/src/tests/export.test.tsx +++ b/src/tests/export.test.tsx @@ -7,6 +7,10 @@ import { decodeSvgMetadata, } from "../data/image"; import { serializeAsJSON } from "../data/json"; +import { exportToSvg } from "../scene/export"; +import { FileId } from "../element/types"; +import { getDataURL } from "../data/blob"; +import { getDefaultAppState } from "../appState"; const { h } = window; @@ -101,4 +105,73 @@ describe("export", () => { ]); }); }); + + it("exporting svg containing transformed images", async () => { + const normalizeAngle = (angle: number) => (angle / 180) * Math.PI; + + const elements = [ + API.createElement({ + type: "image", + fileId: "file_A", + x: 0, + y: 0, + scale: [1, 1], + width: 100, + height: 100, + angle: normalizeAngle(315), + }), + API.createElement({ + type: "image", + fileId: "file_A", + x: 100, + y: 0, + scale: [-1, 1], + width: 50, + height: 50, + angle: normalizeAngle(45), + }), + API.createElement({ + type: "image", + fileId: "file_A", + x: 0, + y: 100, + scale: [1, -1], + width: 100, + height: 100, + angle: normalizeAngle(45), + }), + API.createElement({ + type: "image", + fileId: "file_A", + x: 100, + y: 100, + scale: [-1, -1], + width: 50, + height: 50, + angle: normalizeAngle(315), + }), + ]; + const appState = { ...getDefaultAppState(), exportBackground: false }; + const files = { + file_A: { + id: "file_A" as FileId, + dataURL: await getDataURL(await API.loadFile("./fixtures/deer.png")), + mimeType: "image/png", + created: Date.now(), + }, + } as const; + + const svg = await exportToSvg(elements, appState, files); + + const svgText = svg.outerHTML; + + // expect 1 element (deduped) + expect(svgText.match(/ elements (one for each excalidraw image element) + expect(svgText.match(/w7DJ}m+j)b3~j>(9Olp)aaEB|H$e!l;(lg0_ zn-~k%gGIwX0_8x~&ZN69umih|JvL_S6GS_}O2)DbX7`H;eqBivePon?`H4*$aaQ12 zN^$?7$X_XINv?sqUmP$ejF~%WW6HTi>kwx4af4Q11Yq^Au`AR95}0dt!s@cVmjD%7 z(Zg^-{EHWiXeFS$3Z6s0enR0gfSjC99PZl2hwKliE}VVZXwVHOyaLS!)1%R9l|c5*B~|#s z+Yo#7j>*A|R>byi-d&K|N)ixw$#T5&Bw#g7kH-e1L*mO&RsZ9fyMJ@rnsV-9;V;~C z8y9ZKfgz)13~YB=H2gw;j*?wb>u!2!Y9LIcdfb?~NFy9xF~R>H2XUSIL7vc^={$3e z2r!On>ql`tS4e&T4_RreL7kqKyRnjFy)~L?cF^+cq+8I-?IB#`Xf@9pYQI2j?k@u# zggf3pOeJ1W|2Graq{?75vVyU?iW z>{Pf&JQTv=JMpPg#_ebUtB$s_aY}2{K>o;E%umeH2mw`p=J4w8U4(h<7yu`bd>m|~ z)K7{|2Jto|K>LMXlqZ@Q@yCZed;()J*l)=joUnf_W%Rz85O792 z!1SmP@K13zTcHR*dNI^;Hh4WnPAm44RYc1VI(AIF>hJuXbXAawwsb*~+^Wj9VBNN* zih*$q*%o)cYz$y=d$l@&hH4M>IhRIH!P7()ClQKL9~T0fw%xajQ$H{5q`&+|c@66H zchLXIpA}g9rlT z+nB4#MX6dNb2w_17$9i)7Ul|GpQb8evJhKtLKz~;lG&0Y+$v>qxyrG8%h~hvkr5ex zB#38NfG5@GnL^BKQ2^|iPbCQ*^$;*BRdR@7gVNIJh0Rf_qDoV-HgzQiz0MbR-{v$c zi#Dw34mLO_pnHO1Co*31XCJ3&YoTl+O6sqLHtW%R62yayA~4DR&E{L)Mlne}wG+a( z_>?6GJ2qP;_j(%hAvKNVkq;u(HuA^FxSIs=$wtW$mt;837VZyrdA9Azd>XH3!~}Q& zFB!hV*-Uix(b<3F8Bwacl0uAN_ZvhEwZ`Y_qbe;!=Dl>lieE&so^n5V1Oq{#g!=MP zY=&aGD`{^40jtgq+RmtUN>C?Rf9BqH3vT#NA6RlcU#3FMFYPqH&QF@m;cG)$}HbsA*#tE zc!C`%{RmdCw)=cP>Ub_rsMO^VB>Q*5A?;01!MTRFpn>UEJmER)jh$h0);MsbMsE*o z2KQRZ!k=#B$Y;D$^JA2|SJH7@#C$==C+vQ9sjcY7Qo0BR{6ZRkGV7-b>!}Bdh*TX@ zUT2K+E~a~KKDBTC5%|Giv||WsP7b%ne8Uc{Mw`}V;cTYMMvE~VIZ;1#%-Hj9K~VWb z9p`NizfNIFD%naB*O$TMo^4Cz@IFfwG2jjIF~Uy33hLSV$sXo=rJ=l`gnfANn}GrI z4J-7gg(#xEK_ygPZgb#vgZ^sMrTgK1_4-Lxf(as(RBJQq<3>`Bc9QhO&;E;a4?sBT zLChq>!mt=SsaM7I*@khCzC!z??LI4=Ge=;nVAZI^Ise$@($+M$KVb+c(Y_vML;VQzs zQfK#E^qf2uBvVfwzN$AY$BmepA0pO3>2CZPRo8dl3Mm-hxbtir3F%@BCg;0*-+%H? zATO8g=QCE1R8c;5Qf;_}xCMDt++iT)Any&@6u%z3PqzN>E<*|#X@)31wB1s zs;Pes;qpz8J`;Q6_5v$m3}v+Z8eN90Hv$OyxXnEq3_0Nt;GYj&FYh3SY=a!&0cc&?;1XdP@4hw8XeO-+`{cz+Y!3aYT)b4mg;+2+(kAt#z(pcaq9jT`HT#T z(j&dS4%9hMuQJGgDJ`nYDGge*G&yZFZY=TeOrd?EMf~ESm~%RAklmZVkb3z2n6U6En4WuJdz@y6x@z# zBRsh=Sw>R`*1*LyLTxggr~zJA$@(c6GPb#Kxx?ndM8@Z#8v0{}gtV-jg(Z#2;e2@D zLS{xjAmEFf-6rjF2c%zF?^VFq{s1~+5xqzX2yBsbC;%kihZ~{GQ1(^;=Rz^MbRk>0 z=Bdq6^aO=89ekHIP;`bxX2okJwEti`!a?Rxdf294%n|^^pDB#T*oKSGQjjqnBhR!q z(4cshnUuG?shFQwK@rZ>&J#7VOf{J=eOhX6<}%0wx%~v5suh9G+(`uJN8NuF??(1* z9k&9T`vepu4LP96j;#RRBw~-ujpr;-IEVSN;94TM-2H4HhHxpmQ&fOFCw>}9;dpfmQwDi66KI8l9 zsS4sDQQr&6L~!S7P&~Jh83^FeFXbP1Uj}H^tCJ}HX!vaPy&U-?QrjjWs=zxD?-EX_ zSSPRFW`F52_N|U*a6)f^NskU~?TIU23hmdB$#SDWa(N-aoc4`dc)8I#(nf{ zDg0jzQ*;!E2&D0H;7D}yokShC&tzKO6p%i$=5@FukZTr$U88N zwZ4swMZ$9FP6supL>=osDBbV8U9V7hVFag4yr`aBAEj$WMp~zj!tG7Zs|+~fD36X~ ztH@jXyv79JK2aJpA)?l3=CpzX=aprsPPQBuMC!X8*n{9L&XD-qvMS_b${uV%jsCMs zUzm$tjGo`&d{W?;FoU;=P|jR+;MRba+;G|v%%brrF|S>hw6`>-;M=*|(WNro z4}zzCSH)A4hwFW!*IT>##i-vT(@mGDNM*%MnlIjHRAFS3#hVx#x+MRAHcu7{GuH++ z%H`A?TVrkcKTQzfsR|GBSyd*ve|3kmos2YvIp-6!y;qnTc&j6iijOll{u3Lp^;vZT zvhvpZ3?bb28uewhxO}f$#y>b~=(dg{6mO8$?OPI@t#O3FCUA4@1IY)qvNy;;rXW5^ z54_O~&Lh;^hA<+vpCdGJ%D$@D0t_~nMB%(Z#{?f1FZC&Gcck*P1gUka_B-2JDwG29 z`-=cj;IWiTR_AaQKOtPaV~;37v7FEC`y*24R~#jN53cO9B*dBbJW1zY>#eJ!bNBvR zdMma;Ff0^nh7C<0B?}5dd)k^WB>MKVn@vDkRi&%z=LV2XQ(s!SNHF}P!1kbX>BPkJHRQ` zr{5`i^WWyKHvBD0u|{oLPJKD35|;c)c-5?oqIaho==(S83$e|P22d6u<_`g-8)p?ia4cZwOub5-GcsiH>uis3{T;$8@FonxbA zU>Eez^vUI-Nrmf>*wcF5m}08SC0BJbmeK-6`)jO_?Vb5}J_&Zeps})h4c{=k{46bK~thiod$mf?EmQFqF1ed)uqR-r+nzbgc|LEx_Gu5cZ00>gGbD6}t!)?87hoaf~-rrPm+#(DaR0 znJP+>UOB=2+h$;ZbKZ$+yv;USboOY>_($_J7~hIthQr#*Une2Ro%;Uha*m9I^Rq1& z7y|jE0ysu>&S_#26$PXMQ$n%Ae@-^Xn!skgtiL2_+4A67r^Y<$ub&ti z3tXrSmQ-9J6eU3-6-e;+QTW_UaTA;q0cX>73)-`GSt|e3e%v)Vb)#=J7e^rK+@VYDYeDe$&OKUsNx!o{s z2J?p6o-&7eu^q@%C_t*_UYrX??I-IR_?CSr+^&bG%CdI33D_VB!hg}@RR>)(=7YOG zQKYICUTZb*w$iY%IF_TJw-@%_>`^=k{}}=LP-Td&*$jDfAGdMj@Jde6s0MGW@;CDE zD&io^9Roz_;qKK#HgBNF?t4zMl$&bn>7eK8V8T)kRuyD$qT5FY^~sugq}c-Ip0^hq z?KD%x$iwqpaPUEe?qJ~es?Iio92s=_U<{c4j+Xxgd`5IwkAq?yMfN$+>Vh?#jx$j; zgF9yXdv5phEtd&$UXSga#LU!%yc5w6Hg$;>_E6)UV?oyiGTX5XU%8KlDQ|W|%_VuA zM2i0yu#DfDqQ~i&*$-Ww!Dj;A2tW+*PO6c<>CRSWf0zxnM$@@U%zJ`7zfV=A!HEuk zY8D$JI(&^7rO@VIh_985hDTjntE>{^SS1n z_0P`ZLL;%g#g7d0J865O)xLD`D7d2&kh$Z7bPnwisW-se-L-(-u8ZBJ@|Z+|1u^CZ z|GzSXPejdja9EbyAkC>i#fV76*2)tB7}wng=qJIiduh& z*?&aa=3p;Ki@Ht-7Y zMOg1AE@z3lvy@MK@6S8`!^<71B^U!|3x9~wA*Qr$ABpruN7jxYELxNpeMDMA$B7&g zoD38ii^B@1;Dc5fehG*Lr6;6T z=}Ci!l4gmQC>ipd(&gf@T>7Zfu&w;wWnBVSgZ04k`ef$m<6aD$#vp5owxeH>wA~l4 zgD=c!#JE$F*V1(M^cAb$3Z{wGTvg_Fe}4*y;yp zFHsVg&%rNZ>)}yaal6r!foSASMTjSAEmYva??Dyx>gVkxb6gt@&c|p|Cw;hUT2tzG zYv7SeYIoYJq3+Oj>J=~Qy)w15e*bvUup#u4lF~=0 z;{%oe>vOe(dG~|5axkgGRwh6vvSIOn(&S$Svd|=%v1{2`uZtTsd(I zH(y>e>afr6+Ydh0C1q2WTWoVpNGNIT3ybsh-ol`yAVJ0Tmmkh|9G5kI9bKu8leY`; z>abOT@}_Zu7ZWEAKm$nDV1|Yej_Dbg!9K`tNofge^%vx$tmVRhPk-TrKc-KbUJnSJ zLj$=lXdfN^2iV3a<8-6~`i~tqGEe8! zYw-E4ZM}L-CmBTMtsohP(VbAU$&yY)Ev&)d?JVT>cLJA2q|)~^fu`JW>cXEBd=G6X zzGtpK^$Z*Brg4MUx2xt#%tRAWzvZgycsVRjxA^BQv_EInCi)=(-X zslq+Fd$G}pLObm2z-{@+xKO$D?TaedKea9 z`^6`pZj?yd7I5Gr-N*c3t%{iXaG7}s1=y;)y6!F(pLwiF>zzUU#D%$si(}j$pD8MS zl<%LcIhFuYU0Gxi0;!$B0HMCGUa=frjbS5v_p;ktK`Q!g-b7Ef*esTm>Y3w%mAjX2 z?s~ivQ5WAh#g)n#VH1QQqMp*cQU;Jf%l>e*-5({<*$#jWZ(f9+RFd#^QlfTTxG5^s zdNs*cj74#v^@nG&nhAcP=(Ri0VK8HsFXqedU)Oo*75(5~Ote(Dcf*!3LJ9N5<)4xM z_w^+;G?rlQxeqUJnC_vQn9j&TeC=d{FRHK^XQ7x9aB{eE&_rX^iFy0G>mA_?N7H@0VV^R zwV?q!)J{iV#7EFVP_I2n;uJUNa`u{vS!1fHR=%yhAz7Lk{V&Zed8kE`I(ecRrMsq} zdO0m@LgKE%2Vz~V`2jtH&ms{Hlb8LYIux(#*sQ-a7$SQ^+qK#%6c5Oj#_-)`kk`W&2@R)^p8#s0ZnmMtqZd%;*8bVi|jtV zs?Ma{EkQYz^<&OXPR2oK=VM_aLy;$#=YKL4<-)i3Zdgwo?_q&Wu>X| zSF&RnJgvpzL7TXtH>JXSG;_sS3q#G6bweBOT!q_Db1$lYDZPX5n9bUR={(iD6kdf2 ztOUqj$W>E@Hdl}HvS*wLJU^La*y4k3GCka?Pk%p>1>d2)D*&)RcJY~i63O5;LN_|l z=%bEP@rA6BIr2CAYdZi)VhWQO!EEz}I@Ze|3`Cr}EsZ}NmDF9dq#WW|WB(j8! zUv^8@yG5%maID&I+OF;{z6L8WajEvoyG@e!+B1gLV6Ng}utmL-x;V+Zf3?vFgkS^# zn%cDEW@m8MTFa2#1CE)P$hMEN;qXkc&U0stO|I-Ru?nLX)doa-m!H1X-&vbdfW=s& ztQQ2bp;Y~kw=u@pQ71y8azF<8< zTev-Re?A!K+Q*!!2xe<>CJvTuP7GVXjs_(a=~VaA(7-D4rGS=su{=9J*$1z(DK?iZ zPB-LEg7O1r?q{x0&InpjD6VR67`^yhMRnHA&$sZMv6fO7O(_f!vkHH+1tFPw=eytu zRZjfE&Hb+_1BR!|!5mLR$sqX_jIPTh$wYyL9fD%L_o@#~W4HfQ?{}Sif#|{bxI=%Z zLZoMnMQs+3iaDS^1Gy&7Tb?^~M;oi3dtof10|~y@+Kn#CgrS=%x|{|o!wGC{AGx9E zYP`-CIq2&Z98igf`BV!HC-FP+aIG#g7tMW4a4n~q@X(=-8A@CJmK{124WSidV6aze z_0Z(AOTXaRUmscIo%FSyhT#6Y_MMLmG={XQtsR29`Nk^)pV%J@Sr(I+w^&7Yzg+ti zpW=LYF|G)A_1TH|oAVKRV-8R%(mu$wCSy?#oZ9U?KGnIz|5BaZSmgLbk5lIN0;Vzw zzTWP#!1k-{bscY4)PfF*BsMo;i)oTLys>#*qfUo>;$k=c{a_p`xz}VApW!Kd`KJNq?db0FPe(>jltCep++Aw1W4?iJktshF0YfY(n9r1nKzD zS8S3&Q{r4uQHO{=P>q0+^PNd|%aGtst8@fJ2Ca@PV>vy( z#Y_I?6e`R;3>2;OPi<8!b;GQkIRkTe z%zB8`dv_mHd?LE3 z&i9<>bj7}d?=sCUzxcNcwf<<*zpCujs4>AwuQE6*b=9lLfe*X;>V2N#<&5|V^kT2g z&+CAvO9@{AQrS6_RO%HXm3QROVE^0u^A&1)=!(FsqfBCDrpAKA)uT*!MQYA%n|joQ z`?|d}N*_%GC;Zm+)d%F2l%ChOQ$xT^Em{RE(N2FvDpw2c&B+c{>3zd4TKgUj{{MVK ze2f*e6$fDka~2skKS@71>tZ=FyA-d37}EEykP*?W4tlBGaBt>{&7~(FaEF%0{{u8I z=3pNbOr%P^E0h6^SokS*oL_bJlQ4c^V7Ym48lMCN-Q%sBEE&Y?P2izk6yN;1y&PuFBK7g+z(C9;DB;Y7ccJOTj0m8QUgcAs`K$6zmF3~&R-%_S?^3; zb07U98%0lPwf4@D)lnOz_OrBJ0z57tj~|ssX>{pyYssdGGoIj#JfV=nFVq;a<9sWF zI8)Ytg>UhE@VI+k_1W6S2x}z>s9Boy?mvK3zBuWg%V9!7aeItw zzvC#O?>}{4>a+!3HGf3e&A!$P#xLX)T^MAMM#RN^)x%oFv-n$2X}x%a!EV?f4Ic!# zbvkJ9LI;EMH~2+5l4s=G(BhQ-ZGCjiKe^{W%e!xb?GWD@$uxRNdi^Wv`{6|MfkEu+ zbARjNessnBtX48;`OhKy-#8MGuFI|T6Kd~n^ZS-E=8nhXBq;wsOy?lOOZ5BCc+7hL zB$<*h{&SJYxRr0jnv{@-No6_L!(QiqrFmm@I3p}(Bt3R+3D~BT#6cuGkh?fD|M{x;p^sos> zY(u^un9!%t+-AQIJ7XI@@){^CV-$t^*CFw#zYFknAL+n<>{px{{aeQfWeR#ewbf7cUT#!i6s`eY1L9s-1BEHgkamo`UZ7<~EcTszpZzhmKfNDqr4P&p_@NA!A_) zJ`<JkjeEsqPo zom14M;r6*hbiA!W#I-cw z2Rz`o(OIkPrDFwa;GPyLt>Fhd8s>Xe=-eN=#9Hx-tb^B0avImjN8y=z;yO(Zj1s<% zI%J%ceqouTpd}i6Prjx*7^)sFsNfgsjMij2pnqh7$^uLIQQ77q7OH43G+(U*CUBNG z6Z{#Bg6;dSFF)qe%T01rM2saV9`>n<1$u_;laZiYl!9vRRx#5L@#$e2zR;Wb@!7`~lmICd(0A4zRG+W&113s%xI1s;5GBZuiJnr6TBzx!lAcOWlf~4@ zn(_`UF85d8`?ZtW8Z=R_ccN}k(?d0dHSAEQ`AOqxcAZcB*QP%)u?Q;fqJ)l?qIcLv z_vMcX?4fxlfl{L+t02vU&e`H2tXEZQ>vk#?23vCC*X^w+&nuzUuV%(tApgA4@#Hfc z(4(p82mL;mc40>6J*8mzMpJx(f!M?lRXL_;mVe?J$pf1R$=KwN%3aQ-ZFbP^Av}`k zPOK6w=LV|_+d{SJh^uy zSI4k;@V``K@J~f~_B-5wK%T*biH=a+R|<+5`2;yPD?|w|y|3PICyyT)WGDLxc2Vxx zUZ8UU^tI8_3h%2KnvzhnIHSig^Z#Lmh}s(YYFTgcIiF17*Ya?`=H{c! z)IsSsXRRi}?GtUAB(y;MbH>xVOEpi%-L>U8+Ji6XUl#IF{&urU$zDkXHdz?$p(@!$ z$j9()E@86X-QNxvYl<^2Q5^Dc z*Ln?3JT7#4{`8mU(ElJT#3{;lQtx_E|28pUhC+}K6ltGt00ytfQNu5k@w)evM~oH5 z33y8~b&aQi*zB9lVM;K3ah@#Zi+%Xy0qOh;Ez#PH)V#QKuMT^!=_?feuF1 zG+nJ)Ce!>auq{WRCn>H_05TKhHDsTh7Y2%Qy8SX(Yoow>uUKa0r^@5|&>yU7{OvFn ze?l@yS-hOkdi(^#H|EY_fI|^r$ zbkh1Ox0U$atj&C$LK+NwJ~VO-l22nDMBK{lU?lA39Yq`0om_6CD@pk`Eq1|P_^L-d z+zNEQ)YD*@D~?=mZz|`N(XTe|r1v}kopzB*qMs+81D=KH@roYuC29p@a=ANM@{&6< zFN~~=d`r=6q9kzzd|bvLb$T4}R~rd4(HqF6pQ)14ITFOBJhmVS)I`0ZC$L9DClX}Updcp8{<#o5>d9ba;cNYYEl$Pp524PFPHDmh<7sl+T zy;s+m-pvE!g*YVXJ(vl?bwOXo)%4>oxJ zk^@sMI&D;3c*)_!VDbWd%SB--^|OM*+@?W8uaya|QmNcp$b{;84gmuJJv9N5u$svc5@C2OG~_ zn4}TS947JUAQ4=!Czm+h=Ou`Z6db;qe_aC-Q2;|;yk#jVh%FZ!x{Oj-v!IdizXbXG hKbHR&wR|h}*XjK;M|2np{22nEEUzwC_tNss{{u(kp5*`l literal 0 HcmV?d00001 diff --git a/src/tests/fixtures/svg-image-exporting-reference.svg b/src/tests/fixtures/svg-image-exporting-reference.svg new file mode 100644 index 00000000..513c72c1 --- /dev/null +++ b/src/tests/fixtures/svg-image-exporting-reference.svg @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts index f0afdd2c..14d9a8e2 100644 --- a/src/tests/helpers/api.ts +++ b/src/tests/helpers/api.ts @@ -4,6 +4,8 @@ import { ExcalidrawTextElement, ExcalidrawLinearElement, ExcalidrawFreeDrawElement, + ExcalidrawImageElement, + FileId, } from "../../element/types"; import { newElement, newTextElement, newLinearElement } from "../../element"; import { DEFAULT_VERTICAL_ALIGN } from "../../constants"; @@ -13,7 +15,7 @@ import fs from "fs"; import util from "util"; import path from "path"; import { getMimeType } from "../../data/blob"; -import { newFreeDrawElement } from "../../element/newElement"; +import { newFreeDrawElement, newImageElement } from "../../element/newElement"; import { Point } from "../../types"; import { getSelectedElements } from "../../scene/selection"; @@ -77,6 +79,7 @@ export class API { y?: number; height?: number; width?: number; + angle?: number; id?: string; isDeleted?: boolean; groupIds?: string[]; @@ -103,12 +106,17 @@ export class API { : never; points?: T extends "arrow" | "line" ? readonly Point[] : never; locked?: boolean; + fileId?: T extends "image" ? string : never; + scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never; + status?: T extends "image" ? ExcalidrawImageElement["status"] : never; }): T extends "arrow" | "line" ? ExcalidrawLinearElement : T extends "freedraw" ? ExcalidrawFreeDrawElement : T extends "text" ? ExcalidrawTextElement + : T extends "image" + ? ExcalidrawImageElement : ExcalidrawGenericElement => { let element: Mutable = null!; @@ -117,6 +125,7 @@ export class API { const base = { x, y, + angle: rest.angle ?? 0, strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor, backgroundColor: rest.backgroundColor ?? appState.currentItemBackgroundColor, @@ -167,12 +176,23 @@ export class API { ...base, width, height, - type: type as "arrow" | "line", + type, startArrowhead: null, endArrowhead: null, points: rest.points ?? [], }); break; + case "image": + element = newImageElement({ + ...base, + width, + height, + type, + fileId: (rest.fileId as string as FileId) ?? null, + status: rest.status || "saved", + scale: rest.scale || [1, 1], + }); + break; } if (id) { element.id = id;