PPTファイルに音声を自動で埋め込む その3 Pythonによる実装

授業

Pythonを使って,pptファイルのスライド切替えとアニメーションにオーディオデータを埋め込むことができることを確認しました。これから,具体的なPythonコードについて解説していきます。

これらのコードは,とにかく早期に「動くかどうか確認する」ことを優先して作りました。このため汚いし読みにくいし,後々の利用を考えても使いやすいとは言えません。手直ししながら投稿していく予定です。

<余談を読む>

PPTファイルに音声を自動で埋め込む その2 にも書きましたように,pptxファイルの変更の方法は,公開されている技術文書などから得たものではありません。解凍したpptxファイルの中を観測することで仮説を立て,それに基づいてコードを作って確認したものです。(これは格好つけた言い方で,実際は「こうじゃないかなと思ってやってみたら運よく動いた・・・今のところは。」ですね。)

ですから,考え方が間違っている可能性はありますし,複雑なプレゼンのpptxファイルに対しては期待したようには動かないかもしれません。

なお,pptファイルにナレーションを付加する有料ソフトとして,株式会社エーアイさんが「AITalk® 声プラス」と言う製品を販売しています(私は音声合成に「かんたん!AITalk®3」という製品を使っています)。pptプレゼンテーションに合成音でナレーションを付加するソフトへのニーズはあるのでしょう。

商用ソフトがあるくらいですから,pptファイルにオーディオデータを付加する技術に関する資料は,私が方法を知らないだけで,どこかで入手できることは確かでしょう。さらに,Python-pptxライブラリの整備が進んで,私が作ったコードのほとんどが無用のものになる可能性も大だと思います。

でも,面白いし勉強にはなります。それに大量にある講義用のppt資料にナレーションを付けるためには急いで自動化したいし,何といっても年金生活者には高いソフトは導入できません。やれるところまでやってみようと考えています。

<閉じる>

Pythonコード

それでは,作ったPythonコードについて説明していきます。

pptxファイルの展開/再圧縮用のツール

pptxファイルの中身を調べるために,zip展開(unzip)とpptxファイルに再圧縮するためのコードを作りました。Windowsの操作で,拡張子変更やzip/unzipをしても可能なはずですが,私のPCでは再圧縮して作ったpptxファイルを開くときにエラーになり,修復するとオーディオデータが消えてしまうという不具合がありました。また,多数のファイルのuzip/zipを繰り返すので,Pythonコードを作ることにしました。

作ったコードは以下の2つです。

  • PPTX_Decompress.py
    pptxファイルをunzipします。できあがるのは,pptxファイルのファイル名から拡張子を取り除いたものを名前に持つフォルダになります。
  • PPTX_Compress.py
    指定したフォルダをzip圧縮し,拡張子をzipからpptxに戻します。

PPTX_Decompress.py

リストにコードを示します。

# PPTXファイルを解凍
# PPTX decompress
from tkinter import filedialog
import os
from zipfile import ZipFile

# 対象となるPPTXファイルを指定する
# Specify the target PPTX file.
typ_pptx = [("PPTX File","*.pptx")]
fileName = filedialog.askopenfilename(
    title = "PPTXファイルを指定",
    filetypes = typ_pptx, 
    initialdir = dir) 

# ファイル名から拡張子を取り除いた文字列を取得
# Get the string of the file name without the file extension.
name_no_ext = os.path.splitext(os.path.basename(fileName))[0]

# PPTXファイルをzipファイルとして展開
# Unzip the PPTX file.
with ZipFile(fileName, 'r') as zip_file:
    zip_file.extractall(name_no_ext) 
    zip_file.close()

解説は不要かと思います。一応,書いておきます。ほとんどコメントと同じですが。

  • 必要なモジュールをインポート
    from tkinter import filedialog
    import os
    from zipfile import ZipFile
  • ダイアログボックスを開き,変換するpptxファイルを指定
    typ_pptx = [(“PPTX File”,”*.pptx”)]
    fileName = filedialog.askopenfilename(
    title = “PPTXファイルを指定”,
    filetypes = typ_pptx,
    initialdir = dir)
  • ファイル名から拡張子を取り除いた文字列name_no_extを取得
    name_no_ext = os.path.splitext(os.path.basename(fileName))[0]
     この式文の末尾の[0]を[1]にすると,拡張子の文字列が得られます。
  • PPTXファイルをzipファイルとして展開
    with ZipFile(fileName, ‘r’) as zip_file:
    zip_file.extractall(name_no_ext)
    zip_file.close()
     展開したファイルを格納するフォルダ名を引数にしてextractall関数を呼ぶだけです。元のpptxファイル名の拡張子を除いたものをフォルダ名とします。

