kazakhstan/create_pptx.py

462 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Презентация о Казахстане с настоящими PowerPoint-анимациями.
Подход: создаём PPTX через python-pptx, затем распаковываем,
вставляем правильный <p:timing> XML и запаковываем обратно.
"""
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.enum.shapes import MSO_SHAPE
from pptx.oxml.ns import qn
import zipfile, shutil, os, copy, tempfile
from lxml import etree
# ── Colors ──
INK = RGBColor(0x0F, 0x12, 0x18)
CYAN = RGBColor(0x00, 0xE5, 0xFF)
GOLD = RGBColor(0xE8, 0xB7, 0x30)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
GRAY = RGBColor(0x5B, 0x65, 0x73)
BG = RGBColor(0x14, 0x1C, 0x28)
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
W = prs.slide_width
H = prs.slide_height
BL = prs.slide_layouts[6]
# ── helpers ──
def bg(s, c):
s.background.fill.solid()
s.background.fill.fore_color.rgb = c
def txt(s, t, l, tp, w, h, sz=24, b=False, c=INK, a=PP_ALIGN.LEFT, fn='Calibri'):
tb = s.shapes.add_textbox(l, tp, w, h)
tf = tb.text_frame; tf.word_wrap = True
p = tf.paragraphs[0]; p.text = t
p.font.size = Pt(sz); p.font.bold = b; p.font.color.rgb = c; p.font.name = fn
p.alignment = a
return tb
def box(s, l, tp, w, h, fc=BG, bc=None):
sh = s.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, l, tp, w, h)
sh.fill.solid(); sh.fill.fore_color.rgb = fc
if bc: sh.line.color.rgb = bc; sh.line.width = Pt(2)
else: sh.line.fill.background()
return sh
def circ(s, l, tp, sz, c):
sh = s.shapes.add_shape(MSO_SHAPE.OVAL, l, tp, sz, sz)
sh.fill.solid(); sh.fill.fore_color.rgb = c; sh.line.fill.background()
return sh
def rect(s, l, tp, w, h, c):
sh = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, l, tp, w, h)
sh.fill.solid(); sh.fill.fore_color.rgb = c; sh.line.fill.background()
return sh
# counter for unique shape IDs
_shape_id = [100]
def sid():
_shape_id[0] += 1
return _shape_id[0]
# track shapes with their IDs for animation
anim_shapes = [] # list of (slide_idx, shape_id, animation_type, delay_ms, dur_ms)
def add_shape_with_anim(slide, slide_idx, shape, anim_type, delay_ms=0, dur_ms=500):
"""Register a shape for animation. Returns the shape's spId."""
sp = shape.shape_id
anim_shapes.append((slide_idx, sp, anim_type, delay_ms, dur_ms))
return sp
# ══════════════════════════════════════════════════════════════
# SLIDE 1 — Title
# ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BL); bg(s, BG)
add_shape_with_anim(s, 0, circ(s, Inches(10), Inches(0.5), Inches(3), RGBColor(0,61,92)), 'spin', 300, 2000)
add_shape_with_anim(s, 0, circ(s, Inches(10.5), Inches(1), Inches(2), CYAN), 'spin', 500, 2000)
add_shape_with_anim(s, 0, txt(s, '🇰🇿', Inches(1), Inches(1.2), Inches(2), Inches(1.5), sz=80, c=WHITE), 'zoom', 100, 800)
add_shape_with_anim(s, 0, txt(s, 'КАЗАХСТАН', Inches(1), Inches(2.8), Inches(8), Inches(1.5), sz=72, b=True, c=WHITE), 'zoom', 400, 1000)
add_shape_with_anim(s, 0, txt(s, 'Сердце Евразии — девятая по территории страна мира', Inches(1), Inches(4.3), Inches(8), Inches(0.8), sz=24, c=GRAY), 'fly', 700, 800)
for i, (v, l) in enumerate([('2.7 млн км²','Площадь'),('20 млн','Население'),('130+','Народов'),('1 космодром','Байконур')]):
x = Inches(1 + i*3); y = Inches(5.6)
b = box(s, x, y, Inches(2.7), Inches(1.2), RGBColor(28,38,54))
txt(s, v, x+Inches(.2), y+Inches(.1), Inches(2.3), Inches(.6), sz=24, b=True, c=CYAN)
txt(s, l, x+Inches(.2), y+Inches(.65), Inches(2.3), Inches(.4), sz=13, c=GRAY)
add_shape_with_anim(s, 0, b, 'bounce', 900+i*150, 600)
# ══════════════════════════════════════════════════════════════
# SLIDE 2 — Geography
# ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BL); bg(s, WHITE)
rect(s, 0, 0, W, Inches(1.4), BG)
add_shape_with_anim(s, 1, txt(s, 'ГЕОГРАФИЯ', Inches(.8), Inches(.3), Inches(8), Inches(.8), sz=36, b=True, c=CYAN), 'fly', 100, 600)
geo = [('🏔️','Горы','Тянь-Шань, Алтай. Хан-Тенгри 7010 м'),
('🌊','Каспий','Крупнейшее озеро мира. 1894 км'),
('🏜️','Степи','Родина кочевников'),
('🌾','Озёра','45 000+ озёр по всей стране')]
for i,(em,nm,ds) in enumerate(geo):
c2,r2 = i%2, i//2
x = Inches(.8+c2*6.2); y = Inches(2.2+r2*2.5)
b = box(s, x, y, Inches(5.6), Inches(2.2), RGBColor(245,247,250))
txt(s, em, x+Inches(.3), y+Inches(.15), Inches(1), Inches(.7), sz=38)
txt(s, nm, x+Inches(1.3), y+Inches(.2), Inches(4), Inches(.5), sz=22, b=True, c=INK)
txt(s, ds, x+Inches(1.3), y+Inches(.75), Inches(4), Inches(1.2), sz=14, c=GRAY)
add_shape_with_anim(s, 1, b, 'fly', 300+i*200, 700)
# ══════════════════════════════════════════════════════════════
# SLIDE 3 — Facts
# ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BL); bg(s, BG)
add_shape_with_anim(s, 2, txt(s, 'ЦИФРЫ И ФАКТЫ', Inches(.8), Inches(.6), Inches(10), Inches(1), sz=40, b=True, c=WHITE), 'zoom', 100, 600)
facts = [('9','по величине\nв мире'),('1991','год\nнезависимости'),('70+','тонн золота\nв год'),('1','космодром\nБайконур'),('3','столицы\nв истории')]
for i,(n,t) in enumerate(facts):
x = Inches(.5+i*2.5)
b = box(s, x, Inches(2.5), Inches(2.2), Inches(3.5), RGBColor(28,38,54), CYAN)
txt(s, n, x+Inches(.2), Inches(2.9), Inches(1.8), Inches(1.2), sz=52, b=True, c=CYAN, a=PP_ALIGN.CENTER)
txt(s, t, x+Inches(.2), Inches(4.2), Inches(1.8), Inches(1.4), sz=14, c=GRAY, a=PP_ALIGN.CENTER)
add_shape_with_anim(s, 2, b, 'zoom', 200+i*200, 800)
# ══════════════════════════════════════════════════════════════
# SLIDE 4 — Culture
# ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BL); bg(s, RGBColor(240,244,248))
rect(s, 0, 0, W, Inches(1.4), BG)
add_shape_with_anim(s, 3, txt(s, 'КУЛЬТУРА', Inches(.8), Inches(.3), Inches(10), Inches(.8), sz=36, b=True, c=GOLD), 'fly', 100, 600)
cult = [('🦅','Беркутчи','Охота с орлами — ЮНЕСКО'),('🎪','Юрта','Символ свободы кочевников'),
('🎶','Домбра','Душа казахских кюев'),('🤼','Байге','Конные скачки'),
('🎭','Наурыз','7 блюд, 22 марта'),('📖','Абай','Поэт и мыслитель')]
for i,(em,nm,ds) in enumerate(cult):
c2,r2 = i%3, i//3
x = Inches(.8+c2*4.1); y = Inches(1.8+r2*2.6)
b = box(s, x, y, Inches(3.7), Inches(2.2), WHITE)
txt(s, em, x+Inches(.3), y+Inches(.15), Inches(1), Inches(.7), sz=36)
txt(s, nm, x+Inches(1.2), y+Inches(.2), Inches(2.2), Inches(.5), sz=20, b=True, c=INK)
txt(s, ds, x+Inches(.3), y+Inches(1), Inches(3.2), Inches(1), sz=14, c=GRAY)
add_shape_with_anim(s, 3, b, 'fly', 250+i*180, 700)
# ══════════════════════════════════════════════════════════════
# SLIDE 5 — Cities
# ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BL); bg(s, WHITE)
add_shape_with_anim(s, 4, txt(s, 'ГОРОДА', Inches(.8), Inches(.6), Inches(10), Inches(1), sz=40, b=True, c=INK), 'zoom', 100, 600)
cities = [('АСТАНА','Столица. Байтерек,\nХан-Шатыр',CYAN),('АЛМАТЫ','Горы, Медеу,\nШымбулак',GOLD),('ШЫМКЕНТ','Южные ворота,\nдревний город',RGBColor(42,157,143))]
for i,(nm,ds,cl) in enumerate(cities):
x = Inches(.8+i*4.1)
b = box(s, x, Inches(2.2), Inches(3.7), Inches(4.5), RGBColor(245,247,250))
rect(s, x, Inches(2.2), Inches(3.7), Inches(.12), cl)
txt(s, nm, x+Inches(.4), Inches(2.7), Inches(3), Inches(.6), sz=28, b=True, c=INK)
txt(s, ds, x+Inches(.4), Inches(3.5), Inches(3), Inches(2.5), sz=15, c=GRAY)
add_shape_with_anim(s, 4, b, 'fly', 300+i*250, 800)
# ══════════════════════════════════════════════════════════════
# SLIDE 6 — Food
# ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BL); bg(s, RGBColor(253,248,239))
add_shape_with_anim(s, 5, txt(s, 'КУХНЯ', Inches(.8), Inches(.6), Inches(10), Inches(1), sz=40, b=True, c=INK), 'zoom', 100, 600)
foods = [('🥟','Бешбармак','Мясо с лапшой — едят руками'),('🫕','Кумыс','Кобылье молоко'),
('🫓','Баурсаки','Тестовые шарики на празднике'),('🍵','Чай с молоком','С солью — знак уважения')]
for i,(em,nm,ds) in enumerate(foods):
c2,r2 = i%2, i//2
x = Inches(.8+c2*6.2); y = Inches(2.2+r2*2.4)
b = box(s, x, y, Inches(5.6), Inches(2), WHITE, GOLD)
txt(s, em, x+Inches(.3), y+Inches(.15), Inches(1), Inches(.7), sz=38)
txt(s, nm, x+Inches(1.3), y+Inches(.2), Inches(3.5), Inches(.5), sz=22, b=True, c=INK)
txt(s, ds, x+Inches(1.3), y+Inches(.8), Inches(3.8), Inches(1), sz=14, c=GRAY)
add_shape_with_anim(s, 5, b, 'bounce', 200+i*200, 700)
# ══════════════════════════════════════════════════════════════
# SLIDE 7 — History
# ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BL); bg(s, WHITE)
add_shape_with_anim(s, 6, txt(s, 'ИСТОРИЯ', Inches(.8), Inches(.6), Inches(10), Inches(1), sz=40, b=True, c=INK), 'zoom', 100, 600)
rect(s, Inches(1.6), Inches(2.2), Inches(.06), Inches(4.8), CYAN)
tl = [('1465','Казахское ханство'),('XVIII','Российская империя'),('1955','Байконур'),
('1961','Гагарин в космосе'),('1991','Независимость'),('1997','Новая столица')]
for i,(yr,ttl) in enumerate(tl):
y = Inches(2.2+i*.8)
add_shape_with_anim(s, 6, circ(s, Inches(1.4), y+Inches(.08), Inches(.45), CYAN if i%2==0 else GOLD), 'bounce', 200+i*150, 500)
txt(s, yr, Inches(2.2), y, Inches(1.2), Inches(.6), sz=16, b=True, c=INK)
txt(s, ttl, Inches(3.5), y, Inches(9), Inches(.6), sz=14, c=GRAY)
# ══════════════════════════════════════════════════════════════
# SLIDE 8 — Thank You
# ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BL); bg(s, BG)
add_shape_with_anim(s, 7, circ(s, Inches(10), Inches(.5), Inches(3), RGBColor(0,61,92)), 'spin', 300, 2000)
add_shape_with_anim(s, 7, circ(s, Inches(10.5), Inches(1), Inches(2), CYAN), 'spin', 500, 2000)
add_shape_with_anim(s, 7, circ(s, Inches(.5), Inches(5), Inches(2.5), RGBColor(0,61,92)), 'spin', 400, 2000)
add_shape_with_anim(s, 7, circ(s, Inches(1), Inches(5.5), Inches(1.5), GOLD), 'spin', 600, 2000)
add_shape_with_anim(s, 7, txt(s, '🇰🇿', Inches(5.5), Inches(1), Inches(2.5), Inches(1.5), sz=80, c=WHITE, a=PP_ALIGN.CENTER), 'zoom', 200, 1000)
add_shape_with_anim(s, 7, txt(s, 'СПАСИБО ЗА ВНИМАНИЕ!', Inches(1), Inches(2.8), Inches(11.3), Inches(1.2), sz=48, b=True, c=WHITE, a=PP_ALIGN.CENTER), 'fly', 500, 900)
# ── Save initial PPTX ──
tmp_pptx = '/tmp/kz_initial.pptx'
prs.save(tmp_pptx)
print(f'✅ Created {len(prs.slides)} slides, {len(anim_shapes)} animated shapes')
# ══════════════════════════════════════════════════════════════
# STEP 2: Inject proper animation XML
# ══════════════════════════════════════════════════════════════
WORK = '/tmp/kz_pptx_work'
if os.path.exists(WORK):
shutil.rmtree(WORK)
os.makedirs(WORK)
# Unzip
with zipfile.ZipFile(tmp_pptx, 'r') as z:
z.extractall(WORK)
P = 'http://schemas.openxmlformats.org/presentationml/2006/main'
A = 'http://schemas.openxmlformats.org/drawingml/2006/main'
def make_fly_in_xml(sp_id, delay, dur):
"""Fly-in from left animation."""
return f'''<p:timing xmlns:p="{P}" xmlns:a="{A}">
<p:tnLst>
<p:par>
<p:cTn id="1" dur="indefinite" restart="never" nodeType="timingRoot">
<p:childTnLst>
<p:seq concurrent="1" nextAc="seek">
<p:cTn id="2" dur="indefinite" nodeType="mainSeq">
<p:childTnLst>'''
def make_anim_block(sp_id, delay, dur, anim_type):
"""Generate animation block for one shape."""
if anim_type == 'fly':
return f'''
<p:par>
<p:cTn id="{sp_id*10+1}" fill="hold">
<p:stCondLst><p:cond delay="{delay}"/></p:stCondLst>
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+2}" fill="hold">
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+3}" fill="hold">
<p:childTnLst>
<p:set>
<p:cBhvr>
<p:cTn id="{sp_id*10+4}" dur="1" fill="hold"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="1"/></p:to>
</p:set>
</p:childTnLst>
</p:cTn>
</p:par>
<p:animMotion origin="layout" path="left">
<p:cBhvr>
<p:cTn id="{sp_id*10+5}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:vectOml><p:strRef><p:f>0</p:f></p:strRef></p:vectOml>
</p:animMotion>
</p:childTnLst>
</p:cTn>
</p:par>
</p:childTnLst>
</p:cTn>
</p:par>'''
elif anim_type == 'zoom':
return f'''
<p:par>
<p:cTn id="{sp_id*10+1}" fill="hold">
<p:stCondLst><p:cond delay="{delay}"/></p:stCondLst>
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+2}" fill="hold">
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+3}" fill="hold">
<p:childTnLst>
<p:set>
<p:cBhvr>
<p:cTn id="{sp_id*10+4}" dur="1" fill="hold"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="1"/></p:to>
</p:set>
</p:childTnLst>
</p:cTn>
</p:par>
<p:anim calcmode="lin" valueType="num">
<p:cBhvr>
<p:cTn id="{sp_id*10+5}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:varLst>
<p:numVal formula="0"><p:ptVal/></p:numVal>
<p:numVal formula="1"><p:ptVal/></p:numVal>
</p:varLst>
</p:anim>
<p:animEffect transition="with" filter="appear">
<p:cBhvr>
<p:cTn id="{sp_id*10+6}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:filter val="appear"/>
</p:animEffect>
</p:childTnLst>
</p:cTn>
</p:par>
</p:childTnLst>
</p:cTn>
</p:par>'''
elif anim_type == 'spin':
return f'''
<p:par>
<p:cTn id="{sp_id*10+1}" fill="hold">
<p:stCondLst><p:cond delay="{delay}"/></p:stCondLst>
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+2}" fill="hold">
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+3}" fill="hold">
<p:childTnLst>
<p:set>
<p:cBhvr>
<p:cTn id="{sp_id*10+4}" dur="1" fill="hold"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="1"/></p:to>
</p:set>
</p:childTnLst>
</p:cTn>
</p:par>
<p:animRotation>
<p:cBhvr>
<p:cTn id="{sp_id*10+5}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="3600000"/></p:to>
</p:animRotation>
<p:animEffect transition="with" filter="appear">
<p:cBhvr>
<p:cTn id="{sp_id*10+6}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:filter val="appear"/>
</p:animEffect>
</p:childTnLst>
</p:cTn>
</p:par>
</p:childTnLst>
</p:cTn>
</p:par>'''
elif anim_type == 'bounce':
return f'''
<p:par>
<p:cTn id="{sp_id*10+1}" fill="hold">
<p:stCondLst><p:cond delay="{delay}"/></p:stCondLst>
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+2}" fill="hold">
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+3}" fill="hold">
<p:childTnLst>
<p:set>
<p:cBhvr>
<p:cTn id="{sp_id*10+4}" dur="1" fill="fail"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="visible"/></p:to>
</p:set>
</p:childTnLst>
</p:cTn>
</p:par>
<p:anim calcmode="lin" valueType="num">
<p:cBhvr>
<p:cTn id="{sp_id*10+5}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:varLst>
<p:numVal formula="0"><p:ptVal/></p:numVal>
<p:numVal formula="1.2"><p:ptVal/></p:numVal>
<p:numVal formula="0.85"><p:ptVal/></p:numVal>
<p:numVal formula="1.05"><p:ptVal/></p:numVal>
<p:numVal formula="0.97"><p:ptVal/></p:numVal>
<p:numVal formula="1"><p:ptVal/></p:numVal>
</p:varLst>
</p:anim>
<p:animEffect transition="with" filter="appear">
<p:cBhvr>
<p:cTn id="{sp_id*10+6}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:filter val="appear"/>
</p:animEffect>
</p:childTnLst>
</p:cTn>
</p:par>
</p:childTnLst>
</p:cTn>
</p:par>'''
return ''
# Process each slide
for slide_idx in range(8):
slide_path = os.path.join(WORK, 'ppt', 'slides', f'slide{slide_idx+1}.xml')
tree = etree.parse(slide_path)
root = tree.getroot()
# Get shapes on this slide
shapes_on_slide = [(sp_id, anim, delay, dur)
for (si, sp_id, anim, delay, dur) in anim_shapes
if si == slide_idx]
if not shapes_on_slide:
continue
# Build timing XML
timing_parts = []
timing_parts.append(make_fly_in_xml(0, 0, 0)) # header
for sp_id, anim, delay, dur in shapes_on_slide:
timing_parts.append(make_anim_block(sp_id, delay, dur, anim))
timing_parts.append('''
</p:childTnLst>
</p:cTn>
<p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst>
<p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst>
</p:seq>
</p:childTnLst>
</p:cTn>
</p:par>
</p:tnLst>
</p:timing>''')
timing_xml = ''.join(timing_parts)
timing_el = etree.fromstring(timing_xml)
root.append(timing_el)
tree.write(slide_path, xml_declaration=True, encoding='UTF-8', standalone=True)
print(f' ✓ Slide {slide_idx+1}: {len(shapes_on_slide)} animations injected')
# ══════════════════════════════════════════════════════════════
# STEP 3: Repackage
# ══════════════════════════════════════════════════════════════
out_pptx = '/srv/opencode/workspaces/users/literally.ww/kazakhstan/Казахстан_Презентация.pptx'
with zipfile.ZipFile(out_pptx, 'w', zipfile.ZIP_DEFLATED) as zout:
for dirpath, dirnames, filenames in os.walk(WORK):
for fn in filenames:
full = os.path.join(dirpath, fn)
arc = os.path.relpath(full, WORK)
zout.write(full, arc)
print(f'\n🎉 Saved: {out_pptx}')
print(f' {len(anim_shapes)} shape animations across 8 slides')
print(f' Animation types: fly-in, zoom, spin, bounce')
print(f' Open in PowerPoint → F5 → animations play on click/advance')