diff --git a/resources/presentation/build_pr_tour.py b/resources/presentation/build_pr_tour.py new file mode 100644 index 0000000..8e89492 --- /dev/null +++ b/resources/presentation/build_pr_tour.py @@ -0,0 +1,229 @@ +"""Build a PR-tour slide deck matching the SecuriThings workshop deck style. + +Each slide is identical to the existing repo-tour slides (slides 5-22 in +GitHub-Workshop-SecuriThings.pptx): + - dark #0D1117 background + - full-bleed top image (PR page screenshot, 10" x 4.81") + - 👆 emoji (Segoe UI Emoji, 44pt, soft shadow) anchored under the target + - bottom caption strip: orange section name + white description + +Output: github-pr-tour.pptx (10 slides covering tabs + sidebar sections) +""" +from __future__ import annotations + +from pathlib import Path + +from lxml import etree +from pptx import Presentation +from pptx.dml.color import RGBColor +from pptx.oxml.ns import qn +from pptx.util import Emu, Pt + +HERE = Path(__file__).parent +IMG = HERE / "github-pr-ui.png" +OUT = HERE / "github-pr-tour.pptx" + +A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" +P_NS = "http://schemas.openxmlformats.org/presentationml/2006/main" + +# Slide dimensions (matches reference deck): 10" x 5.625" = 9144000 x 5143500 EMU +SLIDE_W = 9144000 +SLIDE_H = 5143500 + +# Image area: full width, 4.81" tall (same as reference) +IMG_W = 9144000 +IMG_H = 4398264 + +# Emoji: 0.6" x 0.6" (slightly smaller than the reference deck's 0.7" +# because the PR sidebar items are tightly packed; bigger emoji bodies +# would visually overlap the neighbouring section) +EMOJI_W = 548640 +EMOJI_H = 548640 +EMOJI_FONT_PT = 38 + +# Source image dimensions in pixels (the cropped PR screenshot) +SRC_PX_W = 1920 +SRC_PX_H = 920 + +# EMU per pixel in the placed image +PX2EMU_X = IMG_W / SRC_PX_W # 4762.5 +PX2EMU_Y = IMG_H / SRC_PX_H # 4780.7 + + +def emoji_pos_below(target_cx_px: int, target_bottom_y_px: int) -> tuple[int, int]: + """Position emoji centered horizontally under target — for top-of-page tabs.""" + x = int(target_cx_px * PX2EMU_X - EMOJI_W / 2) + y = int(target_bottom_y_px * PX2EMU_Y + 45720) + return x, y + + +def emoji_pos_left(target_left_px: int, target_center_y_px: int) -> tuple[int, int]: + """Position emoji to the LEFT of the target, vertically centred on it. + + Used for sidebar headings: a 👉 finger floats in the empty gutter between + the conversation column and the sidebar, pointing right at the heading. + """ + gap_emu = 45720 # ~0.05" between fingertip and target text + x = int(target_left_px * PX2EMU_X - EMOJI_W - gap_emu) + y = int(target_center_y_px * PX2EMU_Y - EMOJI_H / 2) + return x, y + + +# (name, description, emoji_char, anchor_kind, target coords) +# anchor_kind="below" → (target_center_x_px, target_bottom_y_px) +# anchor_kind="left" → (target_left_x_px, target_center_y_px) +TOUR = [ + ("Conversation", + "The unified PR thread — description, comments, reviews, and event log.", + "👆", "below", 420, 348), + ("Commits", + "Browse the individual commits in this PR. Useful for stacked-diff reviews.", + "👆", "below", 567, 348), + ("Checks", + "Status of every CI run for this PR: tests, linters, security scans, deploys.", + "👆", "below", 696, 348), + ("Files changed", + "The full diff. Leave inline comments, suggested changes, and approvals here.", + "👆", "below", 838, 348), + ("Reviewers", + "Request review from people or teams. CODEOWNERS auto-fills here.", + "👉", "left", 1247, 383), + ("Assignees", + "Who is responsible for landing this PR. Often the author, sometimes a shepherd.", + "👉", "left", 1247, 460), + ("Labels", + "Categorise the PR: bug, feature, area/api, needs-triage. Drives filters & automation.", + "👉", "left", 1247, 536), + ("Projects", + "Add to a GitHub Project board — Kanban, roadmap, or sprint tracking.", + "👉", "left", 1247, 613), + ("Milestone", + "Group PRs by release or iteration (e.g. v1.2.0, Sprint 14).", + "👉", "left", 1247, 689), + ("Development", + "Link issues. \"Closes #123\" in the description auto-closes them on merge.", + "👉", "left", 1247, 766), +] + + +def add_background(slide, hex_color: str) -> None: + """Set solid slide background color via cSld/bg.""" + cSld = slide._element.find(qn("p:cSld")) + bg_xml = ( + f'' + f' ' + f' ' + f' ' + f' ' + f'' + ) + bg = etree.fromstring(bg_xml) + cSld.insert(0, bg) + + +def add_emoji(slide, x: int, y: int, glyph: str = "👆") -> None: + """Add an emoji textbox with soft drop-shadow at the given EMU position.""" + tx = slide.shapes.add_textbox(Emu(x), Emu(y), Emu(EMOJI_W), Emu(EMOJI_H)) + tf = tx.text_frame + tf.margin_left = Emu(0) + tf.margin_right = Emu(0) + tf.margin_top = Emu(0) + tf.margin_bottom = Emu(0) + p = tf.paragraphs[0] + from pptx.enum.text import PP_ALIGN + p.alignment = PP_ALIGN.CENTER + run = p.add_run() + run.text = glyph + run.font.name = "Segoe UI Emoji" + run.font.size = Pt(EMOJI_FONT_PT) + run.font.color.rgb = RGBColor(0x00, 0x00, 0x00) + + bodyPr = tf._txBody.find(qn("a:bodyPr")) + bodyPr.set("anchor", "ctr") + + spPr = tx._element.find(qn("p:spPr")) + for el in spPr.findall(qn("a:effectLst")): + spPr.remove(el) + effect_xml = ( + f'' + f' ' + f' ' + f' ' + f'' + ) + spPr.append(etree.fromstring(effect_xml)) + + +def add_caption(slide, name: str, desc: str) -> None: + """Bottom caption strip: orange name + gray dash + white description.""" + # Position matches reference: x=365760, y=4443984, cx=8412480, cy=653796 + tx = slide.shapes.add_textbox( + Emu(365760), Emu(4443984), Emu(8412480), Emu(653796) + ) + tf = tx.text_frame + tf.word_wrap = True + tf.margin_left = Emu(0) + tf.margin_right = Emu(0) + tf.margin_top = Emu(0) + tf.margin_bottom = Emu(0) + bodyPr = tf._txBody.find(qn("a:bodyPr")) + bodyPr.set("anchor", "ctr") + + p = tf.paragraphs[0] + + r1 = p.add_run() + r1.text = name + r1.font.name = "Calibri" + r1.font.size = Pt(20) + r1.font.bold = True + r1.font.color.rgb = RGBColor(0xF0, 0x88, 0x3E) # GitHub orange + + r2 = p.add_run() + r2.text = " — " + r2.font.name = "Calibri" + r2.font.size = Pt(16) + r2.font.color.rgb = RGBColor(0xC9, 0xD1, 0xD9) + + r3 = p.add_run() + r3.text = desc + r3.font.name = "Calibri" + r3.font.size = Pt(14) + r3.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + + +def build_slide(prs, name: str, desc: str, glyph: str, kind: str, a: int, b: int): + blank = prs.slide_layouts[6] + slide = prs.slides.add_slide(blank) + + add_background(slide, "0D1117") + + slide.shapes.add_picture( + str(IMG), Emu(0), Emu(0), width=Emu(IMG_W), height=Emu(IMG_H) + ) + + if kind == "below": + ex, ey = emoji_pos_below(a, b) + elif kind == "left": + ex, ey = emoji_pos_left(a, b) + else: + raise ValueError(f"unknown anchor kind: {kind}") + add_emoji(slide, ex, ey, glyph) + + add_caption(slide, name, desc) + return slide + + +def main() -> None: + prs = Presentation() + prs.slide_width = Emu(SLIDE_W) + prs.slide_height = Emu(SLIDE_H) + + for name, desc, glyph, kind, a, b in TOUR: + build_slide(prs, name, desc, glyph, kind, a, b) + + prs.save(OUT) + print(f"Wrote {OUT} ({len(TOUR)} slides)") + + +if __name__ == "__main__": + main() diff --git a/resources/presentation/github-pr-tour.pptx b/resources/presentation/github-pr-tour.pptx new file mode 100644 index 0000000..f184530 Binary files /dev/null and b/resources/presentation/github-pr-tour.pptx differ diff --git a/resources/presentation/github-pr-ui.png b/resources/presentation/github-pr-ui.png new file mode 100644 index 0000000..9aac5fd Binary files /dev/null and b/resources/presentation/github-pr-ui.png differ