The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
#!/usr/bin/perl -w
#
#  libzvbi test
#
#  Copyright (C) 2000, 2001 Michael H. Schimek
#  Perl Port: Copyright (C) 2007 Tom Zoerner
#
#  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 2 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, write to the Free Software
#  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#

# Perl $Id: caption.pl,v 1.1 2007/11/18 18:48:35 tom Exp tom $
# ZVBI #Id: caption.c,v 1.14 2006/05/22 08:57:05 mschimek Exp #

#
#  Rudimentary render code for Closed Caption (CC) test.
#

use blib;
use strict;
use IO::Handle;
use Switch;
use Tk;
use Video::ZVBI qw(/^VBI_/);

my $vbi;
my $pgno = -1;
my $dx;

my $infile;
my $read_elapsed = 0;

use constant DISP_WIDTH      => 640;
use constant DISP_HEIGHT     => 480;
use constant CELL_WIDTH      => 16;
use constant CELL_HEIGHT     => 26;
use constant DISP_X_OFF      => 40;
use constant DISP_Y_OFF      => 45;

my $tk;
my $canvas;

# hash array to hold IDs of rolling text rows in the canvas
my %shift;
use constant shift_step => 2;

# canvas background color - visible where video would look through
use constant COLORKEY => "#80FF80";

#
#  Remove one row's text content (i.e. all pixmaps in the row's area)
#
sub draw_blank {
        my ($row, $col, $n_cols) = @_;
        my $cid;

        foreach $cid ( $canvas->find( "overlapping",
                                      $col * CELL_WIDTH + 1 + DISP_X_OFF,
                                      $row * CELL_HEIGHT + 1 + DISP_Y_OFF,
                                      ($col + $n_cols) * CELL_WIDTH - 2 + DISP_X_OFF,
                                      ($row + 1) * CELL_HEIGHT - 2 + DISP_X_OFF ) ) {

                my $img = $canvas->itemcget($cid, -image);

                # remove the pixmap from the canvas
                $canvas->delete($cid);

                # destroy the image (important to free the image's memory)
                $img->delete();
        }
}

sub is_transp {
        (($_[0] >> 16) & 0x0F) == VBI_TRANSPARENT_SPACE;
}

#
#  Draw one row of text
#
sub draw_row {
        my ($pg, $row) = @_;
        my $col;

        my ($rows, $columns) = $pg->get_page_size();
        my $prop = $pg->get_page_text_properties();

        # first remove all old text in the row
        draw_blank($row, 0, $columns);

        for ($col = 0; $col < $columns; $col++) {
                # skip transparent characters
                if (is_transp($prop->[$row * $columns + $col])) {
                        next;
                }
                # count number of subsequent non-transparent characters
                # (required as optimisation - drawing each char separately is very slow)
                my $i = $col + 1;
                while (($i < $columns) &&
                                !is_transp($prop->[$row * $columns + $i])) {
                        $i++;
                }
                # create RGBA image of the character sequence
                my $vbi_canvas;
                my $fmt;
                if (Video::ZVBI::check_lib_version(0,2,26)) {
                        $fmt = VBI_PIXFMT_PAL8;
                } else {
                        $fmt = VBI_PIXFMT_RGBA32_LE;
                }
                $pg->draw_cc_page_region ($fmt,
                                          $vbi_canvas, -1, $col, $row, $i - $col, 1);

                # convert into a pixmap via XPM
                my $img = $tk->Pixmap(-data, $pg->canvas_to_xpm($vbi_canvas, $fmt));

                # finally, display the pixmap in the canvas
                my $cid = $canvas->createImage($col * CELL_WIDTH + DISP_X_OFF,
                                               $row * CELL_HEIGHT + DISP_Y_OFF,
                                               -anchor, "nw", -image, $img);
                $col = $i;
        }
}

#
#  Timer event for rolling text rows
#
sub bump {
        my ($snap) = @_;
        my $cid;
        my $renew = 0;

        foreach $cid (keys %shift) {
                my $d = $shift{$cid};
                my $step;
                if ($snap) {
                        $step = $d;
                } else {
                        $step = (($d < shift_step) ? $d : shift_step);
                }
                $canvas->move($cid, 0, 0 - $step);

                $shift{$cid} -= $step;
                if ($shift{$cid} <= 0) {
                        delete $shift{$cid};
                } else {
                        $renew = 1;
                }
        }

        if ($renew) {
                $tk->after(20 * shift_step, sub {bump(0)});
        }
}

