PIL exports pdf file in wrong size - python

I am trying to export an image as a .pdf file in DIN A4 size. For that I am using PIL (python imaging library).
My problem is that the resulting pdf file does not seem to have the correct size. Using 300 DPI the size should be 2480x3507 pixels or 210x297 millimeters.
Depending on the application I open the file with, I get different sizes:
Adobe Acrobat Reader: 210x297 millimeters (correct size; please note next error)
Microsoft Edge: 280x396 millimeters
While the images printed on the page are looking correctly in MS Edge, I only get a fraction of two overlapping images in Adobe Acrobat Reader.
I would like to store the page in DIN A4 and be able to print it out. However, with those wrong sizes the images appear stretched on paper.
To get the image I convert the size of the page (21 x 29.7 cm) into pixels with a given DPI using the following function:
def cm_to_px(centimeters):
return (300 * centimeters) / 2.54 # pixels with resolution of 300 DPI
To paste the images on an A4 paper, I use this code:
# get the page image with the correct size
page = Image.new('1', (cm_to_px(21), cm_to_px(29.7)), 1)
# paste the images on the page
for y in range(0, vertical_per_page):
for x in range(0, horizontal_per_page):
page.paste(image, (img_w, img_h))
# save the image as pdf file
page.save('file.pdf', resolution=300)

Related

Problem converting an image for a 3-color e-ink display

I am trying to process an image file into something that can be displayed on a Black/White/Red e-ink display, but I am running into a problem with the output resolution.
Based on the example code for the display, it expects two arrays of bytes (one for Black/White, one for Red), each 15,000 bytes. The resolution of the e-ink display is 400x300.
I'm using the following Python script to generate two BMP files: one for Black/White and one for Red. This is all working, but the file sizes are 360,000 bytes each, which won't fit in the ESP32 memory. The input image (a PNG file) is 195,316 bytes.
The library I'm using has a function called EPD_4IN2B_V2_Display(BLACKWHITEBUFFER, REDBUFFER);, which wants the full image (one channel for BW, one for Red) to be in memory. But, with these image sizes, it won't fit on the ESP32. And, the example uses 15KB for each color channel (BW, R), so I feel like I'm missing something in the image processing necessary to make this work.
Can anyone shed some light on what I'm missing? How would I update the Python image-processing script to account for this?
I am using the Waveshare 4.2inch E-Ink display and the Waveshare ESP32 driver board. A lot of the Python code is based on this StackOverflow post but I can't seem to find the issue.
import io
import traceback
from wand.image import Image as WandImage
from PIL import Image
# This function takes as input a filename for an image
# It resizes the image into the dimensions supported by the ePaper Display
# It then remaps the image into a tri-color scheme using a palette (affinity)
# for remapping, and the Floyd Steinberg algorithm for dithering
# It then splits the image into two component parts:
# a white and black image (with the red pixels removed)
# a white and red image (with the black pixels removed)
# It then converts these into PIL Images and returns them
# The PIL Images can be used by the ePaper library to display
def getImagesToDisplay(filename):
print(filename)
red_image = None
black_image = None
try:
with WandImage(filename=filename) as img:
img.resize(400, 300)
with WandImage() as palette:
with WandImage(width = 1, height = 1, pseudo ="xc:red") as red:
palette.sequence.append(red)
with WandImage(width = 1, height = 1, pseudo ="xc:black") as black:
palette.sequence.append(black)
with WandImage(width = 1, height = 1, pseudo ="xc:white") as white:
palette.sequence.append(white)
palette.concat()
img.remap(affinity=palette, method='floyd_steinberg')
red = img.clone()
black = img.clone()
red.opaque_paint(target='black', fill='white')
black.opaque_paint(target='red', fill='white')
red_image = Image.open(io.BytesIO(red.make_blob("bmp")))
black_image = Image.open(io.BytesIO(black.make_blob("bmp")))
red_bytes = io.BytesIO(red.make_blob("bmp"))
black_bytes = io.BytesIO(black.make_blob("bmp"))
except Exception as ex:
print ('traceback.format_exc():\n%s',traceback.format_exc())
return (red_image, black_image, red_bytes, black_bytes)
if __name__ == "__main__":
print("Running...")
file_path = "testimage-tree.png"
with open(file_path, "rb") as f:
image_data = f.read()
red_image, black_image, red_bytes, black_bytes = getImagesToDisplay(file_path)
print("bw: ", red_bytes)
print("red: ", black_bytes)
black_image.save("output/bw.bmp")
red_image.save("output/red.bmp")
print("BW file size:", len(black_image.tobytes()))
print("Red file size:", len(red_image.tobytes()))
As requested, and in the event that it may be useful for future reader, I write a little bit more extensively what I've said in comments (and was verified to be indeed the reason of the problem).
The e-ink display needs usually a black&white image. That is 1 bit per pixel image. Not a grayscale (1 channel byte per pixel), even less a RGB (3 channels/bytes per pixel).
I am not familiar with bi-color red/black displays. But it seems quite logical that it behave just like 2 binary displays (one black & white display, and one black-white & red display). Sharing the same location.
What your code seemingly does is to remove all black pixels from a RGB image, and use it as a red image, and remove all red pixels from the same RDB image, and use it as a black image. But since those images are obtained with clone they are still RGB images. RGB images that happen to contain only black and white pixels, or red and white pixels, but still RGB image.
With PIL, it is the mode that control how images are represented in memory, and therefore, how they are saved to file.
Relevant modes are RGB, L (grayscale aka 1 linear byte/channel per pixel), and 1 (binary aka 1 bit per pixel).
So what you need is to convert to mode 1. Usind .convert('1') method on both your images.
Note that 400x300×3 (uncompressed rgb data for your image) is 360000, which is what you got. 400×300 (L mode for same image) is 120000, and 400×300/8 (1 mode, 1 bit/pixel) is 15000, which is precisely the expected size as you mentioned. So that is another confirmation that, indeed, 1 bit/pixel image is expected.

