···11+package xmp
22+33+import (
44+ "strconv"
55+66+ "github.com/jphastings/dotpostcard/types"
77+)
88+99+func extractSecrets(flip types.Flip, regions []xmpRegion) (front, back []types.Polygon) {
1010+ for _, region := range regions {
1111+ // Secrets are alwats polygonal & relative shapes, so skip any others
1212+ if region.Boundary.Shape != "polygon" || region.Boundary.Unit != "relative" {
1313+ continue
1414+ }
1515+1616+ // Secret regions have specific names (in various languages), skip any that don't have the right name
1717+ isSecretRegion := false
1818+ for _, n := range region.Names {
1919+ // Check for any of the indicators that this is a secret region
2020+ if val, ok := privateExplainer[n.Lang]; ok && val == n.Value {
2121+ isSecretRegion = true
2222+ break
2323+ }
2424+ }
2525+ if !isSecretRegion {
2626+ continue
2727+ }
2828+2929+ regionOnFront := true
3030+ poly := types.Polygon{Prehidden: true}
3131+ for i, vert := range region.Boundary.Vertices {
3232+ x, xErr := strconv.ParseFloat(vert.X, 64)
3333+ y, yErr := strconv.ParseFloat(vert.Y, 64)
3434+ if xErr != nil || yErr != nil {
3535+ poly.Points = nil
3636+ break
3737+ }
3838+3939+ // Make sure the region is only one one side of the card (if it's a double sided card)
4040+ if flip != types.FlipNone {
4141+ vertOnFront := y < 0.5
4242+ if i == 0 {
4343+ regionOnFront = vertOnFront
4444+ } else if vertOnFront != regionOnFront {
4545+ poly.Points = nil
4646+ break
4747+ }
4848+ }
4949+5050+ point := types.Point{X: x, Y: y}.TransformToSingleSided(regionOnFront, flip)
5151+ poly.Points = append(poly.Points, point)
5252+ }
5353+ if poly.Points == nil {
5454+ continue
5555+ }
5656+5757+ if regionOnFront {
5858+ front = append(front, poly)
5959+ } else {
6060+ back = append(back, poly)
6161+ }
6262+ }
6363+6464+ return front, back
6565+}
-206
formats/xmp/xmp.go
···11-package xmp
22-33-import (
44- "bytes"
55- "encoding/json"
66- "encoding/xml"
77- "fmt"
88- "io"
99- "math/big"
1010- "strconv"
1111- "time"
1212-1313- "github.com/jphastings/dotpostcard/internal/general"
1414- "github.com/jphastings/dotpostcard/types"
1515-1616- "github.com/trimmer-io/go-xmp/xmp"
1717-)
1818-1919-func MetadataToXMP(meta types.Metadata, dims *types.Size) ([]byte, error) {
2020- var sections []interface{}
2121- if dims != nil {
2222- sections = addTIFFSection(sections, *dims)
2323- }
2424- sections = addIPTCCoreSection(sections, meta)
2525- sections = addIPTCExtSection(sections, meta)
2626- sections = addExifSection(sections, meta)
2727- sections = addDCSection(sections, meta)
2828- sections = addPostcardSection(sections, meta)
2929-3030- x := xmpXML{
3131- NamespaceX: "adobe:ns:meta/",
3232- NamespaceXMPTK: fmt.Sprintf("postcards/v%s", general.Version),
3333- RDF: rdfXML{
3434- Namespace: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
3535- Sections: sections,
3636- },
3737- }
3838-3939- d := &bytes.Buffer{}
4040-4141- // Intro
4242- if _, err := d.Write([]byte("<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>")); err != nil {
4343- return nil, fmt.Errorf("unable to write start of XMP XML data: %w", err)
4444- }
4545-4646- // XML
4747- if err := xml.NewEncoder(d).Encode(x); err != nil {
4848- return nil, fmt.Errorf("unable to write XMP XML data: %w", err)
4949- }
5050-5151- // Outro
5252- if _, err := d.Write([]byte("<?xpacket end='w'?>")); err != nil {
5353- return nil, fmt.Errorf("unable to write end of XMP XML data: %w", err)
5454- }
5555-5656- return d.Bytes(), nil
5757-}
5858-5959-func MetadataFromXMP(r io.Reader) (types.Metadata, types.Size, error) {
6060- d := xmp.NewDecoder(r)
6161- doc := &xmp.Document{}
6262- if err := d.Decode(doc); err != nil {
6363- return types.Metadata{}, types.Size{}, err
6464- }
6565-6666- xPaths, err := doc.ListPaths()
6767- if err != nil {
6868- return types.Metadata{}, types.Size{}, err
6969- }
7070-7171- var meta types.Metadata
7272- var size types.Size
7373- var resolution [3]*big.Rat
7474-7575- for _, xPath := range xPaths {
7676- switch xPath.Path {
7777- case "Postcard:Flip":
7878- meta.Flip = types.Flip(xPath.Value)
7979-8080- case "tiff:ImageWidth":
8181- size.PxWidth, _ = strconv.Atoi(xPath.Value)
8282- case "tiff:ImageLength":
8383- size.PxHeight, _ = strconv.Atoi(xPath.Value)
8484- case "tiff:XResolution":
8585- res, err := scanBigRat(xPath.Value)
8686- if err == nil {
8787- resolution[0] = res
8888- }
8989- case "tiff:YResolution":
9090- res, err := scanBigRat(xPath.Value)
9191- if err == nil {
9292- resolution[1] = res
9393- }
9494- case "tiff:ResolutionUnit":
9595- if xPath.Value != "3" {
9696- // Assume inches
9797- resolution[2] = big.NewRat(100, 254)
9898- }
9999-100100- case "exif:GPSAreaInformation":
101101- meta.Location.Name = xPath.Value
102102- case "exif:GPSLatitude":
103103- meta.Location.Latitude = scanDegrees(xPath.Value)
104104- case "exif:GPSLongitude":
105105- meta.Location.Longitude = scanDegrees(xPath.Value)
106106-107107- case "Postcard:Recipient":
108108- meta.Recipient = scanPerson(xPath.Value)
109109- case "Postcard:Sender":
110110- meta.Sender = scanPerson(xPath.Value)
111111- case "Postcard:Context":
112112- meta.Context.Description = xPath.Value
113113- case "Postcard:ContextAuthor":
114114- meta.Context.Author = scanPerson(xPath.Value)
115115-116116- case "exif:DateTimeOriginal":
117117- if date, err := time.Parse(`2006-01-02`, xPath.Value); err == nil {
118118- meta.SentOn = types.Date{Time: date}
119119- }
120120-121121- case "Postcard:DescriptionFront":
122122- meta.Front.Description = xPath.Value
123123- case "Postcard:DescriptionBack":
124124- meta.Back.Description = xPath.Value
125125- case "Postcard:TranscriptionFront":
126126- _ = json.Unmarshal([]byte(xPath.Value), &meta.Front.Transcription)
127127- case "Postcard:TranscriptionBack":
128128- _ = json.Unmarshal([]byte(xPath.Value), &meta.Back.Transcription)
129129-130130- default:
131131- // TODO: secret sections
132132- // fmt.Printf("%s // %T: %v\n", xPath.Path, xPath.Value, xPath.Value)
133133- }
134134- }
135135-136136- if meta.Flip != types.FlipNone {
137137- size.PxHeight /= 2
138138- }
139139-140140- if resolution[0] != nil {
141141- // Convert units, if necessary
142142- if resolution[2] != nil {
143143- resolution[0].Mul(resolution[0], resolution[2])
144144- resolution[1].Mul(resolution[1], resolution[2])
145145- }
146146-147147- size.SetResolution(resolution[0], resolution[1])
148148- // Postcard height is half reported dimensions if Flip isn't none (ie. this is a stacked web format postcard image)
149149- if meta.Flip != types.FlipNone {
150150- size.CmHeight.Quo(size.CmHeight, big.NewRat(2, 1))
151151- }
152152- meta.Physical.FrontDimensions = size
153153- }
154154-155155- return meta, size, nil
156156-}
157157-158158-func scanBigRat(str string) (*big.Rat, error) {
159159- var a, b int64
160160- if _, err := fmt.Sscanf(str, "%d/%d", &a, &b); err != nil {
161161- return nil, err
162162- }
163163- return big.NewRat(a, b), nil
164164-}
165165-166166-func scanDegrees(str string) *float64 {
167167- var deg int
168168- var min float64
169169- var dir string
170170-171171- if _, err := fmt.Sscanf(str, "%d,%f%s", °, &min, &dir); err != nil {
172172- return nil
173173- }
174174-175175- fl := float64(deg) + min/60
176176- switch dir {
177177- case "N":
178178- if fl > 90 || fl < 0 {
179179- return nil
180180- }
181181- case "E":
182182- if fl > 180 || fl < 0 {
183183- return nil
184184- }
185185- case "S":
186186- if fl > 90 || fl < 0 {
187187- return nil
188188- }
189189- fl *= -1
190190- case "W":
191191- if fl > 180 || fl < 0 {
192192- return nil
193193- }
194194- fl *= -1
195195- default:
196196- return nil
197197- }
198198-199199- return &fl
200200-}
201201-202202-func scanPerson(str string) types.Person {
203203- var p types.Person
204204- p.Scan(str)
205205- return p
206206-}
···11-<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?><x:xmpmeta xmlns:x="adobe:ns:meta/" xmlns:xmptk="postcards/v0.1.0"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"><Iptc4xmpCore:AltTextAccessibility><rdf:Alt><rdf:li xml:lang="en">On the front of a postcard: The word 'Front' in large red letters</rdf:li></rdf:Alt></Iptc4xmpCore:AltTextAccessibility></rdf:Description><rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:description><rdf:Alt><rdf:li xml:lang="en">A postcard stored in the dotpostcard format (https://dotpostcard.org)</rdf:li></rdf:Alt></dc:description></rdf:Description><rdf:Description xmlns:Postcard="https://dotpostcard.org/xmp/1.0/"><Postcard:Flip>book</Postcard:Flip><Postcard:Sender></Postcard:Sender><Postcard:Recipient></Postcard:Recipient><Postcard:Context xml:lang=""></Postcard:Context><Postcard:ContextAuthor></Postcard:ContextAuthor></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end='w'?>11+<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?><x:xmpmeta xmlns:x="adobe:ns:meta/" xmlns:xmptk="postcards/v0.1.0"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"><Iptc4xmpCore:AltTextAccessibility><rdf:Alt><rdf:li xml:lang="en">Both sides of a postcard. On the front: The word 'Front' in large blue letters On the back: Back</rdf:li></rdf:Alt></Iptc4xmpCore:AltTextAccessibility></rdf:Description><rdf:Description xmlns:Iptc4xmpExt="http://iptc.org/std/Iptc4xmpExt/2008-02-29/"><Iptc4xmpExt:ImageRegion><rdf:Bag><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:Name><rdf:Alt><rdf:li xml:lang="en">Private information</rdf:li></rdf:Alt></Iptc4xmpExt:Name><Iptc4xmpExt:RegionBoundary rdf:parseType="Resource"><Iptc4xmpExt:rbUnit>relative</Iptc4xmpExt:rbUnit><Iptc4xmpExt:rbShape>polygon</Iptc4xmpExt:rbShape><Iptc4xmpExt:rbVertices><rdf:Seq><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:rbX>0.3</Iptc4xmpExt:rbX><Iptc4xmpExt:rbY>0.3</Iptc4xmpExt:rbY></rdf:li><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:rbX>0.3</Iptc4xmpExt:rbX><Iptc4xmpExt:rbY>0.4</Iptc4xmpExt:rbY></rdf:li><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:rbX>0.4</Iptc4xmpExt:rbX><Iptc4xmpExt:rbY>0.4</Iptc4xmpExt:rbY></rdf:li><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:rbX>0.4</Iptc4xmpExt:rbX><Iptc4xmpExt:rbY>0.3</Iptc4xmpExt:rbY></rdf:li></rdf:Seq></Iptc4xmpExt:rbVertices></Iptc4xmpExt:RegionBoundary></rdf:li><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:Name><rdf:Alt><rdf:li xml:lang="en">Private information</rdf:li></rdf:Alt></Iptc4xmpExt:Name><Iptc4xmpExt:RegionBoundary rdf:parseType="Resource"><Iptc4xmpExt:rbUnit>relative</Iptc4xmpExt:rbUnit><Iptc4xmpExt:rbShape>polygon</Iptc4xmpExt:rbShape><Iptc4xmpExt:rbVertices><rdf:Seq><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:rbX>0</Iptc4xmpExt:rbX><Iptc4xmpExt:rbY>0.5</Iptc4xmpExt:rbY></rdf:li><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:rbX>0</Iptc4xmpExt:rbX><Iptc4xmpExt:rbY>0.65</Iptc4xmpExt:rbY></rdf:li><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:rbX>0.1</Iptc4xmpExt:rbX><Iptc4xmpExt:rbY>0.65</Iptc4xmpExt:rbY></rdf:li><rdf:li rdf:parseType="Resource"><Iptc4xmpExt:rbX>0.1</Iptc4xmpExt:rbX><Iptc4xmpExt:rbY>0.5</Iptc4xmpExt:rbY></rdf:li></rdf:Seq></Iptc4xmpExt:rbVertices></Iptc4xmpExt:RegionBoundary></rdf:li></rdf:Bag></Iptc4xmpExt:ImageRegion></rdf:Description><rdf:Description xmlns:exif="http://ns.adobe.com/exif/1.0/"><exif:DateTimeOriginal>2006-01-02</exif:DateTimeOriginal><exif:GPSAreaInformation>Front, Italy</exif:GPSAreaInformation><exif:GPSLatitude>45,16.80000000N</exif:GPSLatitude><exif:GPSLongitude>7,39.60000000E</exif:GPSLongitude></rdf:Description><rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:description><rdf:Alt><rdf:li xml:lang="en">A postcard stored in the dotpostcard format (https://dotpostcard.org)</rdf:li></rdf:Alt></dc:description></rdf:Description><rdf:Description xmlns:Postcard="https://dotpostcard.org/xmp/1.0/"><Postcard:Flip>book</Postcard:Flip><Postcard:Sender>Alice (https://alice.example.com)</Postcard:Sender><Postcard:Recipient>Bob (https://bob.example.org)</Postcard:Recipient><Postcard:Context><rdf:Alt><rdf:li xml:lang="en-GB">This is a sample postcard, with all fields expressed.</rdf:li></rdf:Alt></Postcard:Context><Postcard:ContextAuthor>Carol (https://carol.example.net)</Postcard:ContextAuthor><Postcard:DescriptionFront>The word 'Front' in large blue letters</Postcard:DescriptionFront><Postcard:DescriptionBack>The word 'Back' in large red letters</Postcard:DescriptionBack><Postcard:TranscriptionFront>{"text":"Front"}</Postcard:TranscriptionFront><Postcard:TranscriptionBack>{"text":"Back","annotations":[{"type":"locale","value":"en-GB","start":0,"end":4}]}</Postcard:TranscriptionBack><Postcard:PhysicalThicknessMM>0.4</Postcard:PhysicalThicknessMM></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end='w'?>
+1-1
types/annotations.go
···16161717type Annotation struct {
1818 Type AnnotationType `json:"type"`
1919- Value string `json:"value"`
1919+ Value string `json:"value,omitempty"`
2020 // The *byte* count just before this annotation starts
2121 Start uint `json:"start"`
2222 // The *byte* count just after this annotation ends
+67
types/point.go
···34343535 return nil
3636}
3737+3838+// Transforms a point (relative to a single image) so that it will point to the same spot
3939+// (when relative to the double-sided equivalent).
4040+func (p Point) TransformToDoubleSided(onFront bool, flip Flip) Point {
4141+ if flip == FlipNone {
4242+ return p
4343+ }
4444+4545+ // For double sided images the front side is always correctly oriented, and half the height of the full image
4646+ if onFront {
4747+ return Point{
4848+ X: p.X,
4949+ Y: p.Y / 2,
5050+ }
5151+ }
5252+5353+ switch flip {
5454+ case FlipLeftHand:
5555+ return Point{
5656+ X: p.Y,
5757+ Y: 1 - p.X/2,
5858+ }
5959+ case FlipRightHand:
6060+ return Point{
6161+ X: (1 - p.Y),
6262+ Y: p.X/2 + 0.5,
6363+ }
6464+ default: // FlipBook & FlipCalendar are the same
6565+ return Point{
6666+ X: p.X,
6767+ Y: (p.Y / 2) + 0.5,
6868+ }
6969+ }
7070+}
7171+7272+// Transforms a point (relative to a double image) so that it will point to the same spot
7373+// (when relative to the single-sided equivalent).
7474+func (p Point) TransformToSingleSided(onFront bool, flip Flip) Point {
7575+ if flip == FlipNone {
7676+ return p
7777+ }
7878+7979+ if onFront {
8080+ return Point{
8181+ X: p.X,
8282+ Y: p.Y * 2,
8383+ }
8484+ }
8585+8686+ switch flip {
8787+ case FlipLeftHand:
8888+ return Point{
8989+ X: 2 - 2*p.Y,
9090+ Y: p.X,
9191+ }
9292+ case FlipRightHand:
9393+ return Point{
9494+ X: 2*p.Y - 1,
9595+ Y: (1 - p.X),
9696+ }
9797+ default: // FlipBook & FlipCalendar are the same
9898+ return Point{
9999+ X: p.X,
100100+ Y: (p.Y * 2) - 1,
101101+ }
102102+ }
103103+}