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
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
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