From: connolly@convex.com (Dan Connolly)
Subject: mif2rtf.pl: here it is.
Summary: perl script to convert FrameMaker MIF files to MS Word RTF format
Date: Tue, 18 Aug 1992 20:27:33 GMT
Organization: Engineering, CONVEX Computer Corp., Richardson, Tx., USA
Keywords: MIF RTF perl Rich Text Format filter
X-Disclaimer: This message was written by a user at CONVEX Computer
              Corp. The opinions expressed are those of the user and
              not necessarily those of CONVEX.

I have had a number of requests for this beast over the last
year or so. I don't use the perl script any more -- the version
under development uses XLisp in stead.

But a lot of folks have had good luck with it. It converts
basic character and paragraph formatting. It does pretty
much what you'd expect for letters and memo's. Somebody
had good luck with a 90 page document. (Chalk one up
for perl! It scales well.)

I have one request: would somebody archive this thing on
an FTP server and let me know where you put it?

Anyway, here it is:

# This is a shell archive.  Remove anything before this line,
# then unpack it by saving it in a file and typing "sh file".
#
# Wrapped by pixel!connolly on Tue Aug 18 15:21:58 CDT 1992
# Contents:  mif2rtf.pl
 
echo x - mif2rtf.pl
sed 's/^@//' > "mif2rtf.pl" <<'@//E*O*F mif2rtf.pl//'
#!/usr/local/bin/perl
# $Id$
#
# USE
#
$Usage = "
$0 -d [mif_file]... >out.rtf
   -d               turn on debugging
   -info TAG GROUP  Put all text from TAG paragraphs into info group GROUP
";
#
# Code marked @@ needs fixing.
# Code marked @# is an improvisation
# Code marked HEURISTIC is not exact.
#
# COPYRIGHT
#
# Copyright 1992 by Convex Computer Corporation, Richardson, Texas.
#
#                        All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and its 
# documentation for any purpose and without fee is hereby granted, 
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in 
# supporting documentation, and that the name Convex not be
# used in advertising or publicity pertaining to distribution of the
# software without specific, written prior permission.  
# 
# CONVEX DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL
# CONVEX BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
# SOFTWARE.
#
# BUGS
#
# @#Footnote paragraph formats not right
# @#should convert page margins (frame BRect->margr etc.)
# @#No way to make TOC entries
# @#master pages
# @# Not all document, section, or table properties are converted.
# @#Hard problems: reference page items
#   Maybe use command line options for those.
# @# LineSpacing doesn't translate
#
# IDEAS
# absolute positioned object: frame brect etc.
#

#########
#
# main
#

local($Debug,
      # 
      # %TagGroup maps MIF paragraph tags to RTF info groups
      # %RTFInfo holds the text of the RTF info groups
      # so that after &convert_pages(),
      # $RTFInfo{$TagGroup{pgftag}} contains all text tagged pgftag
      # 
      # @@ idea: extend this to character tags
      # 
      %TagGroup
      );
@@ARGV = &parse_args(@ARGV);


local(%State,
      # 
      # %State maps MIF and RTF codes to their current settings
      # 
      %CharacterConversions
      );
&initialize_state();

local($Document,
      # $Document is the RTF code to get from the default
      # document format to what's in %State
      # NOTE: some MIF document attrs are RTF Section attrs
      # ASSUME: MIF document defaults are RTF defaults

      %Typeface, $Typeface,
      # %Typeface maps FFamilys to RTF font numbers

      %Tag, $Tag,
      # %Tag maps PgfTags to RTF style numbers

      %PgfCatalog, %FontCatalog,

      @BodyPage,
      %TextRect, %TextFlow,
      %AFrame,
      %Tbl
      );

$Document = "\\ftnbj "; # This is the default in Frame, but not in Word.

local($PageType, $TextRectID, $ID, $TblID,
      $XRefName, $VaraibleName);

&read_whole_file
     ('DPageSize', '$Document. = &change_dims($data, "paperw", "paperh")',
      'DStartPage', '$Document .= &change_attr("pgnstarts", $data)',
      'DPageNumStyle', '$Document .= &select_attr($data, "PageNumStyle",
				 	 "Arabic", "pgndec",
					 "UCRoman", "pgnucrm",
					 "LCRoman", "pgnlcrm",
					 "UCALpha", "pgnucltr",
					 "LCAlpha", "pgnlcltr")',
      'DTwoSides', '$Document .= &change_attr("facingp", 1)',
      'DFNoteRestart', '$Document .= &select_attr($data, "FNoteRestart",
					 "PerPage", "ftnrestart")',
      'DFNoteStartNum', '$Document .= &change_attr("fntstart", $data)',
      'DAutoChBars', '$Document .= &change_attr("revisions", 1)',
      
      'FFamily', '&intern(*Typeface, $data)',
      'PgfTag', '&intern(*Tag, $data)',
      
      'PgfCatalog', '&convert_paragraph_catalog($here)',
      'FontCatalog', '&convert_character_catalog($here)',
      
      'PageType', '$PageType = $data',
      'Page', 'push(@BodyPage, $here) if $PageType eq "BodyPage"',

      'TextRectID',
      '$TextRectID ? &debug("Extra TextRect: $data") : ($TextRectID = $data)',

      'TextFlow', '@TextRect{$TextRectID} = $here;
                   &debug("flow", %TextRect); $TextRectID = ""',

      'Frame', '$AFrame{&data_search($here, "ID", 1)} = $here',

      'TblID', '$TblID = $data',
      'Tbl', '$Tbl{$TblID} = $here',

      'XRefName', '$XRefName = &convert_string($data)',
      'XRefDef', '$Definition{"XRef", $XRefName} = $data',
      
      'VarialbeName', '$VariableName = &convert_string($data)',
      'VariableDef', '$Definition{"Variable", $VariableName} = $data'
      );

