Imports System.Text Imports System.Text.RegularExpressions Imports System.Web.UI.WebControls Imports VB6 = Microsoft.VisualBasic Public Class RpuPreset Public ReadOnly Property Id As Integer = 0 Public ReadOnly Property Offset As New Padding(0) Public Sub New(id As Integer, offset As Padding) Me.Id = id Me.Offset = offset End Sub End Class Public Class RpuEdit Public ReadOnly Property Id As Integer = 0 Public ReadOnly Property StartFrame As Integer Public ReadOnly Property EndFrame As Integer Public Sub New(id As Integer, startFrame As Integer, endFrame As Integer) Me.Id = id Me.StartFrame = startFrame Me.EndFrame = endFrame End Sub End Class Public Class DolbyVisionMetadataFile Private Shared ReadOnly _autoCropThresholdBegin As Integer = 1000 Private Shared ReadOnly _autoCropThresholdEnd As Integer = 1000 Private _crop As Padding? = Nothing Private _lastWriteTime As DateTime = Nothing Private _lastWriteTimeL5 As DateTime = Nothing Private _trimRanges As List(Of Range) = 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 ModifiedRpuFilePath As String Get Return If(String.IsNullOrWhiteSpace(Path), "", $"{Path.DirAndBase()}_Modified.rpu") End Get End Property Public ReadOnly Property TrimmedRpuFilePath As String Get Return If(String.IsNullOrWhiteSpace(Path), "", $"{Path.DirAndBase()}_Trimmed.rpu") End Get End Property Public ReadOnly Property HasPathChanged As Boolean Get Return Path.FileExists() AndAlso _lastWriteTime <> File.GetLastWriteTimeUtc(Path) 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 HasToBeTrimmed As Boolean Get If p.Ranges Is Nothing OrElse Not p.Ranges.Any() Then Return False If _trimRanges Is Nothing OrElse Not _trimRanges.Any() Then Return True If p.Ranges.Count <> _trimRanges.Count Then Return True p.Ranges.Sort() _trimRanges.Sort() For i = 0 To p.Ranges.Count - 1 If p.Ranges(i).CompareTo(_trimRanges(i)) <> 0 Then Return True Next Return False End Get End Property Public ReadOnly Property Crop As Padding Get If _crop Is Nothing Then Select Case p.AutoCropDolbyVisionMode Case AutoCropDolbyVisionMode.Automatic _crop = GetCrop(_autoCropThresholdBegin, _autoCropThresholdEnd) Case AutoCropDolbyVisionMode.ManualThreshold _crop = GetCrop(p.AutoCropDolbyVisionThresholdBegin, p.AutoCropDolbyVisionThresholdEnd) Case Else Throw New NotImplementedException(NameOf(AutoCropDolbyVisionMode)) End Select End If Return _crop.Value End Get End Property Private Sub New() End Sub Public Sub New(filePath As String) Me.Path = filePath Me._lastWriteTime = File.GetLastWriteTimeUtc(filePath) WriteLevel5Export(False, False) ReadLevel5Export() End Sub Private Function GetCrop(thresholdBegin As Integer, thresholdEnd As Integer) As Padding Dim newCrop As New Padding(0) If Not Edits?.Any() Then Return newCrop If Not Presets?.Any() Then Return newCrop Dim max = Edits.Max(Function(x) x.EndFrame) thresholdBegin = Math.Min(max, thresholdBegin) thresholdBegin = Math.Max(0, thresholdBegin) thresholdEnd = Math.Min(max, thresholdEnd) thresholdEnd = Math.Max(0, thresholdEnd) Dim entries = Edits.Where(Function(x) x.StartFrame < (max - thresholdEnd) AndAlso x.EndFrame > thresholdBegin) _ .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).ToList() If entries.Count < 1 Then Return newCrop newCrop.Left = entries.Min(Function(x) x.Offset.Left) newCrop.Top = entries.Min(Function(x) x.Offset.Top) newCrop.Right = entries.Min(Function(x) x.Offset.Right) newCrop.Bottom = entries.Min(Function(x) x.Offset.Bottom) 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(Optional logContent As Boolean = True) 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 If logContent Then Dim exists = File.Exists(Level5JsonFilePath) Dim content = If(exists, File.ReadAllText(Level5JsonFilePath), "No file found!") Log.WriteHeader($"Content of {IO.Path.GetFileName(Level5JsonFilePath)}") Log.WriteLine(content) End If Log.Save() End Try End Sub Public Sub WriteLevel5Export(Optional logContent As Boolean = False, 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, ".") 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 Function ReadProfileFromRpu() As Integer If Not Path?.FileExists() Then Return -1 Dim output = ProcessHelp.GetConsoleOutput(Package.DoViTool.Path.Escape, $"info -i {Path.Escape} -s", False).Split(New String() {VB6.vbCrLf, VB6.vbLf, VB6.vbCr}, StringSplitOptions.RemoveEmptyEntries) For i = 0 To output.Length - 1 Dim line = output(i).Trim() Dim match = Regex.Match(line, "Profile: (\d+)(.*)$") If match.Success Then Dim profile = match.Groups(1).Value Return profile.ToInt() End If Next Return -1 End Function Public Sub WriteEditorConfigFile(offset As Padding, mode As DoviMode, Optional logContent As Boolean = True, Optional overwrite As Boolean = True) If Not Level5JsonFilePath?.FileExists() Then Return If Not overwrite AndAlso EditorConfigFilePath?.FileExists() Then Return 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 = $"{{""mode"":{mode + 0},""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 WriteModifiedRpu(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 ModifiedRpuFilePath.FileExists() Then Return Nothing Try Dim arguments = $"editor -i ""{Path}"" -j ""{EditorConfigFilePath}"" -o ""{ModifiedRpuFilePath}""" 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 = {ModifiedRpuFilePath} 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 ModifiedRpuFilePath End Function Public Function TrimRpu(Optional overwrite As Boolean = True) As String If Not Path?.FileExists() Then Return Nothing If Not HasToBeTrimmed Then Return Nothing If Not overwrite AndAlso TrimmedRpuFilePath.FileExists() Then Return Nothing Dim inputFilePath = If(ModifiedRpuFilePath.FileExists(), ModifiedRpuFilePath, Path) _trimRanges = New List(Of Range)(p.Ranges) Try Dim frames = Edits.Max(Function(x) x.EndFrame) + 1 Dim ranges = String.Join(";", _trimRanges.Select(Function(x) $"{x.Start}-{x.End}")) Dim arguments = $"--input ""{inputFilePath}"" --output ""{TrimmedRpuFilePath}"" --frames {frames} --ranges {ranges}" Using proc As New Proc proc.Package = Package.DoViSplit proc.Project = p proc.Header = "Trimming RPU metadata file" proc.Encoding = Encoding.UTF8 proc.Arguments = arguments proc.AllowedExitCodes = {0} proc.OutputFiles = {TrimmedRpuFilePath} proc.Start() End Using Catch ex As AbortException _trimRanges = Nothing Throw ex Catch ex As Exception _trimRanges = Nothing g.ShowException(ex) Throw New AbortException Finally Log.Save() End Try Return TrimmedRpuFilePath End Function End Class