こんにちは。
駆け出しエンジニアの:DaVです。
つい先日ほぼ皆既の部分月食が確認されたそうですね!
私は室内にいたので観測できなかったのですが、皆さんはいかがでしたでしょうか?
最近月がとても綺麗なので上を見ながら歩きたくなりますよね:)
あ、告白じゃないですよ。
それはさておき、今回はPython勉強の一環として、ライブラリを利用してPowerPointからイメージマップを生成するスクリプト開発に挑戦しましたので、記事に残したいと思います:D
Pythonを用いてPowerPointの自動化や連携を考えている方の参考になれば幸いです:)
読むのが面倒な方はGitHubにも公開していますので、そちらをご覧ください!
もしくは目次から「実行結果」まで読み飛ばしてください。
それでは、早速!
今回のゴール
PythonのPowerPoint用ライブラリを使用し、ハイパーリンクを埋め込んだPowerPointをスライドごとにイメージマップ化することが今回のゴールです:D
パワポ側でワイヤーフレーム的なものを作成した際に便利になるかと思い試してみました。
PowerPoint用ライブラリ「python-pptx」
「python-pptx」は、PowerPointを作成したり更新したりできるライブラリです。
テキストを抽出したり、テンプレートの自動作成するときとかに便利みたいですね:D
ちなみにこのライブラリはプレゼンテーション(.pptx)形式のみ対応しているため、スライドショー(.ppsx)形式に対してのアクセスは出来ないみたいです。
また、スライドマスターやグループ化されたオブジェクトの子要素に対する複雑な操作もできないみたいなので、要注意です。
まぁシンプルな操作はできるので全然問題ないと思いました:)
公式のドキュメントはコチラです。
ということで今回はこの「python-pptx」ライブラリを使用してパワポからイメージマップの自動生成を目指します!
イメージマップとは?
イメージマップはWebサイトに配置した画像に対して、任意の座標にリンクをつける実装方法です。
通常だとAdobeのPhotoshopやBracketsを利用したり、オンラインのジェネレータサービスを利用して座標を取るのですが、これをパワポからでもできないかなぁ~と思い挑戦してみました:)
Webのワイヤーフレームやモックアップを作成する際、PowerPointを使用することもあるかもしれませんので活用できればって感じのノリです。
あ、車輪の再発明は禁句ですよ!
環境準備
というわけで準備編です。
下記環境で諸々をインストールしていきます!
- Windows10 Home 64bit
- Visual Studio Code 1.62.1
- Python 3.9.6
python-pptxをインストール
pipで一発です:D
pip install python-pptx
画像処理ライブラリをインストール
画像のサイズ情報を取得するためにPillowライブラリをインストールします。
pip install pillow
jQuery をダウンロード
後述のプラグインを利用するため、ローカルで実行したい人はコチラからダウンロードしておきましょう。
CDNの場合は不要ですね:D
RWD Image Maps をダウンロード
イメージマップをレスポンシブに対応させるために「RWD Image Maps」を使用します。
コチラから「jquery.rwdImageMaps.min.js」をダウンロードします。
PowerPointの準備
ハイパーリンク付きのプレゼンテーションファイル作成します。
スライド間のリンクがあればとりあえずなんでもOKです。
ただし、先述したようにグループ化されたオブジェクトの子要素にはアクセスできないため、あくまで個別にハイパーリンクを付与するのがポイントです。
こんな感じで貼っていきます↓
とりあえず試してみたい方は以下からご自由にダウンロードしてください:D
ちなみに今回はイメージマップのサイズを「1920*1080px」にしたかったので、PowerPointのサイズを「50.8cm*28.575cm」(1920*1080px)に設定してます:D
コード実装
諸々の準備が整ったら早速実装していきます:D
HTML/CSSテンプレートの作成
まずはこんな感じのイメージマップ用のテンプレートを用意しておきます。
<!DOCTYPE html>
<head>
<meta charset="UTF-8" />
<title>{% title %}</title>
<link rel="stylesheet" href={% csspath %}>
<script type="text/javascript" src={% jqpath %}></script>
<script type="text/javascript" src={% rwdpath %}></script>
</head>
<body>
<div class="container">
<img src={% imgpath %} width={% imgwidth %} height={% imgheight %} usemap="#image-map">
<map name="image-map">
{% maparea %}
</map>
</div>
<script>
$(document).ready(function () {
$('img[usemap]').rwdImageMaps();
});
</script>
</body>
</html>
{%%}で区切っている部分にPowerPointから取得した情報を埋め込む感じです:D
scriptには「RWD Image Maps」用の処理を記述し、レスポンシブ対応します。
このHTMLファイルがPowerPointのスライド枚数分出力される感じです。
CSSは適当ですがこんな感じです。
::-webkit-scrollbar {
display: none;
-webkit-appearance: none;
}
html,body {
margin: 0px;
height: 100%;
background-color:#10100E;
}
.container {
height: 100%;
display: flex;
}
img{
height: auto;
max-height:100%;
width: auto;
max-width: 100%;
margin: auto;
}
今回のキモはPythonからPowerPointへのアクセスすることなので、こんなもんです:(
PythonでPowerPointからHTMLへの変換処理を実装
ちょっと長いのでポイントだけ解説します。
python-pptxのインポート
基本の「Presentation」のほか、リンクやグループオブジェクトを判定するため、「PP_ACTION」と「MSO_SHAPE_TYPE」のモジュールもインポートします。
from pptx import Presentation
from pptx.enum.action import PP_ACTION
from pptx.enum.shapes import MSO_SHAPE_TYPE
PowerPointのプロパティ取得
Presentation()でプロパティを取得します。(ppt_nameには.pptxのパスを渡します。)
dst_ppt = Presentation(ppt_name)
emuをピクセルに変換
PowerPointのサイズは「emu」という単位で取得されるため、ピクセルに変換します。
ADJUST = 12700
PT = (1 / 72) * 2.54
PIXEL = 0.0264
def emuToPx(emu):
return int(emu / ADJUST * PT / PIXEL)
ppt_px_width = emuToPx(dst_ppt.slide_width)
※dpi=96ドットの場合で計算しています。
グループオブジェクトとハイパーリンクの検出
グループの子要素にアクセスできないため、スライド内のグループ化されていないオブジェクトについて、ハイパーリンクであるかどうかを確認してます。
for i, sld in enumerate(dst_ppt.slides, start=1): # スライドループ
…
for shp in sld.shapes: # シェイプループ
…
if shp.shape_type != MSO_SHAPE_TYPE.GROUP: # グループオブジェクトチェック
click_action = shp.click_action
…
if click_action.action == PP_ACTION.NAMED_SLIDE: # ハイパーリンクチェック
…
コード全体
全文を見たい方は下のボタンをクリック、もしくはGitHubをご覧ください:)
# -*- coding: utf-8 -*-
import sys, io, os
import pprint
import codecs
from pptx import Presentation
from pptx.enum.action import PP_ACTION
from pptx.enum.shapes import MSO_SHAPE_TYPE
import glob
import argparse
from PIL import Image
# japanese
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# Index
HEIGHT = 0
WIDTH = 1
CHANNEL = 2
# Path
SRC_PPT_PATH = "src_ppt.pptx"
image_path = "./image"
# Val
ADJUST = 12700
PT = (1 / 72) * 2.54
PIXEL = 0.0264
DEFAULT_HEIGHT = 1920
DEFAULT_WIDTH = 1080
# find pptx file
def getPptName():
name = ''
for fl in glob.glob('./*.pptx'):
name = os.path.split(fl)[1]
print('Target : ' + name)
return name
def emuToPx(emu):
return int(emu / ADJUST * PT / PIXEL)
def getImgSize():
img_list = glob.glob('image/*.png')
if len(img_list) > 0:
img = Image.open(img_list[0])
else :
return 1920, 1080
return img.width, img.height
def convertMain(args):
x, y = getImgSize()
if args.width != 'default':
x = int(args.width)
if args.height != 'default':
y = int(args.height)
csspath = '"' + str(args.csspath) + '"'
jqpath = '"' + str(args.jqpath) + '"'
rwdpath = '"' + str(args.rwdpath) + '"'
imgwidth = '"' + str(x) + '"'
imgheight = '"' + str(y) + '"'
# main
print('#### Start program! ####')
# Get ppt file name
ppt_name = ''
ppt_name = getPptName()
if ppt_name == '':
print("not found .pptx file")
exit()
print("image width : " + str(x) + "px")
print("image height : " + str(y) + "px")
# Get Ppt Infomation
dst_ppt = Presentation(ppt_name)
ppt_px_width = emuToPx(dst_ppt.slide_width)
# Calc image ratio
ratio = x / ppt_px_width
# ppt_data = {slide_number: slide_data, ....}
ppt_data = {}
# ppt_name_data = {slideID: slide_number, ...}
ppt_name_data = {}
# ppt slide loop
for i, sld in enumerate(dst_ppt.slides, start=1):
# slide_data = {name: '', id: '', link_count: ''}
ppt_slide_data = {}
ppt_data[str(i)] = ppt_slide_data
ppt_slide_data["name"] = 'slide' + str(i)
ppt_slide_data["id"] = str(sld.slide_id)
ppt_name_data[str(sld.slide_id)] = 'slide' + str(i)
ppt_slide_data['link_count'] = 0
# j = link count
j = 0
# shape loop
for shp in sld.shapes:
# print("shp.name : ", shp.name)
# Not Group Shape
if shp.shape_type != MSO_SHAPE_TYPE.GROUP:
click_action = shp.click_action
# check shape type
if click_action.action == PP_ACTION.NAMED_SLIDE:
# print("shp.name : ", shp.name)
ppt_slide_data['link_count'] += 1
shp_px_width = emuToPx(shp.width)
shp_px_height = emuToPx(shp.height)
shp_px_x1 = emuToPx(shp.left)
shp_px_y1 = emuToPx(shp.top)
shp_px_x2 = shp_px_x1 + shp_px_width
shp_px_y2 = shp_px_y1 + shp_px_height
img_px_x1 = int(shp_px_x1 * ratio)
img_px_y1 = int(shp_px_y1 * ratio)
img_px_x2 = int(shp_px_x2 * ratio)
img_px_y2 = int(shp_px_y2 * ratio)
ppt_slide_data['link' + str(j) + '_x1'] = str(img_px_x1)
ppt_slide_data['link' + str(j) + '_y1'] = str(img_px_y1)
ppt_slide_data['link' + str(j) + '_x2'] = str(img_px_x2)
ppt_slide_data['link' + str(j) + '_y2'] = str(img_px_y2)
ppt_slide_data['link' + str(j) + '_target_name'] = 'tmp'
target = click_action.target_slide
# DBG
# print("target : " + str(target.slide_id))
ppt_slide_data['link' + str(j) + '_target_id'] = target.slide_id
j += 1
if j == 0:
print("Warning : Not exist link-object in slide" + str(i) + ".")
# PPT dict loop
print('#### Create slideX.html ####')
for k in ppt_data:
# html page data
page_data = {}
page_data['title'] = ppt_name.replace('.pptx','') + ' ' + ppt_data[k]['name']
page_data['csspath'] = csspath
page_data['jqpath'] = jqpath
page_data['rwdpath'] = rwdpath
page_data['imgpath'] = '"' + 'image/' + 'スライド' + k + '.png' + '"'
page_data['imgwidth'] = imgwidth
page_data['imgheight'] = imgheight
# creat area tag
area_all_str = ''
if ppt_data[k]['link_count'] != 0:
for lc in range(ppt_data[k]['link_count']):
idx = str(lc)
area_str = '<area href="' + ppt_name_data[str(ppt_data[k]['link' + idx + '_target_id'])] + '.html" coords="'
# print("LINK " + str(lc))
area_str = area_str + str(ppt_data[k]['link' + idx + '_x1']) + ',' + str(ppt_data[k]['link' + idx + '_y1']) + ',' \
+ str(ppt_data[k]['link' + idx + '_x2']) + ',' + str(ppt_data[k]['link' + idx + '_y2'])
area_str = area_str + '" shape="rect">' + "\n"
area_all_str = str(area_all_str) + area_str
page_data['maparea'] = area_all_str
# read temlate.html
with open('template/template.html','r') as file:
html = file.read()
file.closed
# replace {% %} to page_data
for key, value in page_data.items():
html = html.replace('{% ' + key + ' %}', value)
# html output
f = codecs.open('slide' + k + '.html', 'w', 'utf-8')
print(html, file=f)
print("#### Complete! ####")
def checkArguments(size):
if size.isdigit():
if int(size) > 0 and int(size) < 10000:
return True
else:
print('Value error...')
return False
elif size == 'default':
return True
else:
print('No other than numbers...')
return False
def getArguments():
parser = argparse.ArgumentParser(description='.pptxの各スライドをクリッカブルマップにコンバートします。')
parser.add_argument('-W', '--width', required=False, default='default' ,help='クリッカブルマップ対象の画像の幅(default:image内のpngサイズ)')
parser.add_argument('-H', '--height', required=False, default='default', help='クリッカブルマップ対象の画像の高さ(default:image内のpngサイズ)')
parser.add_argument('-J', '--jqpath', required=False, default='https://code.jquery.com/jquery-3.5.1.js', help='jQueryのパス指定(default:CDN Path)')
parser.add_argument('-R', '--rwdpath', required=False, default='js/jquery.rwdImageMaps.js', help='jQuery RWD Image Maps のパス指定(default:js/jquery.rwdImageMaps.js)')
parser.add_argument('-C', '--csspath', required=False, default='css/style.css', help='CSS のパス指定(default:css/style.css)')
return parser.parse_args()
if __name__ == '__main__':
args = getArguments()
args_flag = False
if checkArguments(args.width) != False and checkArguments(args.height) != False:
args_flag = True
if args_flag != False:
convertMain(args)
else:
exit()
相変わらず殴り書きコードですがほんと許してください:D
使い方
こんな感じの構成になるように準備します。↓
①作成したPowerPointの全スライドを以下手順でPNG形式でエクスポート
「ファイル」>「名前を付けて保存」>「PNG」ですべてのスライドを保存
②エクスポートしたPNGをimageフォルダに格納
③PptxToHtml.pyと同レベルにパワポファイルを配置
④下記コマンドで実行
py PptxToHtml.py
デフォルトではimageフォルダ内のPNGファイルの大きさをベースとしてイメージマップを生成します。
実行オプションを指定することも可能です:)
-W --width クリッカブルマップ対象の画像の幅 (default:image内のpngサイズ)
-H --height クリッカブルマップ対象の画像の高さ (default:image内のpngサイズ)
-J --jqpath jQueryのパス指定 (default:CDN Path)
-R --rwdpath jQuery RWD Image Maps のパス指定 (default:js/jquery.rwdImageMaps.js)
-C --csspath CSS のパス指定 (default:css/style.css)
そのままhtmlファイルを確認できます。
実行結果
実行するとこんな感じです↓
イメージマップになってますね!
今後の課題
ということで無事にパワポからイメージマップが作成できましたが、下記のような課題も見つかりました。
- クリック領域は矩形に限定されてしまう
- 背景画像も限定される
- 使い勝手が微妙
うーん…やはり実用化するにはいまひとつって感じですね:(
改善の余地ありまくりです。
勉強にはなったのでまあ良しとします!
おわりに
いかがでしたでしょうか!
PythonでパワポからHTMLを生成というすでにありそうな試みでしたが、何か発見があれば幸いです。
それにしてもPythonはいろいろなライブラリを簡単に試せて面白いですね:)
気が向いたらもう少し有効な使い方ができるように検討してみたいと思ます!
それでは、また:D
コメント