BMAD-METHOD/docs/diagrams/composite-quickflow.py

140 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""
Composite Quick Flow diagram onto main BMM workflow as a paper overlay.
SCALING PRINCIPLE: Both diagrams use the same font sizes in D2 (27px for workflow boxes).
To maintain visual consistency, the Quick Flow must be scaled by the same factor as the
main diagram. This is calculated from the SVG viewBox dimensions and target PNG width.
Formula:
scale_factor = main_png_width / main_svg_native_width
quick_flow_width = quick_flow_svg_native_width * scale_factor
"""
import re
from PIL import Image, ImageDraw, ImageFilter
# Configuration
MAIN_SVG = "bmm-workflow.svg"
MAIN_IMAGE = "bmm-workflow.png"
OVERLAY_SVG = "quick-flow.svg"
OVERLAY_IMAGE = "quick-flow.png"
OUTPUT_IMAGE = "bmm-workflow-with-quickflow.png"
# Overlay positioning
OVERLAY_X = 150 # Left margin
OVERLAY_Y_FROM_BOTTOM = 140 # Distance from bottom
# Paper effect settings
SHADOW_OFFSET = 0
SHADOW_BLUR = 0
SHADOW_COLOR = (0, 0, 0, 0)
BORDER_COLOR = (255, 255, 255) # No visible border
BORDER_WIDTH = 0
PAPER_PADDING = 0
ROTATION_ANGLE = -5 # 5° clockwise
def get_svg_native_width(svg_path):
"""Extract native width from SVG viewBox attribute."""
with open(svg_path, 'r') as f:
content = f.read()
# Match the first viewBox (the main SVG element)
match = re.search(r'viewBox="[0-9.-]+\s+[0-9.-]+\s+([0-9.]+)\s+[0-9.]+"', content)
if match:
return float(match.group(1))
raise ValueError(f"Could not find viewBox in {svg_path}")
def add_paper_effect(overlay):
"""Add paper-like styling: padding, border, shadow."""
# Add padding (white border around content)
padded_w = overlay.width + PAPER_PADDING * 2
padded_h = overlay.height + PAPER_PADDING * 2
paper = Image.new('RGBA', (padded_w, padded_h), (255, 255, 255, 255))
paper.paste(overlay, (PAPER_PADDING, PAPER_PADDING))
# Draw border
draw = ImageDraw.Draw(paper)
draw.rectangle(
[0, 0, padded_w - 1, padded_h - 1],
outline=BORDER_COLOR,
width=BORDER_WIDTH
)
# Slight rotation for natural "placed" look
paper = paper.rotate(ROTATION_ANGLE, expand=True, fillcolor=(255, 255, 255, 0), resample=Image.BICUBIC)
# Create shadow
shadow_size = (paper.width + SHADOW_OFFSET * 2 + SHADOW_BLUR * 2,
paper.height + SHADOW_OFFSET * 2 + SHADOW_BLUR * 2)
shadow = Image.new('RGBA', shadow_size, (0, 0, 0, 0))
# Draw shadow rectangle
shadow_draw = ImageDraw.Draw(shadow)
shadow_draw.rectangle(
[SHADOW_BLUR + SHADOW_OFFSET,
SHADOW_BLUR + SHADOW_OFFSET,
SHADOW_BLUR + SHADOW_OFFSET + paper.width,
SHADOW_BLUR + SHADOW_OFFSET + paper.height],
fill=SHADOW_COLOR
)
shadow = shadow.filter(ImageFilter.GaussianBlur(SHADOW_BLUR))
# Composite paper onto shadow
shadow.paste(paper, (SHADOW_BLUR, SHADOW_BLUR), paper)
return shadow
def main():
# Load images
main_img = Image.open(MAIN_IMAGE).convert('RGBA')
overlay_img = Image.open(OVERLAY_IMAGE).convert('RGBA')
print(f"Main image: {main_img.size}")
print(f"Overlay image: {overlay_img.size}")
# Calculate proportional scale factor from SVG dimensions
main_svg_width = get_svg_native_width(MAIN_SVG)
overlay_svg_width = get_svg_native_width(OVERLAY_SVG)
scale_factor = main_img.width / main_svg_width
target_overlay_width = int(overlay_svg_width * scale_factor)
print(f"Main SVG native width: {main_svg_width}px")
print(f"Overlay SVG native width: {overlay_svg_width}px")
print(f"Scale factor: {scale_factor:.4f}")
print(f"Target overlay width: {target_overlay_width}px")
# Resize overlay to maintain font scale parity
aspect = overlay_img.height / overlay_img.width
new_height = int(target_overlay_width * aspect)
overlay_img = overlay_img.resize((target_overlay_width, new_height), Image.LANCZOS)
print(f"Overlay resized to: {overlay_img.size}")
# Add paper effect
paper_overlay = add_paper_effect(overlay_img)
# Calculate position (bottom-left area)
pos_x = OVERLAY_X
pos_y = main_img.height - paper_overlay.height - OVERLAY_Y_FROM_BOTTOM
print(f"Placing overlay at: ({pos_x}, {pos_y})")
# Composite
main_img.paste(paper_overlay, (pos_x, pos_y), paper_overlay)
# Save (convert to RGB for PNG without alpha issues)
main_img = main_img.convert('RGB')
main_img.save(OUTPUT_IMAGE, 'PNG')
print(f"Saved: {OUTPUT_IMAGE}")
if __name__ == "__main__":
main()