"""
概要: PowerPointファイルからテキスト、数式、図を抽出し、Markdown形式で出力するスクリプトです。
詳細説明:
このスクリプトは、`python-pptx`と`lxml`ライブラリを使用して、PPTXファイルの内部構造を解析します。
各スライドの内容を、Markdown形式の出力ファイルに変換します。
具体的には、以下の要素を抽出してMarkdownに変換します。
- スライドのタイトル
- テキストボックス内のテキスト(箇条書きのレベルと改行を保持)
- Office Math Markup Language (OMML) で記述された数式(LaTeX形式に変換)
- スライドに埋め込まれた画像(指定されたディレクトリに保存し、Markdownからリンク)
変換されたMarkdownファイルは、プレゼンテーションのコンテンツを再利用しやすい形で提供します。
関連リンク:
:doc:`pptx2md2_usage`
"""
import argparse
import os
import re
from pptx import Presentation
from lxml import etree
# 名前空間の定義
NAMESPACES = {
'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
'm': 'http://schemas.openxmlformats.org/officeDocument/2006/math',
'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
'a14': 'http://schemas.microsoft.com/office/drawing/2010/main'
}
# Unicode数式文字とLaTeXコマンドのマッピング
MATH_UNICODE_MAP = {
'𝑷': 'P', '𝑽': 'V', '𝑹': 'R', '𝑻': 'T',
'𝒂': 'a', '𝒃': 'b', '𝒄': 'c', '𝒅': '\\mathrm{d}', '𝒆': 'e', '𝒇': 'f', '𝒈': 'g',
'𝒉': 'h', '𝒊': 'i', '𝒋': 'j', '𝒌': 'k', '𝒍': 'l', '𝒎': 'm', '𝒏': 'n',
'𝒙': 'x', '𝒚': 'y', '𝒛': 'z',
'𝟐': '2', '𝟏': '1', '𝟎': '0',
'−': '-', '+': '+', '÷': '/', '×': '*', '⋅': '\\cdot',
'…': '...', '∞': '\\infty',
'∑': '\\sum', '∏': '\\prod',
'∫': '\\int', '∬': '\\iint', '∭': '\\iiint', '∮': '\\oint',
'α': '\\alpha', 'β': '\\beta', 'γ': '\\gamma', 'δ': '\\delta', 'ε': '\\epsilon',
'ζ': '\\zeta', 'η': '\\eta', 'θ': '\\theta', 'ι': '\\iota', 'κ': '\\kappa',
'λ': '\\lambda', 'μ': '\\mu', 'ν': '\\nu', 'ξ': '\\xi', 'π': '\\pi', 'ρ': '\\rho',
'σ': '\\sigma', 'τ': '\\tau', 'υ': '\\upsilon', 'φ': '\\phi', 'χ': '\\chi',
'ψ': '\\psi', 'ω': '\\omega',
'Γ': '\\Gamma', 'Δ': '\\Delta', 'Θ': '\\Theta', 'Λ': '\\Lambda',
'Ξ': '\\Xi', 'Π': '\\Pi', 'Σ': '\\Sigma', 'Φ': '\\Phi', 'Ψ': '\\Psi', 'Ω': '\\Omega',
'′': "'", '°': '\\degree', '℃': '\\degree C'
}
# 多項演算子とLaTeXコマンドのマッピング
NARY_TO_LATEX = {
'∑': '\\sum', '∏': '\\prod',
'∫': '\\int', '∬': '\\iint', '∭': '\\iiint', '∮': '\\oint',
'⋀': '\\bigwedge', '⋁': '\\bigvee', '⋂': '\\bigcap', '⋃': '\\bigcup',
}
OPERATOR_CHARS = set(['∑', '∏', '∫', '∬', '∭', '∮'])
pause = 0
[ドキュメント]
def terminate():
"""
概要: プログラムを終了します。
詳細説明:
`pause` グローバル変数が1に設定されている場合、ユーザーがEnterキーを押すまで待機してからプログラムを終了します。
これにより、コマンドプロンプトのウィンドウがすぐに閉じられるのを防ぎます。
:returns: None
"""
if pause:
input("\nPress ENTER to terminate\n")
exit()
[ドキュメント]
def initialize():
"""
概要: コマンドライン引数を解析し、設定を初期化します。
詳細説明:
`argparse` モジュールを使用して、以下のコマンドライン引数を定義および解析します。
- `-i` または `--input`: 入力するPowerPointファイル名 (必須)
- `-o` または `--output`: 出力するMarkdownファイル名 (必須)
- `--xml`: 数式の元のOMML XMLを出力するかどうかを示すフラグ
- `--imagedir`: 画像を保存するディレクトリ名 (デフォルトは"images")
- `--pause`: 終了時に待機するかどうかを示すフラグ (デフォルトは0)
解析された引数は `argparse.Namespace` オブジェクトとして返されます。
:returns: argparse.Namespace
解析されたコマンドライン引数を含むオブジェクト。
"""
parser = argparse.ArgumentParser(description="PowerPointファイルからテキスト、数式、図を抽出し、Markdownに出力します。")
parser.add_argument("-i", "--input", required=True, help="入力するPowerPointファイル名")
parser.add_argument("-o", "--output", required=True, help="出力するMarkdownファイル名")
parser.add_argument("--xml", action="store_true", help="数式の元のOMML XMLを出力します。")
parser.add_argument("--imagedir", default="images", help="画像ディレクトリ")
parser.add_argument("--pause", type=int, default=0, help="終了時待機")
args = parser.parse_args()
return args
[ドキュメント]
def get_slide_title(slide):
"""
概要: スライドからタイトルを抽出します。
詳細説明:
指定されたスライドオブジェクトにタイトルプレースホルダーが存在する場合、そのテキストコンテンツを返します。
タイトルプレースホルダーが見つからない場合は、「無題のスライド」というデフォルト文字列を返します。
抽出されたテキストは前後の空白が取り除かれます。
:param slide: pptx.slide.Slide
タイトルを抽出する対象のスライドオブジェクト。
:returns: str
スライドのタイトル文字列。
"""
if slide.shapes.title:
return slide.shapes.title.text.strip()
return "無題のスライド"
def _safe_text_replace_math_unicode(text: str) -> str:
"""
概要: テキスト内のUnicode数式文字を対応するLaTeXコマンドに置換します。
詳細説明:
`MATH_UNICODE_MAP` に定義されているUnicode文字(例: 数学記号、特殊なアルファベットなど)を、
対応するLaTeXコマンドの文字列に置換します。
入力テキストが空の場合は、空文字列をそのまま返します。
:param text: str
置換処理を行う入力テキスト。
:returns: str
Unicode数式文字がLaTeXコマンドに置換されたテキスト。
"""
if not text: return ""
for u, ltx in MATH_UNICODE_MAP.items():
text = text.replace(u, ltx)
return text
def _find_first(element, candidates):
"""
概要: 指定された要素の子孫から、候補タグのいずれかに最初に一致する要素を見つけます。
詳細説明:
`candidates` リスト内のタグ名を順番に試行し、`element` の直下の子要素の中から、
最初に見つかったタグ名の要素を返します。いずれの候補も見つからなかった場合はNoneを返します。
:param element: lxml.etree._Element
検索を開始する親要素。
:param candidates: list[str]
検索するタグ名のリスト。
:returns: lxml.etree._Element or None
最初に見つかった子要素、または見つからなかった場合はNone。
"""
for cand in candidates:
found = element.find(cand, NAMESPACES)
if found is not None: return found
return None
def _detect_nary_op_char(element):
"""
概要: Nary(多項演算子)要素から演算子文字を検出します。
詳細説明:
OMMLのNary要素から、総和記号などの演算子文字を特定します。
まず `m:naryPr/m:chr` タグの `val` 属性を探します。
見つからない場合は、要素内の `m:t` タグ(下付きや上付きの子孫ではないもの)を走査し、
`OPERATOR_CHARS` に含まれる文字を検出します。
:param element: lxml.etree._Element
Nary要素。
:returns: str
検出された演算子文字、または見つからなかった場合は空文字列。
"""
op_tag = element.find('m:naryPr/m:chr', NAMESPACES)
if op_tag is not None:
val = op_tag.get(f"{{{NAMESPACES['m']}}}val", "")
if val: return val
ts = element.xpath('.//m:t[not(ancestor::m:e) and not(ancestor::m:sub) and not(ancestor::m:sup)]', namespaces=NAMESPACES)
for t in ts:
s = t.text or ""
for ch in s:
if ch in OPERATOR_CHARS: return ch
return ""
[ドキュメント]
def omml_to_latex(element):
"""
概要: Office Math Markup Language (OMML) 要素をLaTeX形式に変換します。
詳細説明:
OMMLのXML要素を再帰的に走査し、対応するLaTeX表現を生成します。
以下のような主要なOMML構造を扱います。
- `m:f`: 分数 (`\\frac`)
- `m:rad`: 根号 (`\\sqrt`)
- `m:sSup`: 上付き文字 (`^`)
- `m:sSub`: 下付き文字 (`_`)
- `m:sSubSup`: 下付き上付き文字 (`_`, `^`)
- `m:d`: 区切り文字 (括弧など)
- `m:r`: テキストのまとまり
- `m:t`: テキストコンテンツ (`_safe_text_replace_math_unicode`で処理)
- `m:limLow`, `m:limUpp`: 限界式の下限、上限
- `m:int`, `m:nary`: 積分、多項演算子 (総和、総乗など)
未知のタグや処理されないタグについては、その子要素を再帰的に処理して結合します。
:param element: lxml.etree._Element
変換するOMML要素。
:returns: str
変換されたLaTeX文字列。
"""
tag = etree.QName(element).localname
if tag in ('oMath', 'oMathPara'):
return "".join(omml_to_latex(child) for child in element)
elif tag == 'f':
num = element.find('m:num', NAMESPACES); den = element.find('m:den', NAMESPACES)
return f"\\frac{{{omml_to_latex(num)}}}{{{omml_to_latex(den)}}}"
elif tag == 'rad':
deg = element.find('m:deg', NAMESPACES); e = element.find('m:e', NAMESPACES)
if deg is not None: return f"\\sqrt[{omml_to_latex(deg)}]{{{omml_to_latex(e)}}}"
return f"\\sqrt{{{omml_to_latex(e)}}}"
elif tag == 'sSup':
e = element.find('m:e', NAMESPACES); sup = element.find('m:sup', NAMESPACES)
return f"{omml_to_latex(e)}^{{{omml_to_latex(sup)}}}"
elif tag == 'sSub':
e = element.find('m:e', NAMESPACES); sub = element.find('m:sub', NAMESPACES)
return f"{omml_to_latex(e)}_{{{omml_to_latex(sub)}}}"
elif tag == 'sSubSup':
e = element.find('m:e', NAMESPACES); sub = element.find('m:sub', NAMESPACES); sup = element.find('m:sup', NAMESPACES)
return f"{omml_to_latex(e)}_{{{omml_to_latex(sub)}}}^{{{omml_to_latex(sup)}}}"
elif tag == 'd':
beg_chr = element.find('m:dPr/m:begChr', NAMESPACES); end_chr = element.find('m:dPr/m:endChr', NAMESPACES)
beg = beg_chr.get(f"{{{NAMESPACES['m']}}}val") if beg_chr is not None else "("
end = end_chr.get(f"{{{NAMESPACES['m']}}}val") if end_chr is not None else ")"
content = "".join(omml_to_latex(child) for child in element if etree.QName(child).localname != 'dPr')
return f"{beg}{content}{end}"
elif tag == 'r': return "".join(omml_to_latex(child) for child in element)
elif tag == 't': return _safe_text_replace_math_unicode(element.text or "")
elif tag == 'limLow':
base = omml_to_latex(element.find('m:e', NAMESPACES)); low = omml_to_latex(element.find('m:lim', NAMESPACES))
return f"{base}_{{{low}}}"
elif tag == 'limUpp':
base = omml_to_latex(element.find('m:e', NAMESPACES)); upp = omml_to_latex(element.find('m:lim', NAMESPACES))
return f"{base}^{{{upp}}}"
elif tag == 'int':
lower = _find_first(element, ['m:sub', 'm:low']); upper = _find_first(element, ['m:sup', 'm:up']); e = element.find('m:e', NAMESPACES)
l_ltx = omml_to_latex(lower) if lower is not None else ""; u_ltx = omml_to_latex(upper) if upper is not None else ""; e_ltx = omml_to_latex(e) if e is not None else ""
return f"\\int_{{{l_ltx}}}^{{{u_ltx}}}{e_ltx}" if (l_ltx or u_ltx) else f"\\int {e_ltx}"
elif tag == 'nary':
op_char = _detect_nary_op_char(element); lower_tag = _find_first(element, ['m:sub', 'm:low']); upper_tag = _find_first(element, ['m:sup', 'm:up']); content_tag = element.find('m:e', NAMESPACES)
l_ltx = omml_to_latex(lower_tag) if lower_tag is not None else ''; u_ltx = omml_to_latex(upper_tag) if upper_tag is not None else ''; c_ltx = omml_to_latex(content_tag) if content_tag is not None else ''
op_ltx = NARY_TO_LATEX.get(op_char, _safe_text_replace_math_unicode(op_char))
if lower_tag is not None or upper_tag is not None: return f"{op_ltx}_{{{l_ltx}}}^{{{u_ltx}}}{c_ltx}"
return f"{op_ltx} {c_ltx}".rstrip()
return "".join(omml_to_latex(child) for child in element)
[ドキュメント]
def split_latex_blocks(s: str):
"""
概要: LaTeX文字列を改行コード `\\\\` で分割し、個別のブロックのリストを生成します。
詳細説明:
入力されたLaTeX文字列を正規表現 `r'\\\\'` (すなわち `\\` という文字列) で分割します。
分割された各パーツは前後の空白が取り除かれ、空文字列ではないものだけがリストに含まれます。
入力文字列が空の場合や、分割後に有効なパーツがない場合は空のリストを返します。
:param s: str
分割するLaTeX文字列。
:returns: list[str]
分割されたLaTeXブロックのリスト。
"""
if not s: return []
parts = [p.strip() for p in re.split(r'\\\\', s) if p.strip()]
return parts if parts else [s.strip()]
[ドキュメント]
def main():
"""
概要: スクリプトの主要な実行フローを制御します。
詳細説明:
コマンドライン引数を初期化し、入力PowerPointファイルの存在を確認します。
ファイルが存在しない場合はエラーメッセージを表示して終了します。
その後、`extract_content_to_markdown` 関数を呼び出して、PowerPointからMarkdownへの変換処理を実行します。
終了時に`pause`フラグが設定されている場合は、ユーザーの入力を待ちます。
:returns: None
"""
args = initialize()
global pause
pause = args.pause
if not os.path.exists(args.input):
print("入力ファイルが見つかりません"); return
extract_content_to_markdown(args.input, args.output, args.imagedir, include_xml=args.xml)
if __name__ == "__main__":
main()
terminate()