print STDOUT &rtf_begin_doc;
print STDOUT &rtf_font_table(%Typeface);
print STDOUT &rtf_color_table(('0 0 0 Black', # These are Frame's colors
			       '1 1 1 White',
			       '1 0 0 Red',
			       '0 1 0 Green',
			       '0 0 1 Blue',
			       '0 1 1 Cyan',
			       '1 0 1 Magenta',
			       '1 1 0 Yellow'));

print STDOUT $Document, "\n";

#&debug("body pages: ", @BodyPage);

local(@Document);

foreach $page (@BodyPage){
#    &debug("converting page:\n", &expand_mif_statement($page));
    push(@Document, &convert_frame($page));
}

if(!defined($StyleSheet) || $StyleSheet) {
    print STDOUT &rtf_style_sheet(%Tag);
}
print STDOUT &rtf_info(%RTFInfo);
print STDOUT @Document;
print STDOUT &rtf_end_doc;

print STDERR "\n"; # after all the .....'s

######

sub parse_args{
    # 
    # USE : @ARGV = &parse_args(@ARGV);
    # SETS: $Debug, %TagGroup
    #
    local(@files);

    while($_ = shift){
        if(/^-d/){
            $Debug = 'debug';
        }
	elsif(/^-info/){
	    $TagGroup{shift} = shift;
	}
	elsif(/^-nostyles/){
	    $StyleSheet = 0;
        }else{
            push(@files, $_);
        }
    }
    @files;
}

#
#
##########


##########
#
# Mif Parsing Routines
#
# I've finally found a perl representation of a mif file that I'm happy with.
# It's pretty memory intensive and not real zippy, but that's the nature
# of the beast with MIF.
# 
sub read_whole_file{
    local(%Callbacks) = @_;
    # global(@Parts);
    @Parts = (0); # put one element in it so there's no statement 0.

    1 while(&read_mif_statement());
}



sub read_mif_statement{
    # 
    # Returns an index in @Parts so that
    # $Parts[$index] is the statement type
    # and @Parts[$index+1, $index+2, ...] are the parts of the statement.
    # For compound statement, @Parts[$index, ...]  looks like
    # ('<', token, index1, index2, index3, ...)
    # where indexN are the substatements
    # 
    # for each statment, $Callbacks{$token} is evaluated
    #   with $token, $data, and $here set
    #

    local($_, $type, $token, $data, @parts, $here, $line);
    # global(@Parts);

    while(<>){
	$line = $.;
	$type = '<>', $token = $1, $data = $2, last
	    if /^\s*<(\w+)\s+(.*\S)?\s*>/;
	$type = '<', $token = $1, last
	    if /^\s*<(\w+)/;
	return 0 if /^\s*>/;
	$type = '=', $token = $1, $data = &read_mif_inset(), last
	    if /^=(\w+)/;
    }
    return 0 unless $type;

    local($callback);
    if($type eq '<>' || $type eq '='){
	$here = $#Parts+1;
	push (@Parts, $type, $token, $data);
	eval $callback if $callback = $Callbacks{$token};
	die "$@ $callback" if $@;
    }else{
	while($_ = &read_mif_statement()){
	    push(@parts, $_);
	}
	$here = $#Parts+1;
	push(@Parts, '<', $token, @parts);
	eval $callback if $callback = $Callbacks{$token};
        die "$@ $callback" if $@;
    }

    $Lines{$here} = $line; # for debugging purposes
    $here;
}

sub read_mif_inset{
    # Read until a line starts with "=EndInset"
    # and return all the lines concatenated
    local($inset);
    while(<>){
	$inset .= $_;
	last if /^=EndInset/;
    }
    $inset;
}


sub new_statement{
    local($s) =	$#Parts + 1;
    push(@Parts, @_);
    return $s;
}

sub type{
    $Parts[$_[0]];
}

sub token{
    $Parts[$_[0]+1];
}

sub data{
    $Parts[$_[0]+2];
}

sub parts{
    local($i) = @_;
    if(&type($_[0]) eq '<'){
	local($s) = $i+2;
	for(; $Parts[$s] > 0; $s++){
	}
	return @Parts[$i+2 .. $s-1];
    }else{
	return @Parts[$i+2]; #data
    }
}

sub do_statement{
    # global(@Parts);
    local($__s, %__callbacks) = @_;
    local($here, $__cb);

    foreach $here (&parts($__s)){
	local($token) = &token($here);
	local($data) = &data($here);

	eval $__cb if $__cb = $__callbacks{$token};
	warn "Error executing: $__cb \n-->$@" if $@;
	warn "IGNORED: $token line:", $Lines{$here}
	    if (&type($here) ne '<>') && !$__cb;

	&assert('$token =~ /\w+/');
    }
}


sub expand_mif_statement{
    # global(@Parts);
    local($i, $leader) = @_;
    local($_) = &type($i);

    if($_ eq '<>'){
	return (sprintf("%s<%s %s>\n", $leader, @Parts[$i+1, $i+2]));
    }elsif($_ eq '='){
	return (sprintf("=%s\n%s", @Parts[$i+1, $i+2]));
    }elsif($_ eq '<'){
	local(@lines);
	@lines = (sprintf("%s<%s\n", $leader, &token($i)));
	foreach (&parts($i)){
	    push (@lines, &expand_mif_statement($_, "  $leader"));
	}
	push(@lines, "$leader>\n");
	return @lines;
    }
}


