commit 5a45320057f8bb244c0c1293e44686a779f1a1f9 Author: jhodgkin Date: Sun Jun 28 21:20:22 2026 -0600 Initial commit: organize Primary song slides repo One folder per song under songs/. Tracks PPTX slide files and Python generation scripts. PDFs and videos excluded (too large for git; originals are in Google Drive). Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d80d478 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Large binary files — store in Google Drive, not git +*.mp4 +*.mov +*.avi +*.mkv +*.pdf diff --git a/README.md b/README.md new file mode 100644 index 0000000..3450754 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Primary Song Slides + +Slideshows and source materials for LDS Primary singing time. + +## Structure + +Each song gets its own folder under `songs/`. Files include PowerPoint presentations (`.pptx`), PDFs (flip charts, lyric sheets), and any other source materials. + +``` +songs/ + a-childs-prayer/ + as-i-search-the-holy-scriptures/ + because-singing-time/ + choose-to-serve-the-lord/ + dearest-mother-i-love-you/ + holy-places/ + i-will-be-valiant/ + i-will-follow-gods-plan/ + keep-the-commandments/ + my-dad/ + primary-singing-time/ +``` + +## Source Files + +All slides were originally created as Google Slides presentations and exported to `.pptx` format for archival. Source presentations remain in Google Drive. + +| Song | Files | +|------|-------| +| As I Search the Holy Scriptures | AsISearchTheHolyScriptures.pptx | +| I Will Be Valiant | IWillBeValiant.pptx | +| My Dad | my-dad.pptx | +| A Child's Prayer | *(pending)* | +| Choose to Serve the Lord | *(pending — file >10MB)* | +| Holy Places | *(pending)* | +| Keep the Commandments | *(pending)* | +| I Will Follow God's Plan | *(pending)* | +| Because Singing Time | *(pending)* | +| Dearest Mother I Love You | *(pending)* | +| Primary Singing Time | *(pending)* | diff --git a/scripts/pdf_to_pptx.py b/scripts/pdf_to_pptx.py new file mode 100644 index 0000000..2bc3ff3 --- /dev/null +++ b/scripts/pdf_to_pptx.py @@ -0,0 +1,83 @@ +import fitz +from pptx import Presentation +from pptx.util import Emu, Inches +import io + +PDF_PATH = r"C:\Users\jhodgkin\Pictures\Holy Places Flip Chart.pdf" +OUT_PATH = r"C:\Users\jhodgkin\Pictures\Holy Places Flip Chart.pptx" +DPI = 150 + +SLIDE_W = Inches(13.333) # widescreen landscape +SLIDE_H = Inches(7.5) +GAP = Inches(0.1) + +doc = fitz.open(PDF_PATH) +prs = Presentation() +prs.slide_width = SLIDE_W +prs.slide_height = SLIDE_H +blank_layout = prs.slide_layouts[6] + +def render_page(page): + mat = fitz.Matrix(DPI / 72, DPI / 72) + pix = page.get_pixmap(matrix=mat, alpha=False) + return io.BytesIO(pix.tobytes("png")), pix.width, pix.height + +def add_single_slide(page, label): + img, pw, ph = render_page(page) + slide = prs.slides.add_slide(blank_layout) + aspect = pw / ph + h = SLIDE_H + w = int(h * aspect) + if w > SLIDE_W: + w = SLIDE_W + h = int(w / aspect) + left = (SLIDE_W - w) // 2 + top = (SLIDE_H - h) // 2 + slide.shapes.add_picture(img, left, top, width=w, height=h) + print(f" Slide: {label}") + +def add_pair_slide(page_a, page_b, label): + img_a, pw_a, ph_a = render_page(page_a) + img_b, pw_b, ph_b = render_page(page_b) + slide = prs.slides.add_slide(blank_layout) + + usable_w = (SLIDE_W - GAP) // 2 + + def fit(pw, ph): + aspect = pw / ph + h = SLIDE_H + w = int(h * aspect) + if w > usable_w: + w = usable_w + h = int(w / aspect) + return w, h + + wa, ha = fit(pw_a, ph_a) + wb, hb = fit(pw_b, ph_b) + + top_a = (SLIDE_H - ha) // 2 + top_b = (SLIDE_H - hb) // 2 + + # Center the pair horizontally + total_w = wa + GAP + wb + left_a = (SLIDE_W - total_w) // 2 + left_b = left_a + wa + GAP + + slide.shapes.add_picture(img_a, left_a, top_a, width=wa, height=ha) + slide.shapes.add_picture(img_b, left_b, top_b, width=wb, height=hb) + print(f" Slide: {label}") + +pages = list(doc) + +# Page 1 alone +add_single_slide(pages[0], "page 1") + +# Pages 2+ in pairs +for i in range(1, len(pages), 2): + if i + 1 < len(pages): + add_pair_slide(pages[i], pages[i + 1], f"pages {i+1}+{i+2}") + else: + add_single_slide(pages[i], f"page {i+1}") + +prs.save(OUT_PATH) +print(f"\nSaved: {OUT_PATH}") diff --git a/songs/as-i-search-the-holy-scriptures/AsISearchTheHolyScriptures.pptx b/songs/as-i-search-the-holy-scriptures/AsISearchTheHolyScriptures.pptx new file mode 100644 index 0000000..70d36fd Binary files /dev/null and b/songs/as-i-search-the-holy-scriptures/AsISearchTheHolyScriptures.pptx differ diff --git a/songs/book-of-mormon-stories/BookOfMormonStories.pptx b/songs/book-of-mormon-stories/BookOfMormonStories.pptx new file mode 100644 index 0000000..d41aa88 Binary files /dev/null and b/songs/book-of-mormon-stories/BookOfMormonStories.pptx differ diff --git a/songs/book-of-mormon-stories/book_of_mormon_stories.py b/songs/book-of-mormon-stories/book_of_mormon_stories.py new file mode 100644 index 0000000..07566be --- /dev/null +++ b/songs/book-of-mormon-stories/book_of_mormon_stories.py @@ -0,0 +1,162 @@ +import fitz +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN +import io + +PDF_PATH = r"C:\Users\jhodgkin\Documents\dev\primary-song-slides\BookOfMormonStories.pdf" +OUT_PATH = r"C:\Users\jhodgkin\Documents\dev\primary-song-slides\BookOfMormonStories.pptx" +DPI = 96 + +SLIDE_W = Inches(13.333) +SLIDE_H = Inches(7.5) +TEXT_BOX_H = Inches(2.1) +FONT_SIZE = Pt(44) +TEXT_COLOR = RGBColor(0x4A, 0x27, 0x11) # deep warm brown + +# PDF pages 0-31 = illustration cards; page 32 = title page (skipped) +# Verse 1 lyrics are from CS 118. Fill in the remaining verses with correct lyrics. +# +# Image guide (0-based page → subject): +# 0 = teacher reading to children (intro) +# 1 = Lehi's family on ancient scroll +# 2 = three men sailing +# 3 = righteous family with halos in jungle +# 4 = large group gathering in jungle +# 5 = lush jungle / hut +# 6 = children springing from open Book of Mormon +# 7 = family with halos (closer) +# 8 = young man in red sash (Nephi? Alma?) +# 9 = angel with wings +# 10 = three men around a fallen figure (Alma struck down) +# 11 = righteous youth with halo in jungle +# 12 = man in chains +# 13 = pile of chains +# 14 = wicked king +# 15 = gravestone with flowers +# 16 = shepherd with sheep and staff +# 17 = flock of many sheep +# 18 = slingshot and sword +# 19 = righteous shepherd with halo +# 20 = four young warriors (shields and spears, no halos) +# 21 = Jesus Christ in red robe, arms open +# 22 = four warriors smiling +# 23 = four warriors with halos in jungle +# 24 = man standing triumphantly on stone wall +# 25 = three arrows flying +# 26 = young man preaching, arms wide +# 27 = young man preaching with halo +# 28 = crucifixion (three crosses) +# 29 = Jesus hugging many children +# 30 = portrait of Jesus Christ +# 31 = five children with halos in jungle +SLIDES = [ + # Verse 1 — Lehi's family / ancient Lamanites + (0, "Book of Mormon stories that my teacher tells to me,"), + (1, "Are about the Lamanites in ancient history."), + (2, "Long ago their fathers came from far across the sea,"), + (3, "Giv'n the land if they lived righteously."), + + # Verse 2 — Lamanites and others in the promised land + (4, "Lamanites met others who were seeking liberty,"), + (5, "And the land soon welcomed all who wanted to be free."), + (6, "Book of Mormon stories say that we must brothers be,"), + (7, "Giv'n the land if we live righteously."), + + # Verse 3 — Alma the Younger + (8, "Alma was rebellious, and he fought against the right."), + (9, "Then one day an angel came to turn him to the light."), + (10, "Struck before his brethren, Alma learned humility."), + (11, "Then he taught in the land righteously."), + + # Verse 4 — Abinadi before King Noah + (12, "Don't forget Abinadi, who stood before the king."), + (13, "All chained up from head to toe, the gospel he did bring."), + (14, "If he would deny it, then the king would set him free."), + (15, "He was true, and he died righteously."), + + # Verse 5 — Ammon the missionary + (16, "Ammon was a missionary serving Lamanites,"), + (17, "Tending King Lamoni's sheep for several days and nights."), + (18, "Robbers came; he saved the sheep by fighting fearlessly."), + (19, "He had learned he could live righteously."), + + # Verse 6 — 2000 Stripling Warriors + # Reordered: warriors (20) -> smiling march (22) -> Christ (21) -> halos (23) + (20, "Once two thousand sons of God were called to fight the foe."), + (22, "Marching as an army into battle they did go."), + (21, "They believed that Christ the Lord their guardian would be."), + (23, "They had learned they should live righteously."), + + # Verse 7 — Samuel the Lamanite + # Reordered: wall (24) -> preaching (26) -> arrows (25) -> righteous (27) + (24, "Samuel the Lamanite, high on the city wall,"), + (26, "Came to warn the people, and repentance was his call."), + (25, "Arrows could not hit him, for a man of God was he,"), + (27, "And he taught in the land righteously."), + + # Verse 8 — Christ's atonement and visit to the Americas + # Reordered: crucifixion (28) -> portrait (30) -> children (29) -> people (31) + (28, "After Christ was crucified and died for you and me,"), + (30, "He came forth to teach the truth to all who would be free."), + (29, "Hands were laid upon each child. He blessed them tenderly,"), + (31, "And they lived in the land righteously."), +] + + +def render_page(page): + mat = fitz.Matrix(DPI / 72, DPI / 72) + pix = page.get_pixmap(matrix=mat, alpha=False) + return io.BytesIO(pix.tobytes("jpeg", jpg_quality=85)), pix.width, pix.height + + +def add_slide(doc, prs, blank_layout, page_idx, lyric): + page = doc[page_idx] + img_bytes, pw, ph = render_page(page) + slide = prs.slides.add_slide(blank_layout) + + aspect = pw / ph + h = SLIDE_H + w = int(h * aspect) + if w > SLIDE_W: + w = SLIDE_W + h = int(w / aspect) + left = (SLIDE_W - w) // 2 + top = (SLIDE_H - h) // 2 + slide.shapes.add_picture(img_bytes, left, top, width=w, height=h) + + tb_top = SLIDE_H - TEXT_BOX_H + txBox = slide.shapes.add_textbox(Inches(0.25), tb_top, SLIDE_W - Inches(0.5), TEXT_BOX_H) + tf = txBox.text_frame + tf.word_wrap = True + + lines = lyric.split("\n") + for i, line in enumerate(lines): + p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() + p.alignment = PP_ALIGN.CENTER + run = p.add_run() + run.text = line + run.font.bold = True + run.font.size = FONT_SIZE + run.font.color.rgb = TEXT_COLOR + + print(f" Slide {len(prs.slides):2d} (pg {page_idx:2d}): {lyric[:60]}") + + +def main(): + doc = fitz.open(PDF_PATH) + prs = Presentation() + prs.slide_width = SLIDE_W + prs.slide_height = SLIDE_H + blank_layout = prs.slide_layouts[6] + + for page_idx, lyric in SLIDES: + add_slide(doc, prs, blank_layout, page_idx, lyric) + + prs.save(OUT_PATH) + print(f"\nSaved: {OUT_PATH} ({len(SLIDES)} slides)") + + +if __name__ == "__main__": + main() diff --git a/songs/dearest-mother-i-love-you/mothers_day_initials.py b/songs/dearest-mother-i-love-you/mothers_day_initials.py new file mode 100644 index 0000000..aea77f8 --- /dev/null +++ b/songs/dearest-mother-i-love-you/mothers_day_initials.py @@ -0,0 +1,55 @@ +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import letter +from reportlab.lib.units import inch +from reportlab.pdfbase.pdfmetrics import stringWidth + +OUT_PATH = r"C:\Users\jhodgkin\Pictures\Dearest Mother I Love You - Initials.pdf" + +LINES = [ + "Gentle words I hear you say.", + "Your kind hands help me each day.", + "You're my mother kind and true;", + "Dearest mother, I love you.", +] + +W, H = letter +MARGIN = 0.5 * inch +FONT = "Helvetica-Bold" + +def first_letters(line): + return " ".join(word[0].upper() for word in line.split()) + +def fit_font_size(c, text, max_width, start=200): + size = start + while size > 1: + if stringWidth(text, FONT, size) <= max_width: + return size + size -= 1 + return size + +def make_pdf(): + c = canvas.Canvas(OUT_PATH, pagesize=letter) + + initials = [first_letters(line) for line in LINES] + max_w = W - 2 * MARGIN + + # Find a single font size that fits all lines + size = min(fit_font_size(c, line, max_w) for line in initials) + + # Distribute lines evenly across the page height + usable_h = H - 2 * MARGIN + slot = usable_h / len(initials) + + for i, text in enumerate(initials): + # Center vertically within each slot + y = H - MARGIN - (i + 0.65) * slot + c.setFont(FONT, size) + c.drawCentredString(W / 2, y, text) + + c.save() + print(f"Saved: {OUT_PATH}") + print(f"Font size: {size}pt") + for line, init in zip(LINES, initials): + print(f" {line!r} -> {init!r}") + +make_pdf() diff --git a/songs/dearest-mother-i-love-you/mothers_day_song.py b/songs/dearest-mother-i-love-you/mothers_day_song.py new file mode 100644 index 0000000..cab097c --- /dev/null +++ b/songs/dearest-mother-i-love-you/mothers_day_song.py @@ -0,0 +1,82 @@ +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import letter +from reportlab.lib.colors import HexColor +from reportlab.lib.units import inch + +OUT_PATH = r"C:\Users\jhodgkin\Pictures\Dearest Mother I Love You.pdf" + +LINES = [ + "Gentle words I hear you say.", + "Your kind hands help me each day.", + "You’re my mother kind and true;", + "Dearest mother, I love you.", +] + +TITLE = "Dearest Mother, I Love You" + +W, H = letter # 8.5 x 11 inches + +PINK = HexColor("#F48FB1") +SOFT_PINK = HexColor("#FCE4EC") +DEEP_ROSE = HexColor("#C2185B") +GOLD = HexColor("#F9A825") + +def draw_flower(c, cx, cy, r=18, petals=6, petal_color=None, center_color=None): + import math + petal_color = petal_color or PINK + center_color = center_color or GOLD + for i in range(petals): + angle = math.radians(i * 360 / petals) + px = cx + math.cos(angle) * r * 1.1 + py = cy + math.sin(angle) * r * 1.1 + c.setFillColor(petal_color) + c.circle(px, py, r * 0.65, fill=1, stroke=0) + c.setFillColor(center_color) + c.circle(cx, cy, r * 0.45, fill=1, stroke=0) + +def make_pdf(): + c = canvas.Canvas(OUT_PATH, pagesize=letter) + + # Corner flowers + corners = [ + (0.65*inch, 0.65*inch), + (W - 0.65*inch, 0.65*inch), + (0.65*inch, H - 0.65*inch), + (W - 0.65*inch, H - 0.65*inch), + ] + for cx, cy in corners: + draw_flower(c, cx, cy) + + # Title + c.setFillColor(DEEP_ROSE) + c.setFont("Helvetica-BoldOblique", 28) + c.drawCentredString(W / 2, H - 1.5*inch, TITLE) + + # Divider line under title + c.setStrokeColor(PINK) + c.setLineWidth(2) + c.line(1.2*inch, H - 1.75*inch, W - 1.2*inch, H - 1.75*inch) + + # Lyrics — evenly spaced + c.setFillColor(HexColor("#4A148C")) + c.setFont("Helvetica-Bold", 26) + top = H - 2.4*inch + spacing = 1.0*inch + for i, line in enumerate(LINES): + y = top - i * spacing + c.drawCentredString(W / 2, y, line) + + # Small decorative flowers between lines + for i in range(len(LINES) - 1): + y = top - i * spacing - spacing / 2 + draw_flower(c, W / 2, y, r=8) + + # Footer + c.setFillColor(DEEP_ROSE) + c.setFont("Helvetica-Oblique", 14) + c.drawCentredString(W / 2, 0.75*inch, "Happy Mother’s Day") + + c.save() + print(f"Saved: {OUT_PATH}") + +make_pdf() diff --git a/songs/holy-places/Holy Places Flip Chart.pptx b/songs/holy-places/Holy Places Flip Chart.pptx new file mode 100644 index 0000000..f029e29 Binary files /dev/null and b/songs/holy-places/Holy Places Flip Chart.pptx differ diff --git a/songs/i-will-be-valiant/IWillBeValiant.pptx b/songs/i-will-be-valiant/IWillBeValiant.pptx new file mode 100644 index 0000000..5f4539c Binary files /dev/null and b/songs/i-will-be-valiant/IWillBeValiant.pptx differ diff --git a/songs/my-country-tis-of-thee/MyCountryTisOfThee.pptx b/songs/my-country-tis-of-thee/MyCountryTisOfThee.pptx new file mode 100644 index 0000000..a8d2511 Binary files /dev/null and b/songs/my-country-tis-of-thee/MyCountryTisOfThee.pptx differ diff --git a/songs/my-country-tis-of-thee/my_country_tis_of_thee.py b/songs/my-country-tis-of-thee/my_country_tis_of_thee.py new file mode 100644 index 0000000..cc997b3 --- /dev/null +++ b/songs/my-country-tis-of-thee/my_country_tis_of_thee.py @@ -0,0 +1,150 @@ +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN +from pptx.oxml.ns import qn +from lxml import etree + +OUT_PATH = r"C:\Users\jhodgkin\Documents\dev\primary-song-slides\MyCountryTisOfThee.pptx" + +SLIDE_W = Inches(13.333) +SLIDE_H = Inches(7.5) + +NAVY = RGBColor(0x0D, 0x27, 0x6D) +GOLD = RGBColor(0xF5, 0xC5, 0x18) +WHITE = RGBColor(0xFF, 0xFF, 0xFF) + +VERSES = [ + ( + "Verse 1", + "My country, 'tis of thee,\n" + "Sweet land of liberty,\n" + "Of thee I sing;\n" + "Land where my fathers died,\n" + "Land of the pilgrims' pride,\n" + "From ev'ry mountainside\n" + "Let freedom ring!" + ), + ( + "Verse 4", + "Our fathers' God, to thee,\n" + "Author of liberty,\n" + "To thee we sing;\n" + "Long may our land be bright\n" + "With freedom's holy light.\n" + "Protect us by thy might,\n" + "Great God, our King!" + ), +] + + +def set_background_color(slide, color): + """Fill slide background with a solid color.""" + bg = slide.background + fill = bg.fill + fill.solid() + fill.fore_color.rgb = color + + +def add_text_paragraph(tf, text, font_size, color, bold=False, space_before=0): + p = tf.add_paragraph() + p.alignment = PP_ALIGN.CENTER + if space_before: + pPr = p._pPr + if pPr is None: + pPr = p._p.get_or_add_pPr() + pPr.set(qn("a:spcBef"), None) + spcBef = etree.SubElement(pPr, qn("a:spcBef")) + spcPts = etree.SubElement(spcBef, qn("a:spcPts")) + spcPts.set("val", str(space_before * 100)) + run = p.add_run() + run.text = text + run.font.size = font_size + run.font.color.rgb = color + run.font.bold = bold + return p + + +def add_slide(prs, blank_layout, verse_label, verse_text): + slide = prs.slides.add_slide(blank_layout) + set_background_color(slide, NAVY) + + # Gold decorative rule above title + rule_top = Inches(0.55) + rule_h = Inches(0.06) + rule = slide.shapes.add_shape( + 1, # MSO_SHAPE_TYPE.RECTANGLE + Inches(1.5), rule_top, SLIDE_W - Inches(3.0), rule_h + ) + rule.fill.solid() + rule.fill.fore_color.rgb = GOLD + rule.line.fill.background() + + # Song title + title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.65), SLIDE_W - Inches(1.0), Inches(0.85)) + tf = title_box.text_frame + tf.word_wrap = False + p = tf.paragraphs[0] + p.alignment = PP_ALIGN.CENTER + run = p.add_run() + run.text = "My Country, 'Tis of Thee" + run.font.size = Pt(36) + run.font.color.rgb = GOLD + run.font.bold = True + + # Verse label + label_box = slide.shapes.add_textbox(Inches(0.5), Inches(1.45), SLIDE_W - Inches(1.0), Inches(0.5)) + tf2 = label_box.text_frame + p2 = tf2.paragraphs[0] + p2.alignment = PP_ALIGN.CENTER + run2 = p2.add_run() + run2.text = verse_label + run2.font.size = Pt(22) + run2.font.color.rgb = WHITE + run2.font.bold = False + run2.font.italic = True + + # Gold rule below label + rule2 = slide.shapes.add_shape( + 1, + Inches(1.5), Inches(1.95), SLIDE_W - Inches(3.0), rule_h + ) + rule2.fill.solid() + rule2.fill.fore_color.rgb = GOLD + rule2.line.fill.background() + + # Verse lyrics + lyric_box = slide.shapes.add_textbox(Inches(0.5), Inches(2.1), SLIDE_W - Inches(1.0), Inches(5.0)) + tf3 = lyric_box.text_frame + tf3.word_wrap = True + lines = verse_text.split("\n") + for i, line in enumerate(lines): + if i == 0: + p = tf3.paragraphs[0] + else: + p = tf3.add_paragraph() + p.alignment = PP_ALIGN.CENTER + run = p.add_run() + run.text = line + run.font.size = Pt(40) + run.font.color.rgb = WHITE + run.font.bold = False + + print(f" Added: {verse_label}") + + +def main(): + prs = Presentation() + prs.slide_width = SLIDE_W + prs.slide_height = SLIDE_H + blank_layout = prs.slide_layouts[6] + + for label, text in VERSES: + add_slide(prs, blank_layout, label, text) + + prs.save(OUT_PATH) + print(f"\nSaved: {OUT_PATH} ({len(VERSES)} slides)") + + +if __name__ == "__main__": + main() diff --git a/songs/my-dad/my-dad.pptx b/songs/my-dad/my-dad.pptx new file mode 100644 index 0000000..b3c4650 Binary files /dev/null and b/songs/my-dad/my-dad.pptx differ diff --git a/songs/my-flag-my-flag/MyFlagMyFlag.pptx b/songs/my-flag-my-flag/MyFlagMyFlag.pptx new file mode 100644 index 0000000..ea21ab7 Binary files /dev/null and b/songs/my-flag-my-flag/MyFlagMyFlag.pptx differ diff --git a/songs/my-flag-my-flag/my_flag_my_flag.py b/songs/my-flag-my-flag/my_flag_my_flag.py new file mode 100644 index 0000000..52bc479 --- /dev/null +++ b/songs/my-flag-my-flag/my_flag_my_flag.py @@ -0,0 +1,86 @@ +import fitz +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN +import io + +PDF_PATH = r"C:\Users\jhodgkin\Documents\dev\primary-song-slides\MyFlagMyFlag-Color.pdf" +OUT_PATH = r"C:\Users\jhodgkin\Documents\dev\primary-song-slides\MyFlagMyFlag.pptx" +DPI = 96 + +SLIDE_W = Inches(13.333) +SLIDE_H = Inches(7.5) +TEXT_BOX_H = Inches(2.1) +FONT_SIZE = Pt(44) +TEXT_COLOR = RGBColor(0x0D, 0x27, 0x6D) # deep navy blue + +# PDF pages 1-8 (0-based) map to the 8 lyric lines +# Page 0 is the overview thumbnail sheet — skip it +SLIDES = [ + (1, "My flag, my flag, my country's flag,"), + (2, "I love to see you wave;"), + (3, "My flag, my flag, my country's flag,"), + (4, "The banner of the brave."), + (5, "Wave on, wave on forever,"), + (6, "The banner of the free;"), + (7, "Wave on, wave on forever,"), + (8, "The flag of liberty."), +] + + +def render_page(page): + mat = fitz.Matrix(DPI / 72, DPI / 72) + pix = page.get_pixmap(matrix=mat, alpha=False) + return io.BytesIO(pix.tobytes("jpeg", jpg_quality=85)), pix.width, pix.height + + +def add_slide(doc, prs, blank_layout, page_idx, lyric): + page = doc[page_idx] + img_bytes, pw, ph = render_page(page) + slide = prs.slides.add_slide(blank_layout) + + aspect = pw / ph + h = SLIDE_H + w = int(h * aspect) + if w > SLIDE_W: + w = SLIDE_W + h = int(w / aspect) + left = (SLIDE_W - w) // 2 + top = (SLIDE_H - h) // 2 + slide.shapes.add_picture(img_bytes, left, top, width=w, height=h) + + tb_top = SLIDE_H - TEXT_BOX_H + txBox = slide.shapes.add_textbox(Inches(0.25), tb_top, SLIDE_W - Inches(0.5), TEXT_BOX_H) + tf = txBox.text_frame + tf.word_wrap = True + + lines = lyric.split("\n") + for i, line in enumerate(lines): + p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() + p.alignment = PP_ALIGN.CENTER + run = p.add_run() + run.text = line + run.font.bold = True + run.font.size = FONT_SIZE + run.font.color.rgb = TEXT_COLOR + + print(f" Slide {len(prs.slides):2d} (pg {page_idx+1:2d}): {lyric[:60]}") + + +def main(): + doc = fitz.open(PDF_PATH) + prs = Presentation() + prs.slide_width = SLIDE_W + prs.slide_height = SLIDE_H + blank_layout = prs.slide_layouts[6] + + for page_idx, lyric in SLIDES: + add_slide(doc, prs, blank_layout, page_idx, lyric) + + prs.save(OUT_PATH) + print(f"\nSaved: {OUT_PATH} ({len(SLIDES)} slides)") + + +if __name__ == "__main__": + main() diff --git a/songs/my-own-sacred-grove/MyOwnSacredGrove.pptx b/songs/my-own-sacred-grove/MyOwnSacredGrove.pptx new file mode 100644 index 0000000..8ba681d Binary files /dev/null and b/songs/my-own-sacred-grove/MyOwnSacredGrove.pptx differ diff --git a/songs/my-own-sacred-grove/my_own_sacred_grove.py b/songs/my-own-sacred-grove/my_own_sacred_grove.py new file mode 100644 index 0000000..94257b4 --- /dev/null +++ b/songs/my-own-sacred-grove/my_own_sacred_grove.py @@ -0,0 +1,124 @@ +import fitz +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN +import io + +PDF_PATH = r"C:\Users\jhodgkin\Documents\dev\primary-song-slides\EDITABLEMyOwnSacredGrove.pdf" +OUT_PATH = r"C:\Users\jhodgkin\Documents\dev\primary-song-slides\MyOwnSacredGrove.pptx" +DPI = 96 + +SLIDE_W = Inches(13.333) # 16:9 widescreen +SLIDE_H = Inches(7.5) +TEXT_BOX_H = Inches(2.1) +FONT_SIZE = Pt(44) +TEXT_COLOR = RGBColor(0x1B, 0x43, 0x32) # deep forest green + +# (0-based PDF page index, lyric line) +# Pages: 0=boy→grove, 1=boy reading+praying, 2=boy praying+pillar of light, +# 3=HF+Jesus plain, 4=HF+Jesus in clouds, 5=two kids praying by bed (warm), +# 6=child with headphones/globe, 7=two kids praying+HF in clouds, +# 8=two kids praying by bed (same scene), 9=confused boy+arrows, +# 10=boy at fork in road, 11=wheelchair child thinking of Jesus, +# 12=Black child praying+golden light, 13=girl reading+boy pondering+stars, +# 14=girl praying+HF in clouds, 15=boy with clock, 16=three children, +# 17=girl holding light+stars, 18=girl by window, 19=two kids praying in grove +SLIDES = [ + # Verse 1 + (0, "Joseph Smith went to a grove full of trees"), + (1, "Seeking God's wisdom, he fell to his knees"), + (2, "As he pled with the heavens, the sky filled with light"), + (3, "And the Father appeared with His Son Jesus Christ"), + (4, "Standing above in the air\nComing to answer his prayer"), + # Chorus + (19, "I will find my own sacred grove"), + (6, "Away from all of the noise of the world"), + (8, "I will turn to prayer"), + (7, "For I know He's there"), + (19, "I will find my own sacred grove"), + # Verse 2 + (9, "So many choices with so much at stake"), + (10, "Life's full of pathways, but which should I take?"), + (12, "If I lift up in prayer in the name of the Son"), + (13, "Through the power of the Holy Ghost, answers will come"), + (14, "Heavenly Father is there\nReady to answer my prayer"), + # Chorus + (19, "I will find my own sacred grove"), + (6, "Away from all of the noise of the world"), + (5, "I will turn to prayer"), + (7, "For I know He's there"), + (19, "I will find my own sacred grove"), + # Bridge + (15, "Sometimes the answers take time"), + (16, "So I'll listen in heart and mind"), + (17, "Revelation will come my way"), + (18, "As I wait patiently in faith"), + # Chorus + (19, "I will find my own sacred grove"), + (6, "Away from all of the noise of the world"), + (8, "I will turn to prayer"), + (7, "For I know He's there"), + (19, "I will find my own sacred grove"), + # Ending + (11, "I will find my own sacred grove"), +] + + +def render_page(page): + mat = fitz.Matrix(DPI / 72, DPI / 72) + pix = page.get_pixmap(matrix=mat, alpha=False) + return io.BytesIO(pix.tobytes("jpeg", jpg_quality=85)), pix.width, pix.height + + +def add_slide(doc, prs, blank_layout, page_idx, lyric): + page = doc[page_idx] + img_bytes, pw, ph = render_page(page) + slide = prs.slides.add_slide(blank_layout) + + # Scale image to fit within slide, centered + aspect = pw / ph + h = SLIDE_H + w = int(h * aspect) + if w > SLIDE_W: + w = SLIDE_W + h = int(w / aspect) + left = (SLIDE_W - w) // 2 + top = (SLIDE_H - h) // 2 + slide.shapes.add_picture(img_bytes, left, top, width=w, height=h) + + # Lyric text box at the bottom of the slide + tb_top = SLIDE_H - TEXT_BOX_H + txBox = slide.shapes.add_textbox(Inches(0.25), tb_top, SLIDE_W - Inches(0.5), TEXT_BOX_H) + tf = txBox.text_frame + tf.word_wrap = True + + lines = lyric.split("\n") + for i, line in enumerate(lines): + p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() + p.alignment = PP_ALIGN.CENTER + run = p.add_run() + run.text = line + run.font.bold = True + run.font.size = FONT_SIZE + run.font.color.rgb = TEXT_COLOR + + print(f" Slide {len(prs.slides):2d} (pg {page_idx+1:2d}): {lyric[:55]}") + + +def main(): + doc = fitz.open(PDF_PATH) + prs = Presentation() + prs.slide_width = SLIDE_W + prs.slide_height = SLIDE_H + blank_layout = prs.slide_layouts[6] + + for page_idx, lyric in SLIDES: + add_slide(doc, prs, blank_layout, page_idx, lyric) + + prs.save(OUT_PATH) + print(f"\nSaved: {OUT_PATH} ({len(SLIDES)} slides)") + + +if __name__ == "__main__": + main()