":
+ # 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 = md2pptx.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 md2pptx.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 = md2pptx.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 md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("funnelColours")
+ funnelBorderColour = md2pptx.globals.processingOptions.getCurrentOption("funnelBorderColour")
+ funnelTitleColour = md2pptx.globals.processingOptions.getCurrentOption("funnelTitleColour")
+ funnelTextColour = md2pptx.globals.processingOptions.getCurrentOption("funnelTextColour")
+ funnelLabelsPercent = md2pptx.globals.processingOptions.getCurrentOption("funnelLabelsPercent")
+ funnelLabelPosition = md2pptx.globals.processingOptions.getCurrentOption("funnelLabelPosition")
+ funnelWidest = md2pptx.globals.processingOptions.getCurrentOption("funnelWidest")
+
+ f = md2pptx.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 = md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("monoFont")
+ baseTextSize = md2pptx.globals.processingOptions.getCurrentOption("baseTextSize")
+ defaultBaseTextSize = md2pptx.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(
+ md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("codeColumns")
+ fixedPitchHeightWidthRatio = md2pptx.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(
+ md2pptx.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 md2pptx.globals.bgcolors)
+ | (className in md2pptx.globals.fgcolors)
+ | (className in md2pptx.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 := md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("titleOnlyLayout")
+ marginBase = md2pptx.globals.processingOptions.getCurrentOption("marginBase")
+ pageTitleSize = md2pptx.globals.processingOptions.getCurrentOption("pageTitleSize")
+ pageSubtitleSize = md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("titleOnlyLayout")
+ contentSlideLayout = md2pptx.globals.processingOptions.getCurrentOption("contentSlideLayout")
+ marginBase = md2pptx.globals.processingOptions.getCurrentOption("marginBase")
+ pageTitleSize = md2pptx.globals.processingOptions.getCurrentOption("pageTitleSize")
+ pageSubtitleSize = md2pptx.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 = md2pptx.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 md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("horizontalcardgap")
+ verticalCardGap = md2pptx.globals.processingOptions.getCurrentOption("verticalcardgap")
+ cardTitleAlign = md2pptx.globals.processingOptions.getCurrentOption("cardtitlealign")
+ cardTitlePosition = md2pptx.globals.processingOptions.getCurrentOption("cardtitleposition")
+ cardShape = md2pptx.globals.processingOptions.getCurrentOption("cardshape")
+ cardLayout = md2pptx.globals.processingOptions.getCurrentOption("cardlayout")
+ cardPercent = md2pptx.globals.processingOptions.getCurrentOption("cardpercent")
+ cardShadow = md2pptx.globals.processingOptions.getCurrentOption("cardshadow")
+ cardTitleSize = md2pptx.globals.processingOptions.getCurrentOption("cardtitlesize")
+ cardBorderWidth = md2pptx.globals.processingOptions.getCurrentOption("cardborderwidth")
+ cardBorderColour = md2pptx.globals.processingOptions.getCurrentOption("cardbordercolour")
+ cardTitleColour = md2pptx.globals.processingOptions.getCurrentOption("cardtitlecolour")
+ cardTitleBackgrounds = md2pptx.globals.processingOptions.getCurrentOption("cardtitlebackground")
+ cardColours = md2pptx.globals.processingOptions.getCurrentOption("cardcolour")
+ cardDividerColour = md2pptx.globals.processingOptions.getCurrentOption("carddividercolour")
+ cardGraphicSize = md2pptx.globals.processingOptions.getCurrentOption("cardgraphicsize")
+ cardGraphicPosition = md2pptx.globals.processingOptions.getCurrentOption("cardGraphicPosition")
+ cardGraphicPadding = int(Inches(md2pptx.globals.processingOptions.getCurrentOption("cardgraphicpadding")))
+ marginBase = md2pptx.globals.processingOptions.getCurrentOption("marginBase")
+ pageTitleSize = md2pptx.globals.processingOptions.getCurrentOption("pageTitleSize")
+ pageSubtitleSize = md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("tableMargin")
+ marginBase = md2pptx.globals.processingOptions.getCurrentOption("marginBase")
+ baseTextSize = md2pptx.globals.processingOptions.getCurrentOption("baseTextSize")
+ tableShadow = md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("compactTables")
+ spanCells = md2pptx.globals.processingOptions.getCurrentOption("spanCells")
+ tableHeadingSize = md2pptx.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,
+ md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("titleOnlyLayout")
+ blankLayout = md2pptx.globals.processingOptions.getCurrentOption("blankLayout")
+ tocTitle = md2pptx.globals.processingOptions.getCurrentOption("tocTitle")
+ marginBase = md2pptx.globals.processingOptions.getCurrentOption("marginBase")
+ pageTitleSize = md2pptx.globals.processingOptions.getCurrentOption("pageTitleSize")
+ pageSubtitleSize = md2pptx.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,
+ md2pptx.globals.processingOptions.getCurrentOption("sectionSlideLayout"),
+ md2pptx.globals.processingOptions.getCurrentOption("sectionTitleSize"),
+ slideInfo.subtitleText,
+ md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("TOCFontSize")
+
+ TOCItemHeight = md2pptx.globals.processingOptions.getCurrentOption("TOCItemHeight")
+
+ TOCItemColour = md2pptx.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(md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("abstractTitle")
+ tocTitle = md2pptx.globals.processingOptions.getCurrentOption("tocTitle")
+ tocStyle = md2pptx.globals.processingOptions.getCurrentOption("tocStyle")
+ sectionTitleSize = md2pptx.globals.processingOptions.getCurrentOption("sectionTitleSize")
+ presTitleSize = md2pptx.globals.processingOptions.getCurrentOption("presTitleSize")
+ sectionSubtitleSize = md2pptx.globals.processingOptions.getCurrentOption("sectionSubtitleSize")
+ presSubtitleSize = md2pptx.globals.processingOptions.getCurrentOption("presSubtitleSize")
+ leftFooterText = md2pptx.globals.processingOptions.getCurrentOption("leftFooterText")
+ footerfontsizespec = md2pptx.globals.processingOptions.getCurrentOption("footerFontSize")
+ middleFooterText = md2pptx.globals.processingOptions.getCurrentOption("middleFooterText")
+ rightFooterText = md2pptx.globals.processingOptions.getCurrentOption("rightFooterText")
+ sectionFooters = md2pptx.globals.processingOptions.getCurrentOption("sectionFooters")
+ liveFooters = md2pptx.globals.processingOptions.getCurrentOption("liveFooters")
+ transition = md2pptx.globals.processingOptions.getCurrentOption("transition")
+ hidden = md2pptx.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,
+ md2pptx.globals.processingOptions.getCurrentOption("sectionSlideLayout"),
+ sectionTitleSize,
+ slideInfo.subtitleText,
+ sectionSubtitleSize,
+ notes_text,
+ )
+
+ elif slideInfo.blockType == "title":
+ slide = createTitleOrSectionSlide(
+ presentation,
+ slideNumber,
+ slideInfo.titleText,
+ md2pptx.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 = md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("glossaryTitle")
+ glossaryTerm = md2pptx.globals.processingOptions.getCurrentOption("glossaryTerm")
+ glossaryTermsPerPage = md2pptx.globals.processingOptions.getCurrentOption("glossaryTermsPerPage")
+ glossaryMeaningWidth = md2pptx.globals.processingOptions.getCurrentOption("glossaryMeaningWidth")
+ glossaryMeaning = md2pptx.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 = md2pptx.globals.processingOptions.getCurrentOption("footnotesTitle")
+ footnotesPerPage = md2pptx.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
+
+
+def cli():
+ 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: {md2pptx.funnel.version}")
+ print(f" runPython: {md2pptx.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
+ 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, ["