@@ -2,6 +2,7 @@ package scanner
22
33import (
44 "bytes"
5+ "encoding/binary"
56 "fmt"
67 "image"
78 "image/color"
@@ -44,8 +45,16 @@ func GeneratePDF(pages []vens.Page, dpi int, isBW bool) ([]byte, error) {
4445 return nil , fmt .Errorf ("decode page %d image config: %w" , i + 1 , err )
4546 }
4647
47- widthMM := float64 (cfg .Width ) / float64 (dpi ) * 25.4
48- heightMM := float64 (cfg .Height ) / float64 (dpi ) * 25.4
48+ // Use actual DPI embedded in the image data when available
49+ pageDPI := dpi
50+ if d := detectImageDPI (p .JPEG ); d > 0 {
51+ pageDPI = d
52+ } else if p .PixelSize != nil && p .PixelSize .XRes > 0 {
53+ pageDPI = p .PixelSize .XRes
54+ }
55+
56+ widthMM := float64 (cfg .Width ) / float64 (pageDPI ) * 25.4
57+ heightMM := float64 (cfg .Height ) / float64 (pageDPI ) * 25.4
4958
5059 pdf .AddPageFormat ("P" , fpdf.SizeType {Wd : widthMM , Ht : heightMM })
5160
@@ -74,6 +83,87 @@ func GeneratePDF(pages []vens.Page, dpi int, isBW bool) ([]byte, error) {
7483 return out .Bytes (), nil
7584}
7685
86+ // detectImageDPI extracts the X resolution (DPI) from image data.
87+ // Supports TIFF (IFD XResolution tag) and JPEG (JFIF APP0 density).
88+ // Returns 0 if the DPI cannot be determined.
89+ func detectImageDPI (data []byte ) int {
90+ if len (data ) < 8 {
91+ return 0
92+ }
93+ // TIFF: starts with "II" (little-endian) or "MM" (big-endian)
94+ if (data [0 ] == 'I' && data [1 ] == 'I' ) || (data [0 ] == 'M' && data [1 ] == 'M' ) {
95+ return detectTIFFDPI (data )
96+ }
97+ // JPEG: starts with FF D8
98+ if data [0 ] == 0xFF && data [1 ] == 0xD8 {
99+ return detectJPEGDPI (data )
100+ }
101+ return 0
102+ }
103+
104+ func detectTIFFDPI (data []byte ) int {
105+ var bo binary.ByteOrder
106+ if data [0 ] == 'I' {
107+ bo = binary .LittleEndian
108+ } else {
109+ bo = binary .BigEndian
110+ }
111+ if bo .Uint16 (data [2 :4 ]) != 42 {
112+ return 0
113+ }
114+ ifdOff := int (bo .Uint32 (data [4 :8 ]))
115+ if ifdOff + 2 > len (data ) {
116+ return 0
117+ }
118+ n := int (bo .Uint16 (data [ifdOff : ifdOff + 2 ]))
119+ for i := range n {
120+ off := ifdOff + 2 + i * 12
121+ if off + 12 > len (data ) {
122+ break
123+ }
124+ tag := bo .Uint16 (data [off : off + 2 ])
125+ if tag == 282 { // XResolution (RATIONAL = num/den)
126+ valOff := int (bo .Uint32 (data [off + 8 : off + 12 ]))
127+ if valOff + 8 > len (data ) {
128+ return 0
129+ }
130+ num := bo .Uint32 (data [valOff : valOff + 4 ])
131+ den := bo .Uint32 (data [valOff + 4 : valOff + 8 ])
132+ if den == 0 {
133+ return 0
134+ }
135+ return int (num / den )
136+ }
137+ }
138+ return 0
139+ }
140+
141+ func detectJPEGDPI (data []byte ) int {
142+ i := 2
143+ for i + 4 < len (data ) {
144+ if data [i ] != 0xFF {
145+ break
146+ }
147+ marker := data [i + 1 ]
148+ segLen := int (binary .BigEndian .Uint16 (data [i + 2 : i + 4 ]))
149+ if marker == 0xE0 && segLen >= 14 { // APP0 (JFIF)
150+ seg := data [i + 4 :]
151+ if len (seg ) >= 10 && string (seg [0 :5 ]) == "JFIF\x00 " {
152+ units := seg [7 ]
153+ xd := int (binary .BigEndian .Uint16 (seg [8 :10 ]))
154+ if units == 1 { // dots per inch
155+ return xd
156+ }
157+ if units == 2 { // dots per cm
158+ return int (float64 (xd ) * 2.54 )
159+ }
160+ }
161+ }
162+ i += 2 + segLen
163+ }
164+ return 0
165+ }
166+
77167// toBitonalPNG converts an image to a 1-bit paletted image (black & white).
78168func toBitonalPNG (img image.Image ) * image.Paletted {
79169 bounds := img .Bounds ()
0 commit comments