workpytools 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- workpytools/__init__.py +1 -0
- workpytools/cli.py +76 -0
- workpytools/common/__init__.py +0 -0
- workpytools/common/browser_preview.py +40 -0
- workpytools/common/clipboard.py +318 -0
- workpytools/common/clustering.py +85 -0
- workpytools/common/config.py +44 -0
- workpytools/common/embedding.py +101 -0
- workpytools/common/help_gen.py +542 -0
- workpytools/common/logging.py +12 -0
- workpytools/common/markdown_html.py +15 -0
- workpytools/common/outline_parse.py +81 -0
- workpytools/common/output.py +71 -0
- workpytools/common/powerpoint.py +43 -0
- workpytools/common/shape_cluster.py +75 -0
- workpytools/common/table_shapes.py +157 -0
- workpytools/common/tabular.py +248 -0
- workpytools/common/textfile.py +90 -0
- workpytools/common/walk.py +133 -0
- workpytools/data/__init__.py +0 -0
- workpytools/data/synonym.tsv +3 -0
- workpytools/data/user.dic +2 -0
- workpytools/processing/__init__.py +0 -0
- workpytools/processing/base.py +23 -0
- workpytools/processing/clipfmt.py +143 -0
- workpytools/processing/clipmd.py +237 -0
- workpytools/processing/clipview.py +194 -0
- workpytools/processing/cwc.py +427 -0
- workpytools/processing/denoise.py +81 -0
- workpytools/processing/help.py +47 -0
- workpytools/processing/ikko.py +205 -0
- workpytools/processing/kukiri.py +94 -0
- workpytools/processing/lsdir.py +218 -0
- workpytools/processing/mdtsv.py +177 -0
- workpytools/processing/mokuji.py +91 -0
- workpytools/processing/nagasa.py +94 -0
- workpytools/processing/outline.py +85 -0
- workpytools/processing/profiler.py +277 -0
- workpytools/processing/seiretsu.py +111 -0
- workpytools/processing/tbl.py +358 -0
- workpytools/processing/touka.py +52 -0
- workpytools/processing/vv.py +147 -0
- workpytools-0.1.4.dist-info/METADATA +458 -0
- workpytools-0.1.4.dist-info/RECORD +48 -0
- workpytools-0.1.4.dist-info/WHEEL +5 -0
- workpytools-0.1.4.dist-info/entry_points.txt +20 -0
- workpytools-0.1.4.dist-info/licenses/LICENSE +21 -0
- workpytools-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import csv
|
|
5
|
+
import io
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from workpytools.common.clipboard import copy_text_to_clipboard, get_clipboard_text
|
|
10
|
+
from workpytools.processing.base import Processor
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_PIPE_ESCAPE = "\x00PIPE\x00" # セル分割前にエスケープ済みパイプを退避する一時トークン
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_markdown_table(text: str) -> bool:
|
|
18
|
+
lines = [line for line in text.split("\n") if line.strip()]
|
|
19
|
+
if len(lines) < 2:
|
|
20
|
+
return False
|
|
21
|
+
if not all(line.strip().startswith("|") and line.strip().endswith("|") for line in lines):
|
|
22
|
+
return False
|
|
23
|
+
separator = lines[1].strip()
|
|
24
|
+
return bool(re.fullmatch(r"\|[\s:|-]+\|", separator)) and "-" in separator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_tsv(text: str) -> bool:
|
|
28
|
+
lines = [line for line in text.split("\n") if line.strip()]
|
|
29
|
+
if not lines:
|
|
30
|
+
return False
|
|
31
|
+
return all("\t" in line for line in lines)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _split_table_row(line: str) -> list[str]:
|
|
35
|
+
stripped = line.strip().strip("|")
|
|
36
|
+
escaped = stripped.replace("\\|", _PIPE_ESCAPE)
|
|
37
|
+
cells = [cell.strip().replace(_PIPE_ESCAPE, "|") for cell in escaped.split("|")]
|
|
38
|
+
return cells
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_separator_row(line: str) -> bool:
|
|
42
|
+
stripped = line.strip()
|
|
43
|
+
return bool(re.fullmatch(r"\|[\s:|-]+\|", stripped)) and "-" in stripped
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def markdown_table_to_tsv(text: str) -> str:
|
|
47
|
+
lines = [line for line in text.split("\n") if line.strip()]
|
|
48
|
+
|
|
49
|
+
table_count = 0
|
|
50
|
+
data_rows: list[list[str]] = []
|
|
51
|
+
br_replaced = False
|
|
52
|
+
|
|
53
|
+
for line in lines:
|
|
54
|
+
if _is_separator_row(line):
|
|
55
|
+
table_count += 1
|
|
56
|
+
continue
|
|
57
|
+
cells = _split_table_row(line)
|
|
58
|
+
new_cells = []
|
|
59
|
+
for cell in cells:
|
|
60
|
+
if "<br>" in cell or "<br/>" in cell or "<br />" in cell:
|
|
61
|
+
br_replaced = True
|
|
62
|
+
cell = re.sub(r"<br\s*/?>", " ", cell)
|
|
63
|
+
new_cells.append(cell)
|
|
64
|
+
data_rows.append(new_cells)
|
|
65
|
+
|
|
66
|
+
if table_count > 1:
|
|
67
|
+
logger.info("表が%d個検出されたため、1つのTSVに連結します", table_count)
|
|
68
|
+
if br_replaced:
|
|
69
|
+
logger.warning("セル内の改行(<br>)はTSVで表現できないため、スペースに置き換えました")
|
|
70
|
+
|
|
71
|
+
out = io.StringIO()
|
|
72
|
+
writer = csv.writer(out, delimiter="\t", lineterminator="\n")
|
|
73
|
+
for row in data_rows:
|
|
74
|
+
writer.writerow(row)
|
|
75
|
+
return out.getvalue().rstrip("\n")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def tsv_to_markdown_table(text: str) -> str:
|
|
79
|
+
reader = csv.reader(io.StringIO(text), delimiter="\t")
|
|
80
|
+
rows = [row for row in reader if row]
|
|
81
|
+
|
|
82
|
+
br_replaced = False
|
|
83
|
+
processed_rows = []
|
|
84
|
+
for row in rows:
|
|
85
|
+
new_row = []
|
|
86
|
+
for cell in row:
|
|
87
|
+
cell = cell.replace("\\", "\\\\").replace("|", "\\|")
|
|
88
|
+
if "\n" in cell:
|
|
89
|
+
br_replaced = True
|
|
90
|
+
cell = cell.replace("\n", "<br>")
|
|
91
|
+
new_row.append(cell)
|
|
92
|
+
processed_rows.append(new_row)
|
|
93
|
+
|
|
94
|
+
max_cols = max((len(row) for row in processed_rows), default=0)
|
|
95
|
+
for row in processed_rows:
|
|
96
|
+
while len(row) < max_cols:
|
|
97
|
+
row.append("")
|
|
98
|
+
|
|
99
|
+
if br_replaced:
|
|
100
|
+
logger.warning("セル内の改行を <br> に置き換えました")
|
|
101
|
+
|
|
102
|
+
lines = []
|
|
103
|
+
if processed_rows:
|
|
104
|
+
header = processed_rows[0]
|
|
105
|
+
lines.append("| " + " | ".join(header) + " |")
|
|
106
|
+
lines.append("| " + " | ".join(["---"] * max_cols) + " |")
|
|
107
|
+
for row in processed_rows[1:]:
|
|
108
|
+
lines.append("| " + " | ".join(row) + " |")
|
|
109
|
+
|
|
110
|
+
return "\n".join(lines)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class MdtsvProcessor(Processor):
|
|
114
|
+
"""Convert clipboard contents between a Markdown table and TSV.
|
|
115
|
+
|
|
116
|
+
Direction is auto-detected from the plain-text content (CF_UNICODETEXT
|
|
117
|
+
only; CF_HTML is never consulted, since Excel's HTML is noisier than
|
|
118
|
+
reconstructing from its TSV payload).
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
name = "mdtsv"
|
|
122
|
+
help = "クリップボードのMarkdownの表とTSVを相互変換する"
|
|
123
|
+
|
|
124
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
125
|
+
group = parser.add_mutually_exclusive_group()
|
|
126
|
+
group.add_argument(
|
|
127
|
+
"--to-tsv", action="store_true", help="入力をMarkdownの表とみなしTSVに変換する"
|
|
128
|
+
)
|
|
129
|
+
group.add_argument(
|
|
130
|
+
"--to-table", action="store_true", help="入力をTSVとみなしMarkdownの表に変換する"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def run(self, args: argparse.Namespace) -> int:
|
|
134
|
+
direction = self._resolve_direction(args)
|
|
135
|
+
|
|
136
|
+
source_text = get_clipboard_text()
|
|
137
|
+
|
|
138
|
+
if direction == "to_tsv":
|
|
139
|
+
result = markdown_table_to_tsv(source_text)
|
|
140
|
+
label = "Markdownの表 → TSV"
|
|
141
|
+
else:
|
|
142
|
+
result = tsv_to_markdown_table(source_text)
|
|
143
|
+
label = "TSV → Markdownの表"
|
|
144
|
+
|
|
145
|
+
if not result.strip():
|
|
146
|
+
raise SystemExit("変換結果が空です")
|
|
147
|
+
|
|
148
|
+
copy_text_to_clipboard(result)
|
|
149
|
+
print(f"{label} に変換しました")
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
def _resolve_direction(self, args: argparse.Namespace) -> str:
|
|
153
|
+
try:
|
|
154
|
+
source_text = get_clipboard_text()
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
raise SystemExit("変換できるテキストがありません") from exc
|
|
157
|
+
|
|
158
|
+
if args.to_tsv:
|
|
159
|
+
if not _is_markdown_table(source_text):
|
|
160
|
+
raise SystemExit("入力をMarkdownの表として解釈できません")
|
|
161
|
+
return "to_tsv"
|
|
162
|
+
|
|
163
|
+
if args.to_table:
|
|
164
|
+
logger.info("--to-table指定: タブの有無にかかわらずTSVとして扱います")
|
|
165
|
+
return "to_table"
|
|
166
|
+
|
|
167
|
+
if _is_markdown_table(source_text):
|
|
168
|
+
logger.info("Markdownの表を検出したため 表 → TSV に変換します")
|
|
169
|
+
return "to_tsv"
|
|
170
|
+
if _is_tsv(source_text):
|
|
171
|
+
logger.info("TSVを検出したため TSV → 表 に変換します")
|
|
172
|
+
return "to_table"
|
|
173
|
+
|
|
174
|
+
raise SystemExit(
|
|
175
|
+
"入力をMarkdownの表ともTSVとも解釈できません"
|
|
176
|
+
"(Markdownの表全体、またはタブ区切りのテキストをコピーしてください)"
|
|
177
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from workpytools.common.clipboard import copy_text_to_clipboard
|
|
8
|
+
from workpytools.common.powerpoint import (
|
|
9
|
+
NoActivePresentationError,
|
|
10
|
+
PowerPointNotRunningError,
|
|
11
|
+
get_active_presentation,
|
|
12
|
+
get_running_powerpoint,
|
|
13
|
+
)
|
|
14
|
+
from workpytools.common.textfile import to_crlf
|
|
15
|
+
from workpytools.processing.base import Processor
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
_NEWLINE_RE = re.compile(r"\r\n|\r|\n")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MokujiProcessor(Processor):
|
|
23
|
+
"""Collect a title per slide across the active presentation and copy
|
|
24
|
+
the list to the clipboard, one title per line.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
name = "mokuji"
|
|
28
|
+
help = "全スライドのタイトルを一覧化してクリップボードにコピーする"
|
|
29
|
+
|
|
30
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def run(self, args: argparse.Namespace) -> int:
|
|
34
|
+
try:
|
|
35
|
+
app = get_running_powerpoint()
|
|
36
|
+
except PowerPointNotRunningError as exc:
|
|
37
|
+
raise SystemExit(
|
|
38
|
+
"PowerPointでプレゼンテーションを開いた状態で実行してください"
|
|
39
|
+
) from exc
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
presentation = get_active_presentation(app)
|
|
43
|
+
except NoActivePresentationError as exc:
|
|
44
|
+
raise SystemExit(
|
|
45
|
+
"PowerPointでプレゼンテーションを開いた状態で実行してください"
|
|
46
|
+
) from exc
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
slide_count = presentation.Slides.Count
|
|
50
|
+
if slide_count == 0:
|
|
51
|
+
print("対象のスライドがありません")
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
titles = [
|
|
55
|
+
self._extract_title(presentation.Slides.Item(i))
|
|
56
|
+
for i in range(1, slide_count + 1)
|
|
57
|
+
]
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
raise SystemExit(f"PowerPointの操作中にエラーが発生しました: {exc}") from exc
|
|
60
|
+
|
|
61
|
+
text = to_crlf("\n".join(titles))
|
|
62
|
+
copy_text_to_clipboard(text)
|
|
63
|
+
logger.info("%d枚のスライドから%d件のタイトルを抽出しました", slide_count, len(titles))
|
|
64
|
+
print(f"{len(titles)}件のタイトルをコピーしました")
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
def _extract_title(self, slide: object) -> str:
|
|
68
|
+
if slide.Shapes.HasTitle: # type: ignore[attr-defined]
|
|
69
|
+
title_text = slide.Shapes.Title.TextFrame.TextRange.Text # type: ignore[attr-defined]
|
|
70
|
+
if title_text.strip():
|
|
71
|
+
return self._flatten(title_text)
|
|
72
|
+
|
|
73
|
+
fallback = self._topmost_text(slide)
|
|
74
|
+
return self._flatten(fallback) if fallback else ""
|
|
75
|
+
|
|
76
|
+
def _topmost_text(self, slide: object) -> str | None:
|
|
77
|
+
best_top: float | None = None
|
|
78
|
+
best_text: str | None = None
|
|
79
|
+
for shape in slide.Shapes: # type: ignore[attr-defined]
|
|
80
|
+
if not getattr(shape, "HasTextFrame", False):
|
|
81
|
+
continue
|
|
82
|
+
text = shape.TextFrame.TextRange.Text
|
|
83
|
+
if not text or not text.strip():
|
|
84
|
+
continue
|
|
85
|
+
if best_top is None or shape.Top < best_top:
|
|
86
|
+
best_top = shape.Top
|
|
87
|
+
best_text = text
|
|
88
|
+
return best_text
|
|
89
|
+
|
|
90
|
+
def _flatten(self, text: str) -> str:
|
|
91
|
+
return _NEWLINE_RE.sub(" ", text).strip()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from workpytools.common.powerpoint import (
|
|
7
|
+
NoActivePresentationError,
|
|
8
|
+
PowerPointNotRunningError,
|
|
9
|
+
get_active_presentation,
|
|
10
|
+
get_running_powerpoint,
|
|
11
|
+
)
|
|
12
|
+
from workpytools.processing.base import Processor
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
_SELECTION_SHAPES = 2 # ppSelectionShapes
|
|
17
|
+
_PP_AUTO_SIZE_NONE = 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NagasaProcessor(Processor):
|
|
21
|
+
"""Unify the width and height of selected shapes to the max found
|
|
22
|
+
among them, resizing around each shape's own center so relative
|
|
23
|
+
positions (e.g. after seiretsu) aren't disturbed.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name = "nagasa"
|
|
27
|
+
help = "選択したシェイプの幅・高さを最大値に統一する"
|
|
28
|
+
|
|
29
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def run(self, args: argparse.Namespace) -> int:
|
|
33
|
+
try:
|
|
34
|
+
app = get_running_powerpoint()
|
|
35
|
+
except PowerPointNotRunningError as exc:
|
|
36
|
+
raise SystemExit(
|
|
37
|
+
"PowerPointでプレゼンテーションを開いた状態で実行してください"
|
|
38
|
+
) from exc
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
get_active_presentation(app)
|
|
42
|
+
except NoActivePresentationError as exc:
|
|
43
|
+
raise SystemExit(
|
|
44
|
+
"PowerPointでプレゼンテーションを開いた状態で実行してください"
|
|
45
|
+
) from exc
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
selection = app.ActiveWindow.Selection
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
raise SystemExit(f"選択状態の取得中にエラーが発生しました: {exc}") from exc
|
|
51
|
+
|
|
52
|
+
if selection.Type != _SELECTION_SHAPES:
|
|
53
|
+
raise SystemExit("対象を選択してから実行してください(2つ以上選択が必要です)")
|
|
54
|
+
|
|
55
|
+
shape_range = selection.ShapeRange
|
|
56
|
+
shapes = [shape_range.Item(i) for i in range(1, shape_range.Count + 1)]
|
|
57
|
+
|
|
58
|
+
if len(shapes) < 2:
|
|
59
|
+
raise SystemExit("対象を選択してから実行してください(2つ以上選択が必要です)")
|
|
60
|
+
|
|
61
|
+
max_width = max(s.Width for s in shapes)
|
|
62
|
+
max_height = max(s.Height for s in shapes)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
app.StartNewUndoEntry()
|
|
66
|
+
for shape in shapes:
|
|
67
|
+
self._disable_autosize(shape)
|
|
68
|
+
center_x = shape.Left + shape.Width / 2
|
|
69
|
+
center_y = shape.Top + shape.Height / 2
|
|
70
|
+
shape.Width = max_width
|
|
71
|
+
shape.Height = max_height
|
|
72
|
+
shape.Left = center_x - max_width / 2
|
|
73
|
+
shape.Top = center_y - max_height / 2
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
raise SystemExit(
|
|
76
|
+
f"PowerPointの操作中にエラーが発生しました(Ctrl+Zで開始前の状態に戻せます): {exc}"
|
|
77
|
+
) from exc
|
|
78
|
+
|
|
79
|
+
logger.info(
|
|
80
|
+
"対象シェイプ数=%d 統一サイズ=幅%.1f 高さ%.1f", len(shapes), max_width, max_height
|
|
81
|
+
)
|
|
82
|
+
print(
|
|
83
|
+
f"{len(shapes)}個のシェイプのサイズを統一しました"
|
|
84
|
+
f"(幅{max_width:.1f}pt, 高さ{max_height:.1f}pt)"
|
|
85
|
+
)
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
def _disable_autosize(self, shape: object) -> None:
|
|
89
|
+
if not getattr(shape, "HasTextFrame", False):
|
|
90
|
+
return
|
|
91
|
+
try:
|
|
92
|
+
shape.TextFrame.AutoSize = _PP_AUTO_SIZE_NONE # type: ignore[attr-defined]
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from workpytools.common.clipboard import ClipboardTextError, get_clipboard_text
|
|
7
|
+
from workpytools.common.outline_parse import OutlineItem, parse_outline
|
|
8
|
+
from workpytools.common.powerpoint import (
|
|
9
|
+
NoActivePresentationError,
|
|
10
|
+
PowerPointNotRunningError,
|
|
11
|
+
get_active_presentation,
|
|
12
|
+
get_running_powerpoint,
|
|
13
|
+
)
|
|
14
|
+
from workpytools.processing.base import Processor
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_LAYOUT_TITLE_AND_CONTENT = 2 # ppLayoutText
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OutlineProcessor(Processor):
|
|
22
|
+
"""Turn a clipboard outline into slides appended to the active
|
|
23
|
+
PowerPoint presentation. Only adds slides -- table-of-contents
|
|
24
|
+
generation is a separate command (mokuji).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
name = "outline"
|
|
28
|
+
help = "クリップボードのアウトラインからPowerPointにスライドを追加する"
|
|
29
|
+
|
|
30
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def run(self, args: argparse.Namespace) -> int:
|
|
34
|
+
try:
|
|
35
|
+
text = get_clipboard_text()
|
|
36
|
+
except ClipboardTextError as exc:
|
|
37
|
+
raise SystemExit(str(exc)) from exc
|
|
38
|
+
|
|
39
|
+
items = parse_outline(text)
|
|
40
|
+
if not items:
|
|
41
|
+
raise SystemExit("アウトラインとして解釈できる内容がありません")
|
|
42
|
+
|
|
43
|
+
presentation, created_new = self._get_or_create_presentation()
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
for item in items:
|
|
47
|
+
self._add_slide(presentation, item)
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
raise SystemExit(f"PowerPointの操作中にエラーが発生しました: {exc}") from exc
|
|
50
|
+
|
|
51
|
+
logger.info(
|
|
52
|
+
"%d件のスライドを追加しました(%s)",
|
|
53
|
+
len(items),
|
|
54
|
+
"新規プレゼンテーション" if created_new else "既存のプレゼンテーションに追記",
|
|
55
|
+
)
|
|
56
|
+
print(f"{len(items)}件のスライドを追加しました")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
def _get_or_create_presentation(self) -> tuple[object, bool]:
|
|
60
|
+
try:
|
|
61
|
+
app = get_running_powerpoint()
|
|
62
|
+
except PowerPointNotRunningError:
|
|
63
|
+
import win32com.client
|
|
64
|
+
|
|
65
|
+
app = win32com.client.Dispatch("PowerPoint.Application")
|
|
66
|
+
app.Visible = True
|
|
67
|
+
presentation = app.Presentations.Add()
|
|
68
|
+
logger.info("PowerPointを新規起動し、新規プレゼンテーションを作成しました")
|
|
69
|
+
return presentation, True
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
presentation = get_active_presentation(app)
|
|
73
|
+
except NoActivePresentationError:
|
|
74
|
+
presentation = app.Presentations.Add()
|
|
75
|
+
logger.info("既存のPowerPointに新規プレゼンテーションを作成しました")
|
|
76
|
+
return presentation, True
|
|
77
|
+
|
|
78
|
+
logger.info("既存のプレゼンテーションに追記します")
|
|
79
|
+
return presentation, False
|
|
80
|
+
|
|
81
|
+
def _add_slide(self, presentation: object, item: OutlineItem) -> None:
|
|
82
|
+
index = presentation.Slides.Count + 1 # type: ignore[attr-defined]
|
|
83
|
+
slide = presentation.Slides.Add(index, _LAYOUT_TITLE_AND_CONTENT) # type: ignore[attr-defined]
|
|
84
|
+
slide.Shapes.Placeholders(1).TextFrame.TextRange.Text = item.title
|
|
85
|
+
slide.Shapes.Placeholders(2).TextFrame.TextRange.Text = item.body.replace("\n", "\r")
|