#
#  Scroll a range of rows upwards
#
sub roll_up {
        my ($pg, $first_row, $last_row) = @_;
        my $cid;

        if (1) { # ---- soft scrolling ----

                # snap possibly still moving rows into their target positions
                bump(1);

                foreach $cid ( $canvas->find("overlapping",
                                             0,
                                             $first_row * CELL_HEIGHT + 1 + DISP_Y_OFF,
                                             DISP_WIDTH,
                                             ($last_row + 1) * CELL_HEIGHT - 1 + DISP_Y_OFF) ) {

                        $shift{$cid} = CELL_HEIGHT;

                        # start time to trigger smooth scrolling
                        $tk->after(20 + 20 * shift_step, sub {bump(0)});
                }

        } else { # ---- jumpy scrolling ----

                foreach ( $canvas->find("overlapping",
                                        0,
                                        $first_row * CELL_HEIGHT + DISP_Y_OFF,
                                        DISP_WIDTH,
                                        ($last_row + 1) * CELL_HEIGHT - 1 + DISP_Y_OFF) ) {

                        $canvas->move($_, 0, 0 - CELL_HEIGHT);
                }
        }
}

#
#  Update a range of text rows
#
sub render {
        my ($pg, $y0, $y1) = @_;

        # snap possibly still moving rows into their target positions
        bump(1);

        foreach my $row ($y0 .. $y1) {
                draw_row ($pg, $row);
        }
}


#
#  Clear all text on-screen
#
sub clear {
        foreach my $cid ( $canvas->find( "all" ) ) {
                my $img = $canvas->itemcget($cid, -image);
                $canvas->delete($cid);
                $img->delete();
        }
}

#
#  Callback invoked by the VBI decoder when a new CC line is available
#
sub cc_handler {
        my ($type, $ev) = @_;

        if ($pgno != -1 && $ev->{pgno} != $pgno) {
                return;
        }

        # Fetching & rendering in the handler
        # is a bad idea, but this is only a test

        my $pg = $vbi->fetch_cc_page ($ev->{pgno});
        die "failed to fetch page $pgno\n" unless defined $pg;

        my ($rows, $columns) = $pg->get_page_size();
        my ($y0, $y1, $roll) = $pg->get_page_dirty_range();

        if (abs ($roll) > $rows) {
                clear ();
        } elsif ($roll == -1) {
                #draw_blank($y0, 0, $columns);
                roll_up ($pg, $y0+1, $y1);
        } else {
                render ($pg, $y0, $y1);
        }
}

#
#  Callback bound to CC channel changes
#
sub reset {
        my $pg = $vbi->fetch_cc_page ($pgno);
        if (defined $pgno) {

                my ($rows, $columns) = $pg->get_page_size();

                render ($pg, 0, $rows - 1);
        } else {
                clear ();
        }
}

#
#  Create the GUI
#
sub init_window {
        my $f;
        my $b;

        $tk = MainWindow->new();

        # at the top: button array to switch CC channels
        $f = $tk->Frame();
        $b = $f->Label(-text, "Page:");
        $b->pack(-side, "left");
        for (my $i=1; $i <= 8; $i++) {
                $b = $f->Radiobutton(-text, $i, -value, $i, -variable, \$pgno, -command, \&reset);
                $b->pack(-side, "left");
        }
        $f->pack(-side, "top");

        # canvas to display CC text (as pixmaps)
        $canvas = $tk->Canvas(-borderwidth, 1, -relief, "sunken",
                              -background, COLORKEY,
                              -height, DISP_HEIGHT, -width, DISP_WIDTH);
        $canvas->pack(-side, "top");
        $canvas->focus();
}

#
#  Feed caption from live stream or file with sample data
#

sub pes_mainloop {
        my $buffer;
        my $bytes_left;
        my $sliced;
        my $n_lines;
        my $pts;

        while (read (STDIN, $buffer, 2048)) {
                my $bytes_left = length($buffer);

                while ($bytes_left > 0) {

                        $n_lines = $dx->cor ($sliced, 64, $pts, $buffer, $bytes_left);
                        if ($n_lines > 0) {
                                $vbi->decode ($sliced, $n_lines, $pts / 90000.0);
                        }
                }

                $tk->after(20, \&pes_mainloop);
                return;
        }
        print STDERR "\rEnd of stream\n";
}

