#!/usr/bin/env python3 """ Презентация о Казахстане с настоящими 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 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''' ''' 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')