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]) } } */ }