1package Prima::Image::Animate;
2
3use strict;
4use warnings;
5use Carp;
6use Prima;
7
8sub new
9{
10	my $class = shift;
11	my $self = bless {
12		images     => [],
13		model      => 'gif',
14		@_,
15		current    => -1,
16	}, $class;
17
18	$self-> reset;
19
20	return $self;
21}
22
23sub detect_animation
24{
25	my (undef, $extras) = @_;
26	return undef unless                    # more than 1 frame?
27		$extras &&
28		defined($extras->{codecID}) &&
29		$extras->{frames} &&
30		$extras->{frames} > 1;
31	my $c = Prima::Image->codecs($extras-> {codecID}) or return 0;
32	return undef unless $c;
33
34	if ( $c->{name} eq 'GIFLIB') {
35		return 'GIF';
36	} elsif ($c->{name} =~ /^(WebP|PNG)$/) {
37		return $c->{name};
38	} else {
39		return undef;
40	}
41}
42
43sub load
44{
45	my $class = shift;
46
47	my ( $where, %opt) = @_;
48
49	# have any custom notifications?
50	my ( %events, %args);
51	while ( my ( $k, $v) = each %opt) {
52		my $hash = ($k =~ /^on[A-Z]/ ? \%events : \%args);
53		$hash-> {$k} = $v;
54	}
55
56	my $i = Prima::Icon-> new(%events); # dummy object
57
58	my @i = grep { defined } $i-> load(
59		$where,
60		loadExtras => 1,
61		loadAll    => 1,
62		iconUnmask => 1,
63		blending   => 1,
64		%args,
65	);
66	warn $@ if @i && !$i[-1];
67
68	return unless @i;
69	my $model = $class->detect_animation($i[0]->{extras}) or return;
70	$model = 'Prima::Image::Animate::' . $model;
71
72	return $model-> new( images => \@i);
73}
74
75sub add
76{
77	my ( $self, $image) = @_;
78	push @{$self-> {images}}, $image;
79}
80
81sub fixup_rect
82{
83	my ( $self, $info, $image) = @_;
84	return if defined $info-> {rect};
85	$info-> {rect} = {
86		bottom => $self-> {screenHeight} - $info-> {top} - $image-> height,
87		top    => $self-> {screenHeight} - $info-> {top} - 1,
88		right  => $info-> {left} + $image-> width - 1,
89		left   => $info-> {left},
90	};
91}
92
93sub union_rect
94{
95	my ( $self, $r1, $r2) = @_;
96	return { %$r2 } unless grep { $r1-> {$_} } qw(left bottom right top);
97	return { %$r1 } unless grep { $r2-> {$_} } qw(left bottom right top);
98
99	my %ret = %$r1;
100
101
102	for ( qw(left bottom)) {
103		$ret{$_} = $r2-> {$_}
104			if $ret{$_} > $r2-> {$_};
105	}
106	for ( qw(right top)) {
107		$ret{$_} = $r2-> {$_}
108			if $ret{$_} < $r2-> {$_};
109	}
110
111	return \%ret;
112}
113
114sub reset
115{
116	my $self = shift;
117	$self-> {current} = -1;
118
119	delete @{$self}{qw(canvas bgColor saveCanvas
120		saveMask image info
121		screenWidth screenHeight
122		loopCount changedRect cache
123		)};
124
125	my $i = $self-> {images};
126	return unless @$i;
127
128	my $ix = $i-> [0];
129	return unless $ix;
130
131	my $e = $self-> get_extras(0);
132	return unless $e;
133
134	$self-> {image} = $self-> {images}-> [0];
135	$self-> {info}  = $e;
136	$self-> {$_} = $e-> {$_} for qw(screenWidth screenHeight);
137	$self-> {changedRect} = {};
138	$self-> fixup_rect( $e, $ix);
139
140}
141
142sub advance_frame
143{
144	my $self = shift;
145
146	delete @{$self}{qw(image info)};
147	if ( ++$self-> {current} >= @{$self-> {images}}) {
148		# go back to first frame, or stop
149		if ( defined $self-> {loopCount}) {
150		    return 0 if --$self-> {loopCount} <= 0;
151		}
152		$self-> {current} = 0;
153	}
154	$self-> {image} = $self-> {images}-> [$self-> {current}];
155	my $info = $self-> {info} = $self-> get_extras( $self-> {current} );
156	$self-> fixup_rect( $info, $self-> {image});
157
158	# load global extension data
159	if ( $self-> {current} == 0) {
160		unless ( defined $info-> {loopCount}) {
161			$self-> {loopCount} = 1;
162		} elsif ( $info-> {loopCount} == 0) {
163			# loop forever
164			$self-> {loopCount} = undef;
165		} elsif ( !defined $self->{loopCount}) {
166			$self-> {loopCount} = $info-> {loopCount};
167		}
168	}
169	return 1;
170}
171
172sub next  { die }
173sub icon  { die }
174sub image { die }
175sub draw  { die }
176sub get_extras { die }
177
178sub draw_background
179{
180	my ( $self, $canvas, $x, $y) = @_;
181	return 0 unless $self-> {canvas};
182        my $a = $self->bgAlpha // 0xff;
183        return 0 if $a == 0 || !defined $self->bgColor;
184        if ( $a == 0xff ) {
185                my $c = $canvas->color;
186                $canvas->color($self->bgColor);
187                $canvas->bar($x, $y, $x + $self->{screenWidth}, $y + $self->{screenHeight});
188                $canvas->color($c);
189        } else {
190                my $px = $self->{cache}->{bgpixel} //= Prima::Icon->new(
191                        size     => [1,1],
192                        type     => im::RGB,
193                        maskType => im::bpp8,
194                        data     => join('', map { chr } cl::to_bgr($self->bgColor)),
195                        mask     => chr($a),
196                );
197                $canvas->stretch_image( $x, $y, $self->{screenWidth}, $self->{screenHeight}, $px, rop::SrcOver);
198        }
199        return 1;
200}
201
202sub is_stopped
203{
204	my $self = shift;
205	return $self-> {current} >= @{$self-> {images}};
206}
207
208sub width   { $_[0]-> {canvas} ? $_[0]-> {canvas}-> width  : 0 }
209sub height  { $_[0]-> {canvas} ? $_[0]-> {canvas}-> height : 0 }
210sub size    { $_[0]-> {canvas} ? $_[0]-> {canvas}-> size   : (0,0) }
211sub bgColor { $_[0]-> {bgColor} }
212sub bgAlpha { $_[0]-> {bgAlpha} }
213sub current { $_[0]-> {current} }
214sub total   { scalar @{$_[0]-> {images}} }
215
216sub length
217{
218	my $length = 0;
219	$length += $_-> {delayTime} || 0 for
220		map { $_-> {extras} || {} }
221		@{$_[0]-> {images}};
222	return $length / 1000;
223}
224
225sub loopCount
226{
227	return $_[0]-> {loopCount} unless $#_;
228	$_[0]-> {loopCount} = $_[1];
229}
230
231package Prima::Image::Animate::GIF;
232use base 'Prima::Image::Animate';
233
234use constant DISPOSE_NOT_SPECIFIED    => 0; # Leave frame, let new frame draw on top
235use constant DISPOSE_KEEP             => 1; # Leave frame, let new frame draw on top
236use constant DISPOSE_CLEAR            => 2; # Clear the frame's area, revealing bg
237use constant DISPOSE_RESTORE_PREVIOUS => 3; # Restore the previous (composited) frame
238
239sub get_extras
240{
241	my ( $self, $ix) = @_;
242	$ix = $self-> {images}-> [$ix];
243	return unless $ix;
244
245	my $e = $ix-> {extras} || {};
246
247	$e-> {screenHeight}     ||= $ix-> height;
248	$e-> {screenWidth}      ||= $ix-> width;
249	$e-> {$_} ||= 0 for qw(disposalMethod useScreenPalette delayTime left top);
250
251	# gif doesn't support explicit masks, therefore
252	# when image actually has a mask, autoMaskign is set to am::Index
253	$e-> {iconic} = $ix-> isa('Prima::Icon') && $ix-> autoMasking != am::None;
254
255	return $e;
256}
257
258sub next
259{
260	my $self = shift;
261	my %ret;
262
263	# dispose from the previous frame and calculate the changed rect
264	my $info = $self->{info};
265	my @sz = ( $self-> {screenWidth}, $self-> {screenHeight});
266
267	# dispose from the previous frame and calculate the changed rect
268	if ( $info-> {disposalMethod} == DISPOSE_CLEAR) {
269		$self-> {canvas}-> backColor( 0);
270		$self-> {canvas}-> clear;
271		$self-> {mask}-> backColor(cl::Set);
272		$self-> {mask}-> clear;
273
274		%ret = %{ $self-> {changedRect} };
275		$self-> {changedRect} = {};
276	} elsif ( $info-> {disposalMethod} == DISPOSE_RESTORE_PREVIOUS) {
277		# cut to the previous frame, that we expect to be saved for us
278		if ( $self-> {saveCanvas} && $self-> {saveMask}) {
279			$self-> {canvas} = $self-> {saveCanvas};
280			$self-> {mask}   = $self-> {saveMask};
281		}
282		$self-> {changedRect} = $self-> {saveRect};
283		delete $self-> {saveCanvas};
284		delete $self-> {saveMask};
285		delete $self-> {saveRect};
286		%ret = %{ $info-> {rect} };
287	}
288
289	return unless $self->advance_frame;
290
291	$info = $self->{info};
292	if ( $info-> {disposalMethod} == DISPOSE_RESTORE_PREVIOUS) {
293		my $c  = Prima::DeviceBitmap-> new(
294			width      => $sz[0],
295			height     => $sz[1],
296			type       => dbt::Pixmap,
297		);
298		$c-> put_image( 0, 0, $self-> {canvas});
299		$self-> {saveCanvas} = $self-> {canvas};
300		$self-> {canvas} = $c;
301
302		$c = Prima::DeviceBitmap-> new(
303			width      => $sz[0],
304			height     => $sz[1],
305			type       => dbt::Bitmap,
306		);
307		$c-> put_image( 0, 0, $self-> {mask});
308		$self-> {saveMask} = $self-> {mask};
309		$self-> {mask} = $c;
310
311		$self-> {saveRect} = $self-> {changedRect};
312	}
313
314	$self-> {changedRect} = $self->union_rect( $self-> {changedRect}, $info-> {rect});
315	%ret = %{ $self->union_rect( \%ret, $info-> {rect}) };
316
317	# draw the current frame
318	if ( $info-> {iconic}) {
319		my ( $xor, $and) = $self-> {image}-> split;
320		# combine masks
321		$self-> {mask}-> set(
322			color     => cl::Clear,
323			backColor => cl::Set,
324		);
325		$self-> {mask}-> put_image(
326			$info-> {rect}-> {left},
327			$info-> {rect}-> {bottom},
328			$and,
329			rop::AndPut,
330		);
331	} else {
332		my @is = $self->{image}->size;
333		$self-> {mask}-> color(cl::Clear);
334		$self-> {mask}-> bar(
335			$info-> {rect}-> {left},
336			$info-> {rect}-> {bottom},
337			$info-> {rect}-> {left}   + $is[0],
338			$info-> {rect}-> {bottom} + $is[1],
339		);
340	}
341
342	# put non-transparent image pixels
343	$self-> {canvas}-> put_image(
344		$info-> {rect}-> {left},
345		$info-> {rect}-> {bottom},
346		$self-> {image},
347	);
348
349	$ret{$_} ||= 0 for qw(left bottom right top);
350	$ret{delay} = $info-> {delayTime} / 100;
351
352	return \%ret;
353}
354
355sub reset
356{
357	my $self = shift;
358	$self-> SUPER::reset;
359
360	my $e = $self-> get_extras(0);
361	return unless $e;
362
363	$self-> {$_} = $e-> {$_} for qw(screenWidth screenHeight);
364
365	# create canvas and mask
366	$self-> {canvas}  = Prima::DeviceBitmap-> new(
367		width      => $e-> {screenWidth},
368		height     => $e-> {screenHeight},
369		type       => dbt::Pixmap,
370		backColor  => 0,
371	);
372	$self-> {canvas}-> clear; # canvas is all-0 initially
373
374	$self-> {mask}    = Prima::DeviceBitmap-> new(
375		width      => $e-> {screenWidth},
376		height     => $e-> {screenHeight},
377		type       => dbt::Bitmap,
378		backColor  => 0xFFFFFF,
379		color      => 0x000000,
380	);
381	$self-> {mask}-> clear; # mask is all-1 initially
382
383	if ( defined $e-> {screenBackGroundColor}) {
384		my $cm =
385			$e-> {useScreenPalette} ?
386				$e-> {screenPalette} :
387				$self-> {images}-> [0]-> palette;
388		my $i = $e-> {screenBackGroundColor} * 3;
389		$self-> {bgColor} = cl::from_rgb(map { $_ || 0 } @$cm[$i..$i+2]);
390		$self-> {bgAlpha} = 0xff;
391	}
392}
393
394sub icon
395{
396	my $self = shift;
397
398	my $i = Prima::Icon-> new;
399	$i-> combine( $self-> {canvas}-> image, $self-> {mask}-> image);
400	return $i;
401}
402
403sub image
404{
405	my $self = shift;
406
407	my $i = Prima::Image-> new(
408		width     => $self-> {canvas}-> width,
409		height    => $self-> {canvas}-> height,
410		type      => im::RGB,
411		backColor => $self-> {bgColor} || 0,
412	);
413	$i-> begin_paint;
414	$i-> clear;
415	$i-> set(
416		color     => cl::Clear,
417		backColor => cl::Set,
418	);
419	$i-> put_image( 0, 0,$self-> {mask},   rop::AndPut);
420	$i-> put_image( 0, 0,$self-> {canvas}, rop::XorPut);
421	$i-> end_paint;
422
423	return $i;
424}
425
426sub draw
427{
428	my ( $self, $canvas, $x, $y) = @_;
429
430	return unless $self-> {canvas};
431
432	my %save = map { $_ => $canvas-> $_() } qw(color backColor);
433	$canvas-> set(
434		color     => cl::Clear,
435		backColor => cl::Set,
436	);
437	$canvas-> put_image( $x, $y, $self-> {mask},   rop::AndPut);
438	$canvas-> put_image( $x, $y, $self-> {canvas}, rop::XorPut);
439	$canvas-> set( %save);
440}
441
442
443package Prima::Image::Animate::WebPNG;
444use base 'Prima::Image::Animate';
445
446sub new
447{
448	my ( $class, %opt ) = @_;
449
450	# rop::SrcCopy works only with 8-bit alpha
451	for (@{ $opt{images} // [] }) {
452		$_->maskType(im::bpp8) if $_->isa('Prima::Icon');
453	}
454
455	return $class->SUPER::new(%opt);
456}
457
458sub get_extras
459{
460	my ( $self, $ix) = @_;
461	$ix = $self-> {images}-> [$ix];
462	return unless $ix;
463
464	my $e = $ix-> {extras} || {};
465
466	$e-> {screenHeight}     ||= $ix-> height;
467	$e-> {screenWidth}      ||= $ix-> width;
468	$e-> {$_} ||= 0 for qw(disposalMethod blendMethod delayTime left top);
469
470	return $e;
471}
472
473sub next
474{
475	my $self = shift;
476	my $info = $self->{info};
477	my %ret;
478
479	if ( $info-> {disposalMethod} eq 'restore') {
480		# cut to the previous frame, that we expect to be saved for us
481		if ( $self-> {saveCanvas} ) {
482			$self-> {canvas} = $self-> {saveCanvas};
483		}
484		delete $self-> {saveCanvas};
485		%ret = %{ $info-> {rect} };
486	} elsif ( $info-> {disposalMethod} eq 'background') {
487		# dispose from the previous frame and calculate the changed rect
488		$self-> {canvas}-> color(cl::Clear);
489		$self-> {canvas}-> bar(
490			$info-> {rect}-> {left},
491			$info-> {rect}-> {bottom},
492			$self->{image}->width  + $info-> {rect}-> {left} - 1,
493			$self->{image}->height + $info-> {rect}-> {bottom} - 1
494		);
495		%ret = %{ $info-> {rect} };
496	}
497
498	return unless $self->advance_frame;
499	$info = $self->{info};
500	@{$self}{qw(saveCanvas canvas)} = ($self->{canvas}, $self->{canvas}->dup)
501		if $info-> {disposalMethod} eq 'restore';
502
503	%ret = %{ $self->union_rect( \%ret, $info-> {rect}) };
504
505	# draw the current frame
506	$self-> {canvas}-> put_image(
507		$info-> {rect}-> {left},
508		$info-> {rect}-> {bottom},
509		$self-> {image},
510		(( $info-> {blendMethod} eq 'blend') ? rop::SrcOver : rop::SrcCopy)
511	);
512
513	$ret{$_} ||= 0 for qw(left bottom right top);
514	$ret{delay} = $info-> {delayTime} / 1000;
515
516	return \%ret;
517}
518
519sub reset
520{
521	my $self = shift;
522	$self-> SUPER::reset;
523
524	my $e = $self-> get_extras(0);
525	return unless $e;
526
527	$self-> {canvas}  = Prima::DeviceBitmap-> new(
528		width      => $e-> {screenWidth},
529		height     => $e-> {screenHeight},
530		type       => dbt::Layered,
531		backColor  => 0,
532	);
533	$self-> {canvas}-> clear; # canvas is black and transparent
534
535	if ( defined $e-> {background}) {
536		$self-> {bgColor} = cl::from_rgb(cl::to_bgr($e->{background} & 0xffffff));
537		$self-> {bgAlpha} = ($e->{background} >> 24) & 0xff;
538	}
539}
540
541sub icon  { shift->{canvas}->icon }
542sub image { shift->{canvas}->image }
543
544sub draw
545{
546	my ( $self, $canvas, $x, $y) = @_;
547	$canvas-> put_image( $x, $y, $self-> {canvas}, rop::SrcOver) if $self->{canvas};
548}
549
550package Prima::Image::Animate::WebP;
551use base 'Prima::Image::Animate::WebPNG';
552
553package Prima::Image::Animate::PNG;
554use base 'Prima::Image::Animate::WebPNG';
555
556sub new
557{
558	my $class = shift;
559	my $self = $class->SUPER::new(@_);
560	my $i = $self->{images} // [];
561	shift @$i if @$i > 1 && $i->[0]->{extras}->{default_frame};
562	return $self;
563}
564
5651;
566
567__END__
568
569=pod
570
571=head1 NAME
572
573Prima::Image::Animate - animate gif,webp,png files
574
575=head1 DESCRIPTION
576
577The module provides high-level access to GIF, APNG, and WebP animation sequences.
578
579=head1 SYNOPSIS
580
581	use Prima qw(Application Image::Animate);
582	my $x = Prima::Image::Animate->load($ARGV[0]);
583	die $@ unless $x;
584	my ( $X, $Y) = ( 0, 100);
585        my $want_background = 1; # 0 for eventual transparency
586	my $background = $::application-> get_image( $X, $Y, $x-> size);
587	$::application-> begin_paint;
588
589	while ( my $info = $x-> next) {
590		my $frame = $background-> dup;
591		$frame-> begin_paint;
592		$x-> draw_background( $frame, 0, 0) if $want_background;
593		$x-> draw( $frame, 0, 0);
594		$::application-> put_image( $X, $Y, $frame);
595
596		$::application-> sync;
597		select(undef, undef, undef, $info-> {delay});
598	}
599
600        $::application-> put_image( $X, $Y, $g);
601
602=head2 new $CLASS, %OPTIONS
603
604Creates an empty animation container. If C<$OPTIONS{images}> is given, it is
605expected to be an array of images, best if loaded from gif files with
606C<loadExtras> and C<iconUnmask> parameters set ( see L<Prima::image-load> for
607details).
608
609=head2 detect_animation $HASH
610
611Checks C<{extras} hash> obtained from a image loaded with C<loadExtras> flag set,
612to detect whether the image is an animation, and if loading all of its frame is
613supported by the module. Returns file format name on success, undef otherwise.
614
615=head2 load $SOURCE, %OPTIONS
616
617Loads GIF or WebP animation sequence from file or stream C<$SOURCE>. Options
618are the same as understood by C<Prima::Image::load>, and are passed
619down to it.
620
621=head2 add $IMAGE
622
623Appends an image frame to the container.
624
625=head2 bgColor
626
627Return the background color specified by the sequence as the preferred
628background color to use when there is no specific background to superimpose the
629animation to.
630
631=head2 current
632
633Return index of the current frame
634
635=head2 draw $CANVAS, $X, $Y
636
637Draws the current composite frame on C<$CANVAS> at the given coordinates.
638
639=head2 draw_background $CANVAS, $X, $Y
640
641Fills the background on C<$CANVAS> at the given coordinates if file provides that.
642Returns whether the canvas was tainted or not.
643
644=head2 height
645
646Returns height of the composite frame.
647
648=head2 icon
649
650Creates and returns an icon object off the current composite frame.
651
652=head2 image
653
654Creates and returns an image object off the current composite frame.  The
655transparent pixels on the image are replaced with the preferred background
656color.
657
658=head2 is_stopped
659
660Returns true if the animation sequence was stopped, false otherwise.
661If the sequence was stopped, the only way to restart it is to
662call C<reset>.
663
664=head2 length
665
666Returns total animation length (without repeats) in seconds.
667
668=head2 loopCount [ INTEGER ]
669
670Sets and returns number of loops left, undef for indefinite.
671
672=head2 next
673
674Advances one animation frame. The step triggers changes to the internally kept
675icon image that create effect of transparency, if needed.  The method returns a
676hash, where the following fields are initialized:
677
678=over
679
680=item left, bottom, right, top
681
682Coordinates of the changed area since the last frame was updated.
683
684=item delay
685
686Time in seconds how long the frame is expected to be displayed.
687
688=back
689
690=head2 reset
691
692Resets the animation sequence. This call is necessary either when image sequence was altered,
693or when sequence display restart is needed.
694
695=head2 size
696
697Returns width and height of the composite frame.
698
699=head2 total
700
701Return number fo frames
702
703=head2 width
704
705Returns width of the composite frame.
706
707=head1 SEE ALSO
708
709L<Prima::image-load>
710
711=head1 AUTHOR
712
713Dmitry Karasik, E<lt>dmitry@karasik.eu.orgE<gt>.
714
715=cut
716