% File : formatter.pl % RCS : $Id: formatter.pl,v 1.14 2002/10/01 14:25:38 schachte Exp $ % Author : Peter Schachte and Toby Ord % Purpose : Format text for postscript or text file output % % This program implements a simple text formatter. The format of the output % is specified by a "box term." Box terms specify a rectangular area, and % can be combined to create larger areas. Box terms are recursively defined, % each having one of the the following forms: % % empty a 0 x 0 rectangle % text(Atom) just the text of Atom, 1 line high % font(Box,Attribs) Box, constructed with given font attributes % h(Boxes,Sep) Boxes arranged horizontally separated by Sep % v(Boxes,Sep) Boxes arranged vertically separated by Sep % horv(Boxes,Sep,Ind) Like h(Boxes,Sep) if it fits, otherwise % like v(Boxes,empty), except that each box but % the first is indented by Ind % fill(Boxes,Sep,Wid) Rectangle of Boxes layed out like a paragraph % with ragged right margin the width of Wid. % Boxes on same line are separated by Sep. % width(Box) A box 0 lines high the width of Box % height(Box) A 0-width box the height of Box % % The overall strategy for producing the output is to first construct a plan % of the output, and then execute (interpret) the plan to produce the actual % output). Naturally, the output is completely different for text and % postscript, but planning is largely independent of kind of output. The % only difference lies in how to determine the width and height of text. :- ensure_loaded(font). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Public interface and main support % % % format_text(Box, Filename) % format_postscript(Box, Filename) % Produce text or postscript output for the given Box in the specified file format_text(Box, FileName) :- plan_and_render(Box, text, FileName). format_postscript(Box, FileName) :- plan_and_render(Box, ps, FileName). % For easier testing, put output on terminal format_text(Box) :- format_text(Box, ''). format_postscript(Box) :- format_postscript(Box, ''). % plan_and_render(Box, Mode, File) % Plan Box for Mode (text or ps) and produce output in File. plan_and_render(Box, Mode, File) :- ( plan_box(Box, Mode, 'Courier', Plan), plan_size(Plan, Width, _), width_of_display(Mode, MaxWidth), Width =< MaxWidth -> % Commit to first usable plan plan_lines(Plan, Lines), ( File \== '' -> open(File, write, Stream), set_output(Stream), render(Mode, Lines), close(Stream) ; render(Mode, Lines) ) ). render(text, Lines) :- render_as_text(Lines). render(ps, Lines) :- render_as_ps(Lines). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Planning % % % % Recursively traverse the box, planning it from the bottom up. % Mode is either 'text' or 'ps' and defines the drawing mode. % Font is an atom naming the default font to use for text. % plan_box(Box, Mode, Font, Plan) % Plan is a plan to produce the appropriate output for Box in Mode mode with % Font as the default font. plan_box(empty, _Mode, _Font, Plan) :- empty_plan(Plan). plan_box(text(Atom), Mode, Font, Plan) :- text_plan(Atom, Mode, Font, Plan). plan_box(font(Box,FontAttrs), Mode, Font, Plan) :- update_font(FontAttrs, Font, NewFont), plan_box(Box, Mode, NewFont, Plan). plan_box(h(Boxes,Sep), Mode, Font, Plan) :- plan_boxes(Boxes, Mode, Font, Plans), plan_box(Sep, Mode, Font, SepPlan), interleave(Plans, SepPlan, Allplans), line_height(Mode, Lineheight), combine_horizontally(Allplans, Lineheight, Plan). plan_box(v(Boxes,Sep), Mode, Font, Plan) :- plan_boxes(Boxes, Mode, Font, Plans), plan_box(Sep, Mode, Font, SepPlan), interleave(Plans, SepPlan, Allplans), combine_vertically(Allplans, Plan). plan_box(fill(Boxes,HSep,Width), Mode, Font, Plan) :- plan_boxes(Boxes, Mode, Font, Plans), plan_box(Width, Mode, Font, Widthbox), plan_size(Widthbox, Fillwidth, _), plan_box(HSep, Mode, Font, HSepPlan), horizontal_linear_plan(Fillwidth, Plan0), line_height(Mode, Lineheight), plan_fill(Plans, HSepPlan, Fillwidth, Lineheight, Plan0, Plan). plan_box(horv(Boxes,HSep,_Indent), Mode, Font, HPlan) :- plan_box(h(Boxes,HSep), Mode, Font, HPlan). plan_box(horv([Box|Boxes],_HSep,Indent), Mode, Font, VPlan) :- plan_box(v([Box,h([width(Indent),v(Boxes,empty)],empty)],empty), Mode, Font, VPlan). plan_box(width(Box), Mode, Font, Plan) :- plan_box(Box, Mode, Font, Innerplan), plan_size(Innerplan, Width, _), horizontal_linear_plan(Width, Plan). plan_box(height(Box), Mode, Font, Plan) :- plan_box(Box, Mode, Font, Innerplan), plan_size(Innerplan, _, Height), line_height(Mode, Lineheight), vertical_linear_plan(Height, Lineheight, Plan). % convert a list of boxes to a list of plans plan_boxes([], _Mode, _Font, []). plan_boxes([Box|Boxes], Mode, Font, [Plan|Plans]) :- plan_box(Box, Mode, Font, Plan), plan_boxes(Boxes, Mode, Font, Plans). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Combining boxes horizontally and vertically % % % combine_horizontally(Plans, Lineheight, Bigplan) % Bigplan is the result of combining Plans left-to-right. Lineheight is the % height of each line. The tricky part of this is that we must first ensure % that all plans have the same height. combine_horizontally([], _, Empty) :- empty_plan(Empty). combine_horizontally([Plan|Plans], Lineheight, Bigplan) :- combine_horizontally1(Plans, Plan, Lineheight, Bigplan). combine_horizontally1([], BigPlan, _, BigPlan). combine_horizontally1([Plan|Plans], Bigplan0, Lineheight, Bigplan) :- combine_2_horizontally(Bigplan0, Plan, Lineheight, Bigplan1), combine_horizontally1(Plans, Bigplan1, Lineheight, Bigplan). % combine_2_horizontally(Plan1, Plan2, Lineheight, Plan) % Plan is the result of combining Plan1 and Plan2 horizontally. combine_2_horizontally(Plan1, Plan2, Lineheight, Plan) :- plan_size(Plan1, _, Height1), plan_size(Plan2, _, Height2), ( Height1 > Height2 -> extend_plan_height(Plan2, Height1, Lineheight, Plan2a), Plan1a = Plan1 ; Height1 < Height2 -> extend_plan_height(Plan1, Height2, Lineheight, Plan1a), Plan2a = Plan2 ; Plan1a = Plan1, Plan2a = Plan2 ), paste_plans_horizontally(Plan1a, Plan2a, Plan). % combine_vertically(Plans, Bigplan) % Bigplan is the result of combining Plans top-to-bottom. The catch is that % we must make sure they have the same width. combine_vertically([], Empty) :- empty_plan(Empty). combine_vertically([Plan|Plans], Bigplan) :- combine_vertically1(Plans, Plan, Bigplan). combine_vertically1([], BigPlan, BigPlan). combine_vertically1([Plan|Plans], Bigplan0, Bigplan) :- combine_2_vertically(Bigplan0, Plan, Bigplan1), combine_vertically1(Plans, Bigplan1, Bigplan). % combine_2_vertically(Plan1, Plan2, Plan) % Plan is the result of combining Plan1 and Plan2 vertically. combine_2_vertically(Plan1, Plan2, Plan) :- plan_size(Plan1, Width1, _), plan_size(Plan2, Width2, _), ( Width1 > Width2 -> extend_plan_width(Plan2, Width1, Plan2a), Plan1a = Plan1 ; Width1 < Width2 -> extend_plan_width(Plan1, Width2, Plan1a), Plan2a = Plan2 ; Plan1a = Plan1, Plan2a = Plan2 ), paste_plans_vertically(Plan1a, Plan2a, Plan). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % filling % % % Filling boxes into rows like the way words are filled into (ragged-right) % paragraphs on a page. % plan_fill(Plans, Hsep, Width, Lineheight, Bigplan0, Bigplan) % Bigplan is Bigplan0 pasted vertically above the result of laying Plans out % into as many rows as necessary. Bigplan0 has width Width. We place the % plan Hsep between adjacent pairs of plans on Plans in each row (but not % at the beginning or end of each row or between rows). % % Our approach is repeatedly fill 1 row until we run out of plans. In each % row, we place as many boxes as possible without exceeding Width. When we % cannot add any more, we pad out the row to the specified Width and paste it % to the bottom of Bigplan0 and continue with the remaining plans. plan_fill([], _, _, _, Bigplan, Bigplan). plan_fill([Plan|Plans], Hsep, Width, Lineheight, Bigplan0, Bigplan) :- plan_size(Plan, Width1, _), Width1 =< Width, fill_row(Plans, Hsep, Width, Lineheight, Plan, Rowplan, Remains), extend_plan_width(Rowplan, Width, Rowplan1), paste_plans_vertically(Bigplan0, Rowplan1, Bigplan1), plan_fill(Remains, Hsep, Width, Lineheight, Bigplan1, Bigplan). % fill_row(Plans, Hsep, Width, Lineheight, Rowplan0, Rowplan, Remains) % Rowplan is as many of Plans pasted onto the end of Rowplan0 as will fit in % Width. Hsep is also pased before each plan. Remains is the list of Plans % that didn't fit. fill_row([], _, _, _, Rowplan, Rowplan, []). fill_row([Plan|Plans], Hsep, Width, Lineheight, Rowplan0, Rowplan, Remains) :- ( combine_2_horizontally(Rowplan0, Hsep, Lineheight, Rowplan1), combine_2_horizontally(Rowplan1, Plan, Lineheight, Rowplan2), plan_size(Rowplan2, Rowwidth, _), Rowwidth =< Width -> fill_row(Plans, Hsep, Width, Lineheight, Rowplan2, Rowplan, Remains) ; Rowplan = Rowplan0, Remains = [Plan|Plans] ). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % handling plans % % % - a Plan is of the form plan(Width,Height,Lines) % - Width and Height are the width and height of the plan. % - a Line is a list of 'string's and 'tab's % - a 'string' is of the form string(Width,Text,Font) % - Width is the width of the string % - Text is the text of the string % - Font is the font of the string % - a 'tab' is of the form tab(Width) % - Width is the width of the tab % plan_size(Plan, Width, Height) % Width and Height are the size of Plan plan_size(plan(Width,Height,_), Width, Height). % plan_lines(Plan, Lines) % Lines is a list of the lines of Plan plan_lines(plan(_,_,Lines), Lines). % empty_plan(Plan) % Plan is a competely empty plan, 0 by 0 in size. empty_plan(plan(0,0,[])). % horizontal_linear_plan(Width, Plan) % Plan has 0 height, but is Width wide. horizontal_linear_plan(Width, plan(Width,0,[])). % vertical_linear_plan(Height, Lineheight, Plan) % Plan as 0 width, but is Height high; each line is Lineheight high. vertical_linear_plan(Height, Lineheight, plan(0,Height,Empties)) :- Count is Height // Lineheight, list_of(Count, [], Empties). % text_plan(Atom, Mode, Font, Plan) % Plan is a plan to display just the text of Atom in font Font and mode Mode. text_plan(Atom, Mode, Font, plan(Width,Height,[[string(Width,String,Font)]])) :- atom_codes(Atom, String), width(Mode, Font, String, Width), line_height(Mode, Height). % extend_plan_width(Plan0, Width, Plan) % Plan is the same a Plan0, but extended to Width, which must not be smaller % than the width of Plan0. extend_plan_width(plan(Width0,Height,Lines0), Width, plan(Width,Height,Lines)) :- ( Width > Width0 -> Pad is Width - Width0, append_to_each(Lines0, [tab(Pad)], Lines) ; Lines = Lines0 ). % extend_plan_height(Plan0, Height, Lineheight, Plan) % Plan is the same a Plan0, but extended to Height, which must not be smaller % than the height of Plan0. extend_plan_height(plan(Width,Height0,Lines0), Height, Lineheight, plan(Width,Height,Lines)) :- ( Height > Height0 -> Count is (Height - Height0) // Lineheight, list_of(Count, [tab(Width)], Tail), append(Lines0, Tail, Lines) ; Lines = Lines0 ). % paste_plans_horizontally(Plan1, Plan2, Plan) % Plan is the result of placing Plan2 to the right of Plan1. All 3 plans % must have the same height. paste_plans_horizontally(plan(Width1,Height,Lines1), plan(Width2,Height,Lines2), plan(Width,Height,Lines)) :- Width is Width1 + Width2, append_pairwise(Lines1, Lines2, Lines). % paste_plans_vertically(Plan1, Plan2, Plan) % Plan is the result of placing Plan2 below Plan1. All 3 plans must have the % same width. paste_plans_vertically(plan(Width,Height1,Lines1), plan(Width,Height2,Lines2), plan(Width,Height,Lines)) :- Height is Height1 + Height2, append(Lines1, Lines2, Lines). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Font handling % % % update_font(Attrs, Font0, Font) % Font is the same as Font0, except that it has the attributes specified by % Attrs. Attributes not specified on Attrs are not changed. update_font(Attrs, Font0, Font) :- font_name(Family0, Weight0, Slant0, Font0), update_attrs(Attrs, Family0, Weight0, Slant0, Family, Weight, Slant), font_name(Family, Weight, Slant, Font). % update_attrs(Attrs, Family0, Weight0, Slant0, Family, Weight, Slant) % Family, Weight, and Slant are the same as Family0, Weight0, and Slant0, % except where specified otherwise on Attrs. update_attrs([], Family, Weight, Slant, Family, Weight, Slant). update_attrs([Attr=Value|Attrs], Family0, Weight0, Slant0, Family, Weight, Slant) :- update_1_attr(Attr, Value, Family0, Weight0, Slant0, Family1, Weight1, Slant1), update_attrs(Attrs, Family1, Weight1, Slant1, Family, Weight, Slant). update_1_attr(family, Family, _, Weight, Slant, Family, Weight, Slant). update_1_attr(weight, Weight, Family, _, Slant, Family, Weight, Slant). update_1_attr(slant, Slant, Family, Weight, _, Family, Weight, Slant). % width(Mode, Font, String, Width) % Width is the width of String, a list of character codes, when written in % font Font in Mode mode. width(text, _Attrs, String, Width) :- length(String, Width). width(ps, Font, String, Width) :- ps_string_width(String, Font, 0, Width0), Width is 1000 - truncate(1000-Width0). % hack because ceiling % function doesn't work % ps_string_width(Chars, Font, Width0, Width) % Width is the total width of the characters on Chars in font Font, plus % Width0. This does not round up to the nearest integer. ps_string_width([], _Font, Width, Width). ps_string_width([C|Cs], Font, Width0, Width) :- ps_char_width(Font, C, Cwidth), Width1 is Width0 + Cwidth, ps_string_width(Cs, Font, Width1, Width). % font_name(Family, Weight, Slant, Name) % Name is the name of the font with family Family, weight Weight and slant % Slant. font_name(courier, normal, normal, 'Courier'). font_name(courier, normal, italic, 'Courier-Oblique'). font_name(courier, bold, normal, 'Courier-Bold'). font_name(courier, bold, italic, 'Courier-BoldOblique'). font_name(helvetica, normal, normal, 'Helvetica'). font_name(helvetica, normal, italic, 'Helvetica-Oblique'). font_name(helvetica, bold, normal, 'Helvetica-Bold'). font_name(helvetica, bold, italic, 'Helvetica-BoldOblique'). font_name(times, normal, normal, 'Times'). font_name(times, normal, italic, 'Times-Italic'). font_name(times, bold, normal, 'Times-Bold'). font_name(times, bold, italic, 'Times-BoldItalic'). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % rendering postscript % % % render_as_ps(Lines) % Render Lines as postscript, including the document header and footer, and % paginating as necessary. render_as_ps(Lines) :- write('%!PS-Adobe-3.0\n%%Pages: (atend)\n%%EndComments\n'), render_pages_as_ps(Lines, 0, N), % start with page 1 format('%%Trailer\n%%Pages: ~d\n%%EOF\n', [N]). render_pages_as_ps(Lines, N0, N) :- ( Lines = [] -> N = N0 ; N1 is N0 + 1, format('%%Page: ~d ~d\n', [N1,N1]), ps_top_baseline(Ypos), render_page_as_ps(Lines, Ypos, Rest), write('showpage\n'), render_pages_as_ps(Rest, N1, N) ). % render_page_as_ps(Lines, Ypos, Rest) % render one page worth of Lines, beginning at Y position Ypos, leaving Rest % as the lines that don't fit on that page. render_page_as_ps(Lines, Ypos, Rest) :- ( Lines = [] -> Rest = [] ; ps_bottom_baseline(Bottom), Ypos < Bottom -> Rest = Lines ; Lines = [Line1|Lines1], ps_left_margin(Xpos), render_line_as_ps(Line1, Xpos, Ypos), line_height(ps, Height), Ypos1 is Ypos - Height, render_page_as_ps(Lines1, Ypos1, Rest) ). % render_line_as_ps(Cmds, Xpos, Ypos) % Cmds is a list of commands; render it at Y position Ypos and beginning at X % position Xpos. render_line_as_ps([], _, _). render_line_as_ps([Cmd|Cmds], Xpos, Ypos) :- render_ps_command(Cmd, Xpos, Ypos, Xpos1), render_line_as_ps(Cmds, Xpos1, Ypos). % render_ps_command(Cmd, Xpos0, Ypos, Xpos) % Render the single command Cmd as postscript at X position Xpos0 and Y % position Ypos. Xpos is the X position after the command. render_ps_command(tab(Width), Xpos, _, Xpos1) :- Xpos1 is Xpos + Width. render_ps_command(string(Width,Text,Font), Xpos, Ypos, Xpos1) :- Xpos1 is Xpos + Width, format('~d ~d moveto\n', [Xpos,Ypos]), ps_font_size(PSFontSize), format('/~w findfont ~d scalefont setfont\n', [Font,PSFontSize]), escape_some_chars(Text, EscapedText), format('(~s) show\n', [EscapedText]). % adds a backslash in front of non-alphanumeric, non-space characters escape_some_chars([], []). escape_some_chars([C|Cs], Escaped) :- ( must_escape(C) -> Escaped = [C|Escaped1] ; Escaped = [0'\,C|Escaped1] ), escape_some_chars(Cs, Escaped1). % must_escape(Char) % Char is a character that needs to be escaped with a backslash for % postscript. must_escape(Char) :- ( code_type(Char, alnum) -> true ; Char =:= 0' % space character ). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % rendering text % % % render_as_text(Lines) % Write Lines out as text. render_as_text([]). render_as_text([Line|Lines]) :- render_line_as_text(Line), render_as_text(Lines). % render_line_as_text(Cmds) % Render the commands Cmds as text. render_line_as_text([]) :- nl. render_line_as_text([Cmd|Cmds]) :- render_command_as_text(Cmd), render_line_as_text(Cmds). % render_command_as_text(Cmd) % Render the single commands Cmd as text. render_command_as_text(string(_,Text,_)) :- format('~s', [Text]). render_command_as_text(tab(N)) :- tab(N). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Utilities % % % interleave(List, Sep, Interleaved) % Interleaved is the list obtained by inserting Sep between each adjacent % pair of elements of List. If List is empty, so is Interleaved. interleave([], _Sep, []). interleave([X|Xs], Sep, [X|Interleaved]) :- alternate1(Xs, Sep, Interleaved). alternate1([], _, []). alternate1([X|Xs], Sep, [Sep,X|Alt]) :- alternate1(Xs, Sep, Alt). % list_of(N, Item, List) % List is a list of length N; each element is Item. list_of(N, Item, List) :- ( N =< 0 -> List = [] ; List = [Item|List1], N1 is N - 1, list_of(N1, Item, List1) ). % append_pairwise(Ls, L1s, L2s) % L2s is the list of lists obtained by appending each element of Ls with the % corresponding element of L1s. append_pairwise([], [], []). append_pairwise([L|Ls], [L1|L1s], [L2|L2s]) :- append(L, L1, L2), append_pairwise(Ls, L1s, L2s). % append_to_each(Shorters, Suffix, Longers) % Longers is the list of lists obtained by appending Suffix to the end of % each element of Shorters. append_to_each([], _, []). append_to_each([Shorter|Shorters], Suffix, [Longer|Longers]) :- append(Shorter, Suffix, Longer), append_to_each(Shorters, Suffix, Longers). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Constants % % % width_of_display(Mode, Width) % In Mode mode, the maximum displayable width is Width. width_of_display(text, 78). width_of_display(ps, 435). % line_height(Mode, Height) % In Mode mode, the line height is Height. line_height(text, 1). line_height(ps, 14). % ps_font_size(Size) % The font size we use for all postscript output. ps_font_size(12). % ps_top_baseline(Y) % The topmost line of each postscript page is drawn at y position Y. ps_top_baseline(750). % ps_bottom_baseline(Y) % Y is the bottommost Y position we can write at on a postscript page. ps_bottom_baseline(80). % ps_left_margin(X) % X is the left margin for postscript. ps_left_margin(80).