Loading openflexure_microscope/api/static/main_v1.js +128 −20 Original line number Diff line number Diff line // TODO: Remember a position and restore that position // TODO: Right click to auto-focus, centered on that region /* TODO: New capture panel TODO: Make delete button work TODO: Recordings list TODO: New recording panel TODO: Somehow have Python make funky thumbnails for videos? TODO: Remember a position and restore that position TODO: Right click to auto-focus, centered on that region TODO: Box to specify device IP TODO: Dynamically add stream image src from JS (allows streaming from an external IP) */ var baseURI = "/api/v1" Loading Loading @@ -42,7 +54,36 @@ function getCaptures() { } function updateCaptures(response) { console.log(response) // Clear captures list var capturesNode = document.getElementById("captures"); while (capturesNode.firstChild) { capturesNode.removeChild(capturesNode.firstChild); } // For each capture in the response array response.forEach(function(element) { // Store the capture object URI element_uri = `${baseURI}/capture/${element.id}` // Generate inner HTML from capture object html = ` <div class="clearfix"> <div class="left-col"><img src="${element_uri}/thumbnail"></div> <div class="right-col"> <b>${element.file}</b><br> <a href="${element_uri}/download" target="_blank">View</a> <a href="${element_uri}/download?as_attachment=true">Download</a> Delete <a href="${element_uri}/" target="_blank">JSON</a> <br> </div> </div> ` // Create a new capture div, and append HTML var div = document.createElement("div"); div.className = "capture"; div.innerHTML = html; capturesNode.appendChild(div); }); } function updateTextBoxes() { Loading @@ -56,9 +97,9 @@ function updateTextBoxes() { } function updateStagePositions(response) { document.getElementById('x_abs').value = response["x"]; document.getElementById('y_abs').value = response["y"]; document.getElementById('z_abs').value = response["z"]; document.getElementById('x_abs').value = response.x; document.getElementById('y_abs').value = response.y; document.getElementById('z_abs').value = response.z; } function getStagePositions() { Loading @@ -69,13 +110,79 @@ function getStagePositions() { safeRequest("GET", baseURI+"/position", null, updatePositionCallback, false) } // Capture methods function newCapture(filename, keep_on_disk, use_video_port, resizeWidth=null, resizeHeight=null) { // Make a position request function newCaptureCallback(response, status) { if (status != 200) { alert("Capture failed."); } console.log(status, response); getCaptures(); // Update full list of captures when done } payload = { "filename": filename, "keep_on_disk": keep_on_disk, "use_video_port": use_video_port } if ((resizeWidth) && (resizeHeight)) { payload.size = { "width": resizeWidth, "height": resizeHeight } } console.log(payload); safeRequest("POST", baseURI+"/capture/", payload, newCaptureCallback); } function newCaptureFromInput() { captureFilenameInput = document.getElementById('captureFilenameInput'); captureKeepOnDiskCheck = document.getElementById('captureKeepOnDiskCheck'); captureFullResolutionCheck = document.getElementById('captureFullResolutionCheck'); captureResizeCheck = document.getElementById('captureResizeCheck'); captureWidthInput = document.getElementById('captureWidthInput') captureHeightInput = document.getElementById('captureHeightInput') // Get filename if one is given if (captureFilenameInput.value === "") { filename = null; } else { filename = captureFilenameInput.value; } if (captureResizeCheck.checked) { resizeWidth = Number(captureWidthInput.value); resizeHeight = Number(captureHeightInput.value); } else { resizeWidth = null; resizeHeight = null; } newCapture(filename, captureKeepOnDiskCheck.checked, !(captureFullResolutionCheck.checked), resizeWidth, resizeHeight); } function newCaptureResizeToggle(checkbox) { captureWidthInput = document.getElementById('captureWidthInput') captureHeightInput = document.getElementById('captureHeightInput') captureWidthInput.disabled = !(checkbox.checked) captureHeightInput.disabled = !(checkbox.checked) } // Move stage methods // TODO: Combine setStagePositions and moveStagePositions, with bool argument for 'absolute' function setStagePositions(x_abs, y_abs, z_abs) { // Make a position request function absMoveCallback(response, status) { if (status == 400) { alert("Stage cannot be moved further than the safe range."); if (status != 200) { alert("Error moving stage."); } console.log(status, response); updateStagePositions(response); Loading @@ -86,8 +193,8 @@ function setStagePositions(x_abs, y_abs, z_abs) { function moveStagePositions(x_rel, y_rel, z_rel) { // Make a position request function relMoveCallback(response, status) { if (status == 400) { alert("Stage cannot be moved further than the safe range."); if (status != 200) { alert("Error moving stage."); } console.log(status, response); updateStagePositions(response); Loading Loading @@ -131,9 +238,9 @@ function clickHotspotImage(event) { window.addEventListener('wheel', function(e) { multiplier = 1/100; z_delta = e.deltaY * multiplier * focusVelocity; if ((document.getElementById("scrollFocusCheck").checked == true) && (e.target.parentNode.classList["value"] == "column middle")) { if ((document.getElementById("scrollFocusCheck").checked == true) && (e.target.parentNode.classList.value == "column middle")) { console.log(z_delta); console.log(e.target.parentNode.classList["value"]); console.log(e.target.parentNode.classList.value); // Make a position request safeRequest("POST", baseURI+"/position", { "absolute": false, "z": z_delta}, keyMoveCallback) } Loading Loading @@ -196,21 +303,21 @@ function safeRequest(method, url, payload, callback, lock=true, force=false) { var xhr = new XMLHttpRequest(); // new HttpRequest instance xhr.open(method, url); if (lock == true) { requestLock = true; if (lock == true) { // If request is using the lock requestLock = true; // Hold the lock } xhr.onload = function (e) { if (xhr.readyState === 4) { if (lock == true) { // If request is using the lock requestLock = false; // Release the lock } if (xhr.status !== 200) { console.error(xhr.statusText); } callback(JSON.parse(xhr.responseText), xhr.status); } if (lock == true) { requestLock = false; } }; if (method == "POST" || method == "PUT") { Loading @@ -221,6 +328,7 @@ function safeRequest(method, url, payload, callback, lock=true, force=false) { } else { console.log("Cannot start new request while previous requets is pending...") console.log("Cannot start the following request while the previous requets is pending:") console.log(method, url, payload) } } openflexure_microscope/api/static/style_v1.css +7 −1 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ body { } /* Preview sizing */ img { .middle img { width: 100%; height: 100%; object-fit: contain Loading Loading @@ -68,8 +68,14 @@ img { #captures { font-size:12px; margin-top: 20px; } .capture { background-color: #d3d3d3; margin-bottom: 5px; } .capture img { width: 60px; } No newline at end of file openflexure_microscope/api/templates/index_v1.html +16 −7 Original line number Diff line number Diff line Loading @@ -28,14 +28,14 @@ <div class="clearfix"> <div class="left-col">FOV width:</div> <div class="right-col"> <input type="text" id="fovXText" size="4" onchange="fovX = Number(this.value); updateTextBoxes();"> <input type="number" id="fovXText" onchange="fovX = Number(this.value); updateTextBoxes();" style="width: 7em" > </div> </div> <div class="clearfix"> <div class="left-col">FOV height:</div> <div class="right-col"> <input type="text" id="fovYText" size="4" onchange="fovY = Number(this.value); updateTextBoxes();"> <input type="number" id="fovYText" onchange="fovY = Number(this.value); updateTextBoxes();" style="width: 7em" > </div> </div> </p> Loading Loading @@ -72,16 +72,25 @@ </div> <div class="column right"> <h2>Capture</h2> Filename: <br> <input type="text" id="captureFilenameInput" placeholder="Leave blank for default"><br> Store on Pi: <input type="checkbox" id="captureKeepOnDiskCheck"><br> Full resolution: <input type="checkbox" id="captureFullResolutionCheck"><br> Resize capture: <input type="checkbox" id="captureResizeCheck" onclick="newCaptureResizeToggle(this);"><br> <input type="number" id="captureWidthInput" value="640" style="width: 7em" disabled> <input type="number" id="captureHeightInput" value="480" style="width: 7em" disabled><br> <button type="button" onclick="newCaptureFromInput();">Capture</button> <h2>Captures</h2> <div id="captures"> <p>Not yet implemented.</p> <div class="capture">Capture 1</div> </div> <button type="button" onclick="getCaptures();">Update</button> <div id="captures"></div> </div> <div class="column middle"> <img src="/api/v1/stream" ondblclick="clickHotspotImage(event)" ;="" style="width: 100%;"> <img src="/api/v1/stream" ondblclick="clickHotspotImage(event);"> </div> </div> Loading openflexure_microscope/api/v1.py +72 −2 Original line number Diff line number Diff line Loading @@ -14,7 +14,7 @@ import datetime from flask import ( Flask, render_template, Response, redirect, request, jsonify, send_file) redirect, request, jsonify, send_file, abort) import numpy as np Loading Loading @@ -119,8 +119,10 @@ def position(): return jsonify(microscope.state['position']) # Capture routes @app.route(uri('/capture'), methods=['GET', 'POST', 'PUT']) @app.route(uri('/capture/'), methods=['GET', 'POST', 'PUT']) def capture(): """Return JSONified microscope state""" global microscope Loading Loading @@ -173,5 +175,73 @@ def capture(): return jsonify(metadata_array) @app.route(uri('/capture/<capture_uuid>/'), methods=['GET', 'DELETE']) def get_capture(capture_uuid): """Return JSONified capture by UUID""" global microscope print(capture_uuid) capture_obj = microscope.camera.image_from_id(capture_uuid) if not capture_obj: return abort(404) # 404 Not Found # Get capture metadata capture_metadata = capture_obj.metadata # Add API routes to returned metadata uri_dict = { 'uri': {'metadata': uri('/capture/{}/'.format(capture_uuid))} } # If available, also add download link if capture_metadata['available']: uri_dict['uri']['download'] = uri('/capture/{}/download'.format(capture_uuid)) capture_metadata.update(uri_dict) if request.method == 'DELETE': print("DELETE NOT YET IMPLEMENETED") return jsonify(capture_metadata) else: # GET requests return jsonify(capture_metadata) @app.route(uri('/capture/<capture_uuid>/download'), methods=['GET']) def download_capture(capture_uuid): """Return capture file by UUID""" global microscope print(capture_uuid) capture_obj = microscope.camera.image_from_id(capture_uuid) if not capture_obj: return abort(404) # 404 Not Found as_attachment = get_bool(request.args.get('as_attachment')) return send_file( capture_obj.data, mimetype='image/jpeg', as_attachment=as_attachment, attachment_filename=capture_obj.filename) @app.route(uri('/capture/<capture_uuid>/thumbnail'), methods=['GET']) def thumb_capture(capture_uuid): """Return capture thumbnail by UUID""" global microscope print(capture_uuid) capture_obj = microscope.camera.image_from_id(capture_uuid) if not capture_obj: return abort(404) # 404 Not Found return send_file( capture_obj.thumbnail, mimetype='image/jpeg') if __name__ == '__main__': app.run(host='0.0.0.0', threaded=True, debug=True, use_reloader=False) openflexure_microscope/camera/capture.py +29 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,10 @@ import os import datetime import copy import logging from PIL import Image pil_formats = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF'] thumbnail_size = (60, 60) class StreamObject(object): Loading @@ -19,6 +23,9 @@ class StreamObject(object): self.id = uuid.uuid4().hex logging.info("Created {}".format(self.id)) # Store file format self.format = fmt # Create file name iterator = 0 f_path, f_name = self.build_file_path(filename, folder, fmt) Loading Loading @@ -55,6 +62,9 @@ class StreamObject(object): # Object lock self.locked = False # Thumbnail (populated only for JPEG captures) self.thumb_bytes = None def __enter__(self): """Create StreamObject in context, to auto-clean disk data.""" logging.debug("Entering context for {}.\ Loading Loading @@ -190,6 +200,25 @@ class StreamObject(object): """Return a byte string of the capture data.""" return self.data.getvalue() @property def thumbnail(self) -> io.BytesIO: # If no thumbnail exists, try and make one if not self.thumb_bytes: print("Building thumbnail") if self.format.upper() in pil_formats: im = Image.open(self.data) im.thumbnail(thumbnail_size) self.thumb_bytes = io.BytesIO() im.save(self.thumb_bytes, self.format) self.thumb_bytes.seek(0) else: self.thumb_bytes.seek(0) # Copy the buffer, to avoid closing the file data = io.BytesIO(self.thumb_bytes.getbuffer()) return data def load_file(self) -> bool: """Load data stored on disk to the in-memory stream.""" if self.file_exists: # If data file exists Loading Loading
openflexure_microscope/api/static/main_v1.js +128 −20 Original line number Diff line number Diff line // TODO: Remember a position and restore that position // TODO: Right click to auto-focus, centered on that region /* TODO: New capture panel TODO: Make delete button work TODO: Recordings list TODO: New recording panel TODO: Somehow have Python make funky thumbnails for videos? TODO: Remember a position and restore that position TODO: Right click to auto-focus, centered on that region TODO: Box to specify device IP TODO: Dynamically add stream image src from JS (allows streaming from an external IP) */ var baseURI = "/api/v1" Loading Loading @@ -42,7 +54,36 @@ function getCaptures() { } function updateCaptures(response) { console.log(response) // Clear captures list var capturesNode = document.getElementById("captures"); while (capturesNode.firstChild) { capturesNode.removeChild(capturesNode.firstChild); } // For each capture in the response array response.forEach(function(element) { // Store the capture object URI element_uri = `${baseURI}/capture/${element.id}` // Generate inner HTML from capture object html = ` <div class="clearfix"> <div class="left-col"><img src="${element_uri}/thumbnail"></div> <div class="right-col"> <b>${element.file}</b><br> <a href="${element_uri}/download" target="_blank">View</a> <a href="${element_uri}/download?as_attachment=true">Download</a> Delete <a href="${element_uri}/" target="_blank">JSON</a> <br> </div> </div> ` // Create a new capture div, and append HTML var div = document.createElement("div"); div.className = "capture"; div.innerHTML = html; capturesNode.appendChild(div); }); } function updateTextBoxes() { Loading @@ -56,9 +97,9 @@ function updateTextBoxes() { } function updateStagePositions(response) { document.getElementById('x_abs').value = response["x"]; document.getElementById('y_abs').value = response["y"]; document.getElementById('z_abs').value = response["z"]; document.getElementById('x_abs').value = response.x; document.getElementById('y_abs').value = response.y; document.getElementById('z_abs').value = response.z; } function getStagePositions() { Loading @@ -69,13 +110,79 @@ function getStagePositions() { safeRequest("GET", baseURI+"/position", null, updatePositionCallback, false) } // Capture methods function newCapture(filename, keep_on_disk, use_video_port, resizeWidth=null, resizeHeight=null) { // Make a position request function newCaptureCallback(response, status) { if (status != 200) { alert("Capture failed."); } console.log(status, response); getCaptures(); // Update full list of captures when done } payload = { "filename": filename, "keep_on_disk": keep_on_disk, "use_video_port": use_video_port } if ((resizeWidth) && (resizeHeight)) { payload.size = { "width": resizeWidth, "height": resizeHeight } } console.log(payload); safeRequest("POST", baseURI+"/capture/", payload, newCaptureCallback); } function newCaptureFromInput() { captureFilenameInput = document.getElementById('captureFilenameInput'); captureKeepOnDiskCheck = document.getElementById('captureKeepOnDiskCheck'); captureFullResolutionCheck = document.getElementById('captureFullResolutionCheck'); captureResizeCheck = document.getElementById('captureResizeCheck'); captureWidthInput = document.getElementById('captureWidthInput') captureHeightInput = document.getElementById('captureHeightInput') // Get filename if one is given if (captureFilenameInput.value === "") { filename = null; } else { filename = captureFilenameInput.value; } if (captureResizeCheck.checked) { resizeWidth = Number(captureWidthInput.value); resizeHeight = Number(captureHeightInput.value); } else { resizeWidth = null; resizeHeight = null; } newCapture(filename, captureKeepOnDiskCheck.checked, !(captureFullResolutionCheck.checked), resizeWidth, resizeHeight); } function newCaptureResizeToggle(checkbox) { captureWidthInput = document.getElementById('captureWidthInput') captureHeightInput = document.getElementById('captureHeightInput') captureWidthInput.disabled = !(checkbox.checked) captureHeightInput.disabled = !(checkbox.checked) } // Move stage methods // TODO: Combine setStagePositions and moveStagePositions, with bool argument for 'absolute' function setStagePositions(x_abs, y_abs, z_abs) { // Make a position request function absMoveCallback(response, status) { if (status == 400) { alert("Stage cannot be moved further than the safe range."); if (status != 200) { alert("Error moving stage."); } console.log(status, response); updateStagePositions(response); Loading @@ -86,8 +193,8 @@ function setStagePositions(x_abs, y_abs, z_abs) { function moveStagePositions(x_rel, y_rel, z_rel) { // Make a position request function relMoveCallback(response, status) { if (status == 400) { alert("Stage cannot be moved further than the safe range."); if (status != 200) { alert("Error moving stage."); } console.log(status, response); updateStagePositions(response); Loading Loading @@ -131,9 +238,9 @@ function clickHotspotImage(event) { window.addEventListener('wheel', function(e) { multiplier = 1/100; z_delta = e.deltaY * multiplier * focusVelocity; if ((document.getElementById("scrollFocusCheck").checked == true) && (e.target.parentNode.classList["value"] == "column middle")) { if ((document.getElementById("scrollFocusCheck").checked == true) && (e.target.parentNode.classList.value == "column middle")) { console.log(z_delta); console.log(e.target.parentNode.classList["value"]); console.log(e.target.parentNode.classList.value); // Make a position request safeRequest("POST", baseURI+"/position", { "absolute": false, "z": z_delta}, keyMoveCallback) } Loading Loading @@ -196,21 +303,21 @@ function safeRequest(method, url, payload, callback, lock=true, force=false) { var xhr = new XMLHttpRequest(); // new HttpRequest instance xhr.open(method, url); if (lock == true) { requestLock = true; if (lock == true) { // If request is using the lock requestLock = true; // Hold the lock } xhr.onload = function (e) { if (xhr.readyState === 4) { if (lock == true) { // If request is using the lock requestLock = false; // Release the lock } if (xhr.status !== 200) { console.error(xhr.statusText); } callback(JSON.parse(xhr.responseText), xhr.status); } if (lock == true) { requestLock = false; } }; if (method == "POST" || method == "PUT") { Loading @@ -221,6 +328,7 @@ function safeRequest(method, url, payload, callback, lock=true, force=false) { } else { console.log("Cannot start new request while previous requets is pending...") console.log("Cannot start the following request while the previous requets is pending:") console.log(method, url, payload) } }
openflexure_microscope/api/static/style_v1.css +7 −1 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ body { } /* Preview sizing */ img { .middle img { width: 100%; height: 100%; object-fit: contain Loading Loading @@ -68,8 +68,14 @@ img { #captures { font-size:12px; margin-top: 20px; } .capture { background-color: #d3d3d3; margin-bottom: 5px; } .capture img { width: 60px; } No newline at end of file
openflexure_microscope/api/templates/index_v1.html +16 −7 Original line number Diff line number Diff line Loading @@ -28,14 +28,14 @@ <div class="clearfix"> <div class="left-col">FOV width:</div> <div class="right-col"> <input type="text" id="fovXText" size="4" onchange="fovX = Number(this.value); updateTextBoxes();"> <input type="number" id="fovXText" onchange="fovX = Number(this.value); updateTextBoxes();" style="width: 7em" > </div> </div> <div class="clearfix"> <div class="left-col">FOV height:</div> <div class="right-col"> <input type="text" id="fovYText" size="4" onchange="fovY = Number(this.value); updateTextBoxes();"> <input type="number" id="fovYText" onchange="fovY = Number(this.value); updateTextBoxes();" style="width: 7em" > </div> </div> </p> Loading Loading @@ -72,16 +72,25 @@ </div> <div class="column right"> <h2>Capture</h2> Filename: <br> <input type="text" id="captureFilenameInput" placeholder="Leave blank for default"><br> Store on Pi: <input type="checkbox" id="captureKeepOnDiskCheck"><br> Full resolution: <input type="checkbox" id="captureFullResolutionCheck"><br> Resize capture: <input type="checkbox" id="captureResizeCheck" onclick="newCaptureResizeToggle(this);"><br> <input type="number" id="captureWidthInput" value="640" style="width: 7em" disabled> <input type="number" id="captureHeightInput" value="480" style="width: 7em" disabled><br> <button type="button" onclick="newCaptureFromInput();">Capture</button> <h2>Captures</h2> <div id="captures"> <p>Not yet implemented.</p> <div class="capture">Capture 1</div> </div> <button type="button" onclick="getCaptures();">Update</button> <div id="captures"></div> </div> <div class="column middle"> <img src="/api/v1/stream" ondblclick="clickHotspotImage(event)" ;="" style="width: 100%;"> <img src="/api/v1/stream" ondblclick="clickHotspotImage(event);"> </div> </div> Loading
openflexure_microscope/api/v1.py +72 −2 Original line number Diff line number Diff line Loading @@ -14,7 +14,7 @@ import datetime from flask import ( Flask, render_template, Response, redirect, request, jsonify, send_file) redirect, request, jsonify, send_file, abort) import numpy as np Loading Loading @@ -119,8 +119,10 @@ def position(): return jsonify(microscope.state['position']) # Capture routes @app.route(uri('/capture'), methods=['GET', 'POST', 'PUT']) @app.route(uri('/capture/'), methods=['GET', 'POST', 'PUT']) def capture(): """Return JSONified microscope state""" global microscope Loading Loading @@ -173,5 +175,73 @@ def capture(): return jsonify(metadata_array) @app.route(uri('/capture/<capture_uuid>/'), methods=['GET', 'DELETE']) def get_capture(capture_uuid): """Return JSONified capture by UUID""" global microscope print(capture_uuid) capture_obj = microscope.camera.image_from_id(capture_uuid) if not capture_obj: return abort(404) # 404 Not Found # Get capture metadata capture_metadata = capture_obj.metadata # Add API routes to returned metadata uri_dict = { 'uri': {'metadata': uri('/capture/{}/'.format(capture_uuid))} } # If available, also add download link if capture_metadata['available']: uri_dict['uri']['download'] = uri('/capture/{}/download'.format(capture_uuid)) capture_metadata.update(uri_dict) if request.method == 'DELETE': print("DELETE NOT YET IMPLEMENETED") return jsonify(capture_metadata) else: # GET requests return jsonify(capture_metadata) @app.route(uri('/capture/<capture_uuid>/download'), methods=['GET']) def download_capture(capture_uuid): """Return capture file by UUID""" global microscope print(capture_uuid) capture_obj = microscope.camera.image_from_id(capture_uuid) if not capture_obj: return abort(404) # 404 Not Found as_attachment = get_bool(request.args.get('as_attachment')) return send_file( capture_obj.data, mimetype='image/jpeg', as_attachment=as_attachment, attachment_filename=capture_obj.filename) @app.route(uri('/capture/<capture_uuid>/thumbnail'), methods=['GET']) def thumb_capture(capture_uuid): """Return capture thumbnail by UUID""" global microscope print(capture_uuid) capture_obj = microscope.camera.image_from_id(capture_uuid) if not capture_obj: return abort(404) # 404 Not Found return send_file( capture_obj.thumbnail, mimetype='image/jpeg') if __name__ == '__main__': app.run(host='0.0.0.0', threaded=True, debug=True, use_reloader=False)
openflexure_microscope/camera/capture.py +29 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,10 @@ import os import datetime import copy import logging from PIL import Image pil_formats = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF'] thumbnail_size = (60, 60) class StreamObject(object): Loading @@ -19,6 +23,9 @@ class StreamObject(object): self.id = uuid.uuid4().hex logging.info("Created {}".format(self.id)) # Store file format self.format = fmt # Create file name iterator = 0 f_path, f_name = self.build_file_path(filename, folder, fmt) Loading Loading @@ -55,6 +62,9 @@ class StreamObject(object): # Object lock self.locked = False # Thumbnail (populated only for JPEG captures) self.thumb_bytes = None def __enter__(self): """Create StreamObject in context, to auto-clean disk data.""" logging.debug("Entering context for {}.\ Loading Loading @@ -190,6 +200,25 @@ class StreamObject(object): """Return a byte string of the capture data.""" return self.data.getvalue() @property def thumbnail(self) -> io.BytesIO: # If no thumbnail exists, try and make one if not self.thumb_bytes: print("Building thumbnail") if self.format.upper() in pil_formats: im = Image.open(self.data) im.thumbnail(thumbnail_size) self.thumb_bytes = io.BytesIO() im.save(self.thumb_bytes, self.format) self.thumb_bytes.seek(0) else: self.thumb_bytes.seek(0) # Copy the buffer, to avoid closing the file data = io.BytesIO(self.thumb_bytes.getbuffer()) return data def load_file(self) -> bool: """Load data stored on disk to the in-memory stream.""" if self.file_exists: # If data file exists Loading