pyOpenVBA 3.2.0__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/PKG-INFO +8 -3
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/README.md +7 -2
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/pyproject.toml +1 -1
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/__init__.py +1 -1
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_host.py +1 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/__init__.py +168 -0
- pyopenvba-3.4.0/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/excel.py +6 -2
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/vba.py +201 -28
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/.gitignore +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/LICENSE.md +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/docs/architecture.md +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/docs/roadmap.md +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/__main__.py +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/access_read.py +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/cfb.py +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/powerpoint.py +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/vba_pcode.py +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/word.py +0 -0
- {pyopenvba-3.2.0 → pyopenvba-3.4.0}/tests/fuzz_corpus/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyOpenVBA
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Read and write VBA macros inside Excel, Word, and PowerPoint files in pure Python, no dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
6
6
|
Project-URL: Repository, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
@@ -200,7 +200,8 @@ path controls the format:
|
|
|
200
200
|
```python
|
|
201
201
|
from pyopenvba import ExcelFile, WordFile, PowerPointFile
|
|
202
202
|
|
|
203
|
-
# Excel - macro-enabled workbook (.xlsm)
|
|
203
|
+
# Excel - macro-enabled workbook (.xlsm), binary workbook (.xlsb),
|
|
204
|
+
# or add-in (.xlam)
|
|
204
205
|
with ExcelFile.create_new("new_book.xlsm") as wb:
|
|
205
206
|
wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlsm"\r\nEnd Sub\r\n')
|
|
206
207
|
wb.save()
|
|
@@ -209,6 +210,10 @@ with ExcelFile.create_new("new_book.xlsb") as wb:
|
|
|
209
210
|
wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlsb"\r\nEnd Sub\r\n')
|
|
210
211
|
wb.save()
|
|
211
212
|
|
|
213
|
+
with ExcelFile.create_new("new_addin.xlam") as wb:
|
|
214
|
+
wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlam"\r\nEnd Sub\r\n')
|
|
215
|
+
wb.save()
|
|
216
|
+
|
|
212
217
|
# Word - macro-enabled document (.docm)
|
|
213
218
|
with WordFile.create_new("new_doc.docm") as doc:
|
|
214
219
|
doc.set_module("Module1", 'Sub Hello()\r\n MsgBox "docm"\r\nEnd Sub\r\n')
|
|
@@ -329,7 +334,7 @@ modules, `.cls` for class modules and code-behind.
|
|
|
329
334
|
|-----------|------------------------------|:----:|:-----:|:----------:|
|
|
330
335
|
| `.xlsm` | Macro-enabled workbook | yes | yes | yes |
|
|
331
336
|
| `.xlsb` | Binary workbook | yes | yes | yes |
|
|
332
|
-
| `.xlam` | Macro-enabled add-in | yes | yes |
|
|
337
|
+
| `.xlam` | Macro-enabled add-in | yes | yes | yes |
|
|
333
338
|
| `.xls` | Legacy (Excel 97-2003) | yes | yes | no |
|
|
334
339
|
|
|
335
340
|
### Word
|
|
@@ -164,7 +164,8 @@ path controls the format:
|
|
|
164
164
|
```python
|
|
165
165
|
from pyopenvba import ExcelFile, WordFile, PowerPointFile
|
|
166
166
|
|
|
167
|
-
# Excel - macro-enabled workbook (.xlsm)
|
|
167
|
+
# Excel - macro-enabled workbook (.xlsm), binary workbook (.xlsb),
|
|
168
|
+
# or add-in (.xlam)
|
|
168
169
|
with ExcelFile.create_new("new_book.xlsm") as wb:
|
|
169
170
|
wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlsm"\r\nEnd Sub\r\n')
|
|
170
171
|
wb.save()
|
|
@@ -173,6 +174,10 @@ with ExcelFile.create_new("new_book.xlsb") as wb:
|
|
|
173
174
|
wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlsb"\r\nEnd Sub\r\n')
|
|
174
175
|
wb.save()
|
|
175
176
|
|
|
177
|
+
with ExcelFile.create_new("new_addin.xlam") as wb:
|
|
178
|
+
wb.set_module("Module1", 'Sub Hello()\r\n MsgBox "xlam"\r\nEnd Sub\r\n')
|
|
179
|
+
wb.save()
|
|
180
|
+
|
|
176
181
|
# Word - macro-enabled document (.docm)
|
|
177
182
|
with WordFile.create_new("new_doc.docm") as doc:
|
|
178
183
|
doc.set_module("Module1", 'Sub Hello()\r\n MsgBox "docm"\r\nEnd Sub\r\n')
|
|
@@ -293,7 +298,7 @@ modules, `.cls` for class modules and code-behind.
|
|
|
293
298
|
|-----------|------------------------------|:----:|:-----:|:----------:|
|
|
294
299
|
| `.xlsm` | Macro-enabled workbook | yes | yes | yes |
|
|
295
300
|
| `.xlsb` | Binary workbook | yes | yes | yes |
|
|
296
|
-
| `.xlam` | Macro-enabled add-in | yes | yes |
|
|
301
|
+
| `.xlam` | Macro-enabled add-in | yes | yes | yes |
|
|
297
302
|
| `.xls` | Legacy (Excel 97-2003) | yes | yes | no |
|
|
298
303
|
|
|
299
304
|
### Word
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyOpenVBA"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.4.0"
|
|
8
8
|
description = "Read and write VBA macros inside Excel, Word, and PowerPoint files in pure Python, no dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1161,3 +1161,171 @@ _EMPTY_XLSB_B85 = (
|
|
|
1161
1161
|
|
|
1162
1162
|
EMPTY_XLSB_BYTES: bytes = zlib.decompress(base64.b85decode(_EMPTY_XLSB_B85))
|
|
1163
1163
|
# --- end XLSB template ---
|
|
1164
|
+
|
|
1165
|
+
# --- XLAM template (generated by bake_xlam_template.py) ---
|
|
1166
|
+
# Source bytes: 8400 | zlib: 7791 | base85: 9739
|
|
1167
|
+
_EMPTY_XLAM_B85 = (
|
|
1168
|
+
"c-nn=1yEhf(k>9(-Q8V7aJS$R+%-5GC%A9i-"
|
|
1169
|
+
"Gh4w9&AH!LU7j&!9755*gVcX_3HeG|Gu|Yt(mD=>+4n3Gu=J?wVL7^SX?M5D8!eC3hLp#PMi"
|
|
1170
|
+
"fa6qFb&6co<OjDZxu$=%Ay-9*#d*~-m`{gb0ZZOV5Q5-"
|
|
1171
|
+
"v>O*?a$pv^txU$v0JM?@0B19Na!*aMm;E@m<I65KWJ*6tBV3Vq#);Eh6}W&M54KyUAVcJay@"
|
|
1172
|
+
"~J$8G?l|r`#q?g@yCr&1=b8Mx9VQA@(f07Rva)5Hj05@=@M6T2aN_F~Qa&f>G@b!j?z}$I5o"
|
|
1173
|
+
"(kPBBsx6+v(VEc2hRiH6l&XQnm*QDF=sR{`J5byBy@T0B&EwIXMm;$yn_5xs4RVJv1wydr?F"
|
|
1174
|
+
"guXKRSnPWS%YHv%)fl-"
|
|
1175
|
+
"$oEajWE8%h9&&H2$ovqT7F>&4j&;oNFGs`t&V&J|bxaN(_#FqiPE@Y-"
|
|
1176
|
+
"QQ4^(S#XCPGLUkwi>#fH%1Vnx=6sq9%>2=9I{3y3BZ<qwh3E2nc(d24*D9DpVqxh3r^T+%dS"
|
|
1177
|
+
"?BoJX7?a?S@;z&;UOWsYa{ebl!C0*oTA#llO30Q@2hFN$RFcZ`egKfh!MAz8kFUdGNH#}!<O"
|
|
1178
|
+
"jh5h{cBB{wK!>xUq(tWP*5l@y@{)pgBu6?Uv2I5cZD}xnD84N8$w}iUe*Q@(%C06Ve8WK2!t"
|
|
1179
|
+
"sIPK&fS$rUTD1Q<mq&3`tY_6h*NrPJMwIibB@w063DY>e@;H8G*U42X*dLX~E8dZ+Zf^EOP!"
|
|
1180
|
+
">Al^<&-|(sUD=H2?9c?6K3)5EIo6Nc;-yLbF6a<S0X4R`ZD^dVDEL9;m34E-"
|
|
1181
|
+
"8uVJxhm%@hXDi?5dy8L+?K{rSK#KlOxcIAn#O*fMCI=lR#b1VG1ZpBX!PW3AyG)AA@0obreg"
|
|
1182
|
+
"~Dy<c!K3OgM5m(<!D+zTdsfuj?0>ycbh#QhK!cm_U(Su9I}I5)wnPFADu{rO5p{edF^63W^X"
|
|
1183
|
+
"33JT+8=92@57r@ot900KY&n8Vb0Ia;j9Red93t{?MODm*0ASIy~EkG&jlo&Ta!^7wou~E-"
|
|
1184
|
+
"j(c-Zn-mf7rfoM<2W1BdVKl$AXwTdB`1Q6OGk+)43IZD1~=Y;KKTGn{Gf(dE<IYkQNsQZC?>"
|
|
1185
|
+
"78(xs$m~S3^=f3wfWF&tp&rh!m3zGmuLnqy)-"
|
|
1186
|
+
"6zLCK_v3;@)3(%0fC=Fo3$b}#2=@fc{UEVZ&Y*5k!N^P_@2B$e%0iVP`toLcDQ;=mlB<0*1<"
|
|
1187
|
+
"rxW!5JU1ZlExR%TBqd|u^)s?Lu~%4G4e^lr+w!|wbNdk~eoO*XWYiyTLa7N+e~3yGM?OeZ=;"
|
|
1188
|
+
"Kz+oeUb{I(DhvN8BT|AbQV)NOYx0vkg!T@b}^T@MRbkYUsp?-6$x-"
|
|
1189
|
+
"^6OnXD`}I<MkB}|p}ISXafd!?pQs^cprcAu%BjHxW>DoyP_B>#TB!LG+(@QVxqo$>pkiE3?s"
|
|
1190
|
+
"f=l=^7r&SDy4sSKFq`{49J$ooZ$LP7!fH)Dt9O?;;FkGFyEzs<7Yjgn^mr*XLuKj#q~f1)Lm"
|
|
1191
|
+
"g2|a&b5kr{_tOgS0H^ptn-N_v0Ey~YDhlG+q035grDUqAMTq)UEU|Qx)GAqUFED<kFuOc`p9"
|
|
1192
|
+
"<Hi<?;E$q97$`Nn6=kt2(Mhmv~Pe5d6vMVIsJy+&4A-B<1WEBHMdzGdZO#8=DChC&dxd-"
|
|
1193
|
+
"zmY!pu-vK?+>5b)cV@_LSPP%@lFbt#OAq5G3|VVx$0@CpwCQm3Yx3VVH^{=oK2*8a^qh?04x"
|
|
1194
|
+
"32*L>u#jJN6-"
|
|
1195
|
+
")I=Vit=x0I_4i<QHt$!lVGCBB)xwNvXkK}C%H7Fho8#3al1xytG9Kr=`XC9H}cVNk6mF4BqQ"
|
|
1196
|
+
";HY;-"
|
|
1197
|
+
"XiAn`@7%tN1ssRC`?hD=whO4rrxYAYQY=Fx8?D1*xAcjLZj2Hp50GBNbIJSf9qf3efU_Zdrp"
|
|
1198
|
+
"~sf(Gtax~%_JPbu0d|ITxd%hQi!Dt9{)o1&Qin$dM;)y0}tnVV0HPAI+G+ZyC%zdV+8fVW^V"
|
|
1199
|
+
"JtIh1aJTOB_H^yq%?F3_oBWXExbq`(cI~#c;9je>Abmky#)9>Iz>oABue-MKDUV#)y$3lyzA"
|
|
1200
|
+
"3TM<1;D5qd=1Tr=Aaz{>PSBuDM~<f}_?p`4$8M?y@);gGApOXL@$NeDVW#I@*!pW_5yKZ@f<"
|
|
1201
|
+
"UpRve~pTSULV+X{dOdftJdq?jL9ufX+ron%ZS?*p&<uAUA_-"
|
|
1202
|
+
"`})+kO2{WBuEeB}=IEabn8cykGsEE#jhqDOsVyhgc~T)(dTDot(LxKWw4k>MI@do+Uh5MyL3"
|
|
1203
|
+
"h;JIsnRVUQTJR*q=4o)ULw8ylfrBLg)wg^}_Xrn}`mJCjlle({}6QmC+ni<j97{6wPKZ9Y}*"
|
|
1204
|
+
"EpmowVWwj$W>1;`~9VG-"
|
|
1205
|
+
"{V)TIg77jOS??l@TD(N0^euhCEO!@3B!d}%BuRIXNM0nx}mC;qQbzpp^3S&5vWDe^#dgviH+"
|
|
1206
|
+
"Zd5-)ED=*}yp5^($}Fsxk03%`~0F$}PIMcQ86*6N;4=w)??S$6U!WoD-mU5JspJ+8@iOZ&*V"
|
|
1207
|
+
"(Q<k$4FUlT!rpZkDF0SifRiGq|0MwT{^i^L@oR22R#xtA9RKOK{?b|Aj3Qu#6BTOXA?OJq*h"
|
|
1208
|
+
"}ovE|yUyhgBDYB-"
|
|
1209
|
+
"&%eD9P3!l8!ienxF6*jKZQQNSQ&+7mY18mEQF2%wyDeN>BzUg<sw?`JOqgQsD8vE$TE#fJ2E"
|
|
1210
|
+
"%pL=*S68$uzK#UFD((8-FpPGeQ7gFN04r=*a2Q?Ie4-sVxjxyRPXXXtazXW57P-"
|
|
1211
|
+
"WVcFW%<S>odM#DEvynRZ*4N&pXm3yQLaAp#4c^khjXsT6r|VS$}a26hnKU@~K&?_F^aqW~Sj"
|
|
1212
|
+
"&6`EeXW0F)sT)1VIRo9pvKyJ@b5@N?tTS6z7mOyt9C}I)_5zb<eG^X_>Nr}UWsk7tn@nY_t$"
|
|
1213
|
+
"~LTdBqLdiROVRx-QiHLLl+@-"
|
|
1214
|
+
"ckIGrypu3>s0fN5Tyn=A<2zt&&#ApQHhV`=Tb|SX)ev@niCdm7fPh$_6EzDpd8bH|cnpds1}"
|
|
1215
|
+
"`Ea5f!c*k9;+d>(cxogiRrVE&_GY8PF=-"
|
|
1216
|
+
"VRliuorqpKqIE)Vm~1_&P}2{(65Iccr?vJ8eThFG3I)L=b%<9mYS8Wd*3|a&p#G;MR)V~W^P"
|
|
1217
|
+
"I2E-0{u2;aWVV*RbJ>9|($&zOs3%-"
|
|
1218
|
+
"V58QrP_p|)KFjSeV;trxncuD{d?uFO`9Zs!$Uz~QU87PdRcjQ8!JbvzuW&T`icHT!d43&h+z"
|
|
1219
|
+
"Mja{siM)&Rn#tGrRLbUc2H{;|_w`<8VI+lx_y@_>lsJIuiDtYR>X?UZ8hx+`KdyiE{n+n)qI"
|
|
1220
|
+
"%9wXgG|1`=tjq2n8L}KdxDQ5P9h~_--jd(lvL0+ARHy3dM56q(G3~%0U6FiiiB;xeDpsIOBC"
|
|
1221
|
+
">*wZ37#oj=N}2dyEkuco!4lOpB9BZJ&nC98dWbhpDmn<A*RL1qoqOqN3_lRzg*YXoT3IvB*B"
|
|
1222
|
+
"&G8cvH{4MuvCQ~~DTn*gC349b%YWwt@rUbr02H4U_g(R=AFq7@Fq(QaC{pqpisAfVeCnIR2V"
|
|
1223
|
+
"|yhUPZ22=TLaiIQY`Yt`1{73A&eZnsNpxO#DFS@g0XxgyU4)Sc_+0XPThhh%LB6bwy4wes92"
|
|
1224
|
+
"f^To9*p)2zl&d%)0vH(d)nGdo(#gHdbgmi@KC!}UX6;lliHWKF}Cn?3#$)j<$Y=ku>=a32OK"
|
|
1225
|
+
"U=d*Z367av$oKZza`K9v*f<zF<X}g%edgp!kgh$1mC)NV>_&*JT!?+sK(n!>9a%IjVqPDagQ"
|
|
1226
|
+
"$R;S<6|%+-}4Jh>uy+LvERjWFIRdYwBn~q(-l1DwKaD7(e6Lc~~rrE{AA$T*QLwYt%-"
|
|
1227
|
+
"r7)EN%{guL6{DgSB0|4^Aysze~>kha)C<hon&k25ap+@VJ6W<a%dzc8ksS|rzzTX2w9-"
|
|
1228
|
+
"pkc`L<-"
|
|
1229
|
+
"pp07E&y&q1%fy14%mQS0bttS3=YZQSG2^^w4JqV<`9=E9F?{!tDnmxufK9@mH=7Qt76rt=<-"
|
|
1230
|
+
"|Hg7Z%ES<|2`m&MB<ZfM=^0$qQUvyx1*?hB-"
|
|
1231
|
+
"5LLO%kM<tobINTNW5gQhrHGQ7xKB7dIAqX+`FR?rcTu+k~?*!mX$ltxPY6G;NuW^%>h-"
|
|
1232
|
+
"I;rX6Y>`e9etC${K0suE$~uIt<cg~W7!=2zwb}MIg?tkiu|^rj8p#GE;BSUT#Fa%+%tD6r7T"
|
|
1233
|
+
"%mYDsp!a!;FcFml>Eko5VnglQJB`i@_U#G7>x($!U*GMWvK$?N>YE4+ERi7oad-"
|
|
1234
|
+
"8NZEflAXxVqe~|6F{2oE`ey~vOiBFHdqZy79zly~#*hOcS^!T-"
|
|
1235
|
+
"FgfZCO6>MY%at1`!!BHMVB>ZfTW}_bV!;x6*wU}(16v0=|GO{oo80s1*GQ7EU#KL9eIQ9l6W"
|
|
1236
|
+
"ky1pZF!-"
|
|
1237
|
+
"281bryixD<$5wEWEGydwF?Cs+=zN3CJ;f&ud>GDag3Ed^^a6Y;IkHA_Ei9P*WqTFb(#-"
|
|
1238
|
+
"P#`i+s+)ZtzYKIuU%H5V<Mbql=Zt!owp_K*uX=gC@u*gm0$kMOWr1V2%~dYCJ;@=Wr~rzZm#"
|
|
1239
|
+
"WDJPCQT%6|`X*N^y)={n%mMi@d*0`d;0phBgE_aGtMS6l27?{a-Y1OtcNV(SJklu-"
|
|
1240
|
+
")4QOiyKYuT^pl;3kX_K{HVzwz1>9H*&~_eh#mI8-"
|
|
1241
|
+
"{XrB(X*k>v$DLa9iiYs}6fRB?akS>z#~;a9>K!d@p)mp#_AxwnEUYOpu-"
|
|
1242
|
+
"v$dUx=yjOAmB)iEx5X77-"
|
|
1243
|
+
"k)@9nel8!9?u_MA=~dXu2qPP`tgOw5^M)>XWGNt(Fp^y@27hTB@P)Jc>Grnn>bTh^7svX}BI"
|
|
1244
|
+
"_DMAVtWs!9)yRaK+DQa_SOpM7vtq$WsFRn6bVr@$$=xUNT-"
|
|
1245
|
+
"o{p53W|c@NJc)9or75?M69+5l=k9E;SD;iFOyl#j#Fj{{8K31tf@b2J;?YDsxtls8EjKpluD"
|
|
1246
|
+
"zvSotVE;Qk2%Yw;%OwWkXH(MGGb48W}Rn{RRl;~1BVdcVmia!5%6E@#}MfPmXGCmsm+YM)7H"
|
|
1247
|
+
"I7@KRgDqjk3$ZFB))7c7$WVwZmJk{D}!xh;MTCchY8mL_PE@a)g5M@#oBG2>$>`-"
|
|
1248
|
+
"0%E=?x`wjpkPEg%N|jR($sus|FdC^GlVV!SOIv4W;7KU=ayedCS{}(pP)U4Te9Mq{<Mm@fv-"
|
|
1249
|
+
"xM0pGCvbjT1k8z<rcj$~=#veO4)%pz!He>IyU+wlS%Qej^Hmj_MXY7FI3$v!!=A-O`1m#c-"
|
|
1250
|
+
"_hQ&RhcY^mGFu`DNU__d>o)X_#7KW$Us5k=%sP~@t6u9e1_D9Cl>k2?t|#X6ab+<5By0XV8M"
|
|
1251
|
+
"o&b8CSmiO}BrQEBZc3fGVK@+?ZW^zGDzCD!G|PxtlXsIr$%nZP`6(7#HUacwRnovoa&Jh*=A"
|
|
1252
|
+
"TFT;x#*=xO_1iUU(4Ll>u}V8?nyva9gOPvA_YtAC8;iaR&)K1p6XTuRWT2V>UF6%+^JfQCE9"
|
|
1253
|
+
"W8lL?Y!o-h<dB6euYUv8*vx^;cDxbO@%#*EKA$}`sY<-MI3O-"
|
|
1254
|
+
"OmsB<_{2wvO#B%F6|T+Bo4MeJz3!3W<rUomx{3|ybhYZ@Bgn4`z6=0?Z>E!O<l3Vf9Zt6nos"
|
|
1255
|
+
"r@x#dbzXub+TQ`v&E4C<%I&YvSegd5U*W`jN%$Tp!q&<3zMRlWXxnBJBO4Gkg?2Hw<VGGwB&"
|
|
1256
|
+
"E(SEk27{GPp<%y<5)MclWwqz43cbwUC!=I#i1zOMt0rj`;1165F5lAaMQ<8|2_ir3EG8F_dq"
|
|
1257
|
+
"6=*BY%6p@#{BNTkOzLQLqM$mlgt!`n4&709}QMPTTsSdGPKRcr8?YH$u^-g-"
|
|
1258
|
+
"J=+v=?1T>Uv_%U~@?kOV8uf11f)7056&FM;hKjgy&SMA=|JF*(c8~31>*T9gCJHiP1vQGP&%"
|
|
1259
|
+
"&8YO3}y|gG8ZS>604stxLVr)b$OFrb2{U~cyJjAy1cIAN^tc+IA(r{$%8g$;#Muo1ej8jd_s"
|
|
1260
|
+
"ZqyD}M6xVcjJbf*Z5TzcZMR7EA+)4CgchUbZ4j8jVIR9j_NgTV+NYrT<@fJ{_3Qmjbv#^+Re"
|
|
1261
|
+
"NRAH9MTJV;{0hWWJM#M2nuttHwpll;hZ^sZ(647eJ8-"
|
|
1262
|
+
"kIm(VkNPc*)cOXLhoOThsysWq{+Z6Xhhas+hAeB|op13oy~0Dat$b&_z#N;m_OF-viBRZ>(r"
|
|
1263
|
+
"`GHbDRGkf9x9_LKpIfUmAs}{BREd3v4KfqPZV=JC;6N3U#k0YXFKycelm3M$_ai91lPI^CUv"
|
|
1264
|
+
"fyXOq=nhQJxUK$0xUwG+(PzoPw>=2t%^@99W)am~?xlAiehWS|dzH)r&-"
|
|
1265
|
+
"d!^9^OW*Yn$yr1^Ogur#C_A=0c>ujQ9>0VaY%3A|(73x0Wk2MT%|CnAGaMc_CMGJcJxJK^WL"
|
|
1266
|
+
"fhwHrz2C@Q;CjVk!nOZPbjVnvd)nRcz*iiRp_6-eXb1Ls_2Y%N!@iIR=%ueW#-"
|
|
1267
|
+
"9cy>xfUNL$LfK8^#(#B!1kSi5oHW=K+4d*n{GwFa3zWYG5l#E{-XXePn`-"
|
|
1268
|
+
"oS;k@^1MEFGKp5Q{LYj*wfrh%@ts0W#P_lZtHZCeFng}AsaIB^2D~_3`^ujEaXK$(T+a2lM6"
|
|
1269
|
+
"9&&RE4@m<+R2mR-jFk$bBvd~9fG!Wb*dZ8m-"
|
|
1270
|
+
"JIYl=@fx=<ty2hS2N@Y3!os{r$8Je>2Qb}dbJ6qqyK+{k6`4iuR;Sn{(8M;pzyCJJLr<V}Or"
|
|
1271
|
+
"@-->yY(wEN65p3*y$}o>!y|PYE4aX+c-UR@N$Co-pQhqHZz{tGO8x=t%k(%Vemwd*r>BqY>^"
|
|
1272
|
+
">ZfA=uv^(yi0sLw(6;tbt)>fa>Hc=E^2b_<~D(q<_oy3HCSo5~#7<ZZXWJZR~=FBR<++=nBx"
|
|
1273
|
+
"qScmS9)SSgW1cH}6_W&XZEuCx7WNGGxo@Kg)uqI~Q8bLXQZB~oxY*L~u|3&-"
|
|
1274
|
+
"L??Sj)I#veBkpzraL9eahZ@w}O0nH*qrb|x0-"
|
|
1275
|
+
"FboQ+c?LVe>sse%;yy3R&vR){$L$i@5N0@PBAVIMr^PS8NtE&!vFj3)J!OeRy<3*#Cid@6eH"
|
|
1276
|
+
"%liSD@DmRBC#|`(>l$nq?XI!1QHM*Rht@=unapR`D_@hGdW4x=1dTIrCawKhE6Ek_iyky5J{"
|
|
1277
|
+
"fTz!r-{eAo|PwWd+wBsFRk(8AInO-X0ke4EuDt#WOytdxKr-"
|
|
1278
|
+
"R^NE<UmtuA`MWrqaSt%X{qurqA_<8KT6+im!W$I;)rPM30vF@t)Znt>NwIw^^xsHLa4)gb`l"
|
|
1279
|
+
"#N0mLty*;XQbKgTZjE;^~2{-tt&A>$LnlQX~bu-"
|
|
1280
|
+
"D!vcNj&iq(>U3CIy|7)NIN}pmYpt6LKjoW_5_{gYhYQ}d)@9x`ght*qz;1E&slT@J&9>1RUq"
|
|
1281
|
+
"QN8M>{yNqvP~!GH`hAxk%G-"
|
|
1282
|
+
"?~?oH=+%9KKlSxDs?GX+aOtJYe)q+q`otmr^Gc54mWj!hnXqV-"
|
|
1283
|
+
"4{~i)ITg#O;W}=J{?2UR@Z-"
|
|
1284
|
+
"H%Chw&98??9oX@1j%;y!8V^&YK?GE$JIbG+lB9DQ@7i`>kI@hNcxGJ16RtNgNJnNNdVAIw#L"
|
|
1285
|
+
"OfT3XQFol7B6XN?F^}KI*!+;aGC$(DqbPP~28u$v*lY=H2xQS{u(UR)?YwK~><UBEW4*|vfz"
|
|
1286
|
+
"8}q;yXV!E9%Y$w_ztRnfV#l0l_qY)aBeiDg;yh3K`R-"
|
|
1287
|
+
"UH$!^oapVlw9L)_IE=hQ;R3k#TFBlk-O0BD5#mh2xkfF>L_*eMpAE&^O;HPkvvTh9>-"
|
|
1288
|
+
"m3<a0`^-$Ho_=^O-O!?!{H)(-"
|
|
1289
|
+
"*S1u&vf6pQ6a6MiPT64rRTT_K#TX$<AFrxKA;5KcAQbB{}m(b4sI($Cz<zhya(EZIwk@4Jym"
|
|
1290
|
+
"9a^*ZSJVM`{1^wUiP>U#`#-"
|
|
1291
|
+
"X3hARy#)fWSUxjD!bE<+mYyDSLq=q(7C7nLb8ad&Akz^v20x7~l(MT%Dj$aK6iPYcSc79~GR"
|
|
1292
|
+
"NXt_uaufLEJP>AK|f;3W>XBsBg(AlaEQ7p(kw}ad&(L_nrN+W&fwINe8RSxf!ed|t{jZi8<1"
|
|
1293
|
+
"lgLl9rxG!O}5JxNswwGS>KMI>kG$6())xqQRKFFd-"
|
|
1294
|
+
"2!jHv!$nR<?VIX?gjhIqpuW!aHx4D0o_bL+(@xIDXKBF@Rpi8_R0k*CSsp<~=8qYjuuJY3x("
|
|
1295
|
+
"sECw?N&0@m)@KItu1WD2KGR|aXE=u?Nm!;wHF`$|yS*a)?4}tJWr#}?BRpqO&99VKYlr*0@8"
|
|
1296
|
+
"|tJvQ<v>;eoI%I0VXYi^OVLTU;@^%vsp5EJgf4rVkXcS*$?eBk?Iz8N^@NG5FTHQlLrsGsv?"
|
|
1297
|
+
">i_cuo7o!(e>{TNRRWmy;ur1)cfYU9VZ)tou?ZYkb8_{!ntBf4*iwQ$uXNDfuQ-HlWkwhm53"
|
|
1298
|
+
"mCw}E?v*FW)jv=*t16dRw3o@bceRJZKmyqC6i!t=kAT=m2T=#-"
|
|
1299
|
+
"Ha{_Qsv=+@LC=dpS`DFtL@T)?*#jGeSt;}9L4j?`NwaOb)cAA0zZ7&88Pc3-"
|
|
1300
|
+
"i%R>DXvk4m6bg#yXs~1sPIJVZHF?ozZ=`zymHrspzT>eX$GAQ6<taTzWAAPb>RwK0HCPi>8o"
|
|
1301
|
+
">4qHIMm2>vIjCQgl05)mo)WYwS_>O&K$k$RRq{Xy)9&R)3swl|@9T;ou@3G6~L&mGkzU^TbU"
|
|
1302
|
+
"9A$;V}=$&o|JQI1G2iYH`!Bi_kAd{Pxax8dB-`dmI+V~(RyHnb-sm&HN4Ycz%M{dG5DGm>_S"
|
|
1303
|
+
"I?aF)jsTc*yY$%R{>8ks}~%rgnqqw&<uo|T0nu=;K4=zhcj^y&5$2QCzM~y7ya?%;^EM~8z_"
|
|
1304
|
+
"ieFnQ|}i@_6qIPX0m5b7Bx{#110IW(?YGoz`iz+xTNsb<($lyrU>(!BrYd99leo(nG=DD#Nx"
|
|
1305
|
+
"QnN&+iZv0g)DPz<^7Xqv#DeDQi2tdS@Z}V|gUc?p@QxwQ2Fo}xNhBv2`WQ;E@(Y0on(!8Rw<"
|
|
1306
|
+
"4<ulu?h97u$irMs<)bl`r|?*8CbuSg?OjOptC4HkV?JYkv^`BwG{QF#@(Y_9rBMC{kFB@kX}"
|
|
1307
|
+
"a9^y4RQT7SQHbLlw;>7z1@cqv`8=Sw9IrsY&`C(9nmV*KO3zN)Zg$022Wnu*5H^vC-"
|
|
1308
|
+
"Q1NXZ@f~1>AV3uFTT|>zS97e)Q;1x6bA0!Pwiv|x;f!W`MSKTk;Q19@V=tV1#`i+h*~9!D%q"
|
|
1309
|
+
";;HFOl5Yjl%-qYW3HNG%nf5zLyghc!uzRL3iUgEhsHzU`ArBOt|lS%uXwZHcBAhG9M_W-"
|
|
1310
|
+
"+Z_`@CCdl%y34vQ!~eW2TxJLi}!;k1e<de*|r7G$6#y*NkP(5meUGN(*^^2?C_x@S?EV?gbs"
|
|
1311
|
+
"l0mXV6KR-"
|
|
1312
|
+
"0b!l({KpN<H=sIQSz~Bf3)c+@Q~x`EU)`eCU>81%@4w$O0`137_aRtdb;lzXI1!t(~LJCwPM"
|
|
1313
|
+
"p*M^g8LwNK&D{jN%o4_OkK}ovH*#^B2GD16=D^yO|NVa+ce%SV{|9Qi4b=kCg>#v|g4d)*sU"
|
|
1314
|
+
"v2!m(iP;(QcBZZbbiH@bQGCWvDYM$ApLTACV`-&4n-"
|
|
1315
|
+
"p%MV6o?lSk(!U+o`w^U8ThM7t=^GcjpfDqS-94~w-w>0DYDin8Ce)(<mf85638v<Kc?-"
|
|
1316
|
+
"Bbj=(z2O65@z6wma$&wP|W|SWiw~z|Inbx-"
|
|
1317
|
+
"<@7)P~h2{dvxGwT<#Y}ptv>{EsM|MS19?FM*5k0H9cJE`L8#BLLK`(I`VyJ0wI)QiJGlOTaE"
|
|
1318
|
+
"bG0;u7-b*@PgUv=A^nfv?BPcL$YBb6EBhM>atgZpbiw?)@TG_;=LdJDJW;T2h1vI-5nZ-"
|
|
1319
|
+
"Lr`RPJ$goV-BW@B`ZVs@!n0<qUY3(Q4uDq)DzY>>s22e7GNmlt&93^^3@E_s<vQC=2Iq2)i1"
|
|
1320
|
+
"AWbjX;OJQMn)?$Jv#v@khCzIqoAljj>@ifD-L({kj$KDE-"
|
|
1321
|
+
"Eu>UGyjhV<Gh5#+Y}>huGPu!9Zr8v<OdaY63q<%_$tHRM99^XrCL#-psFZ--KT5n%DanoeK*"
|
|
1322
|
+
"cj|&jlY5;?*mm;#`TQWB^t|yvXF49RNE}yweyv0Tt&g%#HB2EK3n|S>n~zx+sm~Q@c(2<i43"
|
|
1323
|
+
"j{tL?sxY;@0S|m72FVQ9?ROgF7>rX*-"
|
|
1324
|
+
"_r1GTnP0;D`q6|i{+ZwZn=!cNqRv}%{jrAW?tb*1LQN4G1{do8T|WN5R}eJRe=jD#7Vx^(`H"
|
|
1325
|
+
"z59=zmo{Ujtv)1OEY5!u+r5;A`;fD#brwHTeHu&EmD3*W~>_a)J^5-"
|
|
1326
|
+
"&Fo<DX)p(f28;#{a0G}wSd=&@;?G{F<+(4uc5C~lz*T`IR7PKc@2N<6#s!Ull&JK`5OM(+W&"
|
|
1327
|
+
"zklKyYoS5t(8|LZOC%kBU2LlN@7KK?(+6cj@"
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
EMPTY_XLAM_BYTES: bytes = zlib.decompress(base64.b85decode(_EMPTY_XLAM_B85))
|
|
1331
|
+
# --- end XLAM template ---
|
|
@@ -55,10 +55,11 @@ class ExcelFile(VBAHostFile):
|
|
|
55
55
|
VBA project (``ThisWorkbook``, ``Sheet1``, and a bare ``Module1``)
|
|
56
56
|
and return an open :class:`ExcelFile` for it.
|
|
57
57
|
|
|
58
|
-
Supported extensions: ``.xlsm`` (default)
|
|
58
|
+
Supported extensions: ``.xlsm`` (default), ``.xlsb``, and
|
|
59
|
+
``.xlam`` (Excel add-in).
|
|
59
60
|
|
|
60
61
|
The bytes are decoded from a baked-in template captured from a
|
|
61
|
-
freshly Excel-authored
|
|
62
|
+
freshly Excel-authored file, so the resulting file opens
|
|
62
63
|
cleanly in Excel without any "found a problem" repair prompt.
|
|
63
64
|
|
|
64
65
|
``path`` is overwritten if it already exists.
|
|
@@ -68,6 +69,9 @@ class ExcelFile(VBAHostFile):
|
|
|
68
69
|
if suffix == ".xlsb":
|
|
69
70
|
from pyopenvba._templates import EMPTY_XLSB_BYTES
|
|
70
71
|
template = EMPTY_XLSB_BYTES
|
|
72
|
+
elif suffix == ".xlam":
|
|
73
|
+
from pyopenvba._templates import EMPTY_XLAM_BYTES
|
|
74
|
+
template = EMPTY_XLAM_BYTES
|
|
71
75
|
else:
|
|
72
76
|
from pyopenvba._templates import EMPTY_XLSM_BYTES
|
|
73
77
|
template = EMPTY_XLSM_BYTES
|
|
@@ -24,7 +24,10 @@ Critical implementation traps (guide section 31)
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
+
import codecs
|
|
27
28
|
import struct
|
|
29
|
+
import unicodedata
|
|
30
|
+
import warnings
|
|
28
31
|
from collections.abc import Callable, Iterable
|
|
29
32
|
from dataclasses import dataclass, field
|
|
30
33
|
from enum import Enum
|
|
@@ -451,6 +454,22 @@ class VBAReference:
|
|
|
451
454
|
libid_secondary: str = "" # twiddled / relative libid where applicable
|
|
452
455
|
|
|
453
456
|
|
|
457
|
+
def _prefer_unicode(ansi: str, unicode_value: str) -> str:
|
|
458
|
+
"""Return the authoritative value of an ANSI / UTF-16 dir record pair.
|
|
459
|
+
|
|
460
|
+
The UTF-16 partner record is lossless by construction, while the ANSI
|
|
461
|
+
record is limited to the project's code page. When the two disagree
|
|
462
|
+
the ANSI side is the damaged one -- either its bytes were undecodable
|
|
463
|
+
(U+FFFD) or the writer had already substituted ``?`` for characters
|
|
464
|
+
outside the page -- so the Unicode record wins (GitHub issue #12).
|
|
465
|
+
Well-formed files carry identical values and are unaffected.
|
|
466
|
+
"""
|
|
467
|
+
cleaned = unicode_value.rstrip("\x00")
|
|
468
|
+
if cleaned and cleaned != ansi:
|
|
469
|
+
return cleaned
|
|
470
|
+
return ansi
|
|
471
|
+
|
|
472
|
+
|
|
454
473
|
def _parse_dir_stream(raw: bytes) -> tuple[_DirInfo, list[_ModuleInfo]]:
|
|
455
474
|
"""
|
|
456
475
|
Parse a decompressed dir stream.
|
|
@@ -663,16 +682,28 @@ def _parse_dir_stream(raw: bytes) -> tuple[_DirInfo, list[_ModuleInfo]]:
|
|
|
663
682
|
# Do not commit: REFERENCEORIGINAL is always followed by REFERENCECONTROL.
|
|
664
683
|
|
|
665
684
|
# ------------- MODULE records -----------------------------
|
|
685
|
+
# Each MBCS record is followed by its UTF-16 partner. When the
|
|
686
|
+
# MBCS decode was lossy -- the project's code page could not
|
|
687
|
+
# represent the name, or the file declares a page we could not
|
|
688
|
+
# resolve -- the Unicode record is the authoritative lossless
|
|
689
|
+
# value, so prefer it (GitHub issue #12).
|
|
666
690
|
elif record_id == 0x0047 and current is not None: # MODULENAMEUNICODE
|
|
667
691
|
current.name_unicode = data.decode("utf-16-le", errors="replace")
|
|
692
|
+
current.name = _prefer_unicode(current.name, current.name_unicode)
|
|
668
693
|
elif record_id == 0x001A and current is not None: # MODULESTREAMNAME
|
|
669
694
|
current.stream_name = data.decode(_enc(), errors="replace")
|
|
670
695
|
elif record_id == 0x0032 and current is not None: # MODULESTREAMNAME unicode
|
|
671
696
|
current.stream_name_unicode = data.decode("utf-16-le", errors="replace")
|
|
697
|
+
current.stream_name = _prefer_unicode(
|
|
698
|
+
current.stream_name, current.stream_name_unicode
|
|
699
|
+
)
|
|
672
700
|
elif record_id == 0x001C and current is not None: # MODULEDOCSTRING (MBCS)
|
|
673
701
|
current.doc_string = data.decode(_enc(), errors="replace")
|
|
674
702
|
elif record_id == 0x0048 and current is not None: # MODULEDOCSTRINGUNICODE
|
|
675
703
|
current.doc_string_unicode = data.decode("utf-16-le", errors="replace")
|
|
704
|
+
current.doc_string = _prefer_unicode(
|
|
705
|
+
current.doc_string, current.doc_string_unicode
|
|
706
|
+
)
|
|
676
707
|
elif record_id == 0x0031 and current is not None: # MODULEOFFSET
|
|
677
708
|
if len(data) >= 4:
|
|
678
709
|
current.text_offset = int(struct.unpack_from("<I", data, 0)[0])
|
|
@@ -1122,15 +1153,148 @@ class VBAProject:
|
|
|
1122
1153
|
# Write-back helpers
|
|
1123
1154
|
# ---------------------------------------------------------------------------
|
|
1124
1155
|
|
|
1156
|
+
# Windows code-page identifiers whose portable Python codec is not
|
|
1157
|
+
# spelled ``cp<N>``.
|
|
1158
|
+
#
|
|
1159
|
+
# These aliases are not merely a convenience: on Windows, CPython falls
|
|
1160
|
+
# through to the operating system's code-page registry, so ``cp28592``
|
|
1161
|
+
# and friends resolve there but raise LookupError on Linux and macOS.
|
|
1162
|
+
# Relying on the ``cp<N>`` spelling therefore makes a project's text
|
|
1163
|
+
# decode correctly on one platform and turn into latin-1 mojibake on
|
|
1164
|
+
# another -- a platform-dependent data bug (caught by the cross-OS
|
|
1165
|
+
# language-matrix CI job on its first run; see GitHub issue #13).
|
|
1166
|
+
# Consulting this table first keeps behavior identical everywhere.
|
|
1167
|
+
#
|
|
1168
|
+
# Pages VBA hosts write that Python does spell ``cp<N>`` portably (874,
|
|
1169
|
+
# 932, 936, 949, 950, 1250-1258, 1361, 65001) are deliberately absent.
|
|
1170
|
+
_CODEPAGE_ALIASES: dict[int, str] = {
|
|
1171
|
+
# Macintosh
|
|
1172
|
+
10000: "mac_roman",
|
|
1173
|
+
10004: "mac_arabic",
|
|
1174
|
+
10006: "mac_greek",
|
|
1175
|
+
10007: "mac_cyrillic",
|
|
1176
|
+
10010: "mac_romanian",
|
|
1177
|
+
10029: "mac_latin2",
|
|
1178
|
+
10079: "mac_iceland",
|
|
1179
|
+
10081: "mac_turkish",
|
|
1180
|
+
10082: "mac_croatian",
|
|
1181
|
+
# KOI8
|
|
1182
|
+
20866: "koi8_r",
|
|
1183
|
+
21866: "koi8_u",
|
|
1184
|
+
# ISO 8859
|
|
1185
|
+
28591: "iso8859_1",
|
|
1186
|
+
28592: "iso8859_2",
|
|
1187
|
+
28593: "iso8859_3",
|
|
1188
|
+
28594: "iso8859_4",
|
|
1189
|
+
28595: "iso8859_5",
|
|
1190
|
+
28596: "iso8859_6",
|
|
1191
|
+
28597: "iso8859_7",
|
|
1192
|
+
28598: "iso8859_8",
|
|
1193
|
+
28599: "iso8859_9",
|
|
1194
|
+
28603: "iso8859_13",
|
|
1195
|
+
28605: "iso8859_15",
|
|
1196
|
+
# ISO 2022 / EUC / GB / misc
|
|
1197
|
+
50220: "iso2022_jp",
|
|
1198
|
+
50225: "iso2022_kr",
|
|
1199
|
+
51932: "euc_jp",
|
|
1200
|
+
51936: "gb2312",
|
|
1201
|
+
51949: "euc_kr",
|
|
1202
|
+
52936: "hz",
|
|
1203
|
+
54936: "gb18030",
|
|
1204
|
+
65000: "utf_7",
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
|
|
1125
1208
|
def _encoding_for_codepage(code_page: int) -> str:
|
|
1209
|
+
"""Map a PROJECTCODEPAGE value to a Python codec name.
|
|
1210
|
+
|
|
1211
|
+
Falls back to ``latin-1`` for pages Python cannot resolve, warning
|
|
1212
|
+
so the degradation is visible: silently decoding a foreign code page
|
|
1213
|
+
as latin-1 produces mojibake that survives round-trip checks (see
|
|
1214
|
+
GitHub issue #12).
|
|
1215
|
+
"""
|
|
1216
|
+
alias = _CODEPAGE_ALIASES.get(code_page)
|
|
1217
|
+
if alias is not None:
|
|
1218
|
+
return alias
|
|
1219
|
+
encoding = f"cp{code_page}"
|
|
1126
1220
|
try:
|
|
1127
|
-
encoding
|
|
1128
|
-
"".encode(encoding)
|
|
1221
|
+
codecs.lookup(encoding)
|
|
1129
1222
|
except LookupError:
|
|
1130
|
-
|
|
1223
|
+
warnings.warn(
|
|
1224
|
+
f"No Python codec for VBA code page {code_page}; falling back "
|
|
1225
|
+
"to latin-1. Text outside the Latin-1 range will be mojibake. "
|
|
1226
|
+
"Please report this code page at "
|
|
1227
|
+
"https://github.com/WilliamSmithEdward/pyOpenVBA/issues",
|
|
1228
|
+
UserWarning,
|
|
1229
|
+
stacklevel=3,
|
|
1230
|
+
)
|
|
1231
|
+
return "latin-1"
|
|
1131
1232
|
return encoding
|
|
1132
1233
|
|
|
1133
1234
|
|
|
1235
|
+
def encode_mbcs(text: str, encoding: str) -> bytes:
|
|
1236
|
+
"""Encode ``text`` to an MBCS code page, salvaging composed characters.
|
|
1237
|
+
|
|
1238
|
+
Equivalent to ``text.encode(encoding, errors="replace")`` except for
|
|
1239
|
+
characters the codec cannot represent directly but *can* represent as
|
|
1240
|
+
a base character plus combining marks. Python's charmap codecs do no
|
|
1241
|
+
composition, so Vietnamese text destined for cp1258 loses every
|
|
1242
|
+
stacked-diacritic character:
|
|
1243
|
+
|
|
1244
|
+
'Tiếng Việt'.encode('cp1258', errors='replace')
|
|
1245
|
+
-> b'Ti?ng Vi?t'
|
|
1246
|
+
|
|
1247
|
+
cp1258 stores ``ệ`` as precomposed ``ê`` (0xEA) plus a combining
|
|
1248
|
+
dot-below byte (0xF2), which is *not* the character's canonical
|
|
1249
|
+
decomposition, so NFD alone does not help either. For each
|
|
1250
|
+
unmappable character this helper decomposes to NFD and tries folding
|
|
1251
|
+
each combining mark back into the base in turn, keeping the first
|
|
1252
|
+
combination the codec accepts and emitting the remaining marks as
|
|
1253
|
+
combining bytes. Decoding those bytes yields a canonically
|
|
1254
|
+
equivalent (NFD-ish) string, so compare with NFC normalization.
|
|
1255
|
+
|
|
1256
|
+
Characters that remain unmappable become ``?``, matching
|
|
1257
|
+
``errors="replace"``. Text that the codec already encodes directly
|
|
1258
|
+
is returned byte-for-byte unchanged, so this never alters output for
|
|
1259
|
+
projects that work today.
|
|
1260
|
+
"""
|
|
1261
|
+
try:
|
|
1262
|
+
return text.encode(encoding)
|
|
1263
|
+
except UnicodeEncodeError:
|
|
1264
|
+
pass
|
|
1265
|
+
out = bytearray()
|
|
1266
|
+
for ch in text:
|
|
1267
|
+
try:
|
|
1268
|
+
out += ch.encode(encoding)
|
|
1269
|
+
continue
|
|
1270
|
+
except UnicodeEncodeError:
|
|
1271
|
+
pass
|
|
1272
|
+
decomposed = unicodedata.normalize("NFD", ch)
|
|
1273
|
+
if len(decomposed) > 1:
|
|
1274
|
+
base, marks = decomposed[0], list(decomposed[1:])
|
|
1275
|
+
folded: bytes | None = None
|
|
1276
|
+
for i, mark in enumerate(marks):
|
|
1277
|
+
combined = unicodedata.normalize("NFC", base + mark)
|
|
1278
|
+
if len(combined) != 1:
|
|
1279
|
+
continue # this mark does not compose with the base
|
|
1280
|
+
rest = "".join(marks[:i] + marks[i + 1:])
|
|
1281
|
+
try:
|
|
1282
|
+
folded = combined.encode(encoding) + rest.encode(encoding)
|
|
1283
|
+
except UnicodeEncodeError:
|
|
1284
|
+
continue
|
|
1285
|
+
break
|
|
1286
|
+
if folded is not None:
|
|
1287
|
+
out += folded
|
|
1288
|
+
continue
|
|
1289
|
+
try:
|
|
1290
|
+
out += decomposed.encode(encoding)
|
|
1291
|
+
continue
|
|
1292
|
+
except UnicodeEncodeError:
|
|
1293
|
+
pass
|
|
1294
|
+
out += ch.encode(encoding, errors="replace")
|
|
1295
|
+
return bytes(out)
|
|
1296
|
+
|
|
1297
|
+
|
|
1134
1298
|
def split_attribute_header(source: str) -> tuple[str, str]:
|
|
1135
1299
|
"""
|
|
1136
1300
|
Split a VBA module source into ``(attribute_header, body)``.
|
|
@@ -1316,7 +1480,7 @@ def rebuild_module_stream(module: VBAModule, code_page: int) -> bytes:
|
|
|
1316
1480
|
invalidation logic that Office performs on the prefix remains valid.
|
|
1317
1481
|
"""
|
|
1318
1482
|
encoding = _encoding_for_codepage(code_page)
|
|
1319
|
-
source_bytes = module.source
|
|
1483
|
+
source_bytes = encode_mbcs(module.source, encoding)
|
|
1320
1484
|
compressed = compress(source_bytes)
|
|
1321
1485
|
return module.prefix_bytes + compressed
|
|
1322
1486
|
|
|
@@ -1368,11 +1532,11 @@ def serialize_dir_modules_section(project: VBAProject) -> bytes:
|
|
|
1368
1532
|
name_u = m.name_unicode or m.name
|
|
1369
1533
|
stream = m.stream_name or m.name
|
|
1370
1534
|
stream_u = m.stream_name_unicode or stream
|
|
1371
|
-
out += _pack_record(0x0019, name
|
|
1535
|
+
out += _pack_record(0x0019, encode_mbcs(name, enc))
|
|
1372
1536
|
out += _pack_record(0x0047, name_u.encode("utf-16-le"))
|
|
1373
|
-
out += _pack_record(0x001A, stream
|
|
1537
|
+
out += _pack_record(0x001A, encode_mbcs(stream, enc))
|
|
1374
1538
|
out += _pack_record(0x0032, stream_u.encode("utf-16-le"))
|
|
1375
|
-
out += _pack_record(0x001C, m.doc_string
|
|
1539
|
+
out += _pack_record(0x001C, encode_mbcs(m.doc_string, enc))
|
|
1376
1540
|
out += _pack_record(0x0048, m.doc_string_unicode.encode("utf-16-le"))
|
|
1377
1541
|
out += _pack_record(0x0031, struct.pack("<I", m.text_offset))
|
|
1378
1542
|
out += _pack_record(0x001E, struct.pack("<I", m.help_context))
|
|
@@ -1432,6 +1596,7 @@ def serialize_project_stream(
|
|
|
1432
1596
|
*,
|
|
1433
1597
|
add_modules: list[tuple[str, str]] | None = None,
|
|
1434
1598
|
delete_names: set[str] | None = None,
|
|
1599
|
+
code_page: int = 1252,
|
|
1435
1600
|
) -> bytes:
|
|
1436
1601
|
"""
|
|
1437
1602
|
Rewrite a PROJECT stream's plain-text body to apply pending mutations.
|
|
@@ -1454,7 +1619,13 @@ def serialize_project_stream(
|
|
|
1454
1619
|
``[Host Extender Info]``) is preserved byte-for-byte except for the
|
|
1455
1620
|
targeted substitutions.
|
|
1456
1621
|
|
|
1457
|
-
|
|
1622
|
+
``code_page``:
|
|
1623
|
+
PROJECTCODEPAGE of the owning project. The PROJECT stream is
|
|
1624
|
+
code-page ANSI per [MS-OVBA] 2.3.1, so its ``Module=`` /
|
|
1625
|
+
``Document=`` declarations carry module names in that page.
|
|
1626
|
+
Defaults to 1252 for callers that have no project handy.
|
|
1627
|
+
|
|
1628
|
+
Returns code-page-encoded bytes with CRLF line endings.
|
|
1458
1629
|
"""
|
|
1459
1630
|
add_modules = add_modules or []
|
|
1460
1631
|
delete_names = delete_names or set()
|
|
@@ -1462,10 +1633,8 @@ def serialize_project_stream(
|
|
|
1462
1633
|
# callers may invoke this to run the dedup/normalization pass on a
|
|
1463
1634
|
# PROJECT stream that was previously corrupted by buggy writes (e.g.
|
|
1464
1635
|
# duplicate ``Module=NAME`` lines from a delete-then-readd flow).
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
except LookupError:
|
|
1468
|
-
text = raw.decode("latin-1", errors="replace")
|
|
1636
|
+
encoding = _encoding_for_codepage(code_page)
|
|
1637
|
+
text = raw.decode(encoding, errors="replace")
|
|
1469
1638
|
|
|
1470
1639
|
rename_ci: dict[str, str] = {k.casefold(): v for k, v in rename_map.items()}
|
|
1471
1640
|
delete_ci: set[str] = {n.casefold() for n in delete_names}
|
|
@@ -1579,7 +1748,7 @@ def serialize_project_stream(
|
|
|
1579
1748
|
for name, _ in ws_new:
|
|
1580
1749
|
seen_workspace.add(name.casefold())
|
|
1581
1750
|
|
|
1582
|
-
return ("\r\n".join(out_lines) + "\r\n"
|
|
1751
|
+
return encode_mbcs("\r\n".join(out_lines) + "\r\n", encoding)
|
|
1583
1752
|
|
|
1584
1753
|
|
|
1585
1754
|
def _project_section_end(lines: list[str]) -> int:
|
|
@@ -1725,7 +1894,7 @@ def parse_vba_project(cfb: CFB) -> VBAProject:
|
|
|
1725
1894
|
project_stream_raw = None
|
|
1726
1895
|
if project_stream_raw is not None:
|
|
1727
1896
|
try:
|
|
1728
|
-
ps = parse_project_stream(project_stream_raw)
|
|
1897
|
+
ps = parse_project_stream(project_stream_raw, code_page=code_page)
|
|
1729
1898
|
project.protection = ps.protection
|
|
1730
1899
|
except VBAProjectError:
|
|
1731
1900
|
pass
|
|
@@ -1767,17 +1936,16 @@ class ProjectStream:
|
|
|
1767
1936
|
host_extender_info: list[str] = field(default_factory=lambda: [])
|
|
1768
1937
|
|
|
1769
1938
|
|
|
1770
|
-
def parse_project_stream(raw: bytes) -> ProjectStream:
|
|
1939
|
+
def parse_project_stream(raw: bytes, *, code_page: int = 1252) -> ProjectStream:
|
|
1771
1940
|
"""Parse the plain-text PROJECT stream.
|
|
1772
1941
|
|
|
1773
|
-
The PROJECT stream is
|
|
1774
|
-
|
|
1775
|
-
``
|
|
1942
|
+
The PROJECT stream is code-page ANSI key=value text terminated by
|
|
1943
|
+
CRLF ([MS-OVBA] 2.3.1); pass the project's PROJECTCODEPAGE as
|
|
1944
|
+
``code_page`` so non-Latin module names decode correctly. Empty
|
|
1945
|
+
lines separate the project information block, the ``[Host Extender
|
|
1946
|
+
Info]`` block, and the ``[Workspace]`` block.
|
|
1776
1947
|
"""
|
|
1777
|
-
|
|
1778
|
-
text = raw.decode("cp1252", errors="replace")
|
|
1779
|
-
except LookupError:
|
|
1780
|
-
text = raw.decode("latin-1", errors="replace")
|
|
1948
|
+
text = raw.decode(_encoding_for_codepage(code_page), errors="replace")
|
|
1781
1949
|
|
|
1782
1950
|
out = ProjectStream()
|
|
1783
1951
|
section = "project" # "project" | "host_extender" | "workspace"
|
|
@@ -1846,9 +2014,15 @@ def parse_project_stream(raw: bytes) -> ProjectStream:
|
|
|
1846
2014
|
# PROJECTwm stream parser ([MS-OVBA] 2.3.4.4)
|
|
1847
2015
|
# ---------------------------------------------------------------------------
|
|
1848
2016
|
|
|
1849
|
-
def parse_projectwm(
|
|
2017
|
+
def parse_projectwm(
|
|
2018
|
+
raw: bytes, *, code_page: int = 1252
|
|
2019
|
+
) -> list[tuple[str, str]]:
|
|
1850
2020
|
"""Parse the PROJECTwm stream into (mbcs_name, unicode_name) pairs.
|
|
1851
2021
|
|
|
2022
|
+
``code_page`` selects the MBCS codec for the first name in each pair;
|
|
2023
|
+
pass the project's PROJECTCODEPAGE so non-Latin names decode
|
|
2024
|
+
correctly.
|
|
2025
|
+
|
|
1852
2026
|
Format:
|
|
1853
2027
|
loop:
|
|
1854
2028
|
ModuleName_MBCS : null-terminated bytes
|
|
@@ -1876,10 +2050,9 @@ def parse_projectwm(raw: bytes) -> list[tuple[str, str]]:
|
|
|
1876
2050
|
pos += 2
|
|
1877
2051
|
unicode_name = raw[start:pos].decode("utf-16-le", errors="replace")
|
|
1878
2052
|
pos += 2 # skip the u16=0 terminator
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
mbcs_name = mbcs.decode("latin-1", errors="replace")
|
|
2053
|
+
mbcs_name = mbcs.decode(
|
|
2054
|
+
_encoding_for_codepage(code_page), errors="replace"
|
|
2055
|
+
)
|
|
1883
2056
|
out.append((mbcs_name, unicode_name))
|
|
1884
2057
|
return out
|
|
1885
2058
|
|
|
@@ -1903,7 +2076,7 @@ def serialize_projectwm(
|
|
|
1903
2076
|
if not mbcs:
|
|
1904
2077
|
# An empty MBCS name would terminate the stream prematurely.
|
|
1905
2078
|
continue
|
|
1906
|
-
buf += mbcs
|
|
2079
|
+
buf += encode_mbcs(mbcs, enc)
|
|
1907
2080
|
buf += b"\x00"
|
|
1908
2081
|
buf += unicode_name.encode("utf-16-le", errors="replace")
|
|
1909
2082
|
buf += b"\x00\x00"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb
RENAMED
|
File without changes
|
{pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_document.docm
RENAMED
|
File without changes
|
{pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm
RENAMED
|
File without changes
|
{pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb
RENAMED
|
File without changes
|
{pyopenvba-3.2.0 → pyopenvba-3.4.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|