AutoPage / utils /pptx_utils.py
Mqleet's picture
upd code
fcaa164
raw
history blame
73.3 kB
from pptx.enum.text import PP_ALIGN
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
from pptx.dml.color import RGBColor
from pptx.enum.dml import MSO_LINE_DASH_STYLE
from pptx.dml.color import RGBColor
from pptx.util import Pt
from pptx.oxml.xmlchemy import OxmlElement
from pptx.oxml.ns import qn
import json
add_border_label_function = r'''
from pptx.enum.shapes import MSO_SHAPE_TYPE, MSO_SHAPE, MSO_AUTO_SHAPE_TYPE
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
def pt_to_emu(points: float) -> int:
return int(points * 12700)
def emu_to_inches(emu: int) -> float:
return emu / 914400
def add_border_and_labels(
prs,
border_color=RGBColor(255, 0, 0), # Red border for shapes
border_width=Pt(2), # 2-point border width
label_outline_color=RGBColor(0, 0, 255), # Blue outline for label circle
label_text_color=RGBColor(0, 0, 255), # Blue text color
label_diameter_pt=40 # Diameter of the label circle in points
):
"""
Iterates over all slides and shapes in the Presentation 'prs', applies a
red border to each shape, and places a transparent (no fill), blue-outlined
circular label with a blue number in the center of each shape. Labels start
from 0 and increment for every shape that gets a border.
Args:
prs: The Presentation object to modify.
border_color: RGBColor for the shape border color (default: red).
border_width: The width of the shape border (Pt).
label_outline_color: The outline color for the label circle (default: blue).
label_text_color: The color of the label text (default: blue).
label_diameter_pt: The diameter of the label circle, in points (default: 40).
"""
label_diameter_emu = pt_to_emu(label_diameter_pt) # convert diameter (points) to EMUs
label_counter = 0 # Start labeling at 0
labeled_elements = {}
for slide in prs.slides:
for shape in slide.shapes:
# Skip shapes that are labels themselves
if shape.name.startswith("Label_"):
continue
try:
# --- 1) Add red border to the shape (if supported) ---
shape.line.fill.solid()
shape.line.fill.fore_color.rgb = border_color
shape.line.width = border_width
# --- 2) Calculate center for the label circle ---
label_left = shape.left + (shape.width // 2) - (label_diameter_emu // 2)
label_top = shape.top + (shape.height // 2) - (label_diameter_emu // 2)
# --- 3) Create label circle (an OVAL) in the center of the shape ---
label_shape = slide.shapes.add_shape(
MSO_AUTO_SHAPE_TYPE.OVAL,
label_left,
label_top,
label_diameter_emu,
label_diameter_emu
)
label_shape.name = f"Label_{label_counter}" # so we can skip it later
# **Make the circle completely transparent** (no fill at all)
label_shape.fill.background()
# **Give it a blue outline**
label_shape.line.fill.solid()
label_shape.line.fill.fore_color.rgb = label_outline_color
label_shape.line.width = Pt(3)
# --- 4) Add the label number (centered, blue text) ---
tf = label_shape.text_frame
tf.text = str(label_counter)
paragraph = tf.paragraphs[0]
paragraph.alignment = PP_ALIGN.CENTER
run = paragraph.runs[0]
font = run.font
font.size = Pt(40) # Larger font
font.bold = True
font.name = "Arial"
font._element.get_or_change_to_solidFill()
font.fill.fore_color.rgb = label_text_color
# Record properties from the original shape and label text.
labeled_elements[label_counter] = {
'left': f'{emu_to_inches(shape.left)} Inches',
'top': f'{emu_to_inches(shape.top)} Inches',
'width': f'{emu_to_inches(shape.width)} Inches',
'height': f'{emu_to_inches(shape.height)} Inches',
'font_size': f'{shape.text_frame.font.size} PT' if hasattr(shape, 'text_frame') else None,
}
# --- 5) Increment label counter (so every shape has a unique label) ---
label_counter += 1
except Exception as e:
# If the shape doesn't support borders or text, skip gracefully
print(f"Could not add border/label to shape (type={shape.shape_type}): {e}")
return labeled_elements
'''
add_border_function = r'''
from pptx.enum.shapes import MSO_SHAPE_TYPE, MSO_SHAPE, MSO_AUTO_SHAPE_TYPE
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
def emu_to_inches(emu: int) -> float:
return emu / 914400
def add_border(
prs,
border_color=RGBColor(255, 0, 0), # Red border for shapes
border_width=Pt(2), # 2-point border width
):
"""
Iterates over all slides and shapes in the Presentation 'prs', applies a
red border to each shape, and places a transparent (no fill).
Args:
prs: The Presentation object to modify.
border_color: RGBColor for the shape border color (default: red).
border_width: The width of the shape border (Pt).
"""
labeled_elements = {}
for slide in prs.slides:
for shape in slide.shapes:
try:
# --- 1) Add red border to the shape (if supported) ---
shape.line.fill.solid()
shape.line.fill.fore_color.rgb = border_color
shape.line.width = border_width
if hasattr(shape, 'name'):
labeled_elements[shape.name] = {
'left': f'{emu_to_inches(shape.left)} Inches',
'top': f'{emu_to_inches(shape.top)} Inches',
'width': f'{emu_to_inches(shape.width)} Inches',
'height': f'{emu_to_inches(shape.height)} Inches',
}
except Exception as e:
# If the shape doesn't support borders or text, skip gracefully
print(f"Could not add border to shape (type={shape.shape_type}): {e}")
return labeled_elements
'''
create_id_map_function = r'''
def create_element_id_map(presentation):
"""
Given a python-pptx Presentation object, this function creates
and returns a dictionary mapping each element's (shape's) unique id
to a sequential integer starting from 0.
Parameters:
presentation (Presentation): A python-pptx Presentation object.
Returns:
dict: A dictionary with keys as element IDs (integers) and values as sequential integers.
"""
element_id_map = {}
counter = 0
# Iterate over each slide in the presentation
for slide in presentation.slides:
# Iterate over each shape (element) on the slide
for shape in slide.shapes:
if hasattr(shape, "name"):
element_id_map[counter] = shape.name
counter += 1
return element_id_map
'''
save_helper_info_border_label = r'''
location_info = add_border_and_labels(poster, label_diameter_pt=80)
id_map = create_element_id_map(poster)
import json
with open('{}_element_id_map.json', 'w') as f:
json.dump(id_map, f)
with open('{}_location_info.json', 'w') as f:
json.dump(location_info, f)
poster.save("{}_bordered.pptx")
'''
save_helper_info_border = r'''
location_info = add_border(poster)
import json
with open('{}_location_info.json', 'w') as f:
json.dump(location_info, f)
poster.save("{}_bordered.pptx")
'''
utils_functions = r'''
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
from pptx.dml.color import RGBColor
from pptx.enum.dml import MSO_LINE_DASH_STYLE
from pptx.dml.color import RGBColor
from pptx.util import Pt
from pptx.oxml.xmlchemy import OxmlElement
from pptx.oxml.ns import qn
import pptx
import json
from pptx.enum.text import MSO_AUTO_SIZE
def emu_to_inches(emu: int) -> float:
return emu / 914400
def _px_to_pt(px):
"""
Approximate conversion from pixels to points.
A common assumption is 1px ~ 0.75pt.
Adjust as needed for your environment.
"""
return px * 0.75
def _parse_font_size(font_size):
"""
Internal helper to convert a numeric font size (e.g., 12)
to a python-pptx Pt object. If it's already a Pt, return as-is.
"""
if font_size is None:
return None
if isinstance(font_size, (int, float)):
return Pt(font_size)
return font_size # Assume user provided a Pt object already
def _parse_alignment(alignment):
"""
Internal helper to convert a string alignment (e.g., "left", "center")
to the corresponding PP_ALIGN constant.
Default to PP_ALIGN.LEFT if unrecognized or None.
"""
if not isinstance(alignment, str):
# If user passed None or something else, default to PP_ALIGN.LEFT
return PP_ALIGN.LEFT
alignment = alignment.lower().strip()
alignment_map = {
"left": PP_ALIGN.LEFT,
"center": PP_ALIGN.CENTER,
"right": PP_ALIGN.RIGHT,
"justify": PP_ALIGN.JUSTIFY,
}
return alignment_map.get(alignment, PP_ALIGN.LEFT)
def create_poster(width_inch=48, height_inch=36):
"""
Create a new Presentation object, set its slide size (e.g., 48x36 inches).
:param width_inch: Float or int specifying width in inches (default 48).
:param height_inch: Float or int specifying height in inches (default 36).
:return: A python-pptx Presentation object.
"""
prs = Presentation()
prs.slide_width = Inches(width_inch)
prs.slide_height = Inches(height_inch)
return prs
def add_blank_slide(prs):
"""
Add a blank slide to the Presentation (layout index 6 is typically blank).
:param prs: The Presentation object to add a slide to.
:return: The newly added slide object.
"""
blank_layout = prs.slide_layouts[6]
return prs.slides.add_slide(blank_layout)
def shape_fill_color(shape, fill_color):
"""
Set the fill color of a shape to the specified RGB color.
:param shape: The shape object to modify.
:param fill_color: A tuple (r, g, b) for the fill color.
"""
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(*fill_color)
def add_textbox(
slide,
name,
left_inch,
top_inch,
width_inch,
height_inch,
text="",
word_wrap=True,
font_size=40,
bold=False,
italic=False,
alignment="left",
fill_color=None,
font_name="Arial"
):
"""
Create a textbox shape on the given slide, optionally fill its background with
a color if fill_color is specified as (r, g, b).
:param slide: Slide object to place the textbox on.
:param name: Name for the shape (shape.name).
:param left_inch: Left coordinate (in inches).
:param top_inch: Top coordinate (in inches).
:param width_inch: Width (in inches).
:param height_inch: Height (in inches).
:param text: Text to display in the textbox.
:param word_wrap: If True, wrap text in the textbox.
:param font_size: Numeric font size (e.g. 40).
:param bold: Boolean to set run.font.bold.
:param italic: Boolean to set run.font.italic.
:param alignment: String alignment: "left", "center", "right", or "justify".
:param fill_color: (r, g, b) tuple for solid fill background color, or None to skip.
:param font_name: String font name (e.g., "Arial").
:return: The newly created textbox shape.
"""
shape = slide.shapes.add_textbox(
Inches(left_inch), Inches(top_inch),
Inches(width_inch), Inches(height_inch)
)
shape.name = name
# If a fill color is specified, apply a solid fill
if fill_color is not None:
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(*fill_color)
else:
# Otherwise, set "no fill" if you want it transparent
shape.fill.background()
text_frame = shape.text_frame
# Turn off auto-size to ensure stable font size, etc.
text_frame.auto_size = MSO_AUTO_SIZE.NONE
text_frame.word_wrap = word_wrap
# Clear any default paragraphs
text_frame.clear()
# Add a new paragraph
p = text_frame.add_paragraph()
# Instead of setting p.text, explicitly create a Run
run = p.add_run()
run.text = text
# Parse alignment and set it
p.alignment = _parse_alignment(alignment)
# Set the font formatting on the run
font = run.font
font.size = _parse_font_size(font_size)
font.bold = bold
font.italic = italic
font.name = font_name
return shape
def edit_textbox(
shape,
text=None,
word_wrap=None,
font_size=None,
bold=None,
italic=None,
alignment=None,
fill_color=None,
font_name=None
):
"""
Edit properties of an existing textbox shape.
:param shape: The shape object (textbox) to edit.
:param text: New text to set. If None, leaves text unmodified.
:param word_wrap: Boolean to enable/disable word wrap. If None, leaves unmodified.
:param font_size: Font size (int/float or string like '12pt'). If None, leaves unmodified.
:param bold: Boolean to set bold. If None, leaves unmodified.
:param italic: Boolean to set italic. If None, leaves unmodified.
:param alignment: One of 'left', 'center', 'right', 'justify'. If None, leaves unmodified.
:param fill_color: A tuple (r, g, b) for background fill color, or None to leave unmodified.
"""
text_frame = shape.text_frame
text_frame.auto_size = MSO_AUTO_SIZE.NONE
# Update fill color if provided
if fill_color is not None:
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(*fill_color)
# else: If you'd like to remove any existing fill if None, you could:
# else:
# shape.fill.background()
# Update word wrap if provided
if word_wrap is not None:
text_frame.word_wrap = word_wrap
# If text is provided, clear existing paragraphs and add the new text
if text is not None:
text_frame.clear()
p = text_frame.add_paragraph()
run = p.add_run()
run.text = text
# If alignment is provided, apply to the paragraph
if alignment is not None:
p.alignment = _parse_alignment(alignment)
# If font formatting info is provided, apply to the run font
font = run.font
if font_size is not None:
font.size = _parse_font_size(font_size)
if bold is not None:
font.bold = bold
if italic is not None:
font.italic = italic
else:
# If no new text is given, we can selectively change existing text properties.
for p in text_frame.paragraphs:
if alignment is not None:
p.alignment = _parse_alignment(alignment)
for run in p.runs:
font = run.font
if font_size is not None:
font.size = _parse_font_size(font_size)
if bold is not None:
font.bold = bold
if italic is not None:
font.italic = italic
if font_name is not None:
font.name = font_name
def add_image(slide, name, left_inch, top_inch, width_inch, height_inch, image_path):
"""
Add an image to the slide at the specified position and size.
:param slide: The slide object where the image should be placed.
:param name: A string name/label for the shape.
:param left_inch: Left position in inches.
:param top_inch: Top position in inches.
:param width_inch: Width in inches.
:param height_inch: Height in inches.
:param image_path: File path to the image.
:return: The newly created picture shape object.
"""
shape = slide.shapes.add_picture(
image_path,
Inches(left_inch), Inches(top_inch),
width=Inches(width_inch), height=Inches(height_inch)
)
shape.name = name
return shape
def set_shape_position(shape, left_inch, top_inch, width_inch, height_inch):
"""
Move or resize an existing shape to the specified position/dimensions.
:param shape: The shape object to be repositioned.
:param left_inch: New left position in inches.
:param top_inch: New top position in inches.
:param width_inch: New width in inches.
:param height_inch: New height in inches.
"""
shape.left = Inches(left_inch)
shape.top = Inches(top_inch)
shape.width = Inches(width_inch)
shape.height = Inches(height_inch)
def add_line_simple(slide, name, left_inch, top_inch, length_inch, thickness=2, color=(0, 0, 0), orientation="horizontal"):
"""
Add a simple horizontal or vertical line to the slide.
Parameters:
slide: The slide object.
name: The name/label for the line shape.
left_inch: The left (X) coordinate in inches for the starting point.
top_inch: The top (Y) coordinate in inches for the starting point.
length_inch: The length of the line in inches.
thickness: The thickness of the line in points (default is 2).
color: An (R, G, B) tuple specifying the line color (default is black).
orientation: "horizontal" or "vertical" (case-insensitive).
Returns:
The created line shape object.
"""
x1 = Inches(left_inch)
y1 = Inches(top_inch)
if orientation.lower() == "horizontal":
x2 = Inches(left_inch + length_inch)
y2 = y1
elif orientation.lower() == "vertical":
x2 = x1
y2 = Inches(top_inch + length_inch)
else:
raise ValueError("Orientation must be either 'horizontal' or 'vertical'")
# Create a straight connector (used as a line)
line_shape = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, x1, y1, x2, y2)
line_shape.name = name
# Set the line thickness and color
line_shape.line.width = Pt(thickness)
line_shape.line.color.rgb = RGBColor(*color)
return line_shape
def set_paragraph_line_spacing(shape, line_spacing=1.0):
"""
Set line spacing for all paragraphs in a textbox shape.
E.g., line_spacing=1.5 for 1.5x spacing, 2 for double spacing, etc.
:param shape: The textbox shape to modify.
:param line_spacing: A float indicating multiple of single spacing.
"""
text_frame = shape.text_frame
for paragraph in text_frame.paragraphs:
paragraph.line_spacing = line_spacing # direct float: 1.5, 2.0, etc.
def set_shape_text_margins(
shape,
top_px=0,
right_px=0,
bottom_px=0,
left_px=0
):
"""
Set the internal text margins (like "padding") for a textbox shape.
python-pptx uses points or EMUs for margins, so we convert from px -> points -> EMUs as needed.
Note: If your output environment uses a different PX:PT ratio, adjust _px_to_pt().
"""
text_frame = shape.text_frame
text_frame.auto_size = MSO_AUTO_SIZE.NONE
text_frame.margin_top = Pt(_px_to_pt(top_px))
text_frame.margin_right = Pt(_px_to_pt(right_px))
text_frame.margin_bottom = Pt(_px_to_pt(bottom_px))
text_frame.margin_left = Pt(_px_to_pt(left_px))
def adjust_font_size(shape, delta=2):
"""
Increase or decrease the current font size of all runs in a shape by `delta` points.
If a run has no explicitly set font size (font.size is None), we can either skip it or assume a default.
For simplicity, let's skip runs without an explicit size to avoid overwriting theme defaults.
:param shape: The textbox shape to update.
:param delta: Positive or negative integer to adjust the font size.
"""
text_frame = shape.text_frame
text_frame.auto_size = MSO_AUTO_SIZE.NONE
for paragraph in text_frame.paragraphs:
for run in paragraph.runs:
current_size = run.font.size
if current_size is not None:
new_size = current_size.pt + delta
# Prevent negative or zero font size
if new_size < 1:
new_size = 1
run.font.size = Pt(new_size)
def center_shape_horizontally(prs, shape):
"""
Center a shape horizontally on the slide using the presentation's slide width.
:param prs: The Presentation object (which holds slide_width).
:param shape: The shape to center.
"""
new_left = (prs.slide_width - shape.width) // 2
shape.left = new_left
def center_shape_vertically(prs, shape):
"""
Center a shape vertically on the slide using the presentation's slide height.
:param prs: The Presentation object (which holds slide_height).
:param shape: The shape to center.
"""
new_top = (prs.slide_height - shape.height) // 2
shape.top = new_top
def set_shape_text(shape, text, clear_first=True):
"""
Set or replace the text of an existing shape (commonly a textbox).
:param shape: The shape (textbox) whose text needs to be updated.
:param text: The new text content.
:param clear_first: Whether to clear existing paragraphs before adding.
"""
text_frame = shape.text_frame
text_frame.auto_size = MSO_AUTO_SIZE.NONE
if clear_first:
text_frame.clear()
p = text_frame.add_paragraph()
p.text = text
def _set_run_font_color(run, rgb_tuple):
"""
Manually create or replace the solidFill element in this run's XML
to force the color if run.font.color is None or doesn't exist yet.
"""
# Underlying run properties element
rPr = run.font._element
# Remove any existing <a:solidFill> elements to avoid duplicates
for child in rPr.iterchildren():
if child.tag == qn('a:solidFill'):
rPr.remove(child)
# Create a new solidFill element with the specified color
solid_fill = OxmlElement('a:solidFill')
srgb_clr = OxmlElement('a:srgbClr')
# Format the tuple (r, g, b) into a hex string "RRGGBB"
srgb_clr.set('val', '{:02X}{:02X}{:02X}'.format(*rgb_tuple))
solid_fill.append(srgb_clr)
rPr.append(solid_fill)
def set_text_style(shape, font_size=None, bold=None, italic=None, alignment=None, color=None, font_name=None):
"""
Adjust text style on an existing textbox shape.
:param shape: The textbox shape whose style is being updated.
:param font_size: Numeric font size (e.g. 40) or None to skip.
:param bold: Boolean or None (to skip).
:param italic: Boolean or None (to skip).
:param alignment: String alignment ('left', 'center', 'right', 'justify') or None (to skip).
:param color: A tuple (r, g, b), each int from 0-255, or None (to skip).
:param font_name: String font name (e.g., 'Arial') or None
"""
text_frame = shape.text_frame
# Disable auto-sizing so our manual settings are respected
text_frame.auto_size = MSO_AUTO_SIZE.NONE
# Convert the alignment string into a PP_ALIGN enum value
parsed_alignment = _parse_alignment(alignment) if alignment else None
# Convert the raw font size to a python-pptx Pt object
parsed_font_size = _parse_font_size(font_size)
# Iterate over paragraphs and runs in the shape
for paragraph in text_frame.paragraphs:
if parsed_alignment is not None:
paragraph.alignment = parsed_alignment
for run in paragraph.runs:
# Font size
if parsed_font_size is not None:
run.font.size = parsed_font_size
# Bold
if bold is not None:
run.font.bold = bold
# Italic
if italic is not None:
run.font.italic = italic
# Font name
if font_name is not None:
run.font.name = font_name
# Color
if color is not None:
# Sometimes run.font.color may be None. We can try:
if run.font.color is not None:
# If a ColorFormat object already exists, just set it
run.font.color.rgb = RGBColor(*color)
else:
# Otherwise, manually set the run color in the underlying XML
_set_run_font_color(run, color)
def save_presentation(prs, file_name="poster.pptx"):
"""
Save the current Presentation object to disk.
:param prs: The Presentation object.
:param file_name: The file path/name for the saved pptx file.
"""
prs.save(file_name)
def set_slide_background_color(slide, rgb=(255, 255, 255)):
"""
Sets the background color for a single Slide object.
:param slide: A pptx.slide.Slide object
:param rgb: A tuple of (R, G, B) color values, e.g. (255, 0, 0) for red
"""
bg_fill = slide.background.fill
bg_fill.solid()
bg_fill.fore_color.rgb = RGBColor(*rgb)
def style_shape_border(shape, color=(30, 144, 255), thickness=2, line_style="square_dot"):
"""
Applies a border (line) style to a given shape, where line_style is a
string corresponding to an MSO_LINE_DASH_STYLE enum value from python-pptx.
Valid line_style strings (based on the doc snippet) are:
-----------------------------------------------------------------
'solid' -> MSO_LINE_DASH_STYLE.SOLID
'round_dot' -> MSO_LINE_DASH_STYLE.ROUND_DOT
'square_dot' -> MSO_LINE_DASH_STYLE.SQUARE_DOT
'dash' -> MSO_LINE_DASH_STYLE.DASH
'dash_dot' -> MSO_LINE_DASH_STYLE.DASH_DOT
'dash_dot_dot' -> MSO_LINE_DASH_STYLE.DASH_DOT_DOT
'long_dash' -> MSO_LINE_DASH_STYLE.LONG_DASH
'long_dash_dot'-> MSO_LINE_DASH_STYLE.LONG_DASH_DOT
-----------------------------------------------------------------
:param shape: pptx.shapes.base.Shape object to style
:param color: A tuple (R, G, B) for the border color (default is (30, 144, 255))
:param thickness: Border thickness in points (default is 2)
:param line_style:String representing the line dash style; defaults to 'square_dot'
"""
# Map our string keys to MSO_LINE_DASH_STYLE values from your doc snippet
dash_style_map = {
"solid": MSO_LINE_DASH_STYLE.SOLID,
"round_dot": MSO_LINE_DASH_STYLE.ROUND_DOT,
"square_dot": MSO_LINE_DASH_STYLE.SQUARE_DOT,
"dash": MSO_LINE_DASH_STYLE.DASH,
"dash_dot": MSO_LINE_DASH_STYLE.DASH_DOT,
"dash_dot_dot": MSO_LINE_DASH_STYLE.DASH_DOT_DOT,
"long_dash": MSO_LINE_DASH_STYLE.LONG_DASH,
"long_dash_dot": MSO_LINE_DASH_STYLE.LONG_DASH_DOT
}
line = shape.line
line.width = Pt(thickness)
line.color.rgb = RGBColor(*color)
# Default to 'solid' if the requested style isn't in dash_style_map
dash_style_enum = dash_style_map.get(line_style.lower(), MSO_LINE_DASH_STYLE.SOLID)
line.dash_style = dash_style_enum
def fill_textframe(shape, paragraphs_spec):
"""
Given an existing shape (with a text frame) and a paragraphs_spec
describing paragraphs and runs, populate the shape’s text frame.
'paragraphs_spec' is a list of paragraphs, each containing:
- bullet: bool
- level: int (indent level)
- alignment: str ("left", "center", "right", or "justify")
- font_size: int
- runs: list of run dictionaries, each with:
text: str
bold: bool
italic: bool
color: [r,g,b] or None
font_size: int (optional, overrides paragraph default)
fill_color: [r,g,b] or None
"""
text_frame = shape.text_frame
# Ensure stable layout
text_frame.auto_size = MSO_AUTO_SIZE.NONE
text_frame.word_wrap = True
# Clear out existing paragraphs
text_frame.clear()
for p_data in paragraphs_spec:
p = text_frame.add_paragraph()
# # bulleting
# p.bullet = p_data.get("bullet", False)
# bullet level (indent)
p.level = p_data.get("level", 0)
# paragraph alignment
align_str = p_data.get("alignment", "left")
p.alignment = _parse_alignment(align_str)
# paragraph-level font size
default_font_size = p_data.get("font_size", 24)
p.font.size = Pt(default_font_size)
# Add runs
runs_spec = p_data.get("runs", [])
for run_info in runs_spec:
run = p.add_run()
if p_data.get("bullet", False):
if p.level == 0:
run.text = '\u2022' + run_info.get("text", "")
elif p.level == 1:
run.text = '\u25E6' + run_info.get("text", "")
else:
run.text = '\u25AA' + run_info.get("text", "")
else:
run.text = run_info.get("text", "")
# Font styling
font = run.font
font.bold = run_info.get("bold", False)
font.italic = run_info.get("italic", False)
# If run-specific color was provided
color_tuple = run_info.get("color", None)
if (
color_tuple
and len(color_tuple) == 3
and all(isinstance(c, int) for c in color_tuple)
):
if run.font.color is not None:
# If a ColorFormat object already exists, just set it
run.font.color.rgb = RGBColor(*color_tuple)
else:
# Otherwise, manually set the run color in the underlying XML
_set_run_font_color(run, color_tuple)
# If run-specific font size was provided
if "font_size" in run_info:
font.size = Pt(run_info["font_size"])
# If run-specific shape fill color was provided:
fill_color_tuple = run_info.get("fill_color", None)
if (
fill_color_tuple
and len(fill_color_tuple) == 3
and all(isinstance(c, int) for c in fill_color_tuple)
):
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(*fill_color_tuple)
def add_border_hierarchy(
prs,
name_to_hierarchy: dict,
hierarchy: int,
border_color=RGBColor(255, 0, 0),
border_width=2,
fill_boxes: bool = False,
fill_color=RGBColor(255, 0, 0),
regardless=False
):
"""
Iterates over all slides and shapes in the Presentation 'prs'.
- For shapes whose name maps to the given 'hierarchy' in 'name_to_hierarchy' (or if 'regardless'
is True), draws a red border. Optionally fills the shape with red if 'fill_boxes' is True.
- For all other shapes, removes their border and hides any text.
Returns:
labeled_elements: dict of shape geometry for ALL shapes, regardless of hierarchy match.
"""
border_width = Pt(border_width)
labeled_elements = {}
for slide_idx, slide in enumerate(prs.slides):
for shape_idx, shape in enumerate(slide.shapes):
# Record basic geometry in labeled_elements
shape_name = shape.name if hasattr(shape, 'name') else f"Shape_{slide_idx}_{shape_idx}"
labeled_elements[shape_name] = {
'left': f"{emu_to_inches(shape.left):.2f} Inches",
'top': f"{emu_to_inches(shape.top):.2f} Inches",
'width': f"{emu_to_inches(shape.width):.2f} Inches",
'height': f"{emu_to_inches(shape.height):.2f} Inches",
}
# Determine if this shape should have a border
current_hierarchy = name_to_hierarchy.get(shape_name, None)
if current_hierarchy is None:
# Optional: Print a debug message if the shape’s name isn’t in the dict
print(f"Warning: shape '{shape_name}' not found in name_to_hierarchy.")
try:
if current_hierarchy == hierarchy or regardless:
# Draw border
shape.line.fill.solid()
shape.line.fill.fore_color.rgb = border_color
shape.line.width = border_width
# Optionally fill the shape with red color
if fill_boxes:
shape.fill.solid()
shape.fill.fore_color.rgb = fill_color
else:
# Remove border
shape.line.width = Pt(0)
shape.line.fill.background()
# Hide text if present
if shape.has_text_frame:
shape.text_frame.text = ""
except Exception as e:
print(f"Could not process shape '{shape_name}' (type={shape.shape_type}): {e}")
return labeled_elements
def get_visual_cues(name_to_hierarchy, identifier, poster_path='poster.pptx'):
prs = pptx.Presentation(poster_path)
position_dict_1 = add_border_hierarchy(prs, name_to_hierarchy, 1, border_width=10)
json.dump(position_dict_1, open(f"tmp/position_dict_1_<{identifier}>.json", "w"))
# Save the presentation to disk.
save_presentation(prs, file_name=f"tmp/poster_<{identifier}>_hierarchy_1.pptx")
prs = pptx.Presentation(poster_path)
add_border_hierarchy(prs, name_to_hierarchy, 1, border_width=10, fill_boxes=True)
save_presentation(prs, file_name=f"tmp/poster_<{identifier}>_hierarchy_1_filled.pptx")
prs = pptx.Presentation(poster_path)
position_dict_2 = add_border_hierarchy(prs, name_to_hierarchy, 2, border_width=10)
json.dump(position_dict_2, open(f"tmp/position_dict_2_<{identifier}>.json", "w"))
# Save the presentation to disk.
save_presentation(prs, file_name=f"tmp/poster_<{identifier}>_hierarchy_2.pptx")
prs = pptx.Presentation(poster_path)
add_border_hierarchy(prs, name_to_hierarchy, 2, border_width=10, fill_boxes=True)
# Save the presentation to disk.
save_presentation(prs, file_name=f"tmp/poster_<{identifier}>_hierarchy_2_filled.pptx")
'''
documentation = r'''
create_poster(width_inch=48, height_inch=36):
"""
Create a new Presentation object, set its slide size (e.g., 48x36 inches).
:param width_inch: Float or int specifying width in inches (default 48).
:param height_inch: Float or int specifying height in inches (default 36).
:return: A python-pptx Presentation object.
"""
add_blank_slide(prs):
"""
Add a blank slide to the Presentation (layout index 6 is typically blank).
:param prs: The Presentation object to add a slide to.
:return: The newly added slide object.
"""
def shape_fill_color(shape, fill_color):
"""
Set the fill color of a shape to the specified RGB color.
:param shape: The shape object to modify.
:param fill_color: A tuple (r, g, b) for the fill color.
"""
def add_textbox(
slide,
name,
left_inch,
top_inch,
width_inch,
height_inch,
text="",
word_wrap=True,
font_size=40,
bold=False,
italic=False,
alignment="left",
fill_color=None,
font_name="Arial"
):
"""
Create a textbox shape on the given slide, optionally fill its background with
a color if fill_color is specified as (r, g, b).
:param slide: Slide object to place the textbox on.
:param name: Name for the shape (shape.name).
:param left_inch: Left coordinate (in inches).
:param top_inch: Top coordinate (in inches).
:param width_inch: Width (in inches).
:param height_inch: Height (in inches).
:param text: Text to display in the textbox.
:param word_wrap: If True, wrap text in the textbox.
:param font_size: Numeric font size (e.g. 40).
:param bold: Boolean to set run.font.bold.
:param italic: Boolean to set run.font.italic.
:param alignment: String alignment: "left", "center", "right", or "justify".
:param fill_color: (r, g, b) tuple for solid fill background color, or None to skip.
:param font_name: String font name (e.g., "Arial").
:return: The newly created textbox shape.
"""
add_image(slide, name, left_inch, top_inch, width_inch, height_inch, image_path):
"""
Add an image to the slide at the specified position and size.
:param slide: The slide object where the image should be placed.
:param name: A string name/label for the shape.
:param left_inch: Left position in inches.
:param top_inch: Top position in inches.
:param width_inch: Width in inches.
:param height_inch: Height in inches.
:param image_path: File path to the image.
:return: The newly created picture shape object.
"""
set_shape_position(shape, left_inch, top_inch, width_inch, height_inch):
"""
Move or resize an existing shape to the specified position/dimensions.
:param shape: The shape object to be repositioned.
:param left_inch: New left position in inches.
:param top_inch: New top position in inches.
:param width_inch: New width in inches.
:param height_inch: New height in inches.
"""
def set_text_style(shape, font_size=None, bold=None, italic=None, alignment=None, color=None, font_name=None):
"""
Adjust text style on an existing textbox shape.
:param shape: The textbox shape whose style is being updated.
:param font_size: Numeric font size (e.g. 40) or None to skip.
:param bold: Boolean or None (to skip).
:param italic: Boolean or None (to skip).
:param alignment: String alignment ('left', 'center', 'right', 'justify') or None (to skip).
:param color: A tuple (r, g, b), each int from 0-255, or None (to skip).
:param font_name: String font name (e.g., 'Arial') or None
"""
add_line_simple(slide, name, left_inch, top_inch, length_inch, thickness=2, color=(0, 0, 0), orientation="horizontal"):
"""
Add a simple horizontal or vertical line to the slide.
Parameters:
slide: The slide object.
name: The name/label for the line shape.
left_inch: The left (X) coordinate in inches for the starting point.
top_inch: The top (Y) coordinate in inches for the starting point.
length_inch: The length of the line in inches.
thickness: The thickness of the line in points (default is 2).
color: An (R, G, B) tuple specifying the line color (default is black).
orientation: "horizontal" or "vertical" (case-insensitive).
Returns:
The created line shape object.
"""
set_paragraph_line_spacing(shape, line_spacing=1.0):
"""
Set line spacing for all paragraphs in a textbox shape.
E.g., line_spacing=1.5 for 1.5x spacing, 2 for double spacing, etc.
:param shape: The textbox shape to modify.
:param line_spacing: A float indicating multiple of single spacing.
"""
set_shape_text_margins(
shape,
top_px=0,
right_px=0,
bottom_px=0,
left_px=0
):
"""
Set the internal text margins (like "padding") for a textbox shape.
python-pptx uses points or EMUs for margins, so we convert from px -> points -> EMUs as needed.
Note: If your output environment uses a different PX:PT ratio, adjust _px_to_pt().
"""
adjust_font_size(shape, delta=2):
"""
Increase or decrease the current font size of all runs in a shape by `delta` points.
If a run has no explicitly set font size (font.size is None), we can either skip it or assume a default.
For simplicity, let's skip runs without an explicit size to avoid overwriting theme defaults.
:param shape: The textbox shape to update.
:param delta: Positive or negative integer to adjust the font size.
"""
def set_slide_background_color(slide, rgb=(255, 255, 255)):
"""
Sets the background color for a single Slide object.
:param slide: A pptx.slide.Slide object
:param rgb: A tuple of (R, G, B) color values, e.g. (255, 0, 0) for red
"""
def style_shape_border(shape, color=(30, 144, 255), thickness=2, line_style="square_dot"):
"""
Applies a border (line) style to a given shape, where line_style is a
string corresponding to an MSO_LINE_DASH_STYLE enum value from python-pptx.
Valid line_style strings (based on the doc snippet) are:
-----------------------------------------------------------------
'solid' -> MSO_LINE_DASH_STYLE.SOLID
'round_dot' -> MSO_LINE_DASH_STYLE.ROUND_DOT
'square_dot' -> MSO_LINE_DASH_STYLE.SQUARE_DOT
'dash' -> MSO_LINE_DASH_STYLE.DASH
'dash_dot' -> MSO_LINE_DASH_STYLE.DASH_DOT
'dash_dot_dot' -> MSO_LINE_DASH_STYLE.DASH_DOT_DOT
'long_dash' -> MSO_LINE_DASH_STYLE.LONG_DASH
'long_dash_dot'-> MSO_LINE_DASH_STYLE.LONG_DASH_DOT
-----------------------------------------------------------------
:param shape: pptx.shapes.base.Shape object to style
:param color: A tuple (R, G, B) for the border color (default is (30, 144, 255))
:param thickness: Border thickness in points (default is 2)
:param line_style:String representing the line dash style; defaults to 'square_dot'
"""
save_presentation(prs, file_name="poster.pptx"):
"""
Save the current Presentation object to disk.
:param prs: The Presentation object.
:param file_name: The file path/name for the saved pptx file.
"""
--------------------------------------
Example usage:
poster = create_poster(width_inch=48, height_inch=36)
slide = add_blank_slide(poster)
# Set this particular slide's background to light gray
set_slide_background_color(slide, (200, 200, 200))
title_text_box = add_textbox(
slide,
name='title',
left_inch=5,
top_inch=0,
width_inch=30,
height_inch=5,
text="Poster Title",
word_wrap=True,
font_size=100,
bold=True,
italic=False,
alignment="center",
fill_color=(255, 255, 255), # Fill color
font_name="Arial"
)
shape_fill_color(title_text_box, fill_color=(173, 216, 230)) # Fill color
# Apply a dashed border with "square_dot"
style_shape_border(title_text_box, color=(30, 144, 255), thickness=8, line_style="square_dot")
image = add_image(slide, 'img', 10, 25, 30, 30, 'data/poster_exp/pdf/attention/_page_3_Figure_0.jpeg')
set_shape_position(image, 10, 25, 15, 15)
set_shape_position(image, 10, 5, 20, 15)
set_text_style(title_text_box, font_size=60, bold=True, italic=True, alignment='center', color=(255, 0, 0), font_name='Times New Roman')
added_line = add_line_simple(
slide,
'separation_line',
20,
0,
20,
thickness=2, # in points
color=(120, 120, 20),
orientation='vertical'
)
set_shape_text_margins(
title_text_box,
top_px=10,
right_px=20,
bottom_px=30,
left_px=40
)
adjust_font_size(title_text_box, delta=-20)
set_paragraph_line_spacing(title_text_box, line_spacing=2.0)
save_presentation(poster, file_name="poster.pptx")
'''
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
from pptx.dml.color import RGBColor
import pptx
from pptx.enum.text import MSO_AUTO_SIZE
def emu_to_inches(emu: int) -> float:
return emu / 914400
def _px_to_pt(px):
"""
Approximate conversion from pixels to points.
A common assumption is 1px ~ 0.75pt.
Adjust as needed for your environment.
"""
return px * 0.75
def _parse_font_size(font_size):
"""
Internal helper to convert a numeric font size (e.g., 12)
to a python-pptx Pt object. If it's already a Pt, return as-is.
"""
if font_size is None:
return None
if isinstance(font_size, (int, float)):
return Pt(font_size)
return font_size # Assume user provided a Pt object already
def _parse_alignment(alignment):
"""
Internal helper to convert a string alignment (e.g., "left", "center")
to the corresponding PP_ALIGN constant.
Default to PP_ALIGN.LEFT if unrecognized or None.
"""
if not isinstance(alignment, str):
# If user passed None or something else, default to PP_ALIGN.LEFT
return PP_ALIGN.LEFT
alignment = alignment.lower().strip()
alignment_map = {
"left": PP_ALIGN.LEFT,
"center": PP_ALIGN.CENTER,
"right": PP_ALIGN.RIGHT,
"justify": PP_ALIGN.JUSTIFY,
}
return alignment_map.get(alignment, PP_ALIGN.LEFT)
def create_poster(width_inch=48, height_inch=36):
"""
Create a new Presentation object, set its slide size (e.g., 48x36 inches).
:param width_inch: Float or int specifying width in inches (default 48).
:param height_inch: Float or int specifying height in inches (default 36).
:return: A python-pptx Presentation object.
"""
prs = Presentation()
prs.slide_width = Inches(width_inch)
prs.slide_height = Inches(height_inch)
return prs
def add_blank_slide(prs):
"""
Add a blank slide to the Presentation (layout index 6 is typically blank).
:param prs: The Presentation object to add a slide to.
:return: The newly added slide object.
"""
blank_layout = prs.slide_layouts[6]
return prs.slides.add_slide(blank_layout)
def shape_fill_color(shape, fill_color):
"""
Set the fill color of a shape to the specified RGB color.
:param shape: The shape object to modify.
:param fill_color: A tuple (r, g, b) for the fill color.
"""
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(*fill_color)
def add_textbox(
slide,
name,
left_inch,
top_inch,
width_inch,
height_inch,
text="",
word_wrap=True,
font_size=40,
bold=False,
italic=False,
alignment="left",
fill_color=None,
font_name="Arial"
):
"""
Create a textbox shape on the given slide, optionally fill its background with
a color if fill_color is specified as (r, g, b).
:param slide: Slide object to place the textbox on.
:param name: Name for the shape (shape.name).
:param left_inch: Left coordinate (in inches).
:param top_inch: Top coordinate (in inches).
:param width_inch: Width (in inches).
:param height_inch: Height (in inches).
:param text: Text to display in the textbox.
:param word_wrap: If True, wrap text in the textbox.
:param font_size: Numeric font size (e.g. 40).
:param bold: Boolean to set run.font.bold.
:param italic: Boolean to set run.font.italic.
:param alignment: String alignment: "left", "center", "right", or "justify".
:param fill_color: (r, g, b) tuple for solid fill background color, or None to skip.
:param font_name: String font name (e.g., "Arial").
:return: The newly created textbox shape.
"""
shape = slide.shapes.add_textbox(
Inches(left_inch), Inches(top_inch),
Inches(width_inch), Inches(height_inch)
)
shape.name = name
# If a fill color is specified, apply a solid fill
if fill_color is not None:
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(*fill_color)
else:
# Otherwise, set "no fill" if you want it transparent
shape.fill.background()
text_frame = shape.text_frame
# Turn off auto-size to ensure stable font size, etc.
text_frame.auto_size = MSO_AUTO_SIZE.NONE
text_frame.word_wrap = word_wrap
# Clear any default paragraphs
text_frame.clear()
# Add a new paragraph
p = text_frame.add_paragraph()
# Instead of setting p.text, explicitly create a Run
run = p.add_run()
run.text = text
# Parse alignment and set it
p.alignment = _parse_alignment(alignment)
# Set the font formatting on the run
font = run.font
font.size = _parse_font_size(font_size)
font.bold = bold
font.italic = italic
font.name = font_name
return shape
def fill_textframe(shape, paragraphs_spec):
"""
Given an existing shape (with a text frame) and a paragraphs_spec
describing paragraphs and runs, populate the shape’s text frame.
'paragraphs_spec' is a list of paragraphs, each containing:
- bullet: bool
- level: int (indent level)
- alignment: str ("left", "center", "right", or "justify")
- font_size: int
- runs: list of run dictionaries, each with:
text: str
bold: bool
italic: bool
color: [r,g,b] or None
font_size: int (optional, overrides paragraph default)
fill_color: [r,g,b] or None
"""
text_frame = shape.text_frame
# Ensure stable layout
text_frame.auto_size = MSO_AUTO_SIZE.NONE
text_frame.word_wrap = True
# Clear out existing paragraphs
text_frame.clear()
for p_data in paragraphs_spec:
p = text_frame.add_paragraph()
# # bulleting
# p.bullet = p_data.get("bullet", False)
# bullet level (indent)
p.level = p_data.get("level", 0)
# paragraph alignment
align_str = p_data.get("alignment", "left")
p.alignment = _parse_alignment(align_str)
# paragraph-level font size
default_font_size = p_data.get("font_size", 24)
p.font.size = Pt(default_font_size)
# Add runs
runs_spec = p_data.get("runs", [])
for run_info in runs_spec:
run = p.add_run()
if p_data.get("bullet", False):
if p.level == 0:
run.text = '\u2022' + run_info.get("text", "")
elif p.level == 1:
run.text = '\u25E6' + run_info.get("text", "")
else:
run.text = '\u25AA' + run_info.get("text", "")
else:
run.text = run_info.get("text", "")
# Font styling
font = run.font
font.bold = run_info.get("bold", False)
font.italic = run_info.get("italic", False)
# If run-specific color was provided
color_tuple = run_info.get("color", None)
if (
color_tuple
and len(color_tuple) == 3
and all(isinstance(c, int) for c in color_tuple)
):
if run.font.color is not None:
# If a ColorFormat object already exists, just set it
run.font.color.rgb = RGBColor(*color_tuple)
else:
# Otherwise, manually set the run color in the underlying XML
_set_run_font_color(run, color_tuple)
# If run-specific font size was provided
if "font_size" in run_info:
font.size = Pt(run_info["font_size"])
# If run-specific shape fill color was provided:
fill_color_tuple = run_info.get("fill_color", None)
if (
fill_color_tuple
and len(fill_color_tuple) == 3
and all(isinstance(c, int) for c in fill_color_tuple)
):
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(*fill_color_tuple)
def edit_textbox(
shape,
text=None,
word_wrap=None,
font_size=None,
bold=None,
italic=None,
alignment=None,
fill_color=None,
font_name=None
):
"""
Edit properties of an existing textbox shape.
:param shape: The shape object (textbox) to edit.
:param text: New text to set. If None, leaves text unmodified.
:param word_wrap: Boolean to enable/disable word wrap. If None, leaves unmodified.
:param font_size: Font size (int/float or string like '12pt'). If None, leaves unmodified.
:param bold: Boolean to set bold. If None, leaves unmodified.
:param italic: Boolean to set italic. If None, leaves unmodified.
:param alignment: One of 'left', 'center', 'right', 'justify'. If None, leaves unmodified.
:param fill_color: A tuple (r, g, b) for background fill color, or None to leave unmodified.
"""
text_frame = shape.text_frame
text_frame.auto_size = MSO_AUTO_SIZE.NONE
# Update fill color if provided
if fill_color is not None:
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(*fill_color)
# else: If you'd like to remove any existing fill if None, you could:
# else:
# shape.fill.background()
# Update word wrap if provided
if word_wrap is not None:
text_frame.word_wrap = word_wrap
# If text is provided, clear existing paragraphs and add the new text
if text is not None:
text_frame.clear()
p = text_frame.add_paragraph()
run = p.add_run()
run.text = text
# If alignment is provided, apply to the paragraph
if alignment is not None:
p.alignment = _parse_alignment(alignment)
# If font formatting info is provided, apply to the run font
font = run.font
if font_size is not None:
font.size = _parse_font_size(font_size)
if bold is not None:
font.bold = bold
if italic is not None:
font.italic = italic
else:
# If no new text is given, we can selectively change existing text properties.
for p in text_frame.paragraphs:
if alignment is not None:
p.alignment = _parse_alignment(alignment)
for run in p.runs:
font = run.font
if font_size is not None:
font.size = _parse_font_size(font_size)
if bold is not None:
font.bold = bold
if italic is not None:
font.italic = italic
if font_name is not None:
font.name = font_name
def add_image(slide, name, left_inch, top_inch, width_inch, height_inch, image_path):
"""
Add an image to the slide at the specified position and size.
:param slide: The slide object where the image should be placed.
:param name: A string name/label for the shape.
:param left_inch: Left position in inches.
:param top_inch: Top position in inches.
:param width_inch: Width in inches.
:param height_inch: Height in inches.
:param image_path: File path to the image.
:return: The newly created picture shape object.
"""
shape = slide.shapes.add_picture(
image_path,
Inches(left_inch), Inches(top_inch),
width=Inches(width_inch), height=Inches(height_inch)
)
shape.name = name
return shape
def set_shape_position(shape, left_inch, top_inch, width_inch, height_inch):
"""
Move or resize an existing shape to the specified position/dimensions.
:param shape: The shape object to be repositioned.
:param left_inch: New left position in inches.
:param top_inch: New top position in inches.
:param width_inch: New width in inches.
:param height_inch: New height in inches.
"""
shape.left = Inches(left_inch)
shape.top = Inches(top_inch)
shape.width = Inches(width_inch)
shape.height = Inches(height_inch)
def add_line_simple(slide, name, left_inch, top_inch, length_inch, thickness=2, color=(0, 0, 0), orientation="horizontal"):
"""
Add a simple horizontal or vertical line to the slide.
Parameters:
slide: The slide object.
name: The name/label for the line shape.
left_inch: The left (X) coordinate in inches for the starting point.
top_inch: The top (Y) coordinate in inches for the starting point.
length_inch: The length of the line in inches.
thickness: The thickness of the line in points (default is 2).
color: An (R, G, B) tuple specifying the line color (default is black).
orientation: "horizontal" or "vertical" (case-insensitive).
Returns:
The created line shape object.
"""
x1 = Inches(left_inch)
y1 = Inches(top_inch)
if orientation.lower() == "horizontal":
x2 = Inches(left_inch + length_inch)
y2 = y1
elif orientation.lower() == "vertical":
x2 = x1
y2 = Inches(top_inch + length_inch)
else:
raise ValueError("Orientation must be either 'horizontal' or 'vertical'")
# Create a straight connector (used as a line)
line_shape = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, x1, y1, x2, y2)
line_shape.name = name
# Set the line thickness and color
line_shape.line.width = Pt(thickness)
line_shape.line.color.rgb = RGBColor(*color)
return line_shape
def set_paragraph_line_spacing(shape, line_spacing=1.0):
"""
Set line spacing for all paragraphs in a textbox shape.
E.g., line_spacing=1.5 for 1.5x spacing, 2 for double spacing, etc.
:param shape: The textbox shape to modify.
:param line_spacing: A float indicating multiple of single spacing.
"""
text_frame = shape.text_frame
for paragraph in text_frame.paragraphs:
paragraph.line_spacing = line_spacing # direct float: 1.5, 2.0, etc.
def set_shape_text_margins(
shape,
top_px=0,
right_px=0,
bottom_px=0,
left_px=0
):
"""
Set the internal text margins (like "padding") for a textbox shape.
python-pptx uses points or EMUs for margins, so we convert from px -> points -> EMUs as needed.
Note: If your output environment uses a different PX:PT ratio, adjust _px_to_pt().
"""
text_frame = shape.text_frame
text_frame.auto_size = MSO_AUTO_SIZE.NONE
text_frame.margin_top = Pt(_px_to_pt(top_px))
text_frame.margin_right = Pt(_px_to_pt(right_px))
text_frame.margin_bottom = Pt(_px_to_pt(bottom_px))
text_frame.margin_left = Pt(_px_to_pt(left_px))
def adjust_font_size(shape, delta=2):
"""
Increase or decrease the current font size of all runs in a shape by `delta` points.
If a run has no explicitly set font size (font.size is None), we can either skip it or assume a default.
For simplicity, let's skip runs without an explicit size to avoid overwriting theme defaults.
:param shape: The textbox shape to update.
:param delta: Positive or negative integer to adjust the font size.
"""
text_frame = shape.text_frame
text_frame.auto_size = MSO_AUTO_SIZE.NONE
for paragraph in text_frame.paragraphs:
for run in paragraph.runs:
current_size = run.font.size
if current_size is not None:
new_size = current_size.pt + delta
# Prevent negative or zero font size
if new_size < 1:
new_size = 1
run.font.size = Pt(new_size)
def center_shape_horizontally(prs, shape):
"""
Center a shape horizontally on the slide using the presentation's slide width.
:param prs: The Presentation object (which holds slide_width).
:param shape: The shape to center.
"""
new_left = (prs.slide_width - shape.width) // 2
shape.left = new_left
def center_shape_vertically(prs, shape):
"""
Center a shape vertically on the slide using the presentation's slide height.
:param prs: The Presentation object (which holds slide_height).
:param shape: The shape to center.
"""
new_top = (prs.slide_height - shape.height) // 2
shape.top = new_top
def set_shape_text(shape, text, clear_first=True):
"""
Set or replace the text of an existing shape (commonly a textbox).
:param shape: The shape (textbox) whose text needs to be updated.
:param text: The new text content.
:param clear_first: Whether to clear existing paragraphs before adding.
"""
text_frame = shape.text_frame
text_frame.auto_size = MSO_AUTO_SIZE.NONE
if clear_first:
text_frame.clear()
p = text_frame.add_paragraph()
p.text = text
def _set_run_font_color(run, rgb_tuple):
"""
Manually create or replace the solidFill element in this run's XML
to force the color if run.font.color is None or doesn't exist yet.
"""
# Underlying run properties element
rPr = run.font._element
# Remove any existing <a:solidFill> elements to avoid duplicates
for child in rPr.iterchildren():
if child.tag == qn('a:solidFill'):
rPr.remove(child)
# Create a new solidFill element with the specified color
solid_fill = OxmlElement('a:solidFill')
srgb_clr = OxmlElement('a:srgbClr')
# Format the tuple (r, g, b) into a hex string "RRGGBB"
srgb_clr.set('val', '{:02X}{:02X}{:02X}'.format(*rgb_tuple))
solid_fill.append(srgb_clr)
rPr.append(solid_fill)
def set_text_style(shape, font_size=None, bold=None, italic=None, alignment=None, color=None, font_name=None):
"""
Adjust text style on an existing textbox shape.
:param shape: The textbox shape whose style is being updated.
:param font_size: Numeric font size (e.g. 40) or None to skip.
:param bold: Boolean or None (to skip).
:param italic: Boolean or None (to skip).
:param alignment: String alignment ('left', 'center', 'right', 'justify') or None (to skip).
:param color: A tuple (r, g, b), each int from 0-255, or None (to skip).
:param font_name: String font name (e.g., 'Arial') or None
"""
text_frame = shape.text_frame
# Disable auto-sizing so our manual settings are respected
text_frame.auto_size = MSO_AUTO_SIZE.NONE
# Convert the alignment string into a PP_ALIGN enum value
parsed_alignment = _parse_alignment(alignment) if alignment else None
# Convert the raw font size to a python-pptx Pt object
parsed_font_size = _parse_font_size(font_size)
# Iterate over paragraphs and runs in the shape
for paragraph in text_frame.paragraphs:
if parsed_alignment is not None:
paragraph.alignment = parsed_alignment
for run in paragraph.runs:
# Font size
if parsed_font_size is not None:
run.font.size = parsed_font_size
# Bold
if bold is not None:
run.font.bold = bold
# Italic
if italic is not None:
run.font.italic = italic
# Font name
if font_name is not None:
run.font.name = font_name
# Color
if color is not None:
# Sometimes run.font.color may be None. We can try:
if run.font.color is not None:
# If a ColorFormat object already exists, just set it
run.font.color.rgb = RGBColor(*color)
else:
# Otherwise, manually set the run color in the underlying XML
_set_run_font_color(run, color)
def save_presentation(prs, file_name="poster.pptx"):
"""
Save the current Presentation object to disk.
:param prs: The Presentation object.
:param file_name: The file path/name for the saved pptx file.
"""
prs.save(file_name)
def set_slide_background_color(slide, rgb=(255, 255, 255)):
"""
Sets the background color for a single Slide object.
:param slide: A pptx.slide.Slide object
:param rgb: A tuple of (R, G, B) color values, e.g. (255, 0, 0) for red
"""
bg_fill = slide.background.fill
bg_fill.solid()
bg_fill.fore_color.rgb = RGBColor(*rgb)
def style_shape_border(shape, color=(30, 144, 255), thickness=2, line_style="square_dot"):
"""
Applies a border (line) style to a given shape, where line_style is a
string corresponding to an MSO_LINE_DASH_STYLE enum value from python-pptx.
Valid line_style strings (based on the doc snippet) are:
-----------------------------------------------------------------
'solid' -> MSO_LINE_DASH_STYLE.SOLID
'round_dot' -> MSO_LINE_DASH_STYLE.ROUND_DOT
'square_dot' -> MSO_LINE_DASH_STYLE.SQUARE_DOT
'dash' -> MSO_LINE_DASH_STYLE.DASH
'dash_dot' -> MSO_LINE_DASH_STYLE.DASH_DOT
'dash_dot_dot' -> MSO_LINE_DASH_STYLE.DASH_DOT_DOT
'long_dash' -> MSO_LINE_DASH_STYLE.LONG_DASH
'long_dash_dot'-> MSO_LINE_DASH_STYLE.LONG_DASH_DOT
-----------------------------------------------------------------
:param shape: pptx.shapes.base.Shape object to style
:param color: A tuple (R, G, B) for the border color (default is (30, 144, 255))
:param thickness: Border thickness in points (default is 2)
:param line_style:String representing the line dash style; defaults to 'square_dot'
"""
# Map our string keys to MSO_LINE_DASH_STYLE values from your doc snippet
dash_style_map = {
"solid": MSO_LINE_DASH_STYLE.SOLID,
"round_dot": MSO_LINE_DASH_STYLE.ROUND_DOT,
"square_dot": MSO_LINE_DASH_STYLE.SQUARE_DOT,
"dash": MSO_LINE_DASH_STYLE.DASH,
"dash_dot": MSO_LINE_DASH_STYLE.DASH_DOT,
"dash_dot_dot": MSO_LINE_DASH_STYLE.DASH_DOT_DOT,
"long_dash": MSO_LINE_DASH_STYLE.LONG_DASH,
"long_dash_dot": MSO_LINE_DASH_STYLE.LONG_DASH_DOT
}
line = shape.line
line.width = Pt(thickness)
line.color.rgb = RGBColor(*color)
# Default to 'solid' if the requested style isn't in dash_style_map
dash_style_enum = dash_style_map.get(line_style.lower(), MSO_LINE_DASH_STYLE.SOLID)
line.dash_style = dash_style_enum
def get_visual_cues(name_to_hierarchy, identifier, poster_path='poster.pptx'):
prs = pptx.Presentation(poster_path)
position_dict_1 = add_border_hierarchy(prs, name_to_hierarchy, 1, border_width=10)
json.dump(position_dict_1, open(f"tmp/position_dict_1_<{identifier}>.json", "w"))
# Save the presentation to disk.
save_presentation(prs, file_name=f"tmp/poster_<{identifier}>_hierarchy_1.pptx")
prs = pptx.Presentation(poster_path)
add_border_hierarchy(prs, name_to_hierarchy, 1, border_width=10, fill_boxes=True)
save_presentation(prs, file_name=f"tmp/poster_<{identifier}>_hierarchy_1_filled.pptx")
prs = pptx.Presentation(poster_path)
position_dict_2 = add_border_hierarchy(prs, name_to_hierarchy, 2, border_width=10)
json.dump(position_dict_2, open(f"tmp/position_dict_2_<{identifier}>.json", "w"))
# Save the presentation to disk.
save_presentation(prs, file_name=f"tmp/poster_<{identifier}>_hierarchy_2.pptx")
prs = pptx.Presentation(poster_path)
add_border_hierarchy(prs, name_to_hierarchy, 2, border_width=10, fill_boxes=True)
# Save the presentation to disk.
save_presentation(prs, file_name=f"tmp/poster_<{identifier}>_hierarchy_2_filled.pptx")
from pptx.enum.shapes import MSO_SHAPE_TYPE, MSO_SHAPE, MSO_AUTO_SHAPE_TYPE
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
def emu_to_inches(emu: int) -> float:
return emu / 914400
def add_border(
prs,
border_color=RGBColor(255, 0, 0), # Red border for shapes
border_width=Pt(2), # 2-point border width
):
"""
Iterates over all slides and shapes in the Presentation 'prs', applies a
red border to each shape, and places a transparent (no fill).
Args:
prs: The Presentation object to modify.
border_color: RGBColor for the shape border color (default: red).
border_width: The width of the shape border (Pt).
"""
labeled_elements = {}
for slide in prs.slides:
for shape in slide.shapes:
try:
# --- 1) Add red border to the shape (if supported) ---
shape.line.fill.solid()
shape.line.fill.fore_color.rgb = border_color
shape.line.width = border_width
if hasattr(shape, 'name'):
labeled_elements[shape.name] = {
'left': f'{emu_to_inches(shape.left)} Inches',
'top': f'{emu_to_inches(shape.top)} Inches',
'width': f'{emu_to_inches(shape.width)} Inches',
'height': f'{emu_to_inches(shape.height)} Inches',
}
except Exception as e:
# If the shape doesn't support borders or text, skip gracefully
print(f"Could not add border to shape (type={shape.shape_type}): {e}")
return labeled_elements
def get_hierarchy(outline, hierarchy=1):
name_to_hierarchy = {}
for key, section in outline.items():
if key == "meta":
continue
name_to_hierarchy[section['name']] = hierarchy
if 'subsections' in section:
name_to_hierarchy.update(get_hierarchy(section['subsections'], hierarchy+1))
return name_to_hierarchy
def get_hierarchy_by_keys(outline, hierarchy=1):
name_to_hierarchy = {}
for key, section in outline.items():
if key == "meta":
continue
name_to_hierarchy[key] = hierarchy
if 'subsections' in section:
name_to_hierarchy.update(get_hierarchy_by_keys(section['subsections'], hierarchy+1))
return name_to_hierarchy
def rename_keys_with_name(data):
"""
Recursively rename dictionary keys to data['name'] if:
- The value is a dict,
- It contains a 'name' field.
Otherwise, keep the original key.
"""
if not isinstance(data, dict):
# If it's not a dictionary (e.g. list or scalar), just return it as-is
return data
new_dict = {}
for key, value in data.items():
if isinstance(value, dict) and "name" in value:
# Rename the key to whatever 'name' is in the nested dictionary
new_key = value["name"]
# Recursively process the value (which may contain its own subsections)
new_dict[new_key] = rename_keys_with_name(value)
else:
# Keep the same key if there's no 'name' in value or it's not a dictionary
new_dict[key] = rename_keys_with_name(value)
return new_dict
def add_border_hierarchy(
prs,
name_to_hierarchy: dict,
hierarchy: int,
border_color=RGBColor(255, 0, 0),
border_width=2,
fill_boxes: bool = False,
fill_color=RGBColor(255, 0, 0),
regardless=False
):
"""
Iterates over all slides and shapes in the Presentation 'prs'.
- For shapes whose name maps to the given 'hierarchy' in 'name_to_hierarchy' (or if 'regardless'
is True), draws a red border. Optionally fills the shape with red if 'fill_boxes' is True.
- For all other shapes, removes their border and hides any text.
Returns:
labeled_elements: dict of shape geometry for ALL shapes, regardless of hierarchy match.
"""
border_width = Pt(border_width)
labeled_elements = {}
for slide_idx, slide in enumerate(prs.slides):
for shape_idx, shape in enumerate(slide.shapes):
# Record basic geometry in labeled_elements
shape_name = shape.name if hasattr(shape, 'name') else f"Shape_{slide_idx}_{shape_idx}"
labeled_elements[shape_name] = {
'left': f"{emu_to_inches(shape.left):.2f} Inches",
'top': f"{emu_to_inches(shape.top):.2f} Inches",
'width': f"{emu_to_inches(shape.width):.2f} Inches",
'height': f"{emu_to_inches(shape.height):.2f} Inches",
}
# Determine if this shape should have a border
current_hierarchy = name_to_hierarchy.get(shape_name, None)
if current_hierarchy is None:
# Optional: Print a debug message if the shape’s name isn’t in the dict
print(f"Warning: shape '{shape_name}' not found in name_to_hierarchy.")
try:
if current_hierarchy == hierarchy or regardless:
# Draw border
shape.line.fill.solid()
shape.line.fill.fore_color.rgb = border_color
shape.line.width = border_width
# Optionally fill the shape with red color
if fill_boxes:
shape.fill.solid()
shape.fill.fore_color.rgb = fill_color
else:
# Remove border
shape.line.width = Pt(0)
shape.line.fill.background()
# Hide text if present
if shape.has_text_frame:
shape.text_frame.text = ""
except Exception as e:
print(f"Could not process shape '{shape_name}' (type={shape.shape_type}): {e}")
return labeled_elements