Enforce display aspect ratio in conversions
This commit is contained in:
parent
e6c015e114
commit
a0bcdd55e2
|
|
@ -156,7 +156,7 @@ func FormatClock(sec float64) string {
|
||||||
func ResolveTargetAspect(val string, src *VideoSource) float64 {
|
func ResolveTargetAspect(val string, src *VideoSource) float64 {
|
||||||
if strings.EqualFold(val, "source") {
|
if strings.EqualFold(val, "source") {
|
||||||
if src != nil {
|
if src != nil {
|
||||||
return utils.AspectRatioFloat(src.Width, src.Height)
|
return utils.DisplayAspectRatioFloat(src.Width, src.Height, src.SampleAspectRatio)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -245,19 +245,20 @@ func AspectFilters(target float64, mode string) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ar := fmt.Sprintf("%.6f", target)
|
ar := fmt.Sprintf("%.6f", target)
|
||||||
|
setDAR := fmt.Sprintf("setdar=%s", ar)
|
||||||
|
|
||||||
// Crop mode: center crop to target aspect ratio
|
// Crop mode: center crop to target aspect ratio
|
||||||
if strings.EqualFold(mode, "Crop") || strings.EqualFold(mode, "Auto") {
|
if strings.EqualFold(mode, "Crop") || strings.EqualFold(mode, "Auto") {
|
||||||
// Crop to target aspect ratio with even dimensions for H.264 encoding
|
// Crop to target aspect ratio with even dimensions for H.264 encoding
|
||||||
// Use trunc/2*2 to ensure even dimensions
|
// Use trunc/2*2 to ensure even dimensions
|
||||||
crop := fmt.Sprintf("crop=w='trunc(if(gt(a,%[1]s),ih*%[1]s,iw)/2)*2':h='trunc(if(gt(a,%[1]s),ih,iw/%[1]s)/2)*2':x='(iw-out_w)/2':y='(ih-out_h)/2'", ar)
|
crop := fmt.Sprintf("crop=w='trunc(if(gt(a,%[1]s),ih*%[1]s,iw)/2)*2':h='trunc(if(gt(a,%[1]s),ih,iw/%[1]s)/2)*2':x='(iw-out_w)/2':y='(ih-out_h)/2'", ar)
|
||||||
return []string{crop, "setsar=1"}
|
return []string{crop, setDAR, "setsar=1"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stretch mode: just change the aspect ratio without cropping or padding
|
// Stretch mode: just change the aspect ratio without cropping or padding
|
||||||
if strings.EqualFold(mode, "Stretch") {
|
if strings.EqualFold(mode, "Stretch") {
|
||||||
scale := fmt.Sprintf("scale=w='trunc(ih*%[1]s/2)*2':h='trunc(iw/%[1]s/2)*2'", ar)
|
scale := fmt.Sprintf("scale=w='trunc(ih*%[1]s/2)*2':h='trunc(iw/%[1]s/2)*2'", ar)
|
||||||
return []string{scale, "setsar=1"}
|
return []string{scale, setDAR, "setsar=1"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blur Fill: create blurred background then overlay original video
|
// Blur Fill: create blurred background then overlay original video
|
||||||
|
|
@ -272,10 +273,10 @@ func AspectFilters(target float64, mode string) []string {
|
||||||
|
|
||||||
// Filter: split[bg][fg]; [bg]scale=outW:outH,boxblur=20:5[blurred]; [blurred][fg]overlay=(W-w)/2:(H-h)/2
|
// Filter: split[bg][fg]; [bg]scale=outW:outH,boxblur=20:5[blurred]; [blurred][fg]overlay=(W-w)/2:(H-h)/2
|
||||||
filterStr := fmt.Sprintf("split[bg][fg];[bg]scale=%s:%s:force_original_aspect_ratio=increase,boxblur=20:5[blurred];[blurred][fg]overlay=(W-w)/2:(H-h)/2", outW, outH)
|
filterStr := fmt.Sprintf("split[bg][fg];[bg]scale=%s:%s:force_original_aspect_ratio=increase,boxblur=20:5[blurred];[blurred][fg]overlay=(W-w)/2:(H-h)/2", outW, outH)
|
||||||
return []string{filterStr, "setsar=1"}
|
return []string{filterStr, setDAR, "setsar=1"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Letterbox/Pillarbox: keep source resolution, just pad to target aspect with black bars
|
// Letterbox/Pillarbox: keep source resolution, just pad to target aspect with black bars
|
||||||
pad := fmt.Sprintf("pad=w='trunc(max(iw,ih*%[1]s)/2)*2':h='trunc(max(ih,iw/%[1]s)/2)*2':x='(ow-iw)/2':y='(oh-ih)/2':color=black", ar)
|
pad := fmt.Sprintf("pad=w='trunc(max(iw,ih*%[1]s)/2)*2':h='trunc(max(ih,iw/%[1]s)/2)*2':x='(ow-iw)/2':y='(oh-ih)/2':color=black", ar)
|
||||||
return []string{pad, "setsar=1"}
|
return []string{pad, setDAR, "setsar=1"}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,19 @@ func AspectRatioFloat(w, h int) float64 {
|
||||||
return float64(w) / float64(h)
|
return float64(w) / float64(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisplayAspectRatioFloat calculates display aspect ratio using SAR when available.
|
||||||
|
func DisplayAspectRatioFloat(w, h int, sar string) float64 {
|
||||||
|
base := AspectRatioFloat(w, h)
|
||||||
|
if base <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
sarVal := ParseAspectValue(strings.TrimSpace(sar))
|
||||||
|
if sarVal <= 0 {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return base * sarVal
|
||||||
|
}
|
||||||
|
|
||||||
// ParseAspectValue parses an aspect ratio string like "16:9"
|
// ParseAspectValue parses an aspect ratio string like "16:9"
|
||||||
func ParseAspectValue(val string) float64 {
|
func ParseAspectValue(val string) float64 {
|
||||||
val = strings.TrimSpace(val)
|
val = strings.TrimSpace(val)
|
||||||
|
|
|
||||||
75
main.go
75
main.go
|
|
@ -197,7 +197,7 @@ func (l *fixedHSplitLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
||||||
func resolveTargetAspect(val string, src *videoSource) float64 {
|
func resolveTargetAspect(val string, src *videoSource) float64 {
|
||||||
if strings.EqualFold(val, "source") {
|
if strings.EqualFold(val, "source") {
|
||||||
if src != nil {
|
if src != nil {
|
||||||
return utils.AspectRatioFloat(src.Width, src.Height)
|
return utils.DisplayAspectRatioFloat(src.Width, src.Height, src.SampleAspectRatio)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -7955,7 +7955,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
target := resolveTargetAspect(state.convert.OutputAspect, src)
|
target := resolveTargetAspect(state.convert.OutputAspect, src)
|
||||||
srcAspect := utils.AspectRatioFloat(src.Width, src.Height)
|
srcAspect := utils.DisplayAspectRatioFloat(src.Width, src.Height, src.SampleAspectRatio)
|
||||||
if target == 0 || srcAspect == 0 || utils.RatiosApproxEqual(target, srcAspect, 0.01) {
|
if target == 0 || srcAspect == 0 || utils.RatiosApproxEqual(target, srcAspect, 0.01) {
|
||||||
aspectBox.Hide()
|
aspectBox.Hide()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -9187,7 +9187,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
// If aspect still unset, derive from source
|
// If aspect still unset, derive from source
|
||||||
if targetAR == "" || strings.EqualFold(targetAR, "Source") {
|
if targetAR == "" || strings.EqualFold(targetAR, "Source") {
|
||||||
if src != nil {
|
if src != nil {
|
||||||
if ar := utils.AspectRatioFloat(src.Width, src.Height); ar > 0 && ar < 1.6 {
|
if ar := utils.DisplayAspectRatioFloat(src.Width, src.Height, src.SampleAspectRatio); ar > 0 && ar < 1.6 {
|
||||||
targetAR = "4:3"
|
targetAR = "4:3"
|
||||||
} else {
|
} else {
|
||||||
targetAR = "16:9"
|
targetAR = "16:9"
|
||||||
|
|
@ -10985,12 +10985,12 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
||||||
fullBtn := utils.MakeIconButton("⛶", "Toggle fullscreen", func() {
|
fullBtn := utils.MakeIconButton("⛶", "Toggle fullscreen", func() {
|
||||||
// Placeholder: embed fullscreen toggle into playback surface later.
|
// Placeholder: embed fullscreen toggle into playback surface later.
|
||||||
})
|
})
|
||||||
volBox := container.NewHBox(volIcon, container.NewMax(volSlider))
|
volBox := container.NewHBox(volIcon, container.NewMax(volSlider))
|
||||||
progress := container.NewBorder(nil, nil, currentTime, totalTime, container.NewMax(slider))
|
progress := container.NewBorder(nil, nil, currentTime, totalTime, container.NewMax(slider))
|
||||||
controls = container.NewVBox(
|
controls = container.NewVBox(
|
||||||
container.NewHBox(prevFrameBtn, playBtn, nextFrameBtn, fullBtn, coverBtn, saveFrameBtn, importBtn, layout.NewSpacer(), frameLabel, volBox),
|
container.NewHBox(prevFrameBtn, playBtn, nextFrameBtn, fullBtn, coverBtn, saveFrameBtn, importBtn, layout.NewSpacer(), frameLabel, volBox),
|
||||||
progress,
|
progress,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
slider := widget.NewSlider(0, math.Max(1, float64(len(src.PreviewFrames)-1)))
|
slider := widget.NewSlider(0, math.Max(1, float64(len(src.PreviewFrames)-1)))
|
||||||
slider.Step = 1
|
slider.Step = 1
|
||||||
|
|
@ -13048,7 +13048,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
|
|
||||||
// Aspect ratio conversion (only if user explicitly changed from Source)
|
// Aspect ratio conversion (only if user explicitly changed from Source)
|
||||||
if cfg.OutputAspect != "" && !strings.EqualFold(cfg.OutputAspect, "source") {
|
if cfg.OutputAspect != "" && !strings.EqualFold(cfg.OutputAspect, "source") {
|
||||||
srcAspect := utils.AspectRatioFloat(src.Width, src.Height)
|
srcAspect := utils.DisplayAspectRatioFloat(src.Width, src.Height, src.SampleAspectRatio)
|
||||||
targetAspect := resolveTargetAspect(cfg.OutputAspect, src)
|
targetAspect := resolveTargetAspect(cfg.OutputAspect, src)
|
||||||
if targetAspect > 0 && srcAspect > 0 && !utils.RatiosApproxEqual(targetAspect, srcAspect, 0.01) {
|
if targetAspect > 0 && srcAspect > 0 && !utils.RatiosApproxEqual(targetAspect, srcAspect, 0.01) {
|
||||||
vf = append(vf, aspectFilters(targetAspect, cfg.AspectHandling)...)
|
vf = append(vf, aspectFilters(targetAspect, cfg.AspectHandling)...)
|
||||||
|
|
@ -13056,6 +13056,11 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetAspect := resolveTargetAspect(cfg.OutputAspect, src)
|
||||||
|
if targetAspect > 0 && len(vf) > 0 {
|
||||||
|
vf = appendAspectMetadata(vf, targetAspect)
|
||||||
|
}
|
||||||
|
|
||||||
// Flip horizontal
|
// Flip horizontal
|
||||||
if cfg.FlipHorizontal {
|
if cfg.FlipHorizontal {
|
||||||
vf = append(vf, "hflip")
|
vf = append(vf, "hflip")
|
||||||
|
|
@ -13633,19 +13638,20 @@ func aspectFilters(target float64, mode string) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ar := fmt.Sprintf("%.6f", target)
|
ar := fmt.Sprintf("%.6f", target)
|
||||||
|
setDAR := fmt.Sprintf("setdar=%s", ar)
|
||||||
|
|
||||||
// Crop mode: center crop to target aspect ratio
|
// Crop mode: center crop to target aspect ratio
|
||||||
if strings.EqualFold(mode, "Crop") || strings.EqualFold(mode, "Auto") {
|
if strings.EqualFold(mode, "Crop") || strings.EqualFold(mode, "Auto") {
|
||||||
// Crop to target aspect ratio with even dimensions for H.264 encoding
|
// Crop to target aspect ratio with even dimensions for H.264 encoding
|
||||||
// Use trunc/2*2 to ensure even dimensions
|
// Use trunc/2*2 to ensure even dimensions
|
||||||
crop := fmt.Sprintf("crop=w='trunc(if(gt(a,%[1]s),ih*%[1]s,iw)/2)*2':h='trunc(if(gt(a,%[1]s),ih,iw/%[1]s)/2)*2':x='(iw-out_w)/2':y='(ih-out_h)/2'", ar)
|
crop := fmt.Sprintf("crop=w='trunc(if(gt(a,%[1]s),ih*%[1]s,iw)/2)*2':h='trunc(if(gt(a,%[1]s),ih,iw/%[1]s)/2)*2':x='(iw-out_w)/2':y='(ih-out_h)/2'", ar)
|
||||||
return []string{crop, "setsar=1"}
|
return []string{crop, setDAR, "setsar=1"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stretch mode: just change the aspect ratio without cropping or padding
|
// Stretch mode: just change the aspect ratio without cropping or padding
|
||||||
if strings.EqualFold(mode, "Stretch") {
|
if strings.EqualFold(mode, "Stretch") {
|
||||||
scale := fmt.Sprintf("scale=w='trunc(ih*%[1]s/2)*2':h='trunc(iw/%[1]s/2)*2'", ar)
|
scale := fmt.Sprintf("scale=w='trunc(ih*%[1]s/2)*2':h='trunc(iw/%[1]s/2)*2'", ar)
|
||||||
return []string{scale, "setsar=1"}
|
return []string{scale, setDAR, "setsar=1"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blur Fill: create blurred background then overlay original video
|
// Blur Fill: create blurred background then overlay original video
|
||||||
|
|
@ -13660,19 +13666,41 @@ func aspectFilters(target float64, mode string) []string {
|
||||||
|
|
||||||
// Filter: split[bg][fg]; [bg]scale=outW:outH,boxblur=20:5[blurred]; [blurred][fg]overlay=(W-w)/2:(H-h)/2
|
// Filter: split[bg][fg]; [bg]scale=outW:outH,boxblur=20:5[blurred]; [blurred][fg]overlay=(W-w)/2:(H-h)/2
|
||||||
filterStr := fmt.Sprintf("split[bg][fg];[bg]scale=%s:%s:force_original_aspect_ratio=increase,boxblur=20:5[blurred];[blurred][fg]overlay=(W-w)/2:(H-h)/2", outW, outH)
|
filterStr := fmt.Sprintf("split[bg][fg];[bg]scale=%s:%s:force_original_aspect_ratio=increase,boxblur=20:5[blurred];[blurred][fg]overlay=(W-w)/2:(H-h)/2", outW, outH)
|
||||||
return []string{filterStr, "setsar=1"}
|
return []string{filterStr, setDAR, "setsar=1"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Letterbox/Pillarbox: pad with black bars (auto-detects direction based on aspect ratio change)
|
// Letterbox/Pillarbox: pad with black bars (auto-detects direction based on aspect ratio change)
|
||||||
// Also handles legacy "Letterbox" and "Pillarbox" options for backwards compatibility
|
// Also handles legacy "Letterbox" and "Pillarbox" options for backwards compatibility
|
||||||
if strings.EqualFold(mode, "Letterbox/Pillarbox") || strings.EqualFold(mode, "Letterbox") || strings.EqualFold(mode, "Pillarbox") {
|
if strings.EqualFold(mode, "Letterbox/Pillarbox") || strings.EqualFold(mode, "Letterbox") || strings.EqualFold(mode, "Pillarbox") {
|
||||||
pad := fmt.Sprintf("pad=w='trunc(max(iw,ih*%[1]s)/2)*2':h='trunc(max(ih,iw/%[1]s)/2)*2':x='(ow-iw)/2':y='(oh-ih)/2':color=black", ar)
|
pad := fmt.Sprintf("pad=w='trunc(max(iw,ih*%[1]s)/2)*2':h='trunc(max(ih,iw/%[1]s)/2)*2':x='(ow-iw)/2':y='(oh-ih)/2':color=black", ar)
|
||||||
return []string{pad, "setsar=1"}
|
return []string{pad, setDAR, "setsar=1"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default fallback: same as Letterbox/Pillarbox
|
// Default fallback: same as Letterbox/Pillarbox
|
||||||
pad := fmt.Sprintf("pad=w='trunc(max(iw,ih*%[1]s)/2)*2':h='trunc(max(ih,iw/%[1]s)/2)*2':x='(ow-iw)/2':y='(oh-ih)/2':color=black", ar)
|
pad := fmt.Sprintf("pad=w='trunc(max(iw,ih*%[1]s)/2)*2':h='trunc(max(ih,iw/%[1]s)/2)*2':x='(ow-iw)/2':y='(oh-ih)/2':color=black", ar)
|
||||||
return []string{pad, "setsar=1"}
|
return []string{pad, setDAR, "setsar=1"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendAspectMetadata(vf []string, dar float64) []string {
|
||||||
|
if dar <= 0 {
|
||||||
|
return vf
|
||||||
|
}
|
||||||
|
hasSetDAR := false
|
||||||
|
hasSetSAR := false
|
||||||
|
for _, f := range vf {
|
||||||
|
if strings.HasPrefix(f, "setdar=") {
|
||||||
|
hasSetDAR = true
|
||||||
|
} else if strings.HasPrefix(f, "setsar=") {
|
||||||
|
hasSetSAR = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasSetDAR {
|
||||||
|
vf = append(vf, fmt.Sprintf("setdar=%.6f", dar))
|
||||||
|
}
|
||||||
|
if !hasSetSAR {
|
||||||
|
vf = append(vf, "setsar=1")
|
||||||
|
}
|
||||||
|
return vf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) generateSnippet() {
|
func (s *appState) generateSnippet() {
|
||||||
|
|
@ -13735,13 +13763,16 @@ func (s *appState) generateSnippet() {
|
||||||
// Check if aspect ratio conversion is needed (only if user explicitly set OutputAspect)
|
// Check if aspect ratio conversion is needed (only if user explicitly set OutputAspect)
|
||||||
aspectExplicit := s.convert.OutputAspect != "" && !strings.EqualFold(s.convert.OutputAspect, "Source")
|
aspectExplicit := s.convert.OutputAspect != "" && !strings.EqualFold(s.convert.OutputAspect, "Source")
|
||||||
if aspectExplicit {
|
if aspectExplicit {
|
||||||
srcAspect := utils.AspectRatioFloat(src.Width, src.Height)
|
srcAspect := utils.DisplayAspectRatioFloat(src.Width, src.Height, src.SampleAspectRatio)
|
||||||
targetAspect := resolveTargetAspect(s.convert.OutputAspect, src)
|
targetAspect := resolveTargetAspect(s.convert.OutputAspect, src)
|
||||||
aspectConversionNeeded := targetAspect > 0 && srcAspect > 0 && !utils.RatiosApproxEqual(targetAspect, srcAspect, 0.01)
|
aspectConversionNeeded := targetAspect > 0 && srcAspect > 0 && !utils.RatiosApproxEqual(targetAspect, srcAspect, 0.01)
|
||||||
if aspectConversionNeeded {
|
if aspectConversionNeeded {
|
||||||
vf = append(vf, aspectFilters(targetAspect, s.convert.AspectHandling)...)
|
vf = append(vf, aspectFilters(targetAspect, s.convert.AspectHandling)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if targetAspect := resolveTargetAspect(s.convert.OutputAspect, src); targetAspect > 0 && len(vf) > 0 {
|
||||||
|
vf = appendAspectMetadata(vf, targetAspect)
|
||||||
|
}
|
||||||
|
|
||||||
// Frame rate conversion (only if explicitly set and different from source)
|
// Frame rate conversion (only if explicitly set and different from source)
|
||||||
if s.convert.FrameRate != "" && s.convert.FrameRate != "Source" {
|
if s.convert.FrameRate != "" && s.convert.FrameRate != "Source" {
|
||||||
|
|
@ -15130,12 +15161,22 @@ func buildPlayerView(state *appState) fyne.CanvasObject {
|
||||||
})
|
})
|
||||||
loadBtn.Importance = widget.HighImportance
|
loadBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
// Clear video button
|
||||||
|
clearBtn := widget.NewButton("Clear Video", func() {
|
||||||
|
state.playerFile = nil
|
||||||
|
state.showPlayerView()
|
||||||
|
})
|
||||||
|
clearBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
// Button container
|
||||||
|
buttonContainer := container.NewHBox(loadBtn, clearBtn)
|
||||||
|
|
||||||
// Main content
|
// Main content
|
||||||
mainContent := container.NewVBox(
|
mainContent := container.NewVBox(
|
||||||
instructions,
|
instructions,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
fileLabel,
|
fileLabel,
|
||||||
loadBtn,
|
buttonContainer,
|
||||||
videoContainer,
|
videoContainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user