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>
This commit is contained in:
401
venv/lib/python3.12/site-packages/xlsxwriter/image.py
Normal file
401
venv/lib/python3.12/site-packages/xlsxwriter/image.py
Normal file
@@ -0,0 +1,401 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
Reference in New Issue
Block a user