annotate.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. """
  2. Utility to annotate academic screenshots with Chinese translations and key content highlights.
  3. Usage: python annotate.py input.png output.png "annotation text" [--position top|bottom] [--color red]
  4. """
  5. import sys
  6. import os
  7. from PIL import Image, ImageDraw, ImageFont, ImageFilter
  8. def find_cjk_font():
  9. """Find a CJK-capable font on the system."""
  10. candidates = [
  11. # Windows
  12. "C:/Windows/Fonts/msyh.ttc", # Microsoft YaHei
  13. "C:/Windows/Fonts/simhei.ttf", # SimHei
  14. "C:/Windows/Fonts/simsun.ttc", # SimSun
  15. "C:/Windows/Fonts/msyhbd.ttc", # YaHei Bold
  16. # macOS
  17. "/System/Library/Fonts/PingFang.ttc",
  18. "/System/Library/Fonts/STHeiti Light.ttc",
  19. # Linux
  20. "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
  21. "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
  22. ]
  23. for path in candidates:
  24. if os.path.exists(path):
  25. return path
  26. return None
  27. def annotate_image(input_path, output_path, annotations, banner_text=None):
  28. """
  29. Add Chinese annotations to an image.
  30. annotations: list of dicts with keys:
  31. - text: Chinese translation/annotation text
  32. - position: 'top' or 'bottom' (default: 'top')
  33. - bg_color: background color (default: semi-transparent red)
  34. - text_color: text color (default: white)
  35. banner_text: optional text to add as a banner at the top
  36. """
  37. img = Image.open(input_path).convert("RGBA")
  38. width, height = img.size
  39. font_path = find_cjk_font()
  40. if not font_path:
  41. print("WARNING: No CJK font found, annotations may not render Chinese characters")
  42. font_path = None
  43. # Create overlay for annotations
  44. overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
  45. draw = ImageDraw.Draw(overlay)
  46. # Calculate font sizes based on image width
  47. if width >= 1200:
  48. banner_font_size = 28
  49. annotation_font_size = 22
  50. elif width >= 800:
  51. banner_font_size = 22
  52. annotation_font_size = 18
  53. else:
  54. banner_font_size = 16
  55. annotation_font_size = 14
  56. try:
  57. if font_path:
  58. banner_font = ImageFont.truetype(font_path, banner_font_size)
  59. annotation_font = ImageFont.truetype(font_path, annotation_font_size)
  60. else:
  61. banner_font = ImageFont.load_default()
  62. annotation_font = ImageFont.load_default()
  63. except Exception:
  64. banner_font = ImageFont.load_default()
  65. annotation_font = ImageFont.load_default()
  66. y_offset = 10
  67. # Add banner text at top
  68. if banner_text:
  69. bbox = draw.textbbox((0, 0), banner_text, font=banner_font)
  70. text_height = bbox[3] - bbox[1]
  71. padding = 12
  72. # Semi-transparent dark background
  73. draw.rectangle(
  74. [0, 0, width, text_height + padding * 2],
  75. fill=(0, 0, 0, 180)
  76. )
  77. # Draw text centered
  78. text_width = bbox[2] - bbox[0]
  79. x = (width - text_width) // 2
  80. draw.text((x, padding), banner_text, fill=(255, 255, 255, 255), font=banner_font)
  81. y_offset = text_height + padding * 2 + 10
  82. # Add annotation labels
  83. for i, ann in enumerate(annotations):
  84. text = ann.get("text", "")
  85. pos = ann.get("position", "top")
  86. bg_color = ann.get("bg_color", (220, 50, 50, 200))
  87. text_color = ann.get("text_color", (255, 255, 255, 255))
  88. if not text:
  89. continue
  90. # Word wrap for long text
  91. lines = []
  92. max_chars_per_line = max(20, width // (annotation_font_size + 2))
  93. for line_text in text.split("\n"):
  94. while len(line_text) > max_chars_per_line:
  95. lines.append(line_text[:max_chars_per_line])
  96. line_text = line_text[max_chars_per_line:]
  97. lines.append(line_text)
  98. # Calculate total height
  99. line_height = annotation_font_size + 6
  100. total_height = len(lines) * line_height + 16
  101. if pos == "bottom":
  102. box_y = height - total_height - 10 - i * (total_height + 5)
  103. else:
  104. box_y = y_offset + i * (total_height + 5)
  105. # Draw background
  106. draw.rectangle(
  107. [10, box_y, width - 10, box_y + total_height],
  108. fill=bg_color
  109. )
  110. # Draw text
  111. for j, line in enumerate(lines):
  112. draw.text(
  113. (20, box_y + 8 + j * line_height),
  114. line,
  115. fill=text_color,
  116. font=annotation_font
  117. )
  118. # Composite
  119. result = Image.alpha_composite(img, overlay)
  120. result = result.convert("RGB")
  121. result.save(output_path, "PNG", quality=95)
  122. print(f"Annotated image saved to: {output_path}")
  123. def add_arrow_annotation(input_path, output_path, arrow_x, arrow_y, label_text, label_pos="right"):
  124. """Add an arrow + label annotation to a specific point on the image."""
  125. img = Image.open(input_path).convert("RGBA")
  126. overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
  127. draw = ImageDraw.Draw(overlay)
  128. font_path = find_cjk_font()
  129. font_size = 20 if img.size[0] >= 800 else 14
  130. try:
  131. font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
  132. except:
  133. font = ImageFont.load_default()
  134. # Draw arrow (simple line + triangle)
  135. arrow_len = 30
  136. if label_pos == "right":
  137. end_x = arrow_x + arrow_len
  138. end_y = arrow_y
  139. elif label_pos == "left":
  140. end_x = arrow_x - arrow_len
  141. end_y = arrow_y
  142. elif label_pos == "down":
  143. end_x = arrow_x
  144. end_y = arrow_y + arrow_len
  145. else:
  146. end_x = arrow_x + arrow_len
  147. end_y = arrow_y
  148. # Draw circle at start point
  149. r = 6
  150. draw.ellipse([arrow_x - r, arrow_y - r, arrow_x + r, arrow_y + r], fill=(255, 50, 50, 230))
  151. # Draw line
  152. draw.line([(arrow_x, arrow_y), (end_x, end_y)], fill=(255, 50, 50, 230), width=3)
  153. # Draw label
  154. bbox = draw.textbbox((0, 0), label_text, font=font)
  155. tw = bbox[2] - bbox[0]
  156. th = bbox[3] - bbox[1]
  157. padding = 6
  158. if label_pos == "right":
  159. lx = end_x + 8
  160. ly = end_y - th // 2
  161. elif label_pos == "left":
  162. lx = end_x - tw - 8
  163. ly = end_y - th // 2
  164. else:
  165. lx = end_x - tw // 2
  166. ly = end_y + 8
  167. draw.rectangle([lx - padding, ly - padding, lx + tw + padding, ly + th + padding], fill=(220, 50, 50, 210))
  168. draw.text((lx, ly), label_text, fill=(255, 255, 255, 255), font=font)
  169. result = Image.alpha_composite(img, overlay)
  170. result.convert("RGB").save(output_path, "PNG", quality=95)
  171. print(f"Arrow annotation saved to: {output_path}")
  172. if __name__ == "__main__":
  173. if len(sys.argv) < 4:
  174. print("Usage: python annotate.py input.png output.png 'annotation text' [--banner 'banner text']")
  175. sys.exit(1)
  176. input_path = sys.argv[1]
  177. output_path = sys.argv[2]
  178. annotation_text = sys.argv[3]
  179. banner = None
  180. if "--banner" in sys.argv:
  181. idx = sys.argv.index("--banner")
  182. if idx + 1 < len(sys.argv):
  183. banner = sys.argv[idx + 1]
  184. annotate_image(input_path, output_path, [{"text": annotation_text}], banner_text=banner)