- Зачем: - расширены возможности AI-ассистентов для работы с документами и презентациями - Что: - добавлен skill pdf для работы с PDF файлами - добавлен skill doc для работы с Word документами - добавлен skill slides для создания презентаций PowerPoint - добавлен skill jupyter-notebook для работы с Jupyter ноутбуками - добавлен skill spreadsheet для работы с таблицами (Excel/CSV) - изменена модель Claude с sonnet на opus - Проверка: - проверка доступности skills в AI-ассистентах
202 lines
6.3 KiB
Python
202 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) OpenAI. All rights reserved.
|
|
import argparse
|
|
import sys
|
|
import tempfile
|
|
from os.path import abspath, expanduser, join
|
|
from pathlib import Path
|
|
from typing import Sequence, cast
|
|
|
|
import numpy as np
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
if str(SCRIPT_DIR) not in sys.path:
|
|
sys.path.insert(0, str(SCRIPT_DIR))
|
|
|
|
import render_slides # type: ignore
|
|
from PIL import Image
|
|
from pptx import Presentation
|
|
from pptx.dml.color import RGBColor
|
|
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
|
|
from pptx.util import Emu
|
|
|
|
# Configuration specific to overflow checking
|
|
PAD_PX: int = 100 # fixed padding on every side in pixels
|
|
PAD_RGB = (200, 200, 200)
|
|
EMU_PER_INCH: int = 914_400
|
|
|
|
|
|
def px_to_emu(px: int, dpi: int) -> Emu:
|
|
return Emu(int(px * EMU_PER_INCH // dpi))
|
|
|
|
|
|
def calc_tol(dpi: int) -> int:
|
|
"""Calculate per-channel colour tolerance appropriate for *dpi* (anti-aliasing tolerance)."""
|
|
if dpi >= 300:
|
|
return 0
|
|
# 1 at 250 DPI, 5 at 150 DPI, capped to 10.
|
|
tol = round((300 - dpi) / 25)
|
|
return min(max(tol, 1), 10)
|
|
|
|
|
|
def enlarge_deck(src: str, dst: str, pad_emu: Emu) -> tuple[int, int]:
|
|
"""Enlarge the input PPTX with a fixed grey padding and return the new page size."""
|
|
prs = Presentation(src)
|
|
w0 = cast(Emu, prs.slide_width)
|
|
h0 = cast(Emu, prs.slide_height)
|
|
w1 = Emu(w0 + 2 * pad_emu)
|
|
h1 = Emu(h0 + 2 * pad_emu)
|
|
prs.slide_width = w1
|
|
prs.slide_height = h1
|
|
|
|
for slide in prs.slides:
|
|
# Shift all shapes so the original canvas sits centred in the new deck.
|
|
for shp in list(slide.shapes):
|
|
shp.left = Emu(int(shp.left) + pad_emu)
|
|
shp.top = Emu(int(shp.top) + pad_emu)
|
|
|
|
pads = (
|
|
(Emu(0), Emu(0), pad_emu, h1), # left
|
|
(Emu(int(w1) - int(pad_emu)), Emu(0), pad_emu, h1), # right
|
|
(Emu(0), Emu(0), w1, pad_emu), # top
|
|
(Emu(0), Emu(int(h1) - int(pad_emu)), w1, pad_emu), # bottom
|
|
)
|
|
|
|
sp_tree = slide.shapes._spTree # pylint: disable=protected-access
|
|
|
|
for left, top, width, height in pads:
|
|
pad_shape = slide.shapes.add_shape(
|
|
MSO_AUTO_SHAPE_TYPE.RECTANGLE, left, top, width, height
|
|
)
|
|
pad_shape.fill.solid()
|
|
pad_shape.fill.fore_color.rgb = RGBColor(*PAD_RGB)
|
|
pad_shape.line.fill.background()
|
|
|
|
# Send pad behind all other shapes (index 2 after mandatory nodes)
|
|
sp_tree.remove(pad_shape._element)
|
|
sp_tree.insert(2, pad_shape._element)
|
|
|
|
prs.save(dst)
|
|
return int(w1), int(h1)
|
|
|
|
|
|
def inspect_images(
|
|
paths: Sequence[str],
|
|
pad_ratio_w: float,
|
|
pad_ratio_h: float,
|
|
dpi: int,
|
|
) -> list[int]:
|
|
"""Return 1-based indices of slides that contain pixels outside the pad."""
|
|
|
|
tol = calc_tol(dpi)
|
|
failures: list[int] = []
|
|
pad_colour = np.array(PAD_RGB, dtype=np.uint8)
|
|
|
|
for idx, img_path in enumerate(paths, start=1):
|
|
with Image.open(img_path) as img:
|
|
rgb = img.convert("RGB")
|
|
arr = np.asarray(rgb)
|
|
|
|
h, w, _ = arr.shape
|
|
# Exclude the innermost 1-pixel band
|
|
pad_x = int(w * pad_ratio_w) - 1
|
|
pad_y = int(h * pad_ratio_h) - 1
|
|
|
|
left_margin = arr[:, :pad_x, :]
|
|
right_margin = arr[:, w - pad_x :, :]
|
|
top_margin = arr[:pad_y, :, :]
|
|
bottom_margin = arr[h - pad_y :, :, :]
|
|
|
|
def _is_clean(margin: np.ndarray) -> bool:
|
|
diff = np.abs(margin.astype(np.int16) - pad_colour)
|
|
matches = np.all(diff <= tol, axis=-1)
|
|
mismatch_fraction = 1.0 - (np.count_nonzero(matches) / matches.size)
|
|
if dpi >= 300:
|
|
max_mismatch = 0.01
|
|
elif dpi >= 200:
|
|
max_mismatch = 0.02
|
|
else:
|
|
max_mismatch = 0.03
|
|
return mismatch_fraction <= max_mismatch
|
|
|
|
if not (
|
|
_is_clean(left_margin)
|
|
and _is_clean(right_margin)
|
|
and _is_clean(top_margin)
|
|
and _is_clean(bottom_margin)
|
|
):
|
|
failures.append(idx)
|
|
|
|
return failures
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Check a PPTX for content overflowing the original canvas by rendering with padding "
|
|
"and inspecting the margins."
|
|
)
|
|
)
|
|
parser.add_argument(
|
|
"input_path",
|
|
type=str,
|
|
help="Path to the input PPTX file.",
|
|
)
|
|
parser.add_argument(
|
|
"--width",
|
|
type=int,
|
|
default=1600,
|
|
help=(
|
|
"Approximate maximum width in pixels after isotropic scaling (default 1600). "
|
|
"The actual value may exceed slightly."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--height",
|
|
type=int,
|
|
default=900,
|
|
help=(
|
|
"Approximate maximum height in pixels after isotropic scaling (default 900). "
|
|
"The actual value may exceed slightly."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--pad_px",
|
|
type=int,
|
|
default=PAD_PX,
|
|
help="Padding in pixels to add on each side before rasterization.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
input_path = abspath(expanduser(args.input_path))
|
|
# Width and height refer to the original, unaltered slide dimensions.
|
|
dpi = render_slides.calc_dpi_via_ooxml(input_path, args.width, args.height)
|
|
|
|
# Not using ``tempfile.TemporaryDirectory(delete=False)`` for Python 3.11 compatibility.
|
|
tmpdir = tempfile.mkdtemp()
|
|
enlarged_pptx = join(tmpdir, "enlarged.pptx")
|
|
pad_emu = px_to_emu(args.pad_px, dpi)
|
|
w1, h1 = enlarge_deck(input_path, enlarged_pptx, pad_emu=pad_emu)
|
|
pad_ratio_w = pad_emu / w1
|
|
pad_ratio_h = pad_emu / h1
|
|
|
|
img_dir = join(tmpdir, "imgs")
|
|
img_paths = render_slides.rasterize(enlarged_pptx, img_dir, dpi)
|
|
failing = inspect_images(img_paths, pad_ratio_w, pad_ratio_h, dpi)
|
|
|
|
if failing:
|
|
print(
|
|
"ERROR: Slides with content overflowing original canvas (1-based indexing): "
|
|
+ ", ".join(map(str, failing))
|
|
+ "\n"
|
|
+ "Rendered images with grey paddings for problematic slides are available at: "
|
|
)
|
|
for i in failing:
|
|
print(img_paths[i - 1])
|
|
else:
|
|
print("Test passed. No overflow detected.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|