Chapter 17. Slide Deck Manipulation

This chapter looks at a number of common tasks performed with slide decks:

  • building a slide deck based on a file of text notes;

  • using, changing, and creating master pages for slide decks;

  • adding a slide to an existing deck;

  • rearranging the slides in a deck;

  • appending two (or more) decks;

  • exporting a slide from a deck as an image;

  • extracting all the text from a slide deck.

17.1 Building a Deck from Notes

With Draw methods such as title_slide() and bullets_slide() it’s quite easy to write a program that converts a set of notes in a text file into a slides deck. The points_builder.py example does just that, using textual input formatted as shown below:

Points Builder Text Data
What is a Algorithm?

> An algorithm is a finite set of unambiguous instructions for solving a problem.

>> An algorithm is correct if on all legitimate inputs, it outputs the right answer in a finite amount of time

> Can be expressed as
>>pseudocode
>>flow charts
>>text in a natural language (e.g. English)
>>computer code

// ======================================================

Algorithm Design

> The theoretical  study of how to solve  computational problems
>>sorting a list of numbers
>>   finding a shortest route on a map
>> scheduling when to work on homework
>> answering web search queries
>> and so on...

// ======================================================

The Importance of Algorithms

> Their impact is broad and far-reaching.
>> Internet. Web search, packet routing, ...
>> Computers. Circuit layout, file system, compilers, ...
>> Computer graphics. Movies, virtual reality, ...
>> Security. Cell phones, e-commerce, ...
>> Multimedia. MP3, JPG, DivX, HDTV, ...
>> Social networks.  Recommendations, news feeds, ads, ...

> Their impact is broad and far-reaching.
>> Internet. Web search, packet routing, ...
>> Computers. Circuit layout, file system, compilers, ...
>> Computer graphics. Movies, virtual reality, ...
>> Security. Cell phones, e-commerce, ...
>> Multimedia. MP3, JPG, DivX, HDTV, ...
>> Social networks.  Recommendations, news feeds, ads, ...

// ======================================================

Top Ten Algorithms of the Century

> Ten algorithms having "the greatest influence on the development and practice of science and engineering in the 20th century".

>> Dongarra and Sullivan, "Computing in Science and Engineering", January/February 2000

>> Barry Cipra, "The Best of the 20th Century: Editors Name Top 10 Algorithms", SIAM News, Volume 33, Number 4, May 2000
>>> http://www.siam.org/pdf/news/637.pdf

When points_builder.py reads this text, it generates three slides shown in Fig. 155.

Slides Generated by points builder

Fig. 155 :Slides Generated by Points Builder

The title slide in Fig. 155 is generated automatically, but the other slides are created from the input text by calling Draw.bullets_slide() and Draw.add_bullet().

The reason the output looks so good is that points_builder.py uses one of Impress’ templates, Inspiration.otp. These files are listed in Impress when you open the “Master Pages” section of the Tasks pane, part of which is shown in Fig. 156.

The List of Master Pages in Impress.

Fig. 156 :The List of Master Pages in Impress.

If you move the cursor over the thumbnail images, the name of the template file is displayed as a tooltip.

The main() function of points_builder.py starts by printing the names of all the available templates, before using Inspiration.otp to create a new presentation document:

# partial points_builder.py module
class PointsBuilder:
    def __init__(self, points_fnm: PathOrStr) -> None:
        _ = FileIO.is_exist_file(fnm=points_fnm, raise_err=True)
        self._points_fnm = FileIO.get_absolute_path(points_fnm)

    def main(self) -> None:
        loader = Lo.load_office(Lo.ConnectPipe())

        # create Impress page or Draw slide
        try:
            self._report_templates()
            tmpl_name = "Inspiration.otp"  # "Piano.otp"
            template_fnm = Path(Draw.get_slide_template_path(), tmpl_name)
            _ = FileIO.is_exist_file(template_fnm, True)
            doc = ImpressDoc(
                Lo.create_doc_from_template(template_path=template_fnm, loader=loader)
            )

            self._read_points(doc)

            print(f"Total no. of slides: {doc.get_slides_count()}")

            doc.set_visible()
            Lo.delay(2000)

            msg_result = MsgBox.msgbox(
                "Do you wish to close document?",
                "All done",
                boxtype=MessageBoxType.QUERYBOX,
                buttons=MessageBoxButtonsEnum.BUTTONS_YES_NO,
            )
            if msg_result == MessageBoxResultsEnum.YES:
                doc.close_doc()
                Lo.close_office()
            else:
                print("Keeping document open")
        except Exception:
            Lo.close_office()
            raise

_report_templates() uses Info.get_dirs("Template") to retrieve a list of all the directories examined by Office when looking for templates. It also calls Draw.get_slide_template_path() to retrieve the default slide template directory, and prints the names of the files in that folder:

See also

Info.get_dirs()

# in points_builder.py
def _report_templates(self) -> None:
    template_dirs = Info.get_dirs(setting="Template")
    print("Templates dir:")
    for dir in template_dirs:
        print(f"  {dir}")

    template_dir = Draw.get_slide_template_path()
    print()
    print(f'Templates files in "{template_dir}"')
    template_fnms = FileIO.get_file_paths(template_dir)
    for fnm in template_fnms:
        print(f"  {fnm}")
_report_templates()'s output:
Templates dir:
  C:\Program Files\LibreOffice\share\template\common
  C:\Program Files\LibreOffice\share\template\en-US
  D:\Users\bigby\Documents\Projects\Python\python-ooouno-ex\$BUNDLED_EXTENSIONS\wiki-publisher\templates
  C:\Users\bigby\AppData\Roaming\LibreOffice\4\user\template