PPTX_Compress.py

# PPTX_Compress 展開されたPPTXフォルダをzip圧縮してPPTXファイルを作成
# Create PPTX files by zipping the expanded PPTX folder.
from tkinter import filedialog
import os
import shutil

# 非圧縮状態のフォルダを指定
# Specify a folder in uncompressed state.
pptx_dir = filedialog.askdirectory(
    title = "Specify a folder.",
    initialdir = dir) 
pptx_name = pptx_dir + '_R'

# フォルダを再びzipにアーカイブ
# Archive the folder to zip again.
shutil.make_archive(pptx_name, format='zip', root_dir=pptx_dir)

# Change file extension from .zip to .pptx.
name_src = pptx_name + '.zip'
name_dst = pptx_name + '.pptx'
# If the destination file already exists, 
# delete the file to avoid an error.
is_file = os.path.isfile(name_dst)
if is_file:
    os.remove(name_dst)
os.rename(name_src, name_dst) 

こちらも必要な関数を並べているだけです。少し注意が必要なのは一番最後の,再圧縮したzipファイルの拡張子を.zipから.pptxに書き直すos.rename関数のところです。リネームの結果,同じ名前のファイルが存在するとエラーになります。

そこで,このコードでは元のpptxファイル名の後ろに“_R”を付けて異なるファイル名になるようにしています。さらに同じ名前があった場合は,そのファイルを削除してからリネームするようにしています。

  • 必要なモジュールをインポート
    from tkinter import filedialog
    import os
    import shutil
     shutilモジュールをインポートしている。
  • 圧縮するフォルダを指定し,圧縮後のファイル名を設定
    pptx_dir = filedialog.askdirectory(
    title = “Specify a folder.”,
    initialdir = dir)
    pptx_name = pptx_dir + ‘_R’
     ダイアログボックスでフォルダ名pptx_dirを取得する。さらに,これに“_R”を追加して新しいファイル名(拡張子無し)であるpptx_nameにする。
  • フォルダを再びzipにアーカイブ
    shutil.make_archive(pptx_name, format=’zip’, root_dir=pptx_dir)
     shutilのmake_archive関数で,フォルダpptx_dirをpptx_nameという名前のzipファイルに変換する。
  • リネームの操作
    name_src = pptx_name + ‘.zip’
    name_dst = pptx_name + ‘.pptx’
     zipファイルとリネーム後のpptxファイルの名前を拡張子も含めて決定する。
    is_file = os.path.isfile(name_dst)
    if is_file:
    os.remove(name_dst)
     リネーム後のファイルが存在する場合は,削除。
    os.rename(name_src, name_dst)
     name_srcからname_dstにリネーム。

PPTX_Compress.pyは,作成したPythonアプリケーションでは関数として登録しています。

アプリケーションの仕様

作成しようとしているPythonアプリケーションは,私の講義資料のpptファイルを自動で処理することを目的としています。つまり汎用ではなく限定された条件で使うことになります。処理できるpptファイルや,オーディオファイルの付加の方法も細かい設定はできません。

処理するpptファイルやオーディオ付加の条件は以下のとおりです。

  • pptファイルはpptx形式であること
  • pptファイルのアニメーションは既に設定済みであること
    アプリケーションには新たにアニメーションを設定したりタイミングを招請したりする機能は未実装。
  • pptファイルにオーディオデータは付加されていないこと
    現在の仕様では,オーディオデータが付加されていないことを前提に処理している。既にオーディオファイルが埋め込まれている場合,データの関連づけが崩れて正常に動作しなくなる可能性がある。オーディオ関連のxmlコードやオーディオファイルを削除する機能を加えれば対応は可能だが,現時点では未実装。
  • スライド切替えとクリックで起動されるアニメーションにのみオーディオデータを付加
    クリックではなく直前の動作で起動されるアニメーションへのオーディオ付加は未実装。
  • ユーザが用意するオーディオファイルの命名規則がある
    これは,Pythonコードに関する本質的な問題ではない。現在作っているアプリケーションでは,テキスト形式の「台本」(スクリプトと呼んでいる)に従ってpptxファイルのデータ加工を進めていく。付加するオーディオファイルのファイル名は拡張子の前に”×××_01_02.wav”のようにスライド番号とクリック番号をスクリプトの規則にしたがって並べておく必要がある。スライド番号とクリック番号は2桁0パッドの文字列で表している。(つまり最大値は99に限定される。)

講義資料については,上の条件はほとんど問題になりません。講義中に使う場合は,スライドの画面切り替えやアニメーションを起動するためのマウスの操作と口頭での説明は,教員が受講者の反応を見ながらおこないます。pptファイルに音声を埋め込む必要はありません。ただしデモのための音声や効果音を埋め込むことは皆無ではないので,そのようなファイルでは注意が必要です。