sub old_mainloop {
        my $sliced;
        my $timestamp;
        my $n_lines;

        # one one frame's worth of sliced data from the input stream or file
        ($n_lines, $timestamp, $sliced) = read_sliced ();
        if (defined $n_lines) {
                my $buf = "";
                my $set;
                # pack the read data into the normal slicer output format
                # (i.e. the format delivered by the librarie's internal slicer)
                foreach $set (@$sliced) {
                        $buf .= pack "LLa56", @$set;
                }
                # pass the full frame's data to the decoder
                $vbi->decode ($buf, $n_lines, $timestamp);

                # FIXME: reading from STDIN, so $tk->fileevent(readable) could be used instead of polling
                $tk->after(20, \&old_mainloop);
        } else {
                print STDERR "\rEnd of stream\n";
        }
}

# ----------------------------------------------------------------------------
#
#  Generate artificial caption data
#
my @sim_buf;
my $cmd_time;

sub cmd {
        my ($n) = @_;
        my $sliced;

        $sliced = pack "LLCCx54", VBI_SLICED_CAPTION_525,
                                  21,
                                  Video::ZVBI::par8 ($n >> 8),
                                  Video::ZVBI::par8 ($n & 0x7F);

        push @sim_buf, ["sliced", $sliced, $cmd_time];
        #$vbi->decode ($sliced, 1, $cmd_time);

        $cmd_time += 1 / 29.97;
}

sub printc {
        cmd ($_[0] * 256 + 0x80);

        push @sim_buf, ["delay", 1];
}

sub prints {
        my @s = unpack "C*", $_[0];
        my $i;

        for ($i=0; $s[$i] && $s[$i+1]; $i += 2) {
                cmd ($s[$i] * 256 + $s[$i+1]);
        }
        if ($s[$i]) {
                cmd ($s[$i] * 256 + 0x80);
        }
        push @sim_buf, ["delay", 1];
}

use constant white => 0;
use constant green => 1;
use constant red => 4;
use constant yellow => 5;
use constant blue => 2;
use constant cyan => 3;
use constant magenta => 6;
use constant black => 7;

use constant mapping_row => (2, 3, 4, 5,  10, 11, 12, 13, 14, 15,  0, 6, 7, 8, 9, -1);

use constant italic => 7;
use constant underline => 1;
use constant opaque => 0;
use constant semi_transp => 1;

my $ch;

sub BACKG {           cmd (0x2000);
                      cmd (0x1020 + (($ch & 1) << 11) + ($_[0] << 1) + $_[1]); }
sub PREAMBLE {
                      cmd (0x1040 + (($ch & 1) << 11) + (((mapping_row)[$_[0]] & 14) << 7)
                           + (((mapping_row)[$_[0]] & 1) << 5) + ($_[1] << 1) + $_[2]); }
sub INDENT {
                      cmd (0x1050 + (($ch & 1) << 11) + (((mapping_row)[$_[0]] & 14) << 7)
                           + (((mapping_row)[$_[0]] & 1) << 5) + (($_[1] / 4) << 1) + $_[2]); }
sub MIDROW          { cmd (0x1120 + (($ch & 1) << 11) + ($_[0] << 1) + $_[1]); }
sub SPECIAL_CHAR    { cmd (0x1130 + (($ch & 1) << 11) + $_[0]) }
sub CCODE           { ($_[0] + (($_[1] & 1) << 11) + (($_[1] & 2) << 7)) }
sub RESUME_CAPTION  { cmd (CCODE (0x1420, $ch)) }
sub BACKSPACE       { cmd (CCODE (0x1421, $ch)) }
sub DELETE_EOR      { cmd (CCODE (0x1424, $ch)) }
sub ROLL_UP         { cmd (CCODE (0x1425, $ch) + $_[0] - 2) }
sub FLASH_ON        { cmd (CCODE (0x1428, $ch)) }
sub RESUME_DIRECT   { cmd (CCODE (0x1429, $ch)) }
sub TEXT_RESTART    { cmd (CCODE (0x142A, $ch)) }
sub RESUME_TEXT     { cmd (CCODE (0x142B, $ch)) }
sub END_OF_CAPTION  { cmd (CCODE (0x142F, $ch)) }
sub ERASE_DISPLAY   { cmd (CCODE (0x142C, $ch)) }
sub CR              { cmd (CCODE (0x142D, $ch)) }
sub ERASE_HIDDEN    { cmd (CCODE (0x142E, $ch)) }
sub TAB             { cmd (CCODE (0x1720, $ch) + $_[0]) }
sub TRANSP          { (cmd (0x2000), cmd (0x172D + (($ch & 1) << 11))) }
sub BLACK           { (cmd (0x2000), cmd (0x172E + (($ch & 1) << 11) + $_[0])) }

