mapillary-downloader 0.7.6__tar.gz → 0.7.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/PKG-INFO +1 -1
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/pyproject.toml +1 -1
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/exif_writer.py +2 -3
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/utils.py +4 -2
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/worker.py +5 -1
- mapillary_downloader-0.7.8/src/mapillary_downloader/xmp_writer.py +154 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/LICENSE.md +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/README.md +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/__init__.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/__main__.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/client.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/downloader.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/ia_check.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/ia_meta.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/ia_stats.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/logging_config.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/metadata_reader.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/tar_sequences.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/webp_converter.py +0 -0
- {mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/worker_pool.py +0 -0
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/exif_writer.py
RENAMED
|
@@ -72,9 +72,6 @@ def write_exif_to_image(image_path, metadata):
|
|
|
72
72
|
if "model" in metadata and metadata["model"]:
|
|
73
73
|
exif_dict["0th"][piexif.ImageIFD.Model] = metadata["model"].encode("utf-8")
|
|
74
74
|
|
|
75
|
-
if "exif_orientation" in metadata and metadata["exif_orientation"]:
|
|
76
|
-
exif_dict["0th"][piexif.ImageIFD.Orientation] = metadata["exif_orientation"]
|
|
77
|
-
|
|
78
75
|
if "width" in metadata and metadata["width"]:
|
|
79
76
|
exif_dict["0th"][piexif.ImageIFD.ImageWidth] = metadata["width"]
|
|
80
77
|
|
|
@@ -88,6 +85,8 @@ def write_exif_to_image(image_path, metadata):
|
|
|
88
85
|
exif_dict["0th"][piexif.ImageIFD.DateTime] = datetime_bytes
|
|
89
86
|
exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal] = datetime_bytes
|
|
90
87
|
exif_dict["Exif"][piexif.ExifIFD.DateTimeDigitized] = datetime_bytes
|
|
88
|
+
exif_dict["Exif"][piexif.ExifIFD.SubSecTimeOriginal] = ('000'+str(metadata["captured_at"] % 1000))[-3:]
|
|
89
|
+
exif_dict["Exif"][piexif.ExifIFD.SubSecTimeDigitized] = ('000'+str(metadata["captured_at"] % 1000))[-3:]
|
|
91
90
|
|
|
92
91
|
# GPS data - prefer computed_geometry over geometry
|
|
93
92
|
geometry = metadata.get("computed_geometry") or metadata.get("geometry")
|
|
@@ -77,7 +77,7 @@ def safe_json_save(file_path, data):
|
|
|
77
77
|
temp_file.replace(file_path)
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def http_get_with_retry(url, params=None, max_retries=5, base_delay=1.0, timeout=60):
|
|
80
|
+
def http_get_with_retry(url, params=None, max_retries=5, base_delay=1.0, timeout=60, session=None):
|
|
81
81
|
"""HTTP GET with exponential backoff retry.
|
|
82
82
|
|
|
83
83
|
Args:
|
|
@@ -86,6 +86,7 @@ def http_get_with_retry(url, params=None, max_retries=5, base_delay=1.0, timeout
|
|
|
86
86
|
max_retries: Maximum retry attempts (default: 5)
|
|
87
87
|
base_delay: Initial delay in seconds (default: 1.0)
|
|
88
88
|
timeout: Request timeout in seconds (default: 60)
|
|
89
|
+
session: Optional requests.Session for connection pooling
|
|
89
90
|
|
|
90
91
|
Returns:
|
|
91
92
|
requests.Response object
|
|
@@ -93,9 +94,10 @@ def http_get_with_retry(url, params=None, max_retries=5, base_delay=1.0, timeout
|
|
|
93
94
|
Raises:
|
|
94
95
|
requests.RequestException: If all retries exhausted
|
|
95
96
|
"""
|
|
97
|
+
getter = session or requests
|
|
96
98
|
for attempt in range(max_retries):
|
|
97
99
|
try:
|
|
98
|
-
response =
|
|
100
|
+
response = getter.get(url, params=params, timeout=timeout)
|
|
99
101
|
response.raise_for_status()
|
|
100
102
|
return response
|
|
101
103
|
except RequestException as e:
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/worker.py
RENAMED
|
@@ -7,6 +7,7 @@ from datetime import datetime
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import requests
|
|
9
9
|
from mapillary_downloader.exif_writer import write_exif_to_image
|
|
10
|
+
from mapillary_downloader.xmp_writer import write_xmp_to_image
|
|
10
11
|
from mapillary_downloader.webp_converter import convert_to_webp
|
|
11
12
|
from mapillary_downloader.utils import http_get_with_retry
|
|
12
13
|
|
|
@@ -105,7 +106,7 @@ def download_and_convert_image(image_data, output_dir, quality, convert_webp, se
|
|
|
105
106
|
|
|
106
107
|
try:
|
|
107
108
|
# Use retry logic with 3 attempts for image downloads
|
|
108
|
-
response = http_get_with_retry(image_url, max_retries=3, base_delay=1.0, timeout=60)
|
|
109
|
+
response = http_get_with_retry(image_url, max_retries=3, base_delay=1.0, timeout=60, session=session)
|
|
109
110
|
|
|
110
111
|
with open(jpg_path, "wb") as f:
|
|
111
112
|
for chunk in response.iter_content(chunk_size=8192):
|
|
@@ -117,6 +118,9 @@ def download_and_convert_image(image_data, output_dir, quality, convert_webp, se
|
|
|
117
118
|
# Write EXIF metadata
|
|
118
119
|
write_exif_to_image(jpg_path, image_data)
|
|
119
120
|
|
|
121
|
+
# Write XMP metadata for panoramas
|
|
122
|
+
write_xmp_to_image(jpg_path, image_data)
|
|
123
|
+
|
|
120
124
|
# Convert to WebP if requested
|
|
121
125
|
if convert_webp:
|
|
122
126
|
webp_path = convert_to_webp(jpg_path, output_path=final_path, delete_original=False)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""XMP metadata writer for panoramic Mapillary images."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger("mapillary_downloader")
|
|
6
|
+
|
|
7
|
+
# XMP namespace identifier for APP1 segment
|
|
8
|
+
XMP_NAMESPACE = b"http://ns.adobe.com/xap/1.0/\x00"
|
|
9
|
+
|
|
10
|
+
# XMP packet template for GPano metadata
|
|
11
|
+
XMP_TEMPLATE = """<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
|
12
|
+
<x:xmpmeta xmlns:x="adobe:ns:meta/">
|
|
13
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
14
|
+
<rdf:Description rdf:about=""
|
|
15
|
+
xmlns:GPano="http://ns.google.com/photos/1.0/panorama/"
|
|
16
|
+
GPano:ProjectionType="equirectangular"
|
|
17
|
+
GPano:UsePanoramaViewer="True"
|
|
18
|
+
GPano:FullPanoWidthPixels="{width}"
|
|
19
|
+
GPano:FullPanoHeightPixels="{height}"
|
|
20
|
+
GPano:CroppedAreaImageWidthPixels="{width}"
|
|
21
|
+
GPano:CroppedAreaImageHeightPixels="{height}"
|
|
22
|
+
GPano:CroppedAreaLeftPixels="0"
|
|
23
|
+
GPano:CroppedAreaTopPixels="0"{pose_heading}/>
|
|
24
|
+
</rdf:RDF>
|
|
25
|
+
</x:xmpmeta>
|
|
26
|
+
<?xpacket end="w"?>"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_xmp_packet(metadata):
|
|
30
|
+
"""Build XMP packet with GPano metadata.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
metadata: Dictionary with width, height, and optionally compass_angle
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
XMP XML string
|
|
37
|
+
"""
|
|
38
|
+
width = metadata.get("width", 0)
|
|
39
|
+
height = metadata.get("height", 0)
|
|
40
|
+
|
|
41
|
+
# Get compass angle (prefer computed)
|
|
42
|
+
compass = metadata.get("computed_compass_angle") or metadata.get("compass_angle")
|
|
43
|
+
|
|
44
|
+
# Build pose heading attribute if available
|
|
45
|
+
if compass is not None:
|
|
46
|
+
pose_heading = f'\n GPano:PoseHeadingDegrees="{compass:.1f}"'
|
|
47
|
+
else:
|
|
48
|
+
pose_heading = ""
|
|
49
|
+
|
|
50
|
+
return XMP_TEMPLATE.format(
|
|
51
|
+
width=width,
|
|
52
|
+
height=height,
|
|
53
|
+
pose_heading=pose_heading,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def write_xmp_to_image(image_path, metadata):
|
|
58
|
+
"""Write XMP GPano metadata to a JPEG image for panoramas.
|
|
59
|
+
|
|
60
|
+
Only writes metadata if is_pano is True in the metadata dict.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
image_path: Path to the JPEG image file
|
|
64
|
+
metadata: Dictionary of metadata from Mapillary API
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if XMP was written, False if skipped or failed
|
|
68
|
+
"""
|
|
69
|
+
# Only write XMP for panoramas
|
|
70
|
+
if not metadata.get("is_pano"):
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
# Need dimensions to write meaningful GPano data
|
|
74
|
+
if not metadata.get("width") or not metadata.get("height"):
|
|
75
|
+
logger.warning(f"Skipping XMP for {image_path}: missing dimensions")
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# Read the JPEG file
|
|
80
|
+
with open(image_path, "rb") as f:
|
|
81
|
+
data = f.read()
|
|
82
|
+
|
|
83
|
+
# Verify JPEG signature
|
|
84
|
+
if data[:2] != b"\xff\xd8":
|
|
85
|
+
logger.warning(f"Skipping XMP for {image_path}: not a valid JPEG")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
# Build XMP packet
|
|
89
|
+
xmp_xml = build_xmp_packet(metadata)
|
|
90
|
+
xmp_bytes = xmp_xml.encode("utf-8")
|
|
91
|
+
|
|
92
|
+
# Build APP1 segment with XMP namespace
|
|
93
|
+
xmp_segment = XMP_NAMESPACE + xmp_bytes
|
|
94
|
+
segment_length = len(xmp_segment) + 2 # +2 for length bytes
|
|
95
|
+
|
|
96
|
+
if segment_length > 65535:
|
|
97
|
+
logger.warning(f"Skipping XMP for {image_path}: XMP too large")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# APP1 marker (0xFFE1) + length + data
|
|
101
|
+
app1_marker = b"\xff\xe1"
|
|
102
|
+
length_bytes = segment_length.to_bytes(2, byteorder="big")
|
|
103
|
+
full_segment = app1_marker + length_bytes + xmp_segment
|
|
104
|
+
|
|
105
|
+
# Find insertion point - after SOI (0xFFD8) and any existing APP0/APP1 segments
|
|
106
|
+
# We want to insert after EXIF APP1 but before other segments
|
|
107
|
+
pos = 2 # Skip SOI
|
|
108
|
+
|
|
109
|
+
while pos < len(data) - 1:
|
|
110
|
+
if data[pos] != 0xFF:
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
marker = data[pos + 1]
|
|
114
|
+
|
|
115
|
+
# Stop at SOS (start of scan) or non-marker data
|
|
116
|
+
if marker == 0xDA or marker == 0x00:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
# Check if this is an APP1 with XMP namespace (skip if exists)
|
|
120
|
+
if marker == 0xE1: # APP1
|
|
121
|
+
seg_len = int.from_bytes(data[pos + 2 : pos + 4], byteorder="big")
|
|
122
|
+
seg_data = data[pos + 4 : pos + 2 + seg_len]
|
|
123
|
+
if seg_data.startswith(XMP_NAMESPACE):
|
|
124
|
+
# XMP already exists, replace it
|
|
125
|
+
new_data = data[:pos] + full_segment + data[pos + 2 + seg_len :]
|
|
126
|
+
with open(image_path, "wb") as f:
|
|
127
|
+
f.write(new_data)
|
|
128
|
+
logger.debug(f"Replaced XMP in {image_path}")
|
|
129
|
+
return True
|
|
130
|
+
# Skip this APP1 (probably EXIF)
|
|
131
|
+
pos += 2 + seg_len
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Skip APP0 (JFIF) segments
|
|
135
|
+
if marker == 0xE0: # APP0
|
|
136
|
+
seg_len = int.from_bytes(data[pos + 2 : pos + 4], byteorder="big")
|
|
137
|
+
pos += 2 + seg_len
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# Found a different marker, insert XMP here
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
# Insert XMP segment at current position
|
|
144
|
+
new_data = data[:pos] + full_segment + data[pos:]
|
|
145
|
+
|
|
146
|
+
with open(image_path, "wb") as f:
|
|
147
|
+
f.write(new_data)
|
|
148
|
+
|
|
149
|
+
logger.debug(f"Wrote XMP GPano metadata to {image_path}")
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.warning(f"Failed to write XMP to {image_path}: {e}")
|
|
154
|
+
return False
|
|
File without changes
|
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/__init__.py
RENAMED
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/__main__.py
RENAMED
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/client.py
RENAMED
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/downloader.py
RENAMED
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/ia_check.py
RENAMED
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/ia_meta.py
RENAMED
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/ia_stats.py
RENAMED
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/logging_config.py
RENAMED
|
File without changes
|
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/tar_sequences.py
RENAMED
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/webp_converter.py
RENAMED
|
File without changes
|
{mapillary_downloader-0.7.6 → mapillary_downloader-0.7.8}/src/mapillary_downloader/worker_pool.py
RENAMED
|
File without changes
|