|
|
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 |
|
|
|
|
|
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): |
|
|
|
|
|
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 fill_color is not None: |
|
|
shape.fill.solid() |
|
|
shape.fill.fore_color.rgb = RGBColor(*fill_color) |
|
|
else: |
|
|
|
|
|
shape.fill.background() |
|
|
|
|
|
text_frame = shape.text_frame |
|
|
|
|
|
text_frame.auto_size = MSO_AUTO_SIZE.NONE |
|
|
text_frame.word_wrap = word_wrap |
|
|
|
|
|
|
|
|
text_frame.clear() |
|
|
|
|
|
|
|
|
p = text_frame.add_paragraph() |
|
|
|
|
|
run = p.add_run() |
|
|
run.text = text |
|
|
|
|
|
|
|
|
p.alignment = _parse_alignment(alignment) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
text_frame.auto_size = MSO_AUTO_SIZE.NONE |
|
|
text_frame.word_wrap = True |
|
|
|
|
|
text_frame.clear() |
|
|
|
|
|
for p_data in paragraphs_spec: |
|
|
p = text_frame.add_paragraph() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p.level = p_data.get("level", 0) |
|
|
|
|
|
|
|
|
align_str = p_data.get("alignment", "left") |
|
|
p.alignment = _parse_alignment(align_str) |
|
|
|
|
|
|
|
|
default_font_size = p_data.get("font_size", 24) |
|
|
p.font.size = Pt(default_font_size) |
|
|
|
|
|
|
|
|
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 = run.font |
|
|
font.bold = run_info.get("bold", False) |
|
|
font.italic = run_info.get("italic", False) |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
run.font.color.rgb = RGBColor(*color_tuple) |
|
|
else: |
|
|
|
|
|
_set_run_font_color(run, color_tuple) |
|
|
|
|
|
|
|
|
if "font_size" in run_info: |
|
|
font.size = Pt(run_info["font_size"]) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
if fill_color is not None: |
|
|
shape.fill.solid() |
|
|
shape.fill.fore_color.rgb = RGBColor(*fill_color) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if word_wrap is not None: |
|
|
text_frame.word_wrap = word_wrap |
|
|
|
|
|
|
|
|
if text is not None: |
|
|
text_frame.clear() |
|
|
p = text_frame.add_paragraph() |
|
|
run = p.add_run() |
|
|
run.text = text |
|
|
|
|
|
|
|
|
if alignment is not None: |
|
|
p.alignment = _parse_alignment(alignment) |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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'") |
|
|
|
|
|
|
|
|
line_shape = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, x1, y1, x2, y2) |
|
|
line_shape.name = name |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
rPr = run.font._element |
|
|
|
|
|
|
|
|
for child in rPr.iterchildren(): |
|
|
if child.tag == qn('a:solidFill'): |
|
|
rPr.remove(child) |
|
|
|
|
|
|
|
|
solid_fill = OxmlElement('a:solidFill') |
|
|
srgb_clr = OxmlElement('a:srgbClr') |
|
|
|
|
|
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 |
|
|
|
|
|
text_frame.auto_size = MSO_AUTO_SIZE.NONE |
|
|
|
|
|
|
|
|
parsed_alignment = _parse_alignment(alignment) if alignment else None |
|
|
|
|
|
|
|
|
parsed_font_size = _parse_font_size(font_size) |
|
|
|
|
|
|
|
|
for paragraph in text_frame.paragraphs: |
|
|
if parsed_alignment is not None: |
|
|
paragraph.alignment = parsed_alignment |
|
|
|
|
|
for run in paragraph.runs: |
|
|
|
|
|
if parsed_font_size is not None: |
|
|
run.font.size = parsed_font_size |
|
|
|
|
|
|
|
|
if bold is not None: |
|
|
run.font.bold = bold |
|
|
|
|
|
|
|
|
if italic is not None: |
|
|
run.font.italic = italic |
|
|
|
|
|
|
|
|
if font_name is not None: |
|
|
run.font.name = font_name |
|
|
|
|
|
|
|
|
if color is not None: |
|
|
|
|
|
if run.font.color is not None: |
|
|
|
|
|
run.font.color.rgb = RGBColor(*color) |
|
|
else: |
|
|
|
|
|
_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' |
|
|
""" |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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_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_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_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), |
|
|
border_width=Pt(2), |
|
|
): |
|
|
""" |
|
|
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: |
|
|
|
|
|
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: |
|
|
|
|
|
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): |
|
|
|
|
|
return data |
|
|
|
|
|
new_dict = {} |
|
|
for key, value in data.items(): |
|
|
if isinstance(value, dict) and "name" in value: |
|
|
|
|
|
new_key = value["name"] |
|
|
|
|
|
new_dict[new_key] = rename_keys_with_name(value) |
|
|
else: |
|
|
|
|
|
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): |
|
|
|
|
|
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", |
|
|
} |
|
|
|
|
|
|
|
|
current_hierarchy = name_to_hierarchy.get(shape_name, None) |
|
|
if current_hierarchy is None: |
|
|
|
|
|
print(f"Warning: shape '{shape_name}' not found in name_to_hierarchy.") |
|
|
|
|
|
try: |
|
|
if current_hierarchy == hierarchy or regardless: |
|
|
|
|
|
shape.line.fill.solid() |
|
|
shape.line.fill.fore_color.rgb = border_color |
|
|
shape.line.width = border_width |
|
|
|
|
|
|
|
|
if fill_boxes: |
|
|
shape.fill.solid() |
|
|
shape.fill.fore_color.rgb = fill_color |
|
|
else: |
|
|
|
|
|
shape.line.width = Pt(0) |
|
|
shape.line.fill.background() |
|
|
|
|
|
|
|
|
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 |
|
|
|