Templates files in "C:\Program Files\LibreOffice\share\template\common\presnt"
  C:\Program Files\LibreOffice\share\template\common\presnt\Beehive.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Blueprint_Plans.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Blue_Curve.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Candy.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\DNA.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Focus.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Forestbird.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Freshes.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Grey_Elegant.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Growing_Liberty.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Inspiration.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Lights.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Metropolis.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Midnightblue.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Nature_Illustration.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Pencil.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Piano.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Portfolio.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Progress.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Sunset.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Vintage.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Vivid.otp
  C:\Program Files\LibreOffice\share\template\common\presnt\Yellow_Idea.otp

points_builder.py employs the Inspiration.otp template, via the call:

tmpl_name = "Inspiration.otp"  # "Piano.otp"
template_fnm = Path(Draw.get_slide_template_path(), tmpl_name)
_ = FileIO.is_exist_file(template_fnm, True)
doc = ImpressDoc(
    Lo.create_doc_from_template(template_path=template_fnm, loader=loader)
)

Lo.create_doc_from_template() is a variant of Lo.create_doc() which specifies a template for the new document. It calls XComponentLoader.loadComponentFromURL() with the template file as an argument, and sets the AsTemplate property:

#in the Lo class (simplified)
_ms_factory: XMultiServiceFactory = None

@classmethod
def create_doc_from_template(cls, template_path: PathOrStr, loader: XComponentLoader) -> XComponent:
    if not FileIO.is_openable(template_path):
        raise Exception(f"Template file can not be opened: '{template_path}'")
    Lo.print(f"Opening template: '{template_path}'")
    template_url = FileIO.fnm_to_url(fnm=template_path)

    props = Props.make_props(Hidden=True, AsTemplate=True)
    cls._doc = loader.loadComponentFromURL(template_url, "_blank", 0, props)
    cls._ms_factory = cls.qi(XMultiServiceFactory, cls._doc, raise_err=True)
    return cls._doc

The _read_points() method in points_builder.py loads the text points file line-by-line. It ignores blank lines and lines starting with “//”, and examines the first character on each line:

# in points_builder.py
def _read_points(self, doc: ImpressDoc) -> None:
    curr_slide = doc.get_slide(idx=0)
    curr_slide.title_slide(
        title="Python-Generated Slides",
        sub_title="Using LibreOffice",
    )
    try:

        def process_bullet(
            line: str, draw_text: DrawText[ImpressDoc] | None
        ) -> None:
            # count the number of '>'s to determine the bullet level
            if draw_text is None:
                print(f"No slide body for {line}")
                return

            pos = 0
            s_lst = [*line]
            ch = s_lst[pos]
            while ch == ">":
                pos += 1
                ch = s_lst[pos]
            sub_str = "".join(s_lst[pos:]).strip()

            draw_text.add_bullet(level=pos - 1, text=sub_str)

        body: DrawText[ImpressDoc] | None = None
        with open(self._points_fnm, "r") as file:
            # remove empty lines
            data = (row for row in file if row.strip())
            # chain generator
            # strip of remove anything starting //
            # // for comment
            data = (row for row in data if not row.lstrip().startswith("//"))

            for row in data:
                ch = row[:1]
                if ch == ">":
                    process_bullet(line=row, draw_text=body)
                else:
                    curr_slide = doc.add_slide()
                    body = curr_slide.bullets_slide(title=row.strip())
        print(f"Read in point file: {self._points_fnm.name}")
    except Exception as e:
        print(f"Error reading points file: {self._points_fnm}")
        print(f"  {e}")

If the line starts with a >, then process_bullet() is called to determine how many >’s start the line. Depending on the number, draw_text.add_bullet() is called with a different bullet indentation level value. If the line doesn’t start with a >, then it’s assumed to be the title line of a new slide, and Draw.add_slide() and Draw.bullets_slide() create a new bullets-based slide.

17.2 Master Pages

When a new slide deck is created it always has a single slide and a default master page, and every slide created subsequently will use this master page to format its background. The easiest way to view the master page is through the Impress GUI – by clicking on the View, Master, Slide Master menu, which displays something like Fig. 157.

The Default Master Page.

Fig. 157 :The Default Master Page.

There are five presentation shapes in Fig. 157. From top-to-bottom, left-to-right, they are TitleTextShape, OutlinerShape, DateTimeShape, FooterShape, and SlideNumberShape.

Even though a new slide links to this master page, the date/time, footer, and slide number text are not automatically displayed on the slide; their rendering must be turned on.

It’s possible to create more master pages in addition to the default one in Fig. 157, and link a slide to one of those.

The master_use.py example illustrates a number of master page features: the default master page has text added to its footer section, and a shape and text are placed in its top-left corner. The slide deck holds four slides – three of them link to the default master page, and are set to display its footer and slide number. However, the third slide in the deck links to a second master page with a slightly different appearance.

Fig. 158 shows the all the slides in the deck.

A Slide Deck with Two Master Pages

Fig. 158 :A Slide Deck with Two Master Pages.

Slides 1, 2, and 4 use the default master page, while slide 3 uses the new master.

The main() method for master_use.py is:

# in master_use.py
class MasterUse:
    def main(self) -> None:
        loader = Lo.load_office(Lo.ConnectPipe())
        try:
            doc = ImpressDoc(Draw.create_impress_doc(loader))

            # report on the shapes on the default master page
            master_page = doc.get_master_page(idx=0)
            print("Default Master Page")
            Draw.show_shapes_info(master_page.component)

            # set the master page's footer text
            master_page.set_master_footer(text="Master Use Slides")

            # add a rectangle and text to the default master page
            # at the top-left of the slide
            sz = master_page.get_size_mm()
            _ = master_page.draw_rectangle(
                x=5,
                y=7,
                width=round(sz.Width / 6),
                height=round(sz.Height / 6),
            )
            _ = master_page.draw_text(
                msg="Default Master Page",
                x=10,
                y=15,
                width=100,
                height=10,
                font_size=24,
            )

            # set slide 1 to use the master page's slide number
            # but its own footer text
            slide1 = doc.get_slide(idx=0)
            slide1.title_slide(title="Slide 1")

            # IsPageNumberVisible = True: use the master page's slide number
            # change the master page's footer for first slide; does not work if the master already has a footer
            slide1.set_property(
                IsPageNumberVisible=True,
                IsFooterVisible=True,
                FooterText="MU Slides",
            )

            # add three more slides, which use the master page's
            # slide number and footer
            for i in range(1, 4):  # 1, 2, 3
                slide = doc.insert_slide(idx=i)
                _ = slide.bullets_slide(title=f"Slide {i}")
                slide.set_property(IsPageNumberVisible=True, IsFooterVisible=True)

            # create master page 2
            master2 = doc.insert_master_page(idx=1)
            _ = master2.add_slide_number()

            print("Master Page 2")
            Draw.show_shapes_info(master2.component)

            # link master page 2 to third slide

            doc.get_slide(idx=2).set_master_page(master2.component)

            # put ellipse and text on master page 2
            ellipse = master2.draw_ellipse(
                x=5,
                y=7,
                width=round(sz.Width / 6),
                height=round(sz.Height / 6),
            )
            ellipse.component.FillColor = CommonColor.GREEN_YELLOW
            _ = master2.draw_text(
                msg="Master Page 2",
                x=10,
                y=15,
                width=100,
                height=10,
                font_size=24,
            )

            doc.set_visible()

            Lo.delay(2_000)

            msg_result = MsgBox.msgbox(
                "Do you wish to close document?",
                "All done",
                boxtype=MessageBoxType.QUERYBOX,
                buttons=MessageBoxButtonsEnum.BUTTONS_YES_NO,
            )
            if msg_result == MessageBoxResultsEnum.YES:
                doc.close_doc()
                Lo.close_office()
            else:
                print("Keeping document open")
        except Exception:
            Lo.close_office()
            raise

17.2.1 Accessing a Master Page

A presentation (or drawing) document can access its master pages through the XMasterPagesSupplier interface in the GenericDrawingDocument service. XMasterPagesSupplier.getMasterPages() returns a single XDrawPages object:

# doc is an XComponent
mp_supp = Lo.qi(XMasterPagesSupplier, doc, True)
pgs = mp_supp.getMasterPages()  # XDrawPages

Or for Class ImpressDoc:

mp_supp = doc.qi(XMasterPagesSupplier, True)
pgs = mp_supp.getMasterPages()  # XDrawPages

The XDrawPages object is an indexed collection, with the default master page at position 0:

master_page = Lo.qi(XDrawPage, pgs.getByIndex(0))

Note that there’s no XMasterPage interface – both slides and master pages are manipulated using XDrawPage.

These preceding lines are packaged up as Draw.get_master_page():

# in the Draw class (overload method, simplified)
@staticmethod
def get_master_page(doc: XComponent, idx: int) -> XDrawPage:
    mp_supp = Lo.qi(XMasterPagesSupplier, doc)
    pgs = mp_supp.getMasterPages()
    return Lo.qi(XDrawPage, pgs.getByIndex(idx), True)

There’s a second way of obtaining a master page, via the link between a slide and its master; the linked master is called a target. This is implemented by an overloaded Draw.get_master_page() method:

# in the Draw class (overload method, simplified)
@staticmethod
def get_master_page(slide: XDrawPage) -> XDrawPage:
    mp_target = Lo.qi(XMasterPageTarget, slide, True)
    return mp_target.getMasterPage()

17.2.2 What’s on a Master Page?

The default master page was shown in Fig. 157, and its structure is confirmed in master_use.py when Draw.show_shapes_info() is called:

# in main() of master_use.py
master_page = doc.get_master_page(idx=0)
print("Default Master Page")
Draw.show_shapes_info(master_page.component)

The output:

Default Master Page
Draw Page shapes:
  Shape service: com.sun.star.presentation.TitleTextShape; z-order: 0
  Shape service: com.sun.star.presentation.OutlinerShape; z-order: 1
  Shape service: com.sun.star.presentation.DateTimeShape; z-order: 2
  Shape service: com.sun.star.presentation.FooterShape; z-order: 3
  Shape service: com.sun.star.presentation.SlideNumberShape;z-order:4

A new master page is created by using XMasterPagesSupplier.getMasterPages() and XDrawPages.insertNewByIndex(), as shown in Draw.insert_master_page():

# in the Draw class
@staticmethod
def insert_master_page(doc: XComponent, idx: int) -> XDrawPage:
    try:
        mp_supp = Lo.qi(XMasterPagesSupplier, doc, True)
        pgs = mp_supp.getMasterPages()
        result = pgs.insertNewByIndex(idx)
        if result is None:
            raise NoneError("None Value: insertNewByIndex() return None")
        return result
    except Exception as e:
        raise DrawPageError("Unable to insert master page") from e

The new master page contains no presentation shapes (unlike the default one). They must be added separately.

17.2.3 Modifying a Master Page

master_use.py changes the default master page in three ways: it adds text to the footer shape (which is empty by default), and places a blue rectangle and some text in the top-left corner of the master:

# in main of master_use.py
# set the master page's footer text
master_page.set_master_footer(text="Master Use Slides")

# add a rectangle and text to the default master page
# at the top-left of the slide
sz = master_page.get_size_mm()
_ = master_page.draw_rectangle(
    x=5,
    y=7,
    width=round(sz.Width / 6),
    height=round(sz.Height / 6),
)
_ = master_page.draw_text(
    msg="Default Master Page",
    x=10,
    y=15,
    width=100,
    height=10,
    font_size=24,
)

Draw.set_master_footer() searches through the shapes on the page looking for a FooterShape. The shape is cast to a text interface, and a string added:

# in the Draw class
@classmethod
def set_master_footer(cls, master: XDrawPage, text: str) -> None:
    try:
        footer_shape = cls.find_shape_by_type(
            slide=master, shape_type=DrawingNameSpaceKind.SHAPE_TYPE_FOOTER
        )
        txt_field = Lo.qi(XText, footer_shape, True)
        txt_field.setString(text)
    except ShapeMissingError:
        raise
    except Exception as e:
        raise DrawPageError("Unable to set master footer") from e

