From 24c7990f9c83a1fa81aa9b23d7712980ac556467 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 7 Jan 2021 00:51:45 +0800 Subject: [PATCH] feat: ImageData relates API and tests --- __test__/draw.spec.ts | 265 ++++++++++++++++++- __test__/failure/transform.png | Bin 0 -> 2686 bytes __test__/index.spec.ts | 23 ++ __test__/snapshots/createImageData.png | Bin 0 -> 1692 bytes __test__/snapshots/getImageData.png | Bin 0 -> 1363 bytes __test__/snapshots/lineTo.png | Bin 0 -> 1315 bytes __test__/snapshots/moveTo.png | Bin 0 -> 1447 bytes __test__/snapshots/putImageData.png | Bin 0 -> 1360 bytes __test__/snapshots/quadraticCurveTo.png | Bin 0 -> 2473 bytes __test__/snapshots/rect.png | Bin 0 -> 1351 bytes __test__/snapshots/save-restore.png | Bin 0 -> 1438 bytes __test__/snapshots/setLineDash.png | Bin 0 -> 1163 bytes __test__/snapshots/setTransform.png | Bin 0 -> 2286 bytes __test__/snapshots/stroke-and-filling.png | Bin 0 -> 1428 bytes __test__/snapshots/stroke.png | Bin 0 -> 1336 bytes __test__/snapshots/strokeRect.png | Bin 0 -> 6611 bytes __test__/snapshots/transform.png | Bin 0 -> 2686 bytes __test__/snapshots/translate.png | Bin 0 -> 1394 bytes index.js | 17 ++ skia | 2 +- skia-c/skia_c.cpp | 23 ++ skia-c/skia_c.hpp | 4 + src/ctx.rs | 309 ++++++++++++++++++++-- src/image.rs | 6 +- src/sk.rs | 136 +++++++++- 25 files changed, 748 insertions(+), 37 deletions(-) create mode 100644 __test__/failure/transform.png create mode 100644 __test__/snapshots/createImageData.png create mode 100644 __test__/snapshots/getImageData.png create mode 100644 __test__/snapshots/lineTo.png create mode 100644 __test__/snapshots/moveTo.png create mode 100644 __test__/snapshots/putImageData.png create mode 100644 __test__/snapshots/quadraticCurveTo.png create mode 100644 __test__/snapshots/rect.png create mode 100644 __test__/snapshots/save-restore.png create mode 100644 __test__/snapshots/setLineDash.png create mode 100644 __test__/snapshots/setTransform.png create mode 100644 __test__/snapshots/stroke-and-filling.png create mode 100644 __test__/snapshots/stroke.png create mode 100644 __test__/snapshots/strokeRect.png create mode 100644 __test__/snapshots/transform.png create mode 100644 __test__/snapshots/translate.png diff --git a/__test__/draw.spec.ts b/__test__/draw.spec.ts index 148f9662..e13dfff2 100644 --- a/__test__/draw.spec.ts +++ b/__test__/draw.spec.ts @@ -180,7 +180,23 @@ test('closePath-arc', async (t) => { await snapshotImage(t) }) -test.todo('createImageData') +test('createImageData', async (t) => { + const { ctx } = t.context + const imageData = ctx.createImageData(256, 256) + + // Iterate through every pixel + for (let i = 0; i < imageData.data.length; i += 4) { + // Modify pixel data + imageData.data[i + 0] = 190 // R value + imageData.data[i + 1] = 0 // G value + imageData.data[i + 2] = 210 // B value + imageData.data[i + 3] = 255 // A value + } + + // Draw image data to the canvas + ctx.putImageData(imageData, 20, 20) + await snapshotImage(t) +}) test('createLinearGradient', async (t) => { const { ctx } = t.context @@ -255,3 +271,250 @@ test('fillRect', async (t) => { ctx.fillRect(20, 10, 150, 100) await snapshotImage(t) }) + +test.todo('fillText') + +test.todo('getContextAttributes') + +test('getImageData', async (t) => { + const { ctx } = t.context + ctx.rect(10, 10, 100, 100) + ctx.fill() + const imageData = ctx.getImageData(60, 60, 200, 100) + ctx.putImageData(imageData, 150, 10) + await snapshotImage(t) +}) + +test.todo('isPointInPath') + +test.todo('isPointInStroke') + +test('lineTo', async (t) => { + const { ctx } = t.context + ctx.beginPath() // Start a new path + ctx.moveTo(30, 50) // Move the pen to (30, 50) + ctx.lineTo(150, 100) // Draw a line to (150, 100) + ctx.stroke() // Render the path + await snapshotImage(t) +}) + +test.todo('measureText') + +test('moveTo', async (t) => { + const { ctx } = t.context + ctx.beginPath() + ctx.moveTo(50, 50) // Begin first sub-path + ctx.lineTo(200, 50) + ctx.moveTo(50, 90) // Begin second sub-path + ctx.lineTo(280, 120) + ctx.stroke() + await snapshotImage(t) +}) + +test('putImageData', async (t) => { + const { ctx } = t.context + function putImageData( + imageData: ImageData, + dx: number, + dy: number, + dirtyX: number, + dirtyY: number, + dirtyWidth: number, + dirtyHeight: number, + ) { + const data = imageData.data + const height = imageData.height + const width = imageData.width + dirtyX = dirtyX || 0 + dirtyY = dirtyY || 0 + dirtyWidth = dirtyWidth !== undefined ? dirtyWidth : width + dirtyHeight = dirtyHeight !== undefined ? dirtyHeight : height + const limitBottom = dirtyY + dirtyHeight + const limitRight = dirtyX + dirtyWidth + for (let y = dirtyY; y < limitBottom; y++) { + for (let x = dirtyX; x < limitRight; x++) { + const pos = y * width + x + ctx.fillStyle = + 'rgba(' + + data[pos * 4 + 0] + + ',' + + data[pos * 4 + 1] + + ',' + + data[pos * 4 + 2] + + ',' + + data[pos * 4 + 3] / 255 + + ')' + ctx.fillRect(x + dx, y + dy, 1, 1) + } + } + } + + // Draw content onto the canvas + ctx.fillRect(0, 0, 100, 100) + // Create an ImageData object from it + const imagedata = ctx.getImageData(0, 0, 100, 100) + // use the putImageData function that illustrates how putImageData works + putImageData(imagedata, 150, 0, 50, 50, 25, 25) + + await snapshotImage(t) +}) + +test('quadraticCurveTo', async (t) => { + const { ctx } = t.context + // Quadratic Bézier curve + ctx.beginPath() + ctx.moveTo(50, 20) + ctx.quadraticCurveTo(230, 30, 50, 100) + ctx.stroke() + + // Start and end points + ctx.fillStyle = 'blue' + ctx.beginPath() + ctx.arc(50, 20, 5, 0, 2 * Math.PI) // Start point + ctx.arc(50, 100, 5, 0, 2 * Math.PI) // End point + ctx.fill() + + // Control point + ctx.fillStyle = 'red' + ctx.beginPath() + ctx.arc(230, 30, 5, 0, 2 * Math.PI) + ctx.fill() + await snapshotImage(t) +}) + +test('rect', async (t) => { + const { ctx } = t.context + ctx.fillStyle = 'yellow' + ctx.rect(10, 20, 150, 100) + ctx.fill() + await snapshotImage(t) +}) + +test.todo('resetTransform') + +test('save-restore', async (t) => { + const { ctx } = t.context + // Save the default state + ctx.save() + + ctx.fillStyle = 'green' + ctx.fillRect(10, 10, 100, 100) + + // Restore the default state + ctx.restore() + + ctx.fillRect(150, 40, 100, 100) + + await snapshotImage(t) +}) + +test.todo('rotate') + +test.todo('scale') + +test('setLineDash', async (t) => { + const { ctx } = t.context + // Dashed line + ctx.beginPath() + ctx.setLineDash([5, 15]) + ctx.moveTo(0, 50) + ctx.lineTo(300, 50) + ctx.stroke() + + // Solid line + ctx.beginPath() + ctx.setLineDash([]) + ctx.moveTo(0, 100) + ctx.lineTo(300, 100) + ctx.stroke() + await snapshotImage(t) +}) + +test('setTransform', async (t) => { + const { ctx } = t.context + ctx.setTransform(1, 0.2, 0.8, 1, 0, 0) + ctx.fillRect(0, 0, 100, 100) + await snapshotImage(t) +}) + +test('stroke', async (t) => { + const { ctx } = t.context + // First sub-path + ctx.lineWidth = 26 + ctx.strokeStyle = 'orange' + ctx.moveTo(20, 20) + ctx.lineTo(160, 20) + ctx.stroke() + + // Second sub-path + ctx.lineWidth = 14 + ctx.strokeStyle = 'green' + ctx.moveTo(20, 80) + ctx.lineTo(220, 80) + ctx.stroke() + + // Third sub-path + ctx.lineWidth = 4 + ctx.strokeStyle = 'pink' + ctx.moveTo(20, 140) + ctx.lineTo(280, 140) + ctx.stroke() + await snapshotImage(t) +}) + +test('stroke-and-filling', async (t) => { + const { ctx } = t.context + ctx.lineWidth = 16 + ctx.strokeStyle = 'red' + + // Stroke on top of fill + ctx.beginPath() + ctx.rect(25, 25, 100, 100) + ctx.fill() + ctx.stroke() + + // Fill on top of stroke + ctx.beginPath() + ctx.rect(175, 25, 100, 100) + ctx.stroke() + ctx.fill() + await snapshotImage(t) +}) + +test('strokeRect', async (t) => { + const { ctx } = t.context + ctx.shadowColor = '#d53' + ctx.shadowBlur = 20 + ctx.lineJoin = 'bevel' + ctx.lineWidth = 15 + ctx.strokeStyle = '#38f' + ctx.strokeRect(30, 30, 160, 90) + await snapshotImage(t) +}) + +test.todo('strokeText') + +test('transform', async (t) => { + const { ctx } = t.context + ctx.transform(1, 0.2, 0.8, 1, 0, 0) + ctx.fillRect(0, 0, 100, 100) + ctx.resetTransform() + ctx.fillRect(220, 0, 100, 100) + await snapshotImage(t) +}) + +test('translate', async (t) => { + const { ctx } = t.context + // Moved square + ctx.translate(110, 30) + ctx.fillStyle = 'red' + ctx.fillRect(0, 0, 80, 80) + + // Reset current transformation matrix to the identity matrix + ctx.setTransform(1, 0, 0, 1, 0, 0) + + // Unmoved square + ctx.fillStyle = 'gray' + ctx.fillRect(0, 0, 80, 80) + await snapshotImage(t) +}) diff --git a/__test__/failure/transform.png b/__test__/failure/transform.png new file mode 100644 index 0000000000000000000000000000000000000000..c699bc181a3b2edaf831f59c3faabd0b91578742 GIT binary patch literal 2686 zcmeHJ`BM{D9DkdT7}Hn?E((;v2=7xAo> zU`0UGL5d3XKya#At9T^>qDX_tp+!WH%SymV=-Vv#1Al}2%kE^}_wzp2dy}+4f1hz| zTQ&e-obOWc3IJ&ElLlB!`1&vD$p`o{OIYk1#DY&UYaI>%+s#+JC@3lGZT-Rn5-)U5 zbNG}Jh1LP*Z51zlU@IIO2A+x(A@s@94~LKcbBw5grX2gzAw!+(OXIwn#5dWkhnHm> z5;_L7_1&$Xm@U4o=k|DsKXSQ6ofV$QzV3*=iNu{FIw{chPI2%!fHyk6&^?k4E%@?h zg0GM;MKhUT?U_=<@Fr{IX<^5 zR~xgv5qB*(TMly18w5k?c>lD4oN$y-B7y0MC`|vI(U3=Q_tEChqQFjSqMpLGyUCXYa1@~&Q1_v#r4pz42-$0nQV{tPBNuC8c_TA~6vS^Dc_BaE z?35ygnwbB-w{kBzk$Iouksy#CH7y8pp(g4zQa95cX7t<%bVZ4v>|>!=>1fT6Xs8K4 z!O#QK9<9TBRBP1vCWv3GO*9i9wlpS?yhXc^!2M8I?OLNhIkCvoSyqUq8u1a?fKbDVphrmQJFAT0a}y6)VfcN^siN4LFFEI{5ju7n-?4 ztc*RLA}5K)^%(t3GqWenzY+x_4<5!4P=0xT!jQBC(|L>0%;hMHSOH;u(9#T@zFcP? z6?@0vjL8I8Q|K^{kYS83DU3KBup7^KWyzq$XM~KxSo5sI&+geu+e*gNg$|R67eF%S zx;eWh2bX=!BP4mmcG$b%?aam39*mF?B@}WLEKD{V^77*{UfE+!8y^}|7d)eHJl{`w zn-u1;B_^cDhBZC-ItB9#wln0t4Qq8+0Wx;%+^k0o5`l<5%rF2yVyQ` zEorZLm)aC#_&`;`dUI&Sy|t7#@pjOg>+baC4KwD9M7`juY7~yv&0uJQ7GJrmmzqRK zARXOHR}24(wUjy9aPZ_}Z6q(bD%(~OPOcFtPUzYJZo!Bc&!82frrNEI?9*35K83f2 z<4%NeS2x?R^lvr~c#%C*!E2+fm$OUXQ8prLjbt<(_sWhg)un}1!6S6u^;Z>~fpi@5 zx>_cZHOa1kS+%=bgu0E0N^hG#D)58H2M0I7sCr3@m!ti2VNyxV>I{vXN7|X332%@{ z-p0f@aKKauWUnh|ur!r$SWX;FX)a7@`_Ow2Q+qSu8Fi;eYg*JfWbwLZ!uD2*>Z(C~ z0C_@Kf%}kdAyq&!=%%3naBPH#Po1Z z&m2cYQ|IuA4alcXf{8@i1V)AN-By!~_GIY$^E}Zf9X6Qkf4*jbhzM&^>OybY zOeOSH@5`e0h5K>m&WhG;t&a<&kLqshUlKKz5HSgB@0%^xyp&+2HMkm%W?fi`vV^-4 zp$h9RR&DC{Ra9BZ06o|KRMUBBUgHp-)ijNxaNAqT% zKRTZhp{zsq!0YH5jK?-x`UB`zuku#%EqF?V2zNR_!+0VZU6HWLnjD^C1J-4_u;Gmp zSoIk1cXCL@@kWOiMR?MYt { t.is(ctx.globalCompositeOperation, 'xor') }) +test('imageSmoothingEnabled state should be ok', (t) => { + const { ctx } = t.context + t.is(ctx.imageSmoothingEnabled, true) + ctx.imageSmoothingEnabled = false + t.is(ctx.imageSmoothingEnabled, false) +}) + +test('imageSmoothingQuality state should be ok', (t) => { + const { ctx } = t.context + t.is(ctx.imageSmoothingQuality, 'low') + ctx.imageSmoothingQuality = 'high' + t.is(ctx.imageSmoothingQuality, 'high') +}) + test('lineCap state should be ok', (t) => { const { ctx } = t.context t.is(ctx.lineCap, 'butt') @@ -114,3 +128,12 @@ test('shadowOffsetY state should be ok', (t) => { ctx.shadowOffsetY = 10 t.is(ctx.shadowOffsetY, 10) }) + +test('lineDash state should be ok', (t) => { + const { ctx } = t.context + const lineDash = [1, 2, 4.5, 7] + ctx.setLineDash(lineDash) + t.deepEqual(ctx.getLineDash(), lineDash) +}) + +test.todo('getTransform') diff --git a/__test__/snapshots/createImageData.png b/__test__/snapshots/createImageData.png new file mode 100644 index 0000000000000000000000000000000000000000..45242e09453faac6e044f57420983bf9e3de99f1 GIT binary patch literal 1692 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;xPaumyX% zIEGZrd3!C8_fUX<>%m-B#YKk%1D)5he`+`|Q?Ouj{A^=ppgv;2|IBFHA9FrO?dAN_ zmMnL=p6i3$@9q2y40iqu3=R_*7#b8985kUaZfaryI)syffkTLaK|qCpLBV5G!Du*) nriT%hFNi5qM%4_l5Qr;d?sj5j_*NUZ3*>T7S3j3^P6Dr&aESc|YIxEM(u*aW)!C?XeLxTb%1A`;b`X&~jotz8|96}5X0xAp)3LZm2LB@RB zhWkzopr0It?q A0{{R3 literal 0 HcmV?d00001 diff --git a/__test__/snapshots/lineTo.png b/__test__/snapshots/lineTo.png new file mode 100644 index 0000000000000000000000000000000000000000..52ff23f96cc3f40cb183da61b7147c81579ed131 GIT binary patch literal 1315 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;vqew>(`O zLn`LHy=@rC;wa*JQG7k8K_jP61E)^{6A%d}G|frq6fj^eJ-_nRpQHQp@_}a17W}FF zp3cv}(D!}Y+v3}ox1HV@%+R2GH>djEx2h7&@(Ob&B01Q-svG&2aONwG9oTvT9a z=>*FAnQ$;90+nzGBP&q{DltG;(g9L317-?L$q|q#K2THON*=*10+|9=QV6pMWC~mf z+$|te;7Z`m{bnu2!jSYwg0>+(YWiph46_jUv40;U&(!kHPRZm+Ag_74`njxgN@xNA Dflz>9 literal 0 HcmV?d00001 diff --git a/__test__/snapshots/moveTo.png b/__test__/snapshots/moveTo.png new file mode 100644 index 0000000000000000000000000000000000000000..ae1a19ab6c458b32780e479d6fb6fa29a49dcbad GIT binary patch literal 1447 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;xPau*P}1 zIEGZrd3)0^=#Ya%+r#-2Hwt7|?df8_ByAyckUJ~iKQ)=LN0LJzVX|-8{N*R7)&KO{ z#{o2mw!muUQ#*x){dd>{mXy9@X82`rzShA>^(YTDZFO+x-^uUsLRV>;;719;*&RR5 zR`W6RT(|ko$hlVOQR(;Cqh?I|lpke(x2<7z*qXMJ|HVb4ZHga+{s&&D+3tGq-`~j( z;}|4-PBIpFMvJ_;-)+&?l%|nmbcdVaSbdFo(h0QetRue#W|@ zfq`LN>DfIW=Dl=#z{JR~q{KqM=9w)!H6v-%tkDn{sv)o-ynbEshe&}%$M%4{;OXk; Jvd$@?2>|n?xs?C_ literal 0 HcmV?d00001 diff --git a/__test__/snapshots/putImageData.png b/__test__/snapshots/putImageData.png new file mode 100644 index 0000000000000000000000000000000000000000..344f92c82b19ac0db6373671274c4914a86e047d GIT binary patch literal 1360 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;xPau(Er) zIEGZrd3)m^Z;OEdi{o#Xf0~DXGacCWViil-ytQ+ad#Vd2?W|^Cxc~jOEdxV@KLdlq z1O|o%1x5x2N1*XdEI>0k85lT(7#IXp7#I{hhJu0@w`>{s%Jp{`ir-=Q@tB$BURjX+ zox$SH9b1M6ymmAXCtxg(g3%Bd4FRGmdKI;Vst0O{IR AIRF3v literal 0 HcmV?d00001 diff --git a/__test__/snapshots/quadraticCurveTo.png b/__test__/snapshots/quadraticCurveTo.png new file mode 100644 index 0000000000000000000000000000000000000000..3b95af06608bbe5ffc25a85a3c8dab6b99915bf0 GIT binary patch literal 2473 zcmeHH?NgIi7C#AMh$tDmilPmU(`lWK5MWz^1jSYCBDheLBu~JlWW|@2pyDP0DnwIv ze5-*t?utqXRasJ!CoiNBgAp)wtJx~jLiPy>HiVW6_!vN?Q5p!@kPrK<|AFVrIrsd| z`JI=0=bp>SPG7QU)gk~`lCgc;E&yV{BtqE2fcmB4zutgaz~7p|3=2qA*xs`MWNyZ` zEzFAgaVPKWp?y^IkFK4!IeB>tF4cxUX=+|veKw}8rcCeUE)%4EzE$~6pZO#$KWA&$ zu5){qm3_JWhWFZ&rlO1FV-F;|K6tP__Fo$+8yq?kV%bnEkf+ zJHNL1)jq8uJkY~$i@h^Tr#V=ZCA)09Usppd2!-xPg8VEi+oo)M2IA#OR73R&zfv*7 z%inBz3phKi`yNMuqK>wzfDG#9gVt9haph$5D_wMgDH6&OE2|^yTl|A_@0XA7_O%`R zv*CbnG(vqxQ;`gAqAdU>SSe_uk>7XEFu~ekq44No8|@w=K8WAMZB4q}jII-><`RH& z7Gr=~M|0T7Sg&E?EBS01<3F-D>H@9 zBW zjaXy97g2}*XM7V@{1gY6bsfi6qpc{f&UthCSNxk|*JlMWR_5y31SnbQB%8J4Yq9ev zU$&`xjD)4A(rYApo+$=8Bm}N?EF0LDi&Sw;e{)x1r@?W-Oxtrc#5~d~2lXd9Wmlj3 z(8+KVkv_WK5enu#R79lp9`)S)I+jEDe36| z=gYSUXUD>T`=TqCN@ozom-Ln~U{}jjo?txVhUssD1<+Da&XPt`3Fe;1d;y>j_$nb5 zmwpCX_-?|`*X?m>?|?e_gsNdffI&XG2p)RexDNYbK4NMy#Ps)K?*`~NQ468=Vc>1^ zx`)yAbxs0+dTA1qB&`969IeNl!IH^ZQ|Wxk4N@gNh&(glw}Qwm1%o?}tgJ-JnP;T) zgAHH6Cvb9OT41z_eV%8%OTqk8eNMrL3?gvqDa_I8dFtV-m?tp*e9$!4Bg2ye=1Pl& zprewQ{q)MauFtSI%Lu?!S*CD@&^{CaWlOc-9&BJ1u*Q*1nt=^%3*pM`*K8vaw~9(M zYY*sp3F`JV^fmJ}^W*3rh|ujQLc>*GAnB5`k@ zBHY%mLq^%5;!q;%SctUH)RB zJ=k!Bst29WraDT_Y=#yYLt$z*=^1|sa!jg@vwYW%JsFAwZcVhy9p&>k_vjiD=_I)F zB7nVxidi(>4IUX9WKo@5jEy{vEuAmlE8H zxlkP|jvC0^7-r@11~}G{CWBs4WPA~Rq?v{u)~4ocDqedP%$ZlS(SrjI7#_m9=p2#C z)aqq>zFK<=%sq8A1NYDS`xsM2*gY&3BcY-*?}b1L&m@=$F=HpRpCp);0Vlz#7^i?$ z)3~0%MGTfPF;y`6`yK~xVmY*AYHWld@4kAz^M~~y{;1A&Wb~!qm)Kb)co$lJQsmxD zfQ};qD#RyQwK) Ky-mB7d-8wVG$N1y literal 0 HcmV?d00001 diff --git a/__test__/snapshots/rect.png b/__test__/snapshots/rect.png new file mode 100644 index 0000000000000000000000000000000000000000..e39188444faa42f3eba90eacb25cd2a0be57ecb6 GIT binary patch literal 1351 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;vqe|2$nB zLn`LHy|z)X!9ak;aQ44yGhK@V28X>qvWwSjNCs*l1{|fx!{zpe7cewVVtL96}5X0xF{l=o}8j#OA1)(GVB` cA&|3+QO|?<#AmCfHjr;UUHx3vIVCg!0MOb_SO5S3 literal 0 HcmV?d00001 diff --git a/__test__/snapshots/save-restore.png b/__test__/snapshots/save-restore.png new file mode 100644 index 0000000000000000000000000000000000000000..6d22f84b134cbff65e37bd7d16224c5dfb13e0df GIT binary patch literal 1438 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;xPau!efN zIEGZrd3(c=>yQD1>%kD4ihQ~2rKyh6FK0)le^g|VlVk>JLkBy~efxN__Kf_y2W$Qw zxyyK9_VHJY3Wv@OFA{W|*CN(Yb8z=~~GnRl!Oh8O!itAYpYG>HyEu0~Htqx{hj7-k{xW0!dL WWmeXS1z&wYKJ#?-b6Mw<&;$S{=4ti- literal 0 HcmV?d00001 diff --git a/__test__/snapshots/setLineDash.png b/__test__/snapshots/setLineDash.png new file mode 100644 index 0000000000000000000000000000000000000000..4bb2352f7ce069d46c54f5d899284960cdf3d7cb GIT binary patch literal 1163 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;vqePM$7~ zAr*7pUfalZKtaIepy;3ckLQDLEE8BH(=zi@*YRo7ftqLwHY`<@)c^i-;mYO9mzS?* zm$|fgUc=-sK#=#9wiYumcx_c#T*Y!=S)?t)zeVY`jI_3I)cny97+xXZ`H3N8k$`k7 S`<$B~k9oTKxvXXndbHT)a5i`%z;u-vev@z)xiwK)M>j6;GF zHm>e_##i?J-p!fi&%S%#liAsF+F>*Q_WGjoNi}nytX7$?@>Aoayyra@hKRM@D&>=F z=0AD(;`4r$pNaM1zkh#y6$GA!I-{td@L>LZu#q`wuHQ4>0<&N#6@6xOc z8<_eh@7euyuh%D=lap2YC&kSB%+J6u=UHZ+JyQ=8L&S#eY`gMU5ewFQd4>Z%>!cY@ zCI&tb0m@i3&t`KdiUP_kW?<;ro6n%KTjRs+Vw)2afs#8~`FR^s#98YMzcz~)a4|e^ zXy<7tc5Ms>v890QlzkcnTyBgEv&&yFcuW&!^*gA+kPzG_$#Al0@c~TA#)>LJex~R4eEHjJLU7v|f1w%Yx4Kn}1)Iz7s#c^-ed34&#gya}TZm zT0H6gWE-tNv!cyXzQ;Dae^w{JaBX=J(2HGv>n5=ujW1soqAQE|vu+rZ6iAvW5YLFSKcF_~z2cutgC? z*MXDE@)(Y}G;VF{U}ChmXvyvXG*u0#lQ%HoZ6_0BhO*VG)|GxeTo@DN6cTfH}FkQ|$dGrqdhM?&;_s+2O_;mB%yYs(X zYWMhls+=^v*q-CSEJ=pv$s#iEwyVs4XU}B7$ea-8cJORm+V{B)+d=YDk_@MlMgF{w zoOHa{o(06U&8ZZrnfc`99e&0QW^4}e(UPpd_~w*gc)jUbpaK_*MjFGK`%!L<8UhO@ zFmCXTbqBFJ7!Tav)x#91;K0b-@ICU8h5##*hyjDeH6f6~2h(^Mw9XiE0Rw1*!}Obf zS3FVooTpwn^-1ibUhv*%hjsmA!VFe9+29tEQzFd7038?x#d-2lEK)X2^7&wF&7z9)p7!*7h7#uFteP?~3Gyl}} zq)g^NV!~*$6Brm86c`y896{Qr-C%xT|9$J9<7^LP@hZe-4~j{n3Lqhh9u67%e=`{y zAHOTk&>(>~gn?1PkbXm+v0xrBJ~84H7S{L^%&6>W2n??fxbTtX&<&1vJ10vifIQ~u L>gTe~DWM4fwRLWZ literal 0 HcmV?d00001 diff --git a/__test__/snapshots/stroke.png b/__test__/snapshots/stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..8a9005edab39c4c9d8eaf2818b32fe0b5095e8cd GIT binary patch literal 1336 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;vqe?>$`{ zLn`LHy>l?P*+HP~V(gQ!7^d=H9>#fUwFS#tL^f+43^GYH{9n!5^DH5HKHHOPKtm9q zq5IWU=HunkSB-bB{}=jSlI6kP)Nf1-2Y#{u%@$%{5b$7Na8O`mU}%)P%KY2^s|&+| zS={JSr{bApZU@><{GXir^@f$fX(!AgBqa!UQil~2qd)&g*5VqqJ4av3?<;1g-*6_5fx%)v$elpf zF>wMN{$+)n#kR#&DtFxSU%wG$h|1OjhJq)|ai{JxzSx~UhoPVpBn%H6pquFeCaekP zm-%hDDxGQm%KYq~`8)y1Gxstu6xe}ch+dAMi%&-_84Ur95K#Zkx->%Y{Ljk@yFhZD Lu6{1-oD!MfMW3=1} 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, data, w * 4, x, y); + return result; + } + void skiac_surface_png_data(skiac_surface *c_surface, skiac_sk_data *data) { auto image = SURFACE_CAST->makeImageSnapshot(); @@ -285,6 +292,22 @@ extern "C" CANVAS_CAST->restore(); } + void skiac_canvas_write_pixels(skiac_canvas *c_canvas, int width, int height, uint8_t *pixels, size_t row_bytes, int x, int y) + { + auto info = SkImageInfo::Make(width, height, SkColorType::kRGBA_8888_SkColorType, SkAlphaType::kUnpremul_SkAlphaType); + CANVAS_CAST->writePixels(info, pixels, row_bytes, x, y); + } + + void skiac_canvas_write_pixels_dirty(skiac_canvas *c_canvas, int width, int height, uint8_t *pixels, size_t row_bytes, size_t length, float x, float y, float dirty_x, float dirty_y, float dirty_width, float dirty_height) + { + auto info = SkImageInfo::Make(width, height, SkColorType::kRGBA_8888_SkColorType, SkAlphaType::kUnpremul_SkAlphaType); + auto data = SkData::MakeFromMalloc(pixels, length); + auto image = SkImage::MakeRasterData(info, data, row_bytes); + auto src_rect = SkRect::MakeXYWH(dirty_x, dirty_y, dirty_width, dirty_height); + auto dst_rect = SkRect::MakeXYWH(x + dirty_x, y + dirty_y, dirty_width, dirty_height); + CANVAS_CAST->drawImageRect(image, src_rect, dst_rect, nullptr); + } + // Paint skiac_paint *skiac_paint_create() diff --git a/skia-c/skia_c.hpp b/skia-c/skia_c.hpp index cc526516..a230da7d 100644 --- a/skia-c/skia_c.hpp +++ b/skia-c/skia_c.hpp @@ -22,6 +22,7 @@ typedef struct skiac_path_effect skiac_path_effect; typedef struct skiac_matrix skiac_matrix; typedef struct skiac_mask_filter skiac_mask_filter; typedef struct skiac_data skiac_data; +typedef struct skiac_image skiac_image; struct skiac_transform { @@ -69,6 +70,7 @@ extern "C" int skiac_surface_get_width(skiac_surface *c_surface); int skiac_surface_get_height(skiac_surface *c_surface); void skiac_surface_read_pixels(skiac_surface *c_surface, skiac_surface_data *data); + bool skiac_surface_read_pixels_rect(skiac_surface *c_surface, uint8_t *data, int x, int y, int w, int h); void skiac_surface_png_data(skiac_surface *c_surface, skiac_sk_data *data); int skiac_surface_get_alpha_type(skiac_surface *c_surface); bool skiac_surface_save(skiac_surface *c_surface, const char *path); @@ -106,6 +108,8 @@ extern "C" void skiac_canvas_clip_path(skiac_canvas *c_canvas, skiac_path *c_path); void skiac_canvas_save(skiac_canvas *c_canvas); void skiac_canvas_restore(skiac_canvas *c_canvas); + void skiac_canvas_write_pixels(skiac_canvas *c_canvas, int width, int height, uint8_t *pixels, size_t row_bytes, int x, int y); + void skiac_canvas_write_pixels_dirty(skiac_canvas *c_canvas, int width, int height, uint8_t *pixels, size_t row_bytes, size_t length, float x, float y, float dirty_x, float dirty_y, float dirty_width, float dirty_height); // Paint skiac_paint *skiac_paint_create(); diff --git a/src/ctx.rs b/src/ctx.rs index 278870dd..7dea3524 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -8,6 +8,7 @@ use napi::*; use crate::error::SkError; use crate::gradient::CanvasGradient; +use crate::image::ImageData; use crate::pattern::Pattern; use crate::sk::*; use crate::state::Context2dRenderingState; @@ -21,7 +22,6 @@ impl From for Error { pub struct Context { pub(crate) surface: Surface, path: Path, - paint: Paint, pub(crate) states: Vec, } @@ -43,6 +43,12 @@ impl Context { Property::new(&env, "globalCompositeOperation")? .with_getter(get_global_composite_operation) .with_setter(set_global_composite_operation), + Property::new(&env, "imageSmoothingEnabled")? + .with_getter(get_image_smoothing_enabled) + .with_setter(set_image_smoothing_enabled), + Property::new(&env, "imageSmoothingQuality")? + .with_getter(get_image_smoothing_quality) + .with_setter(set_image_smoothing_quality), Property::new(&env, "lineCap")? .with_setter(set_line_cap) .with_getter(get_line_cap), @@ -88,8 +94,12 @@ impl Context { Property::new(&env, "moveTo")?.with_method(move_to), Property::new(&env, "fill")?.with_method(fill), Property::new(&env, "fillRect")?.with_method(fill_rect), + Property::new(&env, "_getImageData")?.with_method(get_image_data), + Property::new(&env, "getLineDash")?.with_method(get_line_dash), + Property::new(&env, "putImageData")?.with_method(put_image_data), Property::new(&env, "quadraticCurveTo")?.with_method(quadratic_curve_to), Property::new(&env, "rect")?.with_method(rect), + Property::new(&env, "resetTransform")?.with_method(reset_transform), Property::new(&env, "restore")?.with_method(restore), Property::new(&env, "save")?.with_method(save), Property::new(&env, "scale")?.with_method(scale), @@ -97,6 +107,7 @@ impl Context { Property::new(&env, "stroke")?.with_method(stroke), Property::new(&env, "strokeRect")?.with_method(stroke_rect), Property::new(&env, "translate")?.with_method(translate), + Property::new(&env, "transform")?.with_method(transform), // getter setter method Property::new(&env, "getTransform")?.with_method(get_current_transform), Property::new(&env, "setTransform")?.with_method(set_current_transform), @@ -113,7 +124,6 @@ impl Context { Ok(Context { surface, path: Path::new(), - paint: Paint::default(), states, }) } @@ -138,9 +148,7 @@ impl Context { pub fn restore(&mut self) { self.surface.restore(); if self.states.len() > 1 { - if let Some(state) = self.states.pop() { - mem::drop(state); - }; + mem::drop(self.states.pop().unwrap()); } } @@ -240,20 +248,21 @@ impl Context { #[inline(always)] fn fill_paint(&self) -> result::Result { - let mut paint = self.paint.clone(); + let current_paint = &self.states.last().unwrap().paint; + let mut paint = current_paint.clone(); paint.set_style(PaintStyle::Fill); let last_state = self.states.last().unwrap(); match &last_state.fill_style { Pattern::Color(c, _) => { let mut color = c.clone(); color.alpha = - ((color.alpha as f32) * (self.paint.get_alpha() as f32 / 255.0)).round() as u8; + ((color.alpha as f32) * (current_paint.get_alpha() as f32 / 255.0)).round() as u8; paint.set_color(color.red, color.green, color.blue, color.alpha); } Pattern::Gradient(g) => { let current_transform = self.surface.canvas.get_transform(); let shader = g.get_shader(¤t_transform)?; - paint.set_color(0, 0, 0, self.paint.get_alpha()); + paint.set_color(0, 0, 0, current_paint.get_alpha()); paint.set_shader(&shader); } // TODO, image pattern @@ -272,10 +281,11 @@ impl Context { #[inline(always)] fn stroke_paint(&self) -> result::Result { - let mut paint = self.paint.clone(); + let current_paint = &self.states.last().unwrap().paint; + let mut paint = current_paint.clone(); paint.set_style(PaintStyle::Stroke); let last_state = self.states.last().unwrap(); - let global_alpha = self.paint.get_alpha(); + let global_alpha = current_paint.get_alpha(); match &last_state.stroke_style { Pattern::Color(c, _) => { let mut color = c.clone(); @@ -306,18 +316,25 @@ impl Context { fn shadow_paint(&self, paint: &Paint) -> Option { let alpha = paint.get_alpha(); let last_state = self.states.last().unwrap(); - let mut shadow_alpha = last_state.shadow_color.alpha; - shadow_alpha = shadow_alpha * alpha; + let shadow_color = &last_state.shadow_color; + let mut shadow_alpha = shadow_color.alpha; + shadow_alpha = ((shadow_alpha as f32) * (alpha as f32 / 255.0)) as u8; if shadow_alpha == 0 { return None; } if last_state.shadow_blur == 0f32 - || last_state.shadow_offset_x == 0f32 - || last_state.shadow_offset_y == 0f32 + && last_state.shadow_offset_x == 0f32 + && last_state.shadow_offset_y == 0f32 { return None; } let mut shadow_paint = paint.clone(); + shadow_paint.set_color( + shadow_color.red, + shadow_color.green, + shadow_color.blue, + shadow_color.alpha, + ); shadow_paint.set_alpha(shadow_alpha); let blur_effect = MaskFilter::make_blur(last_state.shadow_blur / 2f32)?; shadow_paint.set_mask_filter(&blur_effect); @@ -656,7 +673,12 @@ fn set_miter_limit(ctx: CallContext) -> Result { let this = ctx.this_unchecked::(); let context_2d = ctx.env.unwrap::(&this)?; let miter: f64 = ctx.get::(0)?.try_into()?; - context_2d.paint.set_stroke_miter(miter as f32); + context_2d + .states + .last_mut() + .unwrap() + .paint + .set_stroke_miter(miter as f32); ctx.env.get_undefined() } @@ -667,7 +689,7 @@ fn get_miter_limit(ctx: CallContext) -> Result { ctx .env - .create_double(context_2d.paint.get_stroke_miter() as f64) + .create_double(context_2d.states.last().unwrap().paint.get_stroke_miter() as f64) } #[js_function(4)] @@ -700,10 +722,118 @@ fn fill_rect(ctx: CallContext) -> Result { ctx.env.get_undefined() } +#[js_function(4)] +fn get_image_data(ctx: CallContext) -> Result { + let this = ctx.this_unchecked::(); + let context_2d = ctx.env.unwrap::(&this)?; + let x: u32 = ctx.get::(0)?.try_into()?; + let y: u32 = ctx.get::(1)?.try_into()?; + let width: u32 = ctx.get::(2)?.try_into()?; + let height: u32 = ctx.get::(3)?.try_into()?; + let pixels = context_2d + .surface + .read_pixels(x, y, width, height) + .ok_or_else(|| { + Error::new( + Status::GenericFailure, + format!("Read pixels from canvas failed"), + ) + })?; + let length = pixels.len(); + ctx.env.create_arraybuffer_with_data(pixels).and_then(|ab| { + ab.into_raw() + .into_typedarray(TypedArrayType::Uint8Clamped, length, 0) + }) +} + +#[js_function] +fn get_line_dash(ctx: CallContext) -> Result { + let this = ctx.this_unchecked::(); + let context_2d = ctx.env.unwrap::(&this)?; + + let line_dash_list = &context_2d.states.last().unwrap().line_dash_list; + + let mut arr = ctx.env.create_array_with_length(line_dash_list.len())?; + + for (index, a) in line_dash_list.iter().enumerate() { + arr.set_element(index as u32, ctx.env.create_double(*a as f64)?)?; + } + Ok(arr) +} + +#[js_function(7)] +fn put_image_data(ctx: CallContext) -> Result { + let this = ctx.this_unchecked::(); + let context_2d = ctx.env.unwrap::(&this)?; + + let image_data_js = ctx.get::(0)?; + let image_data = ctx.env.unwrap::(&image_data_js)?; + let dx: u32 = ctx.get::(1)?.try_into()?; + let dy: u32 = ctx.get::(2)?.try_into()?; + if ctx.length == 3 { + context_2d.surface.canvas.write_pixels(image_data, dx, dy); + } else { + let mut dirty_x: f64 = ctx.get::(3)?.try_into()?; + let mut dirty_y = if ctx.length >= 5 { + ctx.get::(4)?.try_into()? + } else { + 0f64 + }; + let mut dirty_width = if ctx.length >= 6 { + ctx.get::(5)?.try_into()? + } else { + image_data.width as f64 + }; + let mut dirty_height = if ctx.length == 7 { + ctx.get::(6)?.try_into()? + } else { + image_data.height as f64 + }; + // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata + if dirty_width < 0f64 { + dirty_x = dirty_x + dirty_width; + dirty_width = dirty_width.abs(); + } + if dirty_height < 0f64 { + dirty_y = dirty_y + dirty_height; + dirty_height = dirty_height.abs(); + } + if dirty_x < 0f64 { + dirty_width = dirty_width + dirty_x; + dirty_x = 0f64; + } + if dirty_y < 0f64 { + dirty_height = dirty_height + dirty_y; + dirty_y = 0f64; + } + if dirty_width <= 0f64 || dirty_height <= 0f64 { + return ctx.env.get_undefined(); + } + let inverted = context_2d.surface.canvas.get_transform().invert(); + context_2d.surface.canvas.save(); + if let Some(inverted) = inverted { + context_2d.surface.canvas.concat(inverted); + }; + context_2d.surface.canvas.write_pixels_dirty( + image_data, + dx, + dy, + dirty_x, + dirty_y, + dirty_width, + dirty_height, + ); + context_2d.surface.canvas.restore(); + }; + + ctx.env.get_undefined() +} + #[js_function(1)] fn set_global_alpha(ctx: CallContext) -> Result { let this = ctx.this_unchecked::(); let context_2d = ctx.env.unwrap::(&this)?; + let alpha: f64 = ctx.get::(0)?.try_into()?; if alpha < 0.0 || alpha > 1.0 { @@ -716,7 +846,12 @@ fn set_global_alpha(ctx: CallContext) -> Result { )); } - context_2d.paint.set_alpha((alpha * 255.0) as u8); + context_2d + .states + .last_mut() + .unwrap() + .paint + .set_alpha((alpha * 255.0) as u8); ctx.env.get_undefined() } @@ -727,7 +862,7 @@ fn get_global_alpha(ctx: CallContext) -> Result { ctx .env - .create_double((context_2d.paint.get_alpha() as f64) / 255.0) + .create_double((context_2d.states.last().unwrap().paint.get_alpha() as f64) / 255.0) } #[js_function(1)] @@ -737,6 +872,9 @@ fn set_global_composite_operation(ctx: CallContext) -> Result { let blend_string = ctx.get::(0)?.into_utf8()?; context_2d + .states + .last_mut() + .unwrap() .paint .set_blend_mode(BlendMode::from_str(blend_string.as_str()?).map_err(Error::from)?); @@ -748,9 +886,70 @@ fn get_global_composite_operation(ctx: CallContext) -> Result { let this = ctx.this_unchecked::(); let context_2d = ctx.env.unwrap::(&this)?; + ctx.env.create_string( + context_2d + .states + .last() + .unwrap() + .paint + .get_blend_mode() + .as_str(), + ) +} + +#[js_function(1)] +fn set_image_smoothing_enabled(ctx: CallContext) -> Result { + let this = ctx.this_unchecked::(); + let context_2d = ctx.env.unwrap::(&this)?; + let enabled = ctx.get::(0)?; + + context_2d + .states + .last_mut() + .unwrap() + .image_smoothing_enabled = enabled.get_value()?; + + ctx.env.get_undefined() +} + +#[js_function] +fn get_image_smoothing_enabled(ctx: CallContext) -> Result { + let this = ctx.this_unchecked::(); + let context_2d = ctx.env.unwrap::(&this)?; + ctx .env - .create_string(context_2d.paint.get_blend_mode().as_str()) + .get_boolean(context_2d.states.last().unwrap().image_smoothing_enabled) +} + +#[js_function(1)] +fn set_image_smoothing_quality(ctx: CallContext) -> Result { + let this = ctx.this_unchecked::(); + let context_2d = ctx.env.unwrap::(&this)?; + let quality = ctx.get::(0)?.into_utf8()?; + + context_2d + .states + .last_mut() + .unwrap() + .image_smoothing_quality = FilterQuality::from_str(quality.as_str()?).map_err(Error::from)?; + + ctx.env.get_undefined() +} + +#[js_function] +fn get_image_smoothing_quality(ctx: CallContext) -> Result { + let this = ctx.this_unchecked::(); + let context_2d = ctx.env.unwrap::(&this)?; + + ctx.env.create_string( + context_2d + .states + .last() + .unwrap() + .image_smoothing_quality + .as_str(), + ) } #[js_function] @@ -887,14 +1086,50 @@ fn translate(ctx: CallContext) -> Result { ctx.env.get_undefined() } +#[js_function(6)] +fn transform(ctx: CallContext) -> Result { + let a: f64 = ctx.get::(0)?.try_into()?; + let b: f64 = ctx.get::(1)?.try_into()?; + let c: f64 = ctx.get::(2)?.try_into()?; + let d: f64 = ctx.get::(3)?.try_into()?; + let e: f64 = ctx.get::(4)?.try_into()?; + let f: f64 = ctx.get::(5)?.try_into()?; + + let this = ctx.this_unchecked::(); + let context_2d = ctx.env.unwrap::(&this)?; + + let new_transform = Transform::new(a as f32, b as f32, c as f32, d as f32, e as f32, f as f32); + let inverted = new_transform + .invert() + .ok_or_else(|| Error::new(Status::InvalidArg, "Invalid transform".to_owned()))?; + context_2d.path.transform(&inverted); + context_2d.surface.canvas.concat(new_transform); + ctx.env.get_undefined() +} + +#[js_function] +fn reset_transform(ctx: CallContext) -> Result { + let this = ctx.this_unchecked::(); + let context_2d = ctx.env.unwrap::(&this)?; + + context_2d.surface.canvas.reset_transform(); + ctx.env.get_undefined() +} + #[js_function] fn get_line_cap(ctx: CallContext) -> Result { let this = ctx.this_unchecked::(); let context_2d = ctx.env.unwrap::(&this)?; - ctx - .env - .create_string(context_2d.paint.get_stroke_cap().as_str()) + ctx.env.create_string( + context_2d + .states + .last() + .unwrap() + .paint + .get_stroke_cap() + .as_str(), + ) } #[js_function(1)] @@ -906,6 +1141,9 @@ fn set_line_cap(ctx: CallContext) -> Result { let context_2d = ctx.env.unwrap::(&this)?; context_2d + .states + .last_mut() + .unwrap() .paint .set_stroke_cap(StrokeCap::from_str(line_cap.as_str()?)?); @@ -940,9 +1178,15 @@ fn get_line_join(ctx: CallContext) -> Result { let this = ctx.this_unchecked::(); let context_2d = ctx.env.unwrap::(&this)?; - ctx - .env - .create_string(context_2d.paint.get_stroke_join().as_str()) + ctx.env.create_string( + context_2d + .states + .last() + .unwrap() + .paint + .get_stroke_join() + .as_str(), + ) } #[js_function(1)] @@ -954,6 +1198,9 @@ fn set_line_join(ctx: CallContext) -> Result { let context_2d = ctx.env.unwrap::(&this)?; context_2d + .states + .last_mut() + .unwrap() .paint .set_stroke_join(StrokeJoin::from_str(line_join.as_str()?)?); @@ -967,7 +1214,7 @@ fn get_line_width(ctx: CallContext) -> Result { ctx .env - .create_double(context_2d.paint.get_stroke_width() as f64) + .create_double(context_2d.states.last().unwrap().paint.get_stroke_width() as f64) } #[js_function(1)] @@ -977,7 +1224,12 @@ fn set_line_width(ctx: CallContext) -> Result { let this = ctx.this_unchecked::(); let context_2d = ctx.env.unwrap::(&this)?; - context_2d.paint.set_stroke_width(width as f32); + context_2d + .states + .last_mut() + .unwrap() + .paint + .set_stroke_width(width as f32); ctx.env.get_undefined() } @@ -1071,9 +1323,8 @@ fn set_shadow_blur(ctx: CallContext) -> Result { let this = ctx.this_unchecked::(); let context_2d = ctx.env.unwrap::(&this)?; - let last_state = context_2d.states.last_mut().unwrap(); - last_state.shadow_blur = blur as f32; + context_2d.states.last_mut().unwrap().shadow_blur = blur as f32; ctx.env.get_undefined() } diff --git a/src/image.rs b/src/image.rs index 3d778e4b..22815241 100644 --- a/src/image.rs +++ b/src/image.rs @@ -5,9 +5,9 @@ use napi::*; #[derive(Debug, Clone)] pub struct ImageData { - width: u32, - height: u32, - data: *mut u8, + pub(crate) width: u32, + pub(crate) height: u32, + pub(crate) data: *mut u8, } impl Drop for ImageData { diff --git a/src/sk.rs b/src/sk.rs index d7497d3a..4c18caee 100644 --- a/src/sk.rs +++ b/src/sk.rs @@ -6,6 +6,7 @@ use std::slice; use std::str::FromStr; use crate::error::SkError; +use crate::image::ImageData; mod ffi { #[repr(C)] @@ -124,6 +125,15 @@ mod ffi { pub fn skiac_surface_read_pixels(surface: *mut skiac_surface, data: *mut skiac_surface_data); + pub fn skiac_surface_read_pixels_rect( + surface: *mut skiac_surface, + data: *mut u8, + x: i32, + y: i32, + w: i32, + h: i32, + ) -> bool; + pub fn skiac_surface_png_data(surface: *mut skiac_surface, data: *mut skiac_sk_data); pub fn skiac_surface_get_alpha_type(surface: *mut skiac_surface) -> i32; @@ -190,6 +200,31 @@ mod ffi { pub fn skiac_canvas_restore(canvas: *mut skiac_canvas); + pub fn skiac_canvas_write_pixels( + canvas: *mut skiac_canvas, + width: i32, + height: i32, + pixels: *const u8, + row_bytes: usize, + x: i32, + y: i32, + ); + + pub fn skiac_canvas_write_pixels_dirty( + canvas: *mut skiac_canvas, + width: i32, + height: i32, + pixels: *const u8, + row_bytes: usize, + length: usize, + x: f32, + y: f32, + dirty_x: f32, + dirty_y: f32, + dirty_width: f32, + dirty_height: f32, + ); + pub fn skiac_paint_create() -> *mut skiac_paint; pub fn skiac_paint_clone(source: *mut skiac_paint) -> *mut skiac_paint; @@ -794,8 +829,6 @@ pub struct Surface { } impl Surface { - // TODO: use AlphaType - #[inline] pub fn new_rgba(width: u32, height: u32) -> Option { unsafe { Self::from_ptr(ffi::skiac_surface_create_rgba(width as i32, height as i32)) } @@ -868,6 +901,26 @@ impl Surface { } } + #[inline] + pub fn read_pixels(&self, x: u32, y: u32, width: u32, height: u32) -> Option> { + let mut result = vec![0; (width * height * 4) as usize]; + let status = unsafe { + ffi::skiac_surface_read_pixels_rect( + self.ptr, + result.as_mut_ptr(), + x as i32, + y as i32, + width as i32, + height as i32, + ) + }; + if status { + Some(result) + } else { + None + } + } + #[inline] pub fn data(&self) -> Option { unsafe { @@ -1181,6 +1234,50 @@ impl Canvas { ffi::skiac_canvas_restore(self.0); } } + + #[inline] + pub fn write_pixels(&mut self, image: &ImageData, x: u32, y: u32) { + unsafe { + ffi::skiac_canvas_write_pixels( + self.0, + image.width as i32, + image.height as i32, + image.data, + (image.width * 4) as usize, + x as i32, + y as i32, + ); + } + } + + #[inline] + pub fn write_pixels_dirty( + &mut self, + image: &ImageData, + x: u32, + y: u32, + dirty_x: f64, + dirty_y: f64, + dirty_width: f64, + dirty_height: f64, + ) { + unsafe { + ffi::skiac_canvas_write_pixels_dirty( + self.0, + image.width as i32, + image.height as i32, + image.data, + (image.width * 4) as usize, + (image.width * image.height * 4) as usize, + x as f32, + y as f32, + dirty_x as f32, + dirty_y as f32, + dirty_width as f32, + dirty_height as f32, + ) + } + } } #[derive(Debug)] @@ -1258,7 +1355,7 @@ impl Paint { } #[inline] - pub fn get_stroke_width(&mut self) -> f32 { + pub fn get_stroke_width(&self) -> f32 { unsafe { ffi::skiac_paint_get_stroke_width(self.0) } } @@ -1801,6 +1898,39 @@ impl Transform { i += 2; } } + + #[inline] + /// | A B C | + /// | D E F | + /// | 0 0 1 | + /// [interface.js](skia/modules/canvaskit/interface.js) + pub fn invert(&self) -> Option { + let m = [ + self.a, self.b, self.c, self.d, self.e, self.f, 0f32, 0f32, 1f32, + ]; + // Find the determinant by the sarrus rule. https://en.wikipedia.org/wiki/Rule_of_Sarrus + let det = m[0] * m[4] * m[8] + m[1] * m[5] * m[6] + m[2] * m[3] * m[7] + - m[2] * m[4] * m[6] + - m[1] * m[3] * m[8] + - m[0] * m[5] * m[7]; + if det == 0f32 { + return None; + } + // Return the inverse by the formula adj(m)/det. + // adj (adjugate) of a 3x3 is the transpose of it's cofactor matrix. + // a cofactor matrix is a matrix where each term is +-det(N) where matrix N is the 2x2 formed + // by removing the row and column we're currently setting from the source. + // the sign alternates in a checkerboard pattern with a `+` at the top left. + // that's all been combined here into one expression. + Some(Transform { + a: (m[4] * m[8] - m[5] * m[7]) / det, + b: (m[2] * m[7] - m[1] * m[8]) / det, + c: (m[1] * m[5] - m[2] * m[4]) / det, + d: (m[5] * m[6] - m[3] * m[8]) / det, + e: (m[0] * m[8] - m[2] * m[6]) / det, + f: (m[2] * m[3] - m[0] * m[5]) / det, + }) + } } impl Default for Transform {