Files
brent saner 8e70ffac38 v1.16.3
ADDED:
* envs.MustEnv(), envs.MustEnvNoBlank()

UPDATED:
* dep mods, Go version
2026-07-09 19:32:21 -04:00

653 lines
13 KiB
Go

package main
import (
"fmt"
`math`
`slices`
"strconv"
`strings`
mapset `github.com/deckarep/golang-set/v2`
)
type (
AspectRatio struct {
ar float64
arStr string
arW float64
arH float64
gcd int
res *Resolution
}
Resolution struct {
w int
h int
ar *AspectRatio
}
)
func (a *AspectRatio) AsFloat() (ar float64) {
if a == nil {
return
}
if a.ar == 0 {
a.ar = float64(a.res.w) / float64(a.res.h)
}
ar = a.ar
return
}
func (a *AspectRatio) AsSlice() (ar [2]float64) {
if a.arW == 0 {
a.arW = float64(a.res.w) / float64(a.GCD())
}
if a.arH == 0 {
a.arH = float64(a.res.h) / float64(a.GCD())
}
ar = [2]float64{
a.arW,
a.arH,
}
return
}
/*
Cmp is a comparison function that can be used for sorting.
The return cmp value has the same meaning as in [slices.SortFunc]:
If a comes before other, cmp == -1
If a comes after other, cmp == 1
If a and other are equal, cmp == 0
*/
func (a *AspectRatio) Cmp(other AspectRatio) (cmp int) {
var aSlice [2]float64
var oSlice [2]float64
if a == nil {
return
}
aSlice = a.AsSlice()
oSlice = other.AsSlice()
if aSlice[0] == oSlice[0] && aSlice[1] == oSlice[1] {
return
}
if aSlice[0] == oSlice[0] {
if aSlice[1] < oSlice[1] {
cmp = -1
} else {
cmp = 1
}
} else {
if aSlice[0] < oSlice[0] {
cmp = -1
} else {
cmp = 1
}
}
return
}
func (a *AspectRatio) Equals(other AspectRatio) (isEqual bool) {
isEqual = a.Cmp(other) == 0
return
}
func (a *AspectRatio) FloatString() (s string) {
if a == nil {
return
}
if a.ar == 0 {
a.ar = float64(a.res.w) / float64(a.res.h)
}
s = strconv.FormatFloat(a.ar, 'f', -1, 64)
return
}
func (a *AspectRatio) GCD() (gcd int) {
var x int
var y int
if a == nil {
return
}
if a.gcd == 0 {
x = a.res.w
y = a.res.h
// Euclidean GCD ("Greatest Common Divisor") algorithm
for y != 0 {
x, y = y, x%y
}
a.gcd = x
}
gcd = a.gcd
return
}
func (a *AspectRatio) Resolution() (r Resolution) {
var ar [2]float64
var resInts [2]int
if a == nil {
return
}
resInts = a.res.AsSlice()
ar = a.AsSlice()
// Return a fully populated copy.
r = Resolution{
w: resInts[0],
h: resInts[1],
}
r.ar = &AspectRatio{
ar: a.AsFloat(),
arStr: a.String(),
arW: ar[0],
arH: ar[1],
gcd: a.GCD(),
res: &r,
}
return
}
func (a *AspectRatio) String() (s string) {
if a == nil {
return
}
if a.arStr == "" {
if a.arW == 0 {
a.arW = float64(a.res.w) / float64(a.GCD())
}
if a.arH == 0 {
a.arH = float64(a.res.h) / float64(a.GCD())
}
a.arStr = fmt.Sprintf(
"%d:%d",
int(math.Round(a.arW)),
int(math.Round(a.arH)),
)
}
s = a.arStr
return
}
func (r *Resolution) AsSlice() (res [2]int) {
res = [2]int{r.w, r.h}
return
}
func (r *Resolution) AspectRatio() (ar AspectRatio) {
if r == nil {
return
}
if r.ar == nil {
r.ar = NewAspectRatio("", r.w, r.h)
}
ar = AspectRatio{
ar: r.ar.AsFloat(),
arStr: r.ar.String(),
arW: r.ar.arW,
arH: r.ar.arH,
gcd: r.ar.GCD(),
res: &Resolution{
w: r.w,
h: r.h,
},
}
ar.res.ar = &ar
return
}
/*
Cmp is a comparison function that can be used for sorting.
The return cmp value has the same meaning as in [slices.SortFunc]:
If r comes before other, cmp == -1
If r comes after other, cmp == 1
If r and other are equal, cmp == 0
*/
func (r *Resolution) Cmp(other Resolution) (cmp int) {
if r == nil {
return
}
if r.w == other.w && r.h == other.h {
return
}
if r.w == other.w {
if r.h < other.h {
cmp = -1
} else {
cmp = 1
}
} else {
if r.w < other.w {
cmp = -1
} else {
cmp = 1
}
}
return
}
func (r *Resolution) Equals(other Resolution) (isEqual bool) {
isEqual = r.Cmp(other) == 0
return
}
func (r *Resolution) Inverted() (i *Resolution) {
i = &Resolution{
w: r.h,
h: r.w,
}
i.ar = &AspectRatio{
res: i,
}
return
}
func (r *Resolution) String() (s string) {
s = fmt.Sprintf("%dx%d", r.w, r.h)
return
}
var (
resolutions [][2]int = [][2]int{
/*
Common
*/
// qqVGA ("Quarter-QVGA")
[2]int{120, 160}, // 3:4
[2]int{160, 120}, // 4:3, Standard qqVGA
[2]int{160, 128}, // 5:4
[2]int{320, 240}, // ???, qVGA ("Quarter VGA") (commonly inverted as 240x320 on mobile)
// HQVGA ("Half-qVGA")
[2]int{160, 240}, // ???
[2]int{240, 160}, // ???, Standard HQVGA
[2]int{240, 376}, // 3:2, WQVGA ("Wide qVGA")
[2]int{240, 432}, // 3:2, WQVGA
[2]int{360, 240}, // 3:2, WQVGA
[2]int{384, 240}, // 16:10 (8:5), WQVGA
[2]int{400, 240}, // 5:3, WQVGA
[2]int{426, 240}, // ???, WQVGA
[2]int{428, 240}, // ???, WQVGA
[2]int{432, 240}, // 18:10, WQVGA
[2]int{480, 240}, // ???, WQVGA
[2]int{480, 270}, // ???, WQVGA / HVGA ("Half-Size VGA")
[2]int{480, 272}, // ???, WQXGA / HVGA
[2]int{480, 320}, // 3:2, Standard HVGA
[2]int{480, 360}, // 4:3, HVGA
[2]int{640, 240}, // 8:3, HVGA
[2]int{640, 480}, // 4:3, VGA ("Video Graphics Array")
[2]int{640, 384}, // 15:9 (5:3), WVGA (WGA) ("Wide VGA")
[2]int{720, 480}, // 15:10 (3:2), WVGA
[2]int{768, 480}, // 16:10 (8:5), WVGA
[2]int{800, 450}, // 16:9, WVGA
[2]int{800, 480}, // 15:9 (5:3), Standard WVGA
[2]int{848, 480}, // ???, Standard WVGA
[2]int{852, 480}, // ???, Standard WVGA
[2]int{853, 480}, // ???, WVGA
[2]int{854, 480}, // ???, Full WVGA
[2]int{640, 400}, // ???, SVGA ("Super VGA") / UVGA ("Ultra VGA") / XVGA ("Extended VGA")
[2]int{640, 480}, // ???, SVGA / UVGA / XVGA
[2]int{800, 600}, // ???, Standard SVGA / UVGA
[2]int{830, 624}, // 4:3, SVGA / UVGA
[2]int{960, 640}, // 3:2, DVGA ("Double VGA") ("Retina")
[2]int{1136, 640}, // 16:9, DVGA (iPhone 5 variant)
[2]int{1024, 576}, // 16:9, WSVGA ("Wide SVGA")
[2]int{1024, 600}, // 128:75, WSVGA
[2]int{1024, 768}, // 4:3, XGA ("Extended Graphics Array") / SVGA
[2]int{1120, 832}, // 35:26, XGA+ ("Extended Graphics Array Plus")
[2]int{1152, 864}, // 4:3, Standard XGA+
[2]int{1152, 870}, // 192:145, XGA+
[2]int{1152, 900}, // 32:25, XGA+
[2]int{1152, 768}, // 16:9, WXGA ("Wide XGA")
[2]int{1280, 720}, // 16:9, WXGA
[2]int{1280, 768}, // 15:9, Standard WXGA
[2]int{1280, 800}, // 16:10, Standard WXGA
[2]int{1280, 854}, // ???, WXGA+ ("Wide XGA Plus")
[2]int{1360, 768}, // ???, FWXGA ("Full WXGA")
[2]int{1366, 768}, // ???, Standard WXGA / FWXGA
[2]int{1280, 960}, // 4:3, QuadVGA / SXVGA ("Super XVGA") / SXGA-
[2]int{1280, 1024}, // 5:4, SXGA ("Super XGA") / SVGA
[2]int{1360, 768}, // ???, FWXGA
[2]int{1366, 768}, // ???, WXGA
[2]int{1400, 1050}, // 4:3, SXGA+ ("Super XGA Plus")
[2]int{1440, 900}, // 16:10, WXGA+ / WSXGA ("Wide SXGA")
[2]int{1440, 960}, // 3:2, WSXGA
[2]int{1600, 1024}, // 25:16, WSXGA
[2]int{1600, 1200}, // 4:3, UXGA ("Ultra-XGA")
[2]int{1680, 1050}, // 16:10, WSXGA+
[2]int{1920, 1200}, // 16:10, WUXGA ("Widescreen UXGA")
[2]int{2048, 1152}, // 16:9, QWXGA ("Quad-WXGA")
[2]int{2048, 1536}, // 4:3, QXGA ("Quad-XGA")
[2]int{2560, 1600}, // 16:10, WQXGA ("Wide QXGA")
[2]int{2560, 2048}, // 5:4, QSXGA ("Quad-SXGA")
[2]int{2800, 2100}, // 4:3, QSXGA+ ("Quad-SXGA Plus")
[2]int{2880, 1800}, // 16:10, WQXGA+ ("Wide QXGA Plus")
[2]int{3200, 2048}, // 25:16, WQSXGA ("Wide QSXGA")
[2]int{3200, 2400}, // 4:3, QUXGA ("Quad Ultra-XGA")
[2]int{3840, 2400}, // 16:10, WQUXGA ("Wide QUXGA")
[2]int{1136, 640}, // 16:9, Apple variant
[2]int{1334, 750}, // 16:9, Apple variant
[2]int{1792, 828}, // ???, Apple variant
[2]int{2436, 1125}, // ???, Apple variant
[2]int{2532, 1170}, // ???, Apple variant
[2]int{2556, 1179}, // ???, Apple variant
[2]int{2688, 1242}, // ???, Apple variant
[2]int{2778, 1285}, // ???, Apple variant
[2]int{2796, 1290}, // ???, Apple variant
[2]int{2160, 1080}, // 2:1, Android variant
[2]int{2220, 1080}, // ???, Android variant
[2]int{2280, 1080}, // 19:9, Android variant
[2]int{2400, 1080}, // 20:9, Android variant
[2]int{2960, 1440}, // ???, Android variant
[2]int{3040, 1440}, // 19:9, Android variant
[2]int{3120, 1440}, // ???, Android variant
[2]int{2340, 1080}, // ???, Android/Apple variant
// Assorted rare res
[2]int{576, 720}, // 4:5
[2]int{900, 720}, // 5:4
[2]int{960, 720}, // 4:3
[2]int{1200, 720}, // 5:3
[2]int{1280, 720}, // 16:9
[2]int{1280, 524}, // ???
[2]int{1280, 532}, // ???
[2]int{1280, 536}, // ???
[2]int{1280, 540}, // ???
[2]int{1280, 544}, // ???
[2]int{1280, 640}, // ???
[2]int{1280, 674}, // ???
[2]int{1280, 692}, // ???
// 1:1 - Square
[2]int{1080, 1080}, // Square Standard
// 9:16 - Vertical
[2]int{1080, 1920}, // Standard Mobile
// 16:9 - Standard widescreen
[2]int{1920, 1080}, // 1080p ("Full HD"/"FHD")
[2]int{2560, 1440}, // 1440p ("Quad HD"/ "QHD")
[2]int{3840, 2160}, // 2160p ("4k Ultra-HD"/"4k UHD")
[2]int{7680, 4320}, // 4320p ("8k Ultra-HD"/"8k UHD")
// 16:10 - "Computer monitor" widescreen
[2]int{1920, 1200}, // WUXGA ("Widescreen Ultra Extended Graphics Array"), https://www.corsair.com/us/en/explorer/gamer/monitors/wuxga-resolution-explained/
[2]int{2560, 1600}, // WQXGA ("Wide Quad Extended Graphics Array"), https://www.lenovo.com/us/en/glossary/wqxga/
// 21:9 - Ultrawide
[2]int{2560, 1080}, // Ultrawide FHD
[2]int{3440, 1440}, // Ultrawide QHD
// 32:9 - Super Ultrawide
[2]int{5120, 1440},
/*
Cinema
*/
// 37:20 - Flat
[2]int{1998, 1080}, // 2k Flat DCP ("Digital Cinema Package")
[2]int{3996, 2160}, // 4k Flat DCP
// 1024:429 - Scope
[2]int{2048, 858}, // 2k Scope DCP
[2]int{4096, 1716}, // 4k Scope DCP
// 256:135 - Full Container
[2]int{2048, 1080}, // 2k Full Container DCP
[2]int{4096, 2160}, // 4k Full Container DCP
}
)
/*
NewAspectRatio returns a new Aspect Ratio from string form ar.
At least one of width or height must be > 0;
the other can be calculated from ar.
If both height and width are > 0, ar is ignored and it will be calculated from those instead.
If both height and width are <= 0, a will be nil.
If ar is an invalid format, a will be nil.
*/
func NewAspectRatio(ar string, width, height int) (a *AspectRatio) {
var err error
var arW float64
var arH float64
var arStr []string
if width <= 0 && height <= 0 {
return
} else if width > 0 && height > 0 {
// ar is ignored.
a = &AspectRatio{
ar: float64(width) / float64(height),
res: &Resolution{
w: width,
h: height,
},
}
a.res.ar = a
} else {
arStr = strings.SplitN(strings.TrimSpace(ar), ":", 2)
if len(arStr) != 2 {
return
}
if arH, err = strconv.ParseFloat(strings.TrimSpace(arStr[0]), 64); err != nil {
return
}
if arW, err = strconv.ParseFloat(strings.TrimSpace(arStr[1]), 64); err != nil {
return
}
a = &AspectRatio{
ar: arW / arH,
arW: arW,
arH: arH,
}
a.res.ar = a
if width > 0 {
a.res.w = width
a.res.h = int(math.Round(float64(a.res.w) / a.ar))
} else {
a.res.h = height
a.res.w = int(math.Round(float64(a.res.h) * a.ar))
}
}
return
}
func NewResolution(width, height int) (r Resolution) {
r = Resolution{
w: width,
h: height,
}
r.ar = &AspectRatio{
res: &r,
}
return
}
func NewResolutions(resGeom [][2]int) (rsltns []Resolution) {
var idx int
rsltns = make([]Resolution, len(resGeom))
for idx = range resGeom {
rsltns[idx] = NewResolution(resGeom[idx][0], resGeom[idx][1])
}
return
}
func SortAspectRatios(ar []AspectRatio) (sorted []AspectRatio) {
slices.SortStableFunc(
ar,
func(a, b AspectRatio) (cmp int) {
cmp = a.Cmp(b)
return
},
)
return
}
func SortResolutions(res []Resolution) {
slices.SortStableFunc(
res,
func(a, b Resolution) (cmp int) {
cmp = a.Cmp(b)
return
},
)
return
}
func main() {
var ok bool
var idx int
var arStr string
// var ars []string
var ar AspectRatio
var res Resolution
var rsltns []Resolution
var uniqAr mapset.Set[AspectRatio] = mapset.NewSet[AspectRatio]()
var uniqRes mapset.Set[[2]int] = mapset.NewSet[[2]int](resolutions...)
var arMap map[string]mapset.Set[Resolution] = make(map[string]mapset.Set[Resolution])
rsltns = NewResolutions(uniqRes.ToSlice())
SortResolutions(rsltns)
for idx, res = range rsltns {
uniqAr.Add(res.AspectRatio())
ar = res.AspectRatio()
arStr = ar.String()
if _, ok = arMap[arStr]; !ok {
arMap[arStr] = mapset.NewSet[Resolution]()
}
arMap[arStr].Add(res)
fmt.Printf("#%d: %s (%s)\n", idx, res.String(), ar.FloatString())
}
fmt.Println("\n====\n")
/*
for _, aspRatStr = range aspRatKeys {
fmt.Println(aspRatStr)
slices.SortStableFunc(
aspRatRes[aspRatStr],
func(resA, resB [2]int) (cmp int) {
// default cmp is 0, which means fully equal
if resA[0] == resB[0] {
if resA[1] > resB[1] {
cmp = 1
} else {
cmp = -1
}
} else {
if resA[0] > resB[0] {
cmp = 1
} else {
cmp = -1
}
}
return
},
)
for _, res = range aspRatRes[aspRatStr] {
fmt.Printf("\t%dx%d\n", res[0], res[1])
}
}
*/
}