一方,学生さんの予習や復習用に,pptファイルをHTML5形式のスライドショーとしてwebサイトに掲載しています。この場合は,教員がしているクリック操作はかなりたくさんあるのですが,閲覧する人にやってもらうことはそんなに問題ではないでしょう(むしろ,閲覧者が自分でスピードをコントロールできる利点になると思います)。

しかし,閲覧する人にとっては,事前にはクリックの意図が不明なので,とまどうかもしれません。何といっても口頭で説明に関するオーディオや文字の情報が無いので,何を説明しているか困ってしまうかもしれません,講義のpptスライドにオーディオデータを付加しようと考えたのは,この問題に対応したいからです。

アプリケーションで使う関数

アプリケーションで使う関数について説明していきます。このアプリケーションはテキストファイルである.xmlファイルや.xml.relsファイルを扱うので,基本的には文字列処理がメインになります。

私は信号処理や数値シミュレーションなど,数値処理のコードの経験は少しあります。しかし,文字列の処理はファイル名の加工くらいしかやったことがありません。webで調べながら作っているのですが,おかしなコードになっているかもしれません。

コーディングをしている中で自作の関数を付け足していって,主なもので15個くらいあります。オーディオ付加に関連する関数について紹介していきます。

スライド切替え時のオーディオデータを付加する関数

add_slide_trans_audio

スライド切替えの際のナレーションです。1枚のスライドにつき1つのオーディオデータを埋め込むことになります。

def add_slide_trans_audio(f_name, wav_dir, pptx_dir, sld, anim_num):
    # f_name : File name of user defined sound(with extention,".wav").
    # wav_dir : Directory of user defned sound files. 
    # pptx_dir : Directory where pptx files are extracted.
    # sld : Slide information. (Instance variable of SlideInfo class)
    # anim_num : Numbers related to the animations in the slide. (AnimInfo class)

    # The function adds the slide transition audio to the slide.
    #   audio_str : Code for adding audio to the slide xml file.
    #   rels_str  : Code for the slide relation file.(ex. slide1.xml.rels)

    #   In the codes, several parameters are replaced with dummy strings;
    #    name of user defined audio file  => "dummy.wav",
    #    string of relation id (rId)  => "rId",
    #    name of the audio file to be stored in the ppt/media  => "audio.wav".
    
    audio_str = \
        r'<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.'\
        + r'org/markup-compatibility/2006">'\
        + r'<mc:Choice xmlns:p14="http://schemas.microsoft.com/office/'\
        + r'powerpoint/2010/main" Requires="p14">'\
        + r'<p:transition spd="slow" p14:dur="2000"><p:sndAc><p:stSnd>'\
        + r'<p:snd r:embed="rId" name="dummy.wav"/>'\
        + r'</p:stSnd></p:sndAc></p:transition></mc:Choice><mc:Fallback>'\
        + r'<p:transition spd="slow"><p:sndAc><p:stSnd>'\
        + r'<p:snd r:embed="rId" name="dummy.wav"/></p:stSnd></p:sndAc>'\
        + r'</p:transition></mc:Fallback></mc:AlternateContent>'\

    rels_str =  r'<Relationship Id="rId" Type='\
        + r'"http://schemas.openxmlformats.org/officeDocument/2006/'\
        + r'relationships/audio" Target="../media/audio.wav"/>'

    # Delete slide transition code if it remains.
    sld.xml = delete_transition_code(sld.xml) 

    # Find the max value of rId and get the rId string.
    rid_max = find_max_rid(sld.rels)
    rid_str = 'rId' + str(rid_max +1)

    # Replace the dummy codes in the audio_str to determined strings.
    audio_str = audio_str.replace('dummy.wav', f_name)
    audio_str = audio_str.replace('rId', rid_str)  

    # Determine the name of .wav file to be stored in ./ppt/media/.
    audio_file = 'audio' + str(anim_num.audio) + '.wav'

    # Replace the dummy codes in the rels_str to determined strings.
    rels_str = rels_str.replace('rId', rid_str)
    rels_str = rels_str.replace('audio.wav', audio_file)

    # Insert the xml code to xml code of the slide.
    sld.xml, err = insert_xml(sld.xml, '</p:clrMapOvr>', audio_str, 0, 1)

    # Insert the rels_str to xml.rels code of the slide.
    sld.rels, err = insert_xml(sld.rels, 'relationships">', rels_str, 0, 1)

    #  Write the slide information code to the silde.xml and slide.xml.rels.
    write_slide_info(sld, pptx_dir)

    # Copy the user defined audio file to ./ppt/media/
    audio_dir = pptx_dir + '/ppt/media'
    if not os.path.isdir(audio_dir):
        os.mkdir(audio_dir)
    audio_path = pptx_dir + '/ppt/media/' + audio_file
    wav_file_path = wav_dir + '/' + f_name
    shutil.copyfile(wav_file_path, audio_path)
    return
