If we can’t have ProPresenter on an iPad…..what about “SimplePresenter”….

A long time ago I started work on a super, dumbed-down, simplified version of ProPresenter on an iPad.  I got the prototype basically working and kinda stalled and shelved the idea for many months.

Since Pro7 came out, and the chips that Apple are making are just getting silly fast – I started to entertain the idea of picking the project back up again….

This weekend gone, I just added support for Pro7 presentations (using my reversed engineered .proto files) – and it worked a treat!

This is a Pro7 document!!!

Maybe there will be more to come on this…..(I still love the UX of the iPad soooo much and have some ideas for some nice UI interactions when using it to present) 

This is part 3 in the series and it is a work in progress – its barely started in fact…

I have published it now for the sake of quickly sharing some example C# code and will update it over the next few days (maybe weeks).

For now, you can find below the required C# code that will create a simple ProPresenter 7 document with two slides like this:

My First Pro7 Presentation Created By C# Code


TODO:

  • Demo how to use protoc to generate C# code from the .proto files
  • Demo making a new C# project, adding reference to Google.Protobuf and then adding all the generated C# code files
  • Demo writing simple code to create a document
  • DO a bit of a dive into structure of document and the RTF format used for text elements.
  • Point out that you can generate other languages (eg Python, Java)


Here is a dump of some manually written code that creates a new simple ProPresenter 7 document. The underlying “plumbing” code that enables this to work is auto-generated by the protoc compiler.

 

 

// You need to add these namespaces...
using Rv.Data;
using Google.Protobuf;



// Code to read an existing presentation and create a new one from scratch
Rv.Data.Presentation newPresentation, existingPresentation;

using (var input = File.OpenRead(@"C:\Users\media\Documents\ProPresenter\Libraries\Default\Alive.pro"))  // Path to existing presentation
{
    existingPresentation = Rv.Data.Presentation.Parser.ParseFrom(input);
} // Put in break point here to inspect existing presetation and learn the structure


//  ***** Create new presentation *****
newPresentation = new Rv.Data.Presentation
{
    Uuid = new UUID { String = Guid.NewGuid().ToString() },
    Name = "HelloWorld",
    ApplicationInfo = new ApplicationInfo 
    {
        Application = ApplicationInfo.Types.Application.Propresenter,
        Platform = ApplicationInfo.Types.Platform.Windows
    }
};


// Create a new cue (with a single action of type presentation_slide)
Cue newCue = new Cue
{
    Uuid = new UUID { String = Guid.NewGuid().ToString() },
    IsEnabled = true,
    Actions = { new Rv.Data.Action
    {
        Uuid = new UUID { String = Guid.NewGuid().ToString() },
        IsEnabled = true,
        Label = new Rv.Data.Action.Types.Label 
        { 
            Text = "Hello Slide1",
            Color = new Rv.Data.Color
            {
                Red = 0.8f,
                Green = 0.1f,
                Blue = 0.4f,
                Alpha = 1
            } 
        },
        Slide = new Rv.Data.Action.Types.SlideType
        {
            Presentation = new PresentationSlide
            {
                BaseSlide = new Slide
                {
                    Uuid = new UUID
                    {
                        String = Guid.NewGuid().ToString()
                    },
                    BackgroundColor = new Rv.Data.Color
                    {
                        Red = 0.05f,
                        Blue =0.2f,
                        Green = 0.6f,
                        Alpha = 0.4f
                    },
                    DrawsBackgroundColor = true,
                    Size = new Graphics.Types.Size
                    {
                        Width = 1920,
                        Height = 1080
                    },
                    
                    Elements = {
                        new Slide.Types.Element {
                            Element_ = new Graphics.Types.Element
                            {
                                Uuid = new UUID
                                {
                                    String = Guid.NewGuid().ToString()
                                },
                                Fill = new Graphics.Types.Fill
                                {
                                    Color = new Rv.Data.Color{Alpha =0} // Transparent fill for element
                                },
                                Name = "Hello Element",
                                Bounds = new Graphics.Types.Rect {
                                    Origin = new Graphics.Types.Point {X =0, Y=0},
                                    Size = new Graphics.Types.Size {Width = 1920, Height = 1080}
                                },
                                Opacity = 1,
                                Text = new Graphics.Types.Text
                                {
                                    VerticalAlignment = Graphics.Types.Text.Types.VerticalAlignment.Middle,
                                    RtfData = ByteString.CopyFrom("{\\rtf0\\ansi\\ansicpg1252" +
                                    "{\\fonttbl\\f0\\fnil Arial;}" +
                                    "{\\colortbl\\red255\\green255\\blue255;}" +
                                    "{\\*\\expandedcolortbl\\csgenericrgb\\c100000\\c100000\\c100000\\c100000;\\csgenericrgb\\c100000\\c100000\\c100000\\c100000;}" +
                                    "{\\*\\listtable}{\\*\\listoverridetable}" +
                                    "\\uc1\\paperw12240\\margl0\\margr0\\margt0\\margb0" +
                                    "\\pard\\qc\\slmult0\\slleading0\\f0\\b0\\ul0\\strike0\\fs400\\cf0\\strokewidth0\\strokec1\\nosupersub Hello World"+
                                    "\\par\\pard\\qc\\slmult0\\slleading0\\f0\\i\\b0\\ul0\\strike0\\fs200\\cf0\\strokewidth0\\strokec1\\nosupersub (Slide with green background)}",
                                    Encoding.ASCII),
                                },
                                Path = new Graphics.Types.Path
                                {
                                    Shape = new Graphics.Types.Path.Types.Shape
                                    {
                                        Type = Graphics.Types.Path.Types.Shape.Types.Type.Rectangle
                                    }
                                },
                                

                            }
                        } 
                    }
                    
                }
            }
        },
        Type = Rv.Data.Action.Types.ActionType.PresentationSlide
    }
    }
};

