From be149b367d5236dc3c23192bf8f0d90b6a165265 Mon Sep 17 00:00:00 2001 From: Eric Lapouyade Date: Sat, 9 Sep 2017 12:28:22 +0200 Subject: [PATCH] - Add replace_embedded() method (useful for embedding docx) - Add replace_media() method (useful for header/footer images) --- CHANGES.rst | 5 +- README.rst | 5 + docs/index.rst | 26 ++++- docxtpl/__init__.py | 96 ++++++++++++------- tests/embedded.py | 29 ++++++ tests/header_footer_image.py | 7 +- tests/test_files/cellbg.docx | Bin 8213 -> 8213 bytes tests/test_files/dynamic_table.docx | Bin 5265 -> 5265 bytes tests/test_files/escape.docx | Bin 8449 -> 8449 bytes tests/test_files/header_footer.docx | Bin 12560 -> 12560 bytes tests/test_files/header_footer_entities.docx | Bin 9879 -> 9879 bytes tests/test_files/header_footer_utf8.docx | Bin 13957 -> 13957 bytes tests/test_files/inline_image.docx | Bin 264572 -> 264572 bytes tests/test_files/nested_for.docx | Bin 11541 -> 11541 bytes tests/test_files/order.docx | Bin 9341 -> 9341 bytes tests/test_files/richtext.docx | Bin 8897 -> 8897 bytes tests/test_files/richtext_and_if.docx | Bin 7584 -> 7584 bytes tests/test_files/subdoc.docx | Bin 56198 -> 56198 bytes 18 files changed, 125 insertions(+), 43 deletions(-) create mode 100644 tests/embedded.py diff --git a/CHANGES.rst b/CHANGES.rst index 78fa419..65e9539 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,7 @@ -0.4.0 (2017-09-03) +0.4.0 (2017-09-09) ------------------ -- Add replace_medias() utility (useful for header/footer images) +- Add replace_media() method (useful for header/footer images) +- Add replace_embedded() method (useful for embedding docx) 0.3.9 (2017-06-27) ------------------ diff --git a/README.rst b/README.rst index 86a7db7..57dffc4 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,11 @@ You save the document as a .docx file (xml format) : it will be your .docx templ Now you can use python-docx-template to generate as many word documents you want from this .docx template and context variables you will associate. +Share +----- + +If you like this project, please rate and share it here : http://rate.re/github/elapouya/python-docx-template + Documentation ------------- diff --git a/docs/index.rst b/docs/index.rst index 62becfe..734ca99 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -172,13 +172,30 @@ It is not possible to dynamically add images in header/footer, but you can chang The idea is to put a dummy picture in your template, render the template as usual, then replace the dummy picture with another one. You can do that for all medias at the same time. Note: for images, the aspect ratio will be the same as the replaced image -Note2 : it is important to have the source media files as they are required to calculate their CRC to find them in the docx +Note2 : it is important to have the source media files as they are required to calculate their CRC to find them in the docx. +(dummy file name is not important) -Syntax to replace dummy_header_pic.jpg and dummy2.jpg in mydoc.docx:: +Syntax to replace dummy_header_pic.jpg:: - replace_medias('mydoc.docx',[('dummy_header_pic.jpg','header_pic_i_want.jpg'),('dummy2.png','mycompany.png')]) + tpl.replace_media('dummy_header_pic.jpg','header_pic_i_want.jpg') +dummy_header_pic.jpg must exist in the template directory when rendering and saving the generated docx. It must be the same +file as the one inserted manually in the docx template. + +Replace embedded objects +------------------------ + +It works like medias replacement, except it is for embedded objects like embedded docx. + +Syntax to replace embedded_dummy.docx:: + + tpl.replace_embedded('embdded_dummy.docx','embdded_docx_i_want.docx') + + +embdded_dummy.docx must exist in the template directory when rendering and saving the generated docx. It must be the same +file as the one inserted manually in the docx template. + Jinja custom filters -------------------- @@ -200,7 +217,10 @@ Examples The best way to see how it works is to read examples, they are located in `tests/` directory. Templates and generated .docx files are in `tests/test_files/`. +Share +----- +If you like this project, please rate and share it here : http://rate.re/github/elapouya/python-docx-template .. rubric:: Functions index diff --git a/docxtpl/__init__.py b/docxtpl/__init__.py index 1ea1f44..ca1fdcc 100644 --- a/docxtpl/__init__.py +++ b/docxtpl/__init__.py @@ -29,6 +29,8 @@ class DocxTemplate(object): def __init__(self, docx): self.docx = Document(docx) + self.crc_to_new_media = {} + self.crc_to_new_embedded = {} def __getattr__(self, name): return getattr(self.docx, name) @@ -184,6 +186,65 @@ class DocxTemplate(object): def new_subdoc(self): return Subdoc(self) + @staticmethod + def get_file_crc(filename): + with open(filename, 'rb') as fh: + buf = fh.read() + crc = (binascii.crc32(buf) & 0xFFFFFFFF) + return crc + + def replace_media(self,src_file,dst_file): + """Replace one media by another one into a docx + + This has been done mainly because it is not possible to add images in docx header/footer. + With this function, put a dummy picture in your header/footer, then specify it with its replacement in this function + + Syntax: tpl.replace_media('dummy_media_to_replace.png','media_to_paste.jpg') + + Note: for images, the aspect ratio will be the same as the replaced image + Note2 : it is important to have the source media file as it is required to calculate its CRC to find them in the docx + """ + with open(dst_file, 'rb') as fh: + crc = self.get_file_crc(src_file) + self.crc_to_new_media[crc] = fh.read() + + def replace_embedded(self,src_file,dst_file): + """Replace one embdded object by another one into a docx + + This has been done mainly because it is not possible to add images in docx header/footer. + With this function, put a dummy picture in your header/footer, then specify it with its replacement in this function + + Syntax: tpl.replace_embedded('dummy_doc.docx','doc_to_paste.docx') + + Note2 : it is important to have the source file as it is required to calculate its CRC to find them in the docx + """ + with open(dst_file, 'rb') as fh: + crc = self.get_file_crc(src_file) + self.crc_to_new_embedded[crc] = fh.read() + + def post_processing(self,docx_filename): + if self.crc_to_new_media or self.crc_to_new_embedded: + backup_filename = '%s_docxtpl_before_replace_medias' % docx_filename + os.rename(docx_filename,backup_filename) + + with zipfile.ZipFile(backup_filename) as zin: + with zipfile.ZipFile(docx_filename, 'w') as zout: + for item in zin.infolist(): + buf = zin.read(item.filename) + if item.filename.startswith('word/media/') and item.CRC in self.crc_to_new_media: + zout.writestr(item, self.crc_to_new_media[item.CRC]) + elif item.filename.startswith('word/embeddings/') and item.CRC in self.crc_to_new_embedded: + zout.writestr(item, self.crc_to_new_embedded[item.CRC]) + else: + zout.writestr(item, buf) + + os.remove(backup_filename) + + def save(self,filename,*args,**kwargs): + self.docx.save(filename,*args,**kwargs) + self.post_processing(filename) + + class Subdoc(object): """ Class for subdocument to insert into master document """ def __init__(self, tpl): @@ -298,37 +359,4 @@ class InlineImage(object): return self.xml def __str__(self): - return self.xml - -def replace_medias(docx_filename,src_dst_lst): - """Utility function replace any media by another into a docx - - This has been done mainly because it is not possible to add images in docx header/footer. - With this function, put a dummy picture in your header/footer, then specify it with its replacement in this function - - Syntax: replace_medias('mydoc.docx',[('dummy_header_pic.jpg','header_pic_i_want.jpg'),('dummy2.png','mycompany.png')]) - - Note: for images, the aspect ratio will be the same as the replaced image - Note2 : it is important to have the source media files as they are required to calculate their CRC to find them in the docx - """ - crc_to_new_media = {} - for src,dst in src_dst_lst: - with open(src,'rb') as fh: - buf = fh.read() - crc = (binascii.crc32(buf) & 0xFFFFFFFF) - with open(dst,'rb') as fh: - crc_to_new_media[crc] = fh.read() - - backup_filename = '%s_before_replace_medias' % docx_filename - os.rename(docx_filename,backup_filename) - - with zipfile.ZipFile(backup_filename) as zin: - with zipfile.ZipFile(docx_filename, 'w') as zout: - for item in zin.infolist(): - buf = zin.read(item.filename) - if item.filename.startswith('word/media/') and item.CRC in crc_to_new_media: - zout.writestr(item, crc_to_new_media[item.CRC]) - else: - zout.writestr(item, buf) - - os.remove(backup_filename) + return self.xml \ No newline at end of file diff --git a/tests/embedded.py b/tests/embedded.py new file mode 100644 index 0000000..9274a32 --- /dev/null +++ b/tests/embedded.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +''' +Created : 2017-09-09 + +@author: Eric Lapouyade +''' + +from docxtpl import DocxTemplate + +# rendering the "dynamic embedded docx": +embedded_docx_tpl=DocxTemplate('test_files/embedded_embedded_docx_tpl.docx') +context = { + 'name' : 'John Doe', +} +embedded_docx_tpl.render(context) +embedded_docx_tpl.save('test_files/embedded_embedded_docx.docx') + + +# rendring the main document : +tpl=DocxTemplate('test_files/embedded_main_tpl.docx') + +context = { + 'name' : 'John Doe', +} + +tpl.replace_embedded('test_files/embedded_dummy.docx','test_files/embedded_static_docx.docx') +tpl.replace_embedded('test_files/embedded_dummy2.docx','test_files/embedded_embedded_docx.docx') +tpl.render(context) +tpl.save('test_files/embedded.docx') \ No newline at end of file diff --git a/tests/header_footer_image.py b/tests/header_footer_image.py index 56f34b2..3be62de 100644 --- a/tests/header_footer_image.py +++ b/tests/header_footer_image.py @@ -5,7 +5,7 @@ Created : 2017-09-03 @author: Eric Lapouyade ''' -from docxtpl import DocxTemplate, replace_medias +from docxtpl import DocxTemplate DEST_FILE = 'test_files/header_footer_image.docx' @@ -14,7 +14,6 @@ tpl=DocxTemplate('test_files/header_footer_image_tpl.docx') context = { 'mycompany' : 'The World Wide company', } - +tpl.replace_media('test_files/dummy_pic_for_header.png','test_files/python.png') tpl.render(context) -tpl.save(DEST_FILE) -replace_medias(DEST_FILE,[('test_files/dummy_pic_for_header.png','test_files/python.png')]) \ No newline at end of file +tpl.save(DEST_FILE) \ No newline at end of file diff --git a/tests/test_files/cellbg.docx b/tests/test_files/cellbg.docx index 0a34e07917b637d6ac63799c06bfd03012ad9e20..fb18e65785ca0750104522e49e3e2340bf0d7a37 100644 GIT binary patch delta 199 zcmbR0Fx7!Kz?+#xgn@y9gCQnKb0cpvBQucRJeiRn!dS|*5W?_eDS|Nev1YM=)o636 zLPUbOzp;Wvd<18MMJB%zdJGo0ApDmJ!Vs%s12HE1%G!bHK3Ov`eO%TWOtZ?_fN2*f f-7M!0=AV^w0Mi2UwqQCy-X2U(l830jDDMIQ%au8$ delta 199 zcmbR0Fx7!Kz?+#xgn@y9gTa4^@?R0%s(sV0Hy`xZNYSaygitnBo9%4QQidrK;=EJ diff --git a/tests/test_files/dynamic_table.docx b/tests/test_files/dynamic_table.docx index 987bbf2a8dcc69bad78a2934a540ae88adafaee0..360513512a2aa3fb130aa8d1e2572916121e4748 100644 GIT binary patch delta 165 zcmbQJIZ=~0z?+#xgn@y9gCQnKb0cpiBQucRT*=4_VRSORWCM$oa&m)3CO30^2Mg44 zdqI?(;`zn|7CFo>4-t7PkOoofCu9ebncOdA2BuF4*??(wVRtYcC=B6G5Vi&LF9=(K NXUs7Om61-4i>28 z_JSxm#q*5`EOMA%9wPEoAPu6{Psk1=Gr3>L3{0O8vH{cV!tP)?P#D6WAZ!cfUl6te N)4U?qVA@;64FHCMF;f5l diff --git a/tests/test_files/escape.docx b/tests/test_files/escape.docx index 60e84252d9291177452a0789102210199f3add26..0cff2d0e93770214d563ecf563c6e54d5da01513 100644 GIT binary patch delta 199 zcmZp4YINca@MdNaVPIh3V2DZ5+{oL^$PA=6PiEwYFqSe+g)nSc3LuOPtW7LnHKtsu z5Rq8!Z>(UEFu|)}k;!_(kHG>WB09`qhO_u*HV|X7rJNm@u97nY)0^e2!SqKt8!&Ak g525qr-NF2w@(y77kGw6Iwo$ML({&0Eb$bx3gXxcQHelL7 h9zy5KyMy^Vh>zQ004xLJf{Ev diff --git a/tests/test_files/header_footer.docx b/tests/test_files/header_footer.docx index a69df934560abff8b4cdef66fdbfc0aea2dbe4f0..c300a9cc158edc85effff8a6ee198d3ee8808ffe 100644 GIT binary patch delta 396 zcmbP`G$DyMz?+#xgn@y9gCQnKb0hCLMrI(r`5q%bgzq$?(r>g)KW7LC~VGpHi^&ol}fog8( oLDc+)%310|)RyZ*%-N=I1vcZeKEz!n2A*JkgMl-cK4#zr0ALS!aR2}S delta 396 zcmbP`G$DyMz?+#xgn@y9gTa4^@2;&3OWC){;B^Sc@#=4COtY$q& z8$`r{YbBU5`3d)9u*@l*6%d(HJ~Ieo9lr&H@lqfV!mt&Z17=KK$1k(_hp-GpR9qCQ zAwVn!BC=jwn;C5AU8(o%AjahLN~^(it@1apM41X7L}|O~4_2^DleQh0KCEp9rvGSL zgJ~-r8!%l7rFZDKgZWIlwqV*x*B(r_=t9)}){~xmPgemf#;6A|!yZc4>Ou4{0@d8m ogQ)opm9x}`s4drrn6pja3T(z_eTchE3_QX71_Ng>eayfM0ABckg#Z8m diff --git a/tests/test_files/header_footer_entities.docx b/tests/test_files/header_footer_entities.docx index 4e2f8db0e542e136fab3eb674a6c07755b54a827..31c949c4bec0a4b743a09e007cd4308a60702993 100644 GIT binary patch delta 282 zcmbR4JKdKzz?+#xgn@y9gCRCab0hCkMrI(rc`KtdgmIbaC=*y@Hme(0WbzZXQy_uK z8`-~sC021TL6olNe8CEqnZ^GHEHhIe5+d_eFbl#+5}pEK{1nmP1glYz%Y%qal@Ek4 zUMfrlGbT4G*@2Baq+|xBe=FI5X)9%SFx{dIkw2>J3FZr`ID_eM6$daqO9i6-riv|? bFR5w`k%!V#Rjt7MOR5lagw-JOfog65UYu2F delta 282 zcmbR4JKdKzz?+#xgn@y9gCSsv@1atBs>Me_$i{n309*bmj@AaR$@jDh^a%HR_+E>Xcv4C}*SB&BXF(&Ki zZvoSf^}m57?idJwl}>(dXa|-sFfs$vnMT%NdbyDen0^MORgK-j{Cr~vFulpx7EFIK shNw3%fzWv-5PfS+Ao6cb?7`~vO(EiWrdDA7YEy{4Z%jSGdEypeYgBQucRe1?%9!nnsY2g0ajse>^7v7TmzsNu|j zh?sMKV+D&C2pTeh87acv5XL4^M+oDaxH*JjAY}z%wg1F+%XUUE1mq_&<-qNU}OfSGmWgl^l~E`F#QZls~Wq5`T52UV0x3WEtvjf s3{h`j0-^IvAo|vtK;++=*n`#Sn?l6%Os&BD)us@8-*d)!3ybBnaf%N7Lj4}|$d8SSXBbo&&^^bK4L}V%Z z7Y?xEIRc4bkxfDy6!?I;z29v;@8&ARz~CS>nK4Oh^A)2IA+SPi*Ap^e#<#?e++fD5 z+^szPAT2TR`tn!zGBJGK&op_zk!bVbs`kTGjN1=aF+JG}Qn`JV1@qIbVAiB}%->kS ztnM$&%1j{E^o`$`&w_;-zcX7hgIRljG2i0^OU`3qu>&*iv#^+f84|2ekw{h+Yp}?4 zRu&sD;~I=1%Esaj7KvwLaR4(G!5B|r3=MXuR5pyU4#xNhW0-J2)s%Cv*n-X3&cR|2 OX8h!UT57?`;sO9^m3{;O delta 400 zcmew}N#M^U0p0*_W)=|!1_lm>fF;Tsc^5D;1L@5h7-b-g^Gux(Ml=gl>L2S8h{#g* zFC1XSa|9B>BAbLZDDVMwd%xRy-py5rfx$s&GGmh1<|{@aLSTj3t|w%`jBkk_xxtKA zxm$VoL0V$s_2sYbWn%cepK0=bBhlu=RqcnX7`Gp;VtTR{q;mTz3+AU=!K_K|n7^@t zS>0clm6<@S=^MW>p9KpwerL912DA45V!p=-mYm1JVh3j2XJIh|GbC7{B9W{t)?kt8 ztSmNQ#x)p2l#Rt5EE3Pg;s9nWf-#=L7#i$QscaZy9gOi2#xUW4swwASu?3s6orA?5 O%=pOxwbX)>#RUL@y@dV% diff --git a/tests/test_files/nested_for.docx b/tests/test_files/nested_for.docx index 726f6132cf44e1062fdf34a06d9eff1a91083348..039b4b6adabfcf543af4d87110aea078211c6513 100644 GIT binary patch delta 316 zcmbOlH8qMiz?+#xgn@y9gCRCab0hB_MrI(r`3xgJgmI5)CWKMNk^x~nWld%QtMTT_ z0*g%E!TlI4Fpp;rM2Rh*3xv_a?*w6-6j%XalnAYaFcd_-v4V9;N<}e)8D+9Q93aNz zqpGXHbg|kuutbi!09eoDdJQ|U!~qR6F#S`*8cdsO+JNaYD7{tF9nAl)X%D6ywQRw3 tqZUN{eklD{3!=|P8zNt$?EqG{UmGI+PumL2x7LB!TchI%=AY2<1po-TU@HIs delta 316 zcmbOlH8qMiz?+#xgn@y9gCSsv@?sLh!R^q7YL(;-wDDvDX;>0-5SV2K=c0kEFQ^%{0yi31vDVEU(qHJCQnv;os)Pk diff --git a/tests/test_files/order.docx b/tests/test_files/order.docx index f96803613e6e1afcb05aa6f331dc6a5f51e074c2..57b4ac3e5091992fd494c4bdfa0a895f88571e31 100644 GIT binary patch delta 199 zcmezC@z;Ylz?+#xgn@y9gCRCab0cpvBQucRJeiRn!dS}0$^;f!%TfRlQDB?K0v6fH zr3w*w$o-8KEOJY5HdthGh45prK%B@gi0)|da4wL@&*1&d7nB>WgGa6?3k8O&gn_{a`oO#Ufv2d1qQ%)oT9f;E`ls9*!8 hKR{`HMRzd2QqciS?@_b`(|;80!L*$cM18H23joCAIuZZ? delta 199 zcmX@;deD_Oz?+#xgn@y9gCSsv@3rJ@6v-lJ#>rvE6~gK0Y@i27P37XUgmJ{$l5 diff --git a/tests/test_files/richtext_and_if.docx b/tests/test_files/richtext_and_if.docx index c7a748cd94ae6485824f981f34785159579f8be5..27516873f4e57c2e8c015bf0dcb4f5beb3dc3adb 100644 GIT binary patch delta 199 zcmZ2ry}+6`z?+#xgn@y9gCRCab0cpvBQucRJeiRn!dS{Q8N#q;$%HVLvrb_GtFhxy zg@~kaeq#lT#PiPqi%k9}@E9!cMDPPd?RAlVU?r1pNZEm8CQC}2f$1P=YcM@Q+6GLY ghthm9?qGg|j02dSEn^F&Z_C(&X<1o_`WRUk05x+uWB>pF delta 199 zcmZ2ry}+6`z?+#xgn@y9gCSsv@iQor_+Up|!z)B|Hkg@~GOqP^31Jgm$)?j*qv<;X( g52g8J+`;?^83!;uTgDbl-!wuOP2ru delta 218 zcmZqM&fK=0nK!_jnMH(wfq{b|V2ScZ-X2C~Aia4GBR_<(j%hK35yDamVVq$NX9ufM z<28qfB=9}m3KsD=@{JYDa6GjaEH#<&%ww>?^E1ZGV2134Xbuo#a`g>6Fumu78JPZh u!x~H*-?RbKMNoRdZFdVoH{P-Z^AFy#2h+^AA^Mzdy8r-uS5N-{