Source code for todd.visuals.pptx

__all__ = [
    'PPTXVisual',
]

import io
from typing import Any

import cv2
import numpy as np
import numpy.typing as npt
import pptx
import pptx.dml.color
import pptx.enum.shapes
import pptx.enum.text
import pptx.parts.image
import pptx.presentation
import pptx.shapes.autoshape
import pptx.shapes.picture
import pptx.shapes.shapetree
import pptx.slide
import pptx.util

from ..bases.configs import Config
from ..colors import RGB, Color
from ..registries import VisualRegistry
from .base import BaseVisual


[docs] @VisualRegistry.register_() class PPTXVisual(BaseVisual): """Visualize data in the format of PowerPoint. The PowerPoint contains only one slide. For more details, refer to python-pptx_. .. _python-pptx: https://github.com/scanny/python-pptx """
[docs] def __init__(self, width: int, height: int, **kwargs) -> None: """Initialize the PowerPoint with a single slide. To initialize a PowerPoint with width 640pt and height 426pt, use the following code: >>> visual = PPTXVisual(640, 426) Once initialized, the width and height of the slide cannot be altered. We can read the width and height of the PowerPoint by: >>> visual.width 640 >>> visual.height 426 Args: width: the width of the PowerPoint in point height: the height of the PowerPoint in point """ self._presentation = pptx.Presentation(**kwargs) self.presentation.slide_width = pptx.util.Pt(width) self.presentation.slide_height = pptx.util.Pt(height) slides: pptx.slide.Slides = self.presentation.slides slides.add_slide(self.presentation.slide_layouts[6])
@property def width(self) -> int: """Width of the PowerPoint.""" width: pptx.util.Pt = self.presentation.slide_width return int(width.pt) @property def height(self) -> int: """Height of the PowerPoint.""" height: pptx.util.Pt = self.presentation.slide_height return int(height.pt) @property def presentation(self) -> pptx.presentation.Presentation: return self._presentation @property def slide(self) -> pptx.slide.Slide: return self.presentation.slides[0] @property def shapes(self) -> pptx.shapes.shapetree.SlideShapes: return self.slide.shapes @classmethod def _set_color_format_rgb( cls, cf: pptx.dml.color.ColorFormat, color: Color, ) -> None: cf.rgb = pptx.dml.color.RGBColor( *color.to(RGB).to_tuple(), )
[docs] def save(self, path: Any) -> None: """Save the PowerPoint. The save target can either be a filepath, for example: >>> import tempfile >>> with tempfile.NamedTemporaryFile() as f: ... PPTXVisual(640, 426).save(f.name) Or it can simply be a file-like object: >>> with tempfile.TemporaryFile() as f: ... PPTXVisual(640, 426).save(f) Args: path: destination path """ self.presentation.save(path)
[docs] def image( self, image: npt.NDArray[np.uint8], left: int = 0, top: int = 0, width: int | None = None, height: int | None = None, opacity: float = 1.0, ) -> pptx.shapes.picture.Picture: """Add an image to the PowerPoint. Suppose the image is :math:`(426, 640)`: >>> image = np.random.randint(0, 256, (426, 640, 3)) In most cases, the PowerPoint is of the same size as the image, so that the image covers the whole background: >>> h, w, _ = image.shape >>> visual = PPTXVisual(w, h) >>> visual.image(image) <pptx.shapes.picture.Picture object at ...> The returned `pptx.shapes.picture.Picture` object can be used to fine-tune the properties of the image. Note that the DPI of the image should not be changed thoughtlessly, because of the bizarre measurements in PowerPoint. The most common measurement unit in PowerPoint is *Point* (pt), where 1pt equals 1/72 inches. However, images are measured in pixels and 1 pixel equals 1/DPI inches. By default, DPI is 72 so that 1 pixel is 1pt. When DPI is set to other values, the size of the image may become larger or smaller than it is supposed to be. Args: image: :math:`(H, W, 3)` left: x coordinate of the left side of the image top: y coordinate of the top side of the image width: width of the image height: height of the image opacity: opacity of the image """ assert 0.0 <= opacity <= 1.0 h, w, c = image.shape assert c == 3 alpha = np.ones((h, w, 1)) * 255 * opacity image = np.concatenate([image, alpha], axis=-1) success, image = cv2.imencode('.png', image) assert success picture = self.shapes.add_picture( io.BytesIO(image.tobytes()), pptx.util.Pt(left), pptx.util.Pt(top), width if width is None else pptx.util.Pt(width), height if height is None else pptx.util.Pt(height), ) picture_image: pptx.parts.image.Image = picture.image assert picture_image.dpi == (72, 72) return picture
[docs] def rectangle( self, left: int, top: int, width: int, height: int, color: Color = RGB(0., 0., 0.), # noqa: B008 thickness: int = 1, fill: Color | None = None, ) -> pptx.shapes.autoshape.Shape: rectangle: pptx.shapes.autoshape.Shape = self.shapes.add_shape( pptx.enum.shapes.MSO_AUTO_SHAPE_TYPE.RECTANGLE, # noqa: E501 pylint: disable=no-member pptx.util.Pt(left), pptx.util.Pt(top), pptx.util.Pt(width), pptx.util.Pt(height), ) line: pptx.shapes.autoshape.LineFormat = rectangle.line line.width = pptx.util.Pt(thickness) self._set_color_format_rgb(line.color, color) rectangle_fill: pptx.shapes.autoshape.FillFormat = rectangle.fill if fill is None: rectangle_fill.background() else: rectangle_fill.solid() self._set_color_format_rgb(rectangle_fill.fore_color, fill) return rectangle
[docs] def text( self, text: str, x: int, y: int, color: Color = RGB(0., 0., 0.), # noqa: B008 font: Config | None = None, ) -> pptx.shapes.autoshape.Shape: if font is None: font = Config() textbox: pptx.shapes.autoshape.Shape = self.shapes.add_textbox( pptx.util.Pt(x), pptx.util.Pt(y), 1, 1, ) text_frame = textbox.text_frame text_frame.text = text text_frame.margin_left = 0 text_frame.margin_top = 0 text_frame.margin_right = 0 text_frame.margin_bottom = 0 paragraph_font = text_frame.paragraphs[0].font paragraph_font.name = font.get('name', 'Times New Roman') paragraph_font.size = pptx.util.Pt(font.get('size', 12)) self._set_color_format_rgb(paragraph_font.color, color) return textbox
[docs] def point( self, x: int, y: int, size: int, color: Color = RGB(0., 0., 0.), # noqa: B008 ) -> Any: raise NotImplementedError
[docs] def marker( self, x: int, y: int, size: int, color: Color = RGB(0., 0., 0.), # noqa: B008 ) -> Any: raise NotImplementedError
[docs] def line( self, x1: int, y1: int, x2: int, y2: int, color: Color = RGB(0., 0., 0.), # noqa: B008 thickness: int = 1, ) -> Any: raise NotImplementedError