Files
bussines_case_automation/venv/lib/python3.12/site-packages/xlsxwriter/image.py
andrei 0e2e1bddba Add xlsxwriter-based Excel generation scripts with openpyxl implementation
- Created create_excel_xlsxwriter.py and update_excel_xlsxwriter.py
- Uses openpyxl exclusively to preserve Excel formatting and formulas
- Updated server.js to use new xlsxwriter scripts for form submissions
- Maintains all original functionality while ensuring proper Excel file handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 13:53:06 +00:00

402 lines
13 KiB
Python

###############################################################################
#
# Image - A class for representing image objects in Excel.
#
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
#
import hashlib
import os
from io import BytesIO
from pathlib import Path
from struct import unpack
from typing import Tuple, Union
from xlsxwriter.url import Url
from .exceptions import UndefinedImageSize, UnsupportedImageFormat
DEFAULT_DPI = 96.0
class Image:
"""
A class to represent an image in an Excel worksheet.
"""
def __init__(self, source: Union[str, Path, BytesIO]) -> None:
"""
Initialize an Image instance.
Args:
source (Union[str, Path, BytesIO]): The filename, Path or BytesIO
object of the image.
"""
if isinstance(source, (str, Path)):
self.filename = source
self.image_data = None
self.image_name = os.path.basename(source)
elif isinstance(source, BytesIO):
self.filename = ""
self.image_data = source
self.image_name = ""
else:
raise ValueError("Source must be a filename (str) or a BytesIO object.")
self._row: int = 0
self._col: int = 0
self._x_offset: int = 0
self._y_offset: int = 0
self._x_scale: float = 1.0
self._y_scale: float = 1.0
self._url: Union[Url, None] = None
self._anchor: int = 2
self._description: Union[str, None] = None
self._decorative: bool = False
self._header_position: Union[str, None] = None
self._ref_id: Union[str, None] = None
# Derived properties.
self._image_extension: str = ""
self._width: float = 0.0
self._height: float = 0.0
self._x_dpi: float = DEFAULT_DPI
self._y_dpi: float = DEFAULT_DPI
self._digest: Union[str, None] = None
self._get_image_properties()
def __repr__(self) -> str:
"""
Return a string representation of the main properties of the Image
instance.
"""
return (
f"Image:\n"
f" filename = {self.filename!r}\n"
f" image_name = {self.image_name!r}\n"
f" image_type = {self.image_type!r}\n"
f" width = {self._width}\n"
f" height = {self._height}\n"
f" x_dpi = {self._x_dpi}\n"
f" y_dpi = {self._y_dpi}\n"
)
@property
def image_type(self) -> str:
"""Get the image type (e.g., 'PNG', 'JPEG')."""
return self._image_extension.upper()
@property
def width(self) -> float:
"""Get the width of the image."""
return self._width
@property
def height(self) -> float:
"""Get the height of the image."""
return self._height
@property
def x_dpi(self) -> float:
"""Get the horizontal DPI of the image."""
return self._x_dpi
@property
def y_dpi(self) -> float:
"""Get the vertical DPI of the image."""
return self._y_dpi
@property
def description(self) -> Union[str, None]:
"""Get the description/alt-text of the image."""
return self._description
@description.setter
def description(self, value: str) -> None:
"""Set the description/alt-text of the image."""
if value:
self._description = value
@property
def decorative(self) -> bool:
"""Get whether the image is decorative."""
return self._decorative
@decorative.setter
def decorative(self, value: bool) -> None:
"""Set whether the image is decorative."""
self._decorative = value
@property
def url(self) -> Union[Url, None]:
"""Get the image url."""
return self._url
@url.setter
def url(self, value: Url) -> None:
"""Set the image url."""
if value:
self._url = value
def _set_user_options(self, options=None) -> None:
"""
This handles the additional optional parameters to ``insert_button()``.
"""
if options is None:
return
if not self._url:
self._url = Url.from_options(options)
if self._url:
self._url._set_object_link()
self._anchor = options.get("object_position", self._anchor)
self._x_scale = options.get("x_scale", self._x_scale)
self._y_scale = options.get("y_scale", self._y_scale)
self._x_offset = options.get("x_offset", self._x_offset)
self._y_offset = options.get("y_offset", self._y_offset)
self._decorative = options.get("decorative", self._decorative)
self.image_data = options.get("image_data", self.image_data)
self._description = options.get("description", self._description)
# For backward compatibility with older parameter name.
self._anchor = options.get("positioning", self._anchor)
def _get_image_properties(self) -> None:
# Extract dimension information from the image file.
height = 0.0
width = 0.0
x_dpi = DEFAULT_DPI
y_dpi = DEFAULT_DPI
if self.image_data:
# Read the image data from the user supplied byte stream.
data = self.image_data.getvalue()
else:
# Open the image file and read in the data.
with open(self.filename, "rb") as fh:
data = fh.read()
# Get the image digest to check for duplicates.
digest = hashlib.sha256(data).hexdigest()
# Look for some common image file markers.
png_marker = unpack("3s", data[1:4])[0]
jpg_marker = unpack(">H", data[:2])[0]
bmp_marker = unpack("2s", data[:2])[0]
gif_marker = unpack("4s", data[:4])[0]
emf_marker = (unpack("4s", data[40:44]))[0]
emf_marker1 = unpack("<L", data[:4])[0]
if png_marker == b"PNG":
(image_type, width, height, x_dpi, y_dpi) = self._process_png(data)
elif jpg_marker == 0xFFD8:
(image_type, width, height, x_dpi, y_dpi) = self._process_jpg(data)
elif bmp_marker == b"BM":
(image_type, width, height) = self._process_bmp(data)
elif emf_marker1 == 0x9AC6CDD7:
(image_type, width, height, x_dpi, y_dpi) = self._process_wmf(data)
elif emf_marker1 == 1 and emf_marker == b" EMF":
(image_type, width, height, x_dpi, y_dpi) = self._process_emf(data)
elif gif_marker == b"GIF8":
(image_type, width, height, x_dpi, y_dpi) = self._process_gif(data)
else:
raise UnsupportedImageFormat(
f"{self.filename}: Unknown or unsupported image file format."
)
# Check that we found the required data.
if not height or not width:
raise UndefinedImageSize(
f"{self.filename}: no size data found in image file."
)
# Set a default dpi for images with 0 dpi.
if x_dpi == 0:
x_dpi = DEFAULT_DPI
if y_dpi == 0:
y_dpi = DEFAULT_DPI
self._image_extension = image_type
self._width = width
self._height = height
self._x_dpi = x_dpi
self._y_dpi = y_dpi
self._digest = digest
def _process_png(
self,
data: bytes,
) -> Tuple[str, float, float, float, float]:
# Extract width and height information from a PNG file.
offset = 8
data_length = len(data)
end_marker = False
width = 0.0
height = 0.0
x_dpi = DEFAULT_DPI
y_dpi = DEFAULT_DPI
# Search through the image data to read the height and width in the
# IHDR element. Also read the DPI in the pHYs element.
while not end_marker and offset < data_length:
length = unpack(">I", data[offset + 0 : offset + 4])[0]
marker = unpack("4s", data[offset + 4 : offset + 8])[0]
# Read the image dimensions.
if marker == b"IHDR":
width = unpack(">I", data[offset + 8 : offset + 12])[0]
height = unpack(">I", data[offset + 12 : offset + 16])[0]
# Read the image DPI.
if marker == b"pHYs":
x_density = unpack(">I", data[offset + 8 : offset + 12])[0]
y_density = unpack(">I", data[offset + 12 : offset + 16])[0]
units = unpack("b", data[offset + 16 : offset + 17])[0]
if units == 1 and x_density > 0 and y_density > 0:
x_dpi = x_density * 0.0254
y_dpi = y_density * 0.0254
if marker == b"IEND":
end_marker = True
continue
offset = offset + length + 12
return "png", width, height, x_dpi, y_dpi
def _process_jpg(self, data: bytes) -> Tuple[str, float, float, float, float]:
# Extract width and height information from a JPEG file.
offset = 2
data_length = len(data)
end_marker = False
width = 0.0
height = 0.0
x_dpi = DEFAULT_DPI
y_dpi = DEFAULT_DPI
# Search through the image data to read the JPEG markers.
while not end_marker and offset < data_length:
marker = unpack(">H", data[offset + 0 : offset + 2])[0]
length = unpack(">H", data[offset + 2 : offset + 4])[0]
# Read the height and width in the 0xFFCn elements (except C4, C8
# and CC which aren't SOF markers).
if (
(marker & 0xFFF0) == 0xFFC0
and marker != 0xFFC4
and marker != 0xFFC8
and marker != 0xFFCC
):
height = unpack(">H", data[offset + 5 : offset + 7])[0]
width = unpack(">H", data[offset + 7 : offset + 9])[0]
# Read the DPI in the 0xFFE0 element.
if marker == 0xFFE0:
units = unpack("b", data[offset + 11 : offset + 12])[0]
x_density = unpack(">H", data[offset + 12 : offset + 14])[0]
y_density = unpack(">H", data[offset + 14 : offset + 16])[0]
if units == 1:
x_dpi = x_density
y_dpi = y_density
if units == 2:
x_dpi = x_density * 2.54
y_dpi = y_density * 2.54
# Workaround for incorrect dpi.
if x_dpi == 1:
x_dpi = DEFAULT_DPI
if y_dpi == 1:
y_dpi = DEFAULT_DPI
if marker == 0xFFDA:
end_marker = True
continue
offset = offset + length + 2
return "jpeg", width, height, x_dpi, y_dpi
def _process_gif(self, data: bytes) -> Tuple[str, float, float, float, float]:
# Extract width and height information from a GIF file.
x_dpi = DEFAULT_DPI
y_dpi = DEFAULT_DPI
width = unpack("<h", data[6:8])[0]
height = unpack("<h", data[8:10])[0]
return "gif", width, height, x_dpi, y_dpi
def _process_bmp(self, data: bytes) -> Tuple[str, float, float]:
# Extract width and height information from a BMP file.
width = unpack("<L", data[18:22])[0]
height = unpack("<L", data[22:26])[0]
return "bmp", width, height
def _process_wmf(self, data: bytes) -> Tuple[str, float, float, float, float]:
# Extract width and height information from a WMF file.
x_dpi = DEFAULT_DPI
y_dpi = DEFAULT_DPI
# Read the bounding box, measured in logical units.
x1 = unpack("<h", data[6:8])[0]
y1 = unpack("<h", data[8:10])[0]
x2 = unpack("<h", data[10:12])[0]
y2 = unpack("<h", data[12:14])[0]
# Read the number of logical units per inch. Used to scale the image.
inch = unpack("<H", data[14:16])[0]
# Convert to rendered height and width.
width = float((x2 - x1) * x_dpi) / inch
height = float((y2 - y1) * y_dpi) / inch
return "wmf", width, height, x_dpi, y_dpi
def _process_emf(self, data: bytes) -> Tuple[str, float, float, float, float]:
# Extract width and height information from a EMF file.
# Read the bounding box, measured in logical units.
bound_x1 = unpack("<l", data[8:12])[0]
bound_y1 = unpack("<l", data[12:16])[0]
bound_x2 = unpack("<l", data[16:20])[0]
bound_y2 = unpack("<l", data[20:24])[0]
# Convert the bounds to width and height.
width = bound_x2 - bound_x1
height = bound_y2 - bound_y1
# Read the rectangular frame in units of 0.01mm.
frame_x1 = unpack("<l", data[24:28])[0]
frame_y1 = unpack("<l", data[28:32])[0]
frame_x2 = unpack("<l", data[32:36])[0]
frame_y2 = unpack("<l", data[36:40])[0]
# Convert the frame bounds to mm width and height.
width_mm = 0.01 * (frame_x2 - frame_x1)
height_mm = 0.01 * (frame_y2 - frame_y1)
# Get the dpi based on the logical size.
x_dpi = width * 25.4 / width_mm
y_dpi = height * 25.4 / height_mm
# This is to match Excel's calculation. It is probably to account for
# the fact that the bounding box is inclusive-inclusive. Or a bug.
width += 1
height += 1
return "emf", width, height, x_dpi, y_dpi