Imports System.Text Imports System.Text.RegularExpressions Public Class DolbyVisionMetadataFile Private _autoCropThresholdBegin As Integer = 1000 Private _autoCropThresholdEnd As Integer = 1000 Private _lastWriteTime As DateTime = Nothing Private _lastWriteTimeL5 As DateTime = Nothing Public ReadOnly Property Path As String = Nothing Public ReadOnly Property Edits As New List(Of RpuEdit) Public ReadOnly Property Presets As New List(Of RpuPreset) Public ReadOnly Property LastWriteTime As DateTime Get Return _lastWriteTime End Get End Property Public ReadOnly Property LastWriteTimeL5 As DateTime Get Return _lastWriteTimeL5 End Get End Property Public ReadOnly Property Level5JsonFilePath As String Get Return If(String.IsNullOrWhiteSpace(Path), "", $"{Path.DirAndBase()}_L5.json") End Get End Property Public ReadOnly Property EditorConfigFilePath As String Get Return If(String.IsNullOrWhiteSpace(Path), "", $"{Path.DirAndBase()}_Config.json") End Get End Property Public ReadOnly Property CroppedRpuFilePath As String Get Return If(String.IsNullOrWhiteSpace(Path), "", $"{Path.DirAndBase()}_Cropped.rpu") End Get End Property Public ReadOnly Property HasLevel5Changed As Boolean Get Return Level5JsonFilePath.FileExists() AndAlso _lastWriteTimeL5 <> File.GetLastWriteTimeUtc(Level5JsonFilePath) End Get End Property Public ReadOnly Property Crop As Padding Get Select Case p.AutoCropDolbyVisionMode Case AutoCropDolbyVisionMode.Automatic Return GetAutoCrop(_autoCropThresholdBegin, _autoCropThresholdEnd) Case AutoCropDolbyVisionMode.ManualThreshold Return GetCrop(p.AutoCropDolbyVisionThresholdBegin, p.AutoCropDolbyVisionThresholdEnd) Case Else Throw New NotImplementedException(NameOf(AutoCropDolbyVisionMode)) End Select End Get End Property Private Sub New() End Sub Public Sub New(filePath As String) Me.Path = filePath Me._lastWriteTime = File.GetLastWriteTimeUtc(filePath) WriteLevel5Export(True, False) ReadLevel5Export() End Sub Public Function GetAutoCrop() As Padding Return GetAutoCrop(_autoCropThresholdBegin, _autoCropThresholdEnd) End Function Public Function GetAutoCrop(thresholdBegin As Integer, thresholdEnd As Integer) As Padding thresholdBegin = Math.Max(0, thresholdBegin) thresholdEnd = Math.Max(0, thresholdEnd) If Not Edits?.Any() Then Return New Padding(0) If Not Presets?.Any() Then Return New Padding(0) Dim newCrop As New Padding(Integer.MaxValue) Dim entries = Edits.Join(Presets, Function(edit) edit.Id, Function(preset) preset.Id, Function(edit, preset) New With {edit.StartFrame, edit.EndFrame, edit.Id, preset.Offset}).OrderBy(Function(x) x.StartFrame) For i = 0 To entries.Count() - 1 Dim entry = entries.ElementAt(i) Dim same = entries.Where(Function(x) x.Id = entry.Id).Count() Dim take = True take = If(take AndAlso i = 0 AndAlso same = 1 AndAlso entry.Offset = Padding.Empty AndAlso entry.EndFrame < thresholdBegin, False, take) take = If(take AndAlso i = entries.Count() - 1 AndAlso same = 1 AndAlso entry.Offset = Padding.Empty AndAlso entry.StartFrame > entry.EndFrame - thresholdEnd, False, take) If take Then newCrop.Left = Math.Min(newCrop.Left, entry.Offset.Left) newCrop.Top = Math.Min(newCrop.Top, entry.Offset.Top) newCrop.Right = Math.Min(newCrop.Right, entry.Offset.Right) newCrop.Bottom = Math.Min(newCrop.Bottom, entry.Offset.Bottom) End If Next Return newCrop End Function Public Function GetCrop() As Padding Return GetCrop(0, 0) End Function Public Function GetCrop(thresholdBegin As Integer, thresholdEnd As Integer) As Padding thresholdBegin = Math.Max(0, thresholdBegin) thresholdEnd = Math.Max(0, thresholdEnd) If Not Edits?.Any() Then Return New Padding(0) If Not Presets?.Any() Then Return New Padding(0) Dim newCrop As New Padding(Integer.MaxValue) Dim frames = Edits.OrderByDescending(Function(x) x.EndFrame).First().EndFrame Dim entries = Edits.Join(Presets, Function(edit) edit.Id, Function(preset) preset.Id, Function(edit, preset) New With {edit.StartFrame, edit.EndFrame, preset.Offset}) For Each entry In entries If entry.EndFrame > thresholdBegin AndAlso entry.StartFrame <= (frames - thresholdEnd) Then newCrop.Left = Math.Min(newCrop.Left, entry.Offset.Left) newCrop.Top = Math.Min(newCrop.Top, entry.Offset.Top) newCrop.Right = Math.Min(newCrop.Right, entry.Offset.Right) newCrop.Bottom = Math.Min(newCrop.Bottom, entry.Offset.Bottom) End If Next Return newCrop End Function Public Sub RefreshLevel5Data() If Not HasLevel5Changed Then Exit Sub If Not (p.AutoCropMode = AutoCropMode.DolbyVisionOnly OrElse p.AutoCropMode = AutoCropMode.Always) Then Exit Sub ReadLevel5Export() End Sub Public Sub ReadLevel5Export() If Not Path?.FileExists() Then Return If Not Level5JsonFilePath.FileExists() Then Return Edits.Clear() Presets.Clear() Try Dim jsonContent = Level5JsonFilePath.ReadAllText() Dim strippedJsonContent = jsonContent.Right("presets").Left("edits") strippedJsonContent = Regex.Replace(strippedJsonContent, "\s", "") Dim presetMatches = Regex.Matches(strippedJsonContent, "\{.+?:(?\d+),.+?:(?\d+),.+?:(?\d+),.+?:(?\d+),.+?:(?\d+)\}") If presetMatches.Count = 0 Then Return For Each match As Match In presetMatches Dim offset = New Padding(match.Groups("left").Value.ToInt(), match.Groups("top").Value.ToInt(), match.Groups("right").Value.ToInt(), match.Groups("bottom").Value.ToInt()) Presets.Add(New RpuPreset(match.Groups("id").Value.ToInt(), offset)) Next strippedJsonContent = jsonContent.Right("edits") strippedJsonContent = Regex.Replace(strippedJsonContent, "\s", "") Dim editMatches = Regex.Matches(strippedJsonContent, """(?\d+)-(?\d+)"":(?\d+),?") For Each match As Match In editMatches Edits.Add(New RpuEdit(match.Groups("id").Value.ToInt(), match.Groups("start").Value.ToInt(), match.Groups("end").Value.ToInt())) Next _lastWriteTimeL5 = File.GetLastWriteTimeUtc(Level5JsonFilePath) Catch ex As AbortException Throw ex Catch ex As Exception g.ShowException(ex) Throw New AbortException Finally Log.Save() End Try End Sub Public Sub WriteLevel5Export(Optional logContent As Boolean = True, Optional overwrite As Boolean = False) If Not Path?.FileExists() Then Return If Not overwrite AndAlso Level5JsonFilePath.FileExists() Then Return Try Dim pd = Path.Dir Dim l5 = Level5JsonFilePath.Replace(pd, "." & IO.Path.DirectorySeparatorChar) Dim arguments = $"export --data ""level5={l5}"" -i ""{Path}""" Using proc As New Proc proc.Package = Package.DoViTool proc.Project = p proc.Header = "Extract Level5 data from RPU metadata file" proc.Encoding = Encoding.UTF8 proc.Arguments = arguments proc.WorkingDirectory = pd proc.AllowedExitCodes = {0} proc.OutputFiles = {Level5JsonFilePath} proc.Start() End Using Catch ex As AbortException Throw ex Catch ex As Exception g.ShowException(ex) Throw New AbortException Finally If logContent Then Dim exists = File.Exists(Level5JsonFilePath) Dim content = If(exists, File.ReadAllText(Level5JsonFilePath), "No file created!") Log.WriteHeader($"Content of {IO.Path.GetFileName(Level5JsonFilePath)}") Log.WriteLine(content) End If Log.Save() End Try End Sub Public Sub WriteEditorConfigFile(offset As Padding, Optional logContent As Boolean = True, Optional overwrite As Boolean = True) If Not Level5JsonFilePath?.FileExists() Then Return If Not overwrite AndAlso EditorConfigFilePath.FileExists() Then Return If offset = Padding.Empty Then Return If offset.Left < 0 OrElse offset.Top < 0 OrElse offset.Right < 0 OrElse offset.Bottom < 0 Then Throw New ArgumentOutOfRangeException(NameOf(offset)) Try Dim p = "" For Each entry In Presets.OrderBy(Function(x) x.Id) Dim left = Math.Max(0, entry.Offset.Left - offset.Left) Dim top = Math.Max(0, entry.Offset.Top - offset.Top) Dim right = Math.Max(0, entry.Offset.Right - offset.Right) Dim bottom = Math.Max(0, entry.Offset.Bottom - offset.Bottom) p += $"{{" p += $"""id"":{entry.Id}," p += $"""left"":{left}," p += $"""right"":{right}," p += $"""top"":{top}," p += $"""bottom"":{bottom}" p += $"}}," Next p = p.TrimEnd(","c) p = $"""presets"":[{p}]" Dim e = "" For Each entry In Edits.OrderBy(Function(x) x.StartFrame) e += $"""{entry.StartFrame}-{entry.EndFrame}"":{entry.Id}," Next e = e.TrimEnd(","c) e = $"""edits"":{{{e}}}" Dim result = $"{{""active_area"":{{""crop"":false,{p},{e}}}}}" result.WriteFileUTF8(EditorConfigFilePath) Catch ex As AbortException Throw ex Catch ex As Exception g.ShowException(ex) Throw New AbortException Finally If logContent Then Dim exists = File.Exists(EditorConfigFilePath) Dim content = If(exists, File.ReadAllText(EditorConfigFilePath), "No file created!") Log.WriteHeader($"Content of {IO.Path.GetFileName(EditorConfigFilePath)}") Log.WriteLine(content) End If Log.Save() End Try End Sub Public Function WriteCroppedRpu(Optional overwrite As Boolean = True) As String If Not Path?.FileExists() Then Return Nothing If Not EditorConfigFilePath?.FileExists() Then Return Nothing If Not overwrite AndAlso CroppedRpuFilePath.FileExists() Then Return Nothing Try Dim arguments = $"editor -i ""{Path}"" -j ""{EditorConfigFilePath}"" -o ""{CroppedRpuFilePath}""" Using proc As New Proc proc.Package = Package.DoViTool proc.Project = p proc.Header = "Creating new RPU metadata file" proc.Encoding = Encoding.UTF8 proc.Arguments = arguments proc.AllowedExitCodes = {0} proc.OutputFiles = {CroppedRpuFilePath} proc.Start() End Using Catch ex As AbortException Throw ex Catch ex As Exception g.ShowException(ex) Throw New AbortException Finally Log.Save() End Try Return CroppedRpuFilePath End Function End Class