mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
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:
parent
1bf315c4db
commit
6fd620087b
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
4034
apps/landing/stats.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
303
core/thumbnailer/.gitignore
vendored
Normal 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
|
22
core/thumbnailer/Cargo.toml
Normal file
22
core/thumbnailer/Cargo.toml
Normal 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"] }
|
39
core/thumbnailer/README.md
Normal file
39
core/thumbnailer/README.md
Normal 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
|
||||
}
|
||||
|
||||
```
|
142
core/thumbnailer/src/error.rs
Normal file
142
core/thumbnailer/src/error.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
}
|
693
core/thumbnailer/src/film_strip.rs
Normal file
693
core/thumbnailer/src/film_strip.rs
Normal 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
108
core/thumbnailer/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
793
core/thumbnailer/src/movie_decoder.rs
Normal file
793
core/thumbnailer/src/movie_decoder.rs
Normal 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,
|
||||
)
|
||||
}
|
166
core/thumbnailer/src/thumbnailer.rs
Normal file
166
core/thumbnailer/src/thumbnailer.rs
Normal 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 }
|
||||
}
|
||||
}
|
30
core/thumbnailer/src/utils.rs
Normal file
30
core/thumbnailer/src/utils.rs
Normal 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()))
|
||||
}
|
||||
}
|
42
core/thumbnailer/src/video_frame.rs
Normal file
42
core/thumbnailer/src/video_frame.rs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 }], {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`}
|
||||
|
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
@ -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" />;
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue