import os import sys # Uses Pillow imaging library # Install with: python -m pip install Pillow from PIL import Image from PIL import ImageCms from PIL import ImageOps # Application: # Generate pdf files ready for print-on-demand service. # # Background: # Some printers require images in the CMYK color space. # Others can accept images in the sRGB color space. # Most will specify a minimum resolution (e.g. 300 dpi). # # Cameras commonly produce pictures specified in the RGB color space. # Image pixel density is affected by the original picture pixel count, # cropping in photoshop and resizing in the typesetting program. # So images may require three adjustments: # 1. color space (RGB to CMYK or grayscale) # 2. file format (PNG or TIFF to JPG) # 3. resolution (various fixed pixel counts to at least 300 dpi) # This program runs in two modes: # # 1. Single File Info # Report image file properties: color mode (CMYK, RGB, L) and size # # 2. Convert # Process a single image (LaTex independent) # a. Convert image to specified color space (CMYK, SRGB) # b. Scale image to target dpi (default 300) # c. Convert file type to jpeg # # For instructions on usage and list of available color profiles, install python and run: # python prepare_images_for_print.py class ImageConverter: def __init__(self, debug): self.active_srgb_profile_file = '' self.active_cmyk_profile_file = '' self.supported_units = ['in', 'cm', 'mm', 'pt'] def convert_color_space_to_cmyk(self, sourceImage, input_color_profile, output_color_profile): # from: https://stackoverflow.com/questions/43817854/how-to-create-a-cmyk-image-in-python # adjusted_image = ImageCms.profileToProfile( # source_image, # 'USWebCoatedSWOP.icc', # 'sRGB Color Space Profile.icm', # renderingIntent=0, outputMode='RGB') working_image = sourceImage.copy() if working_image.mode == 'CMYK': return working_image if working_image.mode == 'L': # grayscale return working_image if working_image.mode == 'RGB': # rendering intent # ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) # ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 # ImageCms.Intent.SATURATION = 2 # ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 profile_to_profile = ImageCms.profileToProfile( working_image, input_color_profile, #'sRGB Color Space Profile.icm', output_color_profile, #'GRACoL2006_Coated1_GCR_bas.icc', renderingIntent=1, outputMode='CMYK') cmykImage = profile_to_profile.convert('CMYK') cmykImage.mode = 'CMYK' cmykImage.format = 'JPEG' return cmykImage return None def convert_color_space_to_srgb(self, sourceImage, input_color_profile, output_color_profile): # from: https://stackoverflow.com/questions/43817854/how-to-create-a-cmyk-image-in-python # adjusted_image = ImageCms.profileToProfile( # source_image, # 'USWebCoatedSWOP.icc', # 'sRGB Color Space Profile.icm', # renderingIntent=0, outputMode='RGB') working_image = sourceImage.copy() if sourceImage.mode == 'RGB': working_image.mode = 'RGB' working_image.format = 'JPEG' return working_image if sourceImage.mode == 'L': # grayscale working_image.mode = 'L' working_image.format = 'JPEG' return working_image if sourceImage.mode == 'CMYK': # rendering intent # ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) # ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 # ImageCms.Intent.SATURATION = 2 # ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 profile_to_profile = ImageCms.profileToProfile( working_image, input_color_profile, #'GRACoL2006_Coated1_GCR_bas.icc', output_color_profile, #'sRGB Color Space Profile.icm', renderingIntent=1, outputMode='RGB') cmykImage = profile_to_profile.convert('RGB') cmykImage.mode = 'RGB' cmykImage.format = 'JPEG' return cmykImage return None def convert_color_space_to_gray(self, source_image): gray_scale_image = ImageOps.grayscale(source_image) gray_scale_image.mode = 'L' # 8 bits per pixel, grayscale gray_scale_image.format = 'JPEG' return gray_scale_image def process_task(self, task): # task = { # 'width' or 'height': dimension_value, # 'units' : units_string, # 'input_image_file_name' : input_file_name, # 'output_image_file_name' : output_image_file_name, # 'output_color_model': output_color_model, # 'output_color_profile': output_color_profile, # 'target_image_resolution_dpi': target_image_resolution_dpi, # 'preview_only': boolean # 'draft_mode': boolean # } preview_only = task['preview_only'] source_image = Image.open(task['input_image_file_name']) if source_image is None: print(' Error reading, ' + task['input_image_file_name']); return line = ' Input : ' + source_image.format + ', ' + source_image.mode if source_image.size is not None: line += ', ' + str(source_image.size[0]) + 'x' + str(source_image.size[1]) line += ', ' + os.path.basename(task['input_image_file_name']) print(line) # 1. Adjust the color if source_image.mode == 'RGB': input_color_profile = self.active_srgb_profile_file if len(input_color_profile) == 0: print(' Missing required input RGB color profile for ' + task['input_image_file_name']); return elif source_image.mode == 'CMYK': input_color_profile = self.active_cmyk_profile_file if len(input_color_profile) == 0: print(' Missing required input CMYK color profile for ' + task['input_image_file_name']); return elif source_image.mode == 'L': input_color_profile = '' elif source_image.mode == 'RGBA': # remove transparency source_image = source_image.convert('RGB') input_color_profile = self.active_srgb_profile_file else: print(' Unsupported input color mode, ' + source_image.mode + ' in ' + task['input_image_file_name']); return working_image = None if task['output_color_model'] == 'gray': working_image = self.convert_color_space_to_gray(source_image) elif task['output_color_model'] == 'cmyk': working_image = self.convert_color_space_to_cmyk( source_image, input_color_profile, task['output_color_profile']) elif task['output_color_model'] == 'srgb': working_image = self.convert_color_space_to_srgb( source_image, input_color_profile, task['output_color_profile']) else: return if working_image is None: print(' Error in color conversion, in ' + task['input_image_file_name']); return # 2. Resize Image if ('width' in task): target_width = task['width'] elif ('height' in task): target_height = task['height'] target_width = target_height * working_image.width / working_image.height # convert to inches since we will be targetting dpi units_string = task['units'] if units_string == 'mm': target_width_in_inches = target_width / 25.4 elif units_string == 'cm': target_width_in_inches = target_width / 2.54 elif units_string == 'pt': target_width_in_inches = target_width / 249.0 / 250 / 864 elif units_string == 'in': target_width_in_inches = target_width else: print('Error - unsupported units: ' + units_string) return target_dpi = task['target_image_resolution_dpi'] target_width_in_pixels = int(target_width_in_inches * target_dpi) # maintain aspect ratio target_height_in_pixels = int(target_width_in_pixels * working_image.height / working_image.width) if task['draft_mode']: # For reduced file size, # rescale so largest dimension = 75 pixels if target_height_in_pixels < target_width_in_pixels: larger_dimension_in_pixels = target_width_in_pixels else: larger_dimension_in_pixels = target_height_in_pixels scale = 75 / larger_dimension_in_pixels target_width_in_pixels = int(scale * target_width_in_pixels) target_height_in_pixels = int(scale * target_height_in_pixels) resized_image = working_image.resize((target_width_in_pixels, target_height_in_pixels)) resized_image.format = 'JPEG' resized_image.mode = working_image.mode #'RGB' # 3. save the new file output_file_name = task['output_image_file_name'] if len(output_file_name) > 0: # create output folder if needed output_folder = os.path.dirname(output_file_name) if not preview_only: if not os.path.isdir(output_folder): os.makedirs(output_folder) line = ' Output: ' + resized_image.format + ', ' + resized_image.mode if resized_image.size is not None: line += ', ' + str(resized_image.size[0]) + 'x' + str(resized_image.size[1]) line += ', ' + os.path.basename(output_file_name) print(line) if not preview_only: if (os.path.isfile(output_file_name)): os.remove(output_file_name) resized_image.save( output_file_name, format='JPEG', quality=100, dpi=(target_dpi, target_dpi)) # saves a sRGB-elle-V2-srgbtrc.icc(default)? def run_in_manual_mode(self, manual_mode_parameters): # Process a single image file units_string = manual_mode_parameters['units'] if units_string is None: print('Error: Missing Units') return False if units_string not in self.supported_units: print('Error: Unsupported units, ' + units_string) return False dimension_name = manual_mode_parameters['dimension_name'] if dimension_name is None: print('Error: Missing dimension_name, ' + dimension_name) return False if dimension_name not in ['height', 'width']: print('Error: Invalid Dimension, ' + str(dimension_name)) return False dimension_value = manual_mode_parameters['dimension_value'] if dimension_value <= 0: print('Error: Invalid ' + dimension_name + ', ' + str(dimension_value)) return False input_file_name = manual_mode_parameters['input_file_name'] if input_file_name is None: print('Error: Undefined input_file_name') return False if len(input_file_name) == 0: print('Error: Missing input_file_name') return False if (not input_file_name.lower().endswith('.jpg')) and (not input_file_name.lower().endswith('.png')): print('Error: unsupported input_file_name type (expected jpg or png): ' + input_file_name) return False # construct output file name from input file name dot_index = input_file_name.find('.') if manual_mode_parameters['output_color_model'] == 'srgb': output_image_file_name = input_file_name[0:dot_index] + '_srgb.jpg' if manual_mode_parameters['output_color_profile'] == '': print('Error: Missing output color profile') return False elif manual_mode_parameters['output_color_model'] == 'cmyk': output_image_file_name = input_file_name[0:dot_index] + '_cmyk.jpg' if manual_mode_parameters['output_color_profile'] == '': print('Error: Missing output color profile') return False elif manual_mode_parameters['output_color_model'] == 'gray': output_image_file_name = input_file_name[0:dot_index] + '_gray.jpg' else: print('Error: Unsupported output_color_model: ' + manual_mode_parameters['output_color_model']) return False task = { dimension_name: dimension_value, # 'height': 1.2 'units' : units_string, # e.g. in 'input_image_file_name' : input_file_name, 'output_image_file_name' : output_image_file_name, 'output_color_model': manual_mode_parameters['output_color_model'], 'output_color_profile': manual_mode_parameters['output_color_profile'], 'target_image_resolution_dpi': manual_mode_parameters['target_image_resolution_dpi'], 'preview_only': manual_mode_parameters['preview_only'], 'draft_mode': manual_mode_parameters['draft_mode'] } self.process_task(task) return True def report_available_color_profiles(self): # report available color profiles available_icc_profiles = self.get_available_icc_profiles() print('') if len(available_icc_profiles) == 0: print('no icc profiles available') else: print('available icc profiles') for icc_profile_name in available_icc_profiles: print(' icc_profile_name: ' + icc_profile_name) print('') def report_image_info(self, image_file_name): if not os.path.isfile(image_file_name): print('image_file_name not found: ' + image_file_name) else: image = Image.open(image_file_name) print('Filename: ', image.filename) print('Format: ', image.format) print('Mode: ', image.mode) #print('Size: ', image.size) print('Width: ', image.width) print('Height: ', image.height) return True def get_available_icc_profiles(self): # Color profiles are stored in *.icc or *.icm files in same folder as this python porgram # Only one color and one srgb profile should be included. available_icc_profiles = [] folder_contents = os.listdir() # returns files and folders for item_in_folder in folder_contents: if item_in_folder.lower().endswith('.icc'): available_icc_profiles.append(item_in_folder) self.active_cmyk_profile_file = item_in_folder elif item_in_folder.lower().endswith('.icm'): available_icc_profiles.append(item_in_folder) if 'srgb' in item_in_folder.lower(): self.active_srgb_profile_file = item_in_folder return available_icc_profiles # end class #======================================================= # main #======================================================= if __name__ == '__main__': print('prepare_images_for_print.py') default_output_image_resolution_dpi = 300 batch_mode_parameters = { 'preview_only' : False, 'input_folder_name' : '', 'input_graphics_path' : '', 'output_graphics_path' : '', 'max_image_file_count' : 3, # default small to avoid unintentional long running ops 'listTexFiles': True, 'output_color_model': 'gray', # srgb, cmyk, gray 'output_color_profile': '', # name of available icc file name 'target_image_resolution_dpi' : default_output_image_resolution_dpi, 'draft_mode': False } manual_mode_parameters = { 'preview_only' : False, 'input_file_name' : '', 'output_file_name' : '', 'dimension_name' : 'height', # 'height' or 'width' 'dimension_value' : 0, # numerical value 'units' : 'in', 'output_color_model': 'gray', # srgb, cmyk, gray 'output_color_profile': '', # name of available icc file name 'target_image_resolution_dpi' : default_output_image_resolution_dpi, 'draft_mode': False } mode = '' # manual, batch or get_info imageConverter = ImageConverter(False) imageConverter.report_available_color_profiles() argc = len(sys.argv) for i in range(argc): parameter_name = sys.argv[i] if not parameter_name.startswith('-'): continue if not parameter_name.startswith('--'): parameter_value = sys.argv[i+1] # manual mode parameters if parameter_name == '-input_file_name': manual_mode_parameters['input_file_name'] = parameter_value mode = 'manual' elif parameter_name.lower() == '-width': manual_mode_parameters['dimension_name'] = 'width' manual_mode_parameters['dimension_value'] = float(parameter_value) elif parameter_name.lower() == '-height': manual_mode_parameters['dimension_name'] = 'height' manual_mode_parameters['dimension_value'] = float(parameter_value) elif parameter_name == '-units': argString = sys.argv[i+1] if (argString is not None): argString = argString.lower(); if (argString == 'in') or (argString == 'inch'): manual_mode_parameters['units'] = 'in' # Need to specify one of --output_srgb, --output_cmyk, output_gray elif parameter_name == '--output_srgb': batch_mode_parameters['output_color_model'] = 'srgb' manual_mode_parameters['output_color_model'] = 'srgb' batch_mode_parameters['output_color_profile'] = imageConverter.active_srgb_profile_file manual_mode_parameters['output_color_profile'] = imageConverter.active_srgb_profile_file print(imageConverter.active_cmyk_profile_file) elif parameter_name == '--output_cmyk': batch_mode_parameters['output_color_model'] = 'cmyk' manual_mode_parameters['output_color_model'] = 'cmyk' batch_mode_parameters['output_color_profile'] = imageConverter.active_cmyk_profile_file manual_mode_parameters['output_color_profile'] = imageConverter.active_cmyk_profile_file print(imageConverter.active_cmyk_profile_file) elif parameter_name == '--output_gray': batch_mode_parameters['output_color_model'] = 'gray' manual_mode_parameters['output_color_model'] = 'gray' # optional output dot per inch resolution (defaults to 300 dpi) elif parameter_name == '-dpi': dpi = int(parameter_value) batch_mode_parameters['target_image_resolution_dpi'] = dpi manual_mode_parameters['target_image_resolution_dpi'] = dpi # misc parameters elif parameter_name == '--draft': batch_mode_parameters['draft_mode'] = True manual_mode_parameters['draft_mode'] = True elif parameter_name == '--preview': batch_mode_parameters['preview_only'] = True manual_mode_parameters['preview_only'] = True #get information on image file elif parameter_name == '-get_info': manual_mode_parameters['input_file_name'] = parameter_value mode = 'get_info' if (mode == 'manual'): status = imageConverter.run_in_manual_mode(manual_mode_parameters) elif (mode == 'batch'): status = imageConverter.run_in_batch_mode(batch_mode_parameters) elif (mode == 'get_info'): image_file_name = manual_mode_parameters['input_file_name'] status = imageConverter.report_image_info(image_file_name) else: status = False if not status: print('') print('convert_image.py') print(' Convert image type (jpg and png), color model (rgb, cmyk), and resolution') print('') print('Usage 1') print(' python convert_image.py -get_info input_file_name') print(' Report image file properties: color mode (CMYK, RGB, L) and size') print('') print('Usage 2') print(' python convert_image.py -input_file_name input_file_name -width w ') print(' output_file_name defaults to [input_file_name]_cmyk') print(' Option:') print(' -height h alternate dimension specification') print(' -units cm alternate dimension units: in, cm, mm, pt') print(' --output_cmyk use sRGB color model for output images') print(' --output_srgb use sRGB color model for output images') print(' --output_gray use grayscale color model for output images') print(' [At least one of the three color models must be selected]') print('') print('Program Assumptions:') print(' 1. Output images are in jpg format') print(' 2. Color profiles are in same folder as this pyython program.') print(' Only one *.icc file for CMYK and one *.icm file for sRGB') print('') print('For instructions on usage and list of available color profiles, install python and run:') print(' python prepare_images_for_print.py') print('') print('')