A tool for archiving & converting scans of postcards, and information about them.
1package types
2
3import (
4 "fmt"
5 "math"
6 "math/big"
7)
8
9const maxRatioDiff = 0.01
10
11type Size struct {
12 CmWidth *big.Rat `json:"cmW,omitempty"`
13 CmHeight *big.Rat `json:"cmH,omitempty"`
14 PxWidth int `json:"pxW"`
15 PxHeight int `json:"pxH"`
16}
17
18// The fallback surface area of a postcard, if no physical size is provided, but one is needed
19var assumedSurfaceArea = 10.5 * 14.8
20
21// Returns the actual physical dimensions (in cm), or guesses at a physical size
22// based on the assumption that most postcards are 6x4 inches.
23func (s Size) MustPhysical() (float64, float64) {
24 if s.HasPhysical() {
25 w, _ := s.CmWidth.Float64()
26 h, _ := s.CmHeight.Float64()
27 return w, h
28 }
29
30 pxA := float64(s.PxWidth * s.PxHeight)
31 ar := float64(s.PxWidth) / float64(s.PxHeight)
32 res := assumedSurfaceArea / float64(pxA)
33
34 cmH := math.Sqrt(res * pxA / ar)
35 cmW := ar * cmH
36
37 return cmW, cmH
38}
39
40func (s Size) HasPhysical() bool {
41 return s.CmWidth != nil && s.CmHeight != nil
42}
43
44// SetResolution is a helper function for setting the physical dimensions using a Dots Per Centimetre resolution
45func (s *Size) SetResolution(xRes *big.Rat, yRes *big.Rat) {
46 if xRes == nil || yRes == nil {
47 return
48 }
49
50 s.CmWidth = (&big.Rat{}).Quo(
51 big.NewRat(int64(s.PxWidth), 1),
52 xRes,
53 )
54 s.CmHeight = (&big.Rat{}).Quo(
55 big.NewRat(int64(s.PxHeight), 1),
56 yRes,
57 )
58}
59
60// Resolution returns the pixels per centimetre
61func (s Size) Resolution() (xRes *big.Rat, yRes *big.Rat) {
62 xRes = (&big.Rat{}).Quo(big.NewRat(int64(s.PxWidth), 1), s.CmWidth)
63 yRes = (&big.Rat{}).Quo(big.NewRat(int64(s.PxHeight), 1), s.CmHeight)
64 return
65}
66
67type Orientation string
68
69const (
70 OrientationLandscape Orientation = "landscape"
71 OrientationPortrait Orientation = "portrait"
72 OrientationSquare Orientation = "square"
73)
74
75// Returns the most probable orientation of the size. Within 5mm/5px of square is considered square
76func (s Size) Orientation() Orientation {
77 if s.HasPhysical() {
78 diff, _ := new(big.Rat).Sub(s.CmWidth, s.CmHeight).Float64()
79 if math.Abs(diff) <= 0.5 {
80 return OrientationSquare
81 }
82 } else {
83 diff := math.Abs(float64(s.PxWidth - s.PxHeight))
84 if math.Abs(diff) <= 5 {
85 return OrientationSquare
86 }
87 }
88
89 if s.PxWidth > s.PxHeight {
90 return OrientationLandscape
91 }
92 return OrientationPortrait
93}
94
95// SimilarPhysical compares two physical sizes and returns true if their dimensions are within ~1% of each other
96func (s Size) SimilarPhysical(other Size, flip Flip) bool {
97 if !s.HasPhysical() || !other.HasPhysical() {
98 return true
99 }
100
101 if flip.IsHeteroriented() {
102 return similar(s.CmWidth, other.CmHeight) && similar(s.CmHeight, other.CmWidth)
103 } else {
104 return similar(s.CmWidth, other.CmWidth) && similar(s.CmHeight, other.CmHeight)
105 }
106}
107
108func similar(a, b *big.Rat) bool {
109 ratio, _ := big.NewRat(1, 1).Quo(a, b).Float64()
110 return math.Abs(1-ratio) <= maxRatioDiff
111}
112
113func (s Size) String() string {
114 pxSize := fmt.Sprintf("%dpx x %dpx", s.PxWidth, s.PxHeight)
115 if !s.HasPhysical() {
116 return pxSize
117 }
118
119 fw, _ := s.CmWidth.Float64()
120 fh, _ := s.CmHeight.Float64()
121
122 return fmt.Sprintf(
123 "%.1fcm x %.1fcm (%dpx x %dpx)",
124 fw, fh,
125 s.PxWidth, s.PxHeight,
126 )
127}