Feature: Flash file thumbnail generation via Ruffle headless #83

Open
opened 2026-04-24 21:59:15 +02:00 by Aroy · 1 comment
Owner

Area: Backend / File processing pipeline

Background

Flash (.swf) files are supported for playback via Ruffle in the frontend, but they have no thumbnail. Post cards show a static icon fallback. All other media types (video, PDF, image, GIF) generate thumbnails on import.

Proposed solution

Add a generate_flash_thumbnail Celery task that invokes the Ruffle CLI in headless/screenshot mode to render frame 1 of the SWF as a PNG thumbnail.

 `ruffle --screenshot /tmp/thumb.png /path/to/file.swf`

This mirrors the existing generate_video_thumbnail (ffmpegthumbnailer) and generate_pdf_thumbnail (pdf2image) tasks — same _update_file_model / _setup_output_path helpers, same deduplication logic.

Wire it into utils.py alongside the other post-import async dispatches:

  if file_type == "flash" and not file_instance.thumbnail:                                                                                                                                                                                  
      transaction.on_commit(lambda fid=fid: generate_flash_thumbnail.delay(fid))

Once thumbnails exist, the frontend post card already conditionally uses item.urls.sm for flash items and only falls back to the icon when absent.

Implementation checklist

  • Install ruffle binary in backend Docker image (nightly Linux build from GitHub releases)
  • Add generate_flash_thumbnail task in backend/apps/files/tasks.py
  • Dispatch task in backend/apps/files/utils.py for file_type == "flash"
  • Add management command support (e.g. index_images or a dedicated generate_flash_thumbnails command) to backfill existing flash files
  • Test with RUFFLE_RENDERER=software env var for environments without GPU

Caveats / known limitations

  • Frame 1 may be a loading screen or blank for complex SWFs — quality will vary
  • Some SWFs fail to render (ActionScript errors, external network dependencies) — task should catch CalledProcessError and retry up to N times then give up gracefully
  • Ruffle binary version should be pinned in the Dockerfile to avoid silent regressions
**Area:** Backend / File processing pipeline ### Background Flash (`.swf`) files are supported for playback via Ruffle in the frontend, but they have no thumbnail. Post cards show a static icon fallback. All other media types (video, PDF, image, GIF) generate thumbnails on import. ### Proposed solution Add a `generate_flash_thumbnail` Celery task that invokes the Ruffle CLI in headless/screenshot mode to render frame 1 of the SWF as a PNG thumbnail. `ruffle --screenshot /tmp/thumb.png /path/to/file.swf` This mirrors the existing `generate_video_thumbnail` (ffmpegthumbnailer) and `generate_pdf_thumbnail` (pdf2image) tasks — same `_update_file_model` / `_setup_output_path` helpers, same deduplication logic. Wire it into `utils.py` alongside the other post-import async dispatches: ```python if file_type == "flash" and not file_instance.thumbnail: transaction.on_commit(lambda fid=fid: generate_flash_thumbnail.delay(fid)) ``` Once thumbnails exist, the frontend post card already conditionally uses `item.urls.sm` for flash items and only falls back to the icon when absent. ### Implementation checklist - Install ruffle binary in backend Docker image (nightly Linux build from GitHub releases) - Add generate_flash_thumbnail task in backend/apps/files/tasks.py - Dispatch task in backend/apps/files/utils.py for file_type == "flash" - Add management command support (e.g. index_images or a dedicated generate_flash_thumbnails command) to backfill existing flash files - Test with RUFFLE_RENDERER=software env var for environments without GPU ### Caveats / known limitations - Frame 1 may be a loading screen or blank for complex SWFs — quality will vary - Some SWFs fail to render (ActionScript errors, external network dependencies) — task should catch CalledProcessError and retry up to N times then give up gracefully - Ruffle binary version should be pinned in the Dockerfile to avoid silent regressions
Author
Owner

Flash is inherently interactive/dynamic — frame 1 may be a loading screen or blank. The best option is Ruffle's headless renderer.

How Ruffle headless works

Ruffle ships a ruffle desktop binary with a --screenshot flag that renders frame 1 and outputs a PNG. No X display needed with the software renderer.

ruffle --screenshot /tmp/thumb.png /path/to/file.swf

You'd need the ruffle binary available in your Docker image. The nightly builds include it.

Implementation

