Part 2 of a technical exploration of the ProPresenter 7 file format (.pro)

N.B. This information is probably only of interest to developers – nothing here will be of any real use to “regular users” of ProPresenter.

TLDR: I now have a useable set of proto files that enable programmers to automatically generate code for their applications and scripts that can work directly with pro7 document and configuration files.

In part 1 of my series on exploring the new Pro7 file format, I had discovered that Pro7 uses Google protocol buffers and started to reverse engineer the structure of the (.pro) document file format. I now have a somewhat-complete and useable set of proto files that enable programmers to create code for their applications and scripts that can work directly with pro7 document and configuration files.

If you are a programmer who has built applications or scripts to work with .pro6 documents, this might be of interest to you.  You will need to understand how to use .proto files, the tutorials by Google are great – read them to quickly get up to speed on how easy it is to automatically generate code from .proto files!

Please note that the .proto files I have created are unsupported reverse-engineered files. Do NOT contact Renewed Vision (the makers of ProPresenter) for support if you blow up your documents, configuration or the entire computer!

How were they reverse-engineered?

This is not important, and you might like to skip ahead to the next section. This is a rough overview of how the .proto files were reverse engineered – just in case there any nerds out there that are interested.

In my first post I started doing this by manual methods. Over the weeks that followed, I found a much better way. It turns out that the Google protocol buffer compiler generates C#.NET code that allows the use of Reflection in .NET to get the required information to rebuild the .proto files that were originally used to generate it. An example is given at the end of the Protocol Buffer Basics: C# Tutorial…

public void PrintMessage(IMessage message)
{var descriptor = message.Descriptor;foreach (var field in descriptor.Fields.InDeclarationOrder()){Console.WriteLine("Field {0} ({1}): {2}",
            field.FieldNumber,
            field.Name,
            field.Accessor.GetValue(message);}
}

ProPresenter 7 is written in .NET so I could employ this method by creating a new .NET project, and adding references to several of the Pro7 .NET class libraries which are stored in .dll files. After some time exploring, I was able to create a PlaylistDocument object and then use reflection on that object to walk through the data structures and write out the .proto definitions for the playlist and also all of its dependencies (which includes presentations). I won’t go into any more detail here as it’s a bit off topic and it involved some recursive code that made my head hurt!

By the way, the idea that you can load .NET class libraries (stored in .dll files) from other applications and use functionality within them is so cool that I think it deserves a whole other blog post in itself.  A lot of functionality is exposed through the Pro7 class libraries and I might explore them and make another blog post later.

Anyway – the key point is that I now have a complete set of .proto files that seem to work – so it’s time to explore and play!

Using Google “protoc” to decode and explore Pro7 files

Before using  protoc to generate code that can work with ProPresenter files I noticed that protoc can also decode existing protocol buffer files and reveal their data structure in a human readable format.

This seemed like a good way to quickly test my newly reversed engineered .proto files and start exploring the data structures. Using the protoc command to decode existing files and checking if the output looked sensible would give me confidence that the reverse engineered .proto files are working. The command to do this for decoding a Presentation document is: 

protoc --decode rv.data.Presentation /Path/To/propresenter.proto < /Path/To/ProPresenter/PresentationFile

With a similar command to decode a Playlist document:

protoc --decode rv.data.PlaylistDocument /Path/To/propresenter.proto < /Path/To/ProPresenter/PlaylistFile

The output from these commands is JSON-like plain text representation of the data in the file which is easy enough to read and start exploring the structure.  There are also .proto files to work with other data and configuration files as well eg: masks, props, looks, proworkspace, screens, timers, calendars etc. 

For now, lets explore the presentation format…

There is a lot of data in a presentation file so I decided to create a simple presentation that has just a single slide in a “Verse 1” group with a custom slide label and explore the decoded text of that first.

A Simple Presentation – Ready to decode and learn from!

Decoding the simple presentation file resulted in several pages of JSON like text in my terminal.  To make it easier to review the decoded output, I redirected the output into a text file called “SimplePresentation.txt” so that I could then open and review that file in my favourite text editor Visual Studio Code (VSCode)

protoc --decode rv.data.Presentation ./presentation.proto < /mnt/c/Users/gsg/Documents/ProPresenter/Libraries/Default/SimplePresentation.pro > SimplePresentation.txt

VSCode is an awesome editor and it makes it much easier to review and understand the JSON-like text structure thanks to coloured syntax highlighting and collapsible text sections.  At first, I tried the JSON syntax highlighter in VSCode, but later found I actually preferred the C# syntax highlighting for this output.

Here is the complete output
This is selectable text (with syntax colouring is provided by a plugin on this blog).

application_info {
  platform: PLATFORM_WINDOWS
  platform_version {
    major_version: 10
    patch_version: 4294967295
    build: "19041"
  }
  application: APPLICATION_PROPRESENTER
  application_version {
    major_version: 7
    minor_version: 2
    build: "117571592"
  }
}
uuid {
  string: "46af336c-772e-4ed4-bcca-9e489720cf29"
}
name: "SimplePresentation"
last_modified_date {
  seconds: 1599649962
}
background {
  color {
    alpha: 1
  }
}
selected_arrangement {
  string: "0794cd32-a81c-4856-b30f-a6bf70260c58"
}
cue_groups {
  group {
    uuid {
      string: "07638658-411a-4dd6-b14b-851b61a8712e"
    }
    name: "Verse 1"
    color {
      green: 0.466666669
      blue: 0.8
      alpha: 1
    }
    hotKey {
      code: KEY_CODE_ANSI_A
    }
  }
  cue_identifiers {
    string: "63dd033d-e5da-46b5-bc02-03809d2b1beb"
  }
}
cues {
  uuid {
    string: "63dd033d-e5da-46b5-bc02-03809d2b1beb"
  }
  completion_target_uuid {
    string: "00000000-0000-0000-0000-000000000000"
  }
  completion_action_type: COMPLETION_ACTION_TYPE_LAST
  completion_action_uuid {
    string: "00000000-0000-0000-0000-000000000000"
  }
  trigger_time {
  }
  actions {
    uuid {
      string: "e8adc9c4-3583-4c06-b884-d7e0d05fa8c2"
    }
    label {
      text: "SlideLabel"
      color {
        red: 1
        blue: 1
        alpha: 1
      }
    }
    isEnabled: true
    type: ACTION_TYPE_PRESENTATION_SLIDE
    slide {
      presentation {
        base_slide {
          elements {
            element {
              uuid {
                string: "33c8503c-8093-45ce-8817-3510ce86bf38"
              }
              bounds {
                origin {
                  x: 100.51890289103039
                  y: 135
                }
                size {
                  width: 1720.3854707190515
                  height: 810
                }
              }
              opacity: 1
              path {
                closed: true
                points {
                  point {
                  }
                  q0 {
                  }
                  q1 {
                  }
                }
                points {
                  point {
                    x: 1
                  }
                  q0 {
                    x: 1
                  }
                  q1 {
                    x: 1
                  }
                }
                points {
                  point {
                    x: 1
                    y: 1
                  }
                  q0 {
                    x: 1
                    y: 1
                  }
                  q1 {
                    x: 1
                    y: 1
                  }
                }
                points {
                  point {
                    y: 1
                  }
                  q0 {
                    y: 1
                  }
                  q1 {
                    y: 1
                  }
                }
                shape {
                  type: TYPE_RECTANGLE
                }
              }
              fill {
                color {
                  red: 0.117647059
                  green: 0.564705908
                  blue: 1
                  alpha: 1
                }
              }
              stroke {
                width: 3
                color {
                  red: 1
                  green: 1
                  blue: 1
                  alpha: 1
                }
              }
              shadow {
                angle: 315
                offset: 5
                radius: 5
                color {
                  alpha: 1
                }
                opacity: 0.75
              }
              feather {
                radius: 0.05
              }
              text {
                attributes {
                  font {
                    name: "ArialMT"
                    size: 100
                    family: "Arial"
                    face: "Regular"
                  }
                  text_color {
                    red: 1
                    green: 1
                    blue: 1
                    alpha: 1
                  }
                  paragraph_style {
                    alignment: ALIGNMENT_CENTER
                    line_height_multiple: 1
                    text_list {
                    }
                  }
                  stroke_color {
                    red: 1
                    green: 1
                    blue: 1
                    alpha: 1
                  }
                }
                shadow {
                  angle: 315
                  offset: 5
                  radius: 5
                  color {
                    alpha: 1
                  }
                  opacity: 0.75
                }
                rtf_data: "{\\rtf0\\ansi\\ansicpg1252{\\fonttbl\\f0\\fnil ArialMT;}{\\colortbl\\red255\\green255\\blue255;}{\\*\\expandedcolortbl\\csgenericrgb\\c100000\\c100000\\c100000\\c100000;}{\\*\\listtable}{\\*\\listoverridetable}\\uc1\\paperw34407\\margl0\\margr0\\margt0\\margb0\\pard\\li0\\fi0\\ri0\\qc\\sb0\\sa0\\sl240\\slmult1\\slleading0\\f0\\b0\\i0\\ul0\\strike0\\fs200\\expnd0\\expndtw0\\cf0\\strokewidth0\\strokec0\\nosupersub This is the first line of text\\par\\pard\\li0\\fi0\\ri0\\qc\\sb0\\sa0\\sl240\\slmult1\\slleading0\\f0\\b0\\i0\\ul0\\strike0\\fs200\\expnd0\\expndtw0\\cf0\\strokewidth0\\strokec0\\nosupersub And this is the second line of text}"
                vertical_alignment: VERTICAL_ALIGNMENT_MIDDLE
                margins {
                }
              }
              text_line_mask {
              }
            }
            info: 3
          }
          background_color {
            alpha: 1
          }
          size {
            width: 1920
            height: 1080
          }
          uuid {
            string: "d64b342a-472d-42e7-bae1-55508d0669bb"
          }
        }
        notes {
          rtf_data: "{\\rtf0\\ansi\\ansicpg1252{\\fonttbl\\f0\\fnil ArialMT;}{\\colortbl\\red0\\green0\\blue0;\\red255\\green255\\blue255;}{\\*\\expandedcolortbl\\csgenericrgb\\c0\\c0\\c0\\c100000;\\csgenericrgb\\c100000\\c100000\\c100000\\c100000;}{\\*\\listtable}{\\*\\listoverridetable}\\uc1\\paperw12240\\margl0\\margr0\\margt0\\margb0\\pard\\li0\\fi0\\ri0\\ql\\sb0\\sa0\\sl240\\slmult1\\slleading0\\f0\\b0\\i0\\ul0\\strike0\\fs100\\expnd0\\expndtw0\\cf0\\strokewidth0\\strokec1\\nosupersub}"
        }
        chord_chart {
        }
      }
    }
  }
  isEnabled: true
}
ccli {
}
timeline {
}

As you can see above, there is a quite a bit of data even in a simple presentation – but it seems the proto files needed for presentation files are working well and if you have these .proto files, you have the power to easily create, read and update Pro7 data files!

A great feature built into Visual Studio Code is that it lets you collapse logical sections of text and this makes it much easier to look at and learn the structure of the data:

The collapsed view makes it easier explore and learn the presentation structure.
Application Info

This describes which application and which operating system that the presentation file was created with.  It also includes version info for both.

UUID

This is the unique identifier for the presentation.UUIDs are used throughout ProPresenter documents as identifiers for many objects. They are stored in PRo7 documents using a standard textual string representation that 36 characters long. What’s quite interesting about GUIDs is that they are basically globally unique – no two GUIDS in the world are the same when generated using standard methods.  .NET includes a standard method to generate them which I’ll use a lot later when building my own presentation from scratch in code.

Name

This one is self-describing!

Last Modified Date

At first glance, you might think “why not use the filesystem last modified date” – I suppose there are some circumstances where it might differ – eg, if the file was touched/corrupted by an application that doesn’t understand the file format 

Background

There is no background in this simple presentation – explore this later.

Selected Arrangement

This is the UUID of the currently selected arrangement.  In this simple presentation there are no arrangements defined – so I suppose this becomes a UUID for the “master arrangement” of how the slides are defined.

Cue Groups

In this simple presentation there is only one group “Verse 1”.  If there were more than one group, then you would see multiple “cue_groups” in a list.

Cues

Slides are found inside cues. There is only one slide in this simple presentation, so there is only one cue in the decoded data.  If there were multiple slides in the presentation, there would be multiple “cues” in a list.

The structure of a cue is rather complex at first glance.
Briefly, cues contain a list of actions and there are several action types of actions.
One of the many type of actions is for representing a slide: “ACTION_TYPE_PRESENTATION_SLIDE”.
Any action that is a “ACTION_TYPE_PRESENTATION_SLIDE” contains a slide object.
That slide object contains a presentation object.
That presentation object contains a base-slide object
That base_slide object which contains a list of slide elements (text elements etc)

Ccli

There is no Copyright info in this simple presentation – explore this later.


Timeline

There is no timeline in this simple presentation – explore this later.

That’s a brief exploration of the file structure using decoded output from protoc.
However, the point of protoc is to “compile” .proto files into code that you can use in your own applications and scripts to easily create, read and modify these files.

In part 3, I will generate some C# code from these .proto files and demonstrate reading existing ProPresenter documents as well as creating a ProPresenter document from scratch in my own custom application!

Teaser: My First Pro7 Presentation Created From Scratch In My Own C# Application