forked from wolfgangw/digital_cinema_tools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cinemaslides
executable file
·3424 lines (3086 loc) · 136 KB
/
cinemaslides
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env ruby
# encoding: UTF-8
#
# Cinemaslides is a glue tool to create slideshows for digital cinema (DCPs)
# Copyright 2010-2012 Wolfgang Woehl
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
AppVersion = "v0.2014.03.09"
#
# Cinemaslides offers preview modes, a basic set of transition types
# and a basic set of DCP authoring options.
# It will conform images to cinema-compliant specs and transform to X'Y'Z'.
# Additional proof-of-concept features:
# + encrypted DCPs
# + signatures
# + KDM mode
# + Theater Key Retrieval Base URL (see DRAFT-ISDCF Document 8 - Theater Key Retrieval)
# + CompositionMetadataAsset (see DRAFT-ISDCF Document 6 - Composition Metadata Guidelines)
#
# Run "cinemaslides -h" to see options
# Run "cinemaslides --examples" to see a couple of example invocations
#
# Export CINEMASLIDESDIR to point at the desired location for temporary
# files, asset depot etc. or use the default location HOME/cinemaslidesdir.
#
# Export CINEMACERTSTORE to point at a directory that holds your signing
# key and validating certificate chain. Use "make-dc-certificate-cain.rb
# (https://github.com/wolfgangw/digital_cinema_tools/blob/master/make-dc-certificate-chain.rb)
# to create a proof-of-concept, digital cinema compliant X.509 certificate
# chain that will work out of the box with current cinemaslides.
#
# Requires:
# ruby (1.8.7 or later), gem, bash (install your distribution's packages)
# asdcplib (including asdcp-test and kmuuidgen, http://www.cinecert.com/asdcplib/)
# ImageMagick, MPlayer, SoX (install your distribution's packages)
# OpenJPEG (http://code.google.com/p/openjpeg/downloads/list) or Kakadu
# (see the note on Kakadu's terms of use below)
# highline (gem install highline)
# nokogiri (gem install nokogiri, requires ruby-dev, libxml2-dev)
# For encrypted essence DCPs:
# asdcplib's kmrandgen
# For signed DCPs and KDM mode (KDM mode is work in progress, expect bugs and flying saucers):
# xmlsec1 (http://www.aleksey.com/xmlsec/)
# openssl (standard cli interface, install your distribution's package)
#
#
# Kakadu (http://www.kakadusoftware.com/index.php) is a proprietary
# JPEG 2000 implementation, written by Dr. Taubman.
# Kakadu Copyright is owned by NewSouth Innovations Proprietary Ltd,
# commercial arm of the University of New South Wales, Sydney, Australia.
#
# Kakadu is available for demonstration purposes (Windows, Mac, Linux).
# Please see "Downloadable Executables Copyright and Disclaimer" at
# http://www.kakadusoftware.com/index.php?option=com_content&task=view&id=26&Itemid=22
# and make sure to respect these terms of use.
#
AppName = File.basename( $0 )
require 'fileutils'
if RUBY_VERSION <= '1.9'
require 'ftools' # File.copy
begin
require 'rubygems'
rescue LoadError => e
raise e.message
end
end
require 'optparse'
require 'ostruct'
require 'openssl'
require 'digest'
require 'base64'
require 'pp'
require 'tempfile'
require 'nokogiri'
require 'highline/import'
require 'pathname'
require 'shellwords'
# FIXME catch missing parameters, false options, typos etc.
class Optparser
def self.parse(args)
# defaults
options = OpenStruct.new
options.output_type = 'preview'
options.output_type_choices = [ 'preview', 'fullpreview', 'dcp' ]
options.size = '2k'
options.size_choices = [ '2k', '4k' ]
options.aspect = 'flat'
options.aspect_choices = [ 'flat', 'scope', 'hd', 'full', Regexp.new( '\d+(\.\d+)?x\d+(\.\d+)?' ) ] # custom aspect ratios: match '<numeric>x<numeric>'
options.aspect_malformed = FALSE
options.resize = TRUE # option to _not_ resize images (useful for images which are close to target dimensions and would suffer from scaling/-resize)
options.fps = 24.0
options.fps_dcp_choices = [ 24.0, 25.0, 30.0, 48.0, 50.0, 60.0 ]
options.fps_asdcp_choices = [ 23.976, 24.0, 25.0, 30.0, 48.0, 50.0, 60.0 ] # 24000/1001 not DCI compliant but shows up in asdcplib. Why?
options.encoder = 'openjpeg'
options.encoder_choices = [ 'openjpeg-tm', 'openjpeg', 'kakadu' ]
options.output_format = 'jpg'
options.black = NIL
options.black_leader = NIL
options.black_tail = NIL
options.black_intermediate = NIL
options.intermediate_plate = NIL
options.audio_samplerate = 48000
options.audio_samplerate_choices = [ '48000', '48k', '96000', '96k' ]
options.audio_bps = 24
options.audio_bps_choices = [ '16', '24' ]
options.audio_fade = NIL # shall be "in,out"
options.dcp_title = 'Cinemaslides test'
options.issuer = ENV[ 'USER' ] + '@' + `hostname`.chomp
options.tkr_base_url = NIL
options.cma = NIL
options.annotation = "#{ AppName } " + DateTime.now.to_s
options.dcp_package_type = 'OV'
options.dcp_package_type_choices = [ 'ov', 'vf', Regexp.new( /vf\d+/i ) ] # keep regexp choices at the end of these lists
options.dcp_kind = 'test'
options.dcp_kind_choices = [ 'feature', 'trailer', 'test', 'teaser', 'rating', 'advertisement', 'short', 'transitional', 'psa', 'policy' ]
options.dcp_wrap_stereoscopic = FALSE
options.dcp_user_output_path = nil
options.dcp_color_transform_matrix = 'srgb_to_xyz'
options.dcp_color_transform_matrix_choices = [ 'iturec709_to_xyz', 'srgb_to_xyz', '709', 'srgb', Regexp.new( '(\d+(\.\d+)?\s*){9,9}' ) ]
options.dcp_encrypt = FALSE
options.encrypt_headers = TRUE
options.sign = FALSE
options.kdm = FALSE
options.kdm_formulation = 'dci-specific'
options.kdm_formulation_choices = [ 'dci-specific', 'dci-any', 'modified-transitional-1', 'transitional-1', 'ds', 'da', 'mt1', 't1' ]
options.kdm_cpl = NIL
options.kdm_cpl_id = NIL
options.kdm_cpl_content_title_text = NIL
options.kdm_keysdir = NIL
options.kdm_target = NIL
options.kdm_start = '0' # time window will start now
options.kdm_end = '28' # time window will be 4 weeks
options.montage = FALSE
options.keep = FALSE
options.dont_check = FALSE
options.dont_drop = FALSE
options.verbosity = 'info'
options.verbosity_choices = [ 'quiet', 'info', 'debug' ]
options.transition_and_timing = Array.new
options.transition_and_timing_choices = [ 'cut', 'fade', 'crossfade' ]
options.transition_and_timing << 'cut'
options.transition_and_timing << 5 # duration
options.mplayer_gamma = 1.2
opts = OptionParser.new do |opts|
opts.banner = <<BANNER
#{ AppName } #{ AppVersion } #{ ENV[ 'CINEMASLIDESDIR' ].nil? ? "\nExport CINEMASLIDESDIR to point to desired work directory needed for temporary files, thumbnails, asset depot, DCPs (Default: HOME/cinemaslidesdir)" : "\nCINEMASLIDESDIR is set (#{ ENV[ 'CINEMASLIDESDIR' ] })" } #{ ENV[ 'CINEMACERTSTORE' ].nil? ? "\nExport CINEMACERTSTORE to point to a directory which holds your digital cinema compliant signing key and certificates" : "\nCINEMACERTSTORE is set (#{ ENV[ 'CINEMACERTSTORE' ] })" }
Usage:
#{ AppName } [-t, --type <type>] [-k, --size <DCP resolution>] [-a, --aspect <aspect name or widthxheight>] [--fps <fps>] [-x --transition <type,a,b[,c]>]
[--title <DCP title>] [--issuer <DCP issuer/KDM facility code>] [--annotation <DCP/KDM annotation>] [--package-type <CPL type>] [--kind <DCP kind>]
[-b, --black <seconds>] [--bl, --black-leader <seconds>] [--bt, --black-tail <seconds>] [--bi, --black-intermediate <seconds>] [--ip, --intermediate-plate <plate>]
[--audio-fade <in_seconds,out_seconds>] [-s, --samplerate <audio samplerate>] [--bps <bits per audio sample>]
[--dont-check] [--dont-drop] [--dont-resize]
[-j, --encoder <encoder>] [--of, --output-format <image suffix>]
[-o, --dcp-out <path>]
[--keep] [--wrap-stereoscopic] [-m, --montagepreview] [--mg, --mplayer-gamma <gamma>]
[--sign] [--encrypt] [--dont-encrypt-headers]
[--kdm] [--formulation <KDM formulation>] [--cpl <cpl file>] [--cpl-id <CPL UUID>] [ --cpl-content-title <text> ] [--keysdir <directory with referenced keys>]
[--start <days from now>|<datetime>] [--end <days from now>|<datetime>] [--target <certificate>]
[--tkr-base-url <url>] [--cma <CompositionMetadataAsset data>]
[-v, --verbosity <level>] [--examples] [-h, --help]
[ image and audio files ] [ KDM mode parameters ]
BANNER
opts.on( '-t', '--type type', String, "Use 'preview' (half size) or 'fullpreview' (full size) or 'dcp' (Default: preview)" ) do |p|
if options.output_type_choices.include?( p.downcase )
options.output_type = p.downcase
else
options.output_type = 'catch:' + p
end
end
opts.on( '-k', '--size resolution', String, "Use '2k' or '4k' (Default: 2k)" ) do |p|
if options.size_choices.include?( p.downcase )
options.size = p.downcase
else
options.size = 'catch:' + p.downcase
end
end
opts.on( '-a', '--aspect ratio', String, "For standard aspect ratios use 'flat', 'scope' or 'hd' (Default: flat). You can also experiment with custom aspect ratios by saying '<width>x<height>'. The numbers given will be scaled to fit into the target container (Default size or specified with '--size')." ) do |p|
if options.aspect_choices.include?( p.downcase )
options.aspect = p.downcase
elsif p.match( options.aspect_choices.last )
options.aspect = 'Custom aspect ratio:' + p
else
options.aspect_malformed = TRUE
end
end
opts.on( '--dont-resize', 'Do not resize images (Useful for images close to target dimensions)' ) do
options.resize = FALSE
end
opts.on( '--fps fps', 'Framerate (Default: 24)', Float ) do |p| # 23.976
options.fps = p.to_f
end
opts.on( '-x', '--transition transition,seconds[,seconds[,seconds]]', Array, "Use this option to specify the transition type ('cut', 'fade' or 'crossfade') and timing parameters (Default: '-x cut,5'). Separate parameters with comma (no spaces)" ) do |p|
if options.transition_and_timing_choices.include?( p.first.downcase )
options.transition_and_timing = p
else
options.transition_and_timing[ 0 ] = 'malformed'
end
end
opts.on( '-j', '--encoder codec', String, "Use 'openjpeg', 'openjpeg-tm' (for OpenDCP's opendcp_j2k) or 'kakadu' for JPEG 2000 encoding (Default: openjpeg)" ) do |p|
options.encoder = p.downcase
end
opts.on( '--of', '--output-format suffix', String, "Use 'jpg' or any other image related suffix (Default: jpg for previews, tiff for DCPs)" ) do |p|
options.output_format = p
end
opts.on( '-b', '--black seconds', Float, 'Length of black leader and tail (Default: 0)' ) do |p|
options.black = p
end
opts.on( '--bl', '--black-leader seconds', Float, 'Length of black leader (Default: 0)' ) do |p|
options.black_leader = p
end
opts.on( '--bt', '--black-tail seconds', Float, 'Length of black tail (Default: 0)' ) do |p|
options.black_tail = p
end
opts.on( '--bi', '--black-intermediate seconds', Float, 'Length of black intermediate (Default: 0)' ) do |p|
options.black_intermediate = p
end
opts.on( '--ip', '--intermediate-plate plate', String, 'Plate file to use as intermediate plate. Use with --bi | --black-intermediate' ) do |p|
options.intermediate_plate = p
end
opts.on( '-r', '--samplerate rate', String, "Audio samplerate. Use '48000', '48k', '96000' or '96k' (Default: 48k)" ) do |p|
if options.audio_samplerate_choices.include?( p.downcase )
case p.downcase
when '48000', '48k'
options.audio_samplerate = 48000
when '96000', '96k'
options.audio_samplerate = 96000
end
end
end
opts.on( '--bps bps', Integer, "Bits per audio sample. Use '16' or '24' (Default: 24)" ) do |p|
if options.audio_bps_choices.include?( p )
options.audio_bps = p
end
end
opts.on( '--audio-fade seconds,seconds', Array, "Fade in and fade out times for audio (Default: 0 sec fade in, 1 sec fade out)" ) do |p|
options.audio_fade = p
end
opts.on( '--title title', String, 'DCP content title' ) do |p|
options.dcp_title = p
end
opts.on( '--issuer issuer', String, 'DCP/KDM issuer. In KDM mode the first 3 letters will be used to signify the KDM creation facility' ) do |p|
options.issuer = p
end
opts.on( '--tkr-base-url url', String, 'TKR Base URL' ) do |p|
options.tkr_base_url = p
end
opts.on( '--cma cma', String, "CompositionMetadataAsset data (See '#{ AppName } --examples' for format)" ) do |p|
options.cma = p
end
opts.on( '--annotation annotation', String, 'DCP/KDM annotation' ) do |p|
options.annotation = p
end
opts.on( '--package-type type', String, "DCP package type. Use 'OV', 'VF' or 'VF#' (# is a number) (Default: OV). For now this is used for KDMs only" ) do |p|
if options.dcp_package_type_choices.include?( p.downcase ) or p.match( options.dcp_package_type_choices.last )
options.dcp_package_type = p.upcase
end
end
opts.on( '--kind kind', "DCP content kind. Use 'feature', 'trailer, 'test', 'teaser', 'rating', 'advertisement', 'short', 'transitional', 'psa' or 'policy' (Default: test)" ) do |p|
if options.dcp_kind_choices.include?( p.downcase )
options.dcp_kind = p.downcase
end
end
opts.on( '--wrap-stereoscopic', 'Wrap images as stereoscopic essence (Useful when a monoscopic slideshow needs to run on a 3D projector preset)' ) do
options.dcp_wrap_stereoscopic = TRUE
end
opts.on( '-o', '--dcp-out path', String, "DCP location and folder name (Default: Write to #{ AppName }'s working directory)" ) do |p|
options.dcp_user_output_path = p
end
opts.on( '-m', '--montagepreview', 'Display a montage of the images before processing' ) do
options.montage = TRUE
end
opts.on( '--mg', '--mplayer-gamma gamma', Float, 'Tweak mplayer gamma (Used for previews. Range 0.1 - 10. Default: 1.2)' ) do |p|
options.mplayer_gamma = p if ( 0.1 <= p and p <= 10 )
end
opts.on( '--keep', 'Do not remove preview/temporary files' ) do
options.keep = TRUE
end
opts.on( '--dont-check', 'Do not check files' ) do
options.dont_check = TRUE
end
opts.on( '--dont-drop', 'Do not drop and ignore unreadable files or files ImageMagick cannot decode but nag and exit instead' ) do
options.dont_drop = TRUE
end
opts.on( '--sign', 'Sign CPL and PKL (export ENV variable CINEMACERTSTORE to point at a directory that holds your signing certificate and validating certificate chain)' ) do
options.sign = TRUE
end
opts.on( '--encrypt', 'Encrypt trackfiles. Implies signature. Stores content keys in CINEMASLIDESDIR/keys' ) do
options.dcp_encrypt = TRUE
end
opts.on( '--dont-encrypt-headers', 'Do not encrypt JP2K headers (Default: Encrypt headers)' ) do
options.encrypt_headers = FALSE
end
opts.on( '--kdm', 'KDM mode: Generate key delivery message. Use with --cpl or --cpl-id, --start, --end, --issuer and --target' ) do
options.kdm = TRUE
end
opts.on( '-f', '--formulation formulation', String, "Use dci-specific, dci-any, modified-transitional-1, transitional-1 or the respective acronyms (Default: dci-specific | ds)" ) do |p|
if options.kdm_formulation_choices.include?( p.downcase )
options.kdm_formulation = p.downcase
end
end
opts.on( '--cpl file', String, 'KDM mode: Specify target CPL file' ) do |p|
options.kdm_cpl = p
end
opts.on( '--cpl-id uuid', String, 'KDM mode: Use as alternative to specifying the CPL file.' ) do |p|
options.kdm_cpl_id = p
end
opts.on( '--cpl-content-title title', String, 'KDM mode: Content title text. Use when there is no direct access to the CPL' ) do |p|
options.kdm_cpl_content_title_text = p
end
opts.on( '--keysdir keys dir', String, "KDM mode: Location of the referenced CPL's keys. Use with --cpl-id" ) do |p|
options.kdm_keysdir = p
end
opts.on( '--start days', String, 'KDM mode: KDM validity starts <days> from now or at <datetime> (Default: Now)' ) do |p|
options.kdm_start = p
end
opts.on( '--end days', String, 'KDM mode: KDM validity ends <days> from now or at <datetime> (Default: 4 weeks from now)' ) do |p|
options.kdm_end = p
end
opts.on( '--target certificate', String, 'KDM mode: Path to the recipient device certificate' ) do |p|
options.kdm_target = p
end
opts.on( '-v', '--verbosity level', String, "Use 'quiet', 'info' or 'debug' (Default: info)" ) do |p|
if options.verbosity_choices.include?( p )
options.verbosity = p
else
options.verbosity = "info"
end
end
opts.on( '--examples', 'Some examples and explanations' ) do
examples = <<EXAMPLES
#{ AppName } #{ AppVersion }
Specify options in any order. Order of image/audio files matters. Audio is optional.
Audio timing is handled in a first-come, first-served manner -- independently from image timings
In order to use signature and KDM generation you need to have 3 related, digital cinema compliant
certificates in $CINEMACERTSTORE (#{ AppName } needs some specific names for now -- #{ AppVersion })
(Use https://github.com/wolfgangw/digital_cinema_tools/blob/master/make-dc-certificate-chain.rb for that)
Preview slideshow with audio (Half sized preview. Cut transition. Default duration: 5 seconds each):
$ #{ AppName } image1.jpg audio.wav image2.tiff
Preview slideshow with audio (Full sized preview. Transition: crossfades for 1 second, 20 seconds at full level each):
$ #{ AppName } --type fullpreview -x crossfade,1,20 image1.tiff image2.ppm audio1.wav audio2.wav
Create slideshow DCP, use all image files in directory 'slides' (Resolution: 2K. 5 seconds black leader):
$ #{ AppName } --type dcp --size 2k --black-leader 5 slides/*
Create slideshow DCP (Preview thumbnails. Aspect ratio: scope):
$ #{ AppName } audio.wav *.tiff --montagepreview --aspect scope -t dcp --title 'Slideshow Test' --issuer 'Facility'
Transition: fade in for 0.5 seconds, hold for 10, fade out for 4
$ #{ AppName } -x fade,0.5,10,4 ...
Carousel goes berserk (note option --dont-check in order to avoid extensive checks for lots of images)
$ #{ AppName } -t dcp --title "Motion sequence" --fps 24 -x cut,0.04167 --dont-check motion_sequence/
Write DCP to custom location
$ #{ AppName } --dcp-out /media/usb-disk/slideshow --type dcp image.tiff audio.wav --title "First composition"
Write another composition to the same custom location (PKL and ASSETMAP will be extended)
$ #{ AppName } -o /media/usb-disk/slideshow -t dcp image2.tiff image3.tiff song.wav --title "Another composition"
Timings are global. Some workaround kind of finer-grained timing control:
$ #{ AppName } -x cut,3 title title title 1st_slide 2nd_slide credits credits
Slideshow of your truetype fonts:
$ #{ AppName } -x crossfade,2,2 `find /usr/share/fonts/truetype/ -name '*ttf' -type f`
Custom aspect ratios (Work fine on a Solo G3, what about other servers?):
$ #{ AppName } --aspect 1.33x1 | --aspect 3072x2304 | --aspect 3x1 [...]
Encrypt DCP trackfiles and store content keys in $CINEMASLIDESDIR/keys (--encrypt implies signing):
Go check the final CPL for key IDs and compare to stored content keys
Using asdcplib you can decrypt and extract essence with
asdcp-test -x decrypted_ -k '<content key -- 16 bytes in hex>' <encrypted MXF>
$ #{ AppName } -t dcp --encrypt --title "Encryption test" -o ENCRYPTION_TST_F_2K_20101231_WOE_OV -x cut,0.04167 demo_sequence/
Generate KDM for some content, targeting some server certificate with a time window from now to 10 days from now:
$ #{ AppName } -v debug --kdm --cpl cpl.xml --start 0 --end 10 --target server.pem
Generate KDM for some content, targeting some server certificate with a time window from 2012-12-31T00:00:01+01:00 to 2013-01-31T23:59:59+01:00:
$ #{ AppName } -v debug --kdm --cpl cpl.xml --start 2012-12-31T00:00:01+01:00 --end 2013-01-31T23:59:59+01:00 --target server.pem
Set Theater Key Retrieval Base URL (applies to encrypted packages only):
$ #{ AppName } -t dcp --encrypt --tkr-base-url http://www.example.org/tkr/ ...
Add CompositionMetadataAsset. Fieldnames can be abbreviated as long as they are unambiguous.
Separate items with comma. Leading and trailing whitespace will be discarded. Fieldname case and order are ignored.
Format is Fieldname:Value. Allowed fieldnames are:
ReleaseRegion, Distributor, Facility, StereoscopicLuminance, MainSoundConfiguration, MainPictureActiveArea
$ #{ AppName } -t dcp --cma "ReleaseRegion: DE, Distributor: Foo, Facility: Bar, StereoscopicLuminance: 4, MainSoundConfiguration: 5.1, MainPictureActiveArea: 1998x1080"
$ #{ AppName } -t dcp --cma "rel:DE,distr:Foo,fac:Bar,luminance:4,soundconf:5.1,picture:1998x1080"
EXAMPLES
puts examples
exit
end
opts.on_tail( '-h', '--help', 'Display this screen' ) do
puts opts
exit
end
end
begin
opts.parse!( args )
rescue Exception => e
exit 0 if e.class == SystemExit
puts "Options error: #{ e.message }"
exit 1
end
options
end # parse
end # class
# reconstruct original commandline for readme file
commandline = AppName
ARGV.each do |arg|
if arg =~ /.+\s.+/
commandline += ' ' + '"' + arg + '"'
else
commandline += ' ' + arg
end
end
# destructive parse
options = Optparser.parse( ARGV )
module OS
def OS.windows?
(/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
end
def OS.mac?
(/darwin/ =~ RUBY_PLATFORM) != nil
end
def OS.unix?
!OS.windows?
end
def OS.linux?
OS.unix? and not OS.mac?
end
end
class Logger
attr_accessor :prefix
def initialize( prefix, verbosity )
@verbosity = verbosity
@critical = TRUE
case @verbosity
when "quiet"
@info = FALSE
@warn = FALSE
@debug = FALSE
when "info"
@info = TRUE
@warn = TRUE
@debug = FALSE
when "debug"
@info = TRUE
@warn = TRUE
@debug = TRUE
end
@prefix = prefix
@color = Hash.new
# these work ok on a black background:
@color[:info] = ''
@color[:debug] = '32'
@color[:warn] = '33'
@color[:critical] = '1'
end
def info( text )
to_console( @color[:info], text ) if @info == TRUE
end
def warn( text )
to_console( @color[:warn], text ) if @warn == TRUE
end
def debug( text )
to_console( @color[:debug], text ) if @debug == TRUE
end
def critical( text )
to_console( @color[:critical], text ) if @critical == TRUE
end
def cr( text )
carriage_return( @color[:info], text ) unless @verbosity == "quiet"
end
def carriage_return( color, text )
printf "\033[#{ color }m%s %s\033[0m\r", @prefix, text
end
def to_console( color, text )
printf "\033[#{ color }m%s %s\033[0m\n", @prefix, text
end
end
@logger = Logger.new( prefix = '*', options.verbosity )
def check_external( requirements )
available_tools = Array.new
missing_tools = Array.new
requirements.each do |tool|
exitstatus = system "which #{ tool } > /dev/null 2>&1"
case exitstatus
when TRUE
available_tools << tool
when FALSE
@logger.debug( "Missing: #{ tool }" )
missing_tools << tool
end
end
return available_tools, missing_tools
end
def hours_minutes_seconds_verbose( seconds )
t = seconds
hrs = ( ( t / 3600 ) ).to_i
min = ( ( t / 60 ) % 60 ).to_i
sec = t % 60
return [
hrs > 0 ? hrs.to_s + " hour#{ 's' * ( hrs > 1 ? 1 : 0 ) }" : nil ,
min > 0 ? min.to_s + " minute#{ 's' * ( min > 1 ? 1 : 0 ) }" : nil ,
sec == 1 ? sec.to_i.to_s + ' second' : sec != 0 ? sec.to_s + ' seconds' : nil ,
t > 60 ? "(#{ t } seconds)" : nil
].compact.join( ' ' )
end
def hms_from_seconds( seconds )
hours = ( seconds / 3600.0 ).to_i
minutes = ( ( seconds / 60.0 ) % 60 ).to_i
secs = seconds % 60
return [ hours, minutes, secs ].join( ':' )
end
def seconds_from_hms( timestring ) # hh:mm:ss.fraction
a = timestring.split( ':' )
hours = a[0].to_i
minutes = a[1].to_i
secs = a[2].to_f
return ( hours * 3600 + minutes * 60 + secs )
end
def final_report( sequence_frames, fps, transition_and_timing, keep )
sequence_duration = sequence_frames / fps
@logger.debug( "#{ sequence_frames } frames intended by numbers (#{ hours_minutes_seconds_verbose( sequence_duration ) })" )
@logger.debug( "#{ @framecount -1 } frames written" )
@logger.info( "Cinema Slideshow is #{ hours_minutes_seconds_verbose( ( @framecount - 1 ) / fps ) } long (#{ @source[ :orig_name ].length } image#{ 's' * ( @source[ :orig_name ].length == 1 ? 0 : 1 )} | #{ transition_and_timing.join(',').gsub(' ', '') } | #{ @framecount - 1 } frames | #{ fps } fps)" )
@logger.info( "Pick up preview files at #{ @workdir }/" ) if ( keep == TRUE and @output_type != 'dcp' )
@logger.info( "Pick up temporary files at #{ @workdir }/" ) if ( keep == TRUE and @output_type == 'dcp' )
@logger.info( "Pick up DCP at #{ @dcpdir }" ) if @output_type == 'dcp'
end
def cleanup_workdir( keep )
case keep
when FALSE
case @output_type
when 'preview', 'fullpreview'
@logger.info( "Removing preview files (Say '--keep' to keep them)" )
`rm -rf #{ Shellwords.escape @workdir }`
#unless @final_audio.nil?
# `rm #{ @final_audio }` # which lives in @assetsdir_audio, for now
#end
when 'dcp'
@logger.info( "Removing temporary files (Say '--keep' to keep them)" )
`rm -rf #{ Shellwords.escape @conformdir }`
`rm -rf #{ Shellwords.escape @j2cdir }`
if File.dirname( @dcpdir ) != @workdir
`rm -rf #{ Shellwords.escape @workdir }`
end
end
# Remove temporary safe links (for source filenames with spaces and rogue chars)
# Alternatively we could iterate @source and act on difference between original
# filename and the attached item (the safe link) but wth, really. KISS ftw
Dir.glob( File.join( @assetsdir, 'tmp-safe-link-*' ) ).each do |link|
File.delete link
end
end
end
def get_timestamp
#t = Time.now
#[t.year, '%02d' % t.month, '%02d' % t.day, '%02d' % t.hour, '%02d' % t.min, '%02d' % t.sec].join('_')
DateTime.now.to_s
end
timestamp = get_timestamp.gsub( /[:+]/, '_' )
# fit custom aspect ratios into the target container dimensions (1k for preview, 2k/4k for fullpreview/dcp)
def scale_to_fit_container( width, height, container_width, container_height )
factor = container_height / container_width > height / width ? container_width / width : container_height / height
@logger.debug( "Scaling factor to fit custom aspect ratio #{ width } x #{ height } in #{ @size } container: #{ factor }" )
width_scaled = width * factor
height_scaled = height * factor
return width_scaled.floor, height_scaled.floor
end
# target container dimensions are upscaled from 1k numbers
# (1k for preview, 2k and 4k for fullpreview and dcp)
# any custom aspect ratio is scaled to fit the target container
def width_x_height
container_multiplier = @size.split( '' ).first.to_i
container_width = 1024.0 * container_multiplier
container_height = 540.0 * container_multiplier
@logger.debug( "Container: #{ container_width } x #{ container_height } (1k multiplier: #{ container_multiplier })" )
case @aspect
when 'flat' # 1.85 : 1
width, height = 999, 540 # 1.85
when 'scope' # 2.39 : 1
width, height = 1024, 429 # 2.38694638694639
when 'hd' # 1.77 : 1
width, height = 960, 540 # 1.77777777777778
when 'full' # full container 1.8962962962962964
width, height = 1024, 540
else # Custom aspect ratio
custom_width, custom_height = @aspect.split( 'Custom aspect ratio:' ).last.split( 'x' )
width, height = scale_to_fit_container( custom_width.to_f, custom_height.to_f, container_width, container_height )
return [ width, height ].join( 'x' )
end
width *= container_multiplier
height *= container_multiplier
return [ width, height ].join( 'x' )
end
def make_black_sequence( info, duration, fps )
clear_terminal_line
@logger.info( "Black #{ info }: #{ duration } seconds" )
blackfile = sequencefile
make_black_frame( blackfile, fps )
@framecount += 1
sequence_links_to( blackfile, duration, fps )
end
def make_black_frame( filename, fps )
black_asset = File.join( @assetsdir, 'black.' + @output_format )
asset, todo = check_for_asset( black_asset, @output_format, fps )
asset_esc = Shellwords.escape asset
if todo == TRUE
case @output_type
when 'preview', 'fullpreview'
`convert -type TrueColor -size #{ @dimensions } xc:black -depth 8 #{ asset_esc }`
when 'dcp'
`convert -type TrueColor -size #{ @dimensions } xc:black -depth 12 #{ asset_esc }`
end
end
File.symlink( asset, filename )
end
def fade_in_hold_fade_out( image, fps, fade_in_time, duration, fade_out_time )
if fade_in_time > 0
fade_in( image, fps, fade_in_time )
end
if duration > 0
full_level( image, fps, duration )
end
if fade_out_time > 0
fade_out( image, fps, fade_out_time )
end
end
def fade_in( image, fps, fade_in_time )
@logger.info( ">>> Fade in #{ imagecount_info( image ) }" )
initial = -100.0
final = 0.0
step = 100 / ( fade_in_time * fps )
fade( image, fade_in_time, fps, initial, final, step )
end
def fade_out( image, fps, fade_out_time )
@logger.info( "<<< Fade out #{ imagecount_info( image ) }" )
initial = 0.0
final = -100.0
step = - ( 100 / ( fade_out_time * fps ) )
fade( image, fade_out_time, fps, initial, final, step )
end
def fade( image, seconds, fps, initial, final, step )
if step > 0 # fade in
ladder = ( initial .. final ).step( step ).to_a
else # fade out
ladder = ( final .. initial ).step( step.abs ).to_a
end
ladder[ -1 ] = 0 # sic. tighten the floats nut
levels = shear_y( ladder, ladder.collect { |rung| sigmoid( rung, initial, final, -50, 0.125 ) } )
( 1 .. ( seconds * fps ) ).each do |i|
filename = sequencefile
level = levels[ i - 1 ]
@logger.cr( level )
asset, todo = check_for_asset( image, @output_format, fps, level )
if todo == TRUE
convert_apply_level( image, level, asset )
end
File.symlink( asset, filename )
@framecount += 1
end
end
def crossfade( image1, image2, fps, seconds )
clear_terminal_line
@logger.info( "XXX Crossfade #{ imagecount_info( image1 ) }" )
initial = -100.0
final = 0.0
step = 100 / ( seconds * fps )
ladder = ( initial .. final ).step( step ).to_a
ladder[ -1 ] = 0
levels = shear_y( ladder, ladder.collect { |rung| sigmoid( rung, initial, final, 50, 0.125 ) } ).map { |v| v.abs }
case @output_type
when "dcp"
compress = "-compress none"
depth = "-depth 12"
when "preview", "fullpreview"
compress = ""
depth = "-depth 8"
end
( 1 .. ( seconds * fps ) ).each do |i|
filename = sequencefile
level = levels[ i - 1 ]
@logger.cr( level )
asset, todo = check_for_asset( [ image1, image2 ], @output_format, fps, level )
if todo == TRUE
composite( image1, level, image2, depth, compress, asset )
end
File.symlink( asset, filename )
@framecount += 1
end
end
def s_sign( value )
return ( value.to_f / value.to_f.abs ).to_i
end
def sigmoid( value, initial, final, center, rate )
if initial > final
base = final
else
base = initial
end
return ( initial - final ).abs / ( 1.0 + Math.exp( rate * s_sign( final - initial ) * ( -( value - center ).to_f ) ) ) + base
end
def shear_y( ladder, levels )
if levels.first > levels.last
levels = levels.reverse
reversed = true
end
shift_initial = ladder.first - levels.first
shift_final = ladder.last - levels.last
shifts = ( shift_initial .. shift_final ).step( ( shift_final - shift_initial ) / ( ladder.size + 1 ) ).to_a
y = []
levels.each_with_index do |v, index|
y[ index ] = ( 100 * ( v + shifts[ index ] ) ).to_i / 100.0 # Max target 12 bpc requires 2 decimal places precision
end
y = y.reverse if reversed
return y
end
def full_level( image, fps, duration )
@logger.cr( "--- Full level #{ imagecount_info( image ) }" )
level = 0
file = sequencefile
File.symlink( image, file )
if ( 1 ..( duration * fps - 1 ) ).none? # only 1 image needed
@framecount += 1 # temporary fix for FIXME @framecount stumble (Errno::EEXIST) on first fade out frame with 0 or 1 frame full level settings, like with $ cinemaslides 01.jpg 02.jpg -x crossfade,1,0
@logger.cr( "Skip sequence links: Only 1 image needed here" )
else
@framecount += 1
sequence_links_to( file, duration, fps )
end
end
def sequence_links_to( file, seconds, fps )
# work around max entries on some filesystems
# ext3 31999, hfs 32767, no such limit on zfs
batchsize = 30000
amount = ( seconds * fps -1 ).to_i
count = 0 # this file's counter, @framecount is global
( amount / batchsize + ( amount % batchsize > 0 ? 1 : 0 ) ).times do
( batchsize - 1 ).times do
break if count == amount
link = sequencefile
File.symlink( file, link )
@framecount += 1
count += 1
end
break if count == amount
file_clone = sequencefile
FileUtils.copy( file, file_clone )
file = file_clone
@framecount += 1
count += 1
end
end
def imagecount_info( image )
"(#{ @imagecount } of #{ @source[ :orig_name ].length })"
end
def sequencefile
File.join( @conformdir, "#{ '%06d' % @framecount }.#{ @output_format }" )
end
# FIXME ugh
def digest_over_content( file )
Digest::MD5.hexdigest( File.read( file ) )
end
def digest_over_name( file )
Digest::MD5.hexdigest( File.basename( file ) )
end
def digest_over_string( string )
Digest::MD5.hexdigest( string )
end
#
# entry into the asset depot will trigger a relatively strong and good enough md5 digest over full content (including metadata)
# members of the asset depot will trigger a cheaper and good enough digest over filename (which is in part an md5 digest)
# + dimensions + (level unless jpeg 2000 codestream requested) + (encoder + fps if jpeg 2000 codestream is requested) + suffix
#
def build_assetname( id, level, suffix, fps )
File.join( @assetsdir, id + "_#{ @dimensions }_#{ @resize == TRUE ? 'r' : 'nr' }#{ level.nil? ? '' : '_' + level.to_s }#{ @output_type == 'dcp' ? suffix == 'j2c' ? '_' + @encoder_id + '_' + fps.to_s + '_' : '' : '_pre' }_.#{ suffix }" )
end
def check_for_crossfade_asset( files, level, suffix, fps )
level_a = level.to_s
level_b = ((100 * (100-level)).to_i / 100.0).to_s
digest_a = digest_over_name files[0]
digest_b = digest_over_name files[1]
primary_id = [ digest_a, digest_b ].join( '_' )
primary_level = [ level_a, level_b ].join( '_' )
an = build_assetname( primary_id, primary_level, suffix, fps )
if File.exists?( an )
return primary_id, primary_level
else # try the reverse
secondary_id = [ digest_b, digest_a ].join( '_' )
secondary_level = [ level_b, level_a ].join( '_' )
an = build_assetname( secondary_id, secondary_level, suffix, fps )
if File.exists?( an )
return secondary_id, secondary_level
end
end
return primary_id, primary_level
end
def check_for_asset( files, suffix, fps, level = nil )
# 2 images from crossfade?
if files.size == 2
if File.dirname( files.first ) != @assetsdir
id = [ digest_over_content( files[ 0 ] ), digest_over_content( files[ 1 ] ) ].join( '_' )
else
id, level = check_for_crossfade_asset( files, level, suffix, fps )
end
origin = [ File.basename( files[ 0 ] ), File.basename( files[ 1 ] ) ].join( ' X ' )
else # not from crossfade
if File.exists?( files )
if File.dirname( files ) != @assetsdir
id = digest_over_content( files )
else
id = digest_over_name( files )
end
else
id = 'black'
end
origin = File.basename( files )
end
assetname = build_assetname( id, level, suffix, fps )
if File.exists?( assetname )
@logger.debug( "Skip: Asset exists (#{ origin } -> #{ File.basename( assetname ) })" )
todo = FALSE
else
todo = TRUE
end
return assetname, todo
end
# all fade/crossfade ops are based on these assets
def conform( image, fps )
@logger.cr( "Conform image: #{ image }" )
asset, todo = check_for_asset( image, @output_format, fps )
if todo == TRUE
convert_resize_extent_color_specs( image, asset )
end
return asset
end
# scale and fit any image to container size. apply color specs if dcp target
def convert_resize_extent_color_specs( image, filename )
case @output_type
when "preview", "fullpreview"
`convert #{ Shellwords.escape image } \
-type TrueColor \
-alpha Off \
-gamma 0.454545454545455 \
#{ @resize == TRUE ? '-resize ' + @dimensions : '' } \
-background black \
-gravity center \
-extent #{ @dimensions } \
-gamma 2.2 \
-depth 8 \
-strip \
-sampling-factor 2x2 \
#{ Shellwords.escape filename }`
# kakadu needs uncompressed 12bpc files # FIXME dep compress on options.encoder
when "dcp"
`convert #{ Shellwords.escape image } \
-type TrueColor \
-alpha Off \
-gamma 0.454545454545455 \
#{ @resize == TRUE ? '-resize ' + @dimensions : '' } \
-background black \
-gravity center \
-extent #{ @dimensions } \
-recolor '#{ SRGB_TO_XYZ }' \
-gamma 2.6 \
-depth 12 \
-compress none \
#{ Shellwords.escape filename }`
else # hello typo
puts "Use '-t preview' (half size) or '-t fullpreview' (full size) or '-t dcp'.\nDefaults to 'preview'."
exit
end
end
# image is already conformed, just apply level here
def convert_apply_level( image, level, filename )
case @output_type
when "preview", "fullpreview"
`convert #{ Shellwords.escape image } \
-type TrueColor \
-gamma 0.454545454545455 \
#{ fadetype( level ) } \
-gamma 2.2 \
#{ Shellwords.escape filename }`
when "dcp" # -compress none for kakadu
`convert #{ Shellwords.escape image } \
-type TrueColor \
-gamma 0.38461538461538458 \
#{ fadetype( level ) } \
-gamma 2.6 \
-depth 12 \
-compress none \
#{ Shellwords.escape filename }`
end
end
def fadetype( level )
"-fill black -colorize #{ level.abs }"
# composite source -size [source's size] xc:black -blend level.abs result
#"-modulate #{ level + 100 }"#,#{ level + 100 }" # second parameter is saturation. this one has channel clipping issues
#"-modulate #{ level + 100 } -blur 0x#{ level }" # experiment, color starvation -> heavy banding
#"-brightness-contrast #{ level }x#{ level }" # not in ubuntu 10.04's im 6.5.7-8, crushes off into swamp blacks
end
def composite( image1, level, image2, depth, compress, output ) # -compress none for kakadu
`composite -type TrueColor #{ image1 } -dissolve #{ level } #{ image2 } #{ depth } #{ compress } #{ output }`