"""薬剤搬送ロボット(/series1)の記事サムネイル9枚を作る(LOG473)。 前は HTML のサムネイル(白黒・英字の見出し)だった9本を、記事の画像・4コマの絵・記事の図解から作った 1920x1080 の webp に替える。素材はすべて公開中のサイト(pixelstomotors.com/media/ と p2m-site の public/embeds/)から取る。 使い方(p2m-site のフォルダで動かす): python3 make_hri_covers.py <出力フォルダ> 斜めに置く図解(LOG303・LOG305)は、同じフォルダの tilt.js(Playwright)で描く。 """ import json import os import subprocess import sys import urllib.request from pathlib import Path from PIL import Image OUT = Path(sys.argv[1] if len(sys.argv) > 1 else 'hri_covers') SRC = OUT / '_src' HERE = Path(__file__).resolve().parent SITE = 'https://pixelstomotors.com' # 記事の画像(Fusion の画面・三面図) MEDIA = { 'plate': '/media/s-2642x1806_v-frms_webp_3b3bd195-b7fb-4c65-9229-a6eb4f907f01.png', # LOG307 40x40x5mm の板 'parts': '/media/s-2642x1806_v-frms_2fc0ae98-d942-4304-bb8a-09f002fd36e2.webp', # LOG308 角の丸い箱に穴が開いた最初の形 'solid': '/media/s-2720x1894_v-frms_webp_6738acd9-b438-4ced-bfe0-a65c630a5b69.png', # LOG311 ボディ 'pikari': '/media/s-1376x768_v-fms_1f4bba08-2627-4dd8-b02a-ca45b3e44374.webp', # LOG322 ピカリくんの三面図 } # 4コマの扉絵(1080x1350) MANGA = {'ep100': None, 'ep80': None, 'ep110': None} def fetch(url, dst): if not dst.exists(): req = urllib.request.Request(url, headers={'User-Agent': 'p2m-covers'}) dst.write_bytes(urllib.request.urlopen(req, timeout=60).read()) return dst def crop_center(src, cx, cy, w, dst): """中心 (cx, cy)・幅 w の 16:9 を切り出して 1920x1080 にする""" im = Image.open(src).convert('RGB') h = w * 9 / 16 im.crop((int(cx - w / 2), int(cy - h / 2), int(cx + w / 2), int(cy + h / 2))).resize((1920, 1080), Image.LANCZOS).save(dst, quality=85) def crop_band(src, y, dst): """縦長の扉絵から、上から y の位置で横幅いっぱいの 16:9 の帯を切り出す""" im = Image.open(src).convert('RGB') W = im.size[0] h = int(W * 9 / 16) im.crop((0, y, W, y + h)).resize((1920, 1080), Image.LANCZOS).save(dst, quality=85) def main(): OUT.mkdir(parents=True, exist_ok=True) SRC.mkdir(exist_ok=True) src = {k: fetch(SITE + v, SRC / Path(v).name) for k, v in MEDIA.items()} for ep in MANGA: d = json.load(open(f'src/data/manga/{ep}.json', encoding='utf-8')) src[ep] = fetch(SITE + d['cover'], SRC / f'{ep}.webp') # 記事の Fusion の画面:ウィンドウの中の作業画面だけを、形が真ん中に来るように切る crop_center(src['plate'], 1320, 900, 1200, OUT / 'hri-thumb-LOG307.webp') crop_center(src['parts'], 1400, 880, 1500, OUT / 'hri-thumb-LOG308-v3.webp') crop_center(src['solid'], 1560, 1040, 2050, OUT / 'hri-thumb-LOG311.webp') # ピカリくんの三面図:右端の余白を落として 16:9 に Image.open(src['pikari']).convert('RGB').crop((0, 0, 1365, 768)).resize((1920, 1080), Image.LANCZOS).save(OUT / 'hri-thumb-LOG322.webp', quality=85) # 4コマの扉絵:題名の文字を外し、顔が切れない高さで帯にする crop_band(src['ep100'], 560, OUT / 'hri-thumb-LOG267-v2.webp') # 第100話 夜の運び方 crop_band(src['ep80'], 330, OUT / 'hri-thumb-LOG269-v2.webp') # 第80話 過去最高の配送件数 crop_band(src['ep110'], 300, OUT / 'hri-thumb-LOG321-v2.webp') # 第110話 すぐ戻ります # 記事の図解を斜めに置く(回転 -16°・余白が出ない倍率まで寄せる) jobs = [ ['figma-design-token-consolidation-CwxN66LU.html', 'design-token', -16, 3.3, 380, 380], ['does-a-delivery-robot-help-nurses-t7lznGbR.html', 'four-assumptions', -16, 3.0, 330, 600], ] tmp = OUT / '_tilt' (tmp / 'out').mkdir(parents=True, exist_ok=True) subprocess.run(['node', str(HERE / 'tilt.js'), json.dumps(jobs)], check=True, env={**os.environ, 'S': str(tmp)}) for name, log in [('design-token', 303), ('four-assumptions', 305)]: Image.open(tmp / 'out' / f'{name}.png').convert('RGB').save(OUT / f'hri-thumb-LOG{log}-v2.webp', quality=85) for f in sorted(OUT.glob('*.webp')): print(f.name, Image.open(f).size, f.stat().st_size, 'bytes') if __name__ == '__main__': main()