引数
def add_slide_trans_audio(f_name, wav_dir, pptx_dir, sld, anim_num):
  • f_name:ユーザが用意したwav形式のオーディオファイルの名称
    スライド番号,クリック番号,自動アニメーションの番号を文字列として含む。
  • wav_dir:オーディオファイルを格納しているディレクトリのパス
  • pptx_dir:zip展開したpptxファイルのディレクトリ名
    pptxファイルの名前から拡張子を除いた文字列になる。
  • sld:処理するスライドの情報をまとめたクラスSlideInfoのインスタンス
       sld.ind:スライド番号n
       sld.xml:ppt/slides/に格納されているsliden.xml内の文字列
       sld.rels:ppt/slides/_rels/に格納されているsliden.xml.rels内の文字列
  • anim_num :アニメーション番号をまとめたクラスAnimNumのインスタンス
       anim_num.sld:スライドの番号n(sld.indとかぶっています!)
       anim_num.clk:クリックの番号
       anim_num.aut:自動アニメーションの番号
       anim_num.audio:ppt/media/の中に格納するオーディオファイルの番号

クラスを使っていますが,構造体として使っているだけです。あと,Pythonではクラスの引数は参照渡しとなるというので,処理結果を呼び出し側に戻すことも考えてクラスインスタンスを採用しています。

ただし,インスタンスのメンバ変数(と呼ぶのかな?)が重複していたり使い方にはいろいろ問題があるみたいなので,後で変更していくことになるでしょう。

なお,これらのクラスは呼び出し側で次のように定義しています。

class PathInfo:
    def __init__(self):
        self.pptx = ''   # pptx file path.
        self.narr = ''   # Narration folder path.
        self.scpt = ''   # Script file path.
        self.idir = ''   # Initial directory.


class SlideInfo:
    def __init__(self):
        self.ind = 0     # Index of the current slide.
        self.xml = ''   # Content of .xml file.
        self.rels = ''  # Content of .xml.rels file.


class AnimNum:
    def __init__(self):
        self.sld = 0    # Slide number.
        self.clk = 0    # Click number.
        self.aut = 0    # Auto animation number.
        self.audio = 0  # Audio file index in ppt/media/
add_slide_trans_audioがやっていること

PPTファイルに音声を自動で埋め込む その2 Pythonによる実装の準備 で解説したスライド切替えオーディオデータの付加をするための関数です。zip展開したpptxディレクトリの下にある,./ppt/slides/sliden.xmlと./ppt/slides/_rels/sliden.xml.relsの内容を書き換え,./ppt/mediaの下にオーディオファイルをaudiom.wavという名前にして配置します。mはプレゼンテーション内で重ならないような通し番号になります。手順は次のようになります。

ⅰ)rIdの最大値を見つけ,その値に1を加えて新しいrIdを決める

ⅱ)sliden.xmlとliden.xml.relsに挿入する文字列を作る

ⅲ)sliden.xmlとliden.xml.relsに文字列を挿入する

ⅳ)ppt/media/の下に,オーディオファイルをコピーする

それでは,各手順をもう少し詳しくみていきます。

ⅰ)rIdの最大値を見つけ,その値に1を加えて新しいrIdを決める
 sliden.xmlまたはsliden.xml.relsの中を検索して”rIdk“(kは10進数の数字)となっている部分を見つけ,kの最大値を求める。(sliden.xmlとsliden.xml.relsのrIdは共通のはずなので,どちらを使ってもよい。)この値に1を加えて新しいrIdの文字列を決定する。

以下に,オーディオを付加していないシンプルなsliden.xml.relsの例を示します。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml" />
</Relationships>

この例では”rId1″となっている文字列が1つだけ見つかります。”rIdk“のkの最大値は1です。新しいrIdの値は既に使われているものと重ならないように選べばよいので,最大値1に1を加えて得られる”rId2″を新規のrIdとして使うことにします。

<もう少し詳しく読む>

編集を繰り返したpptファイルの場合,rIdの文字列は,”rId1″,”rId3″,”rId4″,・・・のように,1ずつ増加しているのではなく,途中が抜けている可能性があります(確認したわけでがありません)。しかし,その場合でも,最大値を見つけてそれに1を加えることで既存のrId文字列と重ならないものが作れます。それで,最大値に1を加えるというシンプルな方法を採用しました。

<閉じる>