The second page of Master Use contains no shapes initially. An ellipse and some text are added to it in the same way as for the default master page:

# in main of master_use.py
master2 = doc.insert_master_page(idx=1)
_ = master2.add_slide_number()

# put ellipse and text on master page 2
ellipse = master2.draw_ellipse(
    x=5,
    y=7,
    width=round(sz.Width / 6),
    height=round(sz.Height / 6),
)
ellipse.component.FillColor = CommonColor.GREEN_YELLOW

_ = master2.draw_text(
    msg="Master Page 2",
    x=10,
    y=15,
    width=100,
    height=10,
    font_size=24,
)

Unlike the default master page, a number shape must be explicitly added to the second master, by calling master2.add_slide_number() which invokes Draw.add_slide_number():

_ = master2.add_slide_number()

It is implemented as:

# in the Draw class
@classmethod
def add_slide_number(cls, slide: XDrawPage) -> XShape:
    try:
        sz = cls.get_slide_size(slide)
        width = 60
        height = 15
        return cls.add_pres_shape(
            slide=slide,
            shape_type=PresentationKind.SLIDE_NUMBER_SHAPE,
            x=sz.Width - width - 12,
            y=sz.Height - height - 4,
            width=width,
            height=height,
        )
    except ShapeError:
        raise
    except Exception as e:
        raise ShapeError("Unable to add slide number") from e

Draw.add_pres_shape() creates a shape from the com.sun.star.presentation package:

# in the Draw class
@classmethod
def add_pres_shape(
    cls, slide: XDrawPage, shape_type: PresentationKind, x: int, y: int, width: int, height: int
) -> XShape:
    try:
        cls.warns_position(slide=slide, x=x, y=y)
        shape = Lo.create_instance_msf(XShape, shape_type.to_namespace(), raise_err=True)
        if shape is not None:
            slide.add(shape)
            cls.set_position(shape, x, y)
            cls.set_size(shape, width, height)
        return shape
    except Exception as e:
        raise ShapeError("Unable to add slide number") from e

17.2.4 Using a Master Page

New slides are automatically linked to the default master page, but properties must be explicitly set in order for the master’s date/time, footer, and page number to be visible on the slide. For example, the footer and page number are drawn on a slide like so:

# in main of master_use.py
slide1 = doc.get_slide(idx=0)

# ...

slide1.set_property(
    IsPageNumberVisible=True,
    IsFooterVisible=True,
    FooterText="MU Slides",
)

The relevant property for showing the date/time is IsDateTimeVisible. All these properties are define in the com.sun.star.presentation.DrawPage service.

A related property is FooterText, which changes the footer text for a specific slide.

However, this only works if the linked master page doesn’t have its own footer text.

A slide can be linked to a different master by calling Draw.set_master_page():

# in main of master_use.py
# link master page 2 to third slide
doc.get_slide(idx=2).set_master_page(master2.component)

It uses the XMasterPageTarget interface to create the new link:

# in the Draw class
@staticmethod
def set_master_page(slide: XDrawPage, page: XDrawPage) -> None:
    try:
        mp_target = Lo.qi(XMasterPageTarget, slide, True)
        mp_target.setMasterPage(page)
    except Exception as e:
        raise DrawError("Unable to set master page") from e

17.3 Adding a Slide to a Deck

The last section used XDrawPages.insertNewByIndex() to add a master page to the deck The same method is more commonly employed to add an ordinary slide.

An example is shown in modify_slides.py: its main() methods opens a file, adds a title-only slide at the end of the deck, and a title/subtitle slide at the beginning. It finishes by saving the modified deck to a new file:

# in modify_slides.py
class ModifySlides:
    def __init__(self, fnm: PathOrStr, im_fnm: PathOrStr) -> None:
        _ = FileIO.is_exist_file(fnm=fnm, raise_err=True)
        _ = FileIO.is_exist_file(fnm=im_fnm, raise_err=True)
        self._fnm = FileIO.get_absolute_path(fnm)
        self._im_fnm = FileIO.get_absolute_path(im_fnm)

    def main(self) -> None:
        loader = Lo.load_office(Lo.ConnectPipe())

        try:
            doc_component = Lo.open_doc(self._fnm, loader)

            # slideshow start() crashes if the doc is not visible

            if not Info.is_doc_type(obj=doc_component, doc_type=Lo.Service.IMPRESS):
                print("-- Not a slides presentation")
                Lo.close_office()
                return

            doc = ImpressDoc(doc_component)
            doc.set_visible()

            slides = doc.get_slides()
            num_slides = slides.get_count()
            print(f"No. of slides: {num_slides}")

            # add a title-only slide with a graphic at the end
            last_page = ImpressPage(
                owner=doc, component=slides.insert_new_by_index(num_slides)
            )
            last_page.title_only_slide(header="Any Questions?")
            last_page.draw_image(fnm=self._im_fnm)

            # add a title/subtitle slide at the start
            first_page = ImpressPage(owner=doc, component=slides.insert_new_by_index(0))
            first_page.title_slide(
                title="Interesting Slides", sub_title="Brought to you by OooDev"
            )

            Lo.delay(2000)
            msg_result = MsgBox.msgbox(
                "Do you wish to close document?",
                "All done",
                boxtype=MessageBoxType.QUERYBOX,
                buttons=MessageBoxButtonsEnum.BUTTONS_YES_NO,
            )
            if msg_result == MessageBoxResultsEnum.YES:
                doc.close_doc()
                Lo.close_office()
            else:
                print("Keeping document open")
        except Exception:
            Lo.close_office()
            raise

