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