rIdの最大値を見つける部分は,以下のコードになります。

    # Find the max value of rId and get the rId string.
    rid_max = find_max_rid(sld.rels)
    rid_str = 'rId' + str(rid_max +1)

find_max_ridは,自作の関数で,引数に文字列を入れて処理すると,その文字列の中のrId文字列の表している最大の値を整数で返します。

次のrid_str =・・・で,その値に1を加えて新しいrId文字列を作っています。

関数find_max_ridは次のようになっています。

def find_max_rid(str):
    # Find the max of rId number
    rid_num = 0
    rid_max = 0
    pt = 0
    while pt >= 0  :
        (pt, rid_num) = find_rid(str,pt)
        if rid_num >= rid_max :
            rid_max = rid_num
    return rid_max
find_max_rid(str)
  • str:文字列
    この中に含まれているrIdの最大値を求め,整数値で返す。

find_max関数の中では,やはり自作のfind_rid関数を呼んでいます。find_rid(str, pt)は,文字列strの先頭からpt番目の文字から探索を始め,最初に見つかったrId文字列の数値を読み取ります。関数の戻り値は,見つけたrId文字列の直後の位置pt_stopと,読み取った数値rid_numです。rId文字列が存在しない場合は,pt_stopの値は-1にして返します。

これを利用して,find_max_ridではrId文字列の数値を読み取っていき,ptの値が負になるまで繰り返しています。

def find_rid(str,pt):
    pt_start = find_slice_position('"rId',1, pt, str) 
    pt_stop  = find_slice_position('"',-1,pt_start,str)
    if pt_start == -1 or pt_stop == -1 :
        return (-1,0)
    else :
        rid_num = int(str[pt_start:pt_stop])
        return (pt_stop,rid_num)
find_rid(str, pt)
  • str:rId文字列を含んでいる文字列
  • pt:探索を開始する位置(文字列の先頭を0とする)

さらに,find_ridの中でfind_slice_positionを呼んでいます。

def find_slice_position(target_str,d, pt, str) :
    # Finds the target-string and returns sice position.
    # d = 1: after the target,d =-1 : befor the target
    tag_str_len = len(target_str)
    pt_temp = str.find(target_str,pt)
    if pt_temp ==-1 :
        return -1
    else :
        if d == 1 :
          slice_pt = pt_temp + tag_str_len
        else :
          slice_pt= pt_temp
        return slice_pt
find_slice_position(target_str,d, pt, str) 
  • target_str:目標とする文字列
  • d:d>0の場合目標とする文字列の直後,d<0の場合は直前の位置を返す
  • 探索を開始する位置(文字列の先頭を0とする)
  • str:探索の対象とする文字列
  • 戻り値は位置を示す整数値。target_strが見つからないときは,-1を返す。

ⅱ)sliden.xmlとliden.xml.relsに挿入する文字列を作る
 これらの文字列は,関数のコードの中に埋め込まれています。連続した文字列なので,見やすくなるようにところどころにバックスラッシュを入れてあります。

    audio_str = \
        r'<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.'\
        + r'org/markup-compatibility/2006">'\
        + r'<mc:Choice xmlns:p14="http://schemas.microsoft.com/office/'\
        + r'powerpoint/2010/main" Requires="p14">'\
        + r'<p:transition spd="slow" p14:dur="2000"><p:sndAc><p:stSnd>'\
        + r'<p:snd r:embed="rId" name="dummy.wav"/>'\
        + r'</p:stSnd></p:sndAc></p:transition></mc:Choice><mc:Fallback>'\
        + r'<p:transition spd="slow"><p:sndAc><p:stSnd>'\
        + r'<p:snd r:embed="rId" name="dummy.wav"/></p:stSnd></p:sndAc>'\
        + r'</p:transition></mc:Fallback></mc:AlternateContent>'\

    rels_str =  r'<Relationship Id="rId" Type='\
        + r'"http://schemas.openxmlformats.org/officeDocument/2006/'\
        + r'relationships/audio" Target="../media/audio.wav"/>'
  • audio_str
    スライド切替えのオーディオを付加するためのsliden.xmlに追加する文字列。Pythonコードの中では,書き換える部分(赤い文字で表記)を,次のように仮の文字列にしてある。
      rId文字列:数字部分を抜いた”rId”,
      ユーザが用意したオーディオファイルの名前:”dummy.wav”
  • rels_str
    xmlコードで指示したオーディオデータが実際に埋め込まれたppt/media/下にあるwav形式ファイルのどれに対応するかの関係を表す文字列。書き換えのための仮の文字列は,
      rId文字列:”rId”,audio_strと同じに揃える
      ppt/media/下に書き込むwav形式ファイルの名前:”audio.wav”