But if you examine the new file, you’ll see that the title/subtitle slide has become the second slide in the deck. This highlights a restriction on XDrawPages.insertNewByIndex(), which is that a new slide cannot be inserted at index position 0. Instead, it will be inserted at the next spot, position 1.

17.4 Rearranging a Slide Deck

A surprising gap in the presentation API is the lack of a simple way to rearrange slides: for example, to move the second slide to the fifth position.

The closest the API comes is the ability to duplicate a slide, but the copy is always inserted after the original, so isn’t of much use. If you did want to do this, the code would be something like:

# doc is an XComponent
dup = Lo.qi(XDrawPageDuplicator, doc, True)
dup_slide = dup.duplicate(slide)  # XDrawPage
    # dup_slide is located after original slide in the deck

Or for Class ImpressDoc:

# doc is ImpressDoc instance
dup = doc.qi(XDrawPageDuplicator, True)
dup_slide = dup.duplicate(slide)  # XDrawPage
    # dup_slide is located after original slide in the deck

The only way to rearrange slides inside Office is with dispatch commands, in particular with the “Copy” and “Paste” commands. This is complicated by the fact that copying an entire slide is only possible when the deck is displayed in slide-sorter mode.

The copy_slide.py example illustrates the technique but, as with most uses of dispatching, is quite fragile. The better answer is to utilize a third-part API, the ODF Toolkit, which is the topic of Chapter 51.

start.py is called with three arguments – the filename and two slide indices.

The first index is the source slide’s position in the deck, and the second is the position after which the copied slide will appear. For instance:

python -m start points.odp 1 4

will copy the second slide of the deck to after the fifth slide.

The main() method of copy_slide.py:

# in copy_slide.py
def main(self) -> None:
    loader = Lo.load_office(Lo.ConnectPipe())

    try:
        doc = ImpressDoc(Lo.open_doc(fnm=self._fnm, loader=loader))
        num_slides = doc.get_slides_count()
        if self._from_idx >= num_slides or self._to_idx >= num_slides:
            Lo.close_office()
            raise IndexError("One or both indices are out of range")

        doc.set_visible()

        self._copy_to(doc=doc)

        # doc.delete_slide(idx=self._from_idx)
        # a problem if the copying changes the indices

        # Lo.save(doc.component) # overwrites original

        Lo.delay(2000)
        msg_result = MsgBox.msgbox(
            "Do you wish to close document?",
            "All done",
            boxtype=MessageBoxType.QUERYBOX,
            buttons=MessageBoxButtonsEnum.BUTTONS_YES_NO,
        )
        if msg_result == MessageBoxResultsEnum.YES:
            doc.close_doc()
            Lo.close_office()
        else:
            print("Keeping document open")
    except IndexError:
        raise
    except Exception:
        Lo.close_office()
        raise

All the interesting stuff is performed by _copy_to(). One minor thing to note is the call to Lo.save() in main() which causes the changed slide deck to be saved back to its original file.

It is defined as:

# in Lo class
@classmethod
def save(cls, doc: object) -> bool:
    cargs = CancelEventArgs(Lo.save.__qualname__)
    cargs.event_data = {"doc": doc}
    _Events().trigger(LoNamedEvent.DOC_SAVING, cargs)
    if cargs.cancel:
        return False

    store = cls.qi(XStorable, doc, True)
    try:
        store.store()
        cls.print("Saved the document by overwriting")
    except IOException as e:
        raise Exception(f"Could not save the document") from e

    _Events().trigger(LoNamedEvent.DOC_SAVED, EventArgs.from_args(cargs))
    return True

It is often best to avoid calling Lo.save() due to the fact that it overwrites the input file.

The call to Draw.delete_slide() in main() is commented out due to its potential to cause damage. The problem is that the new slide may cause the overall indexing of the slide deck to change. For example, consider what happens if the fourth slide is copied to after the second slide. This will create a new third slide, moving the old third slide, and all later slides, to the right. If the program now deletes the fourth slide, that’s not the slide that’s just been copied, but the re-positioned third slide.

The _copy_to() function in copy_slide.py:

# in copy_slide.py
def _copy_to(self, doc: ImpressDoc) -> None:
    # Copy fromIdx slide to the clipboard in slide-sorter mode,
    # then paste it to after the toIdx slide.

    # Switch to slide sorter view so that slides can be pasted
    Lo.delay(1000)
    Lo.dispatch_cmd(cmd=DrawViewDispatch.DIA_MODE)

    # give Office a few seconds of time to do it
    Lo.delay(3000)

    from_slide = doc.get_slide(idx=self._from_idx)
    to_slide = doc.get_slide(idx=self._to_idx)
    doc.goto_page(from_slide.component)
    Lo.dispatch_cmd(cmd=GlobalEditDispatch.COPY)
    Lo.delay(500)
    print(f"Copied {self._from_idx}")

    doc.goto_page(to_slide.component)
    Lo.delay(500)
    Lo.dispatch_cmd(GlobalEditDispatch.PASTE)
    Lo.delay(500)
    print(f"Paste to after {self._to_idx}")

    # Lo.dispatchCmd("PageMode");  // back to normal mode (not working)
    Lo.dispatch_cmd(cmd=DrawDrawingDispatch.DRAWING_MODE)
    Lo.delay(500)

The method sends out four dispatches: the DiaMode command switches the application into slide-sorter mode, and is followed by Copy, Paste, and finally DrawingMode which switches the mode back to normal.

Dispatch lookups from dispatch namespace are used for convenience.

There are a few complications. One is that Draw.goto_page() must be called twice. The first call ensures that the source slide is the visible, active window before the Copy is processed. The second Draw.goto_page() call makes sure the destination slide is now visible. This means that Paste will insert the copied slide after the destination slide, as required.

If for any reason the above _copy() method is not working correctly try the following alternative:

from ooodev.gui import GUI
from ooodev.draw import Draw