Add to backend/apps/files/tasks.py:

  @shared_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=3)                                         
  def generate_flash_thumbnail(file_id: int) -> Optional[str]:
      try:                                                 
          pstfile = PostFileModel.objects.only(
              "id", "file", "hash_blake3", "thumbnail",                                                                                                                                                                                     
              "thumbnail_hash_blake3", "thumbnail_blur_hash"
          ).get(id=file_id)                                
                                                                                                                      
          if not pstfile.file:                             
              return f"Skipped file {file_id}: no file attached"
                                                           
          _, thumbnail_file_path = _setup_output_path(pstfile.hash_blake3, "flash_thumbnail")                         
                                                           
          subprocess.run(                                  
              ["ruffle", "--screenshot", thumbnail_file_path, pstfile.file.path],                                                                                                                                                           
              check=True,                                  
              timeout=30,                                  
          )                              
                                                                                                                                                                                                                                            
          _update_file_model(pstfile, thumbnail_file_path) 
          os.remove(thumbnail_file_path)                   

          generate_clip_embedding_PostFile.delay(file_id)                                                                                                                                                                                   
          return f"Flash thumbnail generated for file {file_id}"
                                                                                                                      
      except subprocess.CalledProcessError as e:
          _handle_task_error(e, file_id, "flash thumbnail generation")
      except Exception as e:                                                                                          
          _handle_task_error(e, file_id, "flash thumbnail generation")                                                

Hook it into utils.py alongside the PDF/video cases:

  if file_type == "flash" and not file_instance.thumbnail: 
      transaction.on_commit(lambda fid=fid: generate_flash_thumbnail.delay(fid))                                      

Caveats

Issue Reality
Frame 1 quality May be blank/loading screen for complex SWFs
Ruffle binary availability Must be installed in the Docker image
WGPU renderer Needs GPU or software fallback (RUFFLE_RENDERER=software)
Some SWFs fail ActionScript errors, network deps — task should fail gracefully

Installing Ruffle in Docker

  # In your backend Dockerfile — grab the nightly Linux binary                                                        
  RUN wget -q https://github.com/ruffle-rs/ruffle/releases/download/nightly-YYYY-MM-DD/ruffle-nightly-YYYY-MM-DD-linux-x86_64.tar.gz \                                                                                                      
      -O /tmp/ruffle.tar.gz \                              
      && tar -xzf /tmp/ruffle.tar.gz -C /usr/local/bin ruffle \                                                       
      && rm /tmp/ruffle.tar.gz
Flash is inherently interactive/dynamic — frame 1 may be a loading screen or blank. The best option is Ruffle's headless renderer. ### How Ruffle headless works Ruffle ships a ruffle desktop binary with a `--screenshot` flag that renders frame 1 and outputs a PNG. No X display needed with the software renderer. `ruffle --screenshot /tmp/thumb.png /path/to/file.swf` You'd need the ruffle binary available in your Docker image. The nightly builds include it. Implementation Add to `backend/apps/files/tasks.py`: ```python @shared_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=3) def generate_flash_thumbnail(file_id: int) -> Optional[str]: try: pstfile = PostFileModel.objects.only( "id", "file", "hash_blake3", "thumbnail", "thumbnail_hash_blake3", "thumbnail_blur_hash" ).get(id=file_id) if not pstfile.file: return f"Skipped file {file_id}: no file attached" _, thumbnail_file_path = _setup_output_path(pstfile.hash_blake3, "flash_thumbnail") subprocess.run( ["ruffle", "--screenshot", thumbnail_file_path, pstfile.file.path], check=True, timeout=30, ) _update_file_model(pstfile, thumbnail_file_path) os.remove(thumbnail_file_path) generate_clip_embedding_PostFile.delay(file_id) return f"Flash thumbnail generated for file {file_id}" except subprocess.CalledProcessError as e: _handle_task_error(e, file_id, "flash thumbnail generation") except Exception as e: _handle_task_error(e, file_id, "flash thumbnail generation") ``` Hook it into utils.py alongside the PDF/video cases: ```python if file_type == "flash" and not file_instance.thumbnail: transaction.on_commit(lambda fid=fid: generate_flash_thumbnail.delay(fid)) ``` ### Caveats | Issue | Reality | |:-|:-| | Frame 1 quality | May be blank/loading screen for complex SWFs | | Ruffle binary availability | Must be installed in the Docker image | | WGPU renderer | Needs GPU or software fallback (`RUFFLE_RENDERER=software`) | | Some SWFs fail | ActionScript errors, network deps — task should fail gracefully | ### Installing Ruffle in Docker ```dockerfile # In your backend Dockerfile — grab the nightly Linux binary RUN wget -q https://github.com/ruffle-rs/ruffle/releases/download/nightly-YYYY-MM-DD/ruffle-nightly-YYYY-MM-DD-linux-x86_64.tar.gz \ -O /tmp/ruffle.tar.gz \ && tar -xzf /tmp/ruffle.tar.gz -C /usr/local/bin ruffle \ && rm /tmp/ruffle.tar.gz ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Aroy/Gallery-Archivist#83
No description provided.