文字列の置き換えは,標準の文字列関数で実行しています。

    # Replace the dummy codes in the audio_str to determined strings.
    audio_str = audio_str.replace('dummy.wav', f_name)
    audio_str = audio_str.replace('rId', rid_str)  

文字列audio_strの中のダミーの文字列”dummy.wav”を引数で与えたファイル名のf_nameで置き換えます。次に,”rId”もrIdの最大値+1で計算した値を使ったrId文字列で置き換えます。

以上で,sliden.xmlとsliden.xml.relsに挿入する文字列の準備ができました。次にこの文字列を挿入します。

    # Insert the xml code to xml code of the slide.
    sld.xml = insert_xml(sld.xml, '&lt;/p:clrMapOvr>', audio_str, 0, 1)

    # Insert the rels_str to xml.rels code of the slide.
    sld.rels = insert_xml(sld.rels, 'relationships">', rels_str, 0, 1)

ここで,自作の関数insert_xmlを使っています。

def insert_xml(input_str, target_str, insert_str, start_pt, d):
    # If input_str already exists, do nothing and return.
    slice_pt = find_slice_position(insert_str,1, start_pt, input_str)
    if slice_pt >=0 : 
        return input_str, -1
    # Find the slice position before (d&lt;0) or after (d>0) the tareget_str.
    else : 
        slice_pt = find_slice_position(target_str,d, start_pt, input_str)
    if slice_pt == -1 :
        return input_str, -1
    else :
        input_str_back = input_str[slice_pt : ]
        input_str = input_str[0 : slice_pt] + insert_str + input_str_back
        return input_str, 1

文字列のスライスを使った処理になります。中で自作のfind_slice_positionを使っています。これは文字列オブジェクトのstr.find関数でも問題なくできます。無理に関数にしなくてもよかったのかもしれませんが,やっていることが理解しやすくはなります。

insert_xml(input_str,  target_str,  insert_str,  start_pt,  d)
  • input_str:挿入する対象となる文字列
  • target_str:目印(目標)にする文字列
  • insert_str:
    文字列target_strを見つけたら,その直後または直前にinsert_strを挿入する。
  • d: d>0なら直後,d<0なら直前に挿入
    d<0の場合の処理は,str.find関数だけでよいのですが,同じ関数の形で使えるようにしておきました。でも,target_strの直前に挿入する用途はまだありません。

挿入したら,文字列をファイルsliden.xmlとsliden.xml.relsに書き込みます。

    #  Write the slide information code to the silde.xml and slide.xml.rels.
    write_slide_info(sld, pptx_dir)

2つのファイルに書き込むための関数を作りました。

def write_slide_info(slide, pptx_dir):
    typ = type(slide)
    i_sld = slide.ind
    slide_file = pptx_dir + '/ppt/slides/slide'+ str(i_sld) + '.xml'
    rels_file = pptx_dir + '/ppt/slides/_rels/slide'+ str(i_sld) + '.xml.rels'
    
    write_file(slide_file, slide.xml)
    write_file(rels_file, slide.rels)
write_slide_info(slide, pptx_dir)
  • slide:クラスSlideInfoのインスタンス
  • pptx_dir:pptxファイルをzip展開したトップのディレクトリ

この関数の中では自作のwrite_fileを使っています。中身の説明は不要だと思います。

def write_file(f_name,input):
    f = open(f_name, 'w', encoding = 'UTF-8')
    f.write(input)
    f.close()
    return 1

ⅳ)ppt/media/の下にオーディオファイルをコピーする

    # Copy the user defined audio file to ./ppt/media/
    audio_dir = pptx_dir + '/ppt/media'
    if not os.path.isdir(audio_dir):
        os.mkdir(audio_dir)
    audio_path = pptx_dir + '/ppt/media/' + audio_file
    wav_file_path = wav_dir + '/' + f_name
    shutil.copyfile(wav_file_path, audio_path)
    return

shutil(シューティル,Shell Utilitiesの略だそうです)モジュールの関数,shutil.copyfileを使っています。

クリックで起動されるアニメーションにオーディオを付加する関数

add_click_actv_audioは処理の内容は,スライド切替え用の関数add_slide_trans_audioと,ほとんど同じです。異なっているのは,コードを挿入する位置が複数(1つ以上)ある,sliden.xmlに書き込む文字列が異なる,クリックのIdを読み取って書き込む必要がある,などの点です。

また,この関数add_click_actv_audioでは,指定された順番を持つクリック記述の位置1か所だけにコードを埋め込むようにしてあります。

def add_click_actv_audio(f_name, wav_dir, pptx_dir, sld, anim_num):
    # f_name : File name of user defined sound(with extention,".wav").
    # wav_dir : Directory of user defned sound files. 
    # pptx_dir : Directory where pptx files are extracted.
    # sld : Slide information. (Instance variable of SlideInfo class)
    # anim_num : Numbers related to the animations in the slide. (AnimInfo class)

    # The function adds the click-activated audio to the slide.
    #    audio_str : xml strings describing audio data 
    #    rels_str  : Code for the slide relation file.(ex. slide1.xml.rels)    
    #    name of user defined audio file  => "dummy.wav",
    #    string of relation id (rId)  => "rId",
    #    name of the audio file to be stored in the ppt/media  => "audio.wav".
    audio_str = \
        r'&lt;p:subTnLst>&lt;p:audio>&lt;p:cMediaNode>'\
        + r'&lt;p:cTn display="0" masterRel="sameClick">'\
        + r'&lt;p:stCondLst>&lt;p:cond evt="begin" delay="0">'\
        + r'&lt;p:tn val="dummyNum"/>&lt;/p:cond>&lt;/p:stCondLst>&lt;p:endCondLst>'\
        + r'&lt;p:cond evt="onStopAudio" delay="0">&lt;p:tgtEl>'\
        + r'&lt;p:sldTgt/>&lt;/p:tgtEl>&lt;/p:cond>&lt;/p:endCondLst>&lt;/p:cTn>'\
        + r'&lt;p:tgtEl>&lt;p:sndTgt r:embed="rId" name="dummy.wav"/>'\
        + r'&lt;/p:tgtEl>&lt;/p:cMediaNode>&lt;/p:audio>&lt;/p:subTnLst>'
    rels_str =  r'&lt;Relationship Id="rId" Type='\
        + r'"http://schemas.openxmlformats.org/officeDocument/2006/'\
        + r'relationships/audio" Target="../media/audio.wav"/>'
    
    # Find the max value of rId
    rid_max = find_max_rid(sld.rels)
    rid_str = 'rId' + str(rid_max +1)
    
    (clk_id, pt) = find_nth_click(sld.xml, anim_num.clk)

    audio_str = audio_str.replace('dummy.wav', f_name)
    audio_str = audio_str.replace('rId', rid_str)  
    audio_str = audio_str.replace('dummyNum', clk_id) 

    audio_file = 'audio' + str(anim_num.audio) + '.wav'

    rels_str = rels_str.replace('rId', rid_str)
    rels_str = rels_str.replace('audio.wav', audio_file)

    sld.xml, err = insert_xml(sld.xml, '&lt;/p:childTnLst>', audio_str, pt, 1)
    sld.rels,err = insert_xml(sld.rels, 'relationships">', rels_str, 0, 1)
    write_slide_info(sld, pptx_dir)

    audio_dir = pptx_dir + '/ppt/media'
    if not os.path.isdir(audio_dir):
        os.mkdir(audio_dir)
    audio_path = pptx_dir + '/ppt/media/' + audio_file
    wav_file_path = wav_dir + '/' + f_name
    shutil.copyfile(wav_file_path, audio_path)
    return

関数の引数,処理の流れ,使っている関数などはadd_slide_trans_audioとほぼ同じなので,異なるところだけ説明します。

挿入するコード

sliden.xmlに挿入する文字列と挿入する位置が異なります。文字列は,以下の代入文で定義しています。

 audio_str = \
        r'<p:subTnLst><p:audio><p:cMediaNode>'\
        + r'<p:cTn display="0" masterRel="sameClick">'\
        + r'<p:stCondLst><p:cond evt="begin" delay="0">'\
        + r'<p:tn val="dummyNum"/></p:cond></p:stCondLst><p:endCondLst>'\
        + r'<p:cond evt="onStopAudio" delay="0"><p:tgtEl>'\
        + r'<p:sldTgt/></p:tgtEl></p:cond></p:endCondLst></p:cTn>'\
        + r'<p:tgtEl><p:sndTgt r:embed="rId" name="dummy.wav"/>'\
        + r'</p:tgtEl></p:cMediaNode></p:audio></p:subTnLst>'

見づらいので,整形したものを示します。赤い文字で強調している部分が,ダミーの文字列で,sliden.xmlとsliden.xml.relsから読み取って作った文字列と置き換えてから,ファイルに書き込みます。

        <p:subTnLst>
           <p:audio>
             <p:cMediaNode>
               <p:cTn display="0" masterRel="sameClick">
                 <p:stCondLst>
                   <p:cond evt="begin" delay="0">
                     <p:tn val="dummyNum" />
                   </p:cond>
                 </p:stCondLst>
                 <p:endCondLst>
                   <p:cond evt="onStopAudio" delay="0">
                     <p:tgtEl>
                       <p:sldTgt />
                     </p:tgtEl>
                   </p:cond>
                 </p:endCondLst>
               </p:cTn>
               <p:tgtEl>
                 <p:sndTgt r:embed="rId" name="dummy.wav" />
               </p:tgtEl>
             </p:cMediaNode>
           </p:audio>
         </p:subTnLst>