def _copy_to(self, doc: ImpressDoc) -> None:
    # Copy fromIdx slide to the clipboard in slide-sorter mode,
    # then paste it to after the toIdx slide.

    ctl = GUI.get_current_controller(doc.component)
    # Switch to slide sorter view so that slides can be pasted
    Lo.delay(1000)
    Lo.dispatch_cmd(cmd=DrawViewDispatch.DIA_MODE)

    # give Office a few seconds of time to do it
    Lo.delay(3000)

    from_slide = doc.get_slide(idx=self._from_idx)
    to_slide = doc.get_slide(idx=self._to_idx)

    Draw.goto_page(ctl=ctl, page=from_slide.component)
    Lo.dispatch_cmd(cmd=GlobalEditDispatch.COPY)
    Lo.delay(500)
    print(f"Copied {self._from_idx}")

    Draw.goto_page(ctl=ctl, page=to_slide.component)
    Lo.delay(500)
    Lo.dispatch_cmd(GlobalEditDispatch.PASTE)
    Lo.delay(500)
    print(f"Paste to after {self._to_idx}")

    # Lo.dispatchCmd("PageMode");  // back to normal mode (not working)
    Lo.dispatch_cmd(cmd=DrawDrawingDispatch.DRAWING_MODE)
    Lo.delay(500)

Normally the call is Draw.goto_page() with a document argument (e.g. Draw.goto_page(doc, from_slide)). In some cases does not work correctly for the pasting of the slide, for reasons unknown. The solution is to use a reference to the document’s controller, as shown in the above _copy_to() method:

17.5 Appending Slide Decks Together

A common Office forum question is how to add the slides of one deck to the end of another. One solution is to use Copy and Paste dispatches as in 17.4 Rearranging a Slide Deck, but in a more complicated fashion. As you might guess, the ODF Toolkit library described in Chapter 51 is a better solution, but the focus is on using Office here.

This approach means that two application windows could be open at the same time: one containing the deck that is being copied, and another for the destination slide deck. This requires references to two different application views and frames, which can be a problem because of the Static design of many classes.

Another problem is caused by the issue of master page copying. When a slide using a different master page is copied to a deck, Impress will query the user with an ‘Confirmation’ dialog asking if the copied slide’s format (e.g. its master page) should be copied over to the destination deck. The dialog looks like Fig. 159.

Confirmation Dialog screen shot

Fig. 159 :Confirmation Dialog

If you want the format of the copied deck to be retained in its new location, then you have to click the “Yes” button. Carrying out this clicking action programmatically requires stepping outside the Office API, and is using OooDev GUI Automation for windows to interact with the dialog box.

The resulting code is in the append_slides.py example. The main() function mostly processes the *fnms of __init__ arguments, a list of filenames. The first file is the destination deck for the slides copied from the other files:

# in append_slides.py (partial class)

class AppendSlides:
    def __init__(self, *fnms: PathOrStr) -> None:
        if len(fnms) == 0:
            raise ValueError("At lease one file is required. fnms has no values.")
        for fnm in fnms:
            _ = FileIO.is_exist_file(fnm, True)

        self._fnms = fnms

    def main(self) -> None:
        loader = Lo.load_office(Lo.ConnectPipe())

        try:
            doc = Lo.open_doc(fnm=self._fnms[0], loader=loader)

            GUI.set_visible(is_visible=True, odoc=doc)

            self._to_ctrl = GUI.get_current_controller(doc)
            self._to_frame = GUI.get_frame(doc)

            # Switch to slide sorter view so that slides can be pasted
            Lo.delay(500)
            Lo.dispatch_cmd(cmd=DrawViewDispatch.DIA_MODE, frame=self._to_frame)

            to_slides = Draw.get_slides(doc)

            # monitor for Confirmation dialog
            if DialogAuto:
                DialogAuto.monitor_dialog('y')

            for fnm in self._fnms[1:]:  # start at 1
                try:
                    app_doc = Lo.open_doc(fnm=fnm, loader=loader)
                except Exception as e:
                    print(f'Could not open the file "{fnm}"')
                    print(f"  {e}")
                    continue

                self._append_doc(to_slides=to_slides, doc=app_doc)

            Lo.delay(500)
            # Lo.dispatch_cmd(cmd=DrawViewDispatch.PAGE_MODE, frame=self._to_frame)  # does not work
            Lo.dispatch_cmd(cmd=DrawDrawingDispatch.DRAWING_MODE)
            Lo.delay(1000)

            msg_result = MsgBox.msgbox(
                "Do you wish to close document?",
                "All done",
                boxtype=MessageBoxType.QUERYBOX,
                buttons=MessageBoxButtonsEnum.BUTTONS_YES_NO,
            )
            if msg_result == MessageBoxResultsEnum.YES:
                Lo.close_doc(doc=doc,deliver_ownership=True)
                Lo.close_office()
            else:
                print("Keeping document open")
        except Exception:
            Lo.close_office()
            raise

Dispatch lookups from dispatch namespace are used for convenience.

Note that the controller and frame reference for the destination deck are saved as instance Variables of class.

# in AppendSlides.main() of append_slides.py
self._to_ctrl = GUI.get_current_controller(doc)
self._to_frame = GUI.get_frame(doc)

This is done this to reduce the number of arguments passed between the methods.

The for-loop in the middle of main() processes each of the slide decks in turn, appending their slides to the destination deck.

_append_doc() accesses a slide deck in a second Impress window, which means that a second controller and frame reference are needed; they’re stored in self._to_ctrl and self._to_frame:

