From 01840603ea2b08115f7ab05bd3c583206b9015ef Mon Sep 17 00:00:00 2001 From: Jonathan Durand <38976316+jdugh@users.noreply.github.com> Date: Thu, 23 Oct 2025 18:23:40 +0200 Subject: [PATCH] Fix: copySheet() now properly copies sheets with comments (#207) - Copy comment-related files (comments.xml, threadedComment.xml, vmlDrawing.vml) - Generate unique UUIDs for copied comments while preserving personId - Add Content-Types declarations for new files - Preserve UTF-8 encoding using binary mode --- src/index.js | 106 +++++++++++++++++- test/crud-test.ts | 78 +++++++++++++ .../test-copy-sheet-with-comments.xlsx | Bin 0 -> 14485 bytes 3 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 test/templates/test-copy-sheet-with-comments.xlsx diff --git a/src/index.js b/src/index.js index 7158c52..e461e30 100755 --- a/src/index.js +++ b/src/index.js @@ -104,16 +104,23 @@ class Workbook { // Copy sheet file self.archive.file(arcName, etree.tostring(sheet.root)); self.archive.files[arcName].options.binary = binary; + + // Add content type for the new sheet + var sheetContentType = etree.SubElement(self.contentTypes, 'Override'); + sheetContentType.attrib.PartName = '/' + arcName; + sheetContentType.attrib.ContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'; + // copy sheet name in workbook var newSheet = etree.SubElement(self.workbook.find('sheets'), 'sheet'); - newSheet.attrib.name = copyName || 'Sheet' + newSheetIndex; + const finalSheetName = copyName || 'Sheet' + newSheetIndex; + newSheet.attrib.name = finalSheetName; newSheet.attrib.sheetId = newSheetIndex; newSheet.attrib['r:id'] = 'rId' + newSheetIndex; // Copy definedName if any self.workbook.findall('definedNames/definedName').forEach(element => { if (element.text && element.text.split("!").length && element.text.split("!")[0] == sheetName) { var newDefinedName = etree.SubElement(self.workbook.find('definedNames'), 'definedName', element.attrib); - newDefinedName.text = `${copyName}!${element.text.split("!")[1]}`; + newDefinedName.text = `${finalSheetName}!${element.text.split("!")[1]}`; newDefinedName.attrib.localSheetId = newSheetIndex - 1; } }); @@ -122,11 +129,85 @@ class Workbook { newRel.attrib.Type = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'; newRel.attrib.Target = fileName; - //Copy rels sheet - TODO : Maybe we can copy also the 'Target' files in rels, but Excel make this automaticly + // Copy sheet relationships and their target files + var sourceRels = self.loadSheetRels(sheet.filename); var relFileName = 'worksheets' + '/_rels/' + 'sheet' + newSheetIndex + '.xml.rels'; var relArcName = self.prefix + '/' + relFileName; - self.archive.file(relArcName, etree.tostring(self.loadSheetRels(sheet.filename).root)); - self.archive.files[relArcName].options.binary = true; + var newRelsRoot = self.cloneElement(sourceRels.root, true); + + // Generate a new UUID for comments (shared between comments.xml and threadedComment.xml) + var newCommentUuid = self.generateUUID(); + + // Process each relationship to copy target files with unique names + sourceRels.root.findall('Relationship').forEach(function(rel, index) { + var relType = rel.attrib.Type; + var target = rel.attrib.Target; + + // Relationship types that require copying the target file + var needsFileCopy = [ + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments', + 'http://schemas.microsoft.com/office/2017/10/relationships/threadedComment', + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing' + ]; + + if (needsFileCopy.indexOf(relType) !== -1) { + var sheetDirectory = path.dirname(sheet.filename); + var sourceFilePath = path.join(sheetDirectory, target).replace(/\\/g, '/'); + var sourceFile = self.archive.file(sourceFilePath); + + if (sourceFile) { + // Generate unique file name based on newSheetIndex + var fileExtension = path.extname(target); + var fileBaseName = path.basename(target, fileExtension); + var fileDir = path.dirname(target); + var baseNameWithoutNumber = fileBaseName.replace(/\d+$/, ''); + var newFileName = baseNameWithoutNumber + newSheetIndex + fileExtension; + var newTarget = path.join(fileDir, newFileName).replace(/\\/g, '/'); + var newFilePath = path.join(sheetDirectory, newTarget).replace(/\\/g, '/'); + + // Copy file in binary mode to preserve UTF-8 encoding + var binaryContent = sourceFile.asBinary(); + + // Apply file-specific transformations + if (relType === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing') { + // Update VML data attribute to make it unique per sheet + binaryContent = binaryContent.replace(/data="\d+"/, 'data="' + newSheetIndex + '"'); + + } else if (relType === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments') { + // Replace comment UUIDs in both tc= and xr:uid= attributes + var uuidWithoutBraces = newCommentUuid.replace(/[{}]/g, ''); + binaryContent = binaryContent.replace(/(tc=\{)[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}(\}<\/author>)/gi, '$1' + uuidWithoutBraces + '$2'); + binaryContent = binaryContent.replace(/(xr:uid="\{)[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}(\}\")/gi, '$1' + uuidWithoutBraces + '$2'); + + var commentsContentType = etree.SubElement(self.contentTypes, 'Override'); + commentsContentType.attrib.PartName = '/' + newFilePath; + commentsContentType.attrib.ContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml'; + + } else if (relType === 'http://schemas.microsoft.com/office/2017/10/relationships/threadedComment') { + // Replace only the comment UUID in id= attribute (preserve personId) + var uuidWithoutBraces = newCommentUuid.replace(/[{}]/g, ''); + binaryContent = binaryContent.replace(/(\sid="\{)[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}(\}")/gi, '$1' + uuidWithoutBraces + '$2'); + + var threadedCommentContentType = etree.SubElement(self.contentTypes, 'Override'); + threadedCommentContentType.attrib.PartName = '/' + newFilePath; + threadedCommentContentType.attrib.ContentType = 'application/vnd.ms-excel.threadedcomments+xml'; + } + + self.archive.file(newFilePath, binaryContent); + self.archive.files[newFilePath].options.binary = true; // Force binary mode to preserve UTF-8 encoding + + // Update relationship target in the copied rels file + var newRelInRels = newRelsRoot.findall('Relationship')[index]; + if (newRelInRels) { + newRelInRels.attrib.Target = newTarget; + } + } + } + }); + + self.archive.file(relArcName, etree.tostring(newRelsRoot)); + self.archive.files[relArcName].options.binary = binary; + self.archive.file('[Content_Types].xml', etree.tostring(self.contentTypes)); self._rebuild(); return self; @@ -901,6 +982,21 @@ class Workbook { return str; } + // Generate a UUID v4 for comment IDs + generateUUID() { + // Format: {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} + var hexDigits = '0123456789ABCDEF'; + var uuid = '{'; + for (var i = 0; i < 36; i++) { + if (i === 8 || i === 13 || i === 18 || i === 23) { + uuid += '-'; + } else { + uuid += hexDigits[Math.floor(Math.random() * 16)]; + } + } + uuid += '}'; + return uuid; + } // Is ref a range? isRange(ref) { return ref.indexOf(':') !== -1; diff --git a/test/crud-test.ts b/test/crud-test.ts index db3c654..b772206 100644 --- a/test/crud-test.ts +++ b/test/crud-test.ts @@ -1530,6 +1530,84 @@ describe("CRUD operations", function() { done(); }); }); + + it("copies sheets with comments", function(done) { + + fs.readFile(path.join(__dirname, "templates", "test-copy-sheet-with-comments.xlsx"), function(err, data) { + expect(err).toBeNull(); + try { + var t = new XlsxTemplate(data); + t.copySheet("Feuil1", "Feuil3", true); + var newData = t.generate(); + + // Verify that each sheet has its own comment files + var sheet1Rels = etree.parse(t.archive.file("xl/worksheets/_rels/sheet1.xml.rels").asText()).getroot(); + var sheet3Rels = etree.parse(t.archive.file("xl/worksheets/_rels/sheet3.xml.rels").asText()).getroot(); + var sheet1CommentsRel = sheet1Rels.find("./Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments']"); + var sheet3CommentsRel = sheet3Rels.find("./Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments']"); + var sheet1ThreadedCommentsRel = sheet1Rels.find("./Relationship[@Type='http://schemas.microsoft.com/office/2017/10/relationships/threadedComment']"); + var sheet3ThreadedCommentsRel = sheet3Rels.find("./Relationship[@Type='http://schemas.microsoft.com/office/2017/10/relationships/threadedComment']"); + var sheet1VmlDrawingRel = sheet1Rels.find("./Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing']"); + var sheet3VmlDrawingRel = sheet3Rels.find("./Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing']"); + + // Relationship Target must be different + expect(sheet1CommentsRel.attrib.Target).not.toEqual(sheet3CommentsRel.attrib.Target); + expect(sheet1ThreadedCommentsRel.attrib.Target).not.toEqual(sheet3ThreadedCommentsRel.attrib.Target); + expect(sheet1VmlDrawingRel.attrib.Target).not.toEqual(sheet3VmlDrawingRel.attrib.Target); + + // Verify copied files exist with correct numeric naming + expect(t.archive.file("xl/comments3.xml")).not.toBeNull(); + expect(t.archive.file("xl/threadedComments/threadedComment3.xml")).not.toBeNull(); + expect(t.archive.file("xl/drawings/vmlDrawing3.vml")).not.toBeNull(); + + // Verify Content Types are registered + var contentTypes = etree.parse(t.archive.file("[Content_Types].xml").asText()).getroot(); + expect(contentTypes.find("./Override[@PartName='/xl/worksheets/sheet3.xml']")).not.toBeNull(); + expect(contentTypes.find("./Override[@PartName='/xl/comments3.xml']")).not.toBeNull(); + expect(contentTypes.find("./Override[@PartName='/xl/threadedComments/threadedComment3.xml']")).not.toBeNull(); + + + // Verify UUIDs are consistent across comment files + var comments3Content = t.archive.file("xl/comments3.xml").asText(); + var threadedComment3Content = t.archive.file("xl/threadedComments/threadedComment3.xml").asText(); + var threadedComment1Content = t.archive.file("xl/threadedComments/threadedComment1.xml").asText(); + + var commentsUuidMatch = comments3Content.match(/xr:uid="\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}"/i); + expect(commentsUuidMatch).not.toBeNull(); + var commentsUuid = commentsUuidMatch ? commentsUuidMatch[1] : null; + + var commentsAuthorUuidMatch = comments3Content.match(/tc=\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}<\/author>/i); + expect(commentsAuthorUuidMatch).not.toBeNull(); + var commentsAuthorUuid = commentsAuthorUuidMatch ? commentsAuthorUuidMatch[1] : null; + + var threadedCommentUuidMatch = threadedComment3Content.match(/\sid="\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}"/i); + expect(threadedCommentUuidMatch).not.toBeNull(); + var threadedCommentUuid = threadedCommentUuidMatch ? threadedCommentUuidMatch[1] : null; + + // All three UUIDs must match + expect(commentsUuid).toEqual(commentsAuthorUuid); + expect(commentsUuid).toEqual(threadedCommentUuid); + + // Verify personId is preserved from original sheet (references persons/person.xml) + var personIdMatch = threadedComment3Content.match(/personId="\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}"/i); + expect(personIdMatch).not.toBeNull(); + var personId = personIdMatch ? personIdMatch[1] : null; + expect(personId).not.toEqual(commentsUuid); + + var personId1Match = threadedComment1Content.match(/personId="\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}"/i); + expect(personId1Match).not.toBeNull(); + var personId1 = personId1Match ? personId1Match[1] : null; + expect(personId).toEqual(personId1); + + fs.writeFileSync("test/output/copy-sheet-with-comments.xlsx", newData, "binary"); + + done(); + } catch (err) { + done(err); + } + }); + + }); }); describe("Rebuild file", function() { diff --git a/test/templates/test-copy-sheet-with-comments.xlsx b/test/templates/test-copy-sheet-with-comments.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3f098ce965e50baa77bea7572e3a395a722638d8 GIT binary patch literal 14485 zcmeHuWmp_rwl?nW65L&bJA~j)aCdhn5Zr^i1_;632@u>ZxVyVM+)mESVFtL+oFRG9qVElSabMX;YnMMUi#H>kY-Ttfm#_LgmJdNLO^l zlwvcVg|n%?L$DEpSm(yxat3wwuma5Lf!Kgu0G>% zEV^)Pb>ENLiVQl}D8go}aoK}{^32y|(RXgn#T8A_!n|gl7>}m5hh&c&uYZOI0(yQ1 z1(N$qwAUyzl3V~NECmQ=SU|Mv*c(|oFwp;a|8JE49}ew5ef8p)pHUuoEcOu8dp)@r zg(M{DA}H2Gq~!HMd;zg0B8L=rv6bQ#k`j&|=v(h*uZRBmMV^SgUZSft*5WV}G;Wew zmy*E5J3A*xYI6GoQM=;RPGslF%gL+cw-RoY&drh3MfLfgB>R?0M8{7>Dv(C#RdAqD zKfemZ;7#^X@0M0u(!VJMo)l6%ED0>HW6#=)9ZB_`PRQGZ=MUzTI{2Q9-s_-mGF$4| zYe{r@i=(V)%4t@vpJC5M?51OA`R!aJwH5WzlTIeBSAm!n<%W6WZ4X)2rMG%D>*+wc zTPHhoM^W!dpKlmJ{t_VZ{XIzdQ+2vML4kno0T~hz;2Bp-1{WK93w;|Kiy!H*SXs{| zhXd)c<@+OKrwa_Z0F-$n46vhCjfZ@S)pf2!Jg7_sOj4D!aLwGamjRS`lUj-WqHxI2 z%=$Jv%gEM)fdD#m50^CQmI?6cucG)i-%}u*i2kVf=h) zHrU?kELXX}0%Upa^hS6|d14L<`gYys3T=adrznz)w>ebiGPdOQLWXKdwMK5nA!eVi zp@8|p{jAtn$&e*eW0Wc(S98Ci?1ZIF=?l<|E3QZkW~R)Esi}c>reG+wIA%4z%WK;- z=;F}EL~;((EZICcUx}nOJi8dm>AO}CRLy!@+my*dS zC|u1zzh3I=pEq!WWHfwa!vQgKQimpVI~H-cUa)*z^Av`)pO;kWaC*c}EK~?6MOW?R z$cZsjd=E!HAJYLO(v`|(X$o9*)rn8E_;qH$t*!0D+z5=gYXDhtm4xqv^Mtp`JG?6% z6GD9}@h3A%3R`CZAtZ!q4)_yw+Va7gKukqmm?OH2a@D6xD(%IEx&Tvn@qU~OnIp1J zFln2Rdlmc6p1Eg4-_C;mAZX4Vx2t3REzq4aU8F@Jd-x@W92&6)c&|Wafr_d{s%C$l zoH60}Gaz2iRL*GrC&aiR(ynS%cn^CW{|%HFL;jL zafAY%r&Xq$eu4@T1F>UWJirOXE9Y@CP&|H9!M@;mIZ#AqTX2{-lMWY{uArF6lrdLVdu3yiNUJ6@)p{0Y71^SUEAO~g`)XOv5 z_fV$vSlbg8wJ(1?KRO$-e%)5-BV0D@VFFnw?VNMpCe2??bC^1gOG^@ey||~txg>A_ z&N;@}z&mF5bnEW)KKoett9!kqj8leI(Aa_F^Q=ARy$8BCUM-_{%4o|<;h~{90&~L+ z=I;U(1$t_O01(2RfMCS|f&vBv>K_U1&*A#dFra7JP3TWL}P*KaE<5o&hVi7xf$0L0L!kVug43|?1~1_`XO2c7tl%*N`!OD4#~)97a2Q z+GZtsEc61nniGj*Wq4xQ@p!0KURS1b!+C|Xa=J3k`O6Hh%qwT*Npn}nyP*#eZ`Fgv zGz9{wCy>Yp`sD{vKsMJnguP?}5izf3fI36g2|rpL0o;01}&^njQQ!qG2X9d);Dw^sSU#Dc!?WF-ccFx|fS zZ6{In5VWGy+whS$d+fBV`1J=E*h6Qm>C7GRpG|~TgBUfHsemOZoFOcEgn3Ny<-PNj zQR%C5UUkN15RC4>8HwoJ2eEx@=Z&NbmmrQ5>P&&|piiY;K}4 z`>ZTopKVv|7X9kx%}jMrP0wandJNfP>Qk%ejEPmOcB~ofS;*ig6+Z837mSOWBjL@lO@4=O*v9aCiQMpL=Jewdp;#$2&X6b;~^BZ4ZX zok(8ku{;Ij^yg>5^L&1;q#72Jrn3{L6|b(M>Ss`)tW;)JR-mL88>FdKX0BjXRLEy! z6_=`&C~y>~laU#ssZeGfWnosXtP+PPZ~~@=fI$bnHFAbvyEAfTyX%wVWH;m#u216| z#mS=fZ~igV{OdwuQnHuQv=SqOH1B%FrT+j@QK0OX8u?Hmc?SC}J5K^e{FaRDwt-Pw z)LdV}968veI5#RGQwL;yXMT1gH;;l`r4fxR!DQ<*4Vs!!-V8OmTOJlQ@aDz9d01r_ zi-Mx$!5>$AS@f4Cv@5m)c&z^KdCdHi$8qx?QyGy0k7)orUUo3vMF}WwA>CcqGsm&I=ohu{oAQ)uk>Pcy8TN6Mhv8 z%#QxvuUv?S0nzgps0 z&8V;8a*9%|ZFQ(RSUUV!U#d^W+LUitjV~7?xB+!GN`g( zA}74@cuTPk_^X4-B#zoAXq0+Tv|!5JM&pvpT3FAv0wlXy#2wpk?usBzv5*O2 zkdN}jSg%WaOq!LK&6eTSSYRFKo`b(nQikGb33WMt*CKb|m*}AN!b0n1nom+)S zJG3xY$iUF$ti`^&V8WuarISZ|4netT{wCM;@$r$dSF6|4B>kNUX7jVxHSV8j`8WPo zD*cQ9HGo+7Py7e}C;vtN!G8fbRv0njL5M$J;Qu4t`MBXli6- z#PIX|=g@tiHexf+hSq{O^GvvNq+Uxli?D}QYe``dTWh+2!26xes^XJ~aBeKl8y_tf zY{o2HXYBHMgpLm&^Ja;U>~yt9kuxM@8irErkI^8DqgYU+dmfPl%N}<(!qYZ9GWiaH zEHbKdb{5k-(`~!{(&Ajl_1AAZBnF)71$NQo6V^U9wJ$VRJl~p3k7Weg`Qnw!-s11o zS0IHoo5#y$VV_!9`Y5`x8L0V!Ef}Bgu?lamuz#9t+zyxoi=J>&i2E?lU4@;jYeK5k zcyF-{3R73GQmH3Yg}RG_ivh#UTOtmoa~fh5|^zT)kb2=zBFZz!p;eL=gNH&G~~YeRYdT$Wuc@qlR?3ZvIVqz zXC)C3X%w-b!rNjD*FbW)F63;(H015zK?BOK;1bwD#$`wF%@7ZX{5o0%!CAcFtQLK+ zNhc-beLbtO;j|(>&CuFf@MpHWuNaYI#m9>5^;KVGJCZtp_9dNWh=F!-z_kc>Dz56I2&`Jo&ob%3%PVyFr2Gsuu#XaGqj zVDzEO@em6}o;G0a!!lu6YfbJl zh*iax$D~pe)MRLT^K&)gPn zQgms!t^Zz>k{6jKG_qd3%poZ{PuNrm7rG{O!EyOXvRgVS8_bV$g)AVFg91N5pClbR|C(?=DQvWye?b&3DWr{V!YvgDGRI;X;Bm>;lBL{16mJs~=Wq?@ z2tUQAiVM+nU`gA_r{C%IDQ`7uOFO5hAl1@+dTMZ?YsC~8re?xndblp}w0 zn*pQaI)b~pvrVSWa<13hBjySMtqSQq!+g?;Gm5qwd&@HnS}H=J;^z;Q#u-P7r-I-y@0;V@-}dV(dzMQEfrT{QZ`W(z6tO>P!E_V%zr^I-*62 z(~AY{aU*9$!4UAF6u6Z*TaL4W&9O8Tj*bN=H8pQc@;o8uOJNaZVK?sVFf4SIQ2H%S z&5)hIkB#%40!=jCaKMWQtFH7EGuIhT44H*S8?0lr+fu^H8^varwC4NFR!zhV^jW&!#rW5_caON?mwQ!O{^y<>)D)oe;#K=d%^Tr>`Mc1@U zyvmzYYwUeh%V?BWiz4yvuA$hxz}CisVwcI#H9w=Yuo*&OyuY2bqsKi7<-zE}K`u=* z66HEVW!Sls=MARTromEr2s~C+7p#-tg%HePgEH4{Rq=*F4GWG{kMcIfZbwGbLeJDt zx}Ty*G<@IhY}Fn^t(d7)7+Qa~p-^=Ew6TVSlun`de#W}mL+|tA4g!r=>vUmtPPxi% zkbD*Y!CS#1Ue7%)6PN=R+-L#>_4sm(H|YEqq>t~KzF+KLmM$OELU>)9RfPLQZ=cC` zg0vU89zMhVZqlJeYb?+LDAw`-i3=d%{t%xW9NjF99DY=2xyl+A^NeU-1PA<3%}tey z9Fo3=m8`GP8ktNXJd`atktfN-B(^ONT9133%}wa$O?B3Ca&nG6w^J?(qd|C?4I18I zf20=*Poj3}nq=$HnSR6A(aTjxOz&Bq*8c6NQNu>a9_@fgbiiZE{IWVqqc7fItN zDfdd~IN3K<9akLh)lkArvtD)OQ#2YWLI+iJ9L)j|t>G;{VN6O;2Onk4f?^N5rmc%* z1!K(SnsZV0CtP5qR8t|z2@yrqobI%=>NyiHQih1yMrX_ql8##EF}z>6DIA0bfT5#w zg3!oF!$Up-t`*Y)1aQcs9H=ML8P2QS10^?V)k8wpjympVCwIhF+{0InN*V9ctZTB{zieM*V-lvt zK-(Qt>}k9YT$~WYGbCE1GlVrBr??^&B5a;nb%KoK7u_#kh6_u%dqsfswt7U2zu|35 z5c9)q84&Q|jBwVF4{XmQtUqv-c_5m93&?P!LuwiYLsUrUv$hx!3gz`~k|xyt-l1Q02eBs)h_!{Kl zN+h$sE8~>o!N3vUG%2E)MZFvwHVTdAl#9cdNw^Can*d2Xc}`Q_okz z)1u@G!@RL=s9j9w()#8XZ|CiZJbx=Gh_`nY8UoIwfdK*isKWu#ZD_CO^5gq5I9pkY z{Bg`g4>%52YO|gPthOP}@I%cJCFWK%e^ynV7nuH3XZ4LuH|P^3nq3A7| z5vLg!S_cSaOm7bbUyL{(Yr>el_67K|lgv%wRK!JCw-TP~%u2Se=tab)&9ao(4JHa3 zuA&)o-FDI{zy5`s*?^=w1>~y5tU1kfx$qP$uWnlnGe@J}a=vIS1CNw=9JuVQeXeTD z3gtPU!dlDZV7~7%JoNh`MoH#j>JKt*=ee*MpSQ)C30gTxJ|<`*U`d44pzQmyJK#QQ zb|cn+dz`~!qbBc#b*Ym0^UJ^0XrwKZCe8dpwi zWc7;vVc*R$%HlV*{X3c#RVA{cvasuM_##{=SKX;K6%g0iY!YVd2Dk=R;24d*xOG0V zmF_dn0K1V@lO^HCeR+*Z2M%uV@BHyHK)jnixl6bOxK; zOLwqWwDQ}8Egc58M6T_tx?l6eEi@Zou6AE(u3u6zKIwmf{!%!qqi_pY?Eia2S{d)q z7y-zc`X9;3{2OxSRWut}{6S8q-;qU5eBh$`XV$LBFVsG1*T&tL;R%9kbsSb@Tt%^&&38~nwPo1c}Lrg`e zm8n=Z>7z5L&_!+c88?)*=}A#6N}rzjknP?}*Z!&BCywiF@$7!}I-$S4VgNdisAXH^ z1KhioIp>saJ?isMoZhdAJ73KQu}0EhQ67#isIBXjO6{PdyU^2xMs3rE6B#rxKjLGp z_1dt>_}h>4dkQWgCo;G*jddRjYR)p^>6jMI)=lzOJijtogRo6uO+Z|uxwuPgyOYq^ z&souZ60CX%z5TBI+L4K=mPe1OM7El2{fh51Ny)hgr{0zb&B{ zA*^xpB$%|JB6zH$!>h0NBewCS`&m5j?1b^pn0+gtyogkK`t7P564xLz2;htofT1LR z44pq@{(q>kf9U?dG})i3e{sx^1Sk`l=(X=L($Ug_I5@gl9&FfX$pj=+UpfJoc;JHS z;e6_+;9)tfhSY_~G)|qE%dHN?HL;L;ELKW2pDf*+ec_^2r)SS|Am1wJVK}C!KvX8n zz5Sv~Sa|Y8>4i&@Fd^vTPXlhAB8KzP?6m7*pGc*pmNqJ-=stGiOG(xj7II7RW6O;s z>2I^4CbCe0n;0v@q3)7f=^G1>pMYK%*UHgCd5ZP1`osW zw9DgTD+hEFnE(gz1(jQ?_Bt#7Cl7c8V+Ei<2hn6-BjIaPz5 zv`A)pT8uBK#-0h=YW$QPYqFDGEf_AOKKFGi+K+-94(8TZy+h|6E_f^=qIrS z@?qZ^;6j>a2Hz&#CGvpyyCe%KS(7?vZ3eYg+@KaCD7h6>cXq z=tKim?o!@C$02zN^NHY&(1fT#|JBYc@&5HzgFKAWi2%^tmtvTj>}=V)@4(zBEU>S<%dzr3ovS9h zLz+^8YmeX-8dbhYYz@0A1}1%|r;sd&M^k$Cn-i(pd{;pjkJUk3Ee; z(a!Cx__SOBz%c%e`-8g=h?IdE<6yKA7spwmh@lw4tYksqS0|sN%OPzXf%Ho3R!>A; z<7*|JM%2g@8b~Ox%NdZ30p9`T5gCgJAW7w9y3M7`G;}D+&;*YZ8>m{G=$qvFm2PaW z!!>qYR3~eowpKTL=6`P44m*-Al%eRGw4D$?i&O<0JqG5L9KEr9y62=&zLnSXQt(G% z(BxhAy!vfeN?Tz#djSPkVK^TAz`ImqEB#>d$khLEplK*l1 z9YVr!H@_|+RPX{)?$RzOp(+SUEpqLglJZhBsWhUM3{8n~YkIP$_Y{kJ(4_PfS`s82 z31j*pg#-18hGHxl$+pILUDa#36)e}SpZhoXg5aavTGs<;K;Lg0f zC#q6T+}N61tusxM)$MXC>iQGx;22V3?byiKt2_pF7=b=wjOw*q#cqeN4`haCDDQ|# z?QrZ-0Bbt&)PiX9Z+e0W#?=d9MaPv>U+tNzC{sZm4DDLgMYMiS;C$3^xczNs6K$C@^aH@xrhgC2pQBm0f+V1vMq0!@!Gq9L z$xGupg-n3!sqg{P%3DO!V*K7qOzb70P#T^sP!_FK+aUTrHX}Eu-x?)149cI;GsW%v z!gwo8RQYFMLgOOnL*%o>N;XT+^rT9&>aF;VHsptQ`yY-~y_q{6V#z_koV|R9OJPleGFOjkx2}y$lgzm;jSL~uDJ|a%r?ZRuvA!eSGF@+1bK(>0 zaHcP-Vc)gWSG5v~aXrU|ud1RsMW=3#*qJ?JH|89R;VJ{yr(GL8X)d34sR%#jxDn(c zKGUz1i_N(AXAQItql%87d@gql$7L@fYqPPC$_8bdAk58FS*-SVgbIR$GU5(pc+_sa z?gMN8hzoRkoO!)ZE<6XV=uy6d2GNULSj(Jq&Yumuki?~{m+K9Q?k-m?ut6o=Yl^62 z0qt=j3W+p@gNYCsw?0-ATvpp4G094SE4ACEYLffiOj#f43UFz#*t7{2fxJ z5~6NNTUdHD!d#pVB8w^J__(q4(8}gam`(hIi|1HN-((+0Mn;6XWJdBEut)sSs$z}Q zjK{~c*s8BANd$Hv+EQ1J=eq>I&4M4eG&ku0OsW0fN9Iok8Ac8Kh|D+F@pp&`*WHLq zP{Ck>Vf`a@0g)q7lLJPyOIDu6Hq@x+;tO<^F_HD8`V8LY`KE zn1w3X&}~BGYu}uzHj}#bY+y|g`2gqafJn?vQn%rVzUX(7k@*Wk;RM6!d1>3z8qaf# z(e-(+3c2qd-ZZUUQ)G`tjZeRw_AY_+HZua;oeX%P|1lWa8reJ8SUWKMvHu}%8IDSu z19}s{SKfbeL7E#l^EO1~cCrJZBMYnL{GcCUfG)`#gMuP)teptf*z4?iK6H{$RE0AJ zT^uW_!T}=`F5n_6_CEjQ$UzuObl8FJ#*|g*1_YsOuP11e^%(1sJpC=pC#?X;<1aA^ z)9_jKuyF^)c6rx=CTOqUGjL~FSAGk?1^S9(_DVGTZq)`%UxsRQX|+ft<5+~nC&T8l zoBBvg_h5Ax)}gR|qQ2!Z$lr0imqDX+Yg;E`%WDOvrs>*vUZ3<$KChUunJQ{5Iqi2RoRV2`3ok}4u5t#4rySZ=YZ=0i&ZS!+7KjhJLv}Fk8VPFAOyp~6 zBG?Dw{SGp!DY>LZ_cBnu7y&zsDaD-PcCS-0#@)rAxl6nOPW*dqr{1%Ar2sf%19$+) zfL<6w8v{9e8(Rkk0~>pzAMOS8cmB5p2EaYf7N@J^JDGaV(-xiTOeHB&(tjI#=|?CRG)fVJ{2v^pw33e`5PeW(<%?SIW!p{p=+#(`4N?n2Q%`@NTz>(!#TN34kLi{ zsw0`8n=@I_{tdHL_J&E|MC#vvVUx8v2Et-Ke{afTKvX%qhskmgQ2@XbsI$Axmn>zb#@ zKuu^ELwJF#~YZP;(ZGrCF&hj;$U6KjvA#vwiCB7*bstCxVqJC4? zi#sJh;g-R34FnS~6(LU893tFTD)3KbX1h*3iP|n!gt}DtLgk_~xr+TdqHi-j17^q7 z{LnqAVY26a`QG@I41#Co=@-QZyv81om<99o#^{92w>HBy(vQh*QwGI(KNxc>Y@tzk zAWgPo3p15(8YkSoHy4;2X(fiW$&wacQ$3BsjFT_xFBai96Etp}_#PBIF(SPU zNTi)?ZwYa4)ogJiD?s8{N8}pcrNG)*Si&m_?Fq@a6d~^vK{M@^@#Vd zW0;@XKe9##qw6Z4IL=XVy(S#|R0ub+mPL&qo?GB{>|ARHziY~=ARGlpZnkZQbtbluK<5-xBsW%kNpuKI{wsr|I+Yf3-T{h zRDg8%q9OUE@n72_f0+USeTDgH{BN2hU*f#%fBS`m3n;<=aT9;*hI@(fvis;4$~vGC z>o+Jri>;R^FMBzDq0FQFM0wfS@e<(Wy4o**DD0mAzgF2^n!a4A_+{$$>JQVG>lQB& zUdsNz5UlZjBK*5WfR{ioCE8y=u!R2(=%--&66mFx_6w+h_$Sc+OIv#h_)=T?1$aR6 z9{_(TO)mjoDlNYN8OVMD{zZ3rY5j8g{mWXF;-~eCxwpe_C*vT|EqiYE$YkK`WLDZ?N8LdSK2SX)?aJKUjRTry>tMn>t!YR()_O( i|Ig;~jDIr!NB)1{h}1fQ$egi8w4j{`wz|v%Q`G literal 0 HcmV?d00001