pptx2md:PowerPoint から Markdown への変換ツール

【概要】

このプログラムは、Microsoft PowerPointのプレゼンテーションファイル(.pptx)をマークダウン形式のテキストファイル(.md)に変換するツールである。Tkinterを使用したグラフィカルユーザーインターフェース(GUI)で選択したPowerPointファイルを解析し、その内容をマークダウン形式で出力する。

【目次】

【関連する外部ページ】

python-pptx 公式ドキュメント

【サイト内の関連情報】

ppt2txt:PowerPoint からテキストを抽出するツール

第1章 仕様書

## 1. 概要
このプログラムは、Microsoft PowerPointのプレゼンテーションファイル(.pptx)をマークダウン形式のテキストファイル(.md)に変換するツールである。選択したPowerPointファイルを解析し、その内容をマークダウン形式で出力する。

## 2. 主な機能
### 1. PowerPointファイルの選択
   * Tkinterを使用したグラフィカルユーザーインターフェース(GUI)を通じて、ファイルを選択する。
   * 変換対象は.pptxファイルのみである。

### 2. スライドの変換
   * 各スライドを個別のセクションとして処理する。
   * マークダウンでは、各スライドの番号はレベル1の見出しとして表示される(例:`# Slide 1`、`# Slide 2`、`# Slide 3`)。

### 3. スライドタイトルの処理
   * マークダウンでは、スライドタイトルはレベル2の見出しとして表示される(例:`## スライドタイトル`)。

### 4. テキストボックスの処理
   * テキストボックス内のテキストが箇条書き、番号付きリスト、レベル3の見出し、またはその他の形式であるかを判断する。
   * 箇条書きは、マークダウンでハイフン(`-`)と字下げを使用して表す。
     - 例:
       ```
       - りんご
       - バナナ
       ```
   * ネストされた箇条書きは、さらに字下げを追加する。
     - 例:
       ```
       - 果物
         - りんご
         - バナナ
       ```
   * 番号付きリストは、マークダウンで数字と字下げを使用して表す。
     - 例:
       ```
       1. 果物
          1. りんご
          2. バナナ
       ```
   * 単純なテキストや見出しとして識別されないテキストボックスの内容は、通常の段落として出力する。

### 5. 強調とリンクの処理
   * テキストボックス内の太字やアンダーラインは、マークダウンの強調表現として二重アスタリスク(`**`)を使用する。
   * URLを検出し、マークダウンのリンク形式に変換する(例:`[リンクテキスト](URL)`)。

### 6. 出力
   * 変換したマークダウン内容は新しいファイルとして保存される。
   * 元のファイル名を基に、拡張子を.mdに変更して保存する。

## 3. 使用方法
1. プログラムを実行する。
2. ファイル選択ダイアログが表示され、変換したいPowerPointファイル(.pptx)を選択する。
3. 選択が完了すると、変換処理が開始される。
4. 変換が完了すると、元のファイルと同じディレクトリに、同じ名前で拡張子が.mdのファイルが作成される。
5. 処理の結果(成功または失敗)がコンソールに表示される。

## 4. 出力形式
* 各スライドは`# Slide X`(Xはスライド番号)で始まる。
* スライドのタイトルは`## タイトル内容`として記録される。
* テキストボックスの内容は、通常のテキスト、箇条書き、番号付きリスト、または見出しとして変換される。

## 5. エラー処理
* ファイルが選択されなかった場合、プログラムは終了する。
* 指定されたファイルが存在しない場合、エラーメッセージを表示する。
* 変換処理中にエラーが発生した場合、エラーの内容をコンソールに表示する。

## 6. 注意事項
* 複雑なレイアウト、アニメーション、その他の高度な機能は保持されない。

このプログラムを使用することで、PowerPointプレゼンテーションの内容をマークダウン形式に変換し、テキストベースの文書として再利用できる。

第2章 プログラム

本プログラムの動作環境は Python 3.12 である。実行前に、次のコマンドで python-pptx をインストールする。

pip install -U --no-user python-pptx
import os
import re
import sys
import tkinter as tk
from tkinter import filedialog

from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
from pptx.enum.text import PP_ALIGN


URL_PATTERN = re.compile(r'(https?://[^\s\)\]]+)')


