Replies: 2 comments
-
According to this page:
I can add it anyways though. |
Beta Was this translation helpful? Give feedback.
0 replies
-
Hi, I thought your function was super useful, and ended up adapting it to handle multiple pages. Here's the code I came up with - hope it may also come in handy to you or others. import io
import sys
from PIL import Image, JpegImagePlugin, TiffTags
class R:
"""
Simple RATIONAL type for representing fractional values in TIFF tags.
"""
def __init__(self, numerator, denominator=1):
self.numerator = numerator
self.denominator = denominator
def to_bytes(self, length, byteorder):
return self.numerator.to_bytes(4, byteorder) + self.denominator.to_bytes(
4, byteorder
)
def jpeg_to_tiff(jpeg_filenames):
"""
Converts a list of JPEG images to a multi-page TIFF format.
Returns the bytes to be written to a TIFF file.
"""
if not isinstance(jpeg_filenames, list):
raise ValueError("jpeg_filenames must be a list of file names.")
byteorder = sys.byteorder
final_tiff_data = b""
long_values_data = b""
long_tag_offsets_to_update = []
next_ifd_offsets = []
ifd_start_positions = []
tiff_header_offset = 8
for jpeg_filename in jpeg_filenames:
with open(jpeg_filename, "rb") as jpeg_file:
jpeg_bytes = jpeg_file.read()
jpeg_bytes_io = io.BytesIO(jpeg_bytes)
with Image.open(jpeg_bytes_io, formats=("JPEG",)) as jpeg_image:
# Ensure the image is in JPEG format
if jpeg_image.format != "JPEG":
raise ValueError("The given image must be a JPEG.")
# Prepare the tags for the Image File Directory (IFD)
# This section includes tag preparation and calculations for TIFF metadata
image_width = jpeg_image.width
image_height = jpeg_image.height
if jpeg_image.mode == "L":
photometric_interpretation = 1
samples_per_pixel = 1
elif jpeg_image.mode == "RGB":
if "jfif" in jpeg_image.info:
photometric_interpretation = 6
else:
if jpeg_image.info.get("adobe_transform", 1) == 1:
photometric_interpretation = 6
else:
photometric_interpretation = 2
samples_per_pixel = 3
elif jpeg_image.mode == "CMYK":
if jpeg_image.info.get("adobe_transform", 0) == 2:
raise ValueError("TIFF does not support YCCK JPEG images.")
else:
photometric_interpretation = 5
samples_per_pixel = 4
elif jpeg_image.mode == "YCbCr":
photometric_interpretation = 6
samples_per_pixel = 3
else:
raise ValueError(
"The given image does not use a supported mode.\n"
" Expected: L, RGB, CMYK, YCbCr\n"
" Received: " + jpeg_image.mode
)
# Resolution Units:
# 1 None
# 2 Inches
# 3 Centimeters
if "jfif_density" in jpeg_image.info:
jfif_density = jpeg_image.info["jfif_density"]
x_resolution = round(jfif_density[0])
y_resolution = round(jfif_density[1])
if "jfif_unit" in jpeg_image.info:
# JFIF unit IDs are one off from TIFF resolution unit IDs.
resolution_unit = jpeg_image.info["jfif_unit"] + 1
else:
resolution_unit = 2
elif "dpi" in jpeg_image.info:
dpi = jpeg_image.info["dpi"]
x_resolution = round(dpi[0])
y_resolution = round(dpi[1])
resolution_unit = 2
else:
x_resolution = 72
y_resolution = 72
resolution_unit = 2
tags = (
# name, ID, value type, number of values, value
("NewSubfileType", 254, TiffTags.LONG, 1, 0),
("ImageWidth", 256, TiffTags.LONG, 1, image_width),
("ImageLength", 257, TiffTags.LONG, 1, image_height),
("BitsPerSample", 258, TiffTags.SHORT, samples_per_pixel, 8),
("Compression", 259, TiffTags.SHORT, 1, 7), # New JPEG
(
"PhotometricInterpretation",
262,
TiffTags.SHORT,
1,
photometric_interpretation,
),
(
"StripOffsets",
273,
TiffTags.LONG,
1,
0,
), # offset to start of image data
("SamplesPerPixel", 277, TiffTags.SHORT, 1, samples_per_pixel),
("RowsPerStrip", 278, TiffTags.LONG, 1, image_height),
("StripByteCounts", 279, TiffTags.LONG, 1, len(jpeg_bytes)),
("XResolution", 282, TiffTags.RATIONAL, 1, R(x_resolution)),
("YResolution", 283, TiffTags.RATIONAL, 1, R(y_resolution)),
("PlanarConfiguration", 284, TiffTags.SHORT, 1, 1), # Chunky
("ResolutionUnit", 296, TiffTags.SHORT, 1, resolution_unit),
)
# YCbCr
if photometric_interpretation == 6:
# 1 = Center, 2 = Cosited
# recommended to use 2 for 4:2:2, 1 otherwise
# http://web.archive.org/web/20220428165430/http://exif.org/Exif2-2.PDF
ycbcr_positioning = 1
jpeg_subsampling = JpegImagePlugin.get_sampling(jpeg_image)
if jpeg_subsampling == 0:
# 4:4:4
tiff_subsampling = (1, 1)
elif jpeg_subsampling == 1:
# 4:2:2
tiff_subsampling = (2, 1)
ycbcr_positioning = 2
elif jpeg_subsampling == 2:
# 4:2:0
tiff_subsampling = (2, 2)
else:
# 4:2:0
tiff_subsampling = (2, 2)
tags += (
(
"YCbCrCoefficients",
529,
TiffTags.RATIONAL,
3,
(R(299, 1000), R(587, 1000), R(114, 1000)),
),
("YCbCrSubSampling", 530, TiffTags.SHORT, 2, tiff_subsampling),
("YCbCrPositioning", 531, TiffTags.SHORT, 1, ycbcr_positioning),
# min pixel value, max pixel value
(
"ReferenceBlackWhite",
532,
TiffTags.RATIONAL,
samples_per_pixel * 2,
(R(0), R(255), R(0), R(255), R(0), R(255)),
),
)
# Build and append the IFD data for the current JPEG image
ifd_data, long_tag_offsets = build_ifd(
tags, 0xFFFFFFFF, len(jpeg_bytes), byteorder
)
ifd_start = len(final_tiff_data) + tiff_header_offset
ifd_start_positions.append(ifd_start)
final_tiff_data += ifd_data
final_tiff_data += b"\x00\x00\x00\x00" # Adding 4 bytes for the next IFD offset
# Calculate the start of the next IFD
next_ifd_start = len(final_tiff_data) + tiff_header_offset
next_ifd_offsets.append(next_ifd_start)
# Update the placeholder positions for long tag values
for placeholder_pos, long_value in long_tag_offsets:
adjusted_placeholder_pos = ifd_start + placeholder_pos
long_tag_offsets_to_update.append((adjusted_placeholder_pos, long_value))
# Update the offsets for the next IFDs
for i, offset in enumerate(next_ifd_offsets[:-1]):
num_tags = len(tags)
next_ifd_position = (
ifd_start_positions[i] + 2 + (num_tags * 12) - tiff_header_offset
)
next_ifd_value = next_ifd_offsets[i]
final_tiff_data = (
final_tiff_data[:next_ifd_position]
+ next_ifd_value.to_bytes(4, byteorder)
+ final_tiff_data[next_ifd_position + 4 :]
)
# Calculate and update offsets for long tag values
current_offset = len(final_tiff_data)
for placeholder_pos, long_value in long_tag_offsets_to_update:
placeholder_pos -= tiff_header_offset
actual_offset = current_offset + len(long_values_data)
long_values_data += long_value.to_bytes(8, byteorder)
final_tiff_data = (
final_tiff_data[:placeholder_pos]
+ actual_offset.to_bytes(4, byteorder)
+ final_tiff_data[placeholder_pos + 4 :]
)
final_tiff_data += (0).to_bytes(4, byteorder)
final_tiff_data += long_values_data
# Append image data and update StripOffsets
tiff_data = b""
image_data_offset = len(final_tiff_data) + tiff_header_offset
for i, (jpeg_filename, ifd_start) in enumerate(
zip(jpeg_filenames, ifd_start_positions)
):
with open(jpeg_filename, "rb") as jpeg_file:
jpeg_data = jpeg_file.read()
tiff_data += jpeg_data
# Update the StripOffsets in the IFD
strip_offset_pos_in_ifd = 2 + (6 * 12)
strip_offset_pos = ifd_start + strip_offset_pos_in_ifd
final_tiff_data = (
final_tiff_data[:strip_offset_pos]
+ image_data_offset.to_bytes(4, byteorder)
+ final_tiff_data[strip_offset_pos + 4 :]
)
image_data_offset += len(jpeg_data)
final_tiff_data += tiff_data
# Finalize and return TIFF data
final_tiff_data = (
(b"II" if byteorder == "little" else b"MM")
+ (42).to_bytes(2, byteorder)
+ tiff_header_offset.to_bytes(4, byteorder)
+ final_tiff_data
)
return final_tiff_data
def build_ifd(tags, strip_offset, strip_byte_count, byteorder):
ifd_data = b""
long_tag_placeholders = []
ifd_data += len(tags).to_bytes(2, byteorder)
for name, id, value_type, num_values, value in tags:
if name == "StripOffsets":
value = (strip_offset,)
if name == "StripByteCounts":
value = (strip_byte_count,)
if value_type == TiffTags.SHORT:
value_byte_size = 2
elif value_type == TiffTags.LONG:
value_byte_size = 4
elif value_type == TiffTags.RATIONAL:
value_byte_size = 8
value_byte_total = value_byte_size * num_values
ifd_data += id.to_bytes(2, byteorder)
ifd_data += value_type.to_bytes(2, byteorder)
ifd_data += num_values.to_bytes(4, byteorder)
if hasattr(value, "__len__"):
values = value
else:
values = (value,) * num_values
if value_byte_total <= 4:
for value in values:
ifd_data += value.to_bytes(value_byte_size, byteorder)
ifd_data += (0).to_bytes(4 - value_byte_total, byteorder)
else:
placeholder_offset = 0xFFFFFFFF # Placeholder offset, to be replaced later
long_tag_placeholders.append((len(ifd_data), value))
ifd_data += placeholder_offset.to_bytes(4, byteorder)
return ifd_data, long_tag_placeholders
if __name__ == "__main__":
jpeg_filenames = [
"temp_frames/frame_0023_8bit.jpg",
"temp_frames/frame_0023_8bit.jpg",
"temp_frames/frame_0023_8bit.jpg",
# "temp_frames/frame_0023_8bit.jpg",
]
with open("embedded.tif", "wb") as tiff_file:
tiff_data = jpeg_to_tiff(jpeg_filenames)
tiff_file.write(tiff_data) |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
After seeing #7001 I thought there might be a way to embed a JPEG file into a TIFF file without reencoding the JPEG. I then found some C# code that does exactly that. I've now rewritten that C# code in Python using Pillow. I don't know if this is something that would be added to Pillow, but I figure I can at least share it here for other people to try.
edit 1: Added
YCbCrCoefficients
,YCbCrSubSampling
,YCbCrPositioning
, andReferenceBlackWhite
tags forYCbCr
images.edit 2: Fix
YCbCrPositioning
andReferenceBlackWhite
values.edit 3: Use the JFIF and Adobe APP markers when determining the "photometric interpretation" to use.
edit 4: Swap CMYK and YCCK detection.
Beta Was this translation helpful? Give feedback.
All reactions