flashforge-python-api 1.0.2__py3-none-any.whl → 1.2.0__py3-none-any.whl
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.
- flashforge/__init__.py +70 -29
- flashforge/api/__init__.py +1 -0
- flashforge/api/constants/__init__.py +1 -0
- flashforge/api/constants/commands.py +1 -0
- flashforge/api/constants/endpoints.py +7 -0
- flashforge/api/controls/__init__.py +1 -0
- flashforge/api/controls/control.py +64 -61
- flashforge/api/controls/files.py +28 -26
- flashforge/api/controls/info.py +84 -85
- flashforge/api/controls/job_control.py +120 -82
- flashforge/api/controls/temp_control.py +14 -11
- flashforge/api/misc/__init__.py +1 -1
- flashforge/api/network/__init__.py +2 -1
- flashforge/api/network/utils.py +7 -8
- flashforge/client.py +185 -64
- flashforge/discovery/__init__.py +30 -3
- flashforge/discovery/discovery.py +641 -308
- flashforge/models/__init__.py +11 -10
- flashforge/models/machine_info.py +225 -100
- flashforge/models/responses.py +103 -43
- flashforge/tcp/__init__.py +30 -17
- flashforge/tcp/a3_client.py +408 -0
- flashforge/tcp/a4_client.py +300 -0
- flashforge/tcp/ff_client.py +93 -90
- flashforge/tcp/gcode/__init__.py +4 -2
- flashforge/tcp/gcode/a3_gcode_controller.py +109 -0
- flashforge/tcp/gcode/gcode_controller.py +48 -36
- flashforge/tcp/gcode/gcodes.py +7 -1
- flashforge/tcp/parsers/__init__.py +11 -11
- flashforge/tcp/parsers/endstop_status.py +103 -132
- flashforge/tcp/parsers/location_info.py +26 -13
- flashforge/tcp/parsers/print_status.py +49 -56
- flashforge/tcp/parsers/printer_info.py +38 -48
- flashforge/tcp/parsers/temp_info.py +46 -47
- flashforge/tcp/parsers/thumbnail_info.py +65 -40
- flashforge/tcp/tcp_client.py +296 -293
- flashforge_python_api-1.2.0.dist-info/METADATA +133 -0
- flashforge_python_api-1.2.0.dist-info/RECORD +46 -0
- {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/WHEEL +1 -1
- flashforge_python_api-1.0.2.dist-info/METADATA +0 -284
- flashforge_python_api-1.0.2.dist-info/RECORD +0 -43
- {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
|
|
|
14
14
|
class TempData:
|
|
15
15
|
"""
|
|
16
16
|
Represents temperature data for a single component (e.g., extruder or bed).
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
Includes current temperature and target (set) temperature.
|
|
19
19
|
Temperatures are stored as strings but can be retrieved as numbers.
|
|
20
20
|
"""
|
|
@@ -22,23 +22,25 @@ class TempData:
|
|
|
22
22
|
def __init__(self, data: str) -> None:
|
|
23
23
|
"""
|
|
24
24
|
Create a TempData instance by parsing a temperature string.
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
The input string can be in the format "current/set" (e.g., "210/210")
|
|
27
27
|
or just "current" (e.g., "25") if the target temperature is not specified.
|
|
28
28
|
It also handles and removes a trailing "/0.0" if present from some printer firmwares.
|
|
29
29
|
All temperatures are rounded to the nearest integer.
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
Args:
|
|
32
32
|
data: The temperature data string (e.g., "210/210", "25", "60/60/0.0")
|
|
33
33
|
"""
|
|
34
34
|
# Handle potential formatting issues by removing any non-relevant part
|
|
35
|
-
data = data.replace(
|
|
35
|
+
data = data.replace("/0.0", "") # Remove trailing '/0.0' if exists
|
|
36
36
|
|
|
37
37
|
if "/" in data:
|
|
38
38
|
# Replay has current/set temps
|
|
39
|
-
split_temps = data.split(
|
|
39
|
+
split_temps = data.split("/")
|
|
40
40
|
self._current = self._parse_temp_data(split_temps[0].strip())
|
|
41
|
-
self._set =
|
|
41
|
+
self._set = (
|
|
42
|
+
self._parse_temp_data(split_temps[1].strip()) if len(split_temps) > 1 else None
|
|
43
|
+
)
|
|
42
44
|
else:
|
|
43
45
|
# Replay only has current temp (when printer is idle)
|
|
44
46
|
self._current = self._parse_temp_data(data)
|
|
@@ -47,22 +49,22 @@ class TempData:
|
|
|
47
49
|
def _parse_temp_data(self, data: str) -> str:
|
|
48
50
|
"""
|
|
49
51
|
Parse a raw temperature string value, round it, and return as string.
|
|
50
|
-
|
|
52
|
+
|
|
51
53
|
Args:
|
|
52
54
|
data: The raw temperature string (e.g., "210.5", "60")
|
|
53
|
-
|
|
55
|
+
|
|
54
56
|
Returns:
|
|
55
57
|
The rounded temperature as a string
|
|
56
58
|
"""
|
|
57
59
|
if "." in data:
|
|
58
|
-
data = data.split(
|
|
60
|
+
data = data.split(".")[0].strip() # Truncate decimal part before rounding
|
|
59
61
|
temp = round(float(data))
|
|
60
62
|
return str(temp)
|
|
61
63
|
|
|
62
64
|
def get_full(self) -> str:
|
|
63
65
|
"""
|
|
64
66
|
Get the full temperature string, including current and set temperatures.
|
|
65
|
-
|
|
67
|
+
|
|
66
68
|
Returns:
|
|
67
69
|
A string in the format "current/set" or just "current" if set temperature is not available
|
|
68
70
|
"""
|
|
@@ -73,7 +75,7 @@ class TempData:
|
|
|
73
75
|
def get_current(self) -> int:
|
|
74
76
|
"""
|
|
75
77
|
Get the current temperature as a number.
|
|
76
|
-
|
|
78
|
+
|
|
77
79
|
Returns:
|
|
78
80
|
The current temperature in Celsius
|
|
79
81
|
"""
|
|
@@ -82,7 +84,7 @@ class TempData:
|
|
|
82
84
|
def get_set(self) -> int:
|
|
83
85
|
"""
|
|
84
86
|
Get the target (set) temperature as a number.
|
|
85
|
-
|
|
87
|
+
|
|
86
88
|
Returns:
|
|
87
89
|
The set temperature in Celsius, or 0 if not set
|
|
88
90
|
"""
|
|
@@ -92,32 +94,32 @@ class TempData:
|
|
|
92
94
|
class TempInfo:
|
|
93
95
|
"""
|
|
94
96
|
Represents the temperature information for the printer's extruder and bed.
|
|
95
|
-
|
|
97
|
+
|
|
96
98
|
This data is typically parsed from the response of an M105 G-code command,
|
|
97
99
|
which reports the current and target temperatures.
|
|
98
100
|
"""
|
|
99
101
|
|
|
100
102
|
def __init__(self) -> None:
|
|
101
103
|
"""Initialize empty temperature info."""
|
|
102
|
-
self._extruder_temp:
|
|
104
|
+
self._extruder_temp: TempData | None = None
|
|
103
105
|
"""Temperature data for the extruder."""
|
|
104
106
|
|
|
105
|
-
self._bed_temp:
|
|
107
|
+
self._bed_temp: TempData | None = None
|
|
106
108
|
"""Temperature data for the print bed."""
|
|
107
109
|
|
|
108
|
-
def from_replay(self, replay: str) -> Optional[
|
|
110
|
+
def from_replay(self, replay: str) -> Optional["TempInfo"]:
|
|
109
111
|
"""
|
|
110
112
|
Parse a raw string replay from M105 command to populate temperature info.
|
|
111
|
-
|
|
113
|
+
|
|
112
114
|
The M105 response format is usually a single line (after the "ok" or command echo)
|
|
113
115
|
containing temperature segments like "T0:25/0" or "T:210/210 B:60/60".
|
|
114
116
|
This method splits the relevant line by spaces and then parses each segment.
|
|
115
117
|
It looks for segments starting with "T0:", "T):", or "T:" for extruder temperature,
|
|
116
118
|
and "B:" for bed temperature.
|
|
117
|
-
|
|
119
|
+
|
|
118
120
|
Args:
|
|
119
121
|
replay: The raw multi-line string response from the printer
|
|
120
|
-
|
|
122
|
+
|
|
121
123
|
Returns:
|
|
122
124
|
The populated TempInfo instance, or None if parsing fails
|
|
123
125
|
"""
|
|
@@ -125,29 +127,24 @@ class TempInfo:
|
|
|
125
127
|
return None
|
|
126
128
|
|
|
127
129
|
try:
|
|
128
|
-
|
|
129
|
-
if
|
|
130
|
-
logger.error(
|
|
130
|
+
lines = [line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()]
|
|
131
|
+
if not lines:
|
|
132
|
+
logger.error("TempInfo replay has invalid data: %s", replay)
|
|
131
133
|
return None
|
|
132
134
|
|
|
133
|
-
# Relevant temperature data is usually on the second line (data[1])
|
|
134
|
-
# e.g., "T0:25/0 B:28/0 @:0 B@:0" or "T:210/210 B:60/60"
|
|
135
|
-
temp_data = data[1].split(' ')
|
|
136
135
|
extruder_data_str = None
|
|
137
136
|
bed_data_str = None
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
elif segment.startswith('B:'):
|
|
150
|
-
bed_data_str = segment.replace('B:', '')
|
|
138
|
+
for line in lines:
|
|
139
|
+
for segment in line.split(" "):
|
|
140
|
+
if segment.startswith("T0:"):
|
|
141
|
+
extruder_data_str = segment.replace("T0:", "")
|
|
142
|
+
elif segment.startswith("T):"):
|
|
143
|
+
extruder_data_str = segment.replace("T):", "")
|
|
144
|
+
elif segment.startswith("T:"):
|
|
145
|
+
extruder_data_str = segment.replace("T:", "")
|
|
146
|
+
elif segment.startswith("B:"):
|
|
147
|
+
bed_data_str = segment.replace("B:", "")
|
|
151
148
|
|
|
152
149
|
# If we found extruder data, create TempData object
|
|
153
150
|
if extruder_data_str:
|
|
@@ -160,8 +157,10 @@ class TempInfo:
|
|
|
160
157
|
if bed_data_str:
|
|
161
158
|
self._bed_temp = TempData(bed_data_str)
|
|
162
159
|
else:
|
|
163
|
-
logger.warning(
|
|
164
|
-
|
|
160
|
+
logger.warning(
|
|
161
|
+
f"No bed temperature found in replay data, defaulting to 0/0: {replay}"
|
|
162
|
+
)
|
|
163
|
+
self._bed_temp = TempData("0/0") # Default if not present
|
|
165
164
|
|
|
166
165
|
return self
|
|
167
166
|
|
|
@@ -170,19 +169,19 @@ class TempInfo:
|
|
|
170
169
|
logger.error(f"Raw replay data: {replay}")
|
|
171
170
|
return None
|
|
172
171
|
|
|
173
|
-
def get_extruder_temp(self) ->
|
|
172
|
+
def get_extruder_temp(self) -> TempData | None:
|
|
174
173
|
"""
|
|
175
174
|
Get the extruder temperature data.
|
|
176
|
-
|
|
175
|
+
|
|
177
176
|
Returns:
|
|
178
177
|
A TempData object for the extruder, or None if not available
|
|
179
178
|
"""
|
|
180
179
|
return self._extruder_temp
|
|
181
180
|
|
|
182
|
-
def get_bed_temp(self) ->
|
|
181
|
+
def get_bed_temp(self) -> TempData | None:
|
|
183
182
|
"""
|
|
184
183
|
Get the print bed temperature data.
|
|
185
|
-
|
|
184
|
+
|
|
186
185
|
Returns:
|
|
187
186
|
A TempData object for the bed, or None if not available
|
|
188
187
|
"""
|
|
@@ -191,10 +190,10 @@ class TempInfo:
|
|
|
191
190
|
def is_cooled(self) -> bool:
|
|
192
191
|
"""
|
|
193
192
|
Check if both the bed and extruder are cooled down to relatively low temperatures.
|
|
194
|
-
|
|
193
|
+
|
|
195
194
|
Bed temperature <= 40°C and extruder temperature <= 200°C (though 200 is still hot).
|
|
196
195
|
Use with caution, as "cooled" here is relative and 200C is still very hot for an extruder.
|
|
197
|
-
|
|
196
|
+
|
|
198
197
|
Returns:
|
|
199
198
|
True if temperatures are at or below the defined thresholds, False otherwise
|
|
200
199
|
"""
|
|
@@ -205,10 +204,10 @@ class TempInfo:
|
|
|
205
204
|
def are_temps_safe(self) -> bool:
|
|
206
205
|
"""
|
|
207
206
|
Check if the current temperatures are within a generally safe operating range.
|
|
208
|
-
|
|
207
|
+
|
|
209
208
|
Prevents overheating (extruder < 250°C, bed < 100°C).
|
|
210
209
|
These are arbitrary "safe" limits and might need adjustment based on specific printer/material.
|
|
211
|
-
|
|
210
|
+
|
|
212
211
|
Returns:
|
|
213
212
|
True if temperatures are below the defined "safe" thresholds, False otherwise
|
|
214
213
|
"""
|
|
@@ -3,6 +3,8 @@ FlashForge Python API - Thumbnail Info Parser
|
|
|
3
3
|
|
|
4
4
|
Handles the parsing, storage, and manipulation of 3D print file thumbnail images.
|
|
5
5
|
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
6
8
|
import base64
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from typing import Optional
|
|
@@ -11,29 +13,29 @@ from typing import Optional
|
|
|
11
13
|
class ThumbnailInfo:
|
|
12
14
|
"""
|
|
13
15
|
Handles the parsing, storage, and manipulation of 3D print file thumbnail images.
|
|
14
|
-
|
|
16
|
+
|
|
15
17
|
Thumbnails are typically retrieved from the printer using a command like M662,
|
|
16
18
|
which returns a mixed response containing text (e.g., "ok") followed by raw binary PNG data.
|
|
17
19
|
This class provides methods to extract the PNG data, convert it to various formats, and save it to a file.
|
|
18
20
|
"""
|
|
19
21
|
|
|
20
|
-
def __init__(self):
|
|
22
|
+
def __init__(self) -> None:
|
|
21
23
|
"""Initialize a new ThumbnailInfo instance."""
|
|
22
|
-
self._image_data:
|
|
23
|
-
self._file_name:
|
|
24
|
+
self._image_data: bytes | None = None
|
|
25
|
+
self._file_name: str | None = None
|
|
24
26
|
|
|
25
|
-
def from_replay(self, replay: str, file_name: str) -> Optional[
|
|
27
|
+
def from_replay(self, replay: str, file_name: str) -> Optional["ThumbnailInfo"]:
|
|
26
28
|
"""
|
|
27
29
|
Parses thumbnail data from a raw printer response string.
|
|
28
|
-
|
|
30
|
+
|
|
29
31
|
The method expects the response to contain an "ok" text delimiter, after which
|
|
30
32
|
the binary PNG data begins. It searches for the PNG signature (0x89 PNG)
|
|
31
33
|
within the binary portion to correctly extract the image.
|
|
32
|
-
|
|
34
|
+
|
|
33
35
|
Args:
|
|
34
36
|
replay: The raw string response from the printer, which may include text and binary data
|
|
35
37
|
file_name: The name of the file for which the thumbnail was retrieved
|
|
36
|
-
|
|
38
|
+
|
|
37
39
|
Returns:
|
|
38
40
|
A ThumbnailInfo instance populated with the image data if parsing is successful,
|
|
39
41
|
or None if the replay is invalid, "ok" is not found, or the PNG signature is missing
|
|
@@ -44,9 +46,24 @@ class ThumbnailInfo:
|
|
|
44
46
|
try:
|
|
45
47
|
# Store the file name
|
|
46
48
|
self._file_name = file_name
|
|
49
|
+
if "Error: File not exists" in replay:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
binary_buffer = replay.encode("latin1")
|
|
53
|
+
magic_marker = b"\xA2\xA2\x2A\x2A"
|
|
54
|
+
magic_offset = binary_buffer.find(magic_marker)
|
|
55
|
+
if magic_offset >= 0 and len(binary_buffer) >= magic_offset + 8:
|
|
56
|
+
payload_length = int.from_bytes(
|
|
57
|
+
binary_buffer[magic_offset + 4 : magic_offset + 8],
|
|
58
|
+
byteorder="big",
|
|
59
|
+
)
|
|
60
|
+
payload_end = magic_offset + 8 + payload_length
|
|
61
|
+
if len(binary_buffer) >= payload_end:
|
|
62
|
+
self._image_data = binary_buffer[magic_offset + 8 : payload_end]
|
|
63
|
+
return self
|
|
47
64
|
|
|
48
65
|
# Find where the PNG data starts (after the "ok" text delimiter)
|
|
49
|
-
ok_index = replay.find(
|
|
66
|
+
ok_index = replay.find("ok")
|
|
50
67
|
if ok_index == -1:
|
|
51
68
|
print("ThumbnailInfo: No 'ok' found in response")
|
|
52
69
|
return None
|
|
@@ -58,11 +75,11 @@ class ThumbnailInfo:
|
|
|
58
75
|
|
|
59
76
|
# Convert the extracted string part (assumed to be binary) into bytes
|
|
60
77
|
# The printer sends binary data as part of a string reply
|
|
61
|
-
binary_buffer = raw_binary_data.encode(
|
|
78
|
+
binary_buffer = raw_binary_data.encode("latin1") # Use latin1 to preserve byte values
|
|
62
79
|
|
|
63
80
|
# Look for the PNG file signature (89 50 4E 47 0D 0A 1A 0A) in the buffer
|
|
64
81
|
# to correctly identify the start of the actual image data
|
|
65
|
-
png_signature = b
|
|
82
|
+
png_signature = b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
|
|
66
83
|
png_start = binary_buffer.find(png_signature)
|
|
67
84
|
|
|
68
85
|
if png_start >= 0:
|
|
@@ -77,59 +94,65 @@ class ThumbnailInfo:
|
|
|
77
94
|
print(f"ThumbnailInfo: Error parsing response: {e}")
|
|
78
95
|
return None
|
|
79
96
|
|
|
80
|
-
def get_image_data(self) ->
|
|
97
|
+
def get_image_data(self) -> str | None:
|
|
81
98
|
"""
|
|
82
99
|
Gets the raw thumbnail image data as a Base64 encoded string.
|
|
83
|
-
|
|
100
|
+
|
|
84
101
|
Returns:
|
|
85
102
|
A Base64 encoded string of the PNG image data, or None if no image data is available
|
|
86
103
|
"""
|
|
87
104
|
if not self._image_data:
|
|
88
105
|
return None
|
|
89
|
-
return base64.b64encode(self._image_data).decode(
|
|
106
|
+
return base64.b64encode(self._image_data).decode("ascii")
|
|
90
107
|
|
|
91
|
-
def get_image_bytes(self) ->
|
|
108
|
+
def get_image_bytes(self) -> bytes | None:
|
|
92
109
|
"""
|
|
93
110
|
Gets the raw thumbnail image data as bytes.
|
|
94
|
-
|
|
111
|
+
|
|
95
112
|
Returns:
|
|
96
113
|
The PNG image data as bytes, or None if no image data is available
|
|
97
114
|
"""
|
|
98
115
|
return self._image_data
|
|
99
116
|
|
|
100
|
-
def get_file_name(self) ->
|
|
117
|
+
def get_file_name(self) -> str | None:
|
|
101
118
|
"""
|
|
102
119
|
Gets the file name associated with this thumbnail.
|
|
103
|
-
|
|
120
|
+
|
|
104
121
|
Returns:
|
|
105
122
|
The file name string, or None if it was not set during parsing
|
|
106
123
|
"""
|
|
107
124
|
return self._file_name
|
|
108
125
|
|
|
109
|
-
def to_base64_data_url(self) ->
|
|
126
|
+
def to_base64_data_url(self) -> str | None:
|
|
110
127
|
"""
|
|
111
128
|
Converts the thumbnail image data to a Base64 data URL, suitable for embedding in web pages.
|
|
112
|
-
|
|
129
|
+
|
|
113
130
|
Returns:
|
|
114
131
|
A Base64 data URL string (e.g., "data:image/png;base64,..."), or None if no image data is available
|
|
115
132
|
"""
|
|
116
133
|
if not self._image_data:
|
|
117
134
|
return None
|
|
118
135
|
|
|
119
|
-
base64_data = base64.b64encode(self._image_data).decode(
|
|
136
|
+
base64_data = base64.b64encode(self._image_data).decode("ascii")
|
|
120
137
|
return f"data:image/png;base64,{base64_data}"
|
|
121
138
|
|
|
122
|
-
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _write_file_sync(file_path: str, data: bytes) -> None:
|
|
141
|
+
"""Helper to write bytes to a file synchronously."""
|
|
142
|
+
with open(file_path, "wb") as f:
|
|
143
|
+
f.write(data)
|
|
144
|
+
|
|
145
|
+
async def save_to_file(self, file_path: str | None = None) -> bool:
|
|
123
146
|
"""
|
|
124
147
|
Saves the thumbnail image data to a file.
|
|
125
|
-
|
|
148
|
+
|
|
126
149
|
If no file_path is provided, it attempts to generate a filename using the
|
|
127
150
|
original filename (stored during from_replay) with a ".png" extension.
|
|
128
|
-
|
|
151
|
+
|
|
129
152
|
Args:
|
|
130
153
|
file_path: Optional. The full path (including filename and extension) where the thumbnail should be saved.
|
|
131
154
|
If not provided, a filename is generated from self._file_name
|
|
132
|
-
|
|
155
|
+
|
|
133
156
|
Returns:
|
|
134
157
|
True if the file was saved successfully, False otherwise
|
|
135
158
|
"""
|
|
@@ -149,9 +172,9 @@ class ThumbnailInfo:
|
|
|
149
172
|
print("ThumbnailInfo: No file path provided and no filename to generate one from")
|
|
150
173
|
return False
|
|
151
174
|
|
|
152
|
-
# Write the bytes to file
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
# Write the bytes to file in a separate thread to avoid blocking the event loop
|
|
176
|
+
loop = asyncio.get_running_loop()
|
|
177
|
+
await loop.run_in_executor(None, self._write_file_sync, file_path, self._image_data)
|
|
155
178
|
|
|
156
179
|
print(f"ThumbnailInfo: Saved thumbnail to {file_path}")
|
|
157
180
|
return True
|
|
@@ -160,13 +183,13 @@ class ThumbnailInfo:
|
|
|
160
183
|
print(f"ThumbnailInfo: Error saving thumbnail to file: {e}")
|
|
161
184
|
return False
|
|
162
185
|
|
|
163
|
-
def save_to_file_sync(self, file_path:
|
|
186
|
+
def save_to_file_sync(self, file_path: str | None = None) -> bool:
|
|
164
187
|
"""
|
|
165
188
|
Synchronous version of save_to_file for compatibility.
|
|
166
|
-
|
|
189
|
+
|
|
167
190
|
Args:
|
|
168
191
|
file_path: Optional. The full path where the thumbnail should be saved
|
|
169
|
-
|
|
192
|
+
|
|
170
193
|
Returns:
|
|
171
194
|
True if the file was saved successfully, False otherwise
|
|
172
195
|
"""
|
|
@@ -187,7 +210,7 @@ class ThumbnailInfo:
|
|
|
187
210
|
return False
|
|
188
211
|
|
|
189
212
|
# Write the bytes to file
|
|
190
|
-
with open(file_path,
|
|
213
|
+
with open(file_path, "wb") as f:
|
|
191
214
|
f.write(self._image_data)
|
|
192
215
|
|
|
193
216
|
print(f"ThumbnailInfo: Saved thumbnail to {file_path}")
|
|
@@ -200,7 +223,7 @@ class ThumbnailInfo:
|
|
|
200
223
|
def get_image_size(self) -> tuple[int, int]:
|
|
201
224
|
"""
|
|
202
225
|
Gets the image dimensions by parsing the PNG header.
|
|
203
|
-
|
|
226
|
+
|
|
204
227
|
Returns:
|
|
205
228
|
A tuple of (width, height) in pixels, or (0, 0) if parsing fails
|
|
206
229
|
"""
|
|
@@ -209,8 +232,8 @@ class ThumbnailInfo:
|
|
|
209
232
|
|
|
210
233
|
try:
|
|
211
234
|
# PNG width and height are stored at bytes 16-23 in big-endian format
|
|
212
|
-
width = int.from_bytes(self._image_data[16:20], byteorder=
|
|
213
|
-
height = int.from_bytes(self._image_data[20:24], byteorder=
|
|
235
|
+
width = int.from_bytes(self._image_data[16:20], byteorder="big")
|
|
236
|
+
height = int.from_bytes(self._image_data[20:24], byteorder="big")
|
|
214
237
|
return (width, height)
|
|
215
238
|
except Exception:
|
|
216
239
|
return (0, 0)
|
|
@@ -218,7 +241,7 @@ class ThumbnailInfo:
|
|
|
218
241
|
def has_image_data(self) -> bool:
|
|
219
242
|
"""
|
|
220
243
|
Checks if this instance contains valid image data.
|
|
221
|
-
|
|
244
|
+
|
|
222
245
|
Returns:
|
|
223
246
|
True if image data is available, False otherwise
|
|
224
247
|
"""
|
|
@@ -236,7 +259,9 @@ class ThumbnailInfo:
|
|
|
236
259
|
|
|
237
260
|
def __repr__(self) -> str:
|
|
238
261
|
"""Detailed string representation for debugging."""
|
|
239
|
-
return (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
262
|
+
return (
|
|
263
|
+
f"ThumbnailInfo("
|
|
264
|
+
f"file_name='{self._file_name}', "
|
|
265
|
+
f"has_data={self.has_image_data()}, "
|
|
266
|
+
f"size={len(self._image_data) if self._image_data else 0} bytes)"
|
|
267
|
+
)
|