Skip to content

Commit 311dfc3

Browse files
committed
Correct scanned image DPI detection
1 parent 6bd1031 commit 311dfc3

File tree

1 file changed

+92
-2
lines changed

1 file changed

+92
-2
lines changed

internal/scanner/pdf.go

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package scanner
22

33
import (
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).
78168
func toBitonalPNG(img image.Image) *image.Paletted {
79169
bounds := img.Bounds()

0 commit comments

Comments
 (0)