sub data_search{
    #
    # use $tag = &data_search($para, 'PgfTag', 2)
    # to search 2 levels of the $para statement,
    # and return the data of the first PgfTag statement found.
    # 
    local($s, $t, $levels) = @_;
    foreach (&parts($s)){
	if(&type($_) eq '<'){
	    if($levels>1){
		$s = &data_search($_, $t, $levels-1);
		return $s if $s ne '';
	    }
	}else{
	    return &data($_) if &token($_) eq $t;
	}
    }
    return '';
}

sub token_search{
    #
    # use &token_search($compound_statement, "token", 3)
    # to search the next three levels of hairy_statement for
    # "token" statements.
    # 
    local($s, $t, $levels) = @_;

    local(@matches, $_);

    foreach (&parts($s)){
	push(@matches, $_) if &token($_) eq $t;
	push(@matches, &token_search($_, $t, $levels-1))
	    if &type($_) eq '<' && $levels>1;
    }
    return @matches;
}


#
#
########

############
#
# %State manipulation
#

sub change_attr{
    local($attr, $val) = @_;

#    &debug("change attr: $attr ($State{$attr}) -> [$val]");
    $State{$attr} = $val
	, return "\\$attr$val "
	    unless $State{$attr} eq $val;
    '';
}

sub change_dims{
    local($dims, @attrs) = @_;
    local($_, $r);

    foreach (&rtf_dimensions($dims)){
	local($attr) = shift(@attrs);
	last unless $attr;
	$attr =~ s-/(\d+)-- && ($_ = int($_/$1)); #HACK
	$r .= &change_attr($attr, $_);
    }
    $r;
}

sub select_attr{
    local($key, $s, %attrs) = @_;

    if($attrs{$key}){
#	&debug("select attr: '$s=$key' from ", join(",", %attrs)),
	$State{$s} = $attrs{$key}, return "\\$attrs{$key} "
	    unless $State{$s} eq $attrs{$key};
    }
    $State{$s} = '';
}

#############
#
# conversions
#

sub convert_paragraph_catalog{
    local($s) = @_;
    #global(%State, %PgfCatalog);

    local($tag);
    &do_statement($s, 'Pgf', '
		  $tag = &convert_string(&data_search($here, "PgfTag"));
		  $PgfCatalog{"Statement", $tag} = $here;'
		  );
}

sub convert_character_catalog{
    local($s) = @_;
    #global(%State, %FontCatalog);

    local($tag);
    &do_statement($s,'Font', '
		  $tag = &convert_string(&data_search($here, "FTag"));
		  $FontCatalog{"Statement", $tag} = $here;'
		  );
}

sub convert_pgf_format{
    local($s) = @_;
    #
    # Returns RTF to change from what's in %State to what's in $s
    # EXCEPT TABS! use &current_tabs() to get them.
    # 

    local($rtf, $_);

    local($lindent) = &data_search($s, 'PgfLIndent');
    local($li);
    if($lindent gt ''){
	($li) = &rtf_dimensions($lindent);
	$State{'LIndent'}=$lindent;
    }else{
	($li) = &rtf_dimensions($State{'LIndent'});
    }

    local($findent) = &data_search($s, 'PgfFIndent');
    local($fi);
    if($findent gt ''){
	($fi) = &rtf_dimensions($findent);
	$State{'FIndent'}=$findent;
    }else{
	($fi) = &rtf_dimensions($State{'FIndent'});
    }
    $rtf .= &change_attr("fi", $fi - $li);
    $rtf .= &change_attr("li", $li);
    &debug("findent: $findent fi: $fi lindent: $findent li: $li");

    &do_statement
	($s,
	 'PgfNumberFont', '$State{"NumberFont"} = &convert_string($data)',
	 'PgfNumFormat', '$State{"NumFormat"} = &convert_string($data)',
	 'PgfNextTag',
	 '$rtf .= &change_attr("snext", &intern(*Tag, $data))',
	 'PgfNumTabs', 'undef($State{"TabStops"})',
	 'TabStop', '&TabStop($here)',
	 'PgfAlignment', '$rtf .= &select_attr($data, "Alignment",
					"Left", "ql",
					"Center", "qc",
					"Right", "qr",
					"LeftRight", "qj")',
	 # RTF & MIF are different about tabs and first indents
	 'PgfRIndent', '$rtf .= &change_dims($data, "ri")',

	 'PgfTopSeparator',
	 '$rtf .= &change_attr("brdrt", 1) if $data ne "`\'"', #@#
	 'PgfBotSeparator',
	 '$rtf .= &change_attr("brdrb", 1) if $data ne "`\'"', #@#

	 'PgfPlacement', '$rtf .= &select_attr($data, "Placement",
					"ColumnTop", "pagebb", #@#
					"PageTop", "pagebb",
					"LPageTop", "pagebb", #@#
					"RPageTop", "pagebb")', #@#
	 'PgfSpBefore', '$rtf .= &change_dims($data, "sb")',
	 'PgfSpAfter', '$rtf .= &change_dims($data, "sa")',
	 
	 #@# withprev
	 'PgfWithNext', '$rtf .= &select_attr($data, "WithNext",
				      "Yes", "keepn", "No", "keepn0")',
	 'PgfBlockSize', '$rtf .= &change_attr("keep", 1) unless $data <2',
	 'PgfLeading', '$rtf .= &change_dims($data, "sl")',
	 'PgfFont', '$rtf .= &convert_char_format($here)'
	 );

#    &debug("converted $State{'PgfTag'} -> $rtf");
    $rtf;
}

sub TabStop{
    local($s) = @_;

    #@# tab alignment ignored!
    $State{'TabStops'} .= " " . &data_search($s, 'TSX');
}

