Skip to content

Instantly share code, notes, and snippets.

@pystardust
Created May 1, 2022 21:09
Show Gist options
  • Save pystardust/48b36f358839d49a5c29509f4d40034c to your computer and use it in GitHub Desktop.
Save pystardust/48b36f358839d49a5c29509f4d40034c to your computer and use it in GitHub Desktop.
Change contrast of pdf using python
"""
# pdf_contrast.py
Modify contrast of pdf
## Install
```
pip install Pillow pdf2image img2pdf tqdm
```
> Save this file as pdf_contrast.py
## USAGE
```
$ python pdf_contrast.py 2.3 -i in.pdf -o out.pdf
Loading pdf in.pdf
Pages: 48
Contrast x2.3: 100%|███████████████████████| 48/48 [00:02<00:00, 18.42pages/s]
Saving pdf to out.pdf
```
"""
from argparse import ArgumentParser
import io
from PIL import ImageEnhance
import img2pdf
import pdf2image
from tqdm import tqdm
def pdf_contrast(input_file: str, contrast: float, output_file: str):
"""
Create a new pdf corresponding to the contrast multiplier
`input_file`: name the of the input_file
`contrast`: contrast multiplier. 1 corresponds to no change
`output_file`: name of the file to be saved
"""
print(f'Loading pdf {input_file}')
input_images = pdf2image.convert_from_path(input_file)
print(f'Pages: {len(input_images)}')
output_images: list[bytes] = []
for img in tqdm(input_images,
desc=f"Contrast x{contrast}",
unit="pages"
):
enhancer = ImageEnhance.Contrast(img)
out_im = enhancer.enhance(contrast)
out_img_bytes = io.BytesIO()
out_im.save(out_img_bytes, format="JPEG")
output_images.append(out_img_bytes.getvalue())
print(f'Saving pdf to {output_file}')
with open(output_file, "wb") as outf:
img2pdf.convert(*output_images, outputstream=outf)
if __name__ == "__main__":
input_file = "in.pdf"
contrast = 1.2
output_file = f"out_contrast{contrast}.pdf"
parser = ArgumentParser(description="Modify contrast of pdfs")
parser.add_argument('-i', '--input',
help="Input filename",
dest="input_file")
parser.add_argument('-o', '--output',
help="Output file name",
dest="output_file")
parser.add_argument('contrast', type=float,
help="""
Contrast multipler, must be a float.
1 corresponds to no change (1x).
2 corresponds to double (2x).
0.5 corresponds to half (0.5x).
""")
args = parser.parse_args()
pdf_contrast(args.input_file, args.contrast, args.output_file)
@akhil-rana
Copy link

I modified this code to use a little less memory for larger files..

from argparse import ArgumentParser
from PIL import ImageEnhance, Image
import img2pdf
import pdf2image
from tqdm import tqdm
import os


def modify_contrast(image_path, contrast, tile_size=(256, 256)):
    """
    Modify the contrast of an image using tiles.
    
    Parameters
    ----------
    image_path : str
        Path to the image
    contrast : float
        Contrast multiplier. 1 corresponds to no change.
    tile_size : tuple
        Tuple specifying the size of each tile (width, height).
        
    Returns
    -------
    Image object
        An image object representing the modified image.
    """
    with Image.open(image_path) as img:
        img_width, img_height = img.size
        enhanced_img = Image.new("RGB", (img_width, img_height))

        for i in range(0, img_height, tile_size[1]):
            for j in range(0, img_width, tile_size[0]):
                box = (j, i, j + tile_size[0], i + tile_size[1])
                tile = img.crop(box)
                enhancer = ImageEnhance.Contrast(tile)
                tile = enhancer.enhance(contrast)
                enhanced_img.paste(tile, box)

        return enhanced_img


def images_to_pdf(image_paths, output_file):
    """
    Convert a list of images to a single pdf.
    
    Parameters
    ----------
    image_paths : list
        List of path to the images.
    output_file : str
        Path to the output pdf file.
        
    Returns
    -------
    None
    """
    with open(output_file, "wb") as outf:
        img2pdf.convert(*[open(img, "rb")
                        for img in image_paths], outputstream=outf)


