【Problem】docxtpl替换图片失败
编辑问题描述
使用 docxtpl 库,替换 docx 文件中的图片失败
# 代码
template = DocxTemplate(template_path)
template.replace_pic(base_pic_name, replace_pic_path)
template.save(save_path)
截图
...
解决方案
问题分析
doxctpl 源码中关于图片替换的部分
def replace_pic(self, embedded_file, dst_file):
"""Replace embedded picture with original-name given by embedded_file.
(give only the file basename, not the full path)
The new picture is given by dst_file (either a filename or a file-like
object)
Notes:
1) embedded_file and dst_file must have the same extension/format
in case dst_file is a file-like object, no check is done on
format compatibility
2) the aspect ratio will be the same as the replaced image
3) There is no need to keep the original file (this is not the case
for replace_embedded and replace_media)
"""
if hasattr(dst_file, 'read'):
# NOTE: file extension not checked
self.pic_to_replace[embedded_file] = dst_file.read()
else:
emp_path, emb_ext = os.path.splitext(embedded_file)
dst_path, dst_ext = os.path.splitext(dst_file)
if emb_ext != dst_ext:
raise ValueError('replace_pic: extensions must match')
with open(dst_file, 'rb') as fh:
self.pic_to_replace[embedded_file] = fh.read()
def save(self, filename, *args, **kwargs):
self.pre_processing()
self.docx.save(filename, *args, **kwargs)
self.post_processing(filename)
def pre_processing(self):
if self.pic_to_replace:
self.build_pic_map()
# Do the actual replacement
for embedded_file, stream in six.iteritems(self.pic_to_replace):
if embedded_file not in self.pic_map:
raise ValueError('Picture "%s" not found in the docx template'
% embedded_file)
self.pic_map[embedded_file][1]._blob = stream
def build_pic_map(self):
"""Searches in docx template all the xml pictures tag and store them
in pic_map dict"""
if self.pic_to_replace:
# Main document
part = self.docx.part
self.pic_map.update(self._img_filename_to_part(part))
# Header/Footer
for relid, rel in six.iteritems(self.docx.part.rels):
if rel.reltype in (REL_TYPE.HEADER, REL_TYPE.FOOTER):
self.pic_map.update(self._img_filename_to_part(rel.target_part))
经分析,替换图片的整个代码逻辑是这样的:
执行
replace_pic()
方法的时候,他会把实例的pic_to_replace
属性(字典类型,默认为空),添加一项,key
为被替换的图片名(不含路径的basename
),`value` 为用来替换的图片的二进制数据。进行文档保存(执行
save
方法)的时候,会有一个预检查的过程,在遇见查的过程中,如果发现pic_to_replace
属性不为空,说明有图片替换的操作,他会执行两个操作先根据文档的元数据(
part
),来构建整个文档中关于图片的pic_map
,pic_map
是一个字典,key
值为文档中的图片的basename
,`value` 包含该图片的所有相关信息,其中,_blob(self.pic_map[embedded_file][1]._blob)
属性中含有该图片的二进制信息(embedded_file
就是basename
)得到上述的
pic_map
以后,把被替换图片的_blob
信息换成用来替换的图片的_blob
信息
这里面有几个暗坑需要注意一下
a. 在执行
replace_pic
方法的时候,他会对来替换的图片和被替换的图片做类型检查,不一致会抛出异常。b. 如果你要替换的图片,在文档中不存在,也会报错(比如,我要把文档中的
python.png
替换成go.png
,如果文档中不含有python.png
图片则会抛出异常)之所以要修复第二个问题,是因为在我的业务逻辑中,我有很多文件,要执行
python.png->go.png
的替换,但是并不是所有的文件都含有python.png
,这种情况无需替换,但也无需抛出异常第一个问题坑的地方在于,在中文环境下,你直接插入图片,他默认的
basename
是不含有扩展名的「图片 1」、「图片 2」,如下图,这种情况下,无论你拿什么类型的图片来替换都是过不了类型检查的经过试验,中文环境下,只有「被替换图片」被插入的时候的时候选择「链接到文件」或者「插入和链接」才可以使的
basename
正常显示为「test.png」,才可以使「替换图片」通过类型检测但是这个时候带来了另一个问题,就是在「链接到文件」和「插入和链接」的情况下,执行
replace_pic
是无效的,也就是说在上面的替换流程 b 里面得到上述的
pic_map
以后,把被替换图片的_blob
信息换成用来替换的图片的_blob
信息也不行经验证,替换确实替换成功了,之所以还会显示替换之前的图片,我个人的一个猜想是,在「word」中,如果你采用了「链接到文件」或「插入和链接」的方式插入图片,它显示图片的逻辑是通过链接,而不是图片文件
_blob
信息,所以即使你替换了_blob
信息,「word」 还是会链接到源文件,经测试,上述操作以后,删除掉被链接的源文件也不行。猜测:有专门一个地方保存「原始文件」的「链接信息」 和
_blob
信息(也有可能是某些缓存问题?)
解决方法
源文件采用正常的「插入图片」方式
修复掉中文环境下的插入图片会自动变成「图片 1」这种格式(为了后续通过类型检查)
hook 掉图片不存在时候的异常
修复代码
def pre_processing(self):
if self.pic_to_replace:
self.build_pic_map()
print(self.pic_map)
for embedded_file, stream in six.iteritems(self.pic_to_replace):
if embedded_file not in self.pic_map:
continue
self.pic_map[embedded_file][1]._blob = stream
def _replace_pic(self, embedded_file, dst_file):
if hasattr(dst_file, 'read'):
self.pic_to_replace[embedded_file] = dst_file.read()
else:
with open(dst_file, 'rb') as fh:
self.pic_to_replace[embedded_file] = fh.read()
template = DocxTemplate(template_path)
DocxTemplate.pre_processing = pre_processing
DocxTemplate.replace_pic = replace_pic
template.replace_pic(base_pic_name, replace_pic_path)
template.save(save_path)
这样的话,我们的完整操作就是:「插入图片」,然后执行
replace_pic(“图片 1”, "logo.png")
即可
补充
2021-01-15:现在最新版的 docxtpl 已经去掉了类型检查
2021-01-18:发现新问题
在用 docx 模块执行 doc->docx 的转换之后,在 doxctpl 中执行
build_pic_map()
后,self.pic_map
为空,也就是说无法构建整个文档的图片映射,获取不到文档中的图片信息。原因: 版本兼容性原因
正常docx文件的图片格式,他的图片选中框周围的圆点矩阵是空心圆形
通过 docx 模块执行 doc->docx 转换之后的 docx 中的图片样式,图片选中框周围的圆点是实心矩形(是一种 docx 内置的元数据类型,不是 docx 标准下的图片)
解决方案:进行兼容性转换即可
- 0
- 0
-
赞助
微信 -
分享