rId文字列とユーザが用意したwav形式ファイルの名前dummy.wavに関しては,スライド切替えオーディオ用の関数add_slide_trans_audioと同じ処理になります。文字列audio_strの中にあるdummyNumは,クリックに関係するIdで,これは対応するクリックに関する記述から読み取る必要があります。

オーディオを付加していないときのクリックに関する記述の例は次のようになっています。

   <p:childTnLst>
     <p:par>
       <p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect">
         <p:stCondLst>
           <p:cond delay="0" />
         </p:stCondLst>
         <p:childTnLst>
           <p:set>
             <p:cBhvr>
               <p:cTn id="6" dur="1" fill="hold">
                 <p:stCondLst>
                   <p:cond delay="0" />
                 </p:stCondLst>
               </p:cTn>
               <p:tgtEl>
                 <p:spTgt spid="4" />
               </p:tgtEl>
               <p:attrNameLst>
                 <p:attrName>style.visibility</p:attrName>
               </p:attrNameLst>
             </p:cBhvr>
             <p:to>
               <p:strVal val="visible" />
             </p:to>
           </p:set>
         </p:childTnLst>
       </p:cTn>
     </p:par>
   </p:childTnLst>

“clickEffect”という文字列を見つけ,遡って探索して最初に見つけた”id=”の次にクリックのIdがあります。この例では”5″となっています。

そして,”clickEffect”の後ろに探索していって,初めに見つけた</p:childTnLst>の直後にダミーの文字列を書き換えたaudio_strを挿入します。

異なっているのは,以下の文があることです。

    (clk_id, pt) = find_nth_click(sld.xml, anim_num.audio)

ここで使っている関数find_nth_click(n番目のクリックを見つける関数)が,指定された番号のクリックの記述を見つけ,クリックのId番号を取得して返す関数です。

def find_nth_click(sld_str, n_clk):
    # Find the n-th click-effect description in the slide.xml string.
    # Return the click ID number and the 'clicEffect">' position.
    target_str = 'clickEffect">'
    pt = 0
    i_clk = 0
    strlen = len(target_str)
    while i_clk &lt;= n_clk-1 :
        pt = sld_str.find(target_str, pt) 
        if pt >=0:
            i_clk += 1
            pt = pt + strlen
        else:
            break
    pt_start = find_slice_position('id="',1, pt-120, sld_str) 
    pt_stop = find_slice_position('"',-1, pt_start, sld_str)
    id = sld_str[pt_start : pt_stop]
    return id, pt
find_nth_click(sld_str, n_clk)
  • sld_str:sliden.xmlの内容の文字列
  • n_clk:クリックの番号
  • 関数の戻り値は,クリックのIdを示す文字列と,n番目のクリック記述の中のclicEffect”>という文字列の直後のスライス位置です。

この関数の中身の説明は不要かと思いますが,念のため書き残しておきます。

  • n_clk番目のtarget_str = “<“clickEffect”>を見つける
    pt = 0 
    i_clk = 0
    strlen = len(target_str)
    while i_clk <= n_clk-1 :  
    pt = sld_str.find(target_str, pt)
    if pt >=0:
    i_clk += 1
    pt = pt + strlen
    else:
    break
     whileループを抜けると,ptはn_clk番目の<“clickEffect”>の直後のスライス位置になっています。
  • 遡って探索して初めのId=”を見つける部分
    pt_start = find_slice_position(‘id=”‘,1, pt-120, sld_str)
     n_clk番目の<“clickEffect”>の120文字前から探索を開始し,見つけたid=”の直後をpt_startとする。
    pt_stop = find_slice_position(‘”‘,-1, pt_start, sld_str)
     pt_startの後ろの”を見つけ,その直前をpt_stopとする。
    id = sld_str[pt_start : pt_stop]
     これで数値の文字列が取得できる。

説明が長く,わかりにくくなってきました。

商売柄,「読んだ人が同じ程度のシステムを再構築できる」ような解説を工夫しなければならないのでです。こういうサイトをのぞいてみようとする方であれば,「ヤじるし」よりはコーディングスキルがあるでしょうから,それに頼って雑な書き方をしてしまいました。暇になったら,書き直していくつもりです。

今回の投稿で解説した関数を使って,PPTファイルのスライド切替えとアニメーションにオーディオデータを追加するコードを作成しました。少し複雑でスライド枚数の多いプレゼンテーションに使って,コードを改良していく予定です。

その4 GUIを持つアプリ に続く。

コメント