#!/usr/bin/perl -w use strict; use blib; use Tk; use Tk::Pgplot; use PGPLOT; use constant IMAGE_SIZE => 129; use constant SLICE_SIZE => 100; use constant XA => int -(IMAGE_SIZE)/2; use constant XB => int IMAGE_SIZE/2; use constant YA => int -(IMAGE_SIZE)/2; use constant YB => int IMAGE_SIZE/2; use constant SCALE => 40/IMAGE_SIZE; sub create_main_menubar ($); sub create_image_area ($); sub create_slice_area ($); sub create_save_dialog ($); sub create_help_dialog ($); sub create_world_labels ($); sub create_option_menu ($$); # Define some colour tables. # Define single-colour ramp functions. my $grey_l = [0.0,1.0]; my $grey_c = [0.0,1.0]; # Define a rainbow colour table. my $rain_l = [-0.5, 0.0, 0.17, 0.33, 0.50, 0.67, 0.83, 1.0, 1.7]; my $rain_r = [ 0.0, 0.0, 0.0, 0.0, 0.6, 1.0, 1.0, 1.0, 1.0]; my $rain_g = [ 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.6, 0.0, 1.0]; my $rain_b = [ 0.0, 0.3, 0.8, 1.0, 0.3, 0.0, 0.0, 0.0, 1.0]; # Iraf "heat" colour table. my $heat_l = [0.0, 0.2, 0.4, 0.6, 1.0]; my $heat_r = [0.0, 0.5, 1.0, 1.0, 1.0]; my $heat_g = [0.0, 0.0, 0.5, 1.0, 1.0]; my $heat_b = [0.0, 0.0, 0.0, 0.3, 1.0]; # AIPS tvfiddle discrete rainbow colour table. my $aips_l = [0.0, 0.1, 0.1, 0.2, 0.2, 0.3, 0.3, 0.4, 0.4, 0.5, 0.5, 0.6, 0.6, 0.7, 0.7, 0.8, 0.8, 0.9, 0.9, 1.0]; my $aips_r = [0.0, 0.0, 0.3, 0.3, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]; my $aips_g = [0.0, 0.0, 0.3, 0.3, 0.0, 0.0, 0.0, 0.0, 0.8, 0.8, 0.6, 0.6, 1.0, 1.0, 1.0, 1.0, 0.8, 0.8, 0.0, 0.0]; my $aips_b = [0.0, 0.0, 0.3, 0.3, 0.7, 0.7, 0.7, 0.7, 0.9, 0.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; # List the supported colour tables. my %cmap = ("grey", [$grey_l, $grey_c, $grey_c, $grey_c, scalar(@$grey_l)], "rainbow", [$rain_l, $rain_r, $rain_g, $rain_b, scalar(@$rain_l)], "heat", [$heat_l, $heat_r, $heat_g, $heat_b, scalar(@$heat_l)], "aips", [$aips_l, $aips_r, $aips_g, $aips_b, scalar(@$aips_l)], ); my $mw = MainWindow->new(-title => 'Pgptkdemo'); $mw->iconname("Pgtkdemo"); $mw->configure(-cursor => 'top_left_arrow'); # Override selected widget defaults. $mw->optionAdd('*font' => '-Adobe-Times-Medium-R-Normal-*-140-*', 'widgetDefault'); # Set default widget colours. my $bg = '#bfe5ff'; my $alt_bg = '#00ddff'; $mw->configure(bg => $bg); $mw->optionAdd('*background' => $bg, 'widgetDefault'); $mw->optionAdd('*activeBackground' => $bg, 'widgetDefault'); $mw->optionAdd('*activeForeground' => 'blue', 'widgetDefault'); $mw->optionAdd('*highlightBackground' => $bg, 'widgetDefault'); $mw->optionAdd('*troughColor' => $bg, 'widgetDefault'); $mw->optionAdd('*Scrollbar.width' => '3m', 'widgetDefault'); $mw->optionAdd('*Scrollbar.background' => $alt_bg, 'widgetDefault'); $mw->optionAdd('*Scrollbar*Foreground' => $alt_bg, 'widgetDefault'); $mw->optionAdd('*Button*Background' => $alt_bg, 'widgetDefault'); $mw->optionAdd('*Button*activeBackground' => $alt_bg, 'widgetDefault'); $mw->optionAdd('*Button*activeForeground' => 'black', 'widgetDefault'); $mw->optionAdd('*Menubutton*activeForeground' => 'black', 'widgetDefault'); # Create the menu-bar my $menubar = create_main_menubar($mw); # Create label widgets for use in displaying image world coordinates my ($world, $xworld, $yworld) = create_world_labels($mw); # Create a PGPLOT window with scroll bars, and enclose them in a frame. # This is the image window. my ($imagearea, $pgimage) = create_image_area($mw); # Create a PGPLOT window, and enclose it in a frame. # This is the slice window. my ($slicearea, $pgslice) = create_slice_area($mw); my %demodata = initialize_demo(); # Create the function-selection option menu. my $function = create_option_menu($mw, $pgimage); # Create dialogs for later display. my $save_dialog = create_save_dialog($mw); my $help_dialog = create_help_dialog($mw); # Place the menubar at the top of the main window and the work-areas # underneath it. $world->pack(-side => 'top', -anchor => 'w'); $imagearea->pack(-side => 'top', -fill => 'both', -expand => 1); $function->pack(-side => 'top', -fill => 'x'); $slicearea->pack(-side => 'top', -fill => 'both', -expand => 1); # Windows in Tk do not take on their final sizes until the whole # application has been mapped. This makes it impossible for the # PGPLOT widget to correctly guess what size of pixmap to allocate # when it starts the first page. To avoid this problem, force Tk # to create all of the windows before drawing the first plot. $mw->idletasks; # Draw the initial image. draw_image($mw, 0); MainLoop; # This procedure creates the main menubar of the application. sub create_main_menubar ($) { my $mw = shift; my $menubar = $mw->Menu; $mw->configure(-menu => $menubar); # Create the file menu. my $filemenu = $menubar->cascade(-label => 'File', -tearoff => 0, -menuitems => [ [Button => 'Save image as ...', -command => sub {$save_dialog->deiconify; $save_dialog->raise}], [Separator => ''], [Button => 'Quit', -command => sub {exit}] ]); # Arrange that Alt-Q will abort the application. $mw->bind('all', '' => sub {exit}); # Create the help menu my $helpmenu = $menubar->cascade(-label => 'Help', -tearoff => 0, -menuitems => [ [Button => 'Usage', -command => sub {$help_dialog->deiconify; $help_dialog->raise}]]); return $menubar; } # Create an area in which to display the world coordinates of the cursor # when it is over the image window. sub create_world_labels ($) { my $mw = shift; # Enclose the area in a frame. my $world = $mw->Frame(-width => '11c', -height => '1c'); # Create a static title label. my $title = $world->Label(-text => 'World coordinates:'); # Create the X and Y labels for displaying the respective coordinates. my $x = $world->Label(-width => 12, -anchor => 'w'); my $y = $world->Label(-width => 12, -anchor => 'w'); # Pack the widgets $title->pack(-side => 'left', -anchor => 'w'); $x->pack(-side => 'left', -anchor => 'w', -padx => '2m'); $y->pack(-side => 'left', -anchor => 'w', -padx => '2m'); return $world, $x, $y; } # This procedure is called whenever cursor motion is detected in the # the image widget. It displays the world coordinates of the cursor # in previously created label widgets. sub report_motion { my $pg = shift; my $e = $pg->XEvent; my $x = $e->x; my $y = $e->y; my $xx = sprintf "X=%.2f", $pg->world('x', $x); my $yy = sprintf "Y=%.2f", $pg->world('y', $y); $xworld->configure(-text => $xx); $yworld->configure(-text => $yy); } # Create the area that contains the image PGPLOT window. sub create_image_area ($) { my $mw = shift; # Frame the workarea my $w = $mw->Frame(-width => '11c', -height => '11c'); # Create the PGPLOT image window. my $pgplot = $w->Pgplot(-name => 'image', -share => 1, -width => '10c', -height => '10c', -mincolors => 25, -maxcolors => 64, -bd => 2, -bg => 'black', -fg => 'white', -cursor => ['crosshair', 'black', 'white']); # Create horizontal and vertical scroll-bars and have them # call the pgplot xview and yview scroll commands to scroll the # image within the window. my $xscroll = $w->Scrollbar(-orient => 'horizontal', -command => ['xview', $pgplot]); my $yscroll = $w->Scrollbar(-orient => 'vertical', -command => ['yview', $pgplot]); # Tell the PGPLOT widget how to update the scrollbar sliders. $pgplot->configure(-xscrollcommand => ['set', $xscroll]); $pgplot->configure(-yscrollcommand => ['set', $yscroll]); # Position the PGPLOT widget and the scrollbars. $xscroll->pack(-side => 'bottom', -fill => 'x'); $yscroll->pack(-side => 'right', -fill => 'y'); $pgplot->pack(-side => 'left', -fill => 'both', -expand => 1); # Bind motion events to the world coordinate x and y label widgets. $pgplot->bind('' => \&report_motion); return ($w, $pgplot); } # A sinc(radius) function. sub sinc_fn ($$) { my ($x, $y) = @_; my $radius = sqrt($x*$x + $y*$y); return (abs($radius) < 1.0e-6) ? 1.0 : sin($radius)/$radius; } # A exp(-(x^2+y^2)/20) function. sub gaus_fn ($$) { my ($x, $y) = @_; return exp(-(($x*$x)+($y*$y))/20.0); } # A cos(radius)*sin(angle) function. sub ring_fn ($$) { my ($x, $y) = @_; return cos(sqrt($x*$x + $y*$y)) * sin(($x==0.0 && $y==0.0) ? 0.0 : atan2($x,$y)); } # A sin(angle) function. sub sin_angle_fn ($$) { my ($x, $y) = @_; return sin(($x==0.0 && $y==0.0) ? 0.0 : atan2($x,$y)); } # A cos(radius) function. sub cos_radius_fn () { my ($x, $y) = @_; return cos(sqrt($x*$x + $y*$y)); } # A (1+sin(6*angle))*exp(-radius^2 / 100)function. sub star_fn ($$) { my ($x, $y) = @_; return (1.0 + sin(($x==0.0 && $y==0.0) ? 0.0 : 6.0*atan2($x,$y))) * exp(-(($x*$x)+($y*$y))/100.0); } sub draw_image { my $mw = shift; my $plotid = shift; # Display a busy-cursor. $mw->Busy(); # Install the new function if ($plotid==0) { $demodata{fn} = \&ring_fn; } elsif ($plotid==1) { $demodata{fn} = \&sinc_fn; } elsif ($plotid==2) { $demodata{fn} = \&gaus_fn; } elsif ($plotid==3) { $demodata{fn} = \&sin_angle_fn; } elsif ($plotid==4) { $demodata{fn} = \&cos_radius_fn; } elsif ($plotid==5) { $demodata{fn} = \&star_fn; } my $fn = $demodata{fn}; # Display a "please wait" message in the slice window. display_busy(%demodata); # Fill the image array via the current display function. my $value; my $pixel = $demodata{image}; my $vmin = my $vmax = &$fn(XA*SCALE, YA*SCALE); my $i = 0; for (my $iy = YA; $iy<=YB; $iy++) { for (my $ix = XA; $ix<=XB; $ix++) { $value = &$fn($ix*SCALE, $iy*SCALE); $pixel->[$i] = $value; if ($value<$vmin) { $vmin = $value; } elsif ($value>$vmax) { $vmax = $value; } $i++; } } # Record the min and max values of the data array. $demodata{datamin} = $vmin; $demodata{datamax} = $vmax; # Display the new image. display_image($demodata{image_id}); # Display instructions in the slice window. display_help(%demodata); # No slice has been selected yet. $demodata{have_slice} = 0; # Reset the cursor. $mw->Unbusy; # Arm the cursor of the image window for the selection of a slice. prepare_for_slice(\%demodata); } # Display the current image function in a specified PGPLOT device. sub display_image ($) { my $id = shift; # Select the specified PGPLOT device and display the image array. pgslct($id); pgask(0); pgpage(); pgsch(1.0); pgvstd(); pgwnad(XA*SCALE, XB*SCALE, YA*SCALE, YB*SCALE); my @tr = (); # Coordinate definition matrix $tr[0] = (XA - 1) * SCALE; $tr[1] = SCALE; $tr[2] = 0.0; $tr[3] = (YA - 1) * SCALE; $tr[4] = 0.0; $tr[5] = SCALE; if ($demodata{monochrome}) { pggray($demodata{image}, IMAGE_SIZE, IMAGE_SIZE, 1, IMAGE_SIZE, 1, IMAGE_SIZE, $demodata{datamax}, $demodata{datamin}, \@tr); } else { pgimag($demodata{image}, IMAGE_SIZE, IMAGE_SIZE, 1, IMAGE_SIZE, 1, IMAGE_SIZE, $demodata{datamin}, $demodata{datamax}, \@tr); } pgsci(1); pgbox("BCNST", 0.0, 0, "BCNST", 0.0, 0); pglab("X", "Y", "Image display demo"); } # Create a labelled option menu. sub create_option_menu ($$) { my $mw = shift; my $pgimage = shift; # Create a frame to enclose the menu. my $w = $mw->Frame(); my $w1 = $w->Frame()->pack(-side => 'top', -fill => 'x'); my $w2 = $w->Frame()->pack(-side => 'top', -fill => 'x'); # Create the option-menu label. my $dlabel = $w1->Label(-text => 'Select a display function:'); # Create the option menu. my ($menutext, $function_menu); my $dmenu = $w1->Optionmenu(-command => [\&draw_image, $mw], -variable => \$function_menu, -textvariable => \$menutext); $dmenu->addOptions(['cos(R)sin(A)' => 0], ['sinc(R)' => 1], ['exp(-R^2/20.0)' => 2], ['sin(A)' => 3], ['cos(R)' => 4], ['(1+sin(6A))exp(-R^2/100)' => 5]); # Create the colormap-selection option menu and label my $clabel = $w2->Label(-text => 'Select a color table:'); my $cmenu = $w2->Optionmenu(-command => [\&recolour_image, $mw, $pgimage, \$function_menu]); $cmenu->addOptions(qw(grey rainbow heat aips)); # Place the label to the left of the menu button. $dlabel->pack(-side => 'left'); $dmenu->pack(-side => 'left'); $clabel->pack(-side => 'left'); $cmenu->pack(-side => 'left'); return $w; } # Implement the demo "redraw_slice" command. sub redraw_slice { if ($demodata{have_slice}) { display_slice(\%demodata, $demodata{va}, $demodata{vb}); } else { display_help(%demodata); } } # Implement the demo "recolour_image" command. This takes one of a set of # supported colour-table names and redisplays the current image with the # specified colour table. # # "aips" - AIPS tvfiddle colour table. # "grey" - A grey-scale colour table. # "heat" - The IRAF "heat" colour table. # "rainbow" - A red colour table. sub recolour_image { my $mw = shift; # Main window my $pgimage = shift; # The pgplot widget my $fn = shift; # Current displayed function my $name = shift; # The name of the desired colour table # If the colour table is found, install it if (exists($cmap{$name})) { pgslct($demodata{image_id}); pgctab(@{$cmap{$name}}, 1.0, 0.5); } else { warn "Unknown colour map name $name\n"; return; } # Redraw the current image if necessary. if ($pgimage->cget(-share)) { draw_image($mw, $$fn); } } # Create the area that contains the slice PGPLOT window. sub create_slice_area ($) { my $mw = shift; # Frame the workarea. my $w = $mw->Frame(-width => '11c', -height => '6c'); # Create the PGPLOT slice window. my $pgplot = $w->Pgplot(-name => 'slice', -share => 1, -width => '10c', -height => '5c', -maxcolors => 2, -bd => 2, -bg => 'black', -fg => 'white'); # Position the PGPLOT widget. $pgplot->pack(-side => 'left', -fill => 'both', -expand => 1); # Arrange for the plot to be redrawn whenever the widget is resized. $pgplot->bind('' => \&redraw_slice); return ($w, $pgplot); } sub initialize_demo { my %demo = (); $demo{image_id} = -1; $demo{slice_id} = -1; $demo{image} = []; $demo{slice} = []; $demo{fn} = undef; $demo{datamin} = undef; $demo{datamax} = undef; $demo{have_slice} = 0; $demo{monochrome} = 0; # Attempt to open the image and slice widgets. (($demo{image_id} = pgopen('image/ptk')) >= 0) || die "Unable to open pgplot image widget ($demo{image_id})"; (($demo{slice_id} = pgopen('slice/ptk')) >= 0) || die "Unable to open pgplot slice widget"; # Now initialize the 2D image array as a 1D array to be indexed in # as a FORTRAN array for (my $i=0; $iworld('x',$wx2), $pg->world('y',$wy2), $demodata); } # This is used as a pgplot image-widget cursor callback. It augments the # cursor in the image window with a line rubber-band anchored at the # selected cursor position and registers a new callback to receive both # the current coordinates and coordinates of the end of the slice when # selected. # Input: # pg pgplot widget # x,y X-window coordinates of the position that the user selected # with the cursor. # demodata demo hash sub start_slice ($$$%) { my ($pg, $x, $y, $demodata) = @_; # Convert from X coordinates to world coordinates. $x = $pgimage->world('x', $x); $y = $pgimage->world('y', $y); $pgimage->setcursor('line', $x, $y, 3); $pgimage->bind('' => [\&end_slice, $x->[0], $y->[0], Ev('x'), Ev('y'), $demodata]); } # Arm the image-widget cursor such that when the user next presses a # mouse button or key within the image window the start of a slice # will be selected. sub prepare_for_slice ($) { my $demodata = shift; $pgimage->setcursor('norm', 0.0, 0.0, 1); $pgimage->bind('' => [\&start_slice, Ev('x'), Ev('y'), $demodata]); } # Implement the demo "slice" command. This takes two pairs of image # world coordinates and plots a 1D representation of the currently # displayed function in the slice window. # # Input: # x1, x2, x3, x4 The two end points of the slice line. # demodata Demo hash sub slice ($$$$\%){ # Read the four coordinate values. my %va = (); # The coordinates of one end of the slice my %vb = (); # The coordinates of the other end of the slice $va{x} = shift; $va{y} = shift; $vb{x} = shift; $vb{y} = shift; my $demodata = shift; # Record the slice vertices so that the slice can be redrawn # when the widget is resized. $demodata->{va} = \%va; $demodata->{vb} = \%vb; $demodata->{have_slice} = 1; # Plot the new slice. display_slice($demodata, \%va, \%vb); } # Display a new slice in the slice window. # Input: # demodata Demo hash # va The vertex of one end of the slice line. # vb The vertex of the opposite end of the slice line. sub display_slice ($$$) { my ($demodata, $va, $vb) = @_; # Determine the slice pixel assignments. my $xa = $va->{x}; my $dx = ($vb->{x} - $va->{x}) / SLICE_SIZE; my $ya = $va->{y}; my $dy = ($vb->{y} - $va->{y}) / SLICE_SIZE; # Make sure that the slice has a finite length by setting a # minimum size of one pixel. my $min_delta =SCALE / SLICE_SIZE; if(abs($dx) < $min_delta && abs($dy) < $min_delta) { $dx = $min_delta; } # Construct the slice in demo->{slice} and keep a tally of the # range of slice values seen. my ($value, $smin, $smax); my $fn = $demodata->{fn}; for(my $i=0; $i{slice}[$i] = $value; if($i==0) { $smin = $smax = $value; } elsif($value < $smin) { $smin = $value; } elsif($value > $smax) { $smax = $value; } } # Determine the length of the slice. my $xlen = $dx * SLICE_SIZE; my $ylen = $dy * SLICE_SIZE; my $slice_length = sqrt($xlen * $xlen + $ylen * $ylen); # Determine the extra length to add to the Y axis to prevent the # slice plot hitting the top and bottom of the plot. my $ymargin = 0.05 * ($demodata->{datamax} - $demodata->{datamin}); # Set up the slice axes. pgslct($demodata->{slice_id}); pgask(0); pgpage(); pgbbuf(); pgsch(2.0); pgvstd(); pgswin(0.0, $slice_length, $demodata->{datamin} - $ymargin, $demodata->{datamax} + $ymargin); pgbox("BCNST", 0.0, 0, "BCNST", 0.0, 0); pglab("Radius", "Image value", "A 1D slice through the image"); # Draw the slice. for(my $i=0; $i{slice}[0]); } else { pgdraw($slice_length * $i / SLICE_SIZE, $demodata->{slice}[$i]); } } pgebuf(); } # Display a "Please wait" message in the slice window. sub display_busy (%) { my %demodata = @_; # Clear the slice plot and replace it with instructional text. pgslct($demodata{slice_id}); pgask(0); pgpage(); pgsch(3.5); pgsvp(0.0, 1.0, 0.0, 1.0); pgswin(0.0, 1.0, 0.0, 1.0); pgmtxt("T", -2.0, 0.5, 0.5, 'Please wait.'); } # Create an unmapped help dialog. # # Note that the dialog is not initially mapped. To display it temporarily # use the command {wm deiconify $w} and then when it is no longer required # call {wm withdraw $w}. sub create_help_dialog ($) { my ($mw) = @_; # Create the dialog container and tell the window-manager what to call # both it and its icon. my $w = $mw->Toplevel(-class => 'dialog'); $w->withdraw; $w->title('Usage information'); $w->iconname('Dialog'); # Create the top-half of the dialog and display display the usage message # in it. my $top = $w->Frame(-relief => 'raised', -bd => 1); my $msg = $top->Message(-width => '12c', -text => 'To see a slice'. ' through the displayed image, move the mouse'. ' into the image display window and use any'. ' mouse button to select the two end points'. ' of a line.'."\n\n". 'To display a different'. ' image select a new image function from the'. ' "Select a display function" option menu.'); $msg->pack(-side => 'left', -expand=> 1, -fill => 'both'); # Create the bottom half of the dialog and place a single OK button in # it. Arrange that pressing the OK button unmaps the dialog. my $bot = $w->Frame(-relief => 'raised', -bd => 1); my $ok = $bot->Button(-text => 'OK', -command => sub {$w->withdraw}); $ok->pack(-pady => '2m'); # Arrange for carriage-return to invoke the OK key. $w->bind('' => sub {$ok->invoke}); # Place the widgets in their assigned places top and bottom. $top->pack(-side => 'top', -fill => 'both', -expand => 1); $bot->pack(-side => 'top', -fill => 'both', -expand => 1); return($w); } # Create an unmapped save-image dialog. # Note that this dialog is not initially mapped. To display it # temporarily use the command {wm deiconify $w} and then when it is no # longer required call {wm withdraw $w}. sub create_save_dialog ($) { my $mw = shift; # Create the toplevel dialog window withdrawn. my $w = $mw->Toplevel(-class => 'dialog'); $w->withdraw; $w->title('Save image'); $w->iconname('Dialog'); # Create the top and bottom frames. my $top = $w->Frame(-relief => 'raised', -bd => 1); $top->pack(-side => 'top', -fill => 'both', -expand => 1); my $bot = $w->Frame(-relief => 'raised', -bd => 1); $bot->pack(-side => 'bottom', -fill => 'both', -expand => 1); # Create a label and an entry widget in the top frame. my $msg = $top->Message(-justify => 'left', -width => '8c', -anchor => 'w', -text => 'Enter a PGPLOT device string:'); my $entry = $top->Entry(-relief => 'sunken', -bd => 2, -width => 30); $msg->pack(-side => 'top', -anchor => 'w'); $entry->pack(-side => 'top', -anchor => 'w'); # Create three buttons in the bottom frame. my $ok = $bot->Button(-text => 'OK'); my $cancel = $bot->Button(-text => 'Cancel', -command => sub {$w->withdraw}); my $help = $bot->Button(-text => 'Help', -state => 'disabled'); $ok->pack(-side => 'left', -expand => 1, -pady => '2m', -padx => '2m'); $cancel->pack(-side => 'left', -expand => 1, -pady => '2m', -padx => '2m'); $help->pack(-side => 'left', -expand => 1, -pady => '2m', -padx => '2m'); # Arrange for carriage-return to invoke the OK key. $w->bind('' => [$ok => 'invoke']); $ok->configure(-command => sub {$w->withdraw; $mw->idletasks; save_image($entry)}); return $w; } sub save_image ($) { my $entry = shift; my $device = $entry->get(); # Open the new PGPLOT device. my $device_id = pgopen($device); # If the device was successfully opened, plot the current image # within it and close the device. if($device_id > 0) { display_image($device_id); pgclos(); } else { warn "cpgopen(\"$device\") failed.\n"; } }