Why does pdf2image increase the size of the image after converting it from a pdf?

I'm using pdf2image to convert a pdf to image(.png). However, the size of the image increases after the conversion. Here's the code I am using:
path = "2x.pdf"
pages = pdf2image.convert_from_path(
path,
dpi=300,
poppler_path=poppler_path,
)
for page in pages:
page.save("output_2x.png","PNG")
Code to find the size of the pdf:
from PyPDF2 import PdfFileReader
input1 = PdfFileReader(open('2x.pdf', 'rb'))
input1.getPage(0).mediaBox
Output: RectangleObject([0, 0, 3301, 5100])
Code to find the size of the image:
img = Image.open("output_2x.png")
img.size
Output: (13755, 21250)
The width increases about 4 times whereas the height increases about 8 times.
The PDF format and thus pypdf gives you the size in "default user space units". See https://pypdf.readthedocs.io/en/latest/modules/PageObject.html#pypdf._page.PageObject.user_unit
It is in multiples of 1/72 inch. Hence a value of 1 means a user space unit is 1/72 inch, and a value of 3 means that a user space unit is 3/72 inch.
The unit you want is "pixel". As long as you don't know the resolution that was used, you cannot convert properly.

Why size of jpg file is bigger than expected?

I generate a grayscale image and save it in jpg format.
SCENE_WIDTH = 28
SCENE_HEIGHT = 28
# draw random noice
p, n = 0.5, SCENE_WIDTH*SCENE_HEIGHT
scene_noise = np.random.binomial(1, p, n).reshape((SCENE_WIDTH, SCENE_HEIGHT))*255
scene_noise = scene_noise.astype(np.uint8)
n = scene_noise
print('%d bytes' % (n.size * n.itemsize)) # 784 bytes
cv2.imwrite('scene_noise.jpg', scene_noise)
print('noise: ', os.path.getsize("scene_noise.jpg")) # 1549 bytes
from PIL import Image
im = Image.fromarray(scene_noise)
im.save('scene_noise2.jpg')
print('noise2: ', os.path.getsize("scene_noise2.jpg")) # 1017 bytes
when I change from:
scene_noise = np.random.binomial(1, p, n).reshape((SCENE_WIDTH, SCENE_HEIGHT))*255
to:
scene_noise = np.random.binomial(255, p, n).reshape((SCENE_WIDTH, SCENE_HEIGHT))
The size of file decrease almost 2 times: ~ 775 bytes.
Can you please explain why JPG file is bigger than the raw version and why the size decreases when I change colors from black and white to full grayscale spectrum?
cv2.__version__.split(".") # ['4', '1', '2']
Two things here:
can you explain why the JPEG file is bigger than the raw version?
The size differs because you are not comparing the same things. The first object is a NumPy array, and the second one is a JPEG file. The JPEG file is bigger than the NumPy array (ie. after creating it with OpenCV) because JPEG encoding includes information in the overhead that a NumPy array does not store nor need.
can you explain why the size decreases when I change colours from black and white to a full grayscale spectrum?
This is due to JPEG encoding. If you truly want to understand all of what happens, I highly suggest to understand how JPEG encoding works as I will not go into much detail about this (I am in no way a specialist in this topic). Information on this is well documented on the Wikipedia JPEG article. The general idea is that the more contrast you have in your picture, the bigger it will be in terms of size. Here, having a picture in black and white only will force you to always go between 0 and 255, whereas a grayscale picture will not usually see as big a change between adjacent pixels.

How can I render an image from a PDF with python as Microsoft Edge is doing?

So when I produce a JPEG from a PDF with Wand and Python, I can see how some lines of the staff are different size, but when I zoom with MS Edge, they're same size... and I can not find the way of doing it same way.
Wand
Cairo
First answer method:
convert +antialias -density 1200 Betlem.pdf -alpha off -resize 640x -monochrome A_betlem_m_en_vull_anar.png
I've tried poppler, pdftocairo, pdf2(png¦svg), inkscape, etc...
pdf = wi(filename = "Betlem.pdf", resolution = 300)
pdfImage = pdf.convert("jpeg")
d = 1
for img in pdfImage.sequence:
page = wi(image=img)
page.save(filename="Betlem_" + str(d) + ".jpg")
d += 1
I expect a result where I can see the staff and notes same way of scaling than in Edge (best) or Firefox (great).
As follows the PDF: http://www.xn--estudiantladolaina-lvb.com/partitures/baixa/pdf/26
EDITED:
You can see how it is rendering bad because of errors in the SVG information on the PDF:
Using Imagemagick 6.9.10.55 Q16 Mac OSX and Ghostscript 9.25, I can convert at high quality using supersampling (4x nominal 72 dpi density and resize down by 1/4 to normal scale) as
convert -density 288 A_betlem_m_en_vull_anar.pdf -alpha off -resize 25% A_betlem_m_en_vull_anar.png
How does this compare to your MS Edge view? (Download my image and view it).
ADDITION:
Here is the same code in Python Wand, which uses Imagemagick.
!/bin/python3.7
from wand.image import Image
with Image(filename='A_betlem_m_en_vull_anar.pdf', resolution=288) as img:
img.alpha_channel='off'
img.resize(width=int(0.25*img.width),height=int(0.25*img.height))
img.save(filename='A_betlem_m_en_vull_anar.png')

Get DPI information of PDF image Python

I have pdf file in which an image is embedded in it how i can get the DPI information of that particular image using python.
i tried using "pdfimages" popler-util it gives me the height and width in pixels.
But how i can get the DPI of image from that.
Like the PostScript format or the EPS format, a PDF file has no resolution because it is a vectorial format. All you can do is retrieving the image dimensions in pt (or pixels):
from PyPDF2 import PdfFileReader
with io.open(path, mode="rb") as f:
input_pdf = PdfFileReader(f)
media_box = input_pdf.getPage(0).mediaBox
min_pt = media_box.lowerLeft
max_pt = media_box.upperRight
pdf_width = max_pt[0] - min_pt[0]
pdf_height = max_pt[1] - min_pt[1]

Categories

Resources