# in AppendSlides of append_slides.py
def _append_doc(self, to_slides: XDrawPages, doc: XComponent) -> None:
    # Append doc to the end of  toSlides.
    # Access the slides in the document, and the document's controller and frame refs.
    # Switch to slide sorter view so that slides can be copied.
    GUI.set_visible(is_visible=True, odoc=doc)

    from_ctrl = GUI.get_current_controller(doc)
    from_frame = GUI.get_frame(doc)
    Lo.dispatch_cmd(cmd="DiaMode", frame=from_frame)
    try:
        from_slides = Draw.get_slides(doc)
        print("- Adding slides")
        self._append_slides(
            to_slides=to_slides, from_slides=from_slides,
            from_ctrl=from_ctrl, from_frame=from_frame
        )
    except mEx.DrawPageMissingError:
        print("- No Slides Found")

    # Lo.dispatchCmd("PageMode");  // back to normal mode (not working)
    Lo.dispatch_cmd(cmd=DrawDrawingDispatch.DRAWING_MODE)
    Lo.close_doc(doc)
    print()

_append_doc() calls _append_slides() to cycle through the slides, copying each one to the destination deck:

# in AppendSlides of append_slides.py
def _append_slides(self,
    to_slides: XDrawPages, from_slides: XDrawPages, from_ctrl: XController, from_frame: XFrame
) -> None:
    # Append fromSlides to the end of toSlides
    # Loop through the fromSlides, copying each one.
    for i in range(from_slides.getCount()):
        from_slide = Draw.get_slide(from_slides, i)

        # the copy will be placed after this slide
        to_slide = Draw.get_slide(to_slides, to_slides.getCount() - 1)

        self._copy_to(
            from_slide=from_slide,
            from_ctrl=from_ctrl,
            from_frame=from_frame,
            to_slide=to_slide,
            to_ctrl=self._to_ctrl,
            to_frame=self._to_frame,
        )

The for-loop in _append_slides() calls _copy_to() which copies and pastes a slide using dispatch commands. In addition ( for Windows ), it deals with the ‘Confirmation’ dialog in Fig. 159 by way of OooDev GUI Automation for windows.

# in main() of append_slides.py
# monitor for Confirmation dialog
if DialogAuto:
    DialogAuto.monitor_dialog('y')

See also

Class DialogAuto

The _copy_to() Method:

# in AppendSlides of append_slides.py
def _copy_to(
    self,
    from_slide: XDrawPage,
    from_ctrl: XController,
    from_frame: XFrame,
    to_slide: XDrawPage,
    to_ctrl: XController,
    to_frame: XFrame,
) -> None:
    # Copy fromSlide to the clipboard, and
    # then paste it to after the toSlide. Unfortunately, the
    # paste requires a "Yes" button to be pressed.

    Draw.goto_page(from_ctrl, from_slide)  # select this slide
    print("-- Copy -->")
    Lo.dispatch_cmd(cmd=GlobalEditDispatch.COPY, frame=from_frame)
    Lo.delay(1000)

    Draw.goto_page(to_ctrl, to_slide)
    print("Paste")

    Lo.dispatch_cmd(cmd=GlobalEditDispatch.PASTE, frame=to_frame)

The Append Slides and Copy Slide examples highlight important missing features in the presentation API. Copying and pasting a slide in a deck should be available as methods in XDrawPages.

If you need a robust way of doing these tasks then take a look at the ODF Toolkit library in Chapter 51.

17.6 Exporting a Slide as an Image

A drawing or slide can be exported as an image by using the GraphicExportFilter service and its XGraphicExportFilter interface. The service is represented in Fig. 160.

The Graphic Export Filter Service, Interfaces, and Methods.

Fig. 160 :The GraphicExportFilter Service, Interfaces, and Methods.

In older documentation, such as the Developer’s Guide, there’s no mention of XGraphicExportFilter. The guide claims that GraphicExportFilter directly supports XExporter, XFilter, and XMimeTypeInfo.

The start.py for Slide to Image example read three arguments from the command line: the filename --file of the slide deck, the index number of the slide --idx, and the format used for saving the slide’s image --out_fmt.

For example:

python -m start --file "resources/presentation/algs.ppt" --out_fmt "jpeg" --idx 0

Optionally start.py also reads a --output_dir that determines where output is saved.

Use python -m start -h to see all options.

The index number (--idx) value may be a source of confusion since slides are numbered from 1 inside Impress’ GUI, but from 0 in the API. In this case index is zero based so, --idx 2 means the third slide in the deck.

The main() function for slide_2_image.py:

# Slide2Image.main() in slide_2_image.py
def main(self) -> None:
    # connect headless. will not need to see slides
    with Lo.Loader(Lo.ConnectPipe(headless=True)) as loader:
        doc = Lo.open_doc(fnm=self._fnm, loader=loader)

        if not Info.is_doc_type(obj=doc, doc_type=Lo.Service.IMPRESS):
            Lo.print("-- Not a slides presentation")
            return

        slide = Draw.get_slide(doc=doc, idx=self._idx)

        names = ImagesLo.get_mime_types()
        Lo.print("Known GraphicExportFilter mime types:")
        for name in names:
            Lo.print(f"  {name}")

        out_fnm = self._out_dir / f"{self._fnm.stem}{self._idx}.{self._img_fmt}"
        Lo.print(f'Saving page {self._idx} to "{out_fnm}"')
        mime = ImagesLo.change_to_mime(self._img_fmt)
        Draw.save_page(page=slide, fnm=out_fnm, mime_type=mime)
        Lo.close_doc(doc)

Note how connection to LibreOffice is done with in headless mode. This basically runs LibreOffice in the background without a GUI interface.

The example uses two mime functions: ImagesLo.get_mime_types() and ImagesLo.change_to_mime(). The first returns an array of all the mime types supported by GraphicExportFilter by calling XMimeTypeInfo.getSupportedMimeTypeNames():

# in ImagesLo class
@staticmethod
def get_mime_types() -> Tuple[str, ...]:
    mi = Lo.create_instance_mcf(
        XMimeTypeInfo, "com.sun.star.drawing.GraphicExportFilter", raise_err=True
    )
    return mi.getSupportedMimeTypeNames()
