image_service.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. """image_service.py — 图片提取和处理服务"""
  2. import base64
  3. import json
  4. from typing import Dict, List
  5. from docx import Document
  6. from docx.enum.text import WD_ALIGN_PARAGRAPH
  7. def extract_images_from_word(doc: Document) -> List[Dict]:
  8. """从 Word 文档提取图片及基本样式信息
  9. Args:
  10. doc: python-docx Document 对象
  11. Returns:
  12. 图片列表,每个元素包含:
  13. - paragraph_index: 图片所在段落索引
  14. - data_url: Base64 编码的 Data URL
  15. - style: 样式信息(宽度、高度、对齐方式、段落样式)
  16. """
  17. images = []
  18. # 建立 rel_id -> 图片数据映射
  19. image_parts = {}
  20. for rel in doc.part.rels.values():
  21. if "image" in rel.target_ref:
  22. image_parts[rel.rId] = {
  23. "blob": rel.target_part.blob,
  24. "content_type": rel.target_part.content_type
  25. }
  26. # 遍历段落查找图片
  27. for para_idx, paragraph in enumerate(doc.paragraphs):
  28. for run in paragraph.runs:
  29. # 查找 inline 图片(w:drawing 元素)
  30. for drawing in run._element.findall('.//{http://schemas.openxmlformats.org/wordprocessingml/2006/main}drawing'):
  31. # 提取图片引用
  32. blip = drawing.find('.//{http://schemas.openxmlformats.org/drawingml/2006/main}blip')
  33. if blip is None:
  34. continue
  35. rel_id = blip.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
  36. if not rel_id or rel_id not in image_parts:
  37. continue
  38. # 提取尺寸(EMU 转厘米,1厘米 = 360000 EMU)
  39. extent = drawing.find('.//{http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing}extent')
  40. width_emu = int(extent.get('cx')) if extent is not None else 360000 * 10 # 默认 10cm
  41. height_emu = int(extent.get('cy')) if extent is not None else 360000 * 7 # 默认 7cm
  42. width_cm = round(width_emu / 360000, 2)
  43. height_cm = round(height_emu / 360000, 2)
  44. # 获取对齐方式
  45. align = 'left'
  46. if paragraph.alignment == WD_ALIGN_PARAGRAPH.CENTER:
  47. align = 'center'
  48. elif paragraph.alignment == WD_ALIGN_PARAGRAPH.RIGHT:
  49. align = 'right'
  50. # 获取段落样式名称
  51. para_style = paragraph.style.name if paragraph.style else "Normal"
  52. # Base64 编码
  53. image_blob = image_parts[rel_id]['blob']
  54. content_type = image_parts[rel_id]['content_type']
  55. b64_data = base64.b64encode(image_blob).decode('utf-8')
  56. data_url = f"data:{content_type};base64,{b64_data}"
  57. images.append({
  58. "paragraph_index": para_idx,
  59. "data_url": data_url,
  60. "style": {
  61. "width": width_cm,
  62. "height": height_cm,
  63. "align": align,
  64. "unit": "cm",
  65. "para_style": para_style # 新增:段落样式
  66. }
  67. })
  68. return images
  69. def create_image_markdown(data_url: str, style: Dict, alt: str = "图片") -> str:
  70. """生成带样式注释的图片 Markdown
  71. Args:
  72. data_url: Base64 编码的 Data URL
  73. style: 样式字典(width, height, align)
  74. alt: 图片替代文本
  75. Returns:
  76. 格式化的 Markdown 字符串
  77. Example:
  78. <!-- img-style: {"width": 4.0, "height": 3.0, "align": "center"} -->
  79. ![图片](data:image/png;base64,...)
  80. """
  81. style_json = json.dumps(style, ensure_ascii=False)
  82. return f'<!-- img-style: {style_json} -->\n![{alt}]({data_url})\n'