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 <ijamespine@me.com>
This commit is contained in:
Ericson "Fogo" Soares 2022-09-24 05:21:13 -03:00 committed by GitHub
parent 1bf315c4db
commit 6fd620087b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 6695 additions and 144 deletions

12
Cargo.lock generated
View file

@ -4266,6 +4266,7 @@ dependencies = [
"sysinfo",
"tempfile",
"thiserror",
"thumbnailer",
"tokio",
"tracing",
"tracing-subscriber",
@ -5295,6 +5296,17 @@ dependencies = [
"num_cpus",
]
[[package]]
name = "thumbnailer"
version = "0.1.0"
dependencies = [
"ffmpeg-sys-next",
"tempfile",
"thiserror",
"tokio",
"webp",
]
[[package]]
name = "tiff"
version = "0.7.3"

View file

@ -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"

View file

@ -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<dyn Error>> {
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::<Vec<_>>();
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(())
}

View file

@ -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"),

4034
apps/landing/stats.html Normal file

File diff suppressed because one or more lines are too long

View file

@ -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
},

View file

@ -37,7 +37,7 @@ const FileItem = ({ file }: FileItemProps) => {
{/* Folder Icons/Thumbnail etc. */}
<FileIcon file={file} />
<View style={tw`px-1.5 py-[1px] mt-1`}>
<Text numberOfLines={1} style={tw`text-gray-300 text-center text-xs font-medium`}>
<Text numberOfLines={1} style={tw`text-xs font-medium text-center text-gray-300`}>
{file?.name}
</Text>
</View>

View file

@ -33,7 +33,7 @@ async fn main() {
.map(|port| port.parse::<u16>().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<String>| 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(),

View file

@ -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"] }

View file

@ -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::<VecDeque<_>>()
};
#[cfg(not(feature = "ffmpeg"))]
let all_files = { image_files.into_iter().collect::<VecDeque<_>>() };
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<P: AsRef<Path>>(
async fn generate_image_thumbnail<P: AsRef<Path>>(
file_path: P,
output_path: P,
) -> Result<(), Box<dyn Error>> {
@ -190,6 +274,7 @@ pub async fn generate_thumbnail<P: AsRef<Path>>(
// 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<P: AsRef<Path>>(
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<P: AsRef<Path>>(
file_path: P,
output_path: P,
) -> Result<(), Box<dyn Error>> {
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<Path>,
) -> Result<Vec<image_path_with_file::Data>, JobError> {
extensions: Vec<String>,
kind: ThumbnailJobStepKind,
) -> Result<Vec<ThumbnailJobStep>, 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())
}

View file

@ -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<Path>) -> (Arc<Node>, Arc<Router>) {
pub async fn new(data_dir: impl AsRef<Path>) -> Result<(Arc<Node>, Arc<Router>), 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),
}

303
core/thumbnailer/.gitignore vendored Normal file
View file

@ -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

View file

@ -0,0 +1,22 @@
[package]
name = "thumbnailer"
version = "0.1.0"
authors = ["Ericson Soares <ericson.ds999@gmail.com>"]
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"] }

View file

@ -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
}
```

View file

@ -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<c_int> 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)),
}
}
}

View file

@ -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),
},
}
}

108
core/thumbnailer/src/lib.rs Normal file
View file

@ -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<Path>,
output_thumbnail_path: impl AsRef<Path>,
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<Path>,
size: u32,
quality: f32,
) -> Result<Vec<u8>, 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);
}
}
}

View file

@ -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<Path>,
prefer_embedded_metadata: bool,
) -> Result<Self, ThumbnailerError> {
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<ThumbnailSize>,
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<bool, ThumbnailerError> {
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<ThumbnailSize>,
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<ThumbnailSize>,
maintain_aspect_ratio: bool,
) -> Result<String, ThumbnailerError> {
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,
)
}

View file

@ -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<Path>,
output_thumbnail_path: impl AsRef<Path>,
) -> 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<Path>,
) -> Result<Vec<u8>, 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<Vec<u8>, 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<u8>
// 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<Self, ThumbnailerError> {
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<Self, ThumbnailerError> {
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 }
}
}

View file

@ -0,0 +1,30 @@
use crate::error::ThumbnailerError;
use std::ffi::CString;
use std::path::Path;
pub(crate) fn from_path(path: impl AsRef<Path>) -> Result<CString, ThumbnailerError> {
#[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::<Vec<u8>>(),
)
.map_err(|_| ThumbnailerError::PathConversion(path.as_ref().to_path_buf()))
}
}

View file

@ -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<u8>,
pub source: Option<FrameSource>,
}
pub(crate) struct FfmpegFrame {
data: *mut AVFrame,
}
impl FfmpegFrame {
pub(crate) fn new() -> Result<Self, FfmpegError> {
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();
}
}

View file

@ -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

View file

@ -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 }], {

View file

@ -12,58 +12,63 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
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 (
<div
onContextMenu={(e) => {
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)}
>
<div
style={{ width: store.gridItemSize, height: store.gridItemSize }}
style={{ width: getExplorerStore().gridItemSize, height: getExplorerStore().gridItemSize }}
className={clsx(
'border-2 border-transparent rounded-lg text-center mb-1 active:translate-y-[1px]',
{
'bg-gray-50 dark:bg-gray-750': props.selected
'bg-gray-50 dark:bg-gray-750': selected
}
)}
>
<div
className={clsx(
'relative grid place-content-center min-w-0 h-full p-1 rounded border-transparent border-2 shrink-0'
'flex items-center justify-center h-full p-1 rounded border-transparent border-2 shrink-0'
)}
>
<FileThumb
className={clsx(
'border-4 border-gray-250 rounded-sm shadow-md shadow-gray-750 max-h-full max-w-full overflow-hidden'
'border-4 border-gray-250 rounded-sm shadow-md shadow-gray-750 object-cover max-w-full max-h-full w-auto overflow-hidden',
isVideo(data.extension || '') && 'border-gray-950'
)}
data={props.data}
size={store.gridItemSize}
data={data}
size={getExplorerStore().gridItemSize}
/>
</div>
</div>
<div className="flex justify-center">
<span
className={clsx(
'px-1.5 py-[1px] truncate text-center rounded-md text-xs font-medium text-gray-550 dark:text-gray-300 cursor-default',
'px-1.5 py-[1px] truncate text-center rounded-md text-xs font-medium text-gray-550 dark:text-gray-300 cursor-default ',
{
'bg-primary !text-white': props.selected
'bg-primary !text-white': selected
}
)}
>
{props.data?.name}
{props.data?.extension && `.${props.data.extension}`}
{data?.name}
{data?.extension && `.${data.extension}`}
</span>
</div>
</div>
@ -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);
}

View file

@ -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 <Folder size={props.size * 0.7} />;
if (isPath(data) && data.is_dir)
return <Folder className={props.iconClassNames} size={props.size * 0.7} />;
const cas_id = isObject(data) ? data.cas_id : data.file?.cas_id;
if (!cas_id) return <div></div>;
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 (
<img
// onLoad={}
style={props.style}
className={clsx('pointer-events-none z-90', props.className)}
src={platform.getThumbnailUrlById(cas_id)}
/>
);
const url = platform.getThumbnailUrlById(cas_id);
if (has_thumbnail && url)
return (
<img
style={props.style}
// width={props.size}
className={clsx('pointer-events-none', props.className)}
src={url}
/>
);
}
const Icon = icons[data.extension as keyof typeof icons];
return (
<div
style={{ width: props.size * 0.8, height: props.size * 0.8 }}
className="relative m-auto transition duration-200 "
className={clsx('relative m-auto transition duration-200 ', props.iconClassNames)}
>
<svg
// BACKGROUND

View file

@ -41,13 +41,18 @@ export const Inspector = (props: Props) => {
});
return (
<div className="p-2 pr-1 overflow-x-hidden custom-scroll inspector-scroll pb-[55px]">
<div className="p-2 pt-0.5 pr-1 overflow-x-hidden custom-scroll inspector-scroll pb-[55px]">
{!!props.data && (
<>
<div className="flex bg-black items-center justify-center w-full h-64 mb-[10px] overflow-hidden rounded-lg ">
<FileThumb size={230} className="!m-0 flex flex-shrink flex-grow-0" data={props.data} />
<div className="flex items-center justify-center w-full overflow-hidden bg-black rounded-md ">
<FileThumb
iconClassNames="!my-10"
size={230}
className="!m-0 flex flex-shrink flex-grow-0"
data={props.data}
/>
</div>
<div className="flex flex-col w-full pt-0.5 pb-4 overflow-hidden bg-white rounded-lg shadow select-text dark:shadow-gray-700 dark:bg-gray-550 dark:bg-opacity-40">
<div className="flex flex-col w-full pt-0.5 pb-4 overflow-hidden shadow select-text">
<h3 className="pt-3 pl-3 text-base font-bold">
{props.data?.name}
{props.data?.extension && `.${props.data.extension}`}

View file

@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ data, context }) => {
className="absolute top-0 left-0 flex w-full"
key={virtualRow.key}
>
{store.layoutMode === 'list' ? (
{explorerStore.layoutMode === 'list' ? (
<WrappedItem
kind="list"
isSelected={store.selectedRowIndex === virtualRow.index}
isSelected={getExplorerStore().selectedRowIndex === virtualRow.index}
index={virtualRow.index}
item={data[virtualRow.index]}
/>
@ -126,13 +131,14 @@ export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
[...Array(amountOfColumns)].map((_, i) => {
const index = virtualRow.index * amountOfColumns + i;
const item = data[index];
const isSelected = explorerStore.selectedRowIndex === index;
return (
<div key={index} className="w-32 h-32">
<div className="flex">
{item && (
<WrappedItem
kind="grid"
isSelected={store.selectedRowIndex === index}
isSelected={isSelected}
index={index}
item={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<WrappedItemProps> = memo(({ item, index, isSelected, kind }) => {
const WrappedItem: React.FC<WrappedItemProps> = ({ item, index, isSelected, kind }) => {
const [_, setSearchParams] = useSearchParams();
const onDoubleClick = useCallback(() => {
@ -169,20 +175,9 @@ const WrappedItem: React.FC<WrappedItemProps> = memo(({ item, index, isSelected,
getExplorerStore().selectedRowIndex = isSelected ? -1 : index;
}, [isSelected, index]);
if (kind === 'list') {
return (
<FileRow
data={item}
index={index}
onClick={onClick}
onDoubleClick={onDoubleClick}
selected={isSelected}
/>
);
}
const ItemComponent = kind === 'list' ? FileRow : FileItem;
return (
<FileItem
<ItemComponent
data={item}
index={index}
onClick={onClick}
@ -190,4 +185,19 @@ const WrappedItem: React.FC<WrappedItemProps> = 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 (
// <ItemComponent
// data={item}
// index={index}
// onClick={onClick}
// onDoubleClick={onDoubleClick}
// selected={isSelected}
// />
// );
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [item, index, isSelected]);
};

View file

@ -1 +1 @@
export const Divider = () => <div className="w-full my-1 h-[1px] bg-gray-100 dark:bg-gray-550" />;
export const Divider = () => <div className="w-full my-1 h-[1px] bg-gray-100 dark:bg-gray-600" />;

View file

@ -58,7 +58,7 @@ export default function GeneralSettings() {
<span className="text-xs font-medium text-gray-700 dark:text-gray-400">
<Database className="inline w-4 h-4 mr-2 -mt-[2px]" />
<b className="mr-2">Data Folder</b>
{node?.data_path}
<span className="select-text">{node?.data_path}</span>
</span>
</div>
</div>