sub convert_char_format{
    local($rtf);
    &do_statement
	($_[0],
	 'FFamily', '$State{"Family"} = &convert_string($data);
                     $rtf .= &change_attr("f", &intern(*Typeface, $data))',
#@#	 'FVar', 'warn "IGNORED: Variation $data" unless $data =~ /regular/i',
	 'FWeight',  '$rtf .= &select_attr(&convert_string($data),"Weight",
				  "Bold", "b", "Regular", "b0")',
	 'FAngle', '$rtf .= &select_attr(&convert_string($data), "Angle",
				 "Italic", "i", "Oblique", "i2",
				 "Regular", "i0")',
	 'FSize', '$rtf .= &change_dims($data, "fs/10")',
	 'FUnderline', '$rtf .= &select_attr($data, "Underline",
				     "Yes", "ul", "No", "ulnone")',
	 'FStrike',  '$rtf .= &select_attr($data, "Strike",
				  "Yes", "strike", "No", "strike0")',
	 'FSupScript',  '$rtf .= &select_attr($data, "SupScript",
				  "Yes", "up6", "No", "up0")',
	 'FSubScript',  '$rtf .= &select_attr($data, "SubScript",
				  "Yes", "dn6", "No", "dn0")',
	 'FChangeBar',  '$rtf .= &select_attr($data, "ChangeBar",
				  "Yes", "revised", "No", "revised0")',
	 'FOutline',  '$rtf .= &select_attr($data, "Outline",
				  "Yes", "outl", "No", "outl0")',
	 'FShadow',  '$rtf .= &select_attr($data, "Shadow",
				  "Yes", "shad", "No", "shad0")',
	 'FSeparation',  '$rtf .= &change_attr("cf", $data)'
	 );
#    &debug("char format: $rtf");
    $rtf;
}

########

sub convert_frame{
    local($s) = @_;
#@@ local(@BRect) = @BRect;
    local(@ret);

    &debug("convert frame: $s");
    &do_statement
	($s,
	 'TextRect',     'push(@ret, &convert_textrect($here))',
	 'Frame',        'push(@ret, &convert_frame($here))',
	 'ImportObject', 'push(@ret, &convert_picture($here))',
	 'BRect', 'push(@ret, &change_dims($data, "posx", "posy", "absw"))',
	 'FrameType', 'push(@ret, &select_attr($data, "FrameType",
				    "Inline", "posyil",
				    "Top", "posyt",
				    "Bottom", "posyb",
				    "Left", "posxl",
				    "Right", "posxr",
				    "Near", "posxi",
				    "Far", "posxo"))'
	 );
    @ret;
}

sub convert_textrect{
    local($tr) = @_;
    local(@ret);

    &do_statement
	($tr,
	 'ID',
	 'local($_) = $TextRect{$data};
	 $_ && !$TextFlow{$_}++ ? push(@ret, &convert_flow($_))
	 : &debug("no flow or repeated flow:$data line: ",$Lines{$_});'
	 );
    @ret;
}

#
# this is one alternative...
#
sub old_convert_textrect{
    local($tr) = @_;
    local($id, @ret);

    &debug("convert textrect $tr");
    for($id = &data_search($tr, 'ID', 1); $id;
	$id = &data_search($tr, 'TRNext', 1)){
	local($s) = $TextRect{$id};
	last if $TextRect[$s]++;
	&debug("TextRectID = $id");
	push(@ret, &convert_flow($s));
    }
    @ret;
}

sub convert_table{
    local($s) = @_;
    local(@r);
    local($rows) = 'push(@r, &convert_rows($here))';

    &do_statement
	($s,
	 'TblTitleContent', 'push(@r, &convert_flow($here))'
#@@ for AVS, we need the table title content, but we captured
#   all the rows as bitmaps.
#@@	 'TblH', $rows,
#@@	 'TblBody', $rows,
#@@	 'TblF', $rows
	 );

    @r;
}

sub convert_rows{
    local($s) = @_;
    local(@r);

    &do_statement
	($s,
	 'Row', 'push(@r, "\\\\trowd ", &convert_cells($here), "\\\\row\n")'
	 );
    @r;
}

sub convert_cells{
    local($s) = @_;
    local(@r);

    &do_statement
	($s,
	 'Cell', 'push(@r, &convert_cell($here))'
	 );
    @r;
}

sub convert_cell{
    local($s) = @_;
    local(@r);

    #@@ cell formatting!
    &do_statement
	($s,
	 'CellContent',
	 'push(@r, &convert_flow($here, "\\\\intbl "), "\\\\cell")'
	 );
    @r;
}

sub convert_picture{
    local($s) = @_;

    local(@r, $color, $unixpath, $dipath, $epsi, $image, $brect);

    &do_statement
	($s,
	 'Separation', '$color = $data',
	 'ImportObFile', '$unixpath = $data',
	 'ImportObFileDI', '$dipath = $data',
	 'EPSI', '$epsi = $data',
	 'FrameImage', 'push(@r, "\\\\frameimage\n", $data)',
	 'BRect', '$brect = $data'
	 );

    &debug("picture: color($color) file($unixpath,$dipath) brect($brect)");

    if($brect){
	local($l, $t, $h, $w) = &rtf_dimensions($brect);
	unshift(@r, "\\picwGoal$w\\pichGoal$h\n");
    }

    undef($unixpath) if $unixpath =~ /internal inset/;

    local($path);
    if($dipath){
	local($_) = &convert_string($dipath);
	if(/<U>([^<]+)/){
	    $path = $1;
	}else{
	    if($unixpath){
		$path = &convert_string($unixpath);
	    }else { warn "no path in: $dipath"; }
	}
    }

    if($path){
	push(@r, "\\xwdfile $path");
    }
    if($epsi){
	$epsi =~ s/.*&%v\s*&//;
	$epsi =~ s/\n&//g;
	push(@r, "\\epsi\n", $epsi);
    }

    @r = ("\\qc{\\pict\n", @r, "}\\par\n"); # put it in a centered paragraph @#
    @r = ("{\\cf$color", @r, "}") if $color;
    @r;
}

sub convert_flow{
    local($s, $Container) = @_;
    local(@r, %Notes);

    &do_statement
	($s,
	 'Notes', '&save_notes($here)',
	 'Para', 'push(@r, &convert_paragraph($here))'
	 );
    @r;
}

sub save_notes{
    local($s) = @_;
    local($_, $n);

    foreach (&parts($s)){
	$n = &data_search($_, 'ID', 1);
#	&debug("note: $n is:", &expand_mif_statement($_));
	$Notes{$n} = $_;
    }
}


sub convert_paragraph{
    local($s) = @_;
    local(@fmt, @lines, @pre, @post, $HyperGroup);
    # global($PgfTag);

    &do_statement($s,
		  'PgfTag',
		  'push(@fmt, &change_style(&convert_string($data)))',
		  'Pgf',
		  'push(@fmt, &convert_pgf_format($here))',
		  'PgfNumString',
		  'push(@lines, &convert_numstring($data))',
		  'ParaLine',
		  'push(@lines, &convert_paraline($here, *pre, *post), "\n")'
		  );

    &debug("convert_paragraph: ",$State{'PgfTag'});
    print STDERR '.'; #@# status

    push(@lines, $HyperGroup), undef($HyperGroup) if $HyperGroup;
    
    local($group);
    $RTFInfo{$group} .= join('', @lines)."\\par\n" #@#lines is arbitrary
	if $group = $TagGroup{$State{'PgfTag'}};
    &debug(%TagGroup);
    &debug("tag: ", $State{'PgfTag'}, " group: ", $group);

    # $Container is, e.g. '\intbl ' (for tables)
    return(@pre, @fmt, $Container, &current_tabs(),
	   @lines, "\\par ", @post, "\n");
}

sub current_tabs{
    local($rtf, $_);
    
    local($li) = &rtf_dimensions($State{'LIndent'});

    foreach (&rtf_dimensions($State{'TabStops'})){
	$_ -= $li; #@# MIF to RTF mindset
	$rtf .= "\\tx$_ ";
    }
    $rtf;
}

sub convert_numstring{
    local($string) = @_;
    local(%State) = %State; # don't clobber font
    local($numfont) = $State{'NumberFont'};
    local($font) = $numfont ? &change_char_style($numfont) : '';
    local($form) = $State{'NumFormat'};

    $string = &convert_string($string, $State{'Family'} ne 'Symbol');

    &debug("numstring($string) [font($numfont): $font form: $form]");
    local($tabs) = &current_tabs();
    "{\\field{\\fldinst PgfNumFormat $form}{\\fldrslt $font$tabs$string}}";
}

sub change_style{
    local($tag) = @_;    

    local($style, $_);

    if($PgfCatalog{"Style", $tag}){
	&set_paragraph_format($tag);
	$style = $PgfCatalog{"Style", $tag};
    }else{
	local($pgf_fmt) = $PgfCatalog{'Statement', $tag};
	if($pgf_fmt){
	    &reset_paragraph_format();
	    &reset_character_format();
	    $style = &convert_pgf_format($pgf_fmt);
	    $PgfCatalog{"Style", $tag} = $style;
	    &save_paragraph_format($tag);
	}else{
	    warn "No catalog entry for '$tag'";
	}
    }

    $State{'PgfTag'} = $tag;

    join('', "\\pard\\plain\\s", &intern(*Tag, $tag), " ", $style);
}

sub change_char_style{
    local($tag) = @_;    
    local($style, $_);

    &reset_character_format();
    $State{'FTag'} = $tag;

    local($s) = $FontCatalog{'Statement', $tag};
    $s ? "\\plain" . &convert_char_format($s)
	: '';
}

sub convert_paraline{
    local($s, *pre, *post) = @_;
    #@# empty_hyper gets around the: <marker><font>hot-text<font> bug.
    local(@text, $empty_hyper);

    &do_statement
	($s,
	 'String',
	 'push(@text, &convert_string($data, $State{"Family"} ne "Symbol"));
          $empty_hyper=0;',
	 'Char',
	 'push(@text, &convert_character($data))',

	 'ATbl',
	 'local(%State) = %State;
          local(@tbl) = &convert_table($Tbl{$data});
          $State{"TblPlacement"} =~ /Top|Left|Right|Near|Far/
            ? push(@pre, "{", @tbl, "}") : push(@post, "{", @tbl, "}")',

	 'AFrame',
	 'local(%State) = %State;
          &debug("converting AFrame $data");
          local(@af) = &convert_frame($AFrame{$data});
          $State{"FrameType"} =~ /Top|Left|Right|Near|Far/
            ? push(@pre, "{", @af, "}") : push(@post, "{", @af, "}")',

	 'FNote',
	 'push(@text, &convert_footnote($Notes{$data}))',

	 'XRefEnd',
	 'push(@text, &convert_xref_end($data))',

	 #@@ there could be a problem with tagged fonts here:
	 # convert_char_format ignores tags. Catalog lookup occurs
	 # in its caller (e.g. convert_character_catalog).
	 # And this is its caller.
	 'Font',
	 'push(@text, $HyperGroup), undef($HyperGroup)
	    if $HyperGroup && !$empty_hyper;
	  push(@text, &convert_char_format($here))',

	 'Marker',
	 'push(@text, &convert_marker($here)); $empty_hyper=1',

	 'Variable',
	 'push(@text, &convert_definition("Variable",
				       &data_search("VariableName", $here)))',
	 'XRef',
	 'push(@text, &convert_xref($here))'
	 );
    @text;
}

sub convert_string{
# Change a MIF String datum (without newlines) to a rtf string
  local($_, $do_hex) = @_;

  # put backslashes out of band
  s/\\\\/\n\n/g;

  # undo MIFisms
  s/^`//;
  s/'.*//;
  s/\\q/'/g;
  s/\\Q/`/g;
  s/\\</</g;
  s/\\>/>/g;

  # convert hex stuff
  $do_hex ? s/\\x(\w\w) /$FrameCode[hex($1)]/ge :
      s/\\x(\w\w) /pack('C', hex($1))/ge;

  # protect RTFisms
  s/\{/\n\{/;
  s/\}/\n\}/;
  s/\\t/\t/;
  s/\\/\n\n/;
  warn "@@unexpected backslash: $_" if /\\/;

  # change backslashes back
  s/\n/\\/g;
  $_;
}

sub convert_character{
    local($name) = @_;
    $CharacterConversions{$name};
}

sub convert_footnote{
    return ("\\chftn{\\footnote\n", &convert_flow($_[0]), "}");
}

sub convert_xref{
    local($s) = @_;

    $XRefSrcText = &convert_string(&data_search($s, 'XRefSrcText'));
    $XRefSrcFile = &convert_string(&data_search($s, 'XRefSrcFile'));

    return "{\\field{\\fldrslt ";
}

sub convert_xref_end{
    return "}{\\fldinst XRefSrcFile=$XRefSrcFile XRefSrcText=$XRefSrcText " .
	"XRefFormat=" . $Definition{'XRef', $XRefName} . "}}";
}

sub convert_definition{
    local($type, $name) = @_;
    #@# converts Frame variables literally to RTF
    return &convert_string($Definition{$type, $name});
}

sub convert_other{
    local($token, $text, $invisible) = @_;
    local($fldpriv) = $invisible ? '\fldpriv\v' : '';
    "{\\field$fldpriv{\\fldinst $token}{\\fldrslt $text}}";
}

sub convert_marker{
    local($type) = &data_search($_[0], 'MType', 1);
    local($text) = &convert_string(&data_search($_[0], 'MText', 1));

    if($type == 0 || $type == 1){
	#@@ these actually occur in pairs.
	return &convert_other('FrameHeaderFooter ' . ($type+1), $text, 1); #@#
    }elsif($type == 2){
	return "{\\v{\\xe $text}}"; #@# text needs to be interpreted
    }elsif($type == 3){
	$RTFInfo{'comment'} .= "$text\n";
    }elsif($type == 4){
	$RTFInfo{'subject'} .= "$text\n";
    }elsif($type == 5){
	$RTFInfo{'author'} .= "$text\n";
    }elsif($type == 6){
	$RTFInfo{'keywords'} .= "$text\n"; #@# glossary, really
    }elsif($type == 7){
	return '{\|}'; #@# equation marker->formula character
    }elsif($type == 8){
	$HyperGroup = "}{\\fldinst FrameHypertext $text}}";
	return "{\\field{\\fldrslt ";
    }elsif($type == 9){
	return &convert_other('FrameXRefMarker ', $text, 1); #@#
    }
    return &convert_other('FrameMarker ' . ($type+1), $text, 1); #@#
}

#
# Output routines
#

sub debug{
    print STDERR @_, " @$.\n" if $Debug;
}