def format_run_text(run):
    text = run.text
    if not text:
        return ""
    if run.font.bold or run.font.underline:
        text = f"**{text}**"
    text = URL_PATTERN.sub(lambda m: f"[{m.group(1)}]({m.group(1)})", text)
    return text


def paragraph_to_text(paragraph):
    return "".join(format_run_text(run) for run in paragraph.runs)


def is_heading_level3(paragraph):
    return paragraph.level == 0 and paragraph.alignment == PP_ALIGN.CENTER


def is_numbered_list(paragraph):
    pPr = paragraph._p.find(
        '{http://schemas.openxmlformats.org/drawingml/2006/main}pPr'
    )
    if pPr is None:
        return False
    return pPr.find(
        '{http://schemas.openxmlformats.org/drawingml/2006/main}buAutoNum'
    ) is not None


def is_bullet_list(paragraph, shape):
    pPr = paragraph._p.find(
        '{http://schemas.openxmlformats.org/drawingml/2006/main}pPr'
    )
    if pPr is not None and pPr.find(
        '{http://schemas.openxmlformats.org/drawingml/2006/main}buChar'
    ) is not None:
        return True
    if paragraph.level > 0:
        return True
    if shape.is_placeholder:
        placeholder_type = shape.placeholder_format.type
        if placeholder_type in (PP_PLACEHOLDER.BODY, PP_PLACEHOLDER.OBJECT):
            return True
    return False


def convert_text_frame(text_frame, shape):
    lines = []
    numbered_counters = {}
    for paragraph in text_frame.paragraphs:
        text = paragraph_to_text(paragraph)
        if not text.strip():
            continue
        indent = "  " * paragraph.level
        if is_numbered_list(paragraph):
            level = paragraph.level
            numbered_counters[level] = numbered_counters.get(level, 0) + 1
            for deeper in list(numbered_counters):
                if deeper > level:
                    numbered_counters[deeper] = 0
            lines.append(f"{indent}{numbered_counters[level]}. {text}")
        elif is_bullet_list(paragraph, shape):
            lines.append(f"{indent}- {text}")
        elif is_heading_level3(paragraph):
            lines.append(f"### {text}")
        else:
            lines.append(text)
    return lines


def collect_text_shapes(shapes):
    result = []
    for shape in shapes:
        if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
            result.extend(collect_text_shapes(shape.shapes))
        elif shape.has_text_frame:
            result.append(shape)
    return result


def convert_slide(slide, slide_number):
    slide_lines = [f"# Slide {slide_number}", ""]
    title_shape = slide.shapes.title
    if title_shape is not None and title_shape.has_text_frame:
        title_text = paragraph_to_text(title_shape.text_frame.paragraphs[0])
        if title_text.strip():
            slide_lines.append(f"## {title_text}")
            slide_lines.append("")
    for shape in collect_text_shapes(slide.shapes):
        if title_shape is not None and shape.shape_id == title_shape.shape_id:
            continue
        lines = convert_text_frame(shape.text_frame, shape)
        if lines:
            slide_lines.extend(lines)
            slide_lines.append("")
    return slide_lines


def convert_pptx_to_markdown(pptx_path):
    presentation = Presentation(pptx_path)
    markdown_lines = []
    for index, slide in enumerate(presentation.slides, start=1):
        markdown_lines.extend(convert_slide(slide, index))
    return "\n".join(markdown_lines).rstrip() + "\n"


def select_pptx_file():
    root = tk.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename(
        title="変換するPowerPointファイルを選択してください",
        filetypes=[("PowerPoint files", "*.pptx")],
    )
    root.destroy()
    return file_path


def main():
    pptx_path = select_pptx_file()
    if not pptx_path:
        print("ファイルが選択されなかったため、処理を終了します。")
        sys.exit(0)

    if not os.path.isfile(pptx_path):
        print(f"エラー: 指定されたファイルが見つかりません: {pptx_path}")
        sys.exit(1)

    try:
        markdown_text = convert_pptx_to_markdown(pptx_path)
    except Exception as e:
        print(f"エラー: 変換処理中に問題が発生しました: {e}")
        sys.exit(1)

    md_path = os.path.splitext(pptx_path)[0] + ".md"
    with open(md_path, "w", encoding="utf-8") as f:
        f.write(markdown_text)

    print(f"変換が完了しました。出力ファイル: {md_path}")


if __name__ == "__main__":
    main()