From 6fd620087b476a325fc3dcfcf1c2a982526ede73 Mon Sep 17 00:00:00 2001 From: "Ericson \"Fogo\" Soares" Date: Sat, 24 Sep 2022 05:21:13 -0300 Subject: [PATCH] Video thumbnails (#376) * Preparing some scaffolding for video thumbnails * Implemented thumbnail generation for videos * Propagating errors of `Node` creation * Using ffmpeg feature gate * Introducing ffmpegthumbnailer-rs as a subcrate on core * - rename to thumbnailer - fix explorer thumbnail bug - add more supported video types - re-fix explorer performance * remove nested licence Co-authored-by: Jamie Pine --- Cargo.lock | Bin 151945 -> 152098 bytes apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/src/main.rs | 20 +- apps/desktop/src-tauri/src/menu.rs | 4 +- apps/landing/stats.html | 4034 +++++++++++++++++ apps/mobile/rust/src/android.rs | 2 +- apps/mobile/src/components/file/FileItem.tsx | 2 +- apps/server/src/main.rs | 4 +- core/Cargo.toml | 3 +- core/src/encode/thumb.rs | 154 +- core/src/lib.rs | 60 +- core/thumbnailer/.gitignore | 303 ++ core/thumbnailer/Cargo.toml | 22 + core/thumbnailer/README.md | 39 + core/thumbnailer/src/error.rs | 142 + core/thumbnailer/src/film_strip.rs | 693 +++ core/thumbnailer/src/lib.rs | 108 + core/thumbnailer/src/movie_decoder.rs | 793 ++++ core/thumbnailer/src/thumbnailer.rs | 166 + core/thumbnailer/src/utils.rs | 30 + core/thumbnailer/src/video_frame.rs | 42 + .../client/src/hooks/useCurrentLibrary.tsx | 5 +- .../src/components/explorer/Explorer.tsx | 4 +- .../src/components/explorer/FileItem.tsx | 66 +- .../src/components/explorer/FileThumb.tsx | 46 +- .../src/components/explorer/Inspector.tsx | 13 +- .../components/explorer/VirtualizedList.tsx | 66 +- .../components/explorer/inspector/Divider.tsx | 2 +- .../settings/client/GeneralSettings.tsx | 2 +- 29 files changed, 6683 insertions(+), 144 deletions(-) create mode 100644 apps/landing/stats.html create mode 100644 core/thumbnailer/.gitignore create mode 100644 core/thumbnailer/Cargo.toml create mode 100644 core/thumbnailer/README.md create mode 100644 core/thumbnailer/src/error.rs create mode 100644 core/thumbnailer/src/film_strip.rs create mode 100644 core/thumbnailer/src/lib.rs create mode 100644 core/thumbnailer/src/movie_decoder.rs create mode 100644 core/thumbnailer/src/thumbnailer.rs create mode 100644 core/thumbnailer/src/utils.rs create mode 100644 core/thumbnailer/src/video_frame.rs diff --git a/Cargo.lock b/Cargo.lock index c4472879ed3f10a5f1ca319516ac2bac6f00898f..bb4270cbbf605dab8404bd900d85f160648b1d5b 100644 GIT binary patch delta 70 zcmeBN%(-X@XTugo{XWi&(%huH#LS%3$$CEww@3Cd8i#Yh#HLSVXB3{!>%u5D**Q;S Ux^N$(Doc54Qo;7Zr;HLg0G>}6+yDRo delta 38 wcmV+>0NMYdqzQ?m34pW#FOHY+y8#`SUHJhGmmvQE4!3uX0VP+rCFTMBYdK{P`v3p{ diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 94835c29a..ec1592f58 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -12,7 +12,7 @@ build = "build.rs" [dependencies] tauri = { version = "1.0.4", features = ["api-all", "macos-private-api"] } rspc = { version = "0.0.5", features = ["tauri"] } -sdcore = { path = "../../../core" } +sdcore = { path = "../../../core", features = ["ffmpeg"] } tokio = { version = "1.17.0", features = ["sync"] } window-shadows = "0.1.2" tracing = "0.1.35" diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 2c210b2b6..891a8cbce 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -3,15 +3,17 @@ windows_subsystem = "windows" )] +use std::error::Error; use std::path::PathBuf; use sdcore::Node; +use tauri::async_runtime::block_on; use tauri::{ api::path, - async_runtime::block_on, http::{ResponseBuilder, Uri}, Manager, RunEvent, }; +use tokio::task::block_in_place; use tracing::{debug, error}; #[cfg(target_os = "macos")] mod macos; @@ -25,12 +27,12 @@ async fn app_ready(app_handle: tauri::AppHandle) { } #[tokio::main] -async fn main() { +async fn main() -> Result<(), Box> { let data_dir = path::data_dir() .unwrap_or_else(|| PathBuf::from("./")) .join("spacedrive"); - let (node, router) = Node::new(data_dir).await; + let (node, router) = Node::new(data_dir).await?; let app = tauri::Builder::default() .plugin(rspc::integrations::tauri::plugin(router, { @@ -44,7 +46,8 @@ async fn main() { let mut path = url.path().split('/').collect::>(); path[0] = url.host().unwrap(); // The first forward slash causes an empty item and we replace it with the URL's host which you expect to be at the start - let (status_code, content_type, body) = node.handle_custom_uri(path); + let (status_code, content_type, body) = + block_in_place(|| block_on(node.handle_custom_uri(path))); ResponseBuilder::new() .status(status_code) .mimetype(content_type) @@ -82,8 +85,7 @@ async fn main() { .on_menu_event(menu::handle_menu_event) .invoke_handler(tauri::generate_handler![app_ready,]) .menu(menu::get_menu()) - .build(tauri::generate_context!()) - .expect("error while building tauri application"); + .build(tauri::generate_context!())?; app.run(move |app_handler, event| { if let RunEvent::ExitRequested { .. } = event { @@ -98,8 +100,10 @@ async fn main() { } }); - block_on(node.shutdown()); + block_in_place(|| block_on(node.shutdown())); app_handler.exit(0); } - }) + }); + + Ok(()) } diff --git a/apps/desktop/src-tauri/src/menu.rs b/apps/desktop/src-tauri/src/menu.rs index 31c5cf09b..184887416 100644 --- a/apps/desktop/src-tauri/src/menu.rs +++ b/apps/desktop/src-tauri/src/menu.rs @@ -45,7 +45,9 @@ fn custom_menu_bar() -> Menu { .add_native_item(MenuItem::Paste) .add_native_item(MenuItem::SelectAll); let view_menu = Menu::new() - .add_item(CustomMenuItem::new("open_search".to_string(), "Search...").accelerator("CmdOrCtrl+F")) + .add_item( + CustomMenuItem::new("open_search".to_string(), "Search...").accelerator("CmdOrCtrl+F"), + ) // .add_item( // CustomMenuItem::new("command_pallete".to_string(), "Command Pallete") // .accelerator("CmdOrCtrl+P"), diff --git a/apps/landing/stats.html b/apps/landing/stats.html new file mode 100644 index 000000000..3450e2f6b --- /dev/null +++ b/apps/landing/stats.html @@ -0,0 +1,4034 @@ + + + + + + + + RollUp Visualizer + + + +
+ + + + + diff --git a/apps/mobile/rust/src/android.rs b/apps/mobile/rust/src/android.rs index 3f51a5d49..3f25a3dd4 100644 --- a/apps/mobile/rust/src/android.rs +++ b/apps/mobile/rust/src/android.rs @@ -78,7 +78,7 @@ pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg( env.get_string(data_dir.into()).unwrap().into() }; - let new_node = Node::new(data_dir).await; + let new_node = Node::new(data_dir).await.expect("Unable to create node"); node.replace(new_node.clone()); new_node }, diff --git a/apps/mobile/src/components/file/FileItem.tsx b/apps/mobile/src/components/file/FileItem.tsx index 352f4e17a..eb28883c3 100644 --- a/apps/mobile/src/components/file/FileItem.tsx +++ b/apps/mobile/src/components/file/FileItem.tsx @@ -37,7 +37,7 @@ const FileItem = ({ file }: FileItemProps) => { {/* Folder Icons/Thumbnail etc. */} - + {file?.name} diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 1a8fcca3e..85892f76e 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -33,7 +33,7 @@ async fn main() { .map(|port| port.parse::().unwrap_or(8080)) .unwrap_or(8080); - let (node, router) = Node::new(data_dir).await; + let (node, router) = Node::new(data_dir).await.expect("Unable to create node"); let signal = utils::axum_shutdown_signal(node.clone()); let app = axum::Router::new() @@ -43,7 +43,7 @@ async fn main() { let node = node.clone(); get(|extract::Path(path): extract::Path| async move { let (status_code, content_type, body) = - node.handle_custom_uri(path.split('/').collect()); + node.handle_custom_uri(path.split('/').collect()).await; ( StatusCode::from_u16(status_code).unwrap(), diff --git a/core/Cargo.toml b/core/Cargo.toml index 59fd1da4e..8ef01c25b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,7 +15,7 @@ p2p = [ mobile = [ ] # This feature allows features to be disabled when the Core is running on mobile. ffmpeg = [ - "dep:ffmpeg-next", + "dep:ffmpeg-next", "dep:thumbnailer" ] # This feature controls whether the Spacedrive Core contains functionality which requires FFmpeg. [dependencies] @@ -55,6 +55,7 @@ async-trait = "^0.1.52" image = "0.24.1" webp = "0.2.2" ffmpeg-next = { version = "5.0.3", optional = true, features = [] } +thumbnailer = { path = "./thumbnailer", optional = true } fs_extra = "1.2.0" tracing = "0.1.35" tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } diff --git a/core/src/encode/thumb.rs b/core/src/encode/thumb.rs index 21fbf2de0..551cfbf51 100644 --- a/core/src/encode/thumb.rs +++ b/core/src/encode/thumb.rs @@ -8,6 +8,7 @@ use crate::{ use image::{self, imageops, DynamicImage, GenericImageView}; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; use std::{ error::Error, ops::Deref, @@ -37,13 +38,26 @@ pub struct ThumbnailJobState { root_path: PathBuf, } -file_path::include!(image_path_with_file { file }); +file_path::include!(file_path_with_file { file }); + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +enum ThumbnailJobStepKind { + Image, + #[cfg(feature = "ffmpeg")] + Video, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ThumbnailJobStep { + file: file_path_with_file::Data, + kind: ThumbnailJobStepKind, +} #[async_trait::async_trait] impl StatefulJob for ThumbnailJob { type Init = ThumbnailJobInit; type Data = ThumbnailJobState; - type Step = image_path_with_file::Data; + type Step = ThumbnailJobStep; fn name(&self) -> &'static str { THUMBNAIL_JOB_NAME @@ -78,21 +92,81 @@ impl StatefulJob for ThumbnailJob { fs::create_dir_all(&thumbnail_dir).await?; let root_path = location.local_path.map(PathBuf::from).unwrap(); - // query database for all files in this location that need thumbnails - let image_files = - get_images(&library_ctx, state.init.location_id, &state.init.path).await?; - info!("Found {:?} files", image_files.len()); + // query database for all image files in this location that need thumbnails + let image_files = get_files_by_extension( + &library_ctx, + state.init.location_id, + &state.init.path, + vec![ + "png".to_string(), + "jpeg".to_string(), + "jpg".to_string(), + "gif".to_string(), + "webp".to_string(), + ], + ThumbnailJobStepKind::Image, + ) + .await?; + info!("Found {:?} image files", image_files.len()); + + #[cfg(feature = "ffmpeg")] + let all_files = { + // query database for all video files in this location that need thumbnails + let video_files = get_files_by_extension( + &library_ctx, + state.init.location_id, + &state.init.path, + // Some formats extracted from https://ffmpeg.org/ffmpeg-formats.html + vec![ + "avi".to_string(), + "asf".to_string(), + "mpeg".to_string(), + // "mpg".to_string(), + "mts".to_string(), + "mpe".to_string(), + "vob".to_string(), + "qt".to_string(), + "mov".to_string(), + "asf".to_string(), + "asx".to_string(), + // "swf".to_string(), + "mjpeg".to_string(), + "ts".to_string(), + "mxf".to_string(), + // "m2v".to_string(), + "m2ts".to_string(), + "f4v".to_string(), + "wm".to_string(), + "3gp".to_string(), + "m4v".to_string(), + "wmv".to_string(), + "mp4".to_string(), + "webm".to_string(), + "flv".to_string(), + ], + ThumbnailJobStepKind::Video, + ) + .await?; + info!("Found {:?} video files", video_files.len()); + + image_files + .into_iter() + .chain(video_files.into_iter()) + .collect::>() + }; + #[cfg(not(feature = "ffmpeg"))] + let all_files = { image_files.into_iter().collect::>() }; ctx.progress(vec![ - JobReportUpdate::TaskCount(image_files.len()), - JobReportUpdate::Message(format!("Preparing to process {} files", image_files.len())), + JobReportUpdate::TaskCount(all_files.len()), + JobReportUpdate::Message(format!("Preparing to process {} files", all_files.len())), ]); state.data = Some(ThumbnailJobState { thumbnail_dir, root_path, }); - state.steps = image_files.into_iter().collect(); + state.steps = all_files; Ok(()) } @@ -105,7 +179,7 @@ impl StatefulJob for ThumbnailJob { let step = &state.steps[0]; ctx.progress(vec![JobReportUpdate::Message(format!( "Processing {}", - step.materialized_path + step.file.materialized_path ))]); let data = state @@ -114,16 +188,16 @@ impl StatefulJob for ThumbnailJob { .expect("critical error: missing data on job state"); // assemble the file path - let path = data.root_path.join(&step.materialized_path); + let path = data.root_path.join(&step.file.materialized_path); trace!("image_file {:?}", step); // get cas_id, if none found skip - let cas_id = match &step.file { + let cas_id = match &step.file.file { Some(f) => f.cas_id.clone(), _ => { warn!( "skipping thumbnail generation for {}", - step.materialized_path + step.file.materialized_path ); return Ok(()); } @@ -136,8 +210,18 @@ impl StatefulJob for ThumbnailJob { if !output_path.exists() { info!("Writing {:?} to {:?}", path, output_path); - if let Err(e) = generate_thumbnail(&path, &output_path).await { - error!("Error generating thumb {:?}", e); + match step.kind { + ThumbnailJobStepKind::Image => { + if let Err(e) = generate_image_thumbnail(&path, &output_path).await { + error!("Error generating thumb for image {:#?}", e); + } + } + #[cfg(feature = "ffmpeg")] + ThumbnailJobStepKind::Video => { + if let Err(e) = generate_video_thumbnail(&path, &output_path).await { + error!("Error generating thumb for video: {:?} {:#?}", &path, e); + } + } } if !state.init.background { @@ -178,7 +262,7 @@ impl StatefulJob for ThumbnailJob { } } -pub async fn generate_thumbnail>( +async fn generate_image_thumbnail>( file_path: P, output_path: P, ) -> Result<(), Box> { @@ -190,6 +274,7 @@ pub async fn generate_thumbnail>( // Optionally, resize the existing photo and convert back into DynamicImage let img = DynamicImage::ImageRgba8(imageops::resize( &img, + // FIXME : Think of a better heuristic to get the thumbnail size (w as f32 * THUMBNAIL_SIZE_FACTOR) as u32, (h as f32 * THUMBNAIL_SIZE_FACTOR) as u32, imageops::FilterType::Triangle, @@ -205,25 +290,31 @@ pub async fn generate_thumbnail>( Ok(encoder.encode(THUMBNAIL_QUALITY).deref().to_owned()) })?; - fs::write(output_path, &webp).await?; + fs::write(output_path, &webp).await.map_err(Into::into) +} + +#[cfg(feature = "ffmpeg")] +async fn generate_video_thumbnail>( + file_path: P, + output_path: P, +) -> Result<(), Box> { + use thumbnailer::to_thumbnail; + + to_thumbnail(file_path, output_path, 256, THUMBNAIL_QUALITY).await?; Ok(()) } -pub async fn get_images( +async fn get_files_by_extension( ctx: &LibraryContext, location_id: i32, path: impl AsRef, -) -> Result, JobError> { + extensions: Vec, + kind: ThumbnailJobStepKind, +) -> Result, JobError> { let mut params = vec![ file_path::location_id::equals(location_id), - file_path::extension::in_vec(vec![ - "png".to_string(), - "jpeg".to_string(), - "jpg".to_string(), - "gif".to_string(), - "webp".to_string(), - ]), + file_path::extension::in_vec(extensions), ]; let path_str = path.as_ref().to_string_lossy().to_string(); @@ -232,11 +323,14 @@ pub async fn get_images( params.push(file_path::materialized_path::starts_with(path_str)); } - ctx.db + Ok(ctx + .db .file_path() .find_many(params) - .include(image_path_with_file::include()) + .include(file_path_with_file::include()) .exec() - .await - .map_err(Into::into) + .await? + .into_iter() + .map(|file| ThumbnailJobStep { file, kind }) + .collect()) } diff --git a/core/src/lib.rs b/core/src/lib.rs index ad5c927d0..fd11734e6 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -2,12 +2,16 @@ use api::{CoreEvent, Ctx, Router}; use job::JobManager; use library::LibraryManager; use node::NodeConfigManager; -use std::{fs::File, io::Read, path::Path, sync::Arc}; +use std::{path::Path, sync::Arc}; +use thiserror::Error; +use tokio::{ + fs::{self, File}, + io::AsyncReadExt, + sync::broadcast, +}; use tracing::{error, info}; use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, EnvFilter}; -use tokio::{fs, sync::broadcast}; - pub mod api; pub(crate) mod encode; pub(crate) mod file; @@ -41,9 +45,9 @@ const CONSOLE_LOG_FILTER: LevelFilter = LevelFilter::DEBUG; const CONSOLE_LOG_FILTER: LevelFilter = LevelFilter::INFO; impl Node { - pub async fn new(data_dir: impl AsRef) -> (Arc, Arc) { + pub async fn new(data_dir: impl AsRef) -> Result<(Arc, Arc), NodeError> { let data_dir = data_dir.as_ref(); - fs::create_dir_all(&data_dir).await.unwrap(); + fs::create_dir_all(&data_dir).await?; tracing_subscriber::registry() .with( @@ -69,18 +73,18 @@ impl Node { .init(); let event_bus = broadcast::channel(1024); - let config = NodeConfigManager::new(data_dir.to_owned()).await.unwrap(); + let config = NodeConfigManager::new(data_dir.to_path_buf()).await?; let jobs = JobManager::new(); - let node_ctx = NodeContext { - config: config.clone(), - jobs: jobs.clone(), - event_bus_tx: event_bus.0.clone(), - }; - let library_manager = - LibraryManager::new(data_dir.to_owned().join("libraries"), node_ctx.clone()) - .await - .unwrap(); + let library_manager = LibraryManager::new( + data_dir.join("libraries"), + NodeContext { + config: Arc::clone(&config), + jobs: Arc::clone(&jobs), + event_bus_tx: event_bus.0.clone(), + }, + ) + .await?; // Trying to resume possible paused jobs let inner_library_manager = Arc::clone(&library_manager); @@ -96,14 +100,12 @@ impl Node { let router = api::mount(); let node = Node { config, - library_manager: LibraryManager::new(data_dir.join("libraries"), node_ctx) - .await - .unwrap(), + library_manager, jobs, event_bus, }; - (Arc::new(node), router) + Ok((Arc::new(node), router)) } pub fn get_request_context(&self) -> Ctx { @@ -116,8 +118,7 @@ impl Node { } // Note: this system doesn't use chunked encoding which could prove a problem with large files but I can't see an easy way to do chunked encoding with Tauri custom URIs. - // It would also be nice to use Tokio Filesystem operations instead of the std ones which block. Tauri's custom URI protocols don't seem to support async out of the box. - pub fn handle_custom_uri( + pub async fn handle_custom_uri( &self, path: Vec<&str>, ) -> ( @@ -139,14 +140,14 @@ impl Node { .join("thumbnails") .join(path[1] /* file_cas_id */) .with_extension("webp"); - match File::open(&filename) { + match File::open(&filename).await { Ok(mut file) => { - let mut buf = match std::fs::metadata(&filename) { + let mut buf = match fs::metadata(&filename).await { Ok(metadata) => Vec::with_capacity(metadata.len() as usize), Err(_) => Vec::new(), }; - file.read_to_end(&mut buf).unwrap(); + file.read_to_end(&mut buf).await.unwrap(); (200, "image/webp", buf) } Err(_) => (404, "text/html", b"File Not Found".to_vec()), @@ -166,3 +167,14 @@ impl Node { info!("Spacedrive Core shutdown successful!"); } } + +/// Error type for Node related errors. +#[derive(Error, Debug)] +pub enum NodeError { + #[error("Failed to create data directory: {0}")] + FailedToCreateDataDirectory(#[from] std::io::Error), + #[error("Failed to initialize config: {0}")] + FailedToInitializeConfig(#[from] node::NodeConfigError), + #[error("Failed to initialize library manager: {0}")] + FailedToInitializeLibraryManager(#[from] library::LibraryManagerError), +} diff --git a/core/thumbnailer/.gitignore b/core/thumbnailer/.gitignore new file mode 100644 index 000000000..defd641a2 --- /dev/null +++ b/core/thumbnailer/.gitignore @@ -0,0 +1,303 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/rust,linux,intellij+all,visualstudiocode,sublimetext,windows,macos,vim,emacs +# Edit at https://www.toptal.com/developers/gitignore?templates=rust,linux,intellij+all,visualstudiocode,sublimetext,windows,macos,vim,emacs + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +### SublimeText ### +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json +sftp-config-alt*.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/rust,linux,intellij+all,visualstudiocode,sublimetext,windows,macos,vim,emacs \ No newline at end of file diff --git a/core/thumbnailer/Cargo.toml b/core/thumbnailer/Cargo.toml new file mode 100644 index 000000000..83508df6b --- /dev/null +++ b/core/thumbnailer/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "thumbnailer" +version = "0.1.0" +authors = ["Ericson Soares "] +edition = "2021" +readme = "README.md" +description = "A simple library to generate video thumbnails using ffmpeg with the webp format" +license = "MIT" +rust-version = "1.64.0" +resolver = "2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ffmpeg-sys-next = "5.1.1" +thiserror = "1.0.35" +webp = "0.2.2" +tokio = { version = "1.21.1", features = ["fs", "rt"] } + +[dev-dependencies] +tempfile = "3.3.0" +tokio = { version = "1.21.1", features = ["fs", "rt", "macros"] } diff --git a/core/thumbnailer/README.md b/core/thumbnailer/README.md new file mode 100644 index 000000000..66f535fd6 --- /dev/null +++ b/core/thumbnailer/README.md @@ -0,0 +1,39 @@ +# FFMPEG Thumbnailer RS + +Rust implementation of a thumbnail generation for video files using ffmpeg. +Based on https://github.com/dirkvdb/ffmpegthumbnailer + +For now only implements the minimum API for Spacedrive needs. PRs are welcome + +## Usage + +```rust + +use ffmpegthumbnailer_rs::{to_thumbnail, ThumbnailerError}; + +#[tokio::main] +async fn main() -> Result<(), ThumbnailerError> { + to_thumbnail("input.mp4", "output.webp", 256, 100.0).await +} + +``` + +Or you can use a builder to change the default options + +```rust + +use ffmpegthumbnailer_rs::{ThumbnailerBuilder, ThumbnailerError}; + +#[tokio::main] +async fn main() -> Result<(), ThumbnailerError> { + let thumbnailer = ThumbnailerBuilder::new() + .width_and_height(420, 315) + .seek_percentage(0.25)? + .with_film_strip(false) + .quality(80.0)? + .build(); + + thumbnailer.process("input.mp4", "output.webp").await +} + +``` \ No newline at end of file diff --git a/core/thumbnailer/src/error.rs b/core/thumbnailer/src/error.rs new file mode 100644 index 000000000..48cb7e0bc --- /dev/null +++ b/core/thumbnailer/src/error.rs @@ -0,0 +1,142 @@ +use std::ffi::c_int; +use std::path::PathBuf; +use thiserror::Error; +use tokio::task::JoinError; + +use ffmpeg_sys_next::{ + AVERROR_BSF_NOT_FOUND, AVERROR_BUFFER_TOO_SMALL, AVERROR_BUG, AVERROR_BUG2, + AVERROR_DECODER_NOT_FOUND, AVERROR_DEMUXER_NOT_FOUND, AVERROR_ENCODER_NOT_FOUND, AVERROR_EOF, + AVERROR_EXIT, AVERROR_EXTERNAL, AVERROR_FILTER_NOT_FOUND, AVERROR_HTTP_BAD_REQUEST, + AVERROR_HTTP_FORBIDDEN, AVERROR_HTTP_NOT_FOUND, AVERROR_HTTP_OTHER_4XX, + AVERROR_HTTP_SERVER_ERROR, AVERROR_HTTP_UNAUTHORIZED, AVERROR_INVALIDDATA, + AVERROR_MUXER_NOT_FOUND, AVERROR_OPTION_NOT_FOUND, AVERROR_PATCHWELCOME, + AVERROR_PROTOCOL_NOT_FOUND, AVERROR_STREAM_NOT_FOUND, AVERROR_UNKNOWN, AVUNERROR, +}; + +/// Error type for the library. +#[derive(Error, Debug)] +pub enum ThumbnailerError { + #[error("I/O Error: {0}")] + Io(#[from] std::io::Error), + #[error("Path conversion error: Path: {0:#?}")] + PathConversion(PathBuf), + #[error("FFMPEG internal error: {0}")] + Ffmpeg(#[from] FfmpegError), + #[error("FFMPEG internal error: {0}; Reason: {1}")] + FfmpegWithReason(FfmpegError, String), + #[error("Failed to decode video frame")] + FrameDecodeError, + #[error("Failed to seek video")] + SeekError, + #[error("Seek not allowed")] + SeekNotAllowed, + #[error("Received an invalid seek percentage: {0}")] + InvalidSeekPercentage(f32), + #[error("Received an invalid quality, expected range [0.0, 100.0], received: {0}")] + InvalidQuality(f32), + #[error("Background task failed: {0}")] + BackgroundTaskFailed(#[from] JoinError), +} + +/// Enum to represent possible errors from FFMPEG library +/// +/// Extracted from https://ffmpeg.org/doxygen/trunk/group__lavu__error.html +#[derive(Error, Debug)] +pub enum FfmpegError { + #[error("Bitstream filter not found")] + BitstreamFilterNotFound, + #[error("Internal bug, also see AVERROR_BUG2")] + InternalBug, + #[error("Buffer too small")] + BufferTooSmall, + #[error("Decoder not found")] + DecoderNotFound, + #[error("Demuxer not found")] + DemuxerNotFound, + #[error("Encoder not found")] + EncoderNotFound, + #[error("End of file")] + Eof, + #[error("Immediate exit was requested; the called function should not be restarted")] + Exit, + #[error("Generic error in an external library")] + External, + #[error("Filter not found")] + FilterNotFound, + #[error("Invalid data found when processing input")] + InvalidData, + #[error("Muxer not found")] + MuxerNotFound, + #[error("Option not found")] + OptionNotFound, + #[error("Not yet implemented in FFmpeg, patches welcome")] + NotImplemented, + #[error("Protocol not found")] + ProtocolNotFound, + #[error("Stream not found")] + StreamNotFound, + #[error("This is semantically identical to AVERROR_BUG it has been introduced in Libav after our AVERROR_BUG and with a modified value")] + InternalBug2, + #[error("Unknown error, typically from an external library")] + Unknown, + #[error("Requested feature is flagged experimental. Set strict_std_compliance if you really want to use it")] + Experimental, + #[error("Input changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_OUTPUT_CHANGED)")] + InputChanged, + #[error("Output changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_INPUT_CHANGED)")] + OutputChanged, + #[error("HTTP Bad Request: 400")] + HttpBadRequest, + #[error("HTTP Unauthorized: 401")] + HttpUnauthorized, + #[error("HTTP Forbidden: 403")] + HttpForbidden, + #[error("HTTP Not Found: 404")] + HttpNotFound, + #[error("Other HTTP error: 4xx")] + HttpOther4xx, + #[error("HTTP Internal Server Error: 500")] + HttpServerError, + #[error("Other OS error, errno = {0}")] + OtherOSError(c_int), + #[error("Frame allocation error")] + FrameAllocation, + #[error("Video Codec allocation error")] + VideoCodecAllocation, + #[error("Filter Graph allocation error")] + FilterGraphAllocation, + #[error("Codec Open Error")] + CodecOpen, +} + +impl From for FfmpegError { + fn from(code: c_int) -> Self { + match code { + AVERROR_BSF_NOT_FOUND => FfmpegError::BitstreamFilterNotFound, + AVERROR_BUG => FfmpegError::InternalBug, + AVERROR_BUFFER_TOO_SMALL => FfmpegError::BufferTooSmall, + AVERROR_DECODER_NOT_FOUND => FfmpegError::DecoderNotFound, + AVERROR_DEMUXER_NOT_FOUND => FfmpegError::DemuxerNotFound, + AVERROR_ENCODER_NOT_FOUND => FfmpegError::EncoderNotFound, + AVERROR_EOF => FfmpegError::Eof, + AVERROR_EXIT => FfmpegError::Exit, + AVERROR_EXTERNAL => FfmpegError::External, + AVERROR_FILTER_NOT_FOUND => FfmpegError::FilterNotFound, + AVERROR_INVALIDDATA => FfmpegError::InvalidData, + AVERROR_MUXER_NOT_FOUND => FfmpegError::MuxerNotFound, + AVERROR_OPTION_NOT_FOUND => FfmpegError::OptionNotFound, + AVERROR_PATCHWELCOME => FfmpegError::NotImplemented, + AVERROR_PROTOCOL_NOT_FOUND => FfmpegError::ProtocolNotFound, + AVERROR_STREAM_NOT_FOUND => FfmpegError::StreamNotFound, + AVERROR_BUG2 => FfmpegError::InternalBug2, + AVERROR_UNKNOWN => FfmpegError::Unknown, + AVERROR_HTTP_BAD_REQUEST => FfmpegError::HttpBadRequest, + AVERROR_HTTP_UNAUTHORIZED => FfmpegError::HttpUnauthorized, + AVERROR_HTTP_FORBIDDEN => FfmpegError::HttpForbidden, + AVERROR_HTTP_NOT_FOUND => FfmpegError::HttpNotFound, + AVERROR_HTTP_OTHER_4XX => FfmpegError::HttpOther4xx, + AVERROR_HTTP_SERVER_ERROR => FfmpegError::HttpServerError, + other => FfmpegError::OtherOSError(AVUNERROR(other)), + } + } +} diff --git a/core/thumbnailer/src/film_strip.rs b/core/thumbnailer/src/film_strip.rs new file mode 100644 index 000000000..c3f09e776 --- /dev/null +++ b/core/thumbnailer/src/film_strip.rs @@ -0,0 +1,693 @@ +use crate::video_frame::VideoFrame; + +static FILM_STRIP_4: [u8; 4 * 4 * 3] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 107, 107, 107, 135, 135, 135, 55, 55, 55, 0, 0, 0, + 159, 159, 159, 195, 195, 195, 82, 82, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +static FILM_STRIP_8: [u8; 8 * 8 * 3] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 32, 32, 55, 55, 55, 58, + 58, 58, 58, 58, 58, 52, 52, 52, 1, 1, 1, 0, 0, 0, 2, 2, 2, 133, 133, 133, 208, 208, 208, 219, + 219, 219, 219, 219, 219, 203, 203, 203, 26, 26, 26, 0, 0, 0, 2, 2, 2, 158, 158, 158, 240, 240, + 240, 251, 251, 251, 251, 251, 251, 235, 235, 235, 31, 31, 31, 0, 0, 0, 0, 0, 0, 70, 70, 70, + 115, 115, 115, 121, 121, 121, 121, 121, 121, 110, 110, 110, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, +]; + +static FILM_STRIP_16: [u8; 16 * 16 * 3] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 56, 56, 56, 89, 89, 89, 114, 114, 114, 124, 124, 124, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 128, 122, 122, 122, 109, 109, 109, 19, 19, 19, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 9, 9, 9, 89, 89, 89, 140, 140, 140, 175, 175, 175, 190, 190, 190, 194, 194, 194, + 194, 194, 194, 194, 194, 194, 193, 193, 193, 187, 187, 187, 168, 168, 168, 64, 64, 64, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 12, 113, 113, 113, 175, 175, 175, 214, 214, 214, 231, 231, + 231, 235, 235, 235, 236, 236, 236, 236, 236, 236, 235, 235, 235, 228, 228, 228, 207, 207, 207, + 80, 80, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 13, 123, 123, 123, 188, 188, 188, 229, + 229, 229, 245, 245, 245, 250, 250, 250, 251, 251, 251, 251, 251, 251, 249, 249, 249, 243, 243, + 243, 221, 221, 221, 86, 86, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 12, 120, 120, 120, + 184, 184, 184, 224, 224, 224, 241, 241, 241, 245, 245, 245, 246, 246, 246, 246, 246, 246, 245, + 245, 245, 238, 238, 238, 217, 217, 217, 85, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 103, 103, 103, 160, 160, 160, 198, 198, 198, 214, 214, 214, 218, 218, 218, 220, 220, 220, + 220, 220, 220, 218, 218, 218, 212, 212, 212, 191, 191, 191, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 26, 26, 26, 32, 32, 32, 35, 35, 35, 36, 36, 36, 36, 36, 36, 36, + 36, 36, 36, 36, 36, 35, 35, 35, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +static FILM_STRIP_32: [u8; 32 * 32 * 3] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, + 23, 23, 23, 28, 28, 28, 32, 32, 32, 34, 34, 34, 36, 36, 36, 37, 37, 37, 37, 37, 37, 38, 38, 38, + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 37, 37, 37, 37, 37, 37, 35, 35, 35, + 29, 29, 29, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 28, 28, 54, 54, 54, 69, 69, 69, 83, 83, 83, 93, 93, 93, + 101, 101, 101, 105, 105, 105, 108, 108, 108, 109, 109, 109, 111, 111, 111, 110, 110, 110, 110, + 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 109, 109, 109, 107, 107, 107, 103, 103, + 103, 97, 97, 97, 88, 88, 88, 13, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 54, 54, 54, 74, 74, 74, 93, 93, 93, 110, 110, + 110, 124, 124, 124, 133, 133, 133, 139, 139, 139, 143, 143, 143, 144, 144, 144, 145, 145, 145, + 145, 145, 145, 145, 145, 145, 145, 145, 145, 146, 146, 146, 145, 145, 145, 144, 144, 144, 141, + 141, 141, 136, 136, 136, 129, 129, 129, 118, 118, 118, 88, 88, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 69, 69, 69, 93, 93, + 93, 117, 117, 117, 138, 138, 138, 154, 154, 154, 165, 165, 165, 172, 172, 172, 176, 176, 176, + 178, 178, 178, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 178, + 178, 178, 177, 177, 177, 174, 174, 174, 170, 170, 170, 161, 161, 161, 146, 146, 146, 128, 128, + 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 28, 28, 28, 82, 82, 82, 110, 110, 110, 138, 138, 138, 162, 162, 162, 180, 180, 180, 192, 192, + 192, 200, 200, 200, 204, 204, 204, 206, 206, 206, 207, 207, 207, 207, 207, 207, 207, 207, 207, + 207, 207, 207, 207, 207, 207, 207, 207, 207, 205, 205, 205, 202, 202, 202, 197, 197, 197, 187, + 187, 187, 172, 172, 172, 151, 151, 151, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 32, 32, 92, 92, 92, 124, 124, 124, 154, 154, 154, 180, + 180, 180, 199, 199, 199, 212, 212, 212, 220, 220, 220, 225, 225, 225, 226, 226, 226, 227, 227, + 227, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 227, 227, 227, 226, 226, 226, + 223, 223, 223, 217, 217, 217, 207, 207, 207, 191, 191, 191, 168, 168, 168, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 100, 100, 100, + 134, 134, 134, 165, 165, 165, 192, 192, 192, 212, 212, 212, 226, 226, 226, 234, 234, 234, 238, + 238, 238, 240, 240, 240, 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, + 241, 241, 241, 241, 240, 240, 240, 236, 236, 236, 230, 230, 230, 220, 220, 220, 203, 203, 203, + 180, 180, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 36, 36, 36, 104, 104, 104, 138, 138, 138, 171, 171, 171, 199, 199, 199, 219, 219, 219, + 233, 233, 233, 240, 240, 240, 245, 245, 245, 247, 247, 247, 248, 248, 248, 248, 248, 248, 248, + 248, 248, 248, 248, 248, 248, 248, 248, 247, 247, 247, 246, 246, 246, 243, 243, 243, 237, 237, + 237, 227, 227, 227, 210, 210, 210, 186, 186, 186, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 36, 36, 105, 105, 105, 140, 140, 140, 173, + 173, 173, 201, 201, 201, 222, 222, 222, 235, 235, 235, 243, 243, 243, 248, 248, 248, 250, 250, + 250, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 250, 250, 250, + 249, 249, 249, 246, 246, 246, 240, 240, 240, 229, 229, 229, 212, 212, 212, 188, 188, 188, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 36, 36, + 104, 104, 104, 138, 138, 138, 171, 171, 171, 199, 199, 199, 219, 219, 219, 233, 233, 233, 240, + 240, 240, 245, 245, 245, 247, 247, 247, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, + 248, 248, 248, 248, 247, 247, 247, 246, 246, 246, 243, 243, 243, 237, 237, 237, 227, 227, 227, + 210, 210, 210, 186, 186, 186, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 100, 100, 100, 134, 134, 134, 165, 165, 165, 192, 192, 192, + 212, 212, 212, 226, 226, 226, 234, 234, 234, 238, 238, 238, 240, 240, 240, 241, 241, 241, 241, + 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, 241, 240, 240, 240, 236, 236, + 236, 230, 230, 230, 220, 220, 220, 203, 203, 203, 180, 180, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 19, 19, 92, 92, 92, 124, 124, + 124, 154, 154, 154, 180, 180, 180, 200, 200, 200, 212, 212, 212, 220, 220, 220, 225, 225, 225, + 226, 226, 226, 227, 227, 227, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, 227, + 227, 227, 226, 226, 226, 223, 223, 223, 217, 217, 217, 207, 207, 207, 191, 191, 191, 146, 146, + 146, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 59, 59, 59, 110, 110, 110, 138, 138, 138, 162, 162, 162, 180, 180, 180, 193, 193, 193, + 200, 200, 200, 204, 204, 204, 206, 206, 206, 207, 207, 207, 208, 208, 208, 208, 208, 208, 208, + 208, 208, 208, 208, 208, 207, 207, 207, 205, 205, 205, 203, 203, 203, 197, 197, 197, 187, 187, + 187, 172, 172, 172, 27, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 27, 27, 58, 58, 58, 69, 69, 69, 77, 77, 77, + 83, 83, 83, 86, 86, 86, 88, 88, 88, 89, 89, 89, 89, 89, 89, 90, 90, 90, 90, 90, 90, 90, 90, 90, + 90, 90, 90, 89, 89, 89, 88, 88, 88, 87, 87, 87, 85, 85, 85, 70, 70, 70, 8, 8, 8, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, +]; + +static FILM_STRIP_64: [u8; 64 * 64 * 3] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 16, 34, 34, 34, 47, + 47, 47, 54, 54, 54, 59, 59, 59, 64, 64, 64, 68, 68, 68, 72, 72, 72, 75, 75, 75, 77, 77, 77, 79, + 79, 79, 81, 81, 81, 82, 82, 82, 82, 82, 82, 83, 83, 83, 83, 83, 83, 84, 84, 84, 84, 84, 84, 84, + 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, + 84, 84, 83, 83, 83, 83, 83, 83, 82, 82, 82, 82, 82, 82, 81, 81, 81, 79, 79, 79, 77, 77, 77, 72, + 72, 72, 57, 57, 57, 30, 30, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 30, 30, 30, 46, 46, + 46, 52, 52, 52, 59, 59, 59, 66, 66, 66, 72, 72, 72, 78, 78, 78, 82, 82, 82, 87, 87, 87, 90, 90, + 90, 93, 93, 93, 95, 95, 95, 97, 97, 97, 98, 98, 98, 99, 99, 99, 100, 100, 100, 100, 100, 100, + 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, + 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 100, 100, 100, 100, 100, + 100, 99, 99, 99, 98, 98, 98, 97, 97, 97, 95, 95, 95, 93, 93, 93, 90, 90, 90, 87, 87, 87, 82, + 82, 82, 61, 61, 61, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 30, 30, 47, 47, 47, 55, 55, 55, 63, 63, 63, + 71, 71, 71, 78, 78, 78, 86, 86, 86, 92, 92, 92, 98, 98, 98, 103, 103, 103, 107, 107, 107, 110, + 110, 110, 112, 112, 112, 114, 114, 114, 116, 116, 116, 117, 117, 117, 118, 118, 118, 118, 118, + 118, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, + 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 118, 118, 118, 118, + 118, 118, 117, 117, 117, 116, 116, 116, 114, 114, 114, 113, 113, 113, 110, 110, 110, 107, 107, + 107, 103, 103, 103, 98, 98, 98, 92, 92, 92, 67, 67, 67, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 16, 46, 46, 46, 54, 54, + 54, 64, 64, 64, 73, 73, 73, 82, 82, 82, 91, 91, 91, 99, 99, 99, 106, 106, 106, 113, 113, 113, + 118, 118, 118, 123, 123, 123, 126, 126, 126, 129, 129, 129, 131, 131, 131, 133, 133, 133, 134, + 134, 134, 135, 135, 135, 135, 135, 135, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, + 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, + 136, 136, 136, 135, 135, 135, 135, 135, 135, 134, 134, 134, 133, 133, 133, 131, 131, 131, 129, + 129, 129, 126, 126, 126, 123, 123, 123, 118, 118, 118, 113, 113, 113, 106, 106, 106, 99, 99, + 99, 41, 41, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 34, 34, 34, 52, 52, 52, 62, 62, 62, 73, 73, 73, 83, 83, 83, 94, 94, 94, 104, + 104, 104, 113, 113, 113, 121, 121, 121, 128, 128, 128, 134, 134, 134, 139, 139, 139, 143, 143, + 143, 146, 146, 146, 149, 149, 149, 150, 150, 150, 152, 152, 152, 152, 152, 152, 153, 153, 153, + 153, 153, 153, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, + 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 153, 153, 153, 153, 153, 153, 153, 153, + 153, 152, 152, 152, 150, 150, 150, 149, 149, 149, 146, 146, 146, 143, 143, 143, 139, 139, 139, + 134, 134, 134, 128, 128, 128, 121, 121, 121, 113, 113, 113, 82, 82, 82, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 47, 47, 59, 59, 59, + 71, 71, 71, 82, 82, 82, 94, 94, 94, 105, 105, 105, 116, 116, 116, 126, 126, 126, 135, 135, 135, + 143, 143, 143, 150, 150, 150, 155, 155, 155, 159, 159, 159, 163, 163, 163, 165, 165, 165, 167, + 167, 167, 169, 169, 169, 169, 169, 169, 170, 170, 170, 170, 170, 170, 171, 171, 171, 171, 171, + 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, + 171, 171, 171, 170, 170, 170, 170, 170, 170, 169, 169, 169, 169, 169, 169, 167, 167, 167, 165, + 165, 165, 163, 163, 163, 160, 160, 160, 155, 155, 155, 150, 150, 150, 143, 143, 143, 135, 135, + 135, 126, 126, 126, 111, 111, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 54, 54, 54, 66, 66, 66, 78, 78, 78, 91, 91, 91, 104, 104, 104, + 116, 116, 116, 128, 128, 128, 139, 139, 139, 148, 148, 148, 157, 157, 157, 164, 164, 164, 169, + 169, 169, 174, 174, 174, 178, 178, 178, 180, 180, 180, 182, 182, 182, 183, 183, 183, 184, 184, + 184, 185, 185, 185, 185, 185, 185, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, + 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 185, + 185, 185, 184, 184, 184, 184, 184, 184, 182, 182, 182, 180, 180, 180, 178, 178, 178, 174, 174, + 174, 170, 170, 170, 164, 164, 164, 157, 157, 157, 148, 148, 148, 139, 139, 139, 128, 128, 128, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 59, 59, 59, 72, 72, 72, 85, 85, 85, 99, 99, 99, 113, 113, 113, 126, 126, 126, 139, 139, 139, + 150, 150, 150, 161, 161, 161, 169, 169, 169, 177, 177, 177, 183, 183, 183, 188, 188, 188, 191, + 191, 191, 194, 194, 194, 196, 196, 196, 197, 197, 197, 198, 198, 198, 199, 199, 199, 199, 199, + 199, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 199, 199, 199, 198, 198, 198, 198, + 198, 198, 196, 196, 196, 194, 194, 194, 191, 191, 191, 188, 188, 188, 183, 183, 183, 177, 177, + 177, 169, 169, 169, 161, 161, 161, 150, 150, 150, 139, 139, 139, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 64, 64, 77, 77, 77, 92, 92, + 92, 106, 106, 106, 121, 121, 121, 135, 135, 135, 149, 149, 149, 161, 161, 161, 172, 172, 172, + 181, 181, 181, 189, 189, 189, 195, 195, 195, 200, 200, 200, 204, 204, 204, 207, 207, 207, 209, + 209, 209, 210, 210, 210, 211, 211, 211, 212, 212, 212, 212, 212, 212, 213, 213, 213, 213, 213, + 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, + 213, 213, 213, 213, 213, 213, 212, 212, 212, 211, 211, 211, 210, 210, 210, 209, 209, 209, 207, + 207, 207, 204, 204, 204, 200, 200, 200, 195, 195, 195, 189, 189, 189, 181, 181, 181, 172, 172, + 172, 161, 161, 161, 149, 149, 149, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 68, 68, 82, 82, 82, 97, 97, 97, 113, 113, 113, 128, 128, + 128, 143, 143, 143, 157, 157, 157, 170, 170, 170, 181, 181, 181, 190, 190, 190, 199, 199, 199, + 205, 205, 205, 210, 210, 210, 214, 214, 214, 217, 217, 217, 219, 219, 219, 220, 220, 220, 221, + 221, 221, 222, 222, 222, 222, 222, 222, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, + 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, + 222, 222, 222, 221, 221, 221, 220, 220, 220, 219, 219, 219, 217, 217, 217, 214, 214, 214, 210, + 210, 210, 205, 205, 205, 199, 199, 199, 191, 191, 191, 181, 181, 181, 170, 170, 170, 157, 157, + 157, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 72, 72, 72, 87, 87, 87, 102, 102, 102, 118, 118, 118, 134, 134, 134, 150, 150, 150, 164, + 164, 164, 177, 177, 177, 189, 189, 189, 198, 198, 198, 207, 207, 207, 213, 213, 213, 218, 218, + 218, 222, 222, 222, 225, 225, 225, 227, 227, 227, 229, 229, 229, 229, 229, 229, 230, 230, 230, + 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, + 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 230, 230, 230, 230, 230, + 230, 229, 229, 229, 227, 227, 227, 225, 225, 225, 222, 222, 222, 218, 218, 218, 213, 213, 213, + 207, 207, 207, 198, 198, 198, 189, 189, 189, 177, 177, 177, 164, 164, 164, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 75, 75, 75, 90, 90, 90, + 106, 106, 106, 123, 123, 123, 139, 139, 139, 155, 155, 155, 170, 170, 170, 183, 183, 183, 195, + 195, 195, 205, 205, 205, 213, 213, 213, 220, 220, 220, 225, 225, 225, 229, 229, 229, 232, 232, + 232, 234, 234, 234, 236, 236, 236, 236, 236, 236, 237, 237, 237, 238, 238, 238, 238, 238, 238, + 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, + 238, 238, 238, 238, 238, 238, 238, 238, 237, 237, 237, 237, 237, 237, 236, 236, 236, 234, 234, + 234, 232, 232, 232, 229, 229, 229, 225, 225, 225, 220, 220, 220, 213, 213, 213, 205, 205, 205, + 195, 195, 195, 183, 183, 183, 170, 170, 170, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 77, 77, 93, 93, 93, 109, 109, 109, 126, 126, 126, + 143, 143, 143, 159, 159, 159, 174, 174, 174, 188, 188, 188, 200, 200, 200, 210, 210, 210, 218, + 218, 218, 225, 225, 225, 230, 230, 230, 234, 234, 234, 237, 237, 237, 239, 239, 239, 241, 241, + 241, 242, 242, 242, 242, 242, 242, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, + 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, + 243, 243, 242, 242, 242, 242, 242, 242, 241, 241, 241, 239, 239, 239, 237, 237, 237, 234, 234, + 234, 230, 230, 230, 225, 225, 225, 218, 218, 218, 210, 210, 210, 200, 200, 200, 188, 188, 188, + 174, 174, 174, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 78, 78, 78, 94, 94, 94, 111, 111, 111, 128, 128, 128, 145, 145, 145, 162, 162, 162, + 177, 177, 177, 191, 191, 191, 203, 203, 203, 213, 213, 213, 221, 221, 221, 228, 228, 228, 233, + 233, 233, 237, 237, 237, 240, 240, 240, 242, 242, 242, 244, 244, 244, 245, 245, 245, 245, 245, + 245, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, + 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 245, 245, 245, 245, + 245, 245, 244, 244, 244, 242, 242, 242, 240, 240, 240, 237, 237, 237, 233, 233, 233, 228, 228, + 228, 221, 221, 221, 213, 213, 213, 203, 203, 203, 191, 191, 191, 177, 177, 177, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 96, + 96, 96, 113, 113, 113, 130, 130, 130, 147, 147, 147, 164, 164, 164, 179, 179, 179, 193, 193, + 193, 205, 205, 205, 215, 215, 215, 224, 224, 224, 230, 230, 230, 236, 236, 236, 239, 239, 239, + 242, 242, 242, 244, 244, 244, 246, 246, 246, 247, 247, 247, 247, 247, 247, 248, 248, 248, 248, + 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, + 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 247, 247, 247, 247, 247, 247, 246, 246, 246, + 244, 244, 244, 242, 242, 242, 240, 240, 240, 236, 236, 236, 230, 230, 230, 224, 224, 224, 215, + 215, 215, 205, 205, 205, 193, 193, 193, 179, 179, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 96, 96, 96, 113, 113, 113, + 131, 131, 131, 148, 148, 148, 165, 165, 165, 180, 180, 180, 194, 194, 194, 206, 206, 206, 217, + 217, 217, 225, 225, 225, 232, 232, 232, 237, 237, 237, 241, 241, 241, 244, 244, 244, 246, 246, + 246, 248, 248, 248, 249, 249, 249, 249, 249, 249, 250, 250, 250, 250, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 249, 249, 249, 249, 249, 249, 248, 248, 248, 246, 246, 246, 244, 244, + 244, 241, 241, 241, 237, 237, 237, 232, 232, 232, 225, 225, 225, 217, 217, 217, 206, 206, 206, + 194, 194, 194, 180, 180, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 96, 96, 96, 113, 113, 113, 131, 131, 131, 148, 148, 148, + 165, 165, 165, 180, 180, 180, 194, 194, 194, 206, 206, 206, 217, 217, 217, 225, 225, 225, 232, + 232, 232, 237, 237, 237, 241, 241, 241, 244, 244, 244, 246, 246, 246, 248, 248, 248, 249, 249, + 249, 249, 249, 249, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 249, + 249, 249, 249, 249, 249, 248, 248, 248, 246, 246, 246, 244, 244, 244, 241, 241, 241, 237, 237, + 237, 232, 232, 232, 225, 225, 225, 217, 217, 217, 206, 206, 206, 194, 194, 194, 180, 180, 180, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 80, 80, 80, 96, 96, 96, 113, 113, 113, 130, 130, 130, 147, 147, 147, 164, 164, 164, 179, 179, + 179, 193, 193, 193, 205, 205, 205, 215, 215, 215, 224, 224, 224, 230, 230, 230, 236, 236, 236, + 239, 239, 239, 242, 242, 242, 244, 244, 244, 246, 246, 246, 247, 247, 247, 247, 247, 247, 248, + 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, + 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 247, 247, 247, 247, 247, 247, + 246, 246, 246, 244, 244, 244, 242, 242, 242, 240, 240, 240, 236, 236, 236, 230, 230, 230, 224, + 224, 224, 215, 215, 215, 205, 205, 205, 193, 193, 193, 179, 179, 179, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 78, 78, 94, 94, 94, + 111, 111, 111, 128, 128, 128, 145, 145, 145, 162, 162, 162, 177, 177, 177, 191, 191, 191, 203, + 203, 203, 213, 213, 213, 221, 221, 221, 228, 228, 228, 233, 233, 233, 237, 237, 237, 240, 240, + 240, 242, 242, 242, 244, 244, 244, 245, 245, 245, 245, 245, 245, 246, 246, 246, 246, 246, 246, + 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, 246, + 246, 246, 246, 246, 246, 246, 246, 246, 245, 245, 245, 245, 245, 245, 244, 244, 244, 242, 242, + 242, 240, 240, 240, 237, 237, 237, 233, 233, 233, 228, 228, 228, 221, 221, 221, 213, 213, 213, + 203, 203, 203, 191, 191, 191, 177, 177, 177, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 77, 77, 93, 93, 93, 109, 109, 109, 126, 126, 126, + 143, 143, 143, 159, 159, 159, 174, 174, 174, 188, 188, 188, 200, 200, 200, 210, 210, 210, 218, + 218, 218, 225, 225, 225, 230, 230, 230, 234, 234, 234, 237, 237, 237, 239, 239, 239, 241, 241, + 241, 242, 242, 242, 242, 242, 242, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, + 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, 243, + 243, 243, 242, 242, 242, 242, 242, 242, 241, 241, 241, 239, 239, 239, 237, 237, 237, 234, 234, + 234, 230, 230, 230, 225, 225, 225, 218, 218, 218, 210, 210, 210, 200, 200, 200, 188, 188, 188, + 174, 174, 174, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 72, 72, 72, 90, 90, 90, 106, 106, 106, 123, 123, 123, 139, 139, 139, 155, 155, 155, + 170, 170, 170, 183, 183, 183, 195, 195, 195, 205, 205, 205, 213, 213, 213, 220, 220, 220, 225, + 225, 225, 229, 229, 229, 232, 232, 232, 234, 234, 234, 236, 236, 236, 236, 236, 236, 237, 237, + 237, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, + 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 237, 237, 237, 237, + 237, 237, 236, 236, 236, 234, 234, 234, 232, 232, 232, 229, 229, 229, 225, 225, 225, 220, 220, + 220, 213, 213, 213, 205, 205, 205, 195, 195, 195, 183, 183, 183, 163, 163, 163, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 57, 57, 87, + 87, 87, 102, 102, 102, 118, 118, 118, 134, 134, 134, 150, 150, 150, 164, 164, 164, 177, 177, + 177, 189, 189, 189, 198, 198, 198, 207, 207, 207, 213, 213, 213, 218, 218, 218, 222, 222, 222, + 225, 225, 225, 227, 227, 227, 229, 229, 229, 229, 229, 229, 230, 230, 230, 231, 231, 231, 231, + 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, + 231, 231, 231, 231, 231, 231, 231, 231, 231, 231, 230, 230, 230, 230, 230, 230, 229, 229, 229, + 227, 227, 227, 225, 225, 225, 222, 222, 222, 218, 218, 218, 213, 213, 213, 207, 207, 207, 198, + 198, 198, 189, 189, 189, 177, 177, 177, 129, 129, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 30, 30, 82, 82, 82, 97, 97, 97, 113, + 113, 113, 128, 128, 128, 143, 143, 143, 157, 157, 157, 170, 170, 170, 181, 181, 181, 191, 191, + 191, 199, 199, 199, 205, 205, 205, 210, 210, 210, 214, 214, 214, 217, 217, 217, 219, 219, 219, + 220, 220, 220, 221, 221, 221, 222, 222, 222, 222, 222, 222, 223, 223, 223, 223, 223, 223, 223, + 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, + 223, 223, 223, 223, 222, 222, 222, 221, 221, 221, 221, 221, 221, 219, 219, 219, 217, 217, 217, + 214, 214, 214, 210, 210, 210, 205, 205, 205, 199, 199, 199, 191, 191, 191, 181, 181, 181, 170, + 170, 170, 71, 71, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61, 61, 61, 92, 92, 92, 106, 106, 106, 121, 121, 121, 135, 135, + 135, 149, 149, 149, 161, 161, 161, 172, 172, 172, 181, 181, 181, 189, 189, 189, 195, 195, 195, + 200, 200, 200, 204, 204, 204, 207, 207, 207, 209, 209, 209, 210, 210, 210, 211, 211, 211, 212, + 212, 212, 212, 212, 212, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, + 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 212, 212, 212, + 211, 211, 211, 210, 210, 210, 209, 209, 209, 207, 207, 207, 204, 204, 204, 200, 200, 200, 195, + 195, 195, 189, 189, 189, 181, 181, 181, 172, 172, 172, 126, 126, 126, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, + 67, 67, 67, 99, 99, 99, 113, 113, 113, 126, 126, 126, 139, 139, 139, 151, 151, 151, 161, 161, + 161, 170, 170, 170, 177, 177, 177, 184, 184, 184, 188, 188, 188, 192, 192, 192, 195, 195, 195, + 197, 197, 197, 198, 198, 198, 199, 199, 199, 200, 200, 200, 200, 200, 200, 201, 201, 201, 201, + 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, + 201, 201, 201, 201, 200, 200, 200, 200, 200, 200, 199, 199, 199, 198, 198, 198, 197, 197, 197, + 195, 195, 195, 192, 192, 192, 189, 189, 189, 184, 184, 184, 178, 178, 178, 170, 170, 170, 126, + 126, 126, 18, 18, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 41, 41, 82, 82, 82, 111, 111, 111, + 128, 128, 128, 139, 139, 139, 149, 149, 149, 157, 157, 157, 164, 164, 164, 170, 170, 170, 175, + 175, 175, 178, 178, 178, 181, 181, 181, 183, 183, 183, 184, 184, 184, 185, 185, 185, 186, 186, + 186, 186, 186, 186, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, + 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 186, 186, 186, 186, 186, 186, 185, + 185, 185, 184, 184, 184, 183, 183, 183, 181, 181, 181, 178, 178, 178, 175, 175, 175, 163, 163, + 163, 129, 129, 129, 71, 71, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, +]; + +struct FilmStrip { + width: u32, + height: u32, + strip: Option<&'static [u8]>, +} + +pub(crate) fn film_strip_filter(video_frame: &mut VideoFrame) { + let FilmStrip { + width, + height, + strip, + } = determine_film_strip(video_frame.width); + + if let Some(strip) = strip { + let mut frame_index = 0; + let mut film_hole_index = 0; + let offset = ((video_frame.width * 3) - 3) as usize; + + for i in 0..(video_frame.height as usize) { + for j in (0..(width as usize * 3)).step_by(3) { + let current_stripe_index = film_hole_index + j; + + video_frame.data[frame_index + j] = strip[current_stripe_index]; + video_frame.data[frame_index + j + 1] = strip[current_stripe_index + 1]; + video_frame.data[frame_index + j + 2] = strip[current_stripe_index + 2]; + + video_frame.data[frame_index + offset - j] = strip[current_stripe_index]; + video_frame.data[frame_index + offset - j + 1] = strip[current_stripe_index + 1]; + video_frame.data[frame_index + offset - j + 2] = strip[current_stripe_index + 2]; + } + + frame_index += video_frame.line_size as usize; + film_hole_index = (i % height as usize) * width as usize * 3; + } + } +} + +fn determine_film_strip(video_width: u32) -> FilmStrip { + match video_width { + // We consider that the smallest film strip is 4, doubling it for each side, we have 8 pixels + 0..=8 => FilmStrip { + width: 0, + height: 0, + strip: None, + }, + 9..=96 => FilmStrip { + width: 4, + height: 4, + strip: Some(&FILM_STRIP_4), + }, + 97..=192 => FilmStrip { + width: 8, + height: 8, + strip: Some(&FILM_STRIP_8), + }, + 193..=384 => FilmStrip { + width: 16, + height: 16, + strip: Some(&FILM_STRIP_16), + }, + 385..=768 => FilmStrip { + width: 32, + height: 32, + strip: Some(&FILM_STRIP_32), + }, + _ => FilmStrip { + width: 64, + height: 64, + strip: Some(&FILM_STRIP_64), + }, + } +} diff --git a/core/thumbnailer/src/lib.rs b/core/thumbnailer/src/lib.rs new file mode 100644 index 000000000..e1b6db295 --- /dev/null +++ b/core/thumbnailer/src/lib.rs @@ -0,0 +1,108 @@ +use crate::{ + film_strip::film_strip_filter, + movie_decoder::{MovieDecoder, ThumbnailSize}, + video_frame::VideoFrame, +}; + +use std::path::Path; + +mod error; +mod film_strip; +mod movie_decoder; +mod thumbnailer; +mod utils; +mod video_frame; + +pub use error::ThumbnailerError; +pub use thumbnailer::{Thumbnailer, ThumbnailerBuilder}; + +/// Helper function to generate a thumbnail file from a video file with reasonable defaults +pub async fn to_thumbnail( + video_file_path: impl AsRef, + output_thumbnail_path: impl AsRef, + size: u32, + quality: f32, +) -> Result<(), ThumbnailerError> { + ThumbnailerBuilder::new() + .with_film_strip(false) + .size(size) + .quality(quality)? + .build() + .process(video_file_path, output_thumbnail_path) + .await +} + +/// Helper function to generate a thumbnail bytes from a video file with reasonable defaults +pub async fn to_webp_bytes( + video_file_path: impl AsRef, + size: u32, + quality: f32, +) -> Result, ThumbnailerError> { + ThumbnailerBuilder::new() + .size(size) + .quality(quality)? + .build() + .process_to_webp_bytes(video_file_path) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + use tokio::fs; + + #[tokio::test] + #[ignore] + async fn test_all_files() { + let video_file_path = [ + Path::new("./samples/video_01.mp4"), + Path::new("./samples/video_02.mov"), + Path::new("./samples/video_03.mov"), + Path::new("./samples/video_04.mov"), + Path::new("./samples/video_05.mov"), + Path::new("./samples/video_06.mov"), + Path::new("./samples/video_07.mp4"), + Path::new("./samples/video_08.mov"), + Path::new("./samples/video_09.MP4"), + ]; + + let expected_webp_files = [ + Path::new("./samples/video_01.webp"), + Path::new("./samples/video_02.webp"), + Path::new("./samples/video_03.webp"), + Path::new("./samples/video_04.webp"), + Path::new("./samples/video_05.webp"), + Path::new("./samples/video_06.webp"), + Path::new("./samples/video_07.webp"), + Path::new("./samples/video_08.webp"), + Path::new("./samples/video_09.webp"), + ]; + + let root = tempdir().unwrap(); + let actual_webp_files = [ + root.path().join("video_01.webp"), + root.path().join("video_02.webp"), + root.path().join("video_03.webp"), + root.path().join("video_04.webp"), + root.path().join("video_05.webp"), + root.path().join("video_06.webp"), + root.path().join("video_07.webp"), + root.path().join("video_08.webp"), + root.path().join("video_09.webp"), + ]; + + for (input, output) in video_file_path.iter().zip(actual_webp_files.iter()) { + if let Err(e) = to_thumbnail(input, output, 128, 100.0).await { + eprintln!("Error: {e}; Input: {}", input.display()); + panic!("{}", e); + } + } + + for (expected, actual) in expected_webp_files.iter().zip(actual_webp_files.iter()) { + let expected_bytes = fs::read(expected).await.unwrap(); + let actual_bytes = fs::read(actual).await.unwrap(); + assert_eq!(expected_bytes, actual_bytes); + } + } +} diff --git a/core/thumbnailer/src/movie_decoder.rs b/core/thumbnailer/src/movie_decoder.rs new file mode 100644 index 000000000..795eabe4c --- /dev/null +++ b/core/thumbnailer/src/movie_decoder.rs @@ -0,0 +1,793 @@ +use crate::{ + error::{FfmpegError, ThumbnailerError}, + utils::from_path, + video_frame::{FfmpegFrame, FrameSource, VideoFrame}, +}; + +use ffmpeg_sys_next::{ + av_buffersink_get_frame, av_buffersrc_write_frame, av_dict_get, av_display_rotation_get, + av_frame_alloc, av_frame_free, av_guess_sample_aspect_ratio, av_packet_alloc, av_packet_free, + av_packet_unref, av_read_frame, av_seek_frame, av_stream_get_side_data, avcodec_alloc_context3, + avcodec_find_decoder, avcodec_flush_buffers, avcodec_free_context, avcodec_open2, + avcodec_parameters_to_context, avcodec_receive_frame, avcodec_send_packet, + avfilter_get_by_name, avfilter_graph_alloc, avfilter_graph_config, + avfilter_graph_create_filter, avfilter_graph_free, avfilter_link, avformat_close_input, + avformat_find_stream_info, avformat_open_input, AVCodec, AVCodecContext, AVCodecID, + AVFilterContext, AVFilterGraph, AVFormatContext, AVFrame, AVMediaType, AVPacket, + AVPacketSideDataType, AVRational, AVStream, AVERROR, AVERROR_EOF, AV_DICT_IGNORE_SUFFIX, + AV_TIME_BASE, EAGAIN, +}; +use std::{ + ffi::{c_int, CString}, + fmt::Write, + path::Path, + time::Duration, +}; + +const AVERROR_EAGAIN: c_int = AVERROR(EAGAIN); + +#[derive(Debug, Clone, Copy)] +pub(crate) enum ThumbnailSize { + Dimensions { width: u32, height: u32 }, + Size(u32), +} + +pub(crate) struct MovieDecoder { + video_stream_index: i32, + format_context: *mut AVFormatContext, + video_codec_context: *mut AVCodecContext, + video_codec: *const AVCodec, + filter_graph: *mut AVFilterGraph, + filter_source: *mut AVFilterContext, + filter_sink: *mut AVFilterContext, + video_stream: *mut AVStream, + frame: *mut AVFrame, + packet: *mut AVPacket, + allow_seek: bool, + use_embedded_data: bool, +} + +impl MovieDecoder { + pub(crate) fn new( + filename: impl AsRef, + prefer_embedded_metadata: bool, + ) -> Result { + let filename = filename.as_ref(); + + let input_file = if filename == Path::new("-") { + Path::new("pipe:") + } else { + filename + }; + let allow_seek = filename != Path::new("-") + && !filename.starts_with("rsts://") + && !filename.starts_with("udp://"); + + let mut decoder = Self { + video_stream_index: -1, + format_context: std::ptr::null_mut(), + video_codec_context: std::ptr::null_mut(), + video_codec: std::ptr::null_mut(), + filter_graph: std::ptr::null_mut(), + filter_source: std::ptr::null_mut(), + filter_sink: std::ptr::null_mut(), + video_stream: std::ptr::null_mut(), + frame: std::ptr::null_mut(), + packet: std::ptr::null_mut(), + allow_seek, + use_embedded_data: false, + }; + + unsafe { + let input_file_cstring = from_path(input_file)?; + match avformat_open_input( + &mut decoder.format_context, + input_file_cstring.as_ptr(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ) { + 0 => { + check_error( + avformat_find_stream_info(decoder.format_context, std::ptr::null_mut()), + "Failed to get stream info", + )?; + } + e => { + return Err(ThumbnailerError::FfmpegWithReason( + FfmpegError::from(e), + "Failed to open input".to_string(), + )) + } + } + } + + decoder.initialize_video(prefer_embedded_metadata)?; + + decoder.frame = unsafe { av_frame_alloc() }; + if decoder.frame.is_null() { + return Err(FfmpegError::FrameAllocation.into()); + } + + Ok(decoder) + } + + pub(crate) fn decode_video_frame(&mut self) -> Result<(), ThumbnailerError> { + let mut frame_finished = false; + + while !frame_finished && self.get_video_packet() { + frame_finished = self.decode_video_packet()?; + } + + if !frame_finished { + return Err(ThumbnailerError::FrameDecodeError); + } + + Ok(()) + } + + pub(crate) fn embedded_metadata_is_available(&self) -> bool { + self.use_embedded_data + } + + pub(crate) fn seek(&mut self, seconds: i64) -> Result<(), ThumbnailerError> { + if !self.allow_seek { + return Err(ThumbnailerError::SeekNotAllowed); + } + + let timestamp = (AV_TIME_BASE as i64) + .checked_mul(seconds as i64) + .unwrap_or(0); + + check_error( + unsafe { av_seek_frame(self.format_context, -1, timestamp, 0) }, + "Seeking video failed", + )?; + unsafe { avcodec_flush_buffers(self.video_codec_context) }; + + let mut key_frame_attempts = 0; + let mut got_frame; + + loop { + let mut count = 0; + got_frame = false; + + while !got_frame && count < 20 { + self.get_video_packet(); + got_frame = self.decode_video_packet().unwrap_or(false); + count += 1; + } + + key_frame_attempts += 1; + + if !((!got_frame || unsafe { (*self.frame).key_frame } == 0) + && key_frame_attempts < 200) + { + break; + } + } + + if !got_frame { + return Err(ThumbnailerError::SeekError); + } + + Ok(()) + } + + pub(crate) fn get_scaled_video_frame( + &mut self, + scaled_size: Option, + maintain_aspect_ratio: bool, + video_frame: &mut VideoFrame, + ) -> Result<(), ThumbnailerError> { + self.initialize_filter_graph( + unsafe { + &(*(*(*self.format_context) + .streams + .offset(self.video_stream_index as isize))) + .time_base + }, + scaled_size, + maintain_aspect_ratio, + )?; + + check_error( + unsafe { av_buffersrc_write_frame(self.filter_source, self.frame) }, + "Failed to write frame to filter graph", + )?; + + let mut new_frame = FfmpegFrame::new()?; + let mut attempts = 0; + let mut ret = unsafe { av_buffersink_get_frame(self.filter_sink, new_frame.as_mut_ptr()) }; + while ret == AVERROR_EAGAIN && attempts < 10 { + self.decode_video_frame()?; + check_error( + unsafe { av_buffersrc_write_frame(self.filter_source, self.frame) }, + "Failed to write frame to filter graph", + )?; + ret = unsafe { av_buffersink_get_frame(self.filter_sink, new_frame.as_mut_ptr()) }; + attempts += 1; + } + if ret < 0 { + return Err(ThumbnailerError::FfmpegWithReason( + FfmpegError::from(ret), + "Failed to get buffer from filter".to_string(), + )); + } + + video_frame.width = unsafe { (*new_frame.as_mut_ptr()).width as u32 }; + video_frame.height = unsafe { (*new_frame.as_mut_ptr()).height as u32 }; + video_frame.line_size = unsafe { (*new_frame.as_mut_ptr()).linesize[0] as u32 }; + video_frame.source = if self.use_embedded_data { + Some(FrameSource::Metadata) + } else { + Some(FrameSource::VideoStream) + }; + + let frame_data_size = video_frame.line_size as usize * video_frame.height as usize; + match video_frame.data.capacity() { + 0 => { + video_frame.data = Vec::with_capacity(frame_data_size); + } + c if c < frame_data_size => { + video_frame.data.reserve_exact(frame_data_size - c); + video_frame.data.clear(); + } + c if c > frame_data_size => { + video_frame.data.shrink_to(frame_data_size); + video_frame.data.clear(); + } + _ => { + video_frame.data.clear(); + } + } + + video_frame.data.extend_from_slice(unsafe { + std::slice::from_raw_parts((*new_frame.as_mut_ptr()).data[0], frame_data_size) + }); + + unsafe { avfilter_graph_free(&mut self.filter_graph) }; + + Ok(()) + } + + pub(crate) fn get_video_duration(&self) -> Duration { + Duration::from_secs(unsafe { (*self.format_context).duration as u64 / AV_TIME_BASE as u64 }) + } + + fn initialize_video(&mut self, prefer_embedded_metadata: bool) -> Result<(), ThumbnailerError> { + self.find_preferred_video_stream(prefer_embedded_metadata)?; + + self.video_stream = unsafe { + *(*self.format_context) + .streams + .offset(self.video_stream_index as isize) + }; + self.video_codec = + unsafe { avcodec_find_decoder((*(*self.video_stream).codecpar).codec_id) }; + if self.video_codec.is_null() { + return Err(FfmpegError::DecoderNotFound.into()); + } + + self.video_codec_context = unsafe { avcodec_alloc_context3(self.video_codec) }; + if self.video_codec_context.is_null() { + return Err(FfmpegError::VideoCodecAllocation.into()); + } + + check_error( + unsafe { + avcodec_parameters_to_context( + self.video_codec_context, + (*self.video_stream).codecpar, + ) + }, + "Failed to get parameters from context", + )?; + + unsafe { (*self.video_codec_context).workaround_bugs = 1 }; + + check_error( + unsafe { + avcodec_open2( + self.video_codec_context, + self.video_codec, + std::ptr::null_mut(), + ) + }, + "Failed to open video codec", + ) + } + + fn find_preferred_video_stream( + &mut self, + prefer_embedded_metadata: bool, + ) -> Result<(), ThumbnailerError> { + let mut video_streams = vec![]; + let mut embedded_data_streams = vec![]; + let empty_cstring = CString::new("").unwrap(); + + for stream_idx in 0..(unsafe { (*self.format_context).nb_streams as i32 }) { + let stream = unsafe { *(*self.format_context).streams.offset(stream_idx as isize) }; + let codec_params = unsafe { (*stream).codecpar }; + + if unsafe { (*codec_params).codec_type } == AVMediaType::AVMEDIA_TYPE_VIDEO { + let codec_id = unsafe { (*codec_params).codec_id }; + if !prefer_embedded_metadata + || !(codec_id == AVCodecID::AV_CODEC_ID_MJPEG + || codec_id == AVCodecID::AV_CODEC_ID_PNG) + { + video_streams.push(stream_idx); + continue; + } + + if unsafe { !(*stream).metadata.is_null() } { + let mut tag = std::ptr::null_mut(); + loop { + tag = unsafe { + av_dict_get( + (*stream).metadata, + empty_cstring.as_ptr() as *const i8, + tag, + AV_DICT_IGNORE_SUFFIX, + ) + }; + if tag.is_null() { + break; + } + if unsafe { + CString::from_raw((*tag).key).to_string_lossy() == "filename" + && CString::from_raw((*tag).value) + .to_string_lossy() + .starts_with("cover.") + } { + if embedded_data_streams.is_empty() { + embedded_data_streams.push(stream_idx); + } else { + embedded_data_streams[0] = stream_idx; + } + continue; + } + } + } + + embedded_data_streams.push(stream_idx); + } + } + + self.use_embedded_data = false; + if prefer_embedded_metadata && !embedded_data_streams.is_empty() { + self.use_embedded_data = true; + self.video_stream_index = embedded_data_streams[0]; + Ok(()) + } else if !video_streams.is_empty() { + self.video_stream_index = video_streams[0]; + Ok(()) + } else { + Err(FfmpegError::StreamNotFound.into()) + } + } + + fn get_video_packet(&mut self) -> bool { + let mut frames_available = true; + let mut frame_decoded = false; + + if !self.packet.is_null() { + unsafe { + av_packet_unref(self.packet); + av_packet_free(&mut self.packet); + } + } + + self.packet = unsafe { av_packet_alloc() }; + + while frames_available && !frame_decoded { + frames_available = unsafe { av_read_frame(self.format_context, self.packet) == 0 }; + if frames_available { + frame_decoded = unsafe { (*self.packet).stream_index } == self.video_stream_index; + if !frame_decoded { + unsafe { av_packet_unref(self.packet) }; + } + } + } + + frame_decoded + } + + fn decode_video_packet(&self) -> Result { + if unsafe { (*self.packet).stream_index } != self.video_stream_index { + return Ok(false); + } + + let ret = unsafe { avcodec_send_packet(self.video_codec_context, self.packet) }; + if ret != AVERROR(EAGAIN) { + if ret == AVERROR_EOF { + return Ok(false); + } else if ret < 0 { + return Err(ThumbnailerError::FfmpegWithReason( + FfmpegError::from(ret), + "Failed to send packet to decoder".to_string(), + )); + } + } + + match unsafe { avcodec_receive_frame(self.video_codec_context, self.frame) } { + 0 => Ok(true), + AVERROR_EAGAIN => Ok(false), + e => Err(ThumbnailerError::FfmpegWithReason( + FfmpegError::from(e), + "Failed to receive frame from decoder".to_string(), + )), + } + } + + fn initialize_filter_graph( + &mut self, + timebase: &AVRational, + scaled_size: Option, + maintain_aspect_ratio: bool, + ) -> Result<(), ThumbnailerError> { + unsafe { self.filter_graph = avfilter_graph_alloc() }; + if self.filter_graph.is_null() { + return Err(FfmpegError::FilterGraphAllocation.into()); + } + + let args = unsafe { + format!( + "video_size={}x{}:pix_fmt={}:time_base={}/{}:pixel_aspect={}/{}", + (*self.video_codec_context).width, + (*self.video_codec_context).height, + (*self.video_codec_context).pix_fmt as i32, + (*timebase).num, + (*timebase).den, + (*self.video_codec_context).sample_aspect_ratio.num, + i32::max((*self.video_codec_context).sample_aspect_ratio.den, 1) + ) + }; + + setup_filter( + &mut self.filter_source, + "buffer", + "thumb_buffer", + &args, + self.filter_graph, + "Failed to create filter source", + )?; + + setup_filter_without_args( + &mut self.filter_sink, + "buffersink", + "thumb_buffersink", + self.filter_graph, + "Failed to create filter sink", + )?; + + let mut yadif_filter = std::ptr::null_mut(); + if unsafe { (*self.frame).interlaced_frame } != 0 { + setup_filter( + &mut yadif_filter, + "yadif", + "thumb_deint", + "deint=1", + self.filter_graph, + "Failed to create deinterlace filter", + )?; + } + + let mut scale_filter = std::ptr::null_mut(); + setup_filter( + &mut scale_filter, + "scale", + "thumb_scale", + &self.create_scale_string(scaled_size, maintain_aspect_ratio)?, + self.filter_graph, + "Failed to create scale filter", + )?; + + let mut format_filter = std::ptr::null_mut(); + setup_filter( + &mut format_filter, + "format", + "thumb_format", + "pix_fmts=rgb24", + self.filter_graph, + "Failed to create format filter", + )?; + + let mut rotate_filter = std::ptr::null_mut(); + let rotation = self.get_stream_rotation(); + if rotation == 3 { + setup_filter( + &mut rotate_filter, + "rotate", + "thumb_rotate", + "PI", + self.filter_graph, + "Failed to create rotate filter", + )?; + } else if rotation != -1 { + setup_filter( + &mut rotate_filter, + "transpose", + "thumb_transpose", + &rotation.to_string(), + self.filter_graph, + "Failed to create transpose filter", + )?; + } + + check_error( + unsafe { + avfilter_link( + if !rotate_filter.is_null() { + rotate_filter + } else { + format_filter + }, + 0, + self.filter_sink, + 0, + ) + }, + "Failed to link final filter", + )?; + + if !rotate_filter.is_null() { + check_error( + unsafe { avfilter_link(format_filter, 0, rotate_filter, 0) }, + "Failed to link format filter", + )?; + } + + check_error( + unsafe { avfilter_link(scale_filter, 0, format_filter, 0) }, + "Failed to link scale filter", + )?; + + if !yadif_filter.is_null() { + check_error( + unsafe { avfilter_link(yadif_filter, 0, scale_filter, 0) }, + "Failed to link yadif filter", + )?; + } + + check_error( + unsafe { + avfilter_link( + self.filter_source, + 0, + if !yadif_filter.is_null() { + yadif_filter + } else { + scale_filter + }, + 0, + ) + }, + "Failed to link source filter", + )?; + + check_error( + unsafe { avfilter_graph_config(self.filter_graph, std::ptr::null_mut()) }, + "Failed to configure filter graph", + )?; + + Ok(()) + } + + fn create_scale_string( + &self, + size: Option, + maintain_aspect_ratio: bool, + ) -> Result { + let mut scaled_width; + let mut scaled_height = -1; + if size.is_none() { + return Ok("w=0:h=0".to_string()); + } + + let size = size.unwrap(); + + match size { + ThumbnailSize::Dimensions { width, height } => { + scaled_width = width as i32; + scaled_height = height as i32; + } + ThumbnailSize::Size(width) => { + scaled_width = width as i32; + } + } + + let mut scale = String::new(); + + if scaled_width != -1 && scaled_height != -1 { + let _ = write!(scale, "w={scaled_width}:h={scaled_height}"); + if maintain_aspect_ratio { + let _ = write!(scale, ":force_original_aspect_ratio=decrease"); + } + } else if !maintain_aspect_ratio { + if scaled_width == -1 { + let _ = write!(scale, "w={scaled_height}:h={scaled_height}"); + } else { + let _ = write!(scale, "w={scaled_width}:h={scaled_width}"); + } + } else { + let size_int = if scaled_height == -1 { + scaled_width + } else { + scaled_height + }; + + let anamorphic; + let aspect_ratio; + unsafe { + scaled_width = (*self.video_codec_context).width; + scaled_height = (*self.video_codec_context).height; + + aspect_ratio = av_guess_sample_aspect_ratio( + self.format_context, + self.video_stream, + self.frame, + ); + anamorphic = aspect_ratio.num != 0 && aspect_ratio.num != aspect_ratio.den; + } + + if anamorphic { + scaled_width = scaled_width * aspect_ratio.num / aspect_ratio.den; + + if size_int != 0 { + if scaled_height > scaled_width { + scaled_width = scaled_width * size_int / scaled_height; + scaled_height = size_int; + } else { + scaled_height = scaled_height * size_int / scaled_width; + scaled_width = size_int; + } + } + + let _ = write!(scale, "w={scaled_width}:h={scaled_height}"); + } else if scaled_height > scaled_width { + let _ = write!( + scale, + "w=-1:h={}", + if size_int == 0 { + scaled_height + } else { + size_int + } + ); + } else { + let _ = write!( + scale, + "w={}:h=-1", + if size_int == 0 { + scaled_width + } else { + size_int + } + ); + } + } + + Ok(scale) + } + + fn get_stream_rotation(&self) -> i32 { + let matrix = unsafe { + av_stream_get_side_data( + self.video_stream, + AVPacketSideDataType::AV_PKT_DATA_DISPLAYMATRIX, + std::ptr::null_mut(), + ) + } as *const i32; + + if !matrix.is_null() { + let angle = (unsafe { av_display_rotation_get(matrix) }).round(); + if angle < -135.0 { + return 3; + } else if angle > 45.0 && angle < 135.0 { + return 2; + } else if angle < -45.0 && angle > -135.0 { + return 1; + } + } + + -1 + } +} + +impl Drop for MovieDecoder { + fn drop(&mut self) { + if !self.video_codec_context.is_null() { + unsafe { + avcodec_free_context(&mut self.video_codec_context); + } + self.video_codec = std::ptr::null_mut(); + } + + if !self.format_context.is_null() { + unsafe { + avformat_close_input(&mut self.format_context); + } + self.format_context = std::ptr::null_mut(); + } + + if !self.packet.is_null() { + unsafe { + av_packet_unref(self.packet); + av_packet_free(&mut self.packet); + self.packet = std::ptr::null_mut(); + } + } + + if !self.frame.is_null() { + unsafe { + av_frame_free(&mut self.frame); + self.frame = std::ptr::null_mut(); + } + } + + self.video_stream_index = -1; + } +} + +fn check_error(return_code: i32, error_message: &str) -> Result<(), ThumbnailerError> { + if return_code < 0 { + Err(ThumbnailerError::FfmpegWithReason( + FfmpegError::from(return_code), + error_message.to_string(), + )) + } else { + Ok(()) + } +} + +fn setup_filter( + filter_ctx: *mut *mut AVFilterContext, + filter_name: &str, + filter_setup_name: &str, + args: &str, + graph_ctx: *mut AVFilterGraph, + error_message: &str, +) -> Result<(), ThumbnailerError> { + let filter_name_cstr = CString::new(filter_name).unwrap(); + let filter_setup_name_cstr = CString::new(filter_setup_name).unwrap(); + let args_cstr = CString::new(args).unwrap(); + + check_error( + unsafe { + avfilter_graph_create_filter( + filter_ctx, + avfilter_get_by_name(filter_name_cstr.as_ptr() as *const i8), + filter_setup_name_cstr.as_ptr() as *const i8, + args_cstr.as_ptr() as *const i8, + std::ptr::null_mut(), + graph_ctx, + ) + }, + error_message, + ) +} + +fn setup_filter_without_args( + filter_ctx: *mut *mut AVFilterContext, + filter_name: &str, + filter_setup_name: &str, + graph_ctx: *mut AVFilterGraph, + error_message: &str, +) -> Result<(), ThumbnailerError> { + let filter_name_cstr = CString::new(filter_name).unwrap(); + let filter_setup_name_cstr = CString::new(filter_setup_name).unwrap(); + + check_error( + unsafe { + avfilter_graph_create_filter( + filter_ctx, + avfilter_get_by_name(filter_name_cstr.as_ptr() as *const i8), + filter_setup_name_cstr.as_ptr() as *const i8, + std::ptr::null_mut(), + std::ptr::null_mut(), + graph_ctx, + ) + }, + error_message, + ) +} diff --git a/core/thumbnailer/src/thumbnailer.rs b/core/thumbnailer/src/thumbnailer.rs new file mode 100644 index 000000000..a52d7fd6c --- /dev/null +++ b/core/thumbnailer/src/thumbnailer.rs @@ -0,0 +1,166 @@ +use crate::{film_strip_filter, MovieDecoder, ThumbnailSize, ThumbnailerError, VideoFrame}; + +use std::{ops::Deref, path::Path}; +use tokio::{fs, task::spawn_blocking}; +use webp::Encoder; + +/// `Thumbnailer` struct holds data from a `ThumbnailerBuilder`, exposing methods +/// to generate thumbnails from video files. +#[derive(Debug, Clone)] +pub struct Thumbnailer { + builder: ThumbnailerBuilder, +} + +impl Thumbnailer { + /// Processes an video input file and write to file system a thumbnail with webp format + pub async fn process( + &self, + video_file_path: impl AsRef, + output_thumbnail_path: impl AsRef, + ) -> Result<(), ThumbnailerError> { + fs::write( + output_thumbnail_path, + &*self.process_to_webp_bytes(video_file_path).await?, + ) + .await + .map_err(Into::into) + } + + /// Processes an video input file and returns a webp encoded thumbnail as bytes + pub async fn process_to_webp_bytes( + &self, + video_file_path: impl AsRef, + ) -> Result, ThumbnailerError> { + let video_file_path = video_file_path.as_ref().to_path_buf(); + let prefer_embedded_metadata = self.builder.prefer_embedded_metadata; + let seek_percentage = self.builder.seek_percentage; + let size = self.builder.size; + let maintain_aspect_ratio = self.builder.maintain_aspect_ratio; + let with_film_strip = self.builder.with_film_strip; + let quality = self.builder.quality; + + spawn_blocking(move || -> Result, ThumbnailerError> { + let mut decoder = MovieDecoder::new(video_file_path, prefer_embedded_metadata)?; + // We actually have to decode a frame to get some metadata before we can start decoding for real + decoder.decode_video_frame()?; + + if !decoder.embedded_metadata_is_available() { + decoder.seek( + (decoder.get_video_duration().as_secs() as f32 * seek_percentage).round() + as i64, + )?; + } + + let mut video_frame = VideoFrame::default(); + + decoder.get_scaled_video_frame(Some(size), maintain_aspect_ratio, &mut video_frame)?; + + if with_film_strip { + film_strip_filter(&mut video_frame); + } + + // Type WebPMemory is !Send, which makes the Future in this function !Send, + // this make us `deref` to have a `&[u8]` and then `to_owned` to make a Vec + // which implies on a unwanted clone... + Ok( + Encoder::from_rgb(&video_frame.data, video_frame.width, video_frame.height) + .encode(quality) + .deref() + .to_vec(), + ) + }) + .await? + } +} + +/// `ThumbnailerBuilder` struct holds data to build a `Thumbnailer` struct, exposing many methods +/// to configure how a thumbnail must be generated. +#[derive(Debug, Clone)] +pub struct ThumbnailerBuilder { + maintain_aspect_ratio: bool, + size: ThumbnailSize, + seek_percentage: f32, + quality: f32, + prefer_embedded_metadata: bool, + with_film_strip: bool, +} + +impl Default for ThumbnailerBuilder { + fn default() -> Self { + Self { + maintain_aspect_ratio: true, + size: ThumbnailSize::Size(128), + seek_percentage: 0.1, + quality: 80.0, + prefer_embedded_metadata: true, + with_film_strip: true, + } + } +} + +impl ThumbnailerBuilder { + /// Creates a new `ThumbnailerBuilder` with default values: + /// - `maintain_aspect_ratio`: true + /// - `size`: 128 pixels + /// - `seek_percentage`: 10% + /// - `quality`: 80 + /// - `prefer_embedded_metadata`: true + /// - `with_film_strip`: true + pub fn new() -> Self { + Default::default() + } + + /// To respect or not the aspect ratio from the video file in the generated thumbnail + pub fn maintain_aspect_ratio(mut self, maintain_aspect_ratio: bool) -> Self { + self.maintain_aspect_ratio = maintain_aspect_ratio; + self + } + + /// To set a thumbnail size, respecting or not its aspect ratio, according to `maintain_aspect_ratio` value + pub fn size(mut self, size: u32) -> Self { + self.size = ThumbnailSize::Size(size); + self + } + + /// To specify width and height of the thumbnail + pub fn width_and_height(mut self, width: u32, height: u32) -> Self { + self.size = ThumbnailSize::Dimensions { width, height }; + self + } + + /// Seek percentage must be a value between 0.0 and 1.0 + pub fn seek_percentage(mut self, seek_percentage: f32) -> Result { + if !(0.0..=1.0).contains(&seek_percentage) { + return Err(ThumbnailerError::InvalidSeekPercentage(seek_percentage)); + } + self.seek_percentage = seek_percentage; + Ok(self) + } + + /// Quality must be a value between 0.0 and 100.0 + pub fn quality(mut self, quality: f32) -> Result { + if !(0.0..=100.0).contains(&quality) { + return Err(ThumbnailerError::InvalidQuality(quality)); + } + self.quality = quality; + Ok(self) + } + + /// To use embedded metadata in the video file, if available, instead of getting a frame as a + /// thumbnail + pub fn prefer_embedded_metadata(mut self, prefer_embedded_metadata: bool) -> Self { + self.prefer_embedded_metadata = prefer_embedded_metadata; + self + } + + /// If `with_film_strip` is true, a film strip will be added to the thumbnail borders + pub fn with_film_strip(mut self, with_film_strip: bool) -> Self { + self.with_film_strip = with_film_strip; + self + } + + /// Builds a `Thumbnailer` struct + pub fn build(self) -> Thumbnailer { + Thumbnailer { builder: self } + } +} diff --git a/core/thumbnailer/src/utils.rs b/core/thumbnailer/src/utils.rs new file mode 100644 index 000000000..e97f9745d --- /dev/null +++ b/core/thumbnailer/src/utils.rs @@ -0,0 +1,30 @@ +use crate::error::ThumbnailerError; +use std::ffi::CString; +use std::path::Path; + +pub(crate) fn from_path(path: impl AsRef) -> Result { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + CString::new(path.as_ref().as_os_str().as_bytes()) + .map_err(|_| ThumbnailerError::PathConversion(path.as_ref().to_path_buf())) + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + CString::from_vec_with_nul( + path.as_ref() + .as_os_str() + .encode_wide() + .chain(Some(0)) + .map(|b| { + let b = b.to_ne_bytes(); + b.get(0).map(|s| *s).into_iter().chain(b.get(1).map(|s| *s)) + }) + .flatten() + .collect::>(), + ) + .map_err(|_| ThumbnailerError::PathConversion(path.as_ref().to_path_buf())) + } +} diff --git a/core/thumbnailer/src/video_frame.rs b/core/thumbnailer/src/video_frame.rs new file mode 100644 index 000000000..bceaa1e96 --- /dev/null +++ b/core/thumbnailer/src/video_frame.rs @@ -0,0 +1,42 @@ +use crate::error::FfmpegError; +use ffmpeg_sys_next::{av_frame_alloc, av_frame_free, AVFrame}; + +#[derive(Debug)] +pub(crate) enum FrameSource { + VideoStream, + Metadata, +} + +#[derive(Debug, Default)] +pub(crate) struct VideoFrame { + pub width: u32, + pub height: u32, + pub line_size: u32, + pub data: Vec, + pub source: Option, +} + +pub(crate) struct FfmpegFrame { + data: *mut AVFrame, +} + +impl FfmpegFrame { + pub(crate) fn new() -> Result { + let data = unsafe { av_frame_alloc() }; + if data.is_null() { + return Err(FfmpegError::FrameAllocation); + } + Ok(Self { data }) + } + + pub(crate) fn as_mut_ptr(&mut self) -> *mut AVFrame { + self.data + } +} + +impl Drop for FfmpegFrame { + fn drop(&mut self) { + unsafe { av_frame_free(&mut self.data) }; + self.data = std::ptr::null_mut(); + } +} diff --git a/packages/client/src/hooks/useCurrentLibrary.tsx b/packages/client/src/hooks/useCurrentLibrary.tsx index fe48c8f38..53cc877a3 100644 --- a/packages/client/src/hooks/useCurrentLibrary.tsx +++ b/packages/client/src/hooks/useCurrentLibrary.tsx @@ -1,7 +1,7 @@ import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from 'react'; import { proxy, useSnapshot } from 'valtio'; -import { useBridgeQuery, useExplorerStore } from '../index'; +import { getExplorerStore, useBridgeQuery, useExplorerStore } from '../index'; // The name of the localStorage key for caching library data const libraryCacheLocalStorageKey = 'sd-library-list'; @@ -24,7 +24,6 @@ export const LibraryContextProvider = ({ // this is a hook to get the current library loaded into the UI. It takes care of a bunch of invariants under the hood. export const useCurrentLibrary = () => { - const explorerStore = useExplorerStore(); const currentLibraryUuid = useSnapshot(currentLibraryUuidStore).id; const ctx = useContext(CringeContext); if (ctx === undefined) @@ -57,7 +56,7 @@ export const useCurrentLibrary = () => { const switchLibrary = useCallback((libraryUuid: string) => { currentLibraryUuidStore.id = libraryUuid; - explorerStore.reset(); + getExplorerStore().reset(); }, []); // memorize library to avoid re-running find function diff --git a/packages/interface/src/components/explorer/Explorer.tsx b/packages/interface/src/components/explorer/Explorer.tsx index 2e839acc8..09fb14160 100644 --- a/packages/interface/src/components/explorer/Explorer.tsx +++ b/packages/interface/src/components/explorer/Explorer.tsx @@ -1,4 +1,4 @@ -import { getExplorerStore, rspc, useCurrentLibrary } from '@sd/client'; +import { getExplorerStore, rspc, useCurrentLibrary, useExplorerStore } from '@sd/client'; import { ExplorerData } from '@sd/core'; import { Inspector } from '../explorer/Inspector'; @@ -11,7 +11,7 @@ interface Props { } export default function Explorer(props: Props) { - const expStore = getExplorerStore(); + const expStore = useExplorerStore(); const { library } = useCurrentLibrary(); rspc.useSubscription(['jobs.newThumbnail', { library_id: library!.uuid, arg: null }], { diff --git a/packages/interface/src/components/explorer/FileItem.tsx b/packages/interface/src/components/explorer/FileItem.tsx index b161bebe8..2e7ee6e26 100644 --- a/packages/interface/src/components/explorer/FileItem.tsx +++ b/packages/interface/src/components/explorer/FileItem.tsx @@ -12,58 +12,63 @@ interface Props extends HTMLAttributes { index: number; } -function FileItem(props: Props) { - const store = useExplorerStore(); +function FileItem({ data, selected, index, ...rest }: Props) { + // const store = useExplorerStore(); + + // store.layoutMode; + + // props.index === store.selectedRowIndex return (
{ - const objectId = isObject(props.data) ? props.data.id : props.data.file?.id; + const objectId = isObject(data) ? data.id : data.file?.id; if (objectId != undefined) { getExplorerStore().contextMenuObjectId = objectId; - if (props.index != undefined) { - getExplorerStore().selectedRowIndex = props.index; + if (index != undefined) { + getExplorerStore().selectedRowIndex = index; } } }} + {...rest} draggable - {...props} - className={clsx('inline-block w-[100px] mb-3', props.className)} + className={clsx('inline-block w-[100px] mb-3', rest.className)} >
- {props.data?.name} - {props.data?.extension && `.${props.data.extension}`} + {data?.name} + {data?.extension && `.${data.extension}`}
@@ -71,3 +76,30 @@ function FileItem(props: Props) { } export default FileItem; + +function isVideo(extension: string) { + return [ + 'avi', + 'asf', + 'mpeg', + 'mts', + 'mpe', + 'vob', + 'qt', + 'mov', + 'asf', + 'asx', + 'mjpeg', + 'ts', + 'mxf', + 'm2ts', + 'f4v', + 'wm', + '3gp', + 'm4v', + 'wmv', + 'mp4', + 'webm', + 'flv' + ].includes(extension); +} diff --git a/packages/interface/src/components/explorer/FileThumb.tsx b/packages/interface/src/components/explorer/FileThumb.tsx index d34bcca0d..9f282b039 100644 --- a/packages/interface/src/components/explorer/FileThumb.tsx +++ b/packages/interface/src/components/explorer/FileThumb.tsx @@ -1,6 +1,7 @@ -import { useExplorerStore, usePlatform } from '@sd/client'; +import { getExplorerStore, useExplorerStore, usePlatform } from '@sd/client'; import { ExplorerItem } from '@sd/core'; import clsx from 'clsx'; +import { useState } from 'react'; import { useSnapshot } from 'valtio'; import icons from '../../assets/icons'; @@ -12,40 +13,47 @@ interface Props { size: number; className?: string; style?: React.CSSProperties; + iconClassNames?: string; } export default function FileThumb({ data, ...props }: Props) { const platform = usePlatform(); - const store = useExplorerStore(); + // const store = useExplorerStore(); - if (isPath(data) && data.is_dir) return ; + if (isPath(data) && data.is_dir) + return ; const cas_id = isObject(data) ? data.cas_id : data.file?.cas_id; - if (!cas_id) return
; + if (cas_id) { + // this won't work + const new_thumbnail = !!getExplorerStore().newThumbnails[cas_id]; - const has_thumbnail = isObject(data) - ? data.has_thumbnail - : isPath(data) - ? data.file?.has_thumbnail - : !!store.newThumbnails[cas_id]; + const has_thumbnail = isObject(data) + ? data.has_thumbnail + : isPath(data) + ? data.file?.has_thumbnail + : new_thumbnail; - if (has_thumbnail) - return ( - - ); + const url = platform.getThumbnailUrlById(cas_id); + + if (has_thumbnail && url) + return ( + + ); + } const Icon = icons[data.extension as keyof typeof icons]; return (
{ }); return ( -
+
{!!props.data && ( <> -
- +
+
-
+

{props.data?.name} {props.data?.extension && `.${props.data.extension}`} diff --git a/packages/interface/src/components/explorer/VirtualizedList.tsx b/packages/interface/src/components/explorer/VirtualizedList.tsx index b5849e5f0..50fadd719 100644 --- a/packages/interface/src/components/explorer/VirtualizedList.tsx +++ b/packages/interface/src/components/explorer/VirtualizedList.tsx @@ -1,7 +1,7 @@ import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '@sd/client'; import { ExplorerContext, ExplorerItem, FilePath } from '@sd/core'; import { useVirtualizer } from '@tanstack/react-virtual'; -import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useKey, useOnWindowResize, useWindowSize } from 'rooks'; import { useSnapshot } from 'valtio'; @@ -25,7 +25,7 @@ export const VirtualizedList: React.FC = ({ data, context }) => { const [goingUp, setGoingUp] = useState(false); const [width, setWidth] = useState(0); - const store = useExplorerStore(); + const explorerStore = useExplorerStore(); function handleWindowResize() { // so the virtualizer can render the correct number of columns @@ -35,16 +35,18 @@ export const VirtualizedList: React.FC = ({ data, context }) => { useLayoutEffect(() => handleWindowResize(), []); // sizing calculations - const amountOfColumns = Math.floor(width / store.gridItemSize) || 8, + const amountOfColumns = Math.floor(width / explorerStore.gridItemSize) || 8, amountOfRows = - store.layoutMode === 'grid' ? Math.ceil(data.length / amountOfColumns) : data.length, + explorerStore.layoutMode === 'grid' ? Math.ceil(data.length / amountOfColumns) : data.length, itemSize = - store.layoutMode === 'grid' ? store.gridItemSize + GRID_TEXT_AREA_HEIGHT : store.listItemSize; + explorerStore.layoutMode === 'grid' + ? explorerStore.gridItemSize + GRID_TEXT_AREA_HEIGHT + : explorerStore.listItemSize; const rowVirtualizer = useVirtualizer({ count: amountOfRows, getScrollElement: () => scrollRef.current, - overscan: 500, + overscan: 200, estimateSize: () => itemSize, measureElement: (index) => itemSize }); @@ -62,15 +64,18 @@ export const VirtualizedList: React.FC = ({ data, context }) => { useKey('ArrowUp', (e) => { e.preventDefault(); setGoingUp(true); - if (store.selectedRowIndex !== -1 && store.selectedRowIndex !== 0) - getExplorerStore().selectedRowIndex = store.selectedRowIndex - 1; + if (explorerStore.selectedRowIndex !== -1 && explorerStore.selectedRowIndex !== 0) + getExplorerStore().selectedRowIndex = explorerStore.selectedRowIndex - 1; }); useKey('ArrowDown', (e) => { e.preventDefault(); setGoingUp(false); - if (store.selectedRowIndex !== -1 && store.selectedRowIndex !== (data.length ?? 1) - 1) - getExplorerStore().selectedRowIndex = store.selectedRowIndex + 1; + if ( + explorerStore.selectedRowIndex !== -1 && + explorerStore.selectedRowIndex !== (data.length ?? 1) - 1 + ) + getExplorerStore().selectedRowIndex = explorerStore.selectedRowIndex + 1; }); // const Header = () => ( @@ -115,10 +120,10 @@ export const VirtualizedList: React.FC = ({ data, context }) => { className="absolute top-0 left-0 flex w-full" key={virtualRow.key} > - {store.layoutMode === 'list' ? ( + {explorerStore.layoutMode === 'list' ? ( @@ -126,13 +131,14 @@ export const VirtualizedList: React.FC = ({ data, context }) => { [...Array(amountOfColumns)].map((_, i) => { const index = virtualRow.index * amountOfColumns + i; const item = data[index]; + const isSelected = explorerStore.selectedRowIndex === index; return (
{item && ( @@ -158,7 +164,7 @@ interface WrappedItemProps { } // Wrap either list item or grid item with click logic as it is the same for both -const WrappedItem: React.FC = memo(({ item, index, isSelected, kind }) => { +const WrappedItem: React.FC = ({ item, index, isSelected, kind }) => { const [_, setSearchParams] = useSearchParams(); const onDoubleClick = useCallback(() => { @@ -169,20 +175,9 @@ const WrappedItem: React.FC = memo(({ item, index, isSelected, getExplorerStore().selectedRowIndex = isSelected ? -1 : index; }, [isSelected, index]); - if (kind === 'list') { - return ( - - ); - } - + const ItemComponent = kind === 'list' ? FileRow : FileItem; return ( - = memo(({ item, index, isSelected, selected={isSelected} /> ); -}); + + // // Memorize the item so that it doesn't get re-rendered when the selection changes + // return useMemo(() => { + // const ItemComponent = kind === 'list' ? FileRow : FileItem; + // return ( + // + // ); + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [item, index, isSelected]); +}; diff --git a/packages/interface/src/components/explorer/inspector/Divider.tsx b/packages/interface/src/components/explorer/inspector/Divider.tsx index ee4e79b3a..c3745c840 100644 --- a/packages/interface/src/components/explorer/inspector/Divider.tsx +++ b/packages/interface/src/components/explorer/inspector/Divider.tsx @@ -1 +1 @@ -export const Divider = () =>
; +export const Divider = () =>
; diff --git a/packages/interface/src/screens/settings/client/GeneralSettings.tsx b/packages/interface/src/screens/settings/client/GeneralSettings.tsx index 4542032f8..90e06dcb8 100644 --- a/packages/interface/src/screens/settings/client/GeneralSettings.tsx +++ b/packages/interface/src/screens/settings/client/GeneralSettings.tsx @@ -58,7 +58,7 @@ export default function GeneralSettings() { Data Folder - {node?.data_path} + {node?.data_path}