##############
#
#
sub initialize_state{
    @FrameCode = 
	('',      '',      '',      '',      '\-',    '',      '\_',    '',
	 '\tab',  '\line', "\266",  "\247",  '',      '',      '',      '',
	 " ",     "\240",  " ",     " ",     " ",     "\255",  '',      '',
	 '',      '',      '',      '',      '',      '',      '',      '',
	 ' ',     '!',     '"',     '#',     "\$",    '%',     '&',     "\'",
	 '(',     ')',     '*',     '+',     ',',     '-',     '.',     '/',
	 '0',     '1',     '2',     '3',     '4',     '5',     '6',     '7',
	 '8',     '9',     ':',     ';',     '<',     '=',     '>',     '?',
	 '@',     'A',     'B',     'C',     'D',     'E',     'F',     'G',
	 'H',     'I',     'J',     'K',     'L',     'M',     'N',     'O',
	 'P',     'Q',     'R',     'S',     'T',     'U',     'V',     'W', 
	 'X',     'Y',     'Z',     '[',     "\\",    ']',     '^',     '_',
	 "\`",    'a',     'b',     'c',     'd',     'e',     'f',     'g',
	 'h',     'i',     'j',     'k',     'l',     'm',     'n',     'o',
	 'p',     'q',     'r',     's',     't',     'u',     'v',     'w',
	 'x',     'y',     'z',     '{',     '|',     '}',     '~',     '',
# 0x80
	 "\304",  "\305",  "\307",  "\311",  "\321",  "\326",  "\334",  "\341",
# 0x88
	 "\340",  "\342",  "\344",  "\343",  "\345",  "\347",  "\351",  "\350",
# 0x90
	 "\352",  "\353",  "\355",  "\354",  "\356",  "\357",  "\361",  "\363",
# 0x98
	 "\362",  "\364",  "\366",  "\365",  "\372",  "\371",  "\373",  "\374",
# 0xa0
	 "**",    "\260",  "\242",  "\243",  "\247",  "\267",  "\266",  "\337",
# 0xa8
	 "\256",  "\251",  "(tm)",  "\264",  "\250",  "",      "\306",  "\330",
# 0xb0
	 "",      "",      "",      "",      "\225",  "",      "",      "",
# 0xb8
	 "",      "",      "",      "\252",  "\272",  "",      "\346",  "\370",
# 0xc0
	 "\277",  "\241",  "\254",  "",      "f",     "",      "",      "\253",
# 0xc8
	 "\273",  "...",   "",      "\300",  "\303",  "\325",  "OE",    "oe",
# 0xd0
	 "\255",  "--",    "``",    "''",    "`",     "'",     "",      "",
# 0xd8
	 "\377",  "Y",     "/",     "\244",  "<",     ">",     "fi",    "fl",
# 0xe0
	 "***",   "\267",  ",",     ",,",    "%.",    "\302",  "\312",  "\301",
# 0xe8
	 "\313",  "\310",  "\315",  "\316",  "\317",  "\314",  "\323",  "\324",
# 0xf0
	 "",      "\322",  "\332",  "\333",  "\331",  "i",     "^",     "~",
# 0xf8
	 "\256",  "\257",  ".",     "\260",  "\270",  "''",    ",");

    %CharacterConversions =
	('Tab', '\tab ',
	 'HardSpace', '\~ ',
	 'SoftHypen', '\_ ', #@#
	 'DiscHypen', '\- ',
	 'Cent', "\242", # from ISO8859-1
	 'Pound', "\243",
	 'Yen', "\245",
	 'EnDash', "\255",
	 'EmDash', '-', #@#
	 'Dagger', '**', #@#
	 'DoubleDagger', '***', #@#
	 'Bullet', "\267",
	 'HardReturn', '\line ',
	 'EndOfPara', "\266",
	 'EndOfFlow', "\247",
	 'NumberSpace', ' ', #@#
	 'ThinSpace', ' ', #@#
	 'EnSpace', ' ',
	 'EmSpace', ' '
	 );

    local(%doc_defaults) =
	('paperw', 12240,
	 'paperh', 15840,
	 'margl', 1800,
	 'margr', 1800,
	 'margt', 1440,
	 'margb', 1440,
	 'facingp', '',
	 'TwoSides', 'No',
	 'gutter', 0,
	 'deftab', 720,
	 'widowctrl', '',
	 'hyphhotz', '',
	 'fntsep', '',
	 'ftnsepc', '',
	 'ftncn', '',
	 'endnotes', 1,
	 'enddoc', 0,
	 'ftntj', 0,
	 'ftnbj', 1,
	 'ftnstart', 1,
	 'pgnstart', 1,
	 'linestart', 1,
	 'landscape', 0,
	 'fracwidth', 0,
	 'nextfile', '',
	 'template', '',
	 'makeback', '',
	 'defformat', '',
	 'revisions', '',
	 'margmirror', '',
	 'revprop', 3,
	 'revbar', 3);

    local(%para_defaults) =
	('snext', '',
	 'sbasedon', '',
	 'pard', '',
	 's', '',
	 'ql', '',
	 'qr', '',
	 'qj', '',
	 'qc', '',
	 'Alignment', 'ql',
	 'LIndent', '0',
	 'FIndent', '',
	 'NumberFont', '',
	 'fi', 0,
	 'li', 0,
	 'ri', 0,
	 'sb', 0,
	 'sa', 0,
	 'sl', '',
	 'intbl', '',
	 'keep', '',
	 'keepn', '',
	 'WithNext', 'keep0',
	 'sbys', '',
	 'pagebb', '',
	 'Placement', 'Anywhere',
	 'noline', '',
	 'TabStops', '',
	 'tx', '',
	 'tqr', '',
	 'tqc', '',
	 'tqdec', '',
	 'tb', '',
	 'brdrt', '',
	 'brdrl', '',
	 'brdrr', '',
	 'box', '',
	 'brdrs', '',
	 'brdrth', '',
	 'brdrsh', '',
	 'brdrdb', '',
	 'brdrdot', '',
	 'brdrhair', '',
	 'brsp', '',
	 'tldot', 1,
	 'tlhyph', '',
	 'tlul', '',
	 'tlth', '');

    local(%char_defaults) =
	('f', '',
	 'Family', '',
	 'b', '',
	 'Weight', 'b0',
	 'i', '',
	 'Angle', 'i0',
	 'strike', '',
	 'Strike', 'strike0',
	 'outl', '',
	 'Outline', 'outl0',
	 'shad', '',
	 'Shadow', 'shad0',
	 'scaps', '',
	 'caps', '',
	 'v', '',
	 'fn', '',
	 'fs', '24',
	 'expnd', 0,
	 'ul', '',
	 'ulw', '',
	 'uld', '',
	 'uldb', '',
	 'ulnone', '',
	 'Underline', 'ulnone',
	 'up', '',
	 'SupScript', 'up0',
	 'dn', '',
	 'SubScript', 'dn0',
	 'revised', '',
	 'ChangeBar', 'revised0',
	 'cf', 0
	 );

    local($_);
    @DocumentAttrs = keys(%doc_defaults);
    foreach (@DocumentAttrs){
	push(@DocumentDefaults, $doc_defaults{$_});
    }
    @State{@DocumentAttrs} = @DocumentDefaults;

    @ParagraphAttrs = keys(%para_defaults);
    foreach (@ParagraphAttrs){
	push(@ParagraphDefaults, $para_defaults{$_});
    }
    @State{@ParagraphAttrs} = @ParagraphDefaults;

    @CharacterAttrs = keys(%char_defaults);
    foreach (@CharacterAttrs){
	push(@CharacterDefaults, $char_defaults{$_});
    }
    @State{@CharacterAttrs} = @CharacterDefaults;
}

