|
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 |
@@ -203,6 +208,94 @@ def download_dataset(dataset_id): |
203 | 208 | return resp |
204 | 209 |
|
205 | 210 |
|
| 211 | +def _build_dataset_qr_response(dataset: DataSet): |
| 212 | + if not dataset.ds_meta_data.dataset_doi: |
| 213 | + abort(404, description="QR available only for synchronized datasets with DOI.") |
| 214 | + |
| 215 | + target_url = url_for("dataset.subdomain_index", doi=dataset.ds_meta_data.dataset_doi, _external=True) |
| 216 | + qr = qrcode.QRCode( |
| 217 | + version=None, |
| 218 | + error_correction=qrcode.constants.ERROR_CORRECT_H, |
| 219 | + box_size=10, |
| 220 | + border=5, |
| 221 | + ) |
| 222 | + qr.add_data(target_url) |
| 223 | + qr.make(fit=True) |
| 224 | + |
| 225 | + img = qr.make_image( |
| 226 | + image_factory=StyledPilImage, |
| 227 | + module_drawer=RoundedModuleDrawer(radius_ratio=0.9), |
| 228 | + fill_color="black", |
| 229 | + back_color="white", |
| 230 | + ).convert("RGBA") |
| 231 | + |
| 232 | + logo_path = os.path.join(current_app.root_path, "static", "media", "logos", "uvlhub_ball.png") |
| 233 | + if os.path.exists(logo_path): |
| 234 | + logo = Image.open(logo_path).convert("RGBA") |
| 235 | + qr_width, qr_height = img.size |
| 236 | + |
| 237 | + logo_size = max(40, qr_width // 5) |
| 238 | + logo.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS) |
| 239 | + |
| 240 | + padding = max(4, logo_size // 12) |
| 241 | + logo_background = Image.new( |
| 242 | + "RGBA", |
| 243 | + (logo.width + (2 * padding), logo.height + (2 * padding)), |
| 244 | + (0, 0, 0, 0), |
| 245 | + ) |
| 246 | + bg_draw = ImageDraw.Draw(logo_background) |
| 247 | + bg_radius = max(6, logo_background.width // 5) |
| 248 | + bg_draw.rounded_rectangle( |
| 249 | + [(0, 0), (logo_background.width - 1, logo_background.height - 1)], |
| 250 | + radius=bg_radius, |
| 251 | + fill=(255, 255, 255, 255), |
| 252 | + ) |
| 253 | + |
| 254 | + rounded_logo = Image.new("RGBA", logo.size, (0, 0, 0, 0)) |
| 255 | + logo_mask = Image.new("L", logo.size, 0) |
| 256 | + logo_mask_draw = ImageDraw.Draw(logo_mask) |
| 257 | + logo_radius = max(4, logo.width // 5) |
| 258 | + logo_mask_draw.rounded_rectangle( |
| 259 | + [(0, 0), (logo.width - 1, logo.height - 1)], |
| 260 | + radius=logo_radius, |
| 261 | + fill=255, |
| 262 | + ) |
| 263 | + rounded_logo.paste(logo, (0, 0), logo_mask) |
| 264 | + |
| 265 | + bg_position = ((qr_width - logo_background.width) // 2, (qr_height - logo_background.height) // 2) |
| 266 | + img.paste(logo_background, bg_position, logo_background) |
| 267 | + |
| 268 | + logo_position = ((qr_width - rounded_logo.width) // 2, (qr_height - rounded_logo.height) // 2) |
| 269 | + img.paste(rounded_logo, logo_position, rounded_logo) |
| 270 | + |
| 271 | + img_io = BytesIO() |
| 272 | + img.save(img_io, format="PNG") |
| 273 | + img_io.seek(0) |
| 274 | + |
| 275 | + return send_file( |
| 276 | + img_io, |
| 277 | + mimetype="image/png", |
| 278 | + as_attachment=True, |
| 279 | + download_name=f"dataset_{dataset.id}_qr.png", |
| 280 | + ) |
| 281 | + |
| 282 | + |
| 283 | +@dataset_bp.route("/datasets/<int:dataset_id>/qr", methods=["GET"]) |
| 284 | +@dataset_bp.route("/datasets/<int:dataset_id>/qr/", methods=["GET"]) |
| 285 | +def dataset_qr_by_id(dataset_id): |
| 286 | + dataset = dataset_service.get_or_404(dataset_id) |
| 287 | + return _build_dataset_qr_response(dataset) |
| 288 | + |
| 289 | + |
| 290 | +@dataset_bp.route("/doi/<path:doi>/qr", methods=["GET"]) |
| 291 | +@dataset_bp.route("/doi/<path:doi>/qr/", methods=["GET"]) |
| 292 | +def dataset_qr_by_doi(doi): |
| 293 | + ds_meta_data = dsmetadata_service.filter_by_doi(doi) |
| 294 | + if not ds_meta_data: |
| 295 | + abort(404, description="Dataset not found for the given DOI.") |
| 296 | + return _build_dataset_qr_response(ds_meta_data.dataset) |
| 297 | + |
| 298 | + |
206 | 299 | @dataset_bp.route("/datasets/download/all", methods=["GET"]) |
207 | 300 | def download_all_dataset(): |
208 | 301 | selected_formats = request.args.getlist("formats") |
@@ -260,6 +353,56 @@ def subdomain_index(doi): |
260 | 353 | return resp |
261 | 354 |
|
262 | 355 |
|
| 356 | +@dataset_bp.route("/doi/<path:doi>/files/raw/<path:filename>", methods=["GET"]) |
| 357 | +@dataset_bp.route("/doi/<path:doi>/files/raw/<path:filename>/", methods=["GET"]) |
| 358 | +def doi_file_raw(doi, filename): |
| 359 | + |
| 360 | + new_doi = doi_mapping_service.get_new_doi(doi) |
| 361 | + if new_doi: |
| 362 | + return redirect( |
| 363 | + url_for("dataset.doi_file_raw", doi=new_doi, filename=filename), |
| 364 | + code=302, |
| 365 | + ) |
| 366 | + |
| 367 | + ds_meta_data = dsmetadata_service.filter_by_doi(doi) |
| 368 | + if not ds_meta_data: |
| 369 | + abort(404) |
| 370 | + |
| 371 | + dataset = ds_meta_data.dataset |
| 372 | + |
| 373 | + selected_file = None |
| 374 | + for fm in dataset.feature_models: |
| 375 | + for hf in fm.hubfiles: |
| 376 | + if hf.name == filename: |
| 377 | + selected_file = hf |
| 378 | + break |
| 379 | + if selected_file: |
| 380 | + break |
| 381 | + |
| 382 | + if not selected_file: |
| 383 | + abort(404, description="File not found in this DOI dataset") |
| 384 | + |
| 385 | + file_path = os.path.join( |
| 386 | + current_app.root_path, |
| 387 | + "..", |
| 388 | + "uploads", |
| 389 | + f"user_{dataset.user_id}", |
| 390 | + f"dataset_{dataset.id}", |
| 391 | + "uvl", |
| 392 | + selected_file.name, |
| 393 | + ) |
| 394 | + |
| 395 | + if not os.path.exists(file_path): |
| 396 | + abort(404, description="File missing on disk") |
| 397 | + |
| 398 | + return send_file( |
| 399 | + file_path, |
| 400 | + mimetype="text/plain; charset=utf-8", |
| 401 | + as_attachment=False, |
| 402 | + download_name=selected_file.name, |
| 403 | + ) |
| 404 | + |
| 405 | + |
263 | 406 | @dataset_bp.route("/datasets/unsynchronized/<int:dataset_id>/", methods=["GET"]) |
264 | 407 | @login_required |
265 | 408 | @is_dataset_owner |
|
0 commit comments