The printed output:
Known GraphicExportFilter mime types:
  image/x-MS-bmp
  image/x-emf
  image/x-eps
  image/gif
  image/jpeg
  image/x-met
  image/x-portable-bitmap
  image/x-pict
  image/x-portable-graymap
  image/png
  image/x-portable-pixmap
  image/x-cmu-raster
  image/svg+xml
  image/x-svm
  image/tiff
  image/x-wmf
  image/x-xpixmap

ImagesLo.change_to_mime() looks through the mime array for a type that contains the supplied format as a substring:

# in ImagesLo class
@classmethod
def change_to_mime(cls, im_format: str) -> str:
    names = cls.get_mime_types()
    imf = im_format.lower().strip()
    for name in names:
        if imf in name:
            print(f"using mime type: {name}")
            return name
    print("No matching mime type, so using image/png")
    return "image/png"

Draw.save_page() creates an XGraphicExportFilter object, configuring it with the page to be exported and the mime type filter:

# in Draw class (simplified)
@staticmethod
def save_page(page: XDrawPage, fnm: PathOrStr, mime_type: str) -> None:
    save_file_url = FileIO.fnm_to_url(fnm)
    Lo.print(f'Saving page in "{fnm}"')

    # create graphics exporter
    gef = Lo.create_instance_mcf(
        XGraphicExportFilter, "com.sun.star.drawing.GraphicExportFilter", raise_err=True
    )

    # set the output 'document' to be specified page
    doc = Lo.qi(XComponent, page, True)
    # link exporter to the document
    gef.setSourceDocument(doc)

    # export the page by converting to the specified mime type
    props = Props.make_props(MediaType=mime_type, URL=save_file_url)

    gef.filter(props)
    Lo.print("Export Complete")

See also

The name of the XExporter.setSourceDocument() method is a bit misleading since its input argument should either be an XDrawPage (a slide, as here), XShape (a shape on the slide), or an XShapes object (a collection of shapes on a slide).

XFilter.filter() exports the slide (or shape), based on values supplied in a properties array. The array should contain the mime type and the URL of the output file.

17.7 Extracting the Text from a Slide Deck

Draw.get_shapes_text() supports the fairly common task of extracting all the text from a presentation. It is used by the Extract Text example:

# extract_text.py
from __future__ import annotations
from ooodev.loader.lo import Lo
from ooodev.office.draw import Draw
from ooodev.utils.type_var import PathOrStr
from ooodev.utils.file_io import FileIO

class ExtractText:
    def __init__(self, fnm: PathOrStr) -> None:
        _ = FileIO.is_exist_file(fnm=fnm, raise_err=True)
        self._fnm = FileIO.get_absolute_path(fnm)

    def main(self) -> None:
        with Lo.Loader(Lo.ConnectPipe(headless=True)) as loader:
            doc = Lo.open_doc(fnm=self._fnm, loader=loader)

            if Draw.is_shapes_based(doc):
                print("Text Content".center(46, "-"))
                print(Draw.get_shapes_text(doc))
                print("-" * 46)
            else:
                print("Text extraction unsupported for this document type")

            Lo.delay(1000)
            Lo.close_doc(doc)

Draw.get_shapes_text() calls get_ordered_shapes() to collect all the shapes from all the slides in the document. It then iterates over the shapes list, converting each shape to text and adding it to a String List:

# in Draw Class (simplified)
@classmethod
def get_shapes_text(cls, doc: XComponent) -> str:
    sb: List[str] = []
    shapes = cls.get_ordered_shapes(doc)
    for shape in shapes:
        text = cls.get_shape_text(shape)
        sb.append(text)
    return "\n".join(sb)

Draw.get_shape_text() pulls the text from a shape by casting it to a text interface, then uses a cursor to select the text:

# in Draw Class (overload method, simplified)
@classmethod
def get_shape_text(cls, shape: XShape) -> str:
    try:
        xtext = Lo.qi(XText, shape, True)
        xtext_cursor = xtext.createTextCursor()
        xtext_rng = Lo.qi(XTextRange, xtext_cursor, True)
        text = xtext_rng.getString()
        return text
    except Exception as e:
        raise DrawError("Error getting shape text from shape") from e

Draw.get_ordered_shapes() iterates over each slide in the document calling another version of itself to extract the shapes from a slide:

# in Draw Class (overload method, simplified)
@classmethod
def get_ordered_shapes(cls, doc: XComponent) -> List[XShape]:
    # get all the shapes in all the pages of the doc, in z-order per slide
    slides = cls.get_slides_list(doc)
    if not slides:
        return []
    shapes: List[XShape] = []
    for slide in slides:
        shapes.extend(cls.get_ordered_shapes(slide))
    return shapes

The ordering of the shapes in a slide may not match their reading order (i.e. top- down, left-to-right). For example, a slide is read by first looking at the text in the TitleShape, before reading the bullets below in the OutlinerShape. However, TitleShape may be stored at the end of the slide’s container.

Draw.get_ordered_shapes() deals with the problem by extracting all the shapes from the slide into a list, and then sorting it based on each shape’s z-order. A shape with z-order 0 will be moved before a shape with z-order 1. This almost always corresponds to the user’s reading order of the shapes. For example, TitleShape usually has a z-order of 0.

# in Draw Class (overload method, simplified)
@classmethod
def get_ordered_shapes(cls, slide: XDrawPage) -> List[XShape]:
    def sorter(obj: XShape) -> int:
        return cls.get_zorder(obj)

    shapes = cls.get_shapes(slide)
    sorted_shapes = sorted(shapes, key=sorter, reverse=False)
    return sorted_shapes

Draw.get_shapes() extracts all the shapes from a slide as a list:

# in Draw Class (overload method, simplified)
@classmethod
def get_shapes(cls, slide: XDrawPage) -> List[XShape]:
    if slide.getCount() == 0:
        Lo.print("Slide does not contain any shapes")
        return []

    shapes: List[XShape] = []
    for i in range(slide.getCount()):
        shapes.append(Lo.qi(XShape, slide.getByIndex(i), True))
    return shapes