sub PAUSE {
        my ($n_frames) = @_;

        push @sim_buf, ["delay", $n_frames];
}

sub hello_world {
        my $i;

        @sim_buf = ();
        $cmd_time = 0.0;
        $pgno = -1;

        prints (" HELLO WORLD! ");
        PAUSE (30);

        $ch = 4;
        TEXT_RESTART;
        prints ("Character set - Text 1");
        CR; CR;
        for ($i = 32; $i <= 127; $i++) {
                printc ($i);
                if (($i & 15) == 15) {
                        CR;
                }
        }
        MIDROW (italic, 0);
        for ($i = 32; $i <= 127; $i++) {
                printc ($i);
                if (($i & 15) == 15) {
                        CR;
                }
        }
        MIDROW (white, underline);
        for ($i = 32; $i <= 127; $i++) {
                printc ($i);
                if (($i & 15) == 15) {
                        CR;
                }
        }
        MIDROW (white, 0);
        prints ("Special: ");
        for ($i = 0; $i <= 15; $i++) {
                SPECIAL_CHAR ($i);
        }
        CR;
        prints ("DONE - Text 1 ");
        PAUSE (50);

        $ch = 5;
        TEXT_RESTART;
        prints ("Styles - Text 2");
        CR; CR;
        MIDROW (white, 0); prints ("WHITE"); CR;
        MIDROW (red, 0); prints ("RED"); CR;
        MIDROW (green, 0); prints ("GREEN"); CR;
        MIDROW (blue, 0); prints ("BLUE"); CR;
        MIDROW (yellow, 0); prints ("YELLOW"); CR;
        MIDROW (cyan, 0); prints ("CYAN"); CR;
        MIDROW (magenta, 0); prints ("MAGENTA"); BLACK (0); CR;
        BACKG (white, opaque); prints ("WHITE"); BACKG (black, opaque); CR;
        BACKG (red, opaque); prints ("RED"); BACKG (black, opaque); CR;
        BACKG (green, opaque); prints ("GREEN"); BACKG (black, opaque); CR;
        BACKG (blue, opaque); prints ("BLUE"); BACKG (black, opaque); CR;
        BACKG (yellow, opaque); prints ("YELLOW"); BACKG (black, opaque); CR;
        BACKG (cyan, opaque); prints ("CYAN"); BACKG (black, opaque); CR;
        BACKG (magenta, opaque); prints ("MAGENTA"); BACKG (black, opaque); CR;
        PAUSE (200);
        TRANSP;
        prints (" TRANSPARENT BACKGROUND ");
        BACKG (black, opaque); CR;
        MIDROW (white, 0); FLASH_ON;
        prints (" Flashing Text  (if implemented) "); CR;
        MIDROW (white, 0); prints ("DONE - Text 2 ");
        PAUSE (50);

        $ch = 0;
        ROLL_UP (2);
        ERASE_DISPLAY;
        prints (" ROLL-UP TEST "); CR; PAUSE (20);
        prints ("The ZVBI library provides"); CR; PAUSE (20);
        prints ("routines to access raw VBI"); CR; PAUSE (20);
        prints ("sampling devices (currently"); CR; PAUSE (20);
        prints ("the Linux V4L and and V4L2"); CR; PAUSE (20);
        prints ("API and the FreeBSD, OpenBSD,"); CR; PAUSE (20);
        prints ("NetBSD and BSDi bktr driver"); CR; PAUSE (20);
        prints ("API are supported), a versatile"); CR; PAUSE (20);
        prints ("raw VBI bit slicer, decoders"); CR; PAUSE (20);
        prints ("for various data services and"); CR; PAUSE (20);
        prints ("basic search, render and export"); CR; PAUSE (20);
        prints ("functions for text pages. The"); CR; PAUSE (20);
        prints ("library was written for the"); CR; PAUSE (20);
        prints ("Zapping TV viewer and Zapzilla"); CR; PAUSE (20);
        prints ("Teletext browser."); CR; PAUSE (20);
        CR; PAUSE (30);
        prints (" DONE - Caption 1 ");
        PAUSE (30);

        $ch = 1;
        RESUME_DIRECT;
        ERASE_DISPLAY;
        MIDROW (yellow, 0);
        INDENT (2, 10, 0); prints (" FOO "); CR;
        INDENT (3, 10, 0); prints (" MIKE WAS HERE "); CR; PAUSE (20);
        MIDROW (red, 0);
        INDENT (6, 13, 0); prints (" AND NOW... "); CR;
        INDENT (8, 13, 0); prints (" TOM'S THERE TOO "); CR; PAUSE (20);
        PREAMBLE (12, cyan, 0);
        prints ("01234567890123456789012345678901234567890123456789"); CR;
        MIDROW (white, 0);
        prints (" DONE - Caption 2 "); CR;
        PAUSE (30);
}

