# -*- cperl -*-
# ABSTRACT: MediaManager


package BeamerReveal::MediaManager;
our $VERSION = '20260205.0754'; # VERSION

use strict;
use warnings;

use Carp;
use Env;

use POSIX;

use File::Which;
use File::Path;
use File::Copy;
use File::Basename;

use Data::UUID;

use MIME::Types;
use MIME::Base64;

use IO::File;

use MCE::Hobo;
use MCE::Util;
use MCE::Shared::Scalar;

use Time::HiRes;

use BeamerReveal::TemplateStore;
use BeamerReveal::IPC::Run;
use IPC::Run qw(harness start pump finish); 

use File::chdir;

use BeamerReveal::Log;

sub min { $_[$_[0] > $_[1] ] }
sub nofdigits { length( "$_[0]" ) }


sub new {
  my $class = shift;
  my ( $jobname, $odir, $base, $presentationprms, $debug ) = @_;

  my $self = {
	      jobname    => $jobname,
	      outputdir  => $odir,
	      base       => $base,
	      debug      => $debug,
	     };
  $class = (ref $class ? ref $class : $class );
  bless $self, $class;

  $self->{presentationparameters} = $presentationprms;

  ########################
  # Prepare all the paths

  $self->{videos}     = "$self->{base}/media/Videos";
  $self->{audios}     = "$self->{base}/media/Audios";	
  $self->{images}     = "$self->{base}/media/Images";	
  $self->{animations} = "$self->{base}/media/Animations";	
  $self->{stills}     = "$self->{base}/media/Stills";	
  $self->{iframes}    = "$self->{base}/media/Iframes";	
  $self->{slides}     = "$self->{base}/media/Slides";
  $self->{reveal}     = "$self->{base}/libs";
  $self->{mtoracle}   = MIME::Types->new();
  
  # put ourselves in the output directory for now
  {
    local $CWD = $self->{outputdir};
  
    # create animation path, but don't remove contents
    for my $item ( qw(animations stills) ) {
      File::Path::make_path( $self->{$item} );
    }

    # create all the ohter paths and also clean them
    for my $item ( qw(reveal videos audios iframes images) ) {
      File::Path::rmtree( $self->{$item} );
      File::Path::make_path( $self->{$item} );
    }
  }

  # read the relevant part of the preamble of the job
  my $texFileName = $self->{jobname};
  my $realTexFileName;
  foreach my $ext ( qw(tex ltx latex) ) {
    $realTexFileName = $texFileName . ".$ext";
    last if ( -r $realTexFileName );
  }

  my $logger = $BeamerReveal::Log::logger;
  
  my $texFile = IO::File->new();
  $texFile->open( "<$realTexFileName" )
    or $logger->fatal( "Error: could not open your original LaTeX source file '$realTexFileName'\n" );
  $self->{preamble} = '';
  my $line;
  do { $line = <$texFile> } until( $line =~ /\\usepackage[^{]*{beamer-reveal}/ );
  $line = "%% Preamble excerpt taken from $realTexFileName\n";
  until ( $line =~ /\\begin\{document\}/ ) {
    $self->{preamble} .= $line;
    $line = <$texFile>;
  }
  $texFile->close();
  $self->{preamble} .= "%% End of preamble excerpt\n";

  ####################################################
  # check all the preconditions for running our tools
  $self->{compiler} = File::Which::which( $self->{presentationparameters}->{compiler} )
    or $logger->fatal( "Error: your setup is incomplete, I cannot find your $self->{presentationparameters}->{compiler } compiler (should be part of your TeX installation)\n" .
		       "Make sure it is accessible in a directory on your PATH list variable\n" );
  
  $self->{pdftoppm} = File::Which::which( 'pdftoppm' )
    or $logger->fatal( "Error: your setup is incomplete, I cannot find pdftoppm (part of the poppler library)\n" .
		       "Install 'Poppler-utils' and make sure pdftoppm is accessible in a directory on your PATH list variable\n" );

  $self->{pdfcrop} = File::Which::which( 'pdfcrop' )
    or $logger->fatal( "Error: your setup is incomplete, I cannot find pdfcrop (should be part of your TeX installation)\n" .
		       "Make sure it is accessible in a directory on your PATH list variable\n" );
  $self->{ffmpeg} = File::Which::which( 'ffmpeg' )
    or $logger->fatal( "Error: your setup is incomplete, I cannot find ffmpeg\n" .
		       "Install 'FFmpeg' (from www.ffmpeg.org) and make sure ffmpeg is accessible in a directory on your PATH list variable\n" );

  ##########################################################################
  # We sell things, before we make the, so we need to keep a backorder list

  $self->{copyBackOrders}  = [];
  $self->{constructionBackOrders} = { 'animations' => {},
				      'stills'     => {}
				    };

  return $self;
}


sub revealToStore {
  my $self = shift;

  my $revealTree = File::ShareDir::dist_dir( 'BeamerReveal' ) . '/libs';
  my $destTree = $self->{reveal};

  File::Copy::Recursive::dircopy( $revealTree,
				  $self->{outputdir} . '/' . $destTree );
}


sub backgroundsToStore() {
  my $self = shift;
  my ( $bgs ) = @_;
  $self->{backgrounds} = $bgs;
}


sub notesToStore() {
  my $self = shift;
  my ( $notes ) = @_;
  $self->{notes} = $notes;
}


sub slideFromStore {
  my $self = shift;
  my ( $slidectr, %optargs ) = @_;
  
  my $logger = $BeamerReveal::Log::logger;
  
  my $fileName = $self->{backgrounds}->[$slidectr];
  my $fileType = $self->{mtoracle}->mimeTypeOf( $fileName );
  
  if( exists $optargs{to_embed} ) {
    my $file = do {
      local $/ = undef;
      open my $fh, "<". $fileName
	or $logger->fatal( "Cannot open $fileType-file $fileName to read" );
      <$fh>;
    };
    return ( $fileType, encode_base64( $file ) );
  }
  else {
    return ( $fileType, $fileName );
  }
}


sub noteFromStore {
  my $self = shift;
  my ( $notectr, %optargs ) = @_;
  
  my $logger = $BeamerReveal::Log::logger;

  my $fileName = $self->{notes}->[$notectr];
  my $fileType = $self->{mtoracle}->mimeTypeOf( $fileName );
  
  if( exists $optargs{to_embed} ) {
    my $file = do {
      local $/ = undef;
      open my $fh, "<". $fileName
	or $logger->fatal( "Cannot open $fileType-file $fileName to read" );
      <$fh>;
    };
    return ( $fileType, encode_base64( $file ) );
  }
  else {
    return ( $fileType, $fileName );
  }
}


sub animationFromStore {
  my $self = shift;
  my ( $animation, %optargs ) = @_;
  
  my $logger = $BeamerReveal::Log::logger;

  my $animid  = Digest::SHA::hmac_sha256_hex( $animation->{tex} );
  my $animdir = "$self->{animations}/$animid";
  my $fullpathid = $animdir . ".mp4";
  # correct the effective path to reside in the output directory
  $animdir = $self->{outputdir} . '/' . $animdir;
  
  unless ( -r ( $self->{outputdir} . '/' . $fullpathid ) ) {
    File::Path::make_path( $animdir );

    my $templateStore = BeamerReveal::TemplateStore->new();
    my $tTemplate = $templateStore->fetch( 'tex', 'animation.tex' );
    my $tStamps =
      {
       'PREAMBLE'  => $self->{preamble},
       'FRAMERATE' => $animation->{framerate},
       'DURATION'  => $animation->{duration},
       'ANIMATION' => builtin::trim($animation->{tex}),
      };
    my $fileContent = BeamerReveal::TemplateStore::stampTemplate( $tTemplate, $tStamps );
  
    my $nofFrames = floor( $animation->{framerate} * $animation->{duration} ) + 1;
    # one extra frame to cover the final value \progress = 1.
    my $nofCores  = MCE::Util::get_ncpu();
    if ( $nofCores > 4 ) {
      $nofCores = ceil( $nofCores / 2 );
    }
    
    $self->{constructionBackOrders}->{animations}->{$animid} = 
      {
       animation   => $animation,
       fileContent => $fileContent,
       nofFrames   => $nofFrames,
       nofCores    => $nofCores,
      };
  }
  
  return $fullpathid;
}


sub stillFromStore {
  my $self = shift;
  my ( $still, %optargs ) = @_;
  
  my $logger = $BeamerReveal::Log::logger;
  
  my $stillid  = Digest::SHA::hmac_sha256_hex( $still->{tex} );
  my $stilldir = "$self->{stills}/$stillid";
  my $fullpathid = $stilldir . ".mp4";
  # correct the effective path to reside in the output directory
  $stilldir = $self->{outputdir} . '/' . $stilldir;
  
  unless ( -r ( $self->{outputdir} . '/' . $fullpathid ) ) {
    File::Path::make_path( $stilldir );

    my $templateStore = BeamerReveal::TemplateStore->new();
    my $tTemplate = $templateStore->fetch( 'tex', 'still.tex' );
    my $tStamps =
      {
       'PREAMBLE'  => $self->{preamble},
       'STILL' => builtin::trim($still->{tex}),
      };
    my $fileContent = BeamerReveal::TemplateStore::stampTemplate( $tTemplate, $tStamps );
  
    $self->{constructionBackOrders}->{stills}->{$stillid} = 
      {
       still       => $still,
       fileContent => $fileContent,
      };
  }
  return $fullpathid;
}


sub processAnimationBackOrders {
  my $self = shift;
  my ( $animProgressId ) = @_;
  
  my $logger = $BeamerReveal::Log::logger;
  
  ####################
  # treat animations
  
  my $totalNofBackOrders = scalar keys %{$self->{constructionBackOrders}->{animations}};
  
  $logger->progress( $animProgressId, 1, 'reusing cached data', 1 ) unless $totalNofBackOrders;
  
  my $i = -1;
  while ( my ( $animid, $bo ) = each %{$self->{constructionBackOrders}->{animations}} ) {
    ++$i;
    my $animation   = $bo->{animation};
    my $fileContent = $bo->{fileContent};
    my $nofFrames   = $bo->{nofFrames};
    my $nofCores    = $bo->{nofCores};
    my $animdir     = $self->{outputdir} . '/' . $self->{animations} . '/' . $animid;
    
    # I cannot get multithreading/multiprocessing to work reliably on MS-Windows
    my $progress;
    my $sliceSize = $nofFrames;
    if ( $^O eq 'MSWin32' ) {
      $logger->log( 2, "- Preparing media generation of $nofFrames (alas, no parallellization on MS-Windows)" );
      $nofCores = 1;
      $progress = MCE::Shared::Scalar->new( 0 );
    } elsif ( $nofCores == 1 ) {
      $progress = MCE::Shared::Scalar->new( 0 );
    } else {
      $sliceSize = ceil( $nofFrames / $nofCores );
      $logger->log( 2, "- Preparing media generation of $nofFrames frames in $nofCores threads at $sliceSize frames per thread" );
      $progress = MCE::Shared->scalar( 0 );
    }
    
    # make planning
    my $frameCounter = 0;
    my $planning = [];
    for ( my $core = 0; $core < $nofCores; ++$core ) {
      # make plan for core
      my $plan = { nstart => $frameCounter,
		   nstop  => min( $frameCounter + $sliceSize, $nofFrames ),
		   nr     => $core
		 };
      $plan->{slicestart} = 1;
      $plan->{slicestop}  = $plan->{nstop} - $plan->{nstart};
      $plan->{nstop}      -= 1;
      
      # register plan
      push @$planning, $plan;
      
      # prepare for next core
      $frameCounter += $sliceSize;
    }
    
    $logger->progress( $animProgressId, 0, "animation @{[$i+1]}/$totalNofBackOrders",
		       $nofFrames + $nofCores * 3 );
    
    if ( $nofCores == 1 ) {
      # single-threaded
      for ( my $i = 0; $i < @$planning; ++$i ) {
	_animWork( $planning->[$i], $nofCores, $fileContent, $animdir, $self, $animation,
		   $sliceSize, $progress, $animProgressId );
      }
    } else {
      my @hobos;
      # multi-threaded
      for ( my $i = 0; $i < @$planning; ++$i ) {
	push @hobos, MCE::Hobo->create
	  (
	   sub {
	     _animWork( @_ )
	   }, ( $planning->[$i], $nofCores, $fileContent, $animdir, $self, $animation,
		$sliceSize, $progress, $animProgressId )
	  );
      }
      
      my $activeworkers = @hobos;
      while ( $activeworkers ) {
	
	my @joinable = MCE::Hobo->list_joinable();
	foreach my $hobo ( @joinable ) {
	  $hobo->join();
	  if ( my $error = $hobo->error() ) {
	    $_->kill() for MCE::Hobo->list();
	    MCE::Hobo->wait_all();
	    exit(-1);
	  }
	  --$activeworkers;
	}
	
 	$logger->progress( $animProgressId, $progress->get() );
	
	Time::HiRes::usleep(250);
      }
      
      # $_->join for @hobos;
      $logger->log( 2, "- returning to single-threaded operation" );
    }
    
    # rename all files in order
    my $framecounter = 0;
    for ( my $core = 0; $core < @$planning; ++$core ) {
      my $plan = $planning->[$core];
      my $coreId = sprintf( '%0' . nofdigits( $nofCores ) . 'd', $core );
      for ( my $frameno = $plan->{slicestart}; $frameno <= $plan->{slicestop}; ++$frameno ) {
	my $frameId = sprintf( '%0' . nofdigits( $sliceSize ) . 'd', $frameno );
	my $src = "$animdir/frame-$coreId-$frameId.jpg";
	my $dst = sprintf( "$animdir/frame-%06d.jpg", $framecounter++ );
	File::Copy::move( $src, $dst );
      }
    }
    
    # run ffmpeg or avconv
    my $cmd = [ $self->{ffmpeg}, '-r', "$animation->{framerate}", '-i', 'frame-%06d.jpg', '-vf', 'crop=iw-mod(iw\,2):ih-mod(ih\,2)', 'animation.mp4' ];
    BeamerReveal::IPC::Run::run( $cmd, 0, 8, $animdir, "Error: ffmpeg run failed" );
    File::Copy::move( "$animdir/animation.mp4",
		      "$animdir/../$animid.mp4" );
    
    
    File::Path::rmtree( $animdir ) unless( defined $self->{debug} );
    
    # all is done
    $logger->progress( $animProgressId, 1, "animation @{[$i+1]}/$totalNofBackOrders", 1 );
  }
}

sub processStillBackOrders {
  my $self = shift;
  my ( $stillProgressId ) = @_;
  
  my $logger = $BeamerReveal::Log::logger;
  ###############
  # treat stills
  
  my $totalNofBackOrders = scalar keys %{$self->{constructionBackOrders}->{stills}};
  
  $logger->progress( $stillProgressId, 1, 'reusing cached data', 1 ) unless $totalNofBackOrders;
  
  my $i = -1;
  while ( my ( $stillid, $bo ) = each %{$self->{constructionBackOrders}->{stills}} ) {
    ++$i;
    my $still       = $bo->{still};
    my $fileContent = $bo->{fileContent};
    my $stilldir    = $self->{outputdir} . '/' . $self->{stills} . '/' . $stillid;
    
    $logger->progress( $stillProgressId, 0, "still @{[$i+1]}/$totalNofBackOrders", 5 );
    
    _stillWork( $fileContent, $stilldir, $self, $still, $stillProgressId );
    
    # run ffmpeg or avconv
    my $cmd = [ $self->{ffmpeg}, '-r', '1', '-i', 'frame-1.jpg', '-vf', 'crop=iw-mod(iw\,2):ih-mod(ih\,2)', 'still.mp4' ];
    BeamerReveal::IPC::Run::run( $cmd, 0, 8, $stilldir, "Error: ffmpeg run failed" );
    File::Copy::move( "$stilldir/still.mp4",
		      "$stilldir/../$stillid.mp4" );
    
    File::Path::rmtree( $stilldir ) unless( defined $self->{debug} );
    
    # all is done
    $logger->progress( $stillProgressId, 5 );
  }
}
    
# =method imageFromStore()
  
#   $path = $mm->imageFromStore( $image )
  
# Fetches the unique ID of the C<$image> and returns that ID (the filename of the oject in the media store). The object is put in back-order such that it can be copied later, using C<processCopyBackOrders>.
 
# =over 4

# =item . C<$image>

# the $image file to store in the media store.

# =item . C<$path>

# the path to the image (in the media store)

# =back

# =cut

sub imageFromStore {
  my $self = shift;
  my ( $image, %optargs ) = @_;
  return $self->_fromStore( 'Images', $image, %optargs );
}


sub videoFromStore {
  my $self = shift;
  my ( $video, %optargs ) = @_;
  return $self->_fromStore( 'Videos', $video, %optargs );
}


sub iframeFromStore {
  my $self = shift;
  my ( $iframe, %optargs ) = @_;
  return $self->_fromStore( 'Iframes', $iframe , %optargs);
}


sub audioFromStore {
  my $self = shift;
  my ( $audio, %optargs ) = @_;
  return $self->_fromStore( 'Audios', $audio, %optargs );
}

  
sub _fromStore {
  my $self = shift;
  my ( $fileType, $fileName, %optargs ) = @_;
  
  my $logger = $BeamerReveal::Log::logger;
  
  my $mimeType = $self->{mtoracle}->mimeTypeOf( $fileName );
  
  if ( exists $optargs{to_embed} ) {
    my $file = do {
      local $/ = undef;
      open my $fh, "<". $fileName
	or $logger->fatal( "Cannot open $fileType-file $fileName to read" );
      <$fh>;
    };
    
    return ( $mimeType, encode_base64( $file ) );
  } else {
    # find extension
    my ( undef, undef, $ext ) = File::Basename::fileparse( $fileName, qr/\.[^.]+$/ );
    
    # create store id
    my $id = Data::UUID->new();
    my $fullpathid;
    do {
      my $uuid = $id->create();
      $fullpathid = "$self->{base}/media/$fileType/@{[$id->to_string( $uuid )]}$ext";
    } until ( ! -e $fullpathid );
    
    # register backorder
    push @{$self->{copyBackOrders}},
      {
       type => $fileType,
       from => $fileName,
       to   => $self->{outputdir} . '/' . $fullpathid,
      };
    
    return ( $mimeType, $fullpathid );
  }
}


sub processCopyBackOrders {
  my $self = shift;
  my ( $progressId ) = @_;
  
  my $logger = $BeamerReveal::Log::logger;
  
  my $totalNofBackOrders = @{$self->{copyBackOrders}};
  
  for ( my $i = 0; $i < $totalNofBackOrders; ++$i ) {
    my $bo = $self->{copyBackOrders}->[$i];
    
    # verify if source file exists
    $logger->fatal( "Error: cannot find media file '$bo->{from}'\n" ) unless( -r $bo->{from} );
    
    # report progress and copy
    $logger->progress( $progressId, $i, "file $i/$totalNofBackOrders", $totalNofBackOrders );
    File::Copy::cp( $bo->{from}, $bo->{to} );
  }
  $logger->progress( $progressId, 1, "file $totalNofBackOrders/$totalNofBackOrders", 1 );
}



sub _animWork {
  my ( $plan, $nofCores, $fileContent, $animdir, $self, $animation, $sliceSize, $progress, $progressId ) = @_;
  
  my $cmd;
  my $logFile = IO::File->new();
  my $coreId = sprintf( '%0' . nofdigits( $nofCores ) . 'd', $plan->{nr} );
  my $logFileName = "$animdir/animation-$coreId-overall.log";
  $logFile->open( ">$logFileName" )
    or die( "Error: cannot open logfile $logFileName" );
  
  say $logFile "- Generating TeX file";
  # generate TeX -file
  my $perCoreContent = BeamerReveal::TemplateStore::stampTemplate
    ( $fileContent,
      {
       SLICESTART  => $plan->{slicestart},
       SLICESTOP   => $plan->{slicestop},
       NSTART      => $plan->{nstart},
      }
    );
  
  my $texFileName = "$animdir/animation-$coreId.tex";
  my $texFile = IO::File->new();
  $texFile->open( ">$texFileName" )
    or die( "Error: cannot open animation file '$texFileName' for writing\n" );
  print $texFile $perCoreContent;
  $texFile->close();
  
  say $logFile "- Running TeX";
  # run TeX
  $cmd = [ $self->{compiler},
	   "-halt-on-error", "-interaction=nonstopmode", "-output-directory=$animdir", "$texFileName" ];
  my $logFilename = $texFileName;
  $logFilename =~ s/\.tex$/.log/;
  
  my $logger = $BeamerReveal::Log::logger;
  my $counter = 0;
  BeamerReveal::IPC::Run::runsmart( $cmd, 1, qr/\[(\d+)\]/,
				    sub {
				      while ( scalar @_ ) {
					my $a = shift @_;
					while ( $a > $counter ) {
					  ++$counter;
					  $progress->incr();
					  if ( $nofCores == 1 ) {
					    $logger->progress( $progressId, $progress->get() );
					  }
					}
				      }
				    },
				    $coreId,
				    4,
				    undef, # directory
				    "Error: animation generation failed: check $logFilename"
				  );
  
  say $logFile "- Cropping PDF file";
  # run pdfcrop
  $cmd = [ $self->{pdfcrop}, '--hires', '--margins', '-0.5', "animation-$coreId.pdf" ];
  BeamerReveal::IPC::Run::run( $cmd, $coreId, 8, $animdir, "Error: pdfcrop run failed" );
  $progress->incr();

  # run pdftoppm
  my $xrange = 2 * int( $self->{presentationparameters}->{canvaswidth} * $animation->{width} );
  my $yrange = 2 * int( $self->{presentationparameters}->{canvasheight} * $animation->{height} );
	   
  say $logFile "- Generating jpg files";
  $cmd = [ $self->{pdftoppm},
	   '-scale-to-x', "$xrange",
	   '-scale-to-y', '-1',
	   "animation-$coreId-crop.pdf", "./frame-$coreId", '-jpeg' ];
  BeamerReveal::IPC::Run::run( $cmd, $coreId, 8, $animdir, "Error: pdftoppm run failed" );
  $progress->incr();

  # correct for too short slicesize in filenames coming from pdftoppm
  say $logFile "- Cleaning up jpg files";
  my $currentDigitCnt = nofdigits( $plan->{slicestop} );
  my $desiredDigitCnt = nofdigits( $sliceSize );
  if ( $currentDigitCnt < $desiredDigitCnt ) {
    for ( my $i = 1; $i <= $plan->{slicestop}; ++$i ) {
      my $src = sprintf( "$animdir/frame-$coreId-%0${currentDigitCnt}d.jpg", $i );
      my $dst = sprintf( "$animdir/frame-$coreId-%0${desiredDigitCnt}d.jpg", $i );
      File::Copy::move( $src, $dst );
    }
  }
  $logFile->close();

  $progress->incr();
}


sub _stillWork {
  my ( $fileContent, $stilldir, $self, $still, $progressId ) = @_;

  my $logger = $BeamerReveal::Log::logger;

  my $cmd;
  my $logFile = IO::File->new();
  my $logFileName = "$stilldir/still.log";
  $logFile->open( ">$logFileName" )
    or die( "Error: cannot open logfile $logFileName" );

  say $logFile "- Generating TeX file";
  # generate TeX -file
  my $content = BeamerReveal::TemplateStore::stampTemplate
    ( $fileContent,
      {
       PROGRESS  => $still->{progress},
      }
    );

  my $texFileName = "$stilldir/still.tex";
  my $texFile = IO::File->new();
  $texFile->open( ">$texFileName" )
    or die( "Error: cannot open still file '$texFileName' for writing\n" );
  print $texFile $content;
  $texFile->close();
  $logger->progress( $progressId, 1 );
  
  say $logFile "- Running TeX";
  # run TeX
  $cmd = [ $self->{compiler},
	   "-halt-on-error", "-interaction=nonstopmode", "-output-directory=$stilldir", "$texFileName" ];
  my $logFilename = $texFileName;
  $logFilename =~ s/\.tex$/.log/;

  my $counter = 0;
  BeamerReveal::IPC::Run::runsmart( $cmd, 1, qr/\[(\d+)\]/,
				    sub {},
				    1,
				    4,
				    undef, # directory
				    "Error: still generation failed: check $logFilename"
				  );
  $logger->progress( $progressId, 2 );
  
  say $logFile "- Cropping PDF file";
  # run pdfcrop
  $cmd = [ $self->{pdfcrop}, '--hires', '--margins', '-0.5', "still.pdf" ];
  BeamerReveal::IPC::Run::run( $cmd, 1, 8, $stilldir, "Error: pdfcrop run failed" );
  $logger->progress( $progressId, 3 );

  # run pdftoppm
  my $xrange = 2 * int( $self->{presentationparameters}->{canvaswidth} * $still->{width} );
  my $yrange = 2 * int( $self->{presentationparameters}->{canvasheight} * $still->{height} );
	   
  say $logFile "- Generating jpg file";
  $cmd = [ $self->{pdftoppm},
	   '-scale-to-x', "$xrange",
	   '-scale-to-y', '-1',
	   "still-crop.pdf", "./frame", '-jpeg' ];
  BeamerReveal::IPC::Run::run( $cmd, 1, 8, $stilldir, "Error: pdftoppm run failed" );
  $logFile->close();
  $logger->progress( $progressId, 4 );
}

    
1;

__END__

=pod

=encoding UTF-8

=head1 NAME

BeamerReveal::MediaManager - MediaManager

=head1 VERSION

version 20260205.0754

=head1 SYNOPSIS

Worker object to manage the media files generated for the Reveal HTML presentation.
Sometimes the management is just a matter of copying files (videos, images, iframe material)
in the media store under a unique ID.
Sometimes the file still needs to be generated (TikZ animations).
The former operations are cheap. Therefore they are copied at every invocation and stored under a unique ID.
The latter are expensive to generate. Therefore they are stored under an ID that is a secure hash value (SHA-standard)
based on the source data that is used to generate the animation.
This makes sure that whenever the animation does not change in between different runs,
we reuse the generated video file. If the source data has changed, we regenerate it.

=head1 METHODS

=head2 new()

  $mm = BeamerReveal::MediaManager->new( $jobname, $base, $presoparams, $id, $debug )

The constructor sets up the manager.

This involves: (a) the directory structure of the filesystemtree in which all objects will be
stored; we cal this the "media store", (b) reading the preamble of the original source file,
(c) checking whether all the auxiliary tools (your latex compiler, pdfcrop, pdftoppm, ffmpeg)
are available.

=over 4

=item . C<$jobname>

name of the job that can lead us back to the original LaTeX source file, such that
we can read the preamble for reuse in the TikZ animations.

=item . C<$base>

directory in shiche the media will reside. Typically, this is the base name of the final HTML
file, followed by the suffix '_files'.

=item . C<$presoparams>

parameters of the presentation. This is required to know the compiler and the resolution of the
presentation

=item . C<$debug>

debug flag (undef = off, defined = on) - runs mediamanager in debug mode, i.e. no file cleaning

=item . C<$mm>

return value: the mediamanager object

=back

=head2 revealToStore()

  $mm->revealToStore()

Fetches the original reveal support files and copies them into the media store.

=head2 backgroundsToStore()

  $mm->backgroundsToStore( $bgarray )

Registers the backgrounds into the mediastore.

=over 4

=item . C<$bgarray> reference to array containing all backgrounds; in normal format these
are just filenames, in embed format, these are Base64-encode ASCII strings.

=back

=head2 notesToStore()

  $mm->notesToStore( $notearray )

Registers the notes into the mediastore.

=over 4

=item . C<$notearray> reference to array containing all notes; in normal format these
are just filenames, in embed format, these are Base64-encode ASCII strings.

=back

=head2 slideFromStore()

  ( $mimetype, $string ) = $mm->slideFromStore( $slidenr )

Fetches the media string of slide with number  $slidenr. Slides are
entered into the store by the C<Frameconverter>. If there is an
C<to_embed> key in the C<%optargs> argument, a base64-encoded ASCII
string will be returned, else the filename will be returned.

=over 4

=item . C<$slidenr>

the slide to fetch

=item . C<$mimetype>

string describing the mimetype of the file

=item . C<$string>

If there is an C<to_embed> key in the C<%optargs> argument, a
base64-encoded ASCII string will be returned, else the filename will
be returned.

=back

=head2 noteFromStore()

  ( $mimetype, $string ) = $mm->noteFromStore( $notenr )

Fetches the media string of note with number  $notenr. Notes are
entered into the store by the C<NoteFactory>. If there is an
C<to_embed> key in the C<%optargs> argument, a base64-encoded ASCII
string will be returned, else the filename will be returned.

=over 4

=item . C<$notenr>

the note to fetch

=item . C<$mimetype>

string describing the mimetype of the file

=item . C<$string>

If there is an C<to_embed> key in the C<%optargs> argument, a
base64-encoded ASCII string will be returned, else the filename will
be returned.

=back

=head2 animationFromStore()

  $path = $mm->animationFromStore( $animation )

Returns the media store path to the animation. If the animation is not yet constructed, it will be put in back order, such that it can be generated later by C<processConstructionBackOrders()>.

=over 4

=item . C<$animation>

the $animation object as it was read from the C<.rvl> file.

=item . C<$path>

the path of the animation (in the media store)

=back

=head2 stillFromStore()

  $path = $mm->stillFromStore( $still )

Returns the media store path to the still. If the still is not yet constructed, it will be put in back order, such that it can be generated later by C<processConstructionBackOrders()>.

=over 4

=item . C<$still>

the $still object as it was read from the C<.rvl> file.

=item . C<$path>

the path of the still (in the media store)

=back

=head2 processConstructionBackOrders()

  $mm->processConnstructionBackOrders( $id )

Generates all animations if they are not cached in the store.
The generation is done in parallel using multithreading. If the method fails
the temporary files are kept, otherwise they are removed. On MS-Windows there
is no working multithreading/multiprocessing.

=over 4

=item . C<$id>

progress ID (controls the corresonding progressbar)

=back

=head2 videoFromStore()

  $path = $mm->videoFromStore( $video )

Fetches the unique ID of the C<$video> and returns that ID (the filename of the oject in the media store). The object is put in back-order such that it can be copied later, using C<processCopyBackOrders>.

=over 4

=item . C<$video>

the $video file to store in the media store.

=item . C<$path>

the path to the video (in the media store)

=back

=head2 iframeFromStore()

  $path = $mm->iframeFromStore( $iframe )

Fetches the unique ID of the C<$iframe> and returns that ID (the filename of the oject in the media store). The object is put in back-order such that it can be copied later, using C<processCopyBackOrders>.

=over 4

=item . C<$iframe>

the $iframe file to store in the media store.

=item . C<$path>

the path to the iframe (in the media store)

=back

=head2 audioFromStore()

  $path = $mm->audioFromStore( $audio )

Fetches the unique ID of the C<$audio> and returns that ID (the filename of the oject in the media store). The object is put in back-order such that it can be copied later, using C<processCopyBackOrders>.

=over 4

=item . C<$audio>

the $audio file to store in the media store.

=item . C<$path>

the path to the audio (in the media store)

=back

=head2 _fromStore()

Helper function; do not use directly.

=head2 processCopyBackOrders()

  $mm->processCopyBackOrders( $id )

Copies the backordered files from their original location into the store, based on the backorder list.

=over 4

=item . C<$id>

progress ID (controls the corresponding progressbar)

=back

=head2 _animWork()

Worker function; do not use directly.

=head2 _stillWork()

Worker function; do not use directly.

=head1 AUTHOR

Walter Daems <wdaems@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2025 by Walter Daems.

This is free software, licensed under:

  The GNU General Public License, Version 3, June 2007

=head1 CONTRIBUTOR

=for stopwords Paul Levrie

Paul Levrie

=cut
