| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- """
- Utility to annotate academic screenshots with Chinese translations and key content highlights.
- Usage: python annotate.py input.png output.png "annotation text" [--position top|bottom] [--color red]
- """
- import sys
- import os
- from PIL import Image, ImageDraw, ImageFont, ImageFilter
- def find_cjk_font():
- """Find a CJK-capable font on the system."""
- candidates = [
- # Windows
- "C:/Windows/Fonts/msyh.ttc", # Microsoft YaHei
- "C:/Windows/Fonts/simhei.ttf", # SimHei
- "C:/Windows/Fonts/simsun.ttc", # SimSun
- "C:/Windows/Fonts/msyhbd.ttc", # YaHei Bold
- # macOS
- "/System/Library/Fonts/PingFang.ttc",
- "/System/Library/Fonts/STHeiti Light.ttc",
- # Linux
- "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
- "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
- ]
- for path in candidates:
- if os.path.exists(path):
- return path
- return None
- def annotate_image(input_path, output_path, annotations, banner_text=None):
- """
- Add Chinese annotations to an image.
-
- annotations: list of dicts with keys:
- - text: Chinese translation/annotation text
- - position: 'top' or 'bottom' (default: 'top')
- - bg_color: background color (default: semi-transparent red)
- - text_color: text color (default: white)
-
- banner_text: optional text to add as a banner at the top
- """
- img = Image.open(input_path).convert("RGBA")
- width, height = img.size
-
- font_path = find_cjk_font()
- if not font_path:
- print("WARNING: No CJK font found, annotations may not render Chinese characters")
- font_path = None
-
- # Create overlay for annotations
- overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
- draw = ImageDraw.Draw(overlay)
-
- # Calculate font sizes based on image width
- if width >= 1200:
- banner_font_size = 28
- annotation_font_size = 22
- elif width >= 800:
- banner_font_size = 22
- annotation_font_size = 18
- else:
- banner_font_size = 16
- annotation_font_size = 14
-
- try:
- if font_path:
- banner_font = ImageFont.truetype(font_path, banner_font_size)
- annotation_font = ImageFont.truetype(font_path, annotation_font_size)
- else:
- banner_font = ImageFont.load_default()
- annotation_font = ImageFont.load_default()
- except Exception:
- banner_font = ImageFont.load_default()
- annotation_font = ImageFont.load_default()
-
- y_offset = 10
-
- # Add banner text at top
- if banner_text:
- bbox = draw.textbbox((0, 0), banner_text, font=banner_font)
- text_height = bbox[3] - bbox[1]
- padding = 12
-
- # Semi-transparent dark background
- draw.rectangle(
- [0, 0, width, text_height + padding * 2],
- fill=(0, 0, 0, 180)
- )
- # Draw text centered
- text_width = bbox[2] - bbox[0]
- x = (width - text_width) // 2
- draw.text((x, padding), banner_text, fill=(255, 255, 255, 255), font=banner_font)
- y_offset = text_height + padding * 2 + 10
-
- # Add annotation labels
- for i, ann in enumerate(annotations):
- text = ann.get("text", "")
- pos = ann.get("position", "top")
- bg_color = ann.get("bg_color", (220, 50, 50, 200))
- text_color = ann.get("text_color", (255, 255, 255, 255))
-
- if not text:
- continue
-
- # Word wrap for long text
- lines = []
- max_chars_per_line = max(20, width // (annotation_font_size + 2))
- for line_text in text.split("\n"):
- while len(line_text) > max_chars_per_line:
- lines.append(line_text[:max_chars_per_line])
- line_text = line_text[max_chars_per_line:]
- lines.append(line_text)
-
- # Calculate total height
- line_height = annotation_font_size + 6
- total_height = len(lines) * line_height + 16
-
- if pos == "bottom":
- box_y = height - total_height - 10 - i * (total_height + 5)
- else:
- box_y = y_offset + i * (total_height + 5)
-
- # Draw background
- draw.rectangle(
- [10, box_y, width - 10, box_y + total_height],
- fill=bg_color
- )
-
- # Draw text
- for j, line in enumerate(lines):
- draw.text(
- (20, box_y + 8 + j * line_height),
- line,
- fill=text_color,
- font=annotation_font
- )
-
- # Composite
- result = Image.alpha_composite(img, overlay)
- result = result.convert("RGB")
- result.save(output_path, "PNG", quality=95)
- print(f"Annotated image saved to: {output_path}")
- def add_arrow_annotation(input_path, output_path, arrow_x, arrow_y, label_text, label_pos="right"):
- """Add an arrow + label annotation to a specific point on the image."""
- img = Image.open(input_path).convert("RGBA")
- overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
- draw = ImageDraw.Draw(overlay)
-
- font_path = find_cjk_font()
- font_size = 20 if img.size[0] >= 800 else 14
- try:
- font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
- except:
- font = ImageFont.load_default()
-
- # Draw arrow (simple line + triangle)
- arrow_len = 30
- if label_pos == "right":
- end_x = arrow_x + arrow_len
- end_y = arrow_y
- elif label_pos == "left":
- end_x = arrow_x - arrow_len
- end_y = arrow_y
- elif label_pos == "down":
- end_x = arrow_x
- end_y = arrow_y + arrow_len
- else:
- end_x = arrow_x + arrow_len
- end_y = arrow_y
-
- # Draw circle at start point
- r = 6
- draw.ellipse([arrow_x - r, arrow_y - r, arrow_x + r, arrow_y + r], fill=(255, 50, 50, 230))
-
- # Draw line
- draw.line([(arrow_x, arrow_y), (end_x, end_y)], fill=(255, 50, 50, 230), width=3)
-
- # Draw label
- bbox = draw.textbbox((0, 0), label_text, font=font)
- tw = bbox[2] - bbox[0]
- th = bbox[3] - bbox[1]
- padding = 6
-
- if label_pos == "right":
- lx = end_x + 8
- ly = end_y - th // 2
- elif label_pos == "left":
- lx = end_x - tw - 8
- ly = end_y - th // 2
- else:
- lx = end_x - tw // 2
- ly = end_y + 8
-
- draw.rectangle([lx - padding, ly - padding, lx + tw + padding, ly + th + padding], fill=(220, 50, 50, 210))
- draw.text((lx, ly), label_text, fill=(255, 255, 255, 255), font=font)
-
- result = Image.alpha_composite(img, overlay)
- result.convert("RGB").save(output_path, "PNG", quality=95)
- print(f"Arrow annotation saved to: {output_path}")
- if __name__ == "__main__":
- if len(sys.argv) < 4:
- print("Usage: python annotate.py input.png output.png 'annotation text' [--banner 'banner text']")
- sys.exit(1)
-
- input_path = sys.argv[1]
- output_path = sys.argv[2]
- annotation_text = sys.argv[3]
-
- banner = None
- if "--banner" in sys.argv:
- idx = sys.argv.index("--banner")
- if idx + 1 < len(sys.argv):
- banner = sys.argv[idx + 1]
-
- annotate_image(input_path, output_path, [{"text": annotation_text}], banner_text=banner)
|