def pdf_contrast(input_file: str, contrast: float):
    """
    Modify the contrast of a pdf.
    
    Parameters
    ----------
    input_file : str
        Path to the input pdf file.
    contrast : float
        Contrast multiplier. 1 corresponds to no change.
        
    Returns
    -------
    None
    """
    temp_folder = "temp"
    if not os.path.exists(temp_folder):
        os.makedirs(temp_folder)

    print(f'Loading pdf {input_file}')

    pdf2image.convert_from_path(
        input_file, output_folder=temp_folder, fmt='jpeg')

    image_paths = [os.path.join(temp_folder, f)
                   for f in os.listdir(temp_folder)]
    modified_images = [modify_contrast(img, contrast) for img in tqdm(
        image_paths, desc="Modifying images")]

    for i, img in enumerate(modified_images):
        img.save(image_paths[i])
    # Generate the output pdf filename
    output_file = (input_file.split('.pdf'))[0] + "_modified_contrast.pdf"

    # Convert the modified images to a pdf
    images_to_pdf(image_paths, output_file)

    # Remove the temporary images
    for img in image_paths:
        os.remove(img)

    print(f'Saving pdf to {output_file}')


if __name__ == "__main__":
    # Set up the argument parser
    parser = ArgumentParser(description="Modify contrast of pdfs")
    parser.add_argument('-i', '--input',
                        help="Input filename",
                        dest="input_file")
    parser.add_argument('contrast', type=float,
                        help="""
                             Contrast multipler, must be a float.
                             1 corresponds to no change (1x).
                             2 corresponds to double (2x).
                             0.5 corresponds to half (0.5x).
                             """)

    # Parse the arguments
    args = parser.parse_args()

    # Call the `pdf_contrast` function
    pdf_contrast(args.input_file, args.contrast)

@chapmanjacobd
Copy link

chapmanjacobd commented Nov 30, 2023

The problem with the tiled version is that it applies the transformation across each tile rather than the whole image so you end up with inconsistent, blocky output.

Contrast: Original image is blended with a gray image of the same size. Here's the only point, where some calculation takes place. The gray value is determined by the mean of the original image' grayscale version.
https://stackoverflow.com/questions/59166448/whats-the-formula-used-in-pil-imageenhance-enhance-feature-for-color-brightnes

I exposed the other PIL ImageEnhance methods:

#!/usr/bin/python3

import argparse
import io
import os

import img2pdf
import pdf2image
from PIL import ImageEnhance
from tqdm import tqdm


def pdf_contrast(args):
    input_images = pdf2image.convert_from_path(args.input_path)
    print(f'Loaded {len(input_images)} pages')

    output_images: list[bytes] = []
    for img in tqdm(input_images, unit="pages"):
        for method in ['Brightness', 'Contrast', 'Color', 'Sharpness']:
            val = getattr(args, method.lower())
            if val != 100:
                enhancer = getattr(ImageEnhance, method)(img)
                img = enhancer.enhance(val / 100)

        out_img_bytes = io.BytesIO()
        img.save(out_img_bytes, format="JPEG")
        output_images.append(out_img_bytes.getvalue())

    print(f'Saving {args.output_path}')
    with open(args.output_path, "wb") as outf:
        img2pdf.convert(*output_images, outputstream=outf)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--brightness", "-b", type=int, default=100)
    parser.add_argument("--contrast", "-c", type=int, default=100)
    parser.add_argument("--color", "-C", type=int, default=100)
    parser.add_argument("--sharpness", "-s", type=int, default=100)

    parser.add_argument("input_path", help="Input PDF file")
    parser.add_argument("output_path", nargs='?', help="Output PDF file")
    args = parser.parse_args()

    if args.output_path is None:
        params = []
        if args.contrast != 100:
            params.append(f"c{args.contrast}")
        if args.brightness != 100:
            params.append(f"b{args.brightness}")
        if args.color != 100:
            params.append(f"C{args.color}")
        if args.sharpness != 100:
            params.append(f"s{args.sharpness}")

        suffix = '.' + '.'.join(params) + '.pdf'
        args.output_path = os.path.splitext(args.input_path)[0] + suffix

    pdf_contrast(args)
$ pdf_contrast.py out.pdf -b 115 -c 120 -C 70 -s 80
Loaded 56 pages
100%|███████████████████| 56/56 [00:08<00:00,  6.82pages/s]
Saving out.c120.b115.C70.s80.pdf

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment