117 lines
4.8 KiB
Python
117 lines
4.8 KiB
Python
"""
|
|
Standalone function to add comments to a .docx file using zipfile + lxml.
|
|
Use AFTER ContractEditor.save() since ContractEditor rewrites document.xml.
|
|
|
|
Usage:
|
|
from add_comments_to_docx import add_comments_to_docx
|
|
# comments = [(anchor_text, comment_text), ...]
|
|
placed = add_comments_to_docx('/tmp/【修】contract.docx', comments)
|
|
"""
|
|
import zipfile, io
|
|
from datetime import datetime
|
|
from lxml import etree
|
|
|
|
W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
|
R_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
|
|
|
|
def qn(tag):
|
|
return f'{{{W}}}{tag}'
|
|
|
|
def add_comments_to_docx(filepath, comments, author='WB'):
|
|
"""Add comments to a docx file.
|
|
|
|
Args:
|
|
filepath: path to docx (modified in place)
|
|
comments: list of (anchor_text, comment_text) tuples
|
|
author: comment author name (default 'WB')
|
|
|
|
Returns:
|
|
int: number of comments successfully placed
|
|
"""
|
|
with open(filepath, 'rb') as f:
|
|
data = f.read()
|
|
zin = zipfile.ZipFile(io.BytesIO(data))
|
|
buf = io.BytesIO()
|
|
zout = zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED)
|
|
doc_xml = zin.read('word/document.xml')
|
|
doc_tree = etree.fromstring(doc_xml)
|
|
body = doc_tree.find(qn('body'))
|
|
|
|
nsmap = {'w': W, 'r': R_NS}
|
|
comments_xml = etree.Element(qn('comments'), nsmap=nsmap)
|
|
|
|
comment_id = 200
|
|
placed = 0
|
|
|
|
for anchor_text, comment_text in comments:
|
|
cid = str(comment_id)
|
|
comment_id += 1
|
|
|
|
# Create comment element
|
|
comment_el = etree.SubElement(comments_xml, qn('comment'))
|
|
comment_el.set(qn('id'), cid)
|
|
comment_el.set(qn('author'), author)
|
|
comment_el.set(qn('date'), datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ'))
|
|
cp = etree.SubElement(comment_el, qn('p'))
|
|
cr = etree.SubElement(cp, qn('r'))
|
|
ct = etree.SubElement(cr, qn('t'))
|
|
ct.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
|
|
ct.text = comment_text
|
|
|
|
# Find anchor in document
|
|
for p in body.iter(qn('p')):
|
|
runs = p.findall(f'.//{qn("r")}')
|
|
full = ''.join(''.join(t.text or '' for t in r.findall(qn('t'))) for r in runs)
|
|
if anchor_text in full:
|
|
# Insert commentRangeStart at beginning of paragraph
|
|
crs = etree.Element(qn('commentRangeStart'))
|
|
crs.set(qn('id'), cid)
|
|
p.insert(0, crs)
|
|
|
|
# Append commentRangeEnd + reference
|
|
cre = etree.Element(qn('commentRangeEnd'))
|
|
cre.set(qn('id'), cid)
|
|
p.append(cre)
|
|
|
|
ref_run = etree.SubElement(p, qn('r'))
|
|
ref_rpr = etree.SubElement(ref_run, qn('rPr'))
|
|
ref_style = etree.SubElement(ref_rpr, qn('rStyle'))
|
|
ref_style.set(qn('val'), 'CommentReference')
|
|
ref_cr = etree.SubElement(ref_run, qn('commentReference'))
|
|
ref_cr.set(qn('id'), cid)
|
|
|
|
placed += 1
|
|
break
|
|
|
|
# Rewrite zip
|
|
for item in zin.namelist():
|
|
if item == 'word/document.xml':
|
|
zout.writestr(item, etree.tostring(doc_tree, xml_declaration=True, encoding='UTF-8', standalone=True))
|
|
elif item == '[Content_Types].xml':
|
|
ct_xml = zin.read(item)
|
|
ct_tree = etree.fromstring(ct_xml)
|
|
if not any('comments.xml' in (el.get('PartName') or '') for el in ct_tree):
|
|
override = etree.SubElement(ct_tree, 'Override')
|
|
override.set('PartName', '/word/comments.xml')
|
|
override.set('ContentType', 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml')
|
|
zout.writestr(item, etree.tostring(ct_tree, xml_declaration=True, encoding='UTF-8', standalone=True))
|
|
elif item == 'word/_rels/document.xml.rels':
|
|
rels_xml = zin.read(item)
|
|
rels_tree = etree.fromstring(rels_xml)
|
|
if not any('comments.xml' in (el.get('Target') or '') for el in rels_tree):
|
|
rel = etree.SubElement(rels_tree, 'Relationship')
|
|
rel.set('Id', f'rId{len(rels_tree) + 10}')
|
|
rel.set('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments')
|
|
rel.set('Target', 'comments.xml')
|
|
zout.writestr(item, etree.tostring(rels_tree, xml_declaration=True, encoding='UTF-8', standalone=True))
|
|
else:
|
|
zout.writestr(item, zin.read(item))
|
|
|
|
zout.writestr('word/comments.xml', etree.tostring(comments_xml, xml_declaration=True, encoding='UTF-8', standalone=True))
|
|
zout.close()
|
|
zin.close()
|
|
|
|
with open(filepath, 'wb') as f:
|
|
f.write(buf.getvalue())
|
|
return placed
|