#
#  Play back the buffered (simulated) CC data
#
sub play_world {
        while ($#sim_buf >= 0) {
                $a = shift @sim_buf;
                if ($a->[0] eq "delay") {
                        # delay event -> stop play-back and start timer
                        $tk->after(25 * $a->[1], \&play_world);
                        last;
                } else {
                        # pass VBI Data to the decoder context
                        # (will trigger display via the CC callback function)
                        $vbi->decode ($a->[1], 1, $a->[2]);
                }
        }
}

# ------- slicer.c -----------------------------------------------------------
#
# Read one frame's worth of sliced data (written by decode.pl)
# from a file or pipe (not used in demo mode)
#
sub read_sliced {
        my $buf;

	#die "stream error: $!\n" if ($infile->ferror ());

	if ($infile->eof () || !($buf = $infile->gets ())) {
		return undef;
        }

	# Time in seconds since last frame.
        die "invalid timestamp in input\n" unless $buf =~ /^(-?\d+|(-?\d*\.\d+))$/;
	my $dt = $buf + 0.0;
	if ($dt < 0.0) {
		$dt = -$dt;
	}

	my $timestamp = $read_elapsed;
	$read_elapsed += $dt;

	my $n_lines = unpack "C", $infile->getc ();
	die "invalid line count in input: $n_lines\n" if ($n_lines < 0);

        my @sliced = ();

	for (my $n = 0; $n < $n_lines; $n++) {

		my $index = unpack "C", $infile->getc ();
		die "invalid index: $index\n" if ($index < 0);

		my $line = (unpack("C", $infile->getc())
			   + 256 * unpack("C", $infile->getc())) & 0xFFF;

		die "IO: $!\n" if ($infile->eof () || $infile->error ());

                my $id;
                my $data;

		switch ($index) {
		case 0 {
			$id = VBI_SLICED_TELETEXT_B;
			$infile->read ($data, 42);
		}
		case 1 {
			$id = VBI_SLICED_CAPTION_625; 
			$infile->read ($data, 2);
		}
		case 2 {
			$id = VBI_SLICED_VPS;
			$infile->read ($data, 13);
		}
		case 3 {
			$id = VBI_SLICED_WSS_625; 
			$infile->read ($data, 2);
		}
		case 4 {
			$id = VBI_SLICED_WSS_CPR1204; 
			$infile->read ($data, 3);
		}
		case 7 {
			$id = VBI_SLICED_CAPTION_525; 
			$infile->read($data, 2);
		}
		else {
			die "\nOops! Unknown data type $index ".
				 "in sliced VBI file\n";
		}
                }

		die "IO: $!\n" if ($infile->error ());

		push @sliced, [$id, $line, $data];
	}

	return ($n_lines, $timestamp, \@sliced);
}

# ----------------------------------------------------------------------------

sub main_func {
        my $success;

        # create the GUI
        init_window ();

        # create a decoder context and enable Closed Captioning decoding
        $vbi = Video::ZVBI::vt::decoder_new ();
        die "Failed to create VT decoder\n" unless defined $vbi;

        $success = $vbi->event_handler_add (VBI_EVENT_CAPTION, \&cc_handler);
        die "Failed to add event handler\n" unless $success;

        if (-t STDIN) {
                # no file or stream on STDIN -> generate demo data
                hello_world ();
                # start play back of the demo data (timer-based, to give control to the main loop below)
                play_world ();
        } else {
                $pgno = 1;

                $infile = new IO::Handle;
                $infile->fdopen(fileno(STDIN), "r");

                my $c = ord($infile->getc() || 1);
                $infile->ungetc($c);

                if (0 == $c) {
                        $dx = Video::ZVBI::dvb_demux::pes_new ();
                        die "Failed to create DVB demuxer\n" unless defined $dx;

                        $tk->after(20, \&pes_mainloop);
                } else {
                        # install timer to poll for incoming data
                        $tk->after(20, \&old_mainloop);
                }
        }

        # everything from here on is event driven
        MainLoop;
}

main_func();