sub reset_paragraph_format{
    @State{@ParagraphAttrs} = @ParagraphDefaults;
    '';
}

sub set_paragraph_format{
    local($tag) = @_;
    local($_);
    &reset_paragraph_format();
    &reset_character_format();
    grep($State{$_} = $PgfCatalog{$_, $tag},
	 @ParagraphAttrs, @CharacterAttrs);
}

sub save_paragraph_format{
    local($tag) = @_;
    local($_);
    grep($PgfCatalog{$_, $tag} = $State{$_},
	 @ParagraphAttrs, @CharacterAttrs);
}

sub reset_character_format{
    @State{@CharacterAttrs} = @CharacterDefaults;
    '';
}
    
###############
#
#

sub rtf_begin_doc{
    return "{\\rtf1\\ansi\n";
}

sub rtf_end_doc{
    return "}\n";
}

sub rtf_info{
    local(%groups) = @_;
    local(@r, $_);

    push(@r, "{\\info\n");
    foreach (keys %groups){
	push(@r, "{\\$_ ", $groups{$_}, "}\n");
    }
    push(@r, "}\n");
    return @r;
}

sub intern{
    local(*arr, $_) = @_;
    s/^\`//;
    s/\'.*//;
    $arr{$_} || ($arr{$_} = ++$arr);
#    $_ = $arr{$_} || ($arr{$_} = ++$arr);
#    &debug("intern: $_[1] -> $_");
#    $_;
}

sub rtf_font_table{
    #
    # HEURISTIC: uses \fnil for unrecognized fonts.
    # recognizes all FrameMaker 2.1 fonts
    #
    local(%fonts) = @_;
    local(@r);

    local($n, $family, $_);
    foreach (keys %fonts){
	$family = 'nil';
	$n = $fonts{$_};
	$family = 'roman' if /serif/ || /times/i || /palatino/i
	    || /bookman/i || /newcenturysch/;
	$family = 'swiss' if /sans/ || /helvetica/i || /avantgarde/i;
	$family = 'modern' if /courier/i;
	$family = 'script' if /cursive/i;
	$family = 'decor' if /zapfchancery/i;
	$family = 'tech' if /symbol/i;

	push(@r, "{\\f$n\\f$family $_;}\n");
    }
    return("{\\fonttbl\n", @r, "}\n");
}


sub rtf_style_sheet{
    local(%styles) = @_;
    local(@r);

    local($n, $style, $_);
    foreach (keys %styles){
	$n = &intern(*Tag, $_);
	#@# all styles mentioned in the document will appear in the
	# stylesheet, but only styles used in the document will be defined!
	$style = $PgfCatalog{'Style', $_};
	push(@r, "{\\s$n $style$_;}\n");
    }
    return("{\\stylesheet\n", @r, "}\n");
}


sub rtf_color_table{
    local(@colors) = @_;

    local($red, $green, $blue, $_);
    foreach (@colors){
	($red, $green, $blue) = split(/\s+/, $_);
	$red = 255 * $red;
	$green = 255 * $green;
	$blue = 255 * $blue;
	push(@r, "\\red$red\\green$green\\blue$blue;\n");
    }
    return("{\\colortbl\n", @r, "}\n");
}

sub rtf_dimensions{
  local($_, $twips) = @_;
  local(@trect);
#
# convert all dimensions to twips
# didot@@ cicero@@
#
  while($_){
    if( s/\s*(-?\d+(\.\d*)?)\s*//){
      $twips = $1 * 1440;
    }else{
      warn "@@Bad dimensions: $_\n";
      return ();
    }
    s/"//;
    s/in//;
    s/pt// && ($twips = $twips/72);
    s-cm-- && ($twips = $twips/2.54);
    s-mm-- && ($twips = $twips/25.4);
    push (@trect, int($twips));
  }
  @trect;
}


##############
#
# tchrists' assert stuff
#

sub assert {
    &panic("ASSERTION BOTCHED: $_[0]",$@) unless eval $_[0];
} 

sub panic {

    select(STDERR);


    print "\npanic: @_";

    exit 1 if $] <= 4.003;  # caller broken

    # stack traceback stolen from perl debugger

    local($i,$_);
    local($p,$f,$l,$s,$h,$a,@a,@sub);
    for ($i = 1; ($p,$f,$l,$s,$h,$w) = caller($i); $i++) {
	@a = @DB'args;
	for (@a) {
	    if (/^StB\000/ && length($_) == length($_main{'_main'})) {
		$_ = sprintf("%s",$_);
	    }
	    else {
		s/'/\\'/g;
		s/([^\0]*)/'$1'/ unless /^-?[\d.]+$/;
		s/([\200-\377])/sprintf("M-%c",ord($1)&0177)/eg;
		s/([\0-\37\177])/sprintf("^%c",ord($1)^64)/eg;
	    }
	}
	$w = $w ? '@ = ' : '$ = ';
	$a = $h ? '(' . join(', ', @a) . ')' : '';
	push(@sub, "$w&$s$a from file $f line $l\n");
	last if $signal;
    }
    for ($i=0; $i <= $#sub; $i++) {
	last if $signal;
	print $sub[$i];
    }
    kill 'TERM', -$Start_Pid;
    exit 1;
} 
@//E*O*F mif2rtf.pl//
chmod u=rwx,g=rx,o=rx mif2rtf.pl
 
exit 0