如何把手機拍攝的成績表照片整理成一個小型 PDF
Turning phone photos of a marksheet into one small PDF
作者以合成的成績表照片測試出一套製作小型 PDF 的流程:先裁切、縮小及壓縮 JPEG,再用 img2pdf 原樣嵌入,並指定 A4 紙張。測試中,三頁照片縮至 1000 像素寬、品質 40 並轉為灰階後,PDF 為 87,696 bytes;PDF 內的 JPEG 與處理後的輸入檔逐 byte 相同。PDF 內容仍是圖片,沒有 OCR;如需可搜尋文字,須另行進行 OCR。
A lot of upload forms want "one PDF, under 100 KB" for something you only have as phone photos: a two-sided marksheet, a certificate, a few pages of a transcript. Putting photos into a PDF is easy. Getting a small one with even pages takes knowing what the PDF actually does with your JPEGs. Here's what I worked out, tested on dummy marksheet-style images I generated (no real documents).
What a PDF does with a JPEG
A PDF can store a JPEG as-is. The image object gets the filter /DCTDecode (DCT is the maths inside JPEG), and the compressed bytes from your file are copied straight in. Nothing is decoded or recompressed, so quality doesn't drop. It also means the PDF can't be smaller than the photos inside it.
Other tools decode the JPEG to pixels and encode a new JPEG. That can make the file smaller or bigger depending on their quality setting, and it's a second round of lossy compression either way.
So the order matters: crop and compress the photos first, then put them in a PDF with something that copies JPEGs unchanged.
My test images
I made three "phone photos" with Pillow: a fake marksheet table on paper-coloured background, a brown desk border around it, some noise so it compresses like a photo, saved at quality 92. Two are 3024 x 4032, and a third is 2448 x 3264 as if taken on a different phone:
page1.jpg 3024x4032 1,441,946 bytes
page2.jpg 3024x4032 1,440,963 bytes
page3.jpg 2448x3264 974,380 bytes (3,857,289 total)
img2pdf: JPEG passthrough
img2pdf (pip install img2pdf) embeds JPEGs without touching them:
$ img2pdf page1.jpg page2.jpg page3.jpg -o straight.pdf
$ ls -l straight.pdf
3859731 straight.pdf
That's 2,442 bytes more than the three photos together, which is the PDF structure. pdfimages -list (from poppler-utils) shows what's inside:
$ pdfimages -list straight.pdf
page num type width height color comp bpc enc interp object ID x-ppi y-ppi size ratio
--------------------------------------------------------------------------------------------
1 0 image 3024 4032 rgb 3 8 jpeg no 13 0 96 96 1408K 3.9%
2 1 image 3024 4032 rgb 3 8 jpeg no 2 0 96 96 1407K 3.9%
3 2 image 2448 3264 rgb 3 8 jpeg no 5 0 96 96 952K 4.1%
To prove it's a straight copy, I pulled the images back out and compared bytes:
$ pdfimages -j straight.pdf x/s
$ cmp x/s-000.jpg page1.jpg && echo identical
identical
ImageMagick and Pillow re-encode
The same three photos through ImageMagick 7:
$ magick page1.jpg page2.jpg page3.jpg magick.pdf
$ ls -l magick.pdf
3493823 magick.pdf
$ pdfimages -j magick.pdf x/m
$ cmp x/m-000.jpg page1.jpg
x/m-000.jpg page1.jpg differ: byte 181, line 6
The image is still /DCTDecode, but it's a new JPEG: 1,301,295 bytes where the original was 1,441,946. ImageMagick decoded the photo and encoded it again.
Pillow does the same. This script:
# pillow_pdf.py: python pillow_pdf.py out.pdf a.jpg b.jpg ...
import sys
from PIL import Image
out, files = sys.argv[1], sys.argv[2:]
ims = [Image.open(f) for f in files]
ims[0].save(out, save_all=True, append_images=ims[1:], resolution=150)
gave a 1,584,553 byte PDF, with page 1's image re-encoded down to 588,020 bytes. Smaller, but that's quality loss you didn't choose, and still nowhere near 100 KB.
Step 1: crop and shrink the photos
For a document photo, most of the bytes go on things nobody needs: the desk, 12 megapixels, and colour on a page that's black text. This script crops to the paper, resizes, optionally converts to greyscale, and saves a JPEG at a chosen quality:
# shrink.py: python shrink.py in.jpg out.jpg left top right bottom [width] [quality] [gray]
import sys
from PIL import Image
src, out = sys.argv[1], sys.argv[2]
box = tuple(int(v) for v in sys.argv[3:7])
width = int(sys.argv[7]) if len(sys.argv) > 7 else 1240
quality = int(sys.argv[8]) if len(sys.argv) > 8 else 60
gray = len(sys.argv) > 9 and sys.argv[9] == "gray"
im = Image.open(src).crop(box) # cut the desk away
h = round(im.height * width / im.width)
im = im.resize((width, h), Image.LANCZOS) # 1240 px is A4 width at 150 DPI
if gray:
im = im.convert("L") # one channel instead of three
im.save(out, quality=quality, optimize=True, dpi=(150, 150))
print(out, im.size, im.mode)
The crop box comes from looking at the photo (any image viewer shows pixel positions). What each setting gave me, for all three pages through img2pdf:
1240 px wide, quality 60, colour -> 150,709 bytes
1240 px wide, quality 60, grey -> 131,641 bytes
1000 px wide, quality 40, grey -> 87,696 bytes
Only the last one is under 100 KB. At 1000 pixels across an A4 width the marks and table lines were still clear when I zoomed in. My images are synthetic, so a real photo will land on different numbers; the method is to step width and quality down until it fits, then zoom in and check every digit is readable.
The gotcha: pages take the image's size, not A4
Look at the page sizes in straight.pdf. I checked them with pypdf:
# pages.py: print each page's size in pt and mm
import sys
from pypdf import PdfReader
for f in sys.argv[1:]:
for i, p in enumerate(PdfReader(f).pages, 1):
w, h = float(p.mediabox.width), float(p.mediabox.height)
print(f"{f} page {i}: {w:.1f} x {h:.1f} pt = {w/72*25.4:.0f} x {h/72*25.4:.0f} mm")
straight.pdf page 1: 2268.0 x 3024.0 pt = 800 x 1067 mm
straight.pdf page 2: 2268.0 x 3024.0 pt = 800 x 1067 mm
straight.pdf page 3: 1836.0 x 2448.0 pt = 648 x 864 mm
Each page is the image's pixel size divided by a DPI. My test photos had no DPI in the header (the JFIF density was only an aspect ratio), so img2pdf assumed 96 and made pages nearly a metre tall. ImageMagick assumed 72 and went bigger still (1067 x 1422 mm). And because the third photo came from a "different phone", its page is smaller than the others. Print that and it comes out uneven; on screen the pages jump in size as you scroll. Even after cropping, my pages were 279, 279 and 277 mm tall, because no two crops are exactly the same shape.
The fix is to give the PDF a fixed paper size and let the image fit inside it:
$ img2pdf --pagesize A4 --border 10mm final1.jpg final2.jpg final3.jpg -o marksheet.pdf
$ pdfinfo marksheet.pdf | grep -E "Pages|Page size|File size"
Pages: 3
Page size: 595.276 x 841.89 pts (A4)
File size: 87696 bytes
All three pages are A4, and cmp on the extracted images still says identical: setting the page size only changes where the image is drawn, not the image data.
No OCR
The result is pictures of text. pdffonts lists no fonts, and pdftotext gave me back three page-break characters and no text. If a portal needs searchable text, that's a separate OCR step; none of the routes above does it.
Without the command line
- Windows: select the photos in File Explorer, right-click, Print, and pick "Microsoft Print to PDF" as the printer. The paper size you choose there becomes the page size.
- macOS Preview: open all the photos in one window, drag the thumbnails into order, select them all, then File > Print and choose Save as PDF from the PDF menu.
- Phone scanner apps: the scan feature in many notes and file apps finds the page edges, crops and straightens. Check the size of what they export, since some save large colour pages.
I didn't run these on my Linux test machine, so I can't say whether they keep the JPEG or re-encode it. Either way, compress first and check the result with pdfimages -list and pdfinfo.
A browser option
I built ToolOcean, and its images-to-PDF page works the img2pdf way. I fed the live page two of my test photos in headless Chrome: both JPEGs came out byte-identical inside the PDF, and the pages were sized from the pixels (1067 x 1422 mm and 864 x 1151 mm, so the mixed-size gotcha applies). It doesn't crop, compress or OCR, so do those first: toolocean.co/images-to-pdf.
Drafted with AI help, tested and checked by me.
來源:DEV Community · dev.to