diff --git a/create_pptx.py b/create_pptx.py index cb30602..abdbac2 100644 --- a/create_pptx.py +++ b/create_pptx.py @@ -1,343 +1,461 @@ #!/usr/bin/env python3 -"""Презентация о Казахстане — чистый PPTX без кривых XML-анимаций.""" +""" +Презентация о Казахстане с настоящими PowerPoint-анимациями. +Подход: создаём PPTX через python-pptx, затем распаковываем, +вставляем правильный 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, MSO_ANCHOR +from pptx.enum.text import PP_ALIGN from pptx.enum.shapes import MSO_SHAPE from pptx.oxml.ns import qn -import lxml.etree as etree +import zipfile, shutil, os, copy, tempfile +from lxml import etree # ── Colors ── -INK = RGBColor(0x0F, 0x12, 0x18) +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_DARK = RGBColor(0x14, 0x1C, 0x28) +GRAY = RGBColor(0x5B, 0x65, 0x73) +BG = RGBColor(0x14, 0x1C, 0x28) prs = Presentation() -prs.slide_width = Inches(13.333) +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] -BLANK_LAYOUT = 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 set_slide_transition(slide, transition_type='push', speed='med', advance_ms=None): - """Set slide transition effect.""" - timing = slide._element.find(qn('p:transition')) - if timing is None: - timing = etree.SubElement(slide._element, qn('p:transition')) +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 - transitions = { - 'push': ('push', {'advClick': '1'}), - 'fade': ('fade', {'advClick': '1'}), - 'wipe': ('wipe', {'dir': 'd', 'advClick': '1'}), - 'cover': ('cover', {'dir': 'l', 'advClick': '1'}), - 'cut': ('cut', {'advClick': '1'}), - 'dissolve': ('dissolve', {'advClick': '1'}), - 'zoom': ('zoom', {'advClick': '1'}), - } - tag_name, attrs = transitions.get(transition_type, transitions['push']) - el = etree.SubElement(timing, qn(f'p:{tag_name}')) - for k, v in attrs.items(): - el.set(k, v) - if advance_ms: - timing.set('advTm', str(advance_ms)) +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 -def add_bg(slide, color): - bg = slide.background - fill = bg.fill - fill.solid() - fill.fore_color.rgb = color +# 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_text(slide, text, left, top, width, height, - font_size=24, bold=False, color=INK, align=PP_ALIGN.LEFT, - font_name='Calibri'): - txBox = slide.shapes.add_textbox(left, top, width, height) - tf = txBox.text_frame - tf.word_wrap = True - p = tf.paragraphs[0] - p.text = text - p.font.size = Pt(font_size) - p.font.bold = bold - p.font.color.rgb = color - p.font.name = font_name - p.alignment = align - return txBox - - -def add_box(slide, left, top, width, height, fill_color=BG_DARK, - border_color=None): - shape = slide.shapes.add_shape( - MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height - ) - shape.fill.solid() - shape.fill.fore_color.rgb = fill_color - if border_color: - shape.line.color.rgb = border_color - shape.line.width = Pt(2) - else: - shape.line.fill.background() - return shape - - -def add_circle(slide, left, top, size, color): - c = slide.shapes.add_shape(MSO_SHAPE.OVAL, left, top, size, size) - c.fill.solid() - c.fill.fore_color.rgb = color - c.line.fill.background() - return c - - -def add_rect(slide, left, top, width, height, color): - r = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height) - r.fill.solid() - r.fill.fore_color.rgb = color - r.line.fill.background() - return r - +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(BLANK_LAYOUT) -add_bg(s, BG_DARK) -set_slide_transition(s, 'zoom') - -add_circle(s, Inches(9.5), Inches(0.8), Inches(4.5), RGBColor(0x00, 0x3D, 0x5C)) -add_circle(s, Inches(10.2), Inches(1.5), Inches(3), CYAN) -add_circle(s, Inches(-0.5), Inches(5.5), Inches(3), RGBColor(0x00, 0x3D, 0x5C)) -add_circle(s, Inches(0), Inches(6), Inches(2), GOLD) - -add_text(s, '🇰🇿', Inches(1), Inches(1.2), Inches(2), Inches(1.5), font_size=80, color=WHITE) -add_text(s, 'КАЗАХСТАН', Inches(1), Inches(2.8), Inches(8), Inches(1.5), - font_size=72, bold=True, color=WHITE) -add_text(s, 'Сердце Евразии — девятая по территории страна мира', - Inches(1), Inches(4.3), Inches(8), Inches(0.8), - font_size=24, color=GRAY) - -stats = [('2.7 млн км²', 'Площадь'), ('20 млн', 'Население'), - ('130+', 'Народов'), ('1 космодром', 'Байконур')] -for i, (val, lbl) in enumerate(stats): - x = Inches(1 + i * 3) - y = Inches(5.6) - add_box(s, x, y, Inches(2.7), Inches(1.2), RGBColor(0x1C, 0x26, 0x36)) - add_text(s, val, x + Inches(0.2), y + Inches(0.1), Inches(2.3), Inches(0.6), - font_size=24, bold=True, color=CYAN) - add_text(s, lbl, x + Inches(0.2), y + Inches(0.65), Inches(2.3), Inches(0.4), - font_size=13, color=GRAY) - +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(BLANK_LAYOUT) -add_bg(s, WHITE) -set_slide_transition(s, 'push') - -add_rect(s, 0, 0, W, Inches(1.4), BG_DARK) -add_text(s, 'ГЕОГРАФИЯ', Inches(0.8), Inches(0.3), Inches(8), Inches(0.8), - font_size=36, bold=True, color=CYAN) -add_text(s, 'От каспийских берегов до алтайских гор', Inches(0.8), Inches(1.4), - Inches(8), Inches(0.5), font_size=16, color=GRAY) - -geo = [ - ('🏔️', 'Горы', 'Тянь-Шань, Чу-Илийские горы, Алтай.\nВысшая точка — Хан-Тенгри (7010 м)'), - ('🌊', 'Каспийское море', 'Крупнейшее озеро мира.\nПобережье тянется на 1894 км'), - ('🏜️', 'Степи', 'Бескрайние равнины —\nисторическая родина кочевников'), - ('🌾', 'Озёра', 'Балхаш, Алаколь, Маркаколь —\nболее 45 000 озёр'), -] -for i, (em, nm, ds) in enumerate(geo): - col, row = i % 2, i // 2 - x = Inches(0.8 + col * 6.2) - y = Inches(2.2 + row * 2.5) - add_box(s, x, y, Inches(5.6), Inches(2.2), RGBColor(0xF5, 0xF7, 0xFA)) - add_text(s, em, x + Inches(0.3), y + Inches(0.15), Inches(1), Inches(0.7), - font_size=38) - add_text(s, nm, x + Inches(1.3), y + Inches(0.2), Inches(4), Inches(0.5), - font_size=22, bold=True, color=INK) - add_text(s, ds, x + Inches(1.3), y + Inches(0.75), Inches(4), Inches(1.2), - font_size=14, color=GRAY) - +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(BLANK_LAYOUT) -add_bg(s, BG_DARK) -set_slide_transition(s, 'fade') - -add_text(s, 'ЦИФРЫ И ФАКТЫ', Inches(0.8), Inches(0.6), Inches(10), Inches(1), - font_size=40, bold=True, color=WHITE) -add_text(s, 'Казахстан в цифрах', Inches(0.8), Inches(1.4), Inches(6), Inches(0.5), - font_size=18, color=GRAY) - -facts = [('9', 'по величине\nв мире'), ('1991', 'год\nнезависимости'), - ('70+', 'тонн золота\nв год'), ('1', 'космодром\nБайконур'), - ('3', 'столицы\nв истории')] -for i, (num, txt) in enumerate(facts): - x = Inches(0.5 + i * 2.5) - add_box(s, x, Inches(2.5), Inches(2.2), Inches(3.5), - RGBColor(0x1C, 0x26, 0x36), CYAN) - add_text(s, num, x + Inches(0.2), Inches(2.9), Inches(1.8), Inches(1.2), - font_size=52, bold=True, color=CYAN, align=PP_ALIGN.CENTER) - add_text(s, txt, x + Inches(0.2), Inches(4.2), Inches(1.8), Inches(1.4), - font_size=14, color=GRAY, align=PP_ALIGN.CENTER) - -add_rect(s, Inches(0.8), Inches(6.5), Inches(11.7), Inches(0.04), CYAN) - +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(BLANK_LAYOUT) -add_bg(s, RGBColor(0xF0, 0xF4, 0xF8)) -set_slide_transition(s, 'cover') - -add_rect(s, 0, 0, W, Inches(1.4), BG_DARK) -add_text(s, 'КУЛЬТУРА И ТРАДИЦИИ', Inches(0.8), Inches(0.3), Inches(10), Inches(0.8), - font_size=36, bold=True, color=GOLD) - -culture = [ - ('🦅', 'Беркутчи', 'Соколиная охота с орлами — наследие ЮНЕСКО'), - ('🎪', 'Юрта', 'Легко разборное жилище — символ свободы'), - ('🎶', 'Домбра', 'Двухструнный инструмент — душа кюев'), - ('🤼', 'Байге', 'Конные скачки на длинные дистанции'), - ('🎭', 'Наурыз', 'Праздник весны — 7 блюд, 22 марта'), - ('📖', 'Абай', 'Великий поэт, философ, мыслитель'), -] -for i, (em, nm, ds) in enumerate(culture): - col, row = i % 3, i // 3 - x = Inches(0.8 + col * 4.1) - y = Inches(1.8 + row * 2.6) - add_box(s, x, y, Inches(3.7), Inches(2.2), WHITE) - add_text(s, em, x + Inches(0.3), y + Inches(0.15), Inches(1), Inches(0.7), - font_size=36) - add_text(s, nm, x + Inches(1.2), y + Inches(0.2), Inches(2.2), Inches(0.5), - font_size=20, bold=True, color=INK) - add_text(s, ds, x + Inches(0.3), y + Inches(1), Inches(3.2), Inches(1), - font_size=14, color=GRAY) - +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(BLANK_LAYOUT) -add_bg(s, WHITE) -set_slide_transition(s, 'push') - -add_text(s, 'ГОРОДА', Inches(0.8), Inches(0.6), Inches(10), Inches(1), - font_size=40, bold=True, color=INK) -add_text(s, 'Мегаполисы Казахстана', Inches(0.8), Inches(1.3), Inches(6), Inches(0.5), - font_size=18, color=GRAY) - -cities = [ - ('АСТАНА', 'Столица с 1997 г.\nБайтерек, Хан-Шатыр,\nфонтаны «Танкіри»', CYAN), - ('АЛМАТЫ', 'Бывшая столица\nГоры, Медеу, Шымбулак,\nалматинские яблоки', GOLD), - ('ШЫМКЕНТ', 'Третий город\nЮжные ворота,\nжаркий климат', RGBColor(0x2A, 0x9D, 0x8F)), -] -for i, (nm, ds, clr) in enumerate(cities): - x = Inches(0.8 + i * 4.1) - add_box(s, x, Inches(2.2), Inches(3.7), Inches(4.5), RGBColor(0xF5, 0xF7, 0xFA)) - add_rect(s, x, Inches(2.2), Inches(3.7), Inches(0.12), clr) - add_text(s, nm, x + Inches(0.4), Inches(2.7), Inches(3), Inches(0.6), - font_size=28, bold=True, color=INK) - add_text(s, ds, x + Inches(0.4), Inches(3.5), Inches(3), Inches(2.5), - font_size=15, color=GRAY) - +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(BLANK_LAYOUT) -add_bg(s, RGBColor(0xFD, 0xF8, 0xEF)) -set_slide_transition(s, 'wipe') - -add_text(s, 'КУХНЯ', Inches(0.8), Inches(0.6), Inches(10), Inches(1), - font_size=40, bold=True, color=INK) -add_text(s, 'Вкус степи — просто и сытно', Inches(0.8), Inches(1.3), Inches(6), Inches(0.5), - font_size=18, color=GRAY) - -foods = [ - ('🥟', 'Бешбармак', 'Главное блюдо — мясо с лапшой.\n«Пять пальцев» — едят руками'), - ('🫕', 'Кумыс', 'Ферментированное кобылье\nмолоко — тонизирующий напиток'), - ('🫓', 'Баурсаки', 'Жареные тестовые шарики\nна каждом празднике'), - ('🍵', 'Чай с молоком', 'С солью — гостям наливают\nпервым, знак уважения'), -] -for i, (em, nm, ds) in enumerate(foods): - col, row = i % 2, i // 2 - x = Inches(0.8 + col * 6.2) - y = Inches(2.2 + row * 2.4) - add_box(s, x, y, Inches(5.6), Inches(2), WHITE, GOLD) - add_text(s, em, x + Inches(0.3), y + Inches(0.15), Inches(1), Inches(0.7), - font_size=38) - add_text(s, nm, x + Inches(1.3), y + Inches(0.2), Inches(3.5), Inches(0.5), - font_size=22, bold=True, color=INK) - add_text(s, ds, x + Inches(1.3), y + Inches(0.8), Inches(3.8), Inches(1), - font_size=14, color=GRAY) - +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(BLANK_LAYOUT) -add_bg(s, WHITE) -set_slide_transition(s, 'dissolve') - -add_text(s, 'ИСТОРИЯ', Inches(0.8), Inches(0.6), Inches(10), Inches(1), - font_size=40, bold=True, color=INK) -add_text(s, 'От кочевников до космоса', Inches(0.8), Inches(1.3), Inches(6), Inches(0.5), - font_size=18, color=GRAY) - -add_rect(s, Inches(1.6), Inches(2.2), Inches(0.06), Inches(4.8), CYAN) - -timeline = [ - ('1465', 'Казахское ханство', 'Формирование народа и кочевого уклада'), - ('XVIII', 'Российская империя', 'Присоединение, оседлый образ жизни'), - ('1955', 'Байконур', 'Запуск первого космодрома в мире'), - ('1961', 'Гагарин', 'Первый полёт в космос с Байконура'), - ('1991', 'Независимость', '16 декабря — последняя республика СССР'), - ('1997', 'Новая столица', 'Перенос столицы в Акмолу (Астану)'), -] -for i, (yr, ttl, dsc) in enumerate(timeline): - y = Inches(2.2 + i * 0.8) - dot = add_circle(s, Inches(1.4), y + Inches(0.08), Inches(0.45), - CYAN if i % 2 == 0 else GOLD) - add_text(s, yr, Inches(2.2), y, Inches(1.2), Inches(0.6), - font_size=16, bold=True, color=INK) - add_text(s, f'{ttl} — {dsc}', Inches(3.5), y, Inches(9), Inches(0.6), - font_size=14, color=GRAY) - +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(BLANK_LAYOUT) -add_bg(s, BG_DARK) -set_slide_transition(s, 'zoom') +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) -add_circle(s, Inches(10), Inches(0.5), Inches(3), RGBColor(0x00, 0x3D, 0x5C)) -add_circle(s, Inches(10.5), Inches(1), Inches(2), CYAN) -add_circle(s, Inches(0.5), Inches(5), Inches(2.5), RGBColor(0x00, 0x3D, 0x5C)) -add_circle(s, Inches(1), Inches(5.5), Inches(1.5), GOLD) - -add_text(s, '🇰🇿', Inches(5.5), Inches(1), Inches(2.5), Inches(1.5), - font_size=80, color=WHITE, align=PP_ALIGN.CENTER) -add_text(s, 'СПАСИБО ЗА ВНИМАНИЕ!', Inches(1), Inches(2.8), Inches(11.3), Inches(1.2), - font_size=48, bold=True, color=WHITE, align=PP_ALIGN.CENTER) -add_text(s, 'Казахстан — древние традиции и современные мегаполисы,\nбескрайние степи и высокие горы', - Inches(2), Inches(4.2), Inches(9.3), Inches(1.2), - font_size=18, color=GRAY, align=PP_ALIGN.CENTER) +# ── 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') -# ── Save ── -out = '/srv/opencode/workspaces/users/literally.ww/kazakhstan/Казахстан_Презентация.pptx' -prs.save(out) -print(f'✅ Saved: {out}') -print(f' Slides: {len(prs.slides)}') +# ══════════════════════════════════════════════════════════════ +# 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''' + + + + + + + ''' + +def make_anim_block(sp_id, delay, dur, anim_type): + """Generate animation block for one shape.""" + if anim_type == 'fly': + return f''' + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + ''' + elif anim_type == 'zoom': + return f''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + elif anim_type == 'spin': + return f''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + elif anim_type == 'bounce': + return f''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + 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(''' + + + + + + + + + +''') + + 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') diff --git a/Казахстан_Презентация.pptx b/Казахстан_Презентация.pptx index 1bd222f..be56ab9 100644 Binary files a/Казахстан_Презентация.pptx and b/Казахстан_Презентация.pptx differ