diff --git a/README.md b/README.md index c7a9320..268af75 100644 --- a/README.md +++ b/README.md @@ -37,20 +37,34 @@ md2pptx moves forward on Python every so often to: Installation is straightforward: -1. Install python-pptx -2. Clone md2pptx into a new directory +1. Clone md2pptx into a new directory +2. Install the cloned directory: + + ```shell + pip install ./path/to/the/md2pptx/directory + ``` The md2pptx repo includes all the essentials, such as funnel.py. You don't install these with eg pip. There are some optional packages, outlined in the User Guide. You can install python-pptx with - `pip3 install python-pptx` +```shell +pip3 install python-pptx +``` (On a Raspberry Pi you might want to use `pip3` (or `python3 -m pip`) to install for Python 3.) -You will probably need to issue the following command from the directory where you install it: +Try without parameters: + +```shell +md2pptx +``` + +You can also install directly from GitHub: - `chmod +x md2pptx` +```shell +pip install git+https://github.com/MartinPacker/md2pptx.git#egg=md2pptx +``` ### Starting To Use md2pptx @@ -85,3 +99,8 @@ See `docs/user-guide.html` or `docs/user-guide.md`. ### Issues & Suggestions This repo's Issues are regularly monitored. Use them for bug reports, suggestions, and questions. + +### Contributing + +Clone the repository, setup a virtual environnement and install `md2pptx` +as an editable package using ``pip install -Ue .``. diff --git a/md2pptx b/md2pptx old mode 100755 new mode 100644 index 563e0f2..e69de29 --- a/md2pptx +++ b/md2pptx @@ -1,6366 +0,0 @@ -#!/usr/bin/env python3 - -""" -md2pptx - Converts (a subset of) Markdown to Powerpoint (PPTX) - -First argument is file to write to - -Reads from stdin -""" - -import re -import sys -import os -import csv -import time -import collections -import collections.abc -from pptx import Presentation -from pptx import __version__ as pptx_version -from pptx.util import Inches, Pt -from pptx.dml.color import RGBColor, MSO_THEME_COLOR -from pptx.enum.text import MSO_AUTO_SIZE, PP_ALIGN -from pptx.enum.shapes import PP_PLACEHOLDER, PP_MEDIA_TYPE -from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR -from pptx.enum.text import MSO_ANCHOR -from pptx.enum.action import PP_ACTION -from pptx.enum.dml import MSO_PATTERN - -import struct -import datetime -import html.parser -from pptx.oxml.xmlchemy import OxmlElement -from pathlib import Path -import urllib.request -import tempfile -import copy -import platform -import shutil -import socket -from pptx.oxml import parse_xml -import uuid -import funnel -import runPython -from card import Card -from rectangle import Rectangle -from colour import * -from paragraph import * -from symbols import resolveSymbols -import globals -from processingOptions import * - - -from lxml import etree -from lxml.html import fromstring - -# Try to import CairoSVG - which might not be installed. -# Flag availability or otherwise -try: - import cairosvg - from cairosvg import helpers - - have_cairosvg = True - -except: - have_cairosvg = False - -# Try to import Pillow - which might not be installed. -# Flag availability or otherwise -try: - import PIL - - have_pillow = True - -except: - have_pillow = False - -# Try to import graphviz - which might not be installed. -# Flag availability or otherwise - -try: - import graphviz - - have_graphviz = True - -except: - have_graphviz = False - - -md2pptx_level = "5.4" -md2pptx_date = "23 February, 2025" - -namespaceURL = { - "mc": "http://schemas.openxmlformats.org/markup-compatibility/2006", - "p": "http://schemas.openxmlformats.org/presentationml/2006/main", - "p14": "http://schemas.microsoft.com/office/powerpoint/2010/main", - "p15": "http://schemas.microsoft.com/office/powerpoint/2012/main", - "a": "http://schemas.openxmlformats.org/drawingml/2006/main", - "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships" -} - -def namespacesFragment(prefixes): - xml = "" - for prefix in prefixes: - xml += 'xmlns:' + prefix + '="' + namespaceURL[prefix] +'" ' - - return xml - -class SlideInfo: - def __init__( - self, - titleText, - subtitleText, - blockType, - bullets, - tableRows, - cards, - code, - sequence, - ): - self.titleText = titleText - self.subtitleText = subtitleText - self.blockType = blockType - self.bullets = bullets - self.tableRows = tableRows - self.cards = cards - self.code = code - self.sequence = sequence - - -# Information about a single table. (A slide might have more than one - or none.) -class TableInfo: - def __init__(self, tableRows, tableCaption): - self.tableRows = [] - self.tableCaption = "" - - -# Information about a video -class AudioVideoInfo: - def __init__(self, elementString): - audioVideoElement = fromstring(elementString) - - if "src" in audioVideoElement.attrib.keys(): - self.source = audioVideoElement.attrib["src"] - else: - self.source = "" - - if audioVideoElement.tag == "audio": - # audio element doesn't have width or height attributes so make it square - self.width = 1024 - self.height = 1024 - else: - # video element can have width and height attributes - # Default is 4:3 - if "width" in audioVideoElement.attrib.keys(): - self.width = int(audioVideoElement.attrib["width"]) - else: - self.width = 1024 - - if "height" in audioVideoElement.attrib.keys(): - self.height = int(audioVideoElement.attrib["height"]) - else: - self.height = 768 - - self.aspectRatio = self.width / self.height - - if "poster" in audioVideoElement.attrib.keys(): - self.poster = audioVideoElement.attrib["poster"] - else: - self.poster = None - - - -# Get a picture's rId -def get_picture_rId(picture): - rId = picture._element.xpath("./p:blipFill/a:blip/@r:embed")[0] - return rId - -# Adds a picture as a background -def add_background(presentation, slide, picture): - # Add the picture with zero dimensions - picture = slide.shapes.add_picture(picture,0,0,0,00) - - # Get the RId for this tiny picture = as we'll need that to set the background - rId = get_picture_rId(picture) - - # find the cSld element to attach the XML to - cSld =slide._element.xpath("./p:cSld")[0] - - # Remove any pre-existing bg element - bg =slide._element.xpath("./p:bg") - if bg != []: - cSld.remove(bg) - - - # Confect the XML - xml = "" - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - xml += ' \n' - - # Parse this XML - parsed_xml = parse_xml(xml) - - # Insert the parsed XML fragment as a child of the cSld element - cSld.insert(0, parsed_xml) - - # Delete the original picture - deleteSimpleShape(picture) - -# Find the extLst element - if it exists in presentation.xml -def findExtLst(prs): - for child in prs._element.getchildren(): - if child.tag.endswith("}extLst"): - return child - - return None - - -def addSlide(presentation, slideLayout, slideInfo=None): - slide = presentation.slides.add_slide(slideLayout) - slide.slideInfo = slideInfo - - backgroundImage = globals.processingOptions.getCurrentOption("backgroundImage") - - if backgroundImage != "": - add_background(presentation, slide, backgroundImage) - return slide - - -def createSectionsXML(prs): - sectionSlideLayout = globals.processingOptions.getCurrentOption("SectionSlideLayout") - - xml = ' \n' - - sectionCount = 0 - for slide in prs.slides: - slideID = str(slide.slide_id) - - for idx, slide_layout in enumerate(prs.slide_layouts): - if slide.slide_layout == slide_layout: - layoutNumber = idx - break - - if layoutNumber == sectionSlideLayout: - # Have a section to contribute - sectionCount += 1 - - # Confect section name from first part of section slide title - title = findTitleShape(slide) - sectionName = title.text.split("\n")[0] - - # Clean up section name - sectionName = "".join( - letter - for letter in sectionName - if ( - (letter.isalnum()) - | (letter in "&-+") - | (letter in "!/*") - | (letter == " ") - ) - ) - - sectionName = ( - sectionName.replace("& ", "& ") - .replace("\r", " ") - .replace("\n", " ") - ) - - # section URI's just need to be a GUID wrapped in braces - xml += ( - ' \n' - ) - - # Only the first slide in the section is added - as section will continue until the next section - # anyway - xml += " \n" - xml += ' \n' - xml += " \n" - xml += " \n" - - # Close out the section list - xml += " \n" - - # Close out the sections extension - xml += " \n" - - parsed_xml = parse_xml(xml) - - return parsed_xml, sectionCount - - -def createExpandingSections(prs): - # Use the slides' layouts to create an XML fragment with sections in - xmlFragment, sectionCount = createSectionsXML(prs) - - if sectionCount > 0: - # Have sections to insert as an XML fragment - if (extLstElement := findExtLst(prs)) is not None: - # Need to remove the extension list element before adding a new one - prs._element.remove(extLstElement) - - # Insert a new extension list element - extLst = OxmlElement("p:extLst") - prs._element.insert(-1, extLst) - - # Insert the fragment in the extension list in presentation.xml - extLst.insert(0, xmlFragment) - - -def deleteSlide(prs, slideNumber): - rId = prs.slides._sldIdLst[slideNumber].rId - prs.part.drop_rel(rId) - del prs.slides._sldIdLst[slideNumber] - - -def startswithOneOf(haystack, needleList): - for needle in needleList: - if haystack.startswith(needle): - return True - - return False - - -# Splits a string into words, converting each word to an integer. Returns them as a -# sorted list -def sortedNumericList(string): - return sorted(list(map(int, set(string.split())))) - - -def substituteFooterVariables(footerText, liveFooters): - # Decide if the footer should be a live link to the section slide - wantLiveFooter = ( - (prs.lastSectionSlide is not None) - & (footerText.find(" -1) - & (liveFooters == "yes") - ) - - # Substitute any section title occurrences - sectionTitleLines = resolveSymbols(prs.lastSectionTitle).split("
") - - footerText = footerText.replace("
", sectionTitleLines[0]) - footerText = footerText.replace("", sectionTitleLines[0]) - - if len(sectionTitleLines) > 1: - footerText = footerText.replace("", sectionTitleLines[1]) - - if len(sectionTitleLines) > 2: - footerText = footerText.replace("", sectionTitleLines[2]) - - # Substitute any presentation title occurrences - presTitleLines = resolveSymbols(prs.lastPresTitle).split("
") - - footerText = footerText.replace("", presTitleLines[0]) - footerText = footerText.replace("", presTitleLines[0]) - - if len(presTitleLines) > 1: - footerText = footerText.replace("", presTitleLines[1]) - - if len(presTitleLines) > 2: - footerText = footerText.replace("", presTitleLines[2]) - - # Substitute any presentation subtitle occurrences - presSubtitleLines = resolveSymbols(prs.lastPresSubtitle).split("
") - - footerText = footerText.replace("", presSubtitleLines[0]) - footerText = footerText.replace("", presSubtitleLines[0]) - - if len(presSubtitleLines) > 1: - footerText = footerText.replace("", presSubtitleLines[1]) - - if len(presSubtitleLines) > 2: - footerText = footerText.replace("", presSubtitleLines[2]) - - # Make newlines happen - footerText = footerText.replace("
", "\n") - - return footerText, wantLiveFooter - -def addTableShadow(t): - tblPr = t._tbl.getchildren()[0] - - xml = """ - - - - - - - -""" - # Parse this XML - parsed_xml = parse_xml(xml) - - # Insert the parsed XML fragment as a child of the pPr element - tblPr.insert(0, parsed_xml) - -def _applyCellBorderStyling( - tcPr, linePosition, lineWidthMultiplier=1, lineCount=1, lineColour="000000" -): - # How wide, relatively speaking to make the lines - lineWidth = int(12700 * lineWidthMultiplier) - - # Whether the line should be single or double - if lineCount == 2: - lineCountValue = "dbl" - else: - lineCountValue = "sng" - - if linePosition == "l": - elementName = "a:lnL" - elif linePosition == "r": - elementName = "a:lnR" - elif linePosition == "t": - elementName = "a:lnT" - else: - elementName = "a:lnB" - - lnX = OxmlElement(elementName) - - lnX.attrib.update( - {"w": str(lineWidth), "cap": "flat", "cmpd": lineCountValue, "algn": "ctr"} - ) - - solidFill = OxmlElement("a:solidFill") - srgbClr = OxmlElement("a:srgbClr") - srgbClr.attrib.update({"val": lineColour}) - - solidFill.append(srgbClr) - lnX.append(solidFill) - - tcPr.append(lnX) - - -def applyCellBorderStyling( - cell, cellBorderStyling, lineWidthMultiplier, lineCount, lineColour -): - if cellBorderStyling == "": - # No cell border styling required - return - - # Get any existing cell properties element - or make one - tc = cell._tc - tcPr = tc.get_or_add_tcPr() - - # Draw any cell borders. More than one might apply - if cellBorderStyling.find("l") > -1: - _applyCellBorderStyling(tcPr, "l", lineWidthMultiplier, lineCount, lineColour) - if cellBorderStyling.find("r") > -1: - _applyCellBorderStyling(tcPr, "r", lineWidthMultiplier, lineCount, lineColour) - if cellBorderStyling.find("t") > -1: - _applyCellBorderStyling(tcPr, "t", lineWidthMultiplier, lineCount, lineColour) - if cellBorderStyling.find("b") > -1: - _applyCellBorderStyling(tcPr, "b", lineWidthMultiplier, lineCount, lineColour) - - -# Apply table line styling -def applyTableLineStyling( - table, - processingOptions, -): - wholeTableLineStyling = processingOptions.getCurrentOption("addTableLines") - linedColumns = processingOptions.getCurrentOption("addTableColumnLines") - linedRows = processingOptions.getCurrentOption("addTableRowLines") - - lastRow = len(table.rows) - 1 - - # Create blank cell styling matrix - cellStyling = [] - for rowNumber, row in enumerate(table.rows): - rowStyling = [] - for cell in row.cells: - rowStyling.append("") - cellStyling.append(rowStyling) - - # apply any "whole table" styling - from addTableLines - if wholeTableLineStyling == "box": - # Line around the table - for rowNumber, row in enumerate(table.rows): - # Figure out whether row is top, middle, or bottom - if rowNumber == 0: - rowStyling = "t" - elif rowNumber == lastRow: - rowStyling = "b" - else: - rowStyling = "" - - lastColumn = len(row.cells) - 1 - - for columnNumber, cell in enumerate(row.cells): - if columnNumber == 0: - columnStyling = "l" - elif columnNumber == lastColumn: - columnStyling = "r" - else: - columnStyling = "" - cellStyling[rowNumber][columnNumber] = rowStyling + columnStyling - - elif wholeTableLineStyling == "all": - # All edges of all cells have lines - for rowNumber, row in enumerate(table.rows): - lastColumn = len(row.cells) - 1 - - for columnNumber, cell in enumerate(row.cells): - cellStyling[rowNumber][columnNumber] = "tlbr" - - # Apply any row styling - from addTableColumnLines - for rowNumber, row in enumerate(table.rows): - if rowNumber + 1 in linedRows: - # Line after this row so below - for columnNumber, cell in enumerate(row.cells): - cellStyling[rowNumber][columnNumber] = ( - cellStyling[rowNumber][columnNumber] + "b" - ) - - elif rowNumber in linedRows: - # Line before this row so above - for columnNumber, cell in enumerate(row.cells): - cellStyling[rowNumber][columnNumber] = ( - cellStyling[rowNumber][columnNumber] + "t" - ) - - # Apply any column styling - from addTableRowLines - for rowNumber, row in enumerate(table.rows): - for columnNumber, cell in enumerate(row.cells): - if columnNumber + 1 in linedColumns: - # Line after this column so to right - cellStyling[rowNumber][columnNumber] = ( - cellStyling[rowNumber][columnNumber] + "r" - ) - - elif columnNumber + 1 in linedColumns: - # Line after this column so to left - cellStyling[rowNumber][columnNumber] = ( - cellStyling[rowNumber][columnNumber] + "r" - ) - - # Apply the styling from the matrix to all cells - for rowNumber, row in enumerate(table.rows): - for columnNumber, cell in enumerate(row.cells): - applyCellBorderStyling( - cell, - cellStyling[rowNumber][columnNumber], - globals.processingOptions.getCurrentOption("addTableLineWidth"), - globals.processingOptions.getCurrentOption("addTableLineCount"), - globals.processingOptions.getCurrentOption("addTableLineColour"), - ) - - - -def reportSlideTitle(slideNumber, indent, titleText): - print(str(slideNumber).rjust(4) + " " + (" " * indent) + titleText) - - -def reportGraphicFilenames(leftFilename, rightFilename=""): - if rightFilename == "": - print(" ---> " + leftFilename.ljust(30)) - else: - print(" ---> " + leftFilename.ljust(30) + " , " + rightFilename) - - -# Given current indenting regime calculate what level the bullet / number is at -def calculateIndentationLevel(firstNonSpace, indentSpaces): - return int(firstNonSpace / indentSpaces) - - -# Calculate picture dimensions given its natural height and bounds -def scalePicture(maxPicWidth, maxPicHeight, imageWidth, imageHeight): - heightIfWidthUsed = maxPicWidth * imageHeight / imageWidth - widthIfHeightUsed = maxPicHeight * imageWidth / imageHeight - - if heightIfWidthUsed > maxPicHeight: - # Use the height to scale - usingHeightToScale = True - - picWidth = widthIfHeightUsed - picHeight = maxPicHeight - - else: - # Use the width to scale - usingHeightToScale = False - - picWidth = maxPicWidth - picHeight = heightIfWidthUsed - return (picWidth, picHeight, usingHeightToScale) - - -def parseMedia(cellString, graphicCount): - graphicTitle = "" - HTML = "" - audioVideoInfo = None - graphicHref = "" - GraphicFilename = "" - printableGraphicFilename = "" - - graphicCount += 1 - - if videoRegexMatch := videoRegex.match(cellString): - # Cell contains a video - audioVideoInfo = AudioVideoInfo(cellString) - _, printableGraphicFilename = handleWhateverGraphicType(audioVideoInfo.source) - - elif audioRegexMatch := audioRegex.match(cellString): - # Cell contains an audio - audioVideoInfo = AudioVideoInfo(cellString) - _, printableGraphicFilename = handleWhateverGraphicType(audioVideoInfo.source) - - elif clickableGraphicMatch := clickableGraphicRegex.match(cellString): - # Cell contains a clickable graphic - graphicTitle = clickableGraphicMatch.group(1) - GraphicFilename = clickableGraphicMatch.group(2) - graphicHref = clickableGraphicMatch.group(3) - - ( - GraphicFilename, - printableGraphicFilename, - ) = handleWhateverGraphicType(GraphicFilename) - - elif graphicMatch := graphicRegex.match(cellString): - # Cell contains a non-clickable graphic - graphicTitle = graphicMatch.group(1) - GraphicFilename = graphicMatch.group(2) - - ( - GraphicFilename, - printableGraphicFilename, - ) = handleWhateverGraphicType(GraphicFilename) - - else: - # Not a graphic or video - GraphicFilename = "" - printableGraphicFilename = "" - HTML = cellString - graphicCount -= 1 - - return ( - graphicTitle, - GraphicFilename, - printableGraphicFilename, - graphicHref, - HTML, - audioVideoInfo, - graphicCount, - ) - - -# Send a shape to the back on a slide -def sendToBack(shapes, shape): - firstShapeElement = shapes[0]._element - firstShapeElement.addprevious(shape._element) - - -# Turn a paragraph into a numbered inList item -def makeNumberedListItem(p): - if ( - p._element.getchildren()[0].tag - == "{http://schemas.openxmlformats.org/drawingml/2006/main}pPr" - ): - pPr = p._element.getchildren()[0] - if len(pPr.getchildren()) > 0: - # Remove Default Text Run Properties element - if present - x = pPr.getchildren()[0] - if x.tag == "{http://schemas.openxmlformats.org/drawingml/2006/main}defRPr": - pPr.remove(x) - else: - pPr = OxmlElement("a:pPr") - p._element.insert(0, pPr) - - buFont = OxmlElement("a:buFont") - buFont.set("typeface", "+mj-lt") - pPr.append(buFont) - - buAutoNum = OxmlElement("a:buAutoNum") - buAutoNum.set("type", "arabicPeriod") - pPr.append(buAutoNum) - -# Add a drop shadow to a shape -def createShadow(shape): - if "Table" in shape.__class__.__name__: - # Table has to be handled differently - return addTableShadow(shape) - - el = OxmlElement("a:effectLst") - - spPr = shape.fill._xPr - - spPr.append(el) - - outerShdw = OxmlElement("a:outerShdw") - outerShdw.set("algn", "tl") - outerShdw.set("blurRad", "50800") - outerShdw.set("dir", "2700000") - outerShdw.set("dist", "95250") - outerShdw.set("rotWithShape", "0") - - el.append(outerShdw) - - prstClr = OxmlElement("a:prstClr") - prstClr.set("val", "black") - - outerShdw.append(prstClr) - - alpha = OxmlElement("a:alpha") - alpha.set("val", "40000") - - prstClr.append(alpha) - - -# Clone a shape in a slide and return the new shape. -# (This is a deep copy so the new shape will have the same -# eg bullet style as the source shape) -def addClonedShape(slide, shape1): - # Clone the element for the shape - el1 = shape1.element - el2 = copy.deepcopy(el1) - - # Insert the cloned element into the shape tree - slide.shapes._spTree.insert_element_before(el2, "p:extLst") - - # Return the shape associated with this new element - return slide.shapes[-1] - - -# Following functions are workarounds for python-pptx not having these functions for the font object -def set_subscript(font): - if font.size is None: - font._element.set("baseline", "-50000") - return - - if font.size < Pt(24): - font._element.set("baseline", "-50000") - else: - font._element.set("baseline", "-25000") - - -def set_superscript(font): - if font.size is None: - font._element.set("baseline", "60000") - return - - if font.size < Pt(24): - font._element.set("baseline", "60000") - else: - font._element.set("baseline", "30000") - - -def setStrikethrough(font): - font._element.set("strike", "sngStrike") - - -def setHighlight(run, color): - # get run properties - rPr = run._r.get_or_add_rPr() - - # Create highlight element - hl = OxmlElement("a:highlight") - - # Create specify RGB Colour element with color specified - srgbClr = OxmlElement("a:srgbClr") - setattr(srgbClr, "val", color) - - # Add colour specification to highlight element - hl.append(srgbClr) - - # Add highlight element to run properties - rPr.append(hl) - - return run - - -# Get the slide object the run is in -def SlideFromRun(run): - return run._parent._parent._parent._parent._parent - - -# Get the slide object the picture is in -def SlideFromPicture(picture): - return picture._parent._parent - - -# Creates a hyperlink to another slide and/or a tooltip - for a -# text run -# Note: To get just a tooltip make to_slide be the source slide -# so it links to itself. -def createRunHyperlinkOrTooltip(run, to_slide, tooltipText=""): - # Get hold of the shape the run is in - if run._parent._parent._parent.__class__.__name__ == "_Cell": - # Run in a table cell has to be handled differently - shape = ( - run._parent._parent._parent._parent._parent._parent._parent._graphic_frame - ) - else: - # Ordinary text run - shape = run._parent._parent._parent - - if to_slide == None: - to_slide = SlideFromRun(run) - hl = run.hyperlink - sca = shape.click_action - sca_hl = sca.hyperlink - - # Add a click action to generate an internal hyperlink address - sca.target_slide = to_slide - - # Use that internal hyperlink address for the run - hl.address = sca_hl.address - - # Also clone the hyperlink click action - hl._hlinkClick.action = sca_hl._hlink.action - - if tooltipText != "": - hl._hlinkClick.set("tooltip", tooltipText) - - # Also clone the hyperlink rId - hl._hlinkClick.rId = sca_hl._hlink.rId - - # Delete the shape click action - sca.target_slide = None - - -# Creates a hyperlink to another slide or a URL and/or a tooltip - for a -# picture -# Note: To get just a tooltip make to_slide be the source slide -# so it links to itself. -def createPictureHyperlinkOrTooltip(picture, target, tooltipText=""): - if target == None: - # If neither a tooltip nor a target slide then return having - # done nothing - if tooltipText == "": - return - - # Tooltip but no target slide - target = SlideFromPicture(picture) - picture.click_action.target_slide = target - elif target.__class__.__name__ == "str": - # Is a URL - picture.click_action.hyperlink.address = target - # URL might be a macro reference - if target[:11] == "ppaction://": - # URL is indeed a macro reference, so treat it as such - picture.click_action.hyperlink._hlink.set("action", target) - else: - # Target is a slide - picture.click_action.target_slide = target - - if tooltipText != "": - picture.click_action.hyperlink._hlink.set("tooltip", tooltipText) - - -# If a tooltip has been set return it else return an empty string -def getPictureTooltip(picture): - if picture.click_action.hyperlink._hlink != None: - # There is a tooltip - return picture.click_action.hyperlink._hlink.get("tooltip") - else: - # There is no tooltip - return "" - - -# Create hyperlink and optional tooltip from a shape eg Chevron -def createShapeHyperlinkAndTooltip(shape, to_slide, tooltipText=""): - shape.click_action.target_slide = to_slide - hl = shape.click_action.hyperlink - hl._hlink.set("tooltip", tooltipText) - - -def getGraphicDimensions(fname): - """Determine the image type of fhandle and return its size. - from draco""" - try: - with open(fname, "rb") as fhandle: - head = fhandle.read(24) - if len(head) != 24: - return -1, -1 - fname2 = fname.lower() - if fname2.endswith(".png"): - check = struct.unpack(">i", head[4:8])[0] - if check != 0x0D0A1A0A: - return -1, -1 - width, height = struct.unpack(">ii", head[16:24]) - elif fname2.endswith(".gif"): - width, height = struct.unpack("H", fhandle.read(2))[0] - 2 - # We are at a SOFn block - fhandle.seek(1, 1) # Skip 'precision' byte. - height, width = struct.unpack(">HH", fhandle.read(4)) - except Exception: # IGNORE:W0703 - return - else: - return -1, -1 - - return width, height - - except EnvironmentError: - return -1, -1 - - -def getVideoInfo(audioVideoInfo): - if audioVideoInfo.source.find("://") > -1: - # Video would be sourced from the web - try: - operUrl = urllib.request.urlopen(audioVideoInfo.source) - except urllib.error.HTTPError as e: - return -1, -1, "Web", None - - except socket.error as s: - return -1, -1, "Web", None - - data = operUrl.read() - - return audioVideoInfo.width, audioVideoInfo.height, "Web", data - else: - # Video would be sourced from a local file - try: - fhandle = open(audioVideoInfo.source, "rb") - except EnvironmentError: - return -1, -1, "Local", None - - return audioVideoInfo.width, audioVideoInfo.height, "Local", None - - -# Render a list of bullets -def renderText(shape, bullets): - baseTextDecrement = globals.processingOptions.getCurrentOption("baseTextDecrement") - baseTextSize = globals.processingOptions.getCurrentOption("baseTextSize") - - tf = shape.text_frame - - for bulletNumber, bullet in enumerate(bullets): - para0 = tf.paragraphs[0] - - if bulletNumber == 0: - # Don't need to create paragraph - p = para0 - else: - # We need a new paragraph - p = tf.add_paragraph() - - # Set the paragraph's level - zero-indexed - p.level = int(bullet[0]) - - # Set the paragraph's font size, adjusted for level, if necessary - if baseTextSize > 0: - p.font.size = Pt(baseTextSize - p.level * baseTextDecrement) - - addFormattedText(p, bullet[1]) - - if bullet[2] == "numbered": - makeNumberedListItem(p) - - tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE - - -def findTitleShape(slide): - if slide.shapes.title == None: - # Have to use first shape as title - return slide.shapes[0] - else: - return slide.shapes.title - - -def findBodyShape(slide): - if len(slide.shapes) > 1: - return slide.shapes[1] - elif slide.shapes.title == None: - return slide.shapes[0] - else: - return None - - -# Returns a top, left, width, height for content to be rendered into -def getContentRect(presentation, slide, topOfContent, margin): - numbersHeight = globals.processingOptions.getCurrentOption("numbersHeight") - # Left and right are always defined by the margins - rectLeft = margin - rectWidth = presentation.slide_width - 2 * margin - if topOfContent == 0: - # There is no title on this slide - rectTop = margin - rectHeight = presentation.slide_height - margin - max(margin, numbersHeight) - else: - # There is a title on this slide - rectTop = topOfContent + margin - rectHeight = presentation.slide_height - rectTop - max(margin, numbersHeight) - - return (rectLeft, rectWidth, rectTop, rectHeight) - - -# Finds the title and adds the text to it, returning title bottom, title shape, and -# flattened title -def formatTitle(presentation, slide, titleText, titleFontSize, subtitleFontSize): - marginBase = globals.processingOptions.getCurrentOption("marginBase") - pageTitleAlign = globals.processingOptions.getCurrentOption("pagetitlealign") - - # Convert page title alignment text value to constant - if pageTitleAlign == "left": - titleAlignment = PP_ALIGN.LEFT - elif pageTitleAlign == "right": - titleAlignment = PP_ALIGN.RIGHT - else: - titleAlignment = PP_ALIGN.CENTER - - # Find title - title = findTitleShape(slide) - - if titleText == " ": - deleteSimpleShape(title) - - return (marginBase, None, "") - - if globals.processingOptions.getCurrentOption("adjustTitles"): - title.top = marginBase - title.left = marginBase - title.width = presentation.slide_width - marginBase * 2 - - # Figure out how many lines title will need (ignoring overflow) - titleLineCount = len(titleLines := titleText.split("
")) - - # This will hold the flattened title lines to be printed - flattenedTitleLines = [] - - # Add the first line of the title text to the first paragraph - firstTitleParagraph = title.text_frame.paragraphs[0] - flattenedTitleLines.append(addFormattedText(firstTitleParagraph, titleLines[0])) - - # Set anchor to top - title.text_frame.vertical_anchor = MSO_ANCHOR.TOP - - # Set this paragraph's font size using pageTitleSize - firstTitleParagraph.font.size = Pt(titleFontSize) - - # No space before the paragraph - firstTitleParagraph.space_after = Pt(0) - - # Set first title paragraph's alignment - firstTitleParagraph.alignment = titleAlignment - - if subtitleFontSize == "same": - subtitleFontSize = titleFontSize - - # If there are additional title lines then add them - for lineNumber, titleLine in enumerate(titleLines[1:]): - # Each title line requires a new paragraph - newPara = title.text_frame.add_paragraph() - - # Use this new paragraph, adding flattened title line to the list - flattenedTitleLines.append(addFormattedText(newPara, titleLine)) - - # No space before the paragraph - newPara.space_before = Pt(0) - - # Set this paragraph's font size using pageSubtitleSize - newPara.font.size = Pt(subtitleFontSize) - - # Set this paragraph's alignment - newPara.alignment = titleAlignment - - # Note: Working off pageTitleSize and pageSubtitleSize - if globals.processingOptions.getCurrentOption("adjustTitles"): - title.height = Pt(titleFontSize) + Pt(subtitleFontSize) * (titleLineCount - 1) - - # Massage title line for printing a little - if titleLineCount > 1: - flattenedTitleText = flattenedTitleLines[0] + " ..." - else: - flattenedTitleText = flattenedTitleLines[0] - - # Return where the next shape below the title would be - vertically - return (title.top + title.height + Inches(0.1), title, flattenedTitleText) - - -# Parse the string after the e.g. ### for a displayable title and -# an optional heading reference -def parseTitleText(titleLineString): - # Get rid of any cruft on the line - slideTitleWithPossibleHref = titleLineString.strip().rstrip("#").rstrip() - - if hrefMatch := slideHrefRegex.match(slideTitleWithPossibleHref): - # Use the explicit href - slideTitle = hrefMatch.group(1) - href = hrefMatch.group(2) - else: - # No href - href = "" - slideTitle = slideTitleWithPossibleHref - - return slideTitle, href - - - -def addFooter(presentation, slideNumber, slide): - numbersHeight = globals.processingOptions.getCurrentOption("numbersHeight") - - numbersglobals.fontsizespec = globals.processingOptions.getCurrentOption("numbersFontSize") - if numbersglobals.fontsizespec == "": - numbersFontSize = Pt(12) - else: - numbersFontSize = Pt(numbersglobals.fontsizespec) - - shapes = slide.shapes - footer = shapes.add_textbox( - Inches(0.1), - presentation.slide_height - numbersHeight, - Inches(0.2), - numbersHeight / 2, - ) - frame = footer.text_frame - p = frame.paragraphs[0] - run = p.add_run() - run.text = str(slideNumber) - font = run.font - font.size = numbersFontSize - - -# Called "Simple" because more complex shapes might not work -def deleteSimpleShape(shape): - if shape == None: - return - shapeElement = shape.element - shapeElement.getparent().remove(shapeElement) - - -def createProcessingSummarySlide(presentation, rawMetadata): - tableMargin = globals.processingOptions.getCurrentOption("tableMargin") - pageTitleSize = globals.processingOptions.getCurrentOption("pageTitleSize") - pageSubtitleSize = globals.processingOptions.getCurrentOption("pageSubtitleSize") - # Use the first slide in the template presentation as the base - slide = presentation.slides[0] - - # Delete any body shape - other than action buttons - bodyShape = findBodyShape(slide) - if bodyShape.name.startswith("Action Button:") is False: - deleteSimpleShape(bodyShape) - - # Build "run time" text - now = datetime.datetime.now() - runTime = now.strftime("%H:%M").lstrip() - runDate = now.strftime("%e %B, %G").lstrip() - runDateTime = "Presentation built: " + runTime + " on " + runDate - - # Format title and add title text - slideTitleBottom, title, flattenedTitle = formatTitle( - presentation, - slide, - banner + "
" + runDateTime, - pageTitleSize, - pageSubtitleSize, - ) - - # Work out how many pairs of columns we need - if globals.processingOptions.hideMetadataStyle: - # Adjust metadata item count to remove style. - metadata = [] - for metadataItem in rawMetadata: - if metadataItem[0].startswith("style.") == False: - metadata.append(metadataItem) - else: - metadata = rawMetadata - - metadataRows = len(metadata) - - maxMetadataRowsPerColumn = 15 - if metadataRows > 4 * maxMetadataRowsPerColumn: - metadataColumnPairs = 5 - elif metadataRows > 3 * maxMetadataRowsPerColumn: - metadataColumnPairs = 4 - elif metadataRows > 2 * maxMetadataRowsPerColumn: - metadataColumnPairs = 3 - elif metadataRows > maxMetadataRowsPerColumn: - metadataColumnPairs = 2 - else: - metadataColumnPairs = 1 - - columns = metadataColumnPairs * 2 - rows = min(maxMetadataRowsPerColumn, metadataRows) - - # Get the rectangle the content will draw in - contentLeft, contentWidth, contentTop, contentHeight = getContentRect( - presentation, slide, slideTitleBottom, tableMargin - ) - - tableHeight = min(contentHeight, Inches(0.25) * rows) - - # Figure out the width of a single-width column - columnWidthUnit = int(contentWidth / (2 * metadataColumnPairs)) - - # Create the table with the above number of rows and columns - newTable = slide.shapes.add_table( - rows, columns, tableMargin, contentTop, contentWidth, tableHeight - ).table - - # Don't want headings - newTable.first_row = False - - cols = newTable.columns - for cp in range(metadataColumnPairs): - cols[2 * cp].width = columnWidthUnit - cols[2 * cp + 1].width = columnWidthUnit - - row = 0 - column = 0 - for item in metadata: - key, value = item - - if row == maxMetadataRowsPerColumn: - # Move to next column - column += 2 - row = 0 - - # Set text of metadata key cell - newTable.cell(row, column).text = key - if globals.processingOptions.dynamicallyChangedOptions.get(key) is not None: - # Set text of metadata value cell - with asterisk - newTable.cell(row, column + 1).text = value + "*" - - # Colour key cell blue - p1 = newTable.cell(row, column).text_frame.paragraphs[0] - p1.font.color.rgb = RGBColor.from_string("0000FF") - - # Colour value cell blue - p2 = newTable.cell(row, column + 1).text_frame.paragraphs[0] - p2.font.color.rgb = RGBColor.from_string("0000FF") - else: - # Set text of metadata value cell - without asterisk - newTable.cell(row, column + 1).text = value - - if metadataColumnPairs == 5: - newTable.cell(row, column).text_frame.paragraphs[0].font.size = Pt(8) - newTable.cell(row, column + 1).text_frame.paragraphs[0].font.size = Pt(8) - elif metadataColumnPairs == 4: - newTable.cell(row, column).text_frame.paragraphs[0].font.size = Pt(10) - newTable.cell(row, column + 1).text_frame.paragraphs[0].font.size = Pt(10) - elif metadataColumnPairs == 3: - newTable.cell(row, column).text_frame.paragraphs[0].font.size = Pt(12) - newTable.cell(row, column + 1).text_frame.paragraphs[0].font.size = Pt(12) - elif metadataColumnPairs == 2: - newTable.cell(row, column).text_frame.paragraphs[0].font.size = Pt(14) - newTable.cell(row, column + 1).text_frame.paragraphs[0].font.size = Pt(14) - else: - newTable.cell(row, column).text_frame.paragraphs[0].font.size = Pt(16) - newTable.cell(row, column + 1).text_frame.paragraphs[0].font.size = Pt(16) - row += 1 - - -# Note: This doesn't use formatTitle() -def createTitleOrSectionSlide( - presentation, - slideNumber, - titleText, - layout, - titleSize, - subtitleText, - subtitleSize, - notes_text, -): - marginBase = globals.processingOptions.getCurrentOption("marginBase") - - slide = addSlide(presentation, presentation.slide_layouts[layout], None) - - # Add title - title = findTitleShape(slide) - flattenedTitle = addFormattedText(title.text_frame.paragraphs[0], titleText) - - title.text_frame.paragraphs[0].font.size = Pt(titleSize) - - # Add subtitle - if there is one - if (subtitleText.strip() != "") & (subtitleText[0:2] != "\n\n"): - # There is a subtitle - chunks = subtitleText.strip().split("\n\n") - subtitleText = chunks[0] - notes_text = "\n\n".join(chunks[1:]) - createSlideNotes(slide, notes_text) - - subtitleShape = findBodyShape(slide) - if subtitleShape != None: - addFormattedText(subtitleShape.text_frame.paragraphs[0], subtitleText) - subtitleShape.text_frame.paragraphs[0].font.size = Pt(subtitleSize) - else: - print("Warning: No subtitle shape on this slide to add text to.") - else: - # Reformat subtitle shape to be out of the way - subtitleShape = findBodyShape(slide) - if subtitleShape != None: - subtitleShape.top = title.top + title.height + marginBase * 2 - subtitleShape.width = title.width - subtitleShape.left = title.left - subtitleShape.height = marginBase * 2 - - reportSlideTitle(slideNumber, 1, flattenedTitle) - - if want_numbers_headings is True: - addFooter(presentation, slideNumber, slide) - - return slide - - -def handleWhateverGraphicType(GraphicFilename): - # Handles both physical file and URI file types - GraphicFilename = GraphicFilename.strip() - - # First ensure we have the data in a file and know if the source was a URI - if ":" in GraphicFilename: - # Is a URI - so we have to retrieve it and store it in a temporary file - is_uri = True - - # Massage the URI into a printable filename - if len(GraphicFilename) > 50: - printableGraphicFilename = ( - GraphicFilename[:25] + "..." + GraphicFilename[-25:] - ) - else: - printableGraphicFilename = GraphicFilename - - # Get Temporary File Directory - which might be None - tempDir = globals.processingOptions.getCurrentOption("tempDir") - - # Retrieve the data into a temporary file - try: - operUrl = urllib.request.urlopen(GraphicFilename) - - except urllib.error.HTTPError as e: - print("HTTP error: " + str(e.code)) - return GraphicFilename, printableGraphicFilename - - except socket.error as s: - print("Socket error. (Web site not found).") - return GraphicFilename, printableGraphicFilename - - data = operUrl.read() - - # Get Content-Type header - if set - content_type = "" - - if str(type(operUrl)) == "": - # Can try to get content header - content_type = operUrl.getheader("content-type") - - if content_type == "": - # Obtain file extension by searching in the URL - extensionPos = GraphicFilename.rindex(".") - lastSlashPos = GraphicFilename.rindex("/") - if lastSlashPos > extensionPos: - fileExt = "" - - else: - fileExt = GraphicFilename[extensionPos:] - - else: - # Set file extension based on Content-Type header_name - # Note: It's been translated to lower case above - if content_type in ["image/jpeg", "image/jpg"]: - # Note: only the first of these two is legitimate - fileExt = "jpg" - elif content_type == "image/png": - fileExt = "png" - elif content_type in ["image/svg+xml", "image/svg"]: - # Note: only the first of these two is legitimate - fileExt = "svg" - elif content_type == "application/postscript": - fileExt = "eps" - else: - fileExt = None - else: - fileExt = None - - # Store in a temporary file - try: - tempGraphicFile = tempfile.NamedTemporaryFile( - delete=False, suffix=fileExt, dir=tempDir - ) - except IOError as e: - print("Couldn't create temporary file. md2pptx terminating") - exit() - - tempGraphicFile.write(data) - convertibleFilename = tempGraphicFile.name - tempGraphicFile.close() - - else: - is_uri = False - - # Files don't get their names edited - printableGraphicFilename = GraphicFilename - convertibleFilename = GraphicFilename - - if is_uri: - lastSlash = GraphicFilename.rfind("/") - lastDot = GraphicFilename.rfind(".") - - PNGname = GraphicFilename[lastSlash + 1 : lastDot] + ".PNG" - else: - PNGname = GraphicFilename[:-4] + ".PNG" - - # Process the file - whatever the origin - based on file extension - if ".svg" in GraphicFilename.lower(): - # is an SVG file - if have_cairosvg: - # Convert SVG file to temporary PNG - # Store in a temporary file - - # Get Temporary File Directory - which might be None - tempDir = globals.processingOptions.getCurrentOption("tempDir") - - try: - graphicFile = tempfile.NamedTemporaryFile( - delete=False, suffix=".PNG", dir=tempDir - ) - except IOError as e: - print("Couldn't create temporary file. md2pptx terminating") - exit() - - cairosvg.svg2png(file_obj=open(convertibleFilename), write_to=graphicFile) - - # Retrieve the temporary file name - GraphicFilename = graphicFile.name - - if globals.processingOptions.getCurrentOption("exportGraphics"): - try: - shutil.copy(GraphicFilename, PNGname) - except: - print("Copy error: " + PNGname) - - else: - print("Don't have CairoSVG installed. Terminating.") - sys.exit() - elif ".eps" in GraphicFilename.lower(): - if have_pillow: - # Get EPS file - im = PIL.Image.open(GraphicFilename) - - # Get Temporary File Directory - which might be None - tempDir = globals.processingOptions.getCurrentOption("tempDir") - - # Store in a temporary file - try: - graphicFile = tempfile.NamedTemporaryFile( - delete=False, suffix=".PNG", dir=tempDir - ) - except IOError as e: - print("Couldn't create temporary file. md2pptx terminating") - exit() - - try: - im.save(graphicFile) - except: - print("Could not convert EPS file. Is Ghostview installed?\n") - print("Terminating.\n") - sys.exit() - - # Retrieve the temporary file name - GraphicFilename = graphicFile.name - if globals.processingOptions.getCurrentOption("exportGraphics"): - try: - shutil.copy(GraphicFilename, PNGname) - except: - print("Copy error: " + PNGname) - - else: - GraphicFilename = convertibleFilename - - return GraphicFilename, printableGraphicFilename - - -def handleGraphViz(slide, renderingRectangle, codeLines, codeType): - # Condition GraphViz source - s = graphviz.Source("\n".join(codeLines), format="png") - - # Invent a temporary filename for the rendered graphic - dotFile = "md2pptx-temporary-dotfile.png" - - # Render the .dot source as a graphic - s.render(cleanup=True, outfile=dotFile) - - # Figure out the dimensions of the rendered graphic - dotGraphicWidth, dotGraphicHeight = getGraphicDimensions(dotFile) - - # Adjust those dimensions with the usual scaling rules - (dotPicWidth, dotPicHeight, scaledByHeight) = scalePicture( - renderingRectangle.width, - renderingRectangle.height, - dotGraphicWidth, - dotGraphicHeight, - ) - - # Add the picture to the current slide - slide.shapes.add_picture( - dotFile, - renderingRectangle.left + (renderingRectangle.width - dotPicWidth) / 2, - renderingRectangle.top + (renderingRectangle.height - dotPicHeight) / 2, - dotPicWidth, - dotPicHeight, - ) - - # Delete the temporary graphic file - os.remove(dotFile) - - -def handleFunnel(slide, renderingRectangle, codeLines, codeType): - funnelColours = globals.processingOptions.getCurrentOption("funnelColours") - funnelBorderColour = globals.processingOptions.getCurrentOption("funnelBorderColour") - funnelTitleColour = globals.processingOptions.getCurrentOption("funnelTitleColour") - funnelTextColour = globals.processingOptions.getCurrentOption("funnelTextColour") - funnelLabelsPercent = globals.processingOptions.getCurrentOption("funnelLabelsPercent") - funnelLabelPosition = globals.processingOptions.getCurrentOption("funnelLabelPosition") - funnelWidest = globals.processingOptions.getCurrentOption("funnelWidest") - - f = funnel.Funnel() - - f.makeFunnel( - slide, - renderingRectangle, - codeLines, - funnelColours, - codeType, - funnelBorderColour, - funnelTitleColour, - funnelTextColour, - funnelLabelsPercent, - funnelLabelPosition, - funnelWidest, - ) - -# Handler function for immediately executing python in a code block -def handleRunPython(pythonType, prs, slide, renderingRectangle, codeLinesOrFile, codeType): - r = runPython.RunPython() - - if pythonType == "inline": - r.run(prs, slide, renderingRectangle, codeLinesOrFile, codeType) - else: - r.runFromFile(codeLinesOrFile[0], prs, slide, renderingRectangle) - -def createCodeBlock(slideInfo, slide, renderingRectangle, codeBlockNumber): - monoFont = globals.processingOptions.getCurrentOption("monoFont") - baseTextSize = globals.processingOptions.getCurrentOption("baseTextSize") - defaultBaseTextSize = globals.processingOptions.getDefaultOption("baseTextSize") - - # A variable number of newlines appear before the actual code - codeLines = slideInfo.code[codeBlockNumber] - - # Figure out code slide type - if codeLines[0].startswith(", , triple backtick line - if startswithOneOf(codeLines[0], ["
", "", "```"]):
-        codeLines.pop(0)
-
-    # Handle any trailing 
, , triple backtick line - if startswithOneOf(codeLines[-1], ["", "
", "```"]): - codeLines.pop(-1) - - codeBox = slide.shapes.add_textbox( - renderingRectangle.left, - renderingRectangle.top, - renderingRectangle.width, - renderingRectangle.height, - ) - - # Try to control text frame but SHAPE_TO_FIT_TEXT doesn't seem to work - tf = codeBox.text_frame - tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT - tf.word_wrap = False - - # Fill the code box with background colour - whether explicit or defaulted - fill = codeBox.fill - fill.solid() - fill.fore_color.rgb = RGBColor.from_string( - globals.processingOptions.getCurrentOption("codeBackground") - ) - - # Get the sole paragraph - p = codeBox.text_frame.paragraphs[0] - - # Set the font size slightly smaller than usual - if len(codeLines) >= 20: - divisor = 1.5 - else: - divisor = 1.2 - if baseTextSize > 0: - p.font.size = int(Pt(baseTextSize) / divisor) - else: - p.font.size = int(Pt(defaultBaseTextSize) / divisor) - - # Estimate how wide the code box would need to be at the current font size - # versus actual width - codeColumns = globals.processingOptions.getCurrentOption("codeColumns") - fixedPitchHeightWidthRatio = globals.processingOptions.getCurrentOption( - "fixedPitchHeightWidthRatio" - ) - - estimatedWidthVersusCodeboxWidth = ( - p.font.size * codeColumns / codeBox.width / fixedPitchHeightWidthRatio - ) - if estimatedWidthVersusCodeboxWidth > 1: - # The code is wider so shrink the font so the code fits - p.font.size = p.font.size / estimatedWidthVersusCodeboxWidth - else: - # The code is narrower so shrink the code textbox so the code just fits - # - assuming declared width is accurate - codeBox.width = int(p.font.size * codeColumns / fixedPitchHeightWidthRatio) - - # Center the code box - actually don't - 5 October 2021 temporary "fix" - # codeBox.left = int((presentation.slide_width - codeBox.width) / 2) - - # Use the code foreground colour - whether explicit or defaulted - p.font.color.rgb = RGBColor.from_string( - globals.processingOptions.getCurrentOption("codeforeground") - ) - - # Adjust code box height based on lines - codeBox.height = min( - len(codeLines) * Pt(baseTextSize + 5), renderingRectangle.height - ) - - # Add code - if codeType == "pre": - # span elements want special handling - for codeLine in codeLines: - # Resolve eg entity references - codeLine = resolveSymbols(codeLine) - - # Split the line - and maybe there are spans - spanFragments = codeLine.split(" 1: - textArray = [] - # Break line down into what will become runs - for fragmentNumber, fragment in enumerate(spanFragments): - if fragmentNumber > 0: - # Find start of span class - fragment = "") - 1] - if ( - (className in globals.bgcolors) - | (className in globals.fgcolors) - | (className in globals.emphases) - ): - afterClosingAngle = afterSpanTag[ - afterSpanTag.index(">") + 1 : - ] - startEnd = afterClosingAngle.index("") - afterSpan2 = afterClosingAngle[:startEnd] - afterSpan3 = afterClosingAngle[startEnd + 7 :] - textArray.append(["SpanClass", [className, afterSpan2]]) - textArray.append(["Normal", afterSpan3]) - fragment = "" - else: - print( - className - + " is not defined. Ignoring reference to it in element." - ) - elif spanStyleMatch := globals.spanStyleRegex.match(fragment): - afterSpanTag = fragment[spanStyleMatch.end(1) :] - styleText = afterSpanTag[7 : afterSpanTag.index(">") - 1] - styleElements = styleText.split(";") - afterClosingAngle = afterSpanTag[ - afterSpanTag.index(">") + 1 : - ] - startEnd = afterClosingAngle.index("") - afterSpan2 = afterClosingAngle[:startEnd] - afterSpan3 = afterClosingAngle[startEnd + 7 :] - textArray.append(["SpanStyle", [styleText, afterSpan2]]) - textArray.append(["Normal", afterSpan3]) - fragment = "" - else: - textArray.append(["Normal", fragment]) - - # Now we have a text array we can add the runs for the line - for textArrayItem in textArray: - textArrayItemType = textArrayItem[0] - if textArrayItemType == "Normal": - # Is not in a span element bracket - className = "" - spanStyle = "" - spanText = textArrayItem[1] - - elif textArrayItemType == "SpanClass": - # Is in a span class element bracket - className = textArrayItem[1][0] - spanText = textArrayItem[1][1] - spanStyle = "" - - else: - # Is in a span style element bracket - spanStyle = textArrayItem[1][0] - spanText = textArrayItem[1][1] - - if spanText != "": - run = p.add_run() - run.text = spanText - font = run.font - font.name = monoFont - - if className != "": - # Augment run with whatever the span class calls for - handleSpanClass(run, className) - - if spanStyle != "": - # Augment the run with whatever the style calls for - handleSpanStyle(run, spanStyle) - - # Add terminating newline - run = p.add_run() - run.text = "\n" - font = run.font - font.name = monoFont - else: - # Line has no spans in - run = p.add_run() - run.text = codeLine + "\n" - font = run.font - font.name = monoFont - - else: - # span doesn't need treating specially - for codeLine in codeLines: - # Resolve eg entity references - codeLine = resolveSymbols(codeLine) - - run = p.add_run() - run.text = codeLine + "\n" - font = run.font - font.name = monoFont - - return slide - - -def createAbstractSlide(presentation, slideNumber, titleText, paragraphs): - titleOnlyLayout = globals.processingOptions.getCurrentOption("titleOnlyLayout") - marginBase = globals.processingOptions.getCurrentOption("marginBase") - pageTitleSize = globals.processingOptions.getCurrentOption("pageTitleSize") - pageSubtitleSize = globals.processingOptions.getCurrentOption("pageSubtitleSize") - - slide = addSlide(presentation, presentation.slide_layouts[titleOnlyLayout], None) - - shapes = slide.shapes - - # Add title and constrain its size and placement - slideTitleBottom, title, flattenedTitle = formatTitle( - presentation, slide, titleText, pageTitleSize, pageSubtitleSize - ) - - reportSlideTitle(slideNumber, 3, "Abstract: " + flattenedTitle) - - # Get the rectangle the content will draw in - contentLeft, contentWidth, contentTop, contentHeight = getContentRect( - presentation, slide, slideTitleBottom, marginBase - ) - - # Add abstract text - abstractBox = slide.shapes.add_textbox( - contentLeft, - contentTop, - contentWidth, - contentHeight, - ) - - p = abstractBox.text_frame.paragraphs[0] - tf = abstractBox.text_frame - f = p.font - f.size = Pt(22) - for para, abstractParagraph in enumerate(paragraphs): - paragraphLevel, paragraphText, paragraphType = abstractParagraph - - if para > 0: - # Spacer paragraph - p = tf.add_paragraph() - f = p.font - f.size = Pt(22) - - # Content paragraph - p = tf.add_paragraph() - f = p.font - f.size = Pt(22) - addFormattedText(p, paragraphText) - - tf.word_wrap = True - - if want_numbers_content is True: - addFooter(presentation, slideNumber, slide) - - return slide - - -# Unified creation of a table or a code or a content slide -def createContentSlide(presentation, slideNumber, slideInfo): - titleOnlyLayout = globals.processingOptions.getCurrentOption("titleOnlyLayout") - contentSlideLayout = globals.processingOptions.getCurrentOption("contentSlideLayout") - marginBase = globals.processingOptions.getCurrentOption("marginBase") - pageTitleSize = globals.processingOptions.getCurrentOption("pageTitleSize") - pageSubtitleSize = globals.processingOptions.getCurrentOption("pageSubtitleSize") - - # slideInfo's body text is only filled in if there is code - and that's - # where the code - plus preamble and postamble is. - if slideInfo.code != "": - haveCode = True - else: - haveCode = False - - # Create the slide and check for bullets and/or cards - if (slideInfo.bullets == []) & (slideInfo.cards == []): - # No bullets or cards so "title only" - slideLayout = titleOnlyLayout - haveBulletsCards = False - else: - # Either bullets or cards or both so not "title only" - slideLayout = contentSlideLayout - haveBulletsCards = True - - slide = addSlide(presentation, presentation.slide_layouts[slideLayout], slideInfo) - - # Check for table / graphics content - if slideInfo.tableRows == []: - haveTableGraphics = False - else: - haveTableGraphics = True - - #################################################################### - # At this point haveCode, haveBulletsCards, haveTableGraphics have # - # been appropriately set # - #################################################################### - - # Add slide title - titleText = slideInfo.titleText - - slideTitleBottom, title, flattenedTitle = formatTitle( - presentation, slide, titleText, pageTitleSize, pageSubtitleSize - ) - - # Log slide's title - reportSlideTitle(slideNumber, 3, flattenedTitle) - - #################################################################### - # Get the dimensions of the content area to place all content in # - #################################################################### - contentLeft, contentWidth, contentTop, contentHeight = getContentRect( - presentation, slide, slideTitleBottom, marginBase - ) - - #################################################################### - # Check whether there are too many elements in the sequence to # - # render - and warn if there are. Then calculate how many to render# - #################################################################### - if len(slideInfo.sequence) > maxBlocks: - print(f"Too many blocks to render. Only {str(maxBlocks)} will be rendered.") - blocksToRender = min(maxBlocks, len(slideInfo.sequence)) - - #################################################################### - # Get the dimensions of the rectangles we'll place the graphics in # - # and their top left corner coordinates # - #################################################################### - allContentSplit = 0 - contentSplit = globals.processingOptions.getCurrentOption("contentSplit") - for b in range(blocksToRender): - allContentSplit = allContentSplit + contentSplit[b] - - verticalCursor = contentTop - horizontalCursor = contentLeft - - codeBlockNumber = 0 - tableBlockNumber = 0 - - for b in range(blocksToRender): - if globals.processingOptions.getCurrentOption("contentSplitDirection") == "vertical": - # Height and top - blockHeight = int(contentHeight * contentSplit[b] / allContentSplit) - blockTop = verticalCursor - verticalCursor = verticalCursor + blockHeight - - # Width and left - blockWidth = contentWidth - blockLeft = contentLeft - else: - # Height and top - blockHeight = contentHeight - blockTop = contentTop - - # Width and left - blockWidth = int(contentWidth * contentSplit[b] / allContentSplit) - blockLeft = horizontalCursor - horizontalCursor = horizontalCursor + blockWidth - - renderingRectangle = Rectangle(blockTop, blockLeft, blockHeight, blockWidth) - - if slideInfo.sequence[b] == "table": - createTableBlock(slideInfo, slide, renderingRectangle, tableBlockNumber) - tableBlockNumber += 1 - - elif slideInfo.sequence[b] == "list": - createListBlock(slideInfo, slide, renderingRectangle) - else: - createCodeBlock(slideInfo, slide, renderingRectangle, codeBlockNumber) - codeBlockNumber += 1 - - if want_numbers_content is True: - addFooter(presentation, slideNumber, slide) - - return slide - - -def createListBlock(slideInfo, slide, renderingRectangle): - horizontalCardGap = globals.processingOptions.getCurrentOption("horizontalcardgap") - verticalCardGap = globals.processingOptions.getCurrentOption("verticalcardgap") - cardTitleAlign = globals.processingOptions.getCurrentOption("cardtitlealign") - cardTitlePosition = globals.processingOptions.getCurrentOption("cardtitleposition") - cardShape = globals.processingOptions.getCurrentOption("cardshape") - cardLayout = globals.processingOptions.getCurrentOption("cardlayout") - cardPercent = globals.processingOptions.getCurrentOption("cardpercent") - cardShadow = globals.processingOptions.getCurrentOption("cardshadow") - cardTitleSize = globals.processingOptions.getCurrentOption("cardtitlesize") - cardBorderWidth = globals.processingOptions.getCurrentOption("cardborderwidth") - cardBorderColour = globals.processingOptions.getCurrentOption("cardbordercolour") - cardTitleColour = globals.processingOptions.getCurrentOption("cardtitlecolour") - cardTitleBackgrounds = globals.processingOptions.getCurrentOption("cardtitlebackground") - cardColours = globals.processingOptions.getCurrentOption("cardcolour") - cardDividerColour = globals.processingOptions.getCurrentOption("carddividercolour") - cardGraphicSize = globals.processingOptions.getCurrentOption("cardgraphicsize") - cardGraphicPosition = globals.processingOptions.getCurrentOption("cardGraphicPosition") - cardGraphicPadding = int(Inches(globals.processingOptions.getCurrentOption("cardgraphicpadding"))) - marginBase = globals.processingOptions.getCurrentOption("marginBase") - pageTitleSize = globals.processingOptions.getCurrentOption("pageTitleSize") - pageSubtitleSize = globals.processingOptions.getCurrentOption("pageSubtitleSize") - - # Get bulleted text shape - either for bullets above cards or first card's body shape - bulletsShape = findBodyShape(slide) - - # Set bulleted shape top, left, width - bulletsShape.top = renderingRectangle.top - bulletsShape.left = renderingRectangle.left - bulletsShape.width = renderingRectangle.width - - bulletCount = len(slideInfo.bullets) - - # Set bulleted text height - depending on whether there's a card - # Remainder is card area height - if there are cards - if slideInfo.cards == []: - # There are no cards so the bullets shape takes the whole content area - bulletsShape.height = renderingRectangle.height - - # There are no cards so the card area is zero height - cardAreaHeight = 0 - cardCount = 0 - else: - # There are cards - if bulletCount > 0: - # Bullets shape vertically shortened - bulletsShape.height = int( - renderingRectangle.height * (100 - cardPercent) / 100 - ) - - # Card area takes the rest of the content area - cardAreaHeight = int(renderingRectangle.height) - bulletsShape.height - else: - # No bullets so content is all cards - bulletsShape.height = 0 - - cardAreaHeight = renderingRectangle.height - - cardCount = len(slideInfo.cards) - - ########################################################### - # Work out card dimensions - based on the various layouts # - ########################################################### - - # card width applies to card title, card graphic, card background, card body - if cardLayout == "horizontal": - # Divide horizontal card space up - cardWidth = int( - (renderingRectangle.width - Inches(horizontalCardGap) * (cardCount - 1)) - / cardCount - ) - else: - # Card takes all the horizontal space - cardWidth = int(renderingRectangle.width) - - # Calculate title top and height - horizontal layout - if cardTitleSize > 0: - # Specified by user. "72" because in points - cardTitleHeightRaw = Inches(cardTitleSize / 72) - else: - # Shrunk to 2/3 of page title height. "72" because in points - cardTitleHeightRaw = Inches(int(10000 * pageTitleSize * 2 / 3 / 72) / 10000) - - # Adjust title height to be slightly larger than the text - cardTitleHeight = cardTitleHeightRaw + Inches(0.1) - - cardGraphicSizeRaw = int(Inches(cardGraphicSize)) - - if bulletCount > 0: - # Bullets so cards and their titles occupy less than whole height - cardAreaTop = bulletsShape.height + renderingRectangle.top - - else: - # No bullets so cards and their titles occupy whole height - cardAreaTop = renderingRectangle.top - - if cardLayout == "horizontal": - # Card takes up all the card area, vertically - cardHeight = cardAreaHeight - else: - # Card layout is horizontol so card height is just a proportion - # of the card area height - - if cardTitlePosition == "above": - paddingFactor = Inches(verticalCardGap - 0.05) - else: - paddingFactor = Inches(verticalCardGap) - - cardHeight = int((cardAreaHeight) / cardCount - paddingFactor) - - # Store slide title shape for cloning - slideTitleShape = findTitleShape(slide) - - ############################################################### - # Work out whether any card has a printable title. If not set # - # cardTitleHeight to 0 # - ############################################################### - cardWithPrintableTitle = False - - for c, card in enumerate(slideInfo.cards): - # Check if there are any titles for any of the cards - if card.title != " ": - cardWithPrintableTitle = True - - if not cardWithPrintableTitle: - # Zero card title height - cardTitleHeight = 0 - - ########################################################### - # Work out card positions - based on the various layouts # - ########################################################### - for c, card in enumerate(slideInfo.cards): - # Calculate each card's vertical position - if cardLayout == "horizontal": - # Card top is at top of card area - card.top = cardAreaTop - else: - # vertical so card tops are progressively further down the card - # area - card.top = int((cardHeight + paddingFactor) * c + cardAreaTop) - - # Calculate each card's background and body top - if cardTitlePosition == "above": - # Card title (if any) above card background - so background top is - # below card top - card.backgroundTop = card.top + cardTitleHeight - card.bodyTop = card.backgroundTop - else: - # card title (if any) inside card background - so background top is - # card top - card.backgroundTop = card.top - - # Leave room above the card body for the card title (if any) - card.bodyTop = card.backgroundTop + cardTitleHeight - - # Calculate each card's horizontal position - if cardLayout == "horizontal": - # Card lefts are progressively across the card area - card.left = marginBase + c * (cardWidth + Inches(horizontalCardGap)) - else: - # Vertical so card lefts are at the left of the card area - card.left = marginBase - - # Card title modeled on slide title - but smaller - cardTitleShape = addClonedShape(slide, slideTitleShape) - - card.titleShape = cardTitleShape - - if card.graphic != "": - # This card has a graphic so get its name - card.graphicDimensions = getGraphicDimensions(card.graphic) - - # Create card graphic shape - to be resized later - card.graphicShape = slide.shapes.add_picture( - card.graphic, - Inches(0), - Inches(0), - ) - - reportGraphicFilenames(card.printableFilename) - - if (card.mediaURL is not None) | (card.graphicTitle is not None): - mediaURL = card.mediaURL - - graphicTitle = "" if card.graphicTitle == None else card.graphicTitle - - pictureInfos.append( - (card.graphicShape, mediaURL, graphicTitle) - ) - - elif card.mediaInfo is not None: - # This card has a video so get its dimensions etc - card.mediaDimensions = getVideoInfo(card.mediaInfo) - - # Create card video shape - to be resized later - card.mediaShape = slide.shapes.add_movie( - card.mediaInfo.source, - Inches(0), - Inches(0), - Inches(0), - Inches(0), - card.mediaInfo.poster - ) - - reportGraphicFilenames(card.printableFilename) - - else: - # Some of this is probably not needed - card.graphicDimensions = None - card.mediaDimensions = None - - # Clear text from cloned title and add in the title text - cardTitleShape.text_frame.paragraphs[0].text = "" - addFormattedText(cardTitleShape.text_frame.paragraphs[0], card.title) - - # Set card title font size - if cardTitleSize > 0: - # Explicitly set title size - cardTitleShape.text_frame.paragraphs[0].font.size = Pt(cardTitleSize) - else: - # Not explicitly set - so default to 2/3 slide title size - cardTitleShape.text_frame.paragraphs[0].font.size = Pt( - pageTitleSize * 2 / 3 - ) - - # Titles are aligned one of three ways - if cardTitleAlign == "l": - cardTitleShape.text_frame.paragraphs[0].alignment = PP_ALIGN.LEFT - elif cardTitleAlign == "c": - cardTitleShape.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER - else: - cardTitleShape.text_frame.paragraphs[0].alignment = PP_ALIGN.RIGHT - - # Fill in card's background - if necessary - if (cardTitleBackgrounds[0] != ("None", "")) & ( - cardTitlePosition != "inside" - ): - # Card title background picked round-robin from array - cardTitleBackground = cardTitleBackgrounds[ - c % len(cardTitleBackgrounds) - ] - - fill = cardTitleShape.fill - fill.solid() - - setColour(fill.fore_color, cardTitleBackground) - - # Create card background and make sure it's behind the card body - # (and maybe card title) - if cardShape == "rounded": - # Rounded Rectangle for card - cardBackgroundShape = slide.shapes.add_shape( - MSO_SHAPE.ROUNDED_RECTANGLE, - Inches(0), - Inches(0), - Inches(0), - Inches(0), - ) - - # Rounding adjustment works better with different values for horizontal and vertical cards - if cardLayout == "horizontal": - # Make the rounding radius small. This is 1/4 the default - cardBackgroundShape.adjustments[0] = 0.0416675 - else: - # Make the rounding radius smallish. This is 1/2 the default - cardBackgroundShape.adjustments[0] = 0.083335 - else: - # Squared-corner Rectangle for card - cardBackgroundShape = slide.shapes.add_shape( - MSO_SHAPE.RECTANGLE, Inches(0), Inches(0), Inches(0), Inches(0) - ) - - card.backgroundShape = cardBackgroundShape - - if cardShape == "line": - # Ensure no fill for card background - cardBackgroundShape.fill.background() - - # Ensure card background is at the back - sendToBack(slide.shapes, cardBackgroundShape) - - # Card shape modeled on bulleted list - if (bulletCount > 0) | (c > 0): - # Copy the bullets shape for not-first card body shape - cardBodyShape = addClonedShape(slide, bulletsShape) - else: - # Co-opt bullets shape as first card shape - cardBodyShape = bulletsShape - - card.bodyShape = cardBodyShape - - # Make card's body transparent - fill = cardBodyShape.fill - fill.background() - - # Fill in card's background - if necessary - if (cardColours[0] != ("None", "")) & (cardShape != "line"): - # Card background volour picked round-robin from array - cardColour = cardColours[c % len(cardColours)] - - fill = cardBackgroundShape.fill - fill.solid() - - setColour(fill.fore_color, cardColour) - - ####################################################################### - # Adjust bullets shape height - and calculate verticals for any cards # - ####################################################################### - - # Set bottom of bullets shape - bulletsShape.bottom = bulletsShape.top + bulletsShape.height - - # Fill in the main bullets shape - renderText(bulletsShape, slideInfo.bullets) - - # Second go on each card - for c, card in enumerate(slideInfo.cards): - # Get the shapes for this card - including any graphic - cardBackgroundShape = card.backgroundShape - cardTitleShape = card.titleShape - cardBodyShape = card.bodyShape - cardGraphicShape = card.graphicShape - cardMediaShape = card.mediaShape - - # Get dimensions of any graphic - if cardGraphicShape is not None : - cardMediaNativeWidth, cardMediaNativeHeight = card.graphicDimensions - - elif cardMediaShape is not None: - cardMediaNativeWidth, cardMediaNativeHeight, _, _ = card.mediaDimensions - - else: - cardMediaNativeWidth, cardMediaNativeHeight = (0, 0) - - # Set the card shapes' width - cardBackgroundShape.width = cardWidth - if cardLayout == "horizontal": - cardBodyShape.width = cardWidth - else: - cardBodyShape.width = cardWidth - cardGraphicSizeRaw - - cardTitleShape.width = cardWidth - - # Set the card shapes' left side - cardBackgroundShape.left = card.left - cardTitleShape.left = card.left - if (cardLayout == "horizontal") | (cardGraphicPosition == "after"): - cardBodyShape.left = card.left - else: - cardBodyShape.left = card.left + cardGraphicSizeRaw + \ - 2 * cardGraphicPadding - - # Position card title - cardTitleShape.top = card.top - cardTitleShape.height = cardTitleHeight - - # Colour the title - if cardTitleColour specified - if cardTitleColour != ("None", ""): - setColour( - cardTitleShape.text_frame.paragraphs[0].font.color, cardTitleColour - ) - - # Calculate positions and heights within card background of body - if cardTitlePosition == "above": - # Any card titles would be above the rest of the card - cardBackgroundShape.top = card.top + cardTitleHeight - cardBodyHeight = cardHeight - cardTitleHeight - cardBackgroundShape.height = cardBodyHeight - else: - # Any card titles would be within the rest of the card - cardBackgroundShape.top = card.top - cardBodyHeight = cardHeight - cardTitleHeight - cardBackgroundShape.height = cardHeight - - # Create any dividing line - if (c > 0) & (cardShape == "line"): - if cardLayout == "horizontal": - # Dividing line is to the left of the card - dividingLine = createLine( - cardBackgroundShape.left - int(Inches(horizontalCardGap / 2)), - cardBackgroundShape.top + Inches(0.75), - cardBackgroundShape.left - int(Inches(horizontalCardGap / 2)), - cardBackgroundShape.top + cardBackgroundShape.height - Inches(0.75), - slide.shapes, - ) - else: - # Dividing line is above the card - dividingLine = createLine( - cardBackgroundShape.left + Inches(0.75), - cardBackgroundShape.top - int(Inches(verticalCardGap / 2)), - cardBackgroundShape.left - + cardBackgroundShape.width - - int(Inches(0.75)), - cardBackgroundShape.top - int(Inches(verticalCardGap / 2)), - slide.shapes, - ) - - # Perhaps set the line colour - if cardDividerColour != ("None", ""): - setColour(dividingLine.line.color, cardDividerColour) - - # Set the line width to a fixed 2pts - dividingLine.line.width = Pt(2.0) - - # Position card body shape - if (cardGraphicShape == None) & (cardMediaShape == None): - # No graphic on this card - cardBodyShape.top = card.bodyTop - cardBodyShape.height = cardBodyHeight - cardBodyShape.left = card.left - cardBodyShape.width = cardWidth - else: - # Make room for graphic, audio, or video - if cardGraphicPosition == "before": - # Graphic before - if cardLayout == "horizontal": - # Leave room above card body shape for graphic - cardBodyShape.top = card.bodyTop + cardGraphicSizeRaw + \ - 2 * cardGraphicPadding - else: - # Don't leave room above card for graphic - cardBodyShape.top = card.bodyTop - cardBodyShape.width = cardWidth - 2 * cardGraphicPadding - \ - cardGraphicSizeRaw - - else: - # graphic after - # Leave room below card body shape for graphic - cardBodyShape.top = card.bodyTop - - if cardLayout == "vertical": - cardBodyShape.width = cardWidth - 2 * cardGraphicPadding - \ - cardGraphicSizeRaw - - if cardLayout == "horizontal": - # Calculate card body shape height, leaving room for any graphic - cardBodyShape.height = cardBodyHeight - cardGraphicSizeRaw - \ - 2 * cardGraphicPadding - else: - cardBodyShape.height = cardBodyHeight - - # Scale graphic, audio, or video - (cardMediaWidth, cardMediaHeight, scaledByHeight) = scalePicture( - cardGraphicSizeRaw, - cardGraphicSizeRaw, - cardMediaNativeWidth, - cardMediaNativeHeight, - ) - - if cardGraphicShape is not None: - cardMediaShape = cardGraphicShape - else: - cardMediaShape = cardMediaShape - - # Vertically position graphic shape - if cardGraphicPosition == "before": - # Graphic before card text - if cardLayout == "horizontal": - cardMediaShape.top = cardTitleShape.top + cardGraphicPadding + \ - cardTitleShape.height + int((cardGraphicSizeRaw - cardMediaHeight) / 2) - else: - cardMediaShape.top = cardBodyShape.top + \ - int((cardBodyHeight - cardMediaHeight) / 2) - else: - # Graphic after card text - if cardLayout == "horizontal": - cardMediaShape.top = cardBodyShape.height + cardBodyShape.top + \ - cardGraphicPadding - else: - cardMediaShape.top = cardBodyShape.top + \ - int((cardBodyHeight - cardGraphicSizeRaw) / 2) - - # Horizontally position card graphic shape - if cardLayout == "horizontal": - cardMediaShape.left = cardTitleShape.left + \ - int((cardTitleShape.width - cardMediaWidth) / 2) - else: - if cardGraphicPosition == "before": - cardMediaShape.left = card.left + cardGraphicPadding - else: - cardMediaShape.left = card.left + cardBodyShape.width + \ - cardGraphicPadding - - # Set dimensions of card graphic shape - cardMediaShape.width = int(cardMediaWidth) - cardMediaShape.height = int(cardMediaHeight) - - # Render any card body text - if card.bullets != "": - renderText(cardBodyShape, card.bullets) - - # Handle card background border line - lf = cardBackgroundShape.line - - if (cardBorderColour != ("None", "")) & (cardShape != "line"): - # Set border line colour - setColour(lf.color, cardBorderColour) - - if cardShape == "line": - # Lines between cards means cards have no border - lf.fill.background() - elif cardBorderWidth > 0: - # Non-zero border line width - lf.width = Pt(cardBorderWidth) - elif cardBorderWidth == 0: - # Zero border line width - lf.fill.background() - - # Create any card shadow - if cardShadow: - createShadow(cardBackgroundShape) - - return slide - - -def createTableBlock(slideInfo, slide, renderingRectangle, tableBlockNumber): - tableRows = slideInfo.tableRows[tableBlockNumber] - tableMargin = globals.processingOptions.getCurrentOption("tableMargin") - marginBase = globals.processingOptions.getCurrentOption("marginBase") - baseTextSize = globals.processingOptions.getCurrentOption("baseTextSize") - tableShadow = globals.processingOptions.getCurrentOption("tableShadow") - - printableTopLeftGraphicFilename = "" - printableTopRightGraphicFilename = "" - printablebottomLeftGraphicFilename = "" - printableBottomRightGraphicFilename = "" - - # Handle table body - if (len(tableRows) <= 2) & (len(tableRows[0]) <= 2): - # This is a table with 1 or 2 rows and 1 or 2 columns - isGraphicsGrid = True - gridRows = len(tableRows) - if gridRows == 1: - gridColumns = len(tableRows[0]) - else: - gridColumns = max(len(tableRows[0]), len(tableRows[1])) - - topGraphicCount = 0 - - topLeftCellString = tableRows[0][0] - # Attempt to retrieve media information for left side - top row - ( - topLeftGraphicTitle, - topLeftGraphicFilename, - printableTopLeftGraphicFilename, - topLeftGraphicHref, - topLeftHTML, - topLeftVideo, - topGraphicCount, - ) = parseMedia(topLeftCellString, topGraphicCount) - - # Attempt to retrieve filename for right side - top row - if len(tableRows[0]) == 2: - topRightCellString = tableRows[0][1] - else: - topRightCellString = "" - - ( - topRightGraphicTitle, - topRightGraphicFilename, - printableTopRightGraphicFilename, - topRightGraphicHref, - topRightHTML, - topRightVideo, - topGraphicCount, - ) = parseMedia(topRightCellString, topGraphicCount) - - if topGraphicCount == 0: - # Revert to normal table processing as no graphic spec in at least one cell - isGraphicsGrid = False - - if gridRows == 2: - # Attempt to retrieve filename for left side - bottom row - bottomGraphicCount = 0 - - bottomLeftCellString = tableRows[1][0] - - # Attempt to retrieve media information for left side - bottom row - ( - bottomLeftGraphicTitle, - bottomLeftGraphicFilename, - printableBottomLeftGraphicFilename, - bottomLeftGraphicHref, - bottomLeftHTML, - bottomLeftVideo, - bottomGraphicCount, - ) = parseMedia(bottomLeftCellString, bottomGraphicCount) - - # Attempt to retrieve filename for right side - bottom row - if gridColumns == 2: - if len(tableRows[1]) == 1: - # There is one cell in bottom row so this is centred "3-up" - bottomRightCellString = "" - else: - bottomRightCellString = tableRows[1][1] - else: - bottomRightCellString = "" - - ( - bottomRightGraphicTitle, - bottomRightGraphicFilename, - printableBottomRightGraphicFilename, - bottomRightGraphicHref, - bottomRightHTML, - bottomRightVideo, - bottomGraphicCount, - ) = parseMedia(bottomRightCellString, bottomGraphicCount) - - if bottomGraphicCount == 0: - # Revert to normal table processing as no graphic spec in at least one cell - isGraphicsGrid = False - - else: - # This is a normal table because it has too many rows or columns to be a graphics grid - isGraphicsGrid = False - - if isGraphicsGrid == True: - - #################################################################### - # Print the media filenames # - #################################################################### - if gridColumns == 2: - # Doing 1- or 2-row side-by-side graphics slide - reportGraphicFilenames( - printableTopLeftGraphicFilename, printableTopRightGraphicFilename - ) - else: - # Doing 2 row, single column graphics slide - reportGraphicFilenames(printableTopLeftGraphicFilename) - - if gridRows == 2: - # Second row of filenames - if gridColumns == 2: - reportGraphicFilenames( - printableBottomLeftGraphicFilename, - printableBottomRightGraphicFilename, - ) - else: - reportGraphicFilenames(printableBottomLeftGraphicFilename) - - #################################################################### - # Get the media dimensions # - #################################################################### - if topLeftGraphicFilename != "": - topLeftMediaWidth, topLeftMediaHeight = getGraphicDimensions( - topLeftGraphicFilename - ) - if topLeftMediaWidth == -1: - if gridRows == 2: - print( - "Missing top left image file: " - + printableTopLeftGraphicFilename - ) - else: - print("Missing left image file: " + printableTopLeftGraphicFilename) - - return slide - - elif topLeftVideo is not None: - ( - topLeftMediaWidth, - topLeftMediaHeight, - topLeftVideoType, - topLeftVideoData, - ) = getVideoInfo(topLeftVideo) - - if topLeftMediaWidth == -1: - if gridRows == 2: - print( - "Missing top left video file: " - + printableTopLeftGraphicFilename - ) - else: - print("Missing left video file: " + printableTopLeftGraphicFilename) - - return slide - - if gridColumns == 2: - # Get top right image dimensions - if topRightGraphicFilename != "": - topRightMediaWidth, topRightMediaHeight = getGraphicDimensions( - topRightGraphicFilename - ) - if topRightMediaWidth == -1: - if gridRows == 2: - print( - "Missing top right image file: " - + printableTopRightGraphicFilename - ) - else: - print( - "Missing right image file: " - + printableTopRightGraphicFilename - ) - - return slide - - elif topRightVideo is not None: - ( - topRightMediaWidth, - topRightMediaHeight, - topRightVideoType, - topRightVideoData, - ) = getVideoInfo(topRightVideo) - - if topRightMediaWidth == -1: - if gridRows == 2: - print( - "Missing top right video file: " - + printableTopRightGraphicFilename - ) - else: - print( - "Missing right video file: " - + printableTopRightGraphicFilename - ) - - return slide - - if gridRows == 2: - # Get bottom left image dimensions - if bottomLeftGraphicFilename != "": - bottomLeftMediaWidth, bottomLeftMediaHeight = getGraphicDimensions( - bottomLeftGraphicFilename - ) - if bottomLeftMediaWidth == -1: - print( - "Missing bottom left image file: " - + printableBottomLeftGraphicFilename - ) - return slide - - elif bottomLeftVideo is not None: - ( - bottomLeftMediaWidth, - bottomLeftMediaHeight, - bottomLeftVideoType, - bottomLeftVideoData, - ) = getVideoInfo(bottomLeftVideo) - - if bottomLeftMediaWidth == -1: - if gridRows == 2: - print( - "Missing bottom left video file: " - + printableBottomLeftGraphicFilename - ) - else: - print( - "Missing left video file: " - + printableBottomLeftGraphicFilename - ) - - return slide - - if gridColumns == 2: - # Get bottom right image dimensions - if bottomRightGraphicFilename != "": - ( - bottomRightMediaWidth, - bottomRightMediaHeight, - ) = getGraphicDimensions(bottomRightGraphicFilename) - - if bottomRightMediaWidth == -1: - print( - "Missing bottom right image file: " - + printableBottomRightGraphicFilename - ) - - return slide - - elif bottomRightVideo is not None: - ( - bottomRightMediaWidth, - bottomRightMediaHeight, - bottomRightVideoType, - bottomRightVideoData, - ) = getVideoInfo(bottomRightVideo) - - if bottomRightMediaWidth == -1: - if gridRows == 2: - print( - "Missing bottom right video file: " - + printableBottomRightGraphicFilename - ) - else: - print( - "Missing right video file: " - + printableBottomRightGraphicFilename - ) - - return slide - - # Calculate maximum picture height on slide - maxPicHeight = renderingRectangle.height - - if gridRows == 2: - # Adjusted if two rows - maxPicHeight = maxPicHeight / 2 + Inches(0.2) - - # Calculate maximum picture width on slide - maxPicWidth = renderingRectangle.width - if gridColumns == 2: - # Adjusted if two columns - maxPicWidth = maxPicWidth / 2 - marginBase - - # Calculate horizontal middle of graphics space - midGraphicsSpaceX = renderingRectangle.left + renderingRectangle.width / 2 - - #################################################################### - # Calculate the size of each graphic - scaled by the above rect # - #################################################################### - - if (topLeftGraphicFilename != "") | (topLeftVideo is not None): - ( - topLeftPicWidth, - topLeftPicHeight, - usingHeightToScale, - ) = scalePicture( - maxPicWidth, maxPicHeight, topLeftMediaWidth, topLeftMediaHeight - ) - - if usingHeightToScale: - # Calculate horizontal start - if (gridColumns == 2) and ( - (topRightGraphicFilename != "") | (topRightVideo is not None) - ): - # Align top left media item to the left - topLeftPicX = ( - renderingRectangle.left - + (midGraphicsSpaceX - marginBase - topLeftPicWidth) / 2 - ) - else: - # Center sole top media item - topLeftPicX = midGraphicsSpaceX - topLeftPicWidth / 2 - else: - # Calculate horizontal start - if (gridColumns == 2) and ( - (topRightGraphicFilename != "") | (topRightVideo is not None) - ): - # Align top left media item to the left - topLeftPicX = renderingRectangle.left - else: - # Center sole top media item - topLeftPicX = midGraphicsSpaceX - topLeftPicWidth / 2 - - # Calculate vertical start - topLeftPicY = renderingRectangle.top + (maxPicHeight - topLeftPicHeight) / 2 - - if gridRows == 2: - topLeftPicY -= Inches(0.2) - - if topLeftGraphicFilename != "": - topLeftPicture = slide.shapes.add_picture( - topLeftGraphicFilename, - topLeftPicX, - topLeftPicY, - topLeftPicWidth, - topLeftPicHeight, - ) - - if topLeftGraphicHref == "": - topLeftGraphicHref = None - - pictureInfos.append( - (topLeftPicture, topLeftGraphicHref, topLeftGraphicTitle) - ) - elif topLeftVideo is not None: - if topLeftVideoType == "Local": - # Can use local file directly - topLeftVideoShape = slide.shapes.add_movie( - topLeftVideo.source, - topLeftPicX, - topLeftPicY, - topLeftPicWidth, - topLeftPicHeight, - topLeftVideo.poster, - ) - else: - # First copy video data to temporary file - tempVideoFile = tempfile.NamedTemporaryFile( - delete=False, suffix="mp4", dir=tempDir - ) - tempVideoFile.write(topLeftVideoData) - convertibleFilename = tempVideoFile.name - tempVideoFile.close() - - # Use temporary file to make video - topLeftVideo = slide.shapes.add_movie( - convertibleFilename, - topLeftPicX, - topLeftPicY, - topLeftPicWidth, - topLeftPicHeight, - topLeftVideo.poster, - ) - - if gridColumns == 2: - # Top right media item - if (topRightGraphicFilename != "") | (topRightVideo is not None): - ( - topRightPicWidth, - topRightPicHeight, - usingHeightToScale, - ) = scalePicture( - maxPicWidth, maxPicHeight, topRightMediaWidth, topRightMediaHeight - ) - - if usingHeightToScale: - # Calculate horizontal start - topRightPicX = ( - renderingRectangle.width + midGraphicsSpaceX - topRightPicWidth - ) / 2 - else: - # Calculate horizontal start - topRightPicX = ( - renderingRectangle.width + midGraphicsSpaceX - topRightPicWidth - ) / 2 - - # Calculate vertical start - topRightPicY = ( - renderingRectangle.top + (maxPicHeight - topRightPicHeight) / 2 - ) - - if gridRows == 2: - topRightPicY -= Inches(0.2) - - if topRightGraphicFilename != "": - topRightPicture = slide.shapes.add_picture( - topRightGraphicFilename, - topRightPicX, - topRightPicY, - topRightPicWidth, - topRightPicHeight, - ) - - if topRightGraphicHref == "": - topRightGraphicHref = None - - pictureInfos.append( - (topRightPicture, topRightGraphicHref, topRightGraphicTitle) - ) - - elif topRightVideo is not None: - if topRightVideoType == "Local": - # Can use local file directly - topRightVideoShape = slide.shapes.add_movie( - topRightVideo.source, - topRightPicX, - topRightPicY, - topRightPicWidth, - topRightPicHeight, - topRightVideo.poster, - ) - else: - # First copy video data to temporary file - tempVideoFile = tempfile.NamedTemporaryFile( - delete=False, suffix="mp4", dir=tempDir - ) - tempVideoFile.write(topRightVideoData) - convertibleFilename = tempVideoFile.name - tempVideoFile.close() - - # Use temporary file to make video - topRightVideo = slide.shapes.add_movie( - convertibleFilename, - topRightPicX, - topRightPicY, - topRightPicWidth, - topRightPicHeight, - topRightVideo.poster, - ) - - if gridRows == 2: - # Need second row of media items - # Bottom left media item - if (bottomLeftGraphicFilename != "") | (bottomLeftVideo is not None): - ( - bottomLeftPicWidth, - bottomLeftPicHeight, - usingHeightToScale, - ) = scalePicture( - maxPicWidth, - maxPicHeight, - bottomLeftMediaWidth, - bottomLeftMediaHeight, - ) - - if usingHeightToScale: - # Calculate horizontal start - if (gridColumns == 2) & ( - (bottomRightGraphicFilename != "") - | (bottomRightVideo is not None) - ): - bottomLeftPicX = ( - marginBase - + (midGraphicsSpaceX - marginBase - bottomLeftPicWidth) / 2 - ) - else: - bottomLeftPicX = midGraphicsSpaceX - bottomLeftPicWidth / 2 - else: - # Calculate horizontal start - if (gridColumns == 2) and (bottomRightGraphicFilename != ""): - # Align bottom left picture to the left - bottomLeftPicX = marginBase - else: - # Center sole bottom media item - bottomLeftPicX = midGraphicsSpaceX - bottomLeftPicWidth / 2 - - # Calculate vertical start - bottomLeftPicY = ( - renderingRectangle.top + (maxPicHeight + bottomLeftPicHeight) / 2 - ) - - if gridRows == 2: - bottomLeftPicY -= Inches(0.2) - - if bottomLeftGraphicFilename != "": - bottomLeftPicture = slide.shapes.add_picture( - bottomLeftGraphicFilename, - bottomLeftPicX, - bottomLeftPicY, - bottomLeftPicWidth, - bottomLeftPicHeight, - ) - - if bottomLeftGraphicHref == "": - bottomLeftGraphicHref = None - - pictureInfos.append( - (bottomLeftPicture, bottomLeftGraphicHref, bottomLeftGraphicTitle) - ) - - elif bottomLeftVideo is not None: - if bottomLeftVideoType == "Local": - # Can use local file directly - bottomLeftVideoShape = slide.shapes.add_movie( - bottomLeftVideo.source, - bottomLeftPicX, - bottomLeftPicY, - bottomLeftPicWidth, - bottomLeftPicHeight, - bottomLeftVideo.poster, - ) - else: - # First copy video data to temporary file - tempVideoFile = tempfile.NamedTemporaryFile( - delete=False, suffix="mp4", dir=tempDir - ) - tempVideoFile.write(bottomLeftVideoData) - convertibleFilename = tempVideoFile.name - tempVideoFile.close() - - # Use temporary file to make video - bottomLeftVideo = slide.shapes.add_movie( - convertibleFilename, - bottomLeftPicX, - bottomLeftPicY, - bottomLeftPicWidth, - bottomLeftPicHeight, - bottomLeftVideo.poster, - ) - - if gridColumns == 2: - # Bottom right media item - if (bottomRightGraphicFilename != "") | (bottomRightVideo is not None): - ( - bottomRightPicWidth, - bottomRightPicHeight, - usingHeightToScale, - ) = scalePicture( - maxPicWidth, - maxPicHeight, - bottomRightMediaWidth, - bottomRightMediaHeight, - ) - - if usingHeightToScale: - # Calculate horizontal start - bottomRightPicX = ( - renderingRectangle.width - + midGraphicsSpaceX - - bottomRightPicWidth - ) / 2 - - else: - # Use the width to scale - # Calculate horizontal start - bottomRightPicX = ( - renderingRectangle.width - + midGraphicsSpaceX - - bottomRightPicWidth - ) / 2 - - # Calculate vertical start - bottomRightPicY = ( - renderingRectangle.top - + (maxPicHeight + bottomRightPicHeight) / 2 - ) - - if gridRows == 2: - bottomRightPicY -= Inches(0.2) - - if bottomRightGraphicFilename != "": - if bottomRightGraphicFilename != "": - bottomRightPicture = slide.shapes.add_picture( - bottomRightGraphicFilename, - bottomRightPicX, - bottomRightPicY, - bottomRightPicWidth, - bottomRightPicHeight, - ) - - if bottomRightGraphicHref == "": - bottomRightGraphicHref = None - - pictureInfos.append( - ( - bottomRightPicture, - bottomRightGraphicHref, - bottomRightGraphicTitle, - ) - ) - elif bottomRightVideo is not None: - if bottomRightVideoType == "Local": - # Can use local file directly - bottomRightVideoShape = slide.shapes.add_movie( - bottomRightVideo.source, - bottomRightPicX, - bottomRightPicY, - bottomRightPicWidth, - bottomRightPicHeight, - bottomRightVideo.poster, - ) - else: - # First copy video data to temporary file - tempVideoFile = tempfile.NamedTemporaryFile( - delete=False, suffix="mp4", dir=tempDir - ) - tempVideoFile.write(bottomRightVideoData) - convertibleFilename = tempVideoFile.name - tempVideoFile.close() - - # Use temporary file to make video - bottomRightVideo = slide.shapes.add_movie( - convertibleFilename, - bottomRightPicX, - bottomRightPicY, - bottomRightPicWidth, - bottomRightPicHeight, - bottomRightVideo.poster, - ) - - else: - ################ - # # - # Normal table # - # # - ################ - - # Calculate maximum number of columns - as this is how wide we'll make the table - columns = 0 - for row in tableRows: - columns = max(columns, len(row)) - - alignments = [] - widths = [] - - # Adjust table if it contains a dash line as it's second line - if len(tableRows) > 1: - firstCellSecondRow = tableRows[1][0] - if (firstCellSecondRow.startswith("-")) | ( - firstCellSecondRow.startswith(":-") - ): - haveTableHeading = True - else: - haveTableHeading = False - else: - haveTableHeading = False - - if haveTableHeading is True: - # Has table heading - tableHeadingBlurb = " with heading" - - # Figure out alignments of cells - for cell in tableRows[1]: - if cell.startswith(":-"): - if cell.endswith("-:"): - alignments.append("c") - else: - alignments.append("l") - elif cell.endswith("-:"): - alignments.append("r") - else: - alignments.append("l") - - widths.append(cell.count("-")) - - # Default any missing columns to left / single width - if len(tableRows[1]) < columns: - for _ in range(columns - len(tableRows[1])): - alignments.append("l") - widths.append(1) - - widths_total = sum(widths) - - # Remove this alignment / widths row from the table - del tableRows[1] - else: - # No table heading - tableHeadingBlurb = " without heading" - - # Use default width - 1 - and default alignment - l - for c in range(columns): - widths.append(1) - alignments.append("l") - - # We don't know the widths so treat all equal - widths_total = columns - - # Calculate number of rows - rows = len(tableRows) - alignments_count = len(alignments) - - # Create the table with the above number of rows and columns - newTableShape = slide.shapes.add_table(rows, columns, 0, 0, 0, 0) - - newTable = newTableShape.table - - newTableShape.top = renderingRectangle.top - newTableShape.left = renderingRectangle.left + tableMargin - marginBase - newTableShape.height = min(renderingRectangle.height, Inches(0.25) * rows) - newTableShape.width = renderingRectangle.width - 2 * (tableMargin - marginBase) - shapeWidth = newTableShape.width - - # Perhaps create a drop shadow for a table - if tableShadow: - createShadow(newTable) - - # Set whether first row is not special - newTable.first_row = haveTableHeading - - print( - " --> " - + str(rows) - + " x " - + str(columns) - + " table" - + tableHeadingBlurb - ) - - # Set column widths - cols = newTable.columns - for colno in range(columns): - cols[colno].width = int(shapeWidth * widths[colno] / widths_total) - - # Get options for filling in the cells - compactTables = globals.processingOptions.getCurrentOption("compactTables") - spanCells = globals.processingOptions.getCurrentOption("spanCells") - tableHeadingSize = globals.processingOptions.getCurrentOption("tableHeadingSize") - - # Fill in the cells - for rowNumber, row in enumerate(tableRows): - # Add dummy cells to the end of the row so that there are as many - # cells in the row as there are columns in the table - cellCount = len(row) - - # Unless there is a non-empty cell there is no anchor cell for this row - if spanCells == "yes": - potentialAnchorCell = None - - for c in range(cellCount, columns): - row.append("") - - for columnNumber, cell in enumerate(row): - newCell = newTable.cell(rowNumber, columnNumber) - - if spanCells == "yes": - if cell != "": - potentialAnchorCell = newCell - else: - if potentialAnchorCell is not None: - # Might need to remove previous cell merge - if potentialAnchorCell.span_width > 1: - potentialAnchorCell.split() - - # Merge the cells from the anchor up to this one - potentialAnchorCell.merge(newCell) - - # For compact table remove the margins around the text - if compactTables > 0: - newCell.margin_top = Pt(0) - newCell.margin_bottom = Pt(0) - - newCell.text = "" - text_frame = newCell.text_frame - - # Set cell's text alignment - p = text_frame.paragraphs[0] - - # Set cell's text size - if necessary - if baseTextSize > 0: - p.font.size = Pt(baseTextSize) - - # For compact table use specified point size for text - if compactTables > 0: - p.font.size = Pt(compactTables) - - if (rowNumber == 0) & (tableHeadingSize > 0): - p.font.size = Pt(tableHeadingSize) - - if columnNumber >= alignments_count: - p.alignment = PP_ALIGN.LEFT - elif alignments[columnNumber] == "r": - p.alignment = PP_ALIGN.RIGHT - elif alignments[columnNumber] == "c": - p.alignment = PP_ALIGN.CENTER - else: - p.alignment = PP_ALIGN.LEFT - - addFormattedText(p, cell) - - # Apply table border styling - whether there is any or not - applyTableLineStyling( - newTable, - globals.processingOptions, - ) - - return slide - - -def createChevron( - text, - x, - y, - width, - height, - filled, - shapes, - fontSize, - wantLink, - unhighlightedBackground, -): - global TOCruns - - # Create shape - shape = shapes.add_shape(MSO_SHAPE.CHEVRON, x, y, width, height) - - # Set shape's text - shape.text = text - - # Set shape's text attributes - tf = shape.text_frame - p = tf.paragraphs[0] - f = p.font - f.size = Pt(fontSize) - f.color.rgb = RGBColor(0, 0, 0) - - # If want link create it from the first run - if wantLink: - TOCruns.append(p.runs[0]) - - # Set shape's outline attributes - shape.line.color.rgb = RGBColor(0, 0, 0) - shape.line.width = Pt(1.0) - - # Potentially fill background - if filled is False: - shape.fill.background() - else: - if wantLink & (unhighlightedBackground != ""): - shape.fill.solid() - shape.fill.fore_color.rgb = RGBColor.from_string(unhighlightedBackground) - - -def createOval( - text, - x, - y, - width, - height, - filled, - shapes, - fontSize, - wantLink, - unhighlightedBackground, -): - global TOCruns - - # Create shape - shape = shapes.add_shape(MSO_SHAPE.OVAL, x, y, width, height) - - # Set shape's text - shape.text = text - - # Set shape's text attributes - tf = shape.text_frame - p = tf.paragraphs[0] - p.alignment = PP_ALIGN.CENTER - f = p.font - f.size = Pt(fontSize) - f.color.rgb = RGBColor(0, 0, 0) - - # If want link create it from the first run - if wantLink: - TOCruns.append(p.runs[0]) - - # Set shape's outline attributes - shape.line.color.rgb = RGBColor(191, 191, 191) - shape.line.width = Pt(1.0) - - # Potentially fill background - if filled is False: - shape.fill.background() - shape.line.width = Pt(3.0) - else: - if wantLink & (unhighlightedBackground != ""): - shape.fill.solid() - shape.fill.fore_color.rgb = RGBColor.from_string(unhighlightedBackground) - - -def createLine(x0, y0, x1, y1, shapes, colour=("RGB", "#BFBFBF"), width=4.0): - # Create line - line = shapes.add_shape(MSO_SHAPE.LINE_INVERSE, x0, y0, x1 - x0, y1 - y0) - - # Set shape's outline attributes - setColour(line.line.color, colour) - - line.line.width = Pt(width) - - return line - - -def delinkify(text): - if linkMatch := linkRegex.match(text): - linkText = linkMatch.group(1) - linkURL = linkMatch.group(2) - return (linkText, linkURL) - - elif linkMatch := indirectReferenceUsageRegex(text): - print(linkMatch.group(1)) - print(linkMatch.group(2)) - return (text, "") - - else: - return (text, "") - - -def createTOCSlide(presentation, slideNumber, titleText, bullets, tocStyle): - global SectionSlides - titleOnlyLayout = globals.processingOptions.getCurrentOption("titleOnlyLayout") - blankLayout = globals.processingOptions.getCurrentOption("blankLayout") - tocTitle = globals.processingOptions.getCurrentOption("tocTitle") - marginBase = globals.processingOptions.getCurrentOption("marginBase") - pageTitleSize = globals.processingOptions.getCurrentOption("pageTitleSize") - pageSubtitleSize = globals.processingOptions.getCurrentOption("pageSubtitleSize") - - if tocStyle != "plain": - if titleText == tocTitle: - reportSlideTitle( - slideNumber, 3, f'Table Of Contents (Style: "{tocStyle}") {titleText}' - ) - - else: - reportSlideTitle(slideNumber, 2, titleText) - - if tocStyle == "plain": - if titleText != tocTitle: - slide = createTitleOrSectionSlide( - presentation, - slideNumber, - titleText, - globals.processingOptions.getCurrentOption("sectionSlideLayout"), - globals.processingOptions.getCurrentOption("sectionTitleSize"), - slideInfo.subtitleText, - globals.processingOptions.getCurrentOption("sectionSubtitleSize"), - notes_text, - ) - else: - # Remove the links from the bullets and replace with target slide title - for bullet in bullets: - linkMatch = linkRegex.match(bullet[1]) - bullet[1] = linkMatch.group(1) - - # Create the TOC slide - with these neutralised titles - slide = createContentSlide( - presentation, - slideNumber, - slideInfo, - ) - - # Postprocess slide to pick up runs - for TOC creation - body = findBodyShape(slide) - text_frame = body.text_frame - for p in text_frame.paragraphs: - TOCruns.append(p.runs[0]) - - # Store the new slide in the list of section slides - for fixing up links - SectionSlides[titleText] = slide - - return slide - - else: - slide = addSlide( - presentation, presentation.slide_layouts[titleOnlyLayout], None - ) - title = findTitleShape(slide) - - SectionSlides[titleText] = slide - - shapes = slide.shapes - - # Add title if TOC slide. Or delete shape if not - if titleText == tocTitle: - # Is TOC slide so add title - slideTitleBottom, title, flattenedTitle = formatTitle( - presentation, slide, tocTitle, pageTitleSize, pageSubtitleSize - ) - else: - # Is not TOC slide so delete title shape and adjust where title bottom - # would be - deleteSimpleShape(title) - slideTitleBottom = marginBase - - # Get the rectangle the content will draw in - contentLeft, contentWidth, contentTop, contentHeight = getContentRect( - presentation, slide, slideTitleBottom, marginBase - ) - - # Create global list of TOC entries - for bullet in bullets: - bulletLevel, bulletText, bulletType = bullet - if bulletLevel == 0: - # Level 0 is top level so create a TOC entry - linkText, linkHref = delinkify(bulletText) - TOCEntries.append([linkText, linkHref]) - - TOCEntryCount = len(TOCEntries) - - TOCFontSize = globals.processingOptions.getCurrentOption("TOCFontSize") - - TOCItemHeight = globals.processingOptions.getCurrentOption("TOCItemHeight") - - TOCItemColour = globals.processingOptions.getCurrentOption("TOCItemColour") - - height = Inches(TOCItemHeight) - - if tocStyle == "chevron": - if height == 0: - height = Inches(1) - - width = height * 2.5 - - entryGap = Inches(-0.5 * height / Inches(1)) - - if TOCFontSize == 0: - TOCFontSize = 14 - - elif tocStyle == "circle": - if height == 0: - height = Inches(1.25) - - width = height - - entryGap = Inches(0.5) - - if TOCFontSize == 0: - TOCFontSize = 12 - - rowGap = Inches(globals.processingOptions.getCurrentOption("TOCRowGap")) - - TOCEntriesPerRow = int( - (presentation.slide_width - 2 * marginBase) / (width + entryGap) - ) - - rowCount = 1 + TOCEntryCount / TOCEntriesPerRow - - # Calculate actual TOC height so it can be vertically centred - TOCHeight = (rowCount * height) + ((rowCount - 1) * rowGap) - - # Calculate where top of TOC should be - TOCtop = slideTitleBottom + (contentHeight - TOCHeight + height) / 2 - - # Calculate actual TOC width - TOCWidth = TOCEntriesPerRow * (width + entryGap) - - # Calculate where the TOC will start - TOCleft = (presentation.slide_width - TOCWidth + entryGap) / 2 - - x = TOCleft - y = TOCtop - - AbsoluteTOCEntryNumber = 1 - - TOCEntryNumber = 1 - - for entry in TOCEntries: - entryText = entry[0] - entryHref = entry[1] - - if entryText == titleText: - wantFilled = False - wantLink = False - else: - wantFilled = True - wantLink = True - - if tocStyle == "chevron": - createChevron( - entryText, - x, - y, - width, - height, - wantFilled, - shapes, - TOCFontSize, - wantLink, - TOCItemColour, - ) - - elif tocStyle == "circle": - # Create the circle - createOval( - entryText, - x, - y, - width, - height, - wantFilled, - shapes, - TOCFontSize, - wantLink, - TOCItemColour, - ) - - # Create half connector to next one - if not last - if AbsoluteTOCEntryNumber < TOCEntryCount: - connector = createLine( - x + width, - y + height / 2, - x + width + entryGap / 2, - y + height / 2, - shapes, - ) - - # Create half connector to previous one - if not first - if AbsoluteTOCEntryNumber > 1: - # z =1 - connector = createLine( - x - entryGap / 2, y + height / 2, x, y + height / 2, shapes - ) - - # Prepare for the next TOC entry - even if there isn't one - x = x + width + entryGap - - # If beyond end of line the next TOC entry would be at the start of the next line - AbsoluteTOCEntryNumber = AbsoluteTOCEntryNumber + 1 - TOCEntryNumber = TOCEntryNumber + 1 - if TOCEntryNumber == TOCEntriesPerRow + 1: - x = TOCleft - y = y + rowGap + height - TOCEntryNumber = 1 - - if want_numbers_content is True: - addFooter(presentation, slideNumber, slide) - - return slide - - -def createSlide(presentation, slideNumber, slideInfo): - abstractTitle = globals.processingOptions.getCurrentOption("abstractTitle") - tocTitle = globals.processingOptions.getCurrentOption("tocTitle") - tocStyle = globals.processingOptions.getCurrentOption("tocStyle") - sectionTitleSize = globals.processingOptions.getCurrentOption("sectionTitleSize") - presTitleSize = globals.processingOptions.getCurrentOption("presTitleSize") - sectionSubtitleSize = globals.processingOptions.getCurrentOption("sectionSubtitleSize") - presSubtitleSize = globals.processingOptions.getCurrentOption("presSubtitleSize") - leftFooterText = globals.processingOptions.getCurrentOption("leftFooterText") - footerfontsizespec = globals.processingOptions.getCurrentOption("footerFontSize") - middleFooterText = globals.processingOptions.getCurrentOption("middleFooterText") - rightFooterText = globals.processingOptions.getCurrentOption("rightFooterText") - sectionFooters = globals.processingOptions.getCurrentOption("sectionFooters") - liveFooters = globals.processingOptions.getCurrentOption("liveFooters") - transition = globals.processingOptions.getCurrentOption("transition") - hidden = globals.processingOptions.getCurrentOption("hidden") - - if slideInfo.blockType in ["content", "code", "table"]: - if (tocStyle != "") & (tocTitle == slideInfo.titleText): - # This is a Table Of Contents slide - slide = createTOCSlide( - presentation, - slideNumber, - slideInfo.titleText, - slideInfo.bullets, - tocStyle, - ) - elif (abstractTitle != "") & (abstractTitle == slideInfo.titleText): - # This is an abstract slide - slide = createAbstractSlide( - presentation, - slideNumber, - slideInfo.titleText, - slideInfo.bullets, - ) - else: - # This is an ordinary contents slide - slide = createContentSlide( - presentation, - slideNumber, - slideInfo, - ) - - elif slideInfo.blockType == "section": - if tocStyle != "": - # This is a section slide in TOC style - slide = createTOCSlide( - presentation, - slideNumber, - slideInfo.titleText, - slideInfo.bullets, - tocStyle, - ) - else: - slide = createTitleOrSectionSlide( - presentation, - slideNumber, - slideInfo.titleText, - globals.processingOptions.getCurrentOption("sectionSlideLayout"), - sectionTitleSize, - slideInfo.subtitleText, - sectionSubtitleSize, - notes_text, - ) - - elif slideInfo.blockType == "title": - slide = createTitleOrSectionSlide( - presentation, - slideNumber, - slideInfo.titleText, - globals.processingOptions.getCurrentOption("titleSlideLayout"), - presTitleSize, - slideInfo.subtitleText, - presSubtitleSize, - notes_text, - ) - - if footerfontsizespec == "": - footerFontSize = Pt(8.0) - else: - footerFontSize = Pt(footerfontsizespec) - - footerBoxTop = prs.slide_height - numbersHeight / 2 - footerFontSize - footerBoxHeight = footerFontSize * 2 - - if slideInfo.blockType in ["title", "section"]: - if sectionFooters == "yes": - wantFooters = True - else: - wantFooters = False - - if slideInfo.blockType == "section": - prs.lastSectionTitle = slideInfo.titleText.strip() - prs.lastSectionSlide = slide - elif slideInfo.blockType == "title": - prs.lastPresTitle = slideInfo.titleText.strip() - prs.lastPresSubtitle = slideInfo.subtitleText.strip() - - else: - wantFooters = True - - if wantFooters: - # Left pseudo-footer - if leftFooterText != "": - leftFooterMargin = Inches(0.5) - leftFooterBoxLeft = leftFooterMargin - leftFooterBoxWidth = prs.slide_width / 3 - leftFooterMargin - leftFooter = slide.shapes.add_textbox( - leftFooterBoxLeft, footerBoxTop, leftFooterBoxWidth, footerBoxHeight - ) - - leftFooter.text, wantHyperLink = substituteFooterVariables( - leftFooterText, liveFooters - ) - - if wantHyperLink: - createShapeHyperlinkAndTooltip(leftFooter, prs.lastSectionSlide, "") - - for fp in leftFooter.text_frame.paragraphs: - fp.alignment = PP_ALIGN.LEFT - fp.font.size = footerFontSize - - # Middle pseudo-footer - if middleFooterText != "": - middleFooterBoxLeft = prs.slide_width / 3 - middleFooterBoxWidth = prs.slide_width / 3 - middleFooter = slide.shapes.add_textbox( - middleFooterBoxLeft, footerBoxTop, middleFooterBoxWidth, footerBoxHeight - ) - - middleFooter.text, wantHyperLink = substituteFooterVariables( - middleFooterText, liveFooters - ) - - if wantHyperLink: - createShapeHyperlinkAndTooltip(middleFooter, prs.lastSectionSlide, "") - - for fp in middleFooter.text_frame.paragraphs: - fp.alignment = PP_ALIGN.CENTER - fp.font.size = footerFontSize - - # Right pseudo-footer - if rightFooterText != "": - rightFooterMargin = Inches(0.25) - rightFooterBoxLeft = prs.slide_width * 2 / 3 - rightFooterBoxWidth = prs.slide_width / 3 - rightFooterMargin - rightFooter = slide.shapes.add_textbox( - rightFooterBoxLeft, footerBoxTop, rightFooterBoxWidth, footerBoxHeight - ) - - rightFooter.text, wantHyperLink = substituteFooterVariables( - rightFooterText, liveFooters - ) - - if wantHyperLink: - createShapeHyperlinkAndTooltip(rightFooter, prs.lastSectionSlide, "") - - for fp in rightFooter.text_frame.paragraphs: - fp.alignment = PP_ALIGN.RIGHT - fp.font.size = footerFontSize - - slideNumber = slideNumber + 1 - - sequence = [] - - addSlideTransition(slide, transition) - - if hidden: - slide._element.set('show', '0') - else: - slide._element.set('show', '1') - - return [slideNumber, slide, sequence] - - -# Add a transition effect - for transitioning INTO the slide. -def addSlideTransition(slide, transitionType): - # Handle "no transition" case - if (transitionType == "none") | (transitionType == ""): - return - - if transitionType in [ - "fracture", - ]: - choiceNS = "p15" - else: - choiceNS = "p14" - - # Construct first boilerplate XML fragment - xml = ' \n' - xml += ( - " \n' - ) - xml += ( - '\n' - ) - - # Add in transition element - if transitionType in [ - "wipe", - ]: - xml += " \n" - - elif transitionType in [ - "push", - ]: - xml += " \n' - - elif transitionType in [ - "vortex", - ]: - xml += " \n' - - elif transitionType in [ - "split", - ]: - xml += " \n' - - elif transitionType in [ - "fracture", - ]: - xml += ' \n' - - else: - xml += " \n" - - # Construct last boilerplate XML fragment - - xml += """ - - - - """ - - xml += ' \n' - - if transitionType in [ - "split", - ]: - xml += ( - " \n' - ) - - else: - xml += " \n" - - xml += """ - - - - """ - - # Turn this into an XML fragment - xmlFragment = parse_xml(xml) - - # Add to slide's XML - slide.element.insert(-1, xmlFragment) - - -def createTaskSlides(prs, slideNumber, tasks, titleStem): - tasksPerPage = globals.processingOptions.getCurrentOption("tasksPerPage") - - taskSlideNumber = 0 - - taskCount = len(tasks) - for taskNumber, task in enumerate(tasks): - if taskNumber % tasksPerPage == 0: - # Is first task in a page - if taskNumber > 0: - # Print a "tasks" slide - as we have one to print out - taskSlideNumber += 1 - if taskCount > tasksPerPage: - # More than one task page - title = titleStem + " - " + str(taskSlideNumber) - else: - # Only one task page - title = titleStem - - taskBlock = [taskRows] - - slideInfo = SlideInfo( - title, "", "table", [], taskBlock, [], [], ["table"] - ) - slide = createContentSlide(prs, slideNumber, slideInfo) - - # Fix up references to be active links to the slide where the task - # was declared - table = findBodyShape(slide).table - for row in table.rows: - cell0Text = row.cells[0].text - if cell0Text not in ["Slide", ""]: - # First cell refers to a specific slide number - so link to it - run = row.cells[0].text_frame.paragraphs[0].runs[0] - createRunHyperlinkOrTooltip( - run, prs.slides[int(cell0Text) - 2 + templateSlideCount], "" - ) - - slideNumber += 1 - - taskRows = [["Slide", "Due", "Task", "Tags", "Done"]] - taskRows.append(["-:", ":--:", ":----", ":----", ":--:"]) - old_sNum = 0 - - sNum, taskText, dueDate, tags, done = task - - if tags != "": - # Sort tags - if there are any - tagList = re.split("[, ]", tags) - sortedTagList = sorted(tagList) - tags = str.join(",", sortedTagList) - - if sNum != old_sNum: - taskRows.append([str(sNum), dueDate, taskText, tags, done]) - else: - taskRows.append(["", dueDate, taskText, tags, done]) - old_sNum = sNum - - # Print a final "tasks" slide - taskSlideNumber += 1 - if taskCount > tasksPerPage: - title = titleStem + " - " + str(taskSlideNumber) - else: - title = titleStem - - taskBlock = [taskRows] - slideInfo = SlideInfo(title, "", "table", [], taskBlock, [], [], ["table"]) - slide = createContentSlide(prs, slideNumber, slideInfo) - - # Fix up references to be active links to the slide where the task - # was declared - table = findBodyShape(slide).table - for row in table.rows: - cell0Text = row.cells[0].text - if cell0Text not in ["Slide", ""]: - # First cell refers to a specific slide number - so link to it - run = row.cells[0].text_frame.paragraphs[0].runs[0] - createRunHyperlinkOrTooltip( - run, prs.slides[int(cell0Text) - 2 + templateSlideCount], "" - ) - - slideNumber += 1 - - -def createGlossarySlides(prs, slideNumber, abbrevDictionary): - termSlideNumber = 0 - glossarySlides = [] - - glossaryTitle = globals.processingOptions.getCurrentOption("glossaryTitle") - glossaryTerm = globals.processingOptions.getCurrentOption("glossaryTerm") - glossaryTermsPerPage = globals.processingOptions.getCurrentOption("glossaryTermsPerPage") - glossaryMeaningWidth = globals.processingOptions.getCurrentOption("glossaryMeaningWidth") - glossaryMeaning = globals.processingOptions.getCurrentOption("glossaryMeaning") - - termCount = len(abbrevDictionary) - - for termNumber, term in enumerate(sorted(abbrevDictionary.keys())): - if termNumber % glossaryTermsPerPage == 0: - # Is first glossary term in a page - if termNumber > 0: - # Print a "glossary" slide - as we have one to print out - termSlideNumber += 1 - if termCount > glossaryTermsPerPage: - # More than one glossary page - title = glossaryTitle + " - " + str(termSlideNumber) - else: - # Only one glossary page - title = glossaryTerm - - glossaryBlock = [glossaryRows] - slideInfo = SlideInfo( - title, "", "table", [], glossaryBlock, [], [], ["table"] - ) - slide = createContentSlide(prs, slideNumber, slideInfo) - - glossarySlides.append(slide) - slideNumber += 1 - - glossaryRows = [[glossaryTerm, glossaryMeaning]] - glossaryRows.append([":-", ":" + ("-" * glossaryMeaningWidth)]) - old_sNum = 0 - - meaning = abbrevDictionary.get(term) - - glossaryRows.append([term, meaning]) - - # Print a final "glossary" slide - termSlideNumber += 1 - if termCount > glossaryTermsPerPage: - # More than one glossary page - title = glossaryTitle + " - " + str(termSlideNumber) - else: - # Only one glossary page - title = glossaryTitle - - glossaryBlock = [glossaryRows] - slideInfo = SlideInfo(title, "", "table", [], glossaryBlock, [], [], ["table"]) - slide = createContentSlide(prs, slideNumber, slideInfo) - glossarySlides.append(slide) - slideNumber += 1 - - return slideNumber, glossarySlides - - -def createSlideNotes(slide, notes_text): - # Remove surrounding white space - notes_text = notes_text.strip().lstrip("\n") - - if slide.notes_slide.notes_text_frame.text != "": - # Notes already filled in - return - - if notes_text != "": - # There is substantive slide note text so create the note - notes_slide = slide.notes_slide - text_frame = notes_slide.notes_text_frame - - # addFormattedText handles eg hyperlinks and entity references - addFormattedText(text_frame.paragraphs[0], notes_text) - - -def createFootnoteSlides(prs, slideNumber, footnoteDefinitions): - footnotesSlideNumber = 0 - footnoteSlides = [] - - footnotesTitle = globals.processingOptions.getCurrentOption("footnotesTitle") - footnotesPerPage = globals.processingOptions.getCurrentOption("footnotesPerPage") - - footnoteCount = len(footnoteDefinitions) - - for footnoteNumber, footnote in enumerate(footnoteDefinitions): - if footnoteNumber % footnotesPerPage == 0: - # Is first footnote in a page - if footnoteNumber > 0: - # Print a "footnotes" slide - as we have one to print out - footnotesSlideNumber += 1 - if footnoteCount > footnotesPerPage: - # More than one footnotes page - title = footnotesTitle + " - " + str(footnotesSlideNumber) - else: - # Only one footnotes page - title = footnotesTitle - - slideInfo = SlideInfo( - title, "", "content", bullets, [], cards, [], ["list"] - ) - slideNumber, slide, sequence = createSlide(prs, slideNumber, slideInfo) - - footnoteSlides.append(slide) - - # Turn off bulleting - removeBullets(findBodyShape(slide).text_frame) - - slideNumber += 1 - bullets = [] - old_sNum = 0 - - bullets.append( - [ - 1, - str(footnoteNumber + 1) + ". " + footnoteDefinitions[footnoteNumber][1], - "bullet", - ] - ) - - # Print a final "footnote" slide - footnotesSlideNumber += 1 - if footnoteCount > footnotesPerPage: - # More than one footnotes page - title = footnotesTitle + " - " + str(footnotesSlideNumber) - else: - # Only one footnotes page - title = footnotesTitle - - slideInfo = SlideInfo(title, "", "content", bullets, [], cards, [], ["list"]) - slideNumber, slide, sequence = createSlide(prs, slideNumber, slideInfo) - - footnoteSlides.append(slide) - - # Turn off bulleting - removeBullets(findBodyShape(slide).text_frame) - - slideNumber += 1 - - return slideNumber, footnoteSlides - - -start_time = time.time() - -banner = ( - "md2pptx Markdown To Powerpoint Converter " + md2pptx_level + " " + md2pptx_date -) - -bannerUnderline = "" -for i in range(len(banner)): - bannerUnderline = bannerUnderline + "=" - -print("\n" + banner + "\n" + bannerUnderline) -print("\nOpen source project: https://github.com/MartinPacker/md2pptx") - -print("\nExternal Dependencies:") -print("\n Python: " + platform.python_version()) - -print(" python-pptx: " + pptx_version) - -if have_pillow: - print(" Pillow: " + PIL.__version__) -else: - print(" Pillow: Not Installed") - -if have_cairosvg: - print(" CairoSVG: " + cairosvg.__version__) -else: - print(" CairoSVG: Not Installed") - -if have_graphviz: - print(" graphviz: " + graphviz.__version__) -else: - print(" graphviz: Not Installed") - -print("\nInternal Dependencies:") -print(f"\n funnel: {funnel.version}") -print(f" runPython: {runPython.version}") - -input_file = [] - -if len(sys.argv) > 2: - # Have input file as well as output file - input_filename = sys.argv[1] - output_filename = sys.argv[2] - - if Path(input_filename).exists(): - input_path = Path(input_filename) - - with input_path.open(mode='r', encoding='utf-8') as file: - input_file = file.readlines() - else: - print("Input file specified but does not exist. Terminating.") -elif len(sys.argv) == 1: - print("No parameters. Terminating") - sys.exit() -else: - output_filename = sys.argv[1] - - input_file = sys.stdin.readlines() - -if len(input_file) == 0: - print("Empty input file. Terminating") - sys.exit() - -slideNumber = 1 - -bulletRegex = re.compile(r"^(\s)*(\*)(.*)") -numberRegex = re.compile(r"^(\s)*(\d+)\.(.*)") -metadataRegex = re.compile("^(.+):(.+)") - -graphicRE = r"!\[(.*?)\]\((.+?)\)" -graphicRegex = re.compile(graphicRE) - -clickableGraphicRE = r"\[" + graphicRE + r"\]\((.+?)\)" -clickableGraphicRegex = re.compile(clickableGraphicRE) - -videoRE = "" -videoRegex = re.compile(videoRE) - -audioRE = "" -audioRegex = re.compile(audioRE) - -linkRegex = re.compile(r"^\[(.+)\]\((.+)\)") -footnoteDefinitionRegex = re.compile(r"^\[\^(.+?)\]: (.+)") -slideHrefRegex = re.compile(r"(.+)\[(.+)\]$") -anchorRegex = re.compile("^") -dynamicMetadataRegex = re.compile("^") -indirectReferenceAnchorRegex = re.compile(r"^\[(.+?)\]: (.+)") -indirectReferenceUsageRegex = re.compile(r"[(.+?)]\[(.+?)]") - -# Default slide layout enumeration -globals.processingOptions.setOptionValuesArray( - [ - ["titleSlideLayout", 0], - ["sectionSlideLayout", 1], - ["contentSlideLayout", 2], - ["titleOnlyLayout", 5], - ["blanklayout", 6], - ] -) - -# Abbreviation Dictionary -abbrevDictionary = {} - -# Abbreviation Runs Dictionary -abbrevRunsDictionary = {} - -# Footnote runs Dictionary -footnoteRunsDictionary = {} - -# Extract metadata -metadata_lines = [] -afterMetadataAndHTML = [] - - -TOCruns = [] -SectionSlides = {} - -inMetadata = True -in_comment = False -inHTML = False -inCode = False - -# Pass 1: Strip out comments and metadata, storing the latter -for line in input_file: - if line.lstrip().startswith(""): - # Note: Not taking text after end of comment - continue - else: - in_comment = True - continue - - elif line.rstrip().endswith("-->"): - # Note: Not taking text after end of comment - in_comment = False - continue - - elif in_comment is True: - continue - - elif (line.lstrip()[:1] == "<") & (inCode is False): - lineLstrip = line.lstrip() - if startswithOneOf(lineLstrip, ["", "
"]):
-            inCode = True
-            afterMetadataAndHTML.append(line)
-
-        elif startswithOneOf(lineLstrip, ["", "
"]): - inCode = False - afterMetadataAndHTML.append(line) - - elif startswithOneOf(lineLstrip, ["
") + dynamicMetadataRegex = re.compile("^") + indirectReferenceAnchorRegex = re.compile(r"^\[(.+?)\]: (.+)") + indirectReferenceUsageRegex = re.compile(r"[(.+?)]\[(.+?)]") + + # Default slide layout enumeration + md2pptx.globals.processingOptions.setOptionValuesArray( + [ + ["titleSlideLayout", 0], + ["sectionSlideLayout", 1], + ["contentSlideLayout", 2], + ["titleOnlyLayout", 5], + ["blanklayout", 6], + ] + ) + + # Abbreviation Dictionary + abbrevDictionary = {} + + # Abbreviation Runs Dictionary + abbrevRunsDictionary = {} + + # Footnote runs Dictionary + footnoteRunsDictionary = {} + + # Extract metadata + metadata_lines = [] + afterMetadataAndHTML = [] + + + TOCruns = [] + SectionSlides = {} + + inMetadata = True + in_comment = False + inHTML = False + inCode = False + + # Pass 1: Strip out comments and metadata, storing the latter + for line in input_file: + if line.lstrip().startswith(""): + # Note: Not taking text after end of comment + continue + else: + in_comment = True + continue + + elif line.rstrip().endswith("-->"): + # Note: Not taking text after end of comment + in_comment = False + continue + + elif in_comment is True: + continue + + elif (line.lstrip()[:1] == "<") & (inCode is False): + lineLstrip = line.lstrip() + if startswithOneOf(lineLstrip, ["", "
"]):
+                inCode = True
+                afterMetadataAndHTML.append(line)
+
+            elif startswithOneOf(lineLstrip, ["", "
"]): + inCode = False + afterMetadataAndHTML.append(line) + + elif startswithOneOf(lineLstrip, ["