|
3 | 3 | import shutil |
4 | 4 | import tempfile |
5 | 5 | from datetime import datetime |
| 6 | +from io import BytesIO |
6 | 7 |
|
| 8 | +import qrcode |
7 | 9 | from flask import ( |
8 | 10 | abort, |
9 | 11 | current_app, |
|
17 | 19 | url_for, |
18 | 20 | ) |
19 | 21 | from flask_login import current_user, login_required |
| 22 | +from PIL import Image, ImageDraw |
| 23 | +from qrcode.image.styledpil import StyledPilImage |
| 24 | +from qrcode.image.styles.moduledrawers.pil import RoundedModuleDrawer |
20 | 25 |
|
21 | 26 | from app.modules.apikeys.decorators import require_api_key |
22 | 27 | from app.modules.dataset import dataset_bp |
@@ -198,6 +203,94 @@ def download_dataset(dataset_id): |
198 | 203 | return resp |
199 | 204 |
|
200 | 205 |
|
| 206 | +def _build_dataset_qr_response(dataset: DataSet): |
| 207 | + if not dataset.ds_meta_data.dataset_doi: |
| 208 | + abort(404, description="QR available only for synchronized datasets with DOI.") |
| 209 | + |
| 210 | + target_url = url_for("dataset.subdomain_index", doi=dataset.ds_meta_data.dataset_doi, _external=True) |
| 211 | + qr = qrcode.QRCode( |
| 212 | + version=None, |
| 213 | + error_correction=qrcode.constants.ERROR_CORRECT_H, |
| 214 | + box_size=10, |
| 215 | + border=5, |
| 216 | + ) |
| 217 | + qr.add_data(target_url) |
| 218 | + qr.make(fit=True) |
| 219 | + |
| 220 | + img = qr.make_image( |
| 221 | + image_factory=StyledPilImage, |
| 222 | + module_drawer=RoundedModuleDrawer(radius_ratio=0.9), |
| 223 | + fill_color="black", |
| 224 | + back_color="white", |
| 225 | + ).convert("RGBA") |
| 226 | + |
| 227 | + logo_path = os.path.join(current_app.root_path, "static", "media", "logos", "uvlhub_ball.png") |
| 228 | + if os.path.exists(logo_path): |
| 229 | + logo = Image.open(logo_path).convert("RGBA") |
| 230 | + qr_width, qr_height = img.size |
| 231 | + |
| 232 | + logo_size = max(40, qr_width // 5) |
| 233 | + logo.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS) |
| 234 | + |
| 235 | + padding = max(4, logo_size // 12) |
| 236 | + logo_background = Image.new( |
| 237 | + "RGBA", |
| 238 | + (logo.width + (2 * padding), logo.height + (2 * padding)), |
| 239 | + (0, 0, 0, 0), |
| 240 | + ) |
| 241 | + bg_draw = ImageDraw.Draw(logo_background) |
| 242 | + bg_radius = max(6, logo_background.width // 5) |
| 243 | + bg_draw.rounded_rectangle( |
| 244 | + [(0, 0), (logo_background.width - 1, logo_background.height - 1)], |
| 245 | + radius=bg_radius, |
| 246 | + fill=(255, 255, 255, 255), |
| 247 | + ) |
| 248 | + |
| 249 | + rounded_logo = Image.new("RGBA", logo.size, (0, 0, 0, 0)) |
| 250 | + logo_mask = Image.new("L", logo.size, 0) |
| 251 | + logo_mask_draw = ImageDraw.Draw(logo_mask) |
| 252 | + logo_radius = max(4, logo.width // 5) |
| 253 | + logo_mask_draw.rounded_rectangle( |
| 254 | + [(0, 0), (logo.width - 1, logo.height - 1)], |
| 255 | + radius=logo_radius, |
| 256 | + fill=255, |
| 257 | + ) |
| 258 | + rounded_logo.paste(logo, (0, 0), logo_mask) |
| 259 | + |
| 260 | + bg_position = ((qr_width - logo_background.width) // 2, (qr_height - logo_background.height) // 2) |
| 261 | + img.paste(logo_background, bg_position, logo_background) |
| 262 | + |
| 263 | + logo_position = ((qr_width - rounded_logo.width) // 2, (qr_height - rounded_logo.height) // 2) |
| 264 | + img.paste(rounded_logo, logo_position, rounded_logo) |
| 265 | + |
| 266 | + img_io = BytesIO() |
| 267 | + img.save(img_io, format="PNG") |
| 268 | + img_io.seek(0) |
| 269 | + |
| 270 | + return send_file( |
| 271 | + img_io, |
| 272 | + mimetype="image/png", |
| 273 | + as_attachment=True, |
| 274 | + download_name=f"dataset_{dataset.id}_qr.png", |
| 275 | + ) |
| 276 | + |
| 277 | + |
| 278 | +@dataset_bp.route("/datasets/<int:dataset_id>/qr", methods=["GET"]) |
| 279 | +@dataset_bp.route("/datasets/<int:dataset_id>/qr/", methods=["GET"]) |
| 280 | +def dataset_qr_by_id(dataset_id): |
| 281 | + dataset = dataset_service.get_or_404(dataset_id) |
| 282 | + return _build_dataset_qr_response(dataset) |
| 283 | + |
| 284 | + |
| 285 | +@dataset_bp.route("/doi/<path:doi>/qr", methods=["GET"]) |
| 286 | +@dataset_bp.route("/doi/<path:doi>/qr/", methods=["GET"]) |
| 287 | +def dataset_qr_by_doi(doi): |
| 288 | + ds_meta_data = dsmetadata_service.filter_by_doi(doi) |
| 289 | + if not ds_meta_data: |
| 290 | + abort(404, description="Dataset not found for the given DOI.") |
| 291 | + return _build_dataset_qr_response(ds_meta_data.dataset) |
| 292 | + |
| 293 | + |
201 | 294 | @dataset_bp.route("/datasets/download/all", methods=["GET"]) |
202 | 295 | def download_all_dataset(): |
203 | 296 | # Crear un directorio temporal |
|
0 commit comments