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 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 21:20:22 -06:00
commit 5a45320057
17 changed files with 788 additions and 0 deletions
@@ -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()
@@ -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()
@@ -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.",
"Youre 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 Mothers Day")
c.save()
print(f"Saved: {OUT_PATH}")
make_pdf()
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
Binary file not shown.
Binary file not shown.
+86
View File
@@ -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()
Binary file not shown.
@@ -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()