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 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 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