// Add new cue (slide)
newPresentation.Cues.Add(newCue);

Cue newCue2 = new Cue
{
    Uuid = new UUID
    {
        String = Guid.NewGuid().ToString()
    },
    IsEnabled = true,
    Actions = { new Rv.Data.Action
    {
        Uuid = new UUID
        {
            String = Guid.NewGuid().ToString()
        },
        Label = new Rv.Data.Action.Types.Label
        { 
            Text = "Hello Slide2",
            Color = new Rv.Data.Color 
            {
                Red = 0.46f,
                Green = 0.0f,
                Blue = 0.8f,
                Alpha = 1}
        },
        Slide = new Rv.Data.Action.Types.SlideType
        {
            Presentation = new PresentationSlide
            {
                BaseSlide = new Slide
                {
                    Uuid = new UUID
                    {
                        String = Guid.NewGuid().ToString()
                    },
                    BackgroundColor = new Rv.Data.Color 
                    {
                        Red = 1,
                        Green = 1,
                        Blue = 1,
                        Alpha = 1
                    },
                    Size = new Graphics.Types.Size 
                    {
                        Width = 1920,
                        Height = 1080
                    }
                }
            }
            
        },
        Type = Rv.Data.Action.Types.ActionType.PresentationSlide,
    }
    }
};

// Add new cue (slide)
newPresentation.Cues.Add(newCue2);


// Add a new group with above two slides
newPresentation.CueGroups.Add(new Presentation.Types.CueGroup
{
    CueIdentifiers = { newCue.Uuid, newCue2.Uuid },
    Group = new Group 
    { 
        Uuid = new UUID
        {
            String = Guid.NewGuid().ToString()
        },
        Name = "Hello Group",
        Color = new Rv.Data.Color
        {
            Red = 0.0f,
            Green = 0.467f,
            Blue = 0.8f,
            Alpha = 1
        }
    },
    
}
);


// Create a single arrangement with above group
newPresentation.Arrangements.Add(new Presentation.Types.Arrangement
{

    Uuid = new UUID
    {
        String = Guid.NewGuid().ToString()
    },
    Name = "Hello Arrangement",

    // Just add the one group to this arrangement
    GroupIdentifiers = { newPresentation.CueGroups[0].Group.Uuid }
    
});

// Set selecte arrangement to the one and only arrangement
newPresentation.SelectedArrangement = newPresentation.Arrangements[0].Uuid;

// Save to file!
using (var fileStream = File.Create(@"C:\Users\media\Documents\ProPresenter\Libraries\Default\HelloWorld.pro"))
{
    newPresentation.WriteTo(fileStream); // This is an override method provided in Google.Protobuf namespace (make sure to add a using Google.Protobuf;)
}
</div>
</div>
</div>
</div>

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