# # PDF Generation Library # # # format_for_pdf() # # Purpose: This formats a floating point number for PDF. It uses format(), but then converts commas to dots to handle commas-as-decimal-divider languags. # subroutine(format_for_pdf(string v, string pattern), ( string formatteds = format(v, pattern); #echo("formatteds: " . formatteds); formatteds = replace_all(formatteds, ",", "."); formatteds; )); #### format_for_pdf() #### # # add_obj() # # Purpose: This adds an object to the current PDF document # # Parameters: n: the object to add # subroutine(add_obj(node pdf, node n), ( string objstr; objstr .= @pdf{"objectnum"}; @pdf{"objectnum"}++; objstr .= " "; objstr .= 0; objstr .= " obj\n"; string type = node_value(subnode_by_name(n, "type")); # If it's a dictionary, add it to the PDF if ((type eq "dictionary") or (type eq "stream")) then ( objstr .= " <<\n"; node sn; string prefix = " "; # Add each subnode of n as a dictionary entry (except type, which specifies the node type, # and objnum, which is the PDF object number, and contents, which is the contents of a page). foreach sn n ( if ((node_name(sn) ne "type") and (node_name(sn) ne "objnum") and (node_name(sn) ne "stream") and (node_name(sn) ne "pdf") and (node_name(sn) ne "contents")) then ( objstr .= prefix . node_name(sn); objstr .= " "; objstr .= node_value(sn); objstr .= "\n"; prefix = " "; ); ); # foreach n # If it's a string, add the /Length if (type eq "stream") then objstr .= " /Length " . length(node_value(subnode_by_name(n, "stream"))) . "\n"; # Finish the dictionary part objstr .= " >>\n"; # If it's a stream, embed the stream data if (type eq "stream") then ( objstr .= "stream\n"; objstr .= node_value(subnode_by_name(n, "stream")) . "\n"; objstr .= "endstream\n"; ); # if stream ); # if dictionary or stream # Embed an array else if (type eq "array") then ( objstr .= "["; node sn; string prefix = ""; foreach sn n ( if ((node_name(sn) ne "type") and (node_name(sn) ne "objnum")) then ( objstr .= node_value(sn); objstr .= " "; ); ); objstr .= "]\n"; ); # if array objstr .= "endobj\n\n"; set_subnode_value(pdf{"xref"}, @pdf{"objectnum"}, @pdf{"objectpos"}); @pdf{"objectpos"} = @pdf{"objectpos"} + length(objstr); @pdf{"data"} = @pdf{"data"} . objstr; )); # add_obj() # # xref() # # Purpose: This computes the xref table for the current PDF document. # # Parameters: return the xref table # subroutine(xref(node pdf), ( "xref\n"; 0; " "; num_subnodes(pdf{"xref"}) + 1; "\n"; "0000000000 65535 f\n"; node x; foreach x (pdf{"xref"}) ( format(node_value(x), "%010ld"); " "; format(0, "%05ld"); " "; "n"; "\n"; ); )); # xref() # # new_obj() # # Purpose: This creates a new PDF object # # Parameters: returns the object # subroutine(new_obj(node pdf), ( # Create the node node o = new_node(); # Remember the object number @o{"objnum"} = @pdf{"objnum"}; @pdf{"objnum"}++; # Save it in pdf{"objects"} set_node_type(subnode_by_name(pdf{"objects"}, @pdf{"objnum"}), 'node'); set_node_value(subnode_by_name(pdf{"objects"}, @pdf{"objnum"}), o); # Return it o; )); # new_obj() # # obj_ref() # # Purpose: This returns a reference to an object, e.g., '12 0 R' for object 12. # # Parameters: o: the object # returns the reference string # subroutine(obj_ref(node o), ( if (!subnode_exists(o, "objnum")) then error("object without objnum"); @(o{"objnum"}) . " 0 R"; )); # obj_ref() # # new_type1_font() # # Purpose: this creates a new font # # Parameters: name: the font name, e.g. "/F1" # basefont: the base font name, e.g. "/Helvetica" # encoding: the encoding, e.g., "/Encoding" # subroutine(new_type1_font(node pdf, string basefont, string encoding), ( # node pdf = @page{"pdf"}; # Choose a unique font name string name = "/F" . @pdf{"fontnamenum"}; @pdf{"fontnamenum"}++; node font = new_obj(pdf); @font{"type"} = "dictionary"; @font{"/Type"} = "/Font"; @font{"/Subtype"} = "/Type1"; @font{"/Name"} = name; @font{"/BaseFont"} = basefont; @font{"/Encoding"} = encoding; # Add the font to the resources for this page node fonts = @pdf{"fonts"}; @fonts{name} = obj_ref(font); set_node_type(pdf{"fonts"}{name}, "node"); @pdf{"fonts"}{name} = fonts; font; )); # new_type1_font() # This adds a standard font to a PDF subroutine(add_standard_font(node pdf, string baseFont), ( if (!node_exists("v.stdfonts")) then v.stdfonts = ""; # node font = new_type1_font(pdf, baseFont, "/MacRomanEncoding"); # node font = new_type1_font(pdf, baseFont, "/StandardEncoding"); node font = new_type1_font(pdf, baseFont, "/WinAnsiEncoding"); set_node_type("v.stdfonts"{baseFont}, 'node'); "v.stdfonts"{baseFont} = font; )); # add_standard_font() # # new_pdf() # # Purpose: This starts creating a new PDF # subroutine(new_pdf, ( node pdf = new_node(); v.characters_cache = ""; # Initialize the globals @pdf{"objectpos"} = 9; @pdf{"objectnum"} = 1; pdf{"xref"} = ""; # Add the header @pdf{"objnum"} = 1; @pdf{"objects"} = ""; # Start font names at /F1 @pdf{"fontnamenum"} = 1; # Start image names at /Im1 @pdf{"imagenamenum"} = 1; # Add the outlines list (empty for now) node outlines = new_obj(pdf); @outlines{"type"} = "dictionary"; @outlines{"/Type"} = "/Outlines"; set_node_type(pdf{"outlines"}, "node"); @pdf{"outlines"} = outlines; v.outlines = ""; set_node_type("v.outlines", "node"); v.outlines = new_node(); # Add the procset node procset = new_obj(pdf); @procset{"type"} = "array"; @procset{0} = "/PDF"; @procset{1} = "/Text"; @procset{2} = "/ImageB"; set_node_type(pdf{"procset"}, "node"); @pdf{"procset"} = procset; # Add the XObject node node xobject = new_obj(pdf); @xobject{"type"} = "dictionary"; set_node_type(pdf{"xobject"}, "node"); @pdf{"xobject"} = xobject; # Add the /Font node node fonts = new_obj(pdf); @fonts{"type"} = "dictionary"; set_node_type(pdf{"fonts"}, "node"); @pdf{"fonts"} = fonts; # Add the /Pages node node pages = new_obj(pdf); @pages{"type"} = "dictionary"; @pages{"/Type"} = "/Pages"; set_node_type(pdf{"pages"}, "node"); @pdf{"pages"} = pages; pdf{"pageslist"} = ""; @pdf{"data"} = "%PDF-1.4\n"; # Define standard fonts add_standard_font(pdf, "/Times-Roman"); add_standard_font(pdf, "/Times-Bold"); add_standard_font(pdf, "/Times-BoldItalic"); add_standard_font(pdf, "/Times-Italic"); add_standard_font(pdf, "/Courier"); add_standard_font(pdf, "/Courier-BoldOblique"); add_standard_font(pdf, "/Courier-Bold"); add_standard_font(pdf, "/Courier-Oblique"); add_standard_font(pdf, "/Helvetica"); add_standard_font(pdf, "/Helvetica-BoldOblique"); add_standard_font(pdf, "/Helvetica-Bold"); add_standard_font(pdf, "/Helvetica-Oblique"); pdf; )); # new_pdf subroutine(font_name_to_font_node(node page, string font_name), ( node pdf = @page{"pdf"}; node font = @pdf{"fonts"}{font_name}; font; )); # font_name_to_font_node() # # new_page() # # Purpose: This creates a new page # # Parameters: # subroutine(new_page(node pdf, int left, int top, int right, int bottom), ( # Create the contents node node contents = new_obj(pdf); @contents{"type"} = "stream"; @contents{"stream"} = ""; node page = new_obj(pdf); set_node_type(page{"pdf"}, "node"); @page{"pdf"} = pdf; @page{"type"} = "dictionary"; @page{"/Type"} = "/Page"; @page{"/Parent"} = obj_ref(@pdf{"pages"}); @page{"/MediaBox"} = "[" . left . " " . top . " " . right . " " . bottom . "]"; @page{"/Contents"} = obj_ref(contents); # Save the contents node in a "contents" subnode of the page, for easy access by the client set_node_type(page{"contents"}, "node"); @page{"contents"} = contents; # Create a resources list containing only the ProcSet (other resources will be added later) node resources = new_obj(pdf); @resources{"type"} = "dictionary"; @resources{"/ProcSet"} = obj_ref(@pdf{"procset"}); @resources{"/Font"} = obj_ref(@pdf{"fonts"}); @resources{"/XObject"} = obj_ref(@pdf{"xobject"}); @page{"/Resources"} = obj_ref(resources); # Add this page to the pageslist int objnum = @page{"objnum"}; set_node_type((pdf{"pageslist"}){objnum}, 'node'); @(pdf{"pageslist"}){objnum} = page; @pdf{"current_page_objnum"} = @page{"objnum"}; #echo("new_page(); current_page_objnum=" . @pdf{"current_page_objnum"}); page; )); # new_page() # # set_nonstroking_color() # # Purpose: This sets the current non-stroking color # # Parameters: page: the page to set the color on # r, g, b: the color # subroutine(set_nonstroking_color(node page, float r, float g, float b), ( node contents = @page{"contents"}; @contents{"stream"} .= " " . format_for_pdf(r, '%0.6f') . " " . format_for_pdf(g, '%0.6f') . " " . format_for_pdf(b, '%0.6f') . " rg\n"; )); # set_nonstroking_color() # # set_stroking_color() # # Purpose: This sets the current stroking color # # Parameters: page: the page to set the color on # r, g, b: the color # subroutine(set_stroking_color(node page, float r, float g, float b), ( node contents = @page{"contents"}; @contents{"stream"} .= " " . format_for_pdf(r, '%0.6f') . " " . format_for_pdf(g, '%0.6f') . " " . format_for_pdf(b, '%0.6f') . " RG\n"; )); # set_stroking_color() # # text_width() # # Purpose: This returns the width of a string, in units (roughly speaking, pixels). # # Parameters: font: the font to use # text: the text to measure # returns the length of the text # subroutine(text_width(node font, float fontSize, string text), ( string baseFont = substr(@font{"/BaseFont"}, 1); string baseFontNodename = replace_all(lowercase(baseFont), "-", "_"); node afm = "lib.afm." . baseFontNodename; node CharMetrics = afm{"CharMetrics"}; float textWidth = 0; for (int i = 0; i < length(text); i++) ( string c = substr(text, i, 1); int asciic = char_to_ascii(c); float charWidth = @(CharMetrics{asciic}){"WX"}; textWidth += (charWidth / 1000); ); textWidth * fontSize; )); # text_width # # text_line_spacing() # # Purpose: This returns the distance between baselines in a font # # Parameters: font: the font to use # text: the text to measure # returns the distance between baselines # subroutine(text_line_spacing(node font, float fontSize), ( string baseFont = substr(@font{"/BaseFont"}, 1); string baseFontNodename = replace_all(lowercase(baseFont), "-", "_"); node afm = "lib.afm." . baseFontNodename; node FontBBox = afm{"FontBBox"}; float fontBoxHeight = @FontBBox{"ury"} - @FontBBox{"lly"}; (fontBoxHeight / 1000) * fontSize; )); # text_line_spacing # # draw_text() # # Purpose: This draws a text string on a page # # Parameters: page: the page # font: the font to use # size: the size to draw # x, y: where to draw # text: the text to draw # subroutine(draw_text(node page, node font, float size, float x, float y, string text), ( #echo("draw_text()"); # Get the font box string baseFont = substr(@font{"/BaseFont"}, 1); string baseFontNodename = replace_all(lowercase(baseFont), "-", "_"); node afm = "lib.afm." . baseFontNodename; node FontBBox = afm{"FontBBox"}; float lly = @FontBBox{"lly"}; float descent = (lly / 1000) * size; #echo("dt2"); #echo("font: " . node_as_string(font)); #echo("page: " . node_as_string(page)); if (!(page?{"contents"})) then error("Internal Error: No page contents in draw_text()"); #echo("dt2.1"); node contents = @page{"contents"}; #echo("dt2.2"); string fontName = @font{"/Name"}; #echo("dt2.3"); string newContents = ""; newContents .= " BT\n"; newContents .= " " . fontName . " " . size . " Tf\n"; newContents .= " " . format_for_pdf(x, '%0.6f') . " " . format_for_pdf((y - descent), '%0.6f') . " Td\n"; text = replace_all(text, '(', '\\('); text = replace_all(text, ')', '\\)'); #echo("text before conversion"); text = convert_charset(text, "UTF-8", "CP1250"); #echo("text before conversion: " . text); # ASCII, ISO-8859-{1,2,3,4,5,7,9,10,13,14,15,16}, KOI8-R, KOI8-U, KOI8-RU, CP{1250,1251,1252,1253,1254,1257}, CP{850,866}, Mac{Roman,CentralEurope,Iceland,Croatian,Romania}, Mac{Cyril- # lic,Ukraine,Greek,Turkish}, Macintosh newContents .= " (" . text . ") Tj\n"; newContents .= " ET\n"; @contents{"stream"} .= newContents; #echo("dt3"); )); # draw_text() subroutine(get_local_pathname(string pathname), ( # echo("pathname=" . pathname); string local_pathname = pathname; string html_directory = v.html_pathname; #echo("html_directory: " . html_directory); string divider = internal.directory_divider; local_pathname = replace_all(local_pathname, "/", divider); int lastdivider = last_index(html_directory, divider); if (lastdivider != -1) then ( html_directory = substr(html_directory, 0, lastdivider + 1); # echo("html_directory: " . html_directory); local_pathname = html_directory . local_pathname; ); # if lastdivider != -1 #echo("local_pathname: " . local_pathname); local_pathname; )); # get_local_pathname() # # load_image_into_pdf() # # Purpose: This loads an image into a PDF document, so it can be drawn later with calls to draw_image() # # Parameters: im: the image handle # subroutine(load_image_into_pdf(node pdf, string im), ( # Choose a unique image name string name = "/Im" . @pdf{"imagenamenum"}; string softMaskName = name . "SoftMask"; @pdf{"imagenamenum"}++; int imageWidth = get_image_width(im); int imageHeight = get_image_height(im); # Compute the pixel and mask data string imageHex = ""; string softMaskHex = ""; for (int y = 0; y < imageHeight; y++) ( for (int x = 0; x < imageWidth; x++) ( # Get the pixel as a 6-digit hex int pixel = get_image_pixel(im, x, y); string pixelHex = convert_base(pixel, 10, 16); while(length(pixelHex) < 6) pixelHex = '0' . pixelHex; imageHex .= pixelHex; # Get the mask as a 2-digit hex int mask = 255 - get_image_pixel_transparency(im, x, y); string maskHex = convert_base(mask, 10, 16); while(length(maskHex) < 2) maskHex = '0' . maskHex; softMaskHex .= maskHex; ); # for x ); # for y # Create the soft mask (transparency mask) node softMaskImg = new_obj(pdf); @softMaskImg{"type"} = "stream"; @softMaskImg{"/Type"} = "/XObject"; @softMaskImg{"/Subtype"} = "/Image"; @softMaskImg{"/Width"} = imageWidth; @softMaskImg{"/Height"} = imageHeight; @softMaskImg{"/ColorSpace"} = "/DeviceGray"; @softMaskImg{"/BitsPerComponent"} = 8; @softMaskImg{"/Filter"} = "/ASCIIHexDecode"; @softMaskImg{"stream"} = softMaskHex; # Add the soft mask to /XObjects node xobjects = @pdf{"xobject"}; @xobjects{softMaskName} = obj_ref(softMaskImg); # Create the RBG image node img = new_obj(pdf); @img{"type"} = "stream"; @img{"/Type"} = "/XObject"; @img{"/Subtype"} = "/Image"; @img{"/Width"} = imageWidth; @img{"/Height"} = imageHeight; @img{"/ColorSpace"} = "/DeviceRGB"; @img{"/BitsPerComponent"} = 8; @img{"/Filter"} = "/ASCIIHexDecode"; @img{"/SMask"} = obj_ref(softMaskImg); @img{"stream"} = imageHex; # Add the image to /XObjects node xobjects = @pdf{"xobject"}; @xobjects{name} = obj_ref(img); name; )); # load_image_into_pdf() # # draw_image() # # Purpose: This draws a bitmap image to the PDF # # Parameters: page: the page to draw on # image: the image object to draw # x, y: where to draw it # xscale, yscale: the scaling factors # subroutine(draw_image(node page, string imageName, float x, float y, float xscale, float yscale), ( node contents = @page{"contents"}; @contents{"stream"} .= " q\n"; @contents{"stream"} .= " " . format_for_pdf(xscale, '%0.6f') . " 0 0 " . format_for_pdf(yscale, '%0.6f') . " " . format_for_pdf(x, '%0.6f') . " " . format_for_pdf(y, '0.6f') . " cm\n"; @contents{"stream"} .= " " . imageName . " Do % Paint image \n"; @contents{"stream"} .= " Q\n"; )); # draw_image() # # draw_wrapped_text() # # Purpose: This draws a text string on a page, wrapping it inside a box. # # Parameters: page: the page # font: the font to use # size: the size to draw # xmin, ymin: the lower left corner of the bounding box # width, height: the width and height of the bounding box # text: the text to draw # alignment: the alignment in the box: "left", "right", or "center" # draw: if this is true, draw the text; otherwise, just compute and return the height of the box. # get_width: if this is true, just compute and return the width of the string. # get_minimum_width: if this is return, compute and return the minimum width of a table cell # containing this string; i.e., the length of the longest word. # returns the width, or height, value of get_width # subroutine(draw_wrapped_text(node page, node font, float size, float xmin, float ymax, float width, string text, string alignment, bool draw, bool wrap, bool get_width, bool get_minimum_width), ( #echo("draw_wrapped_text(); width=" . width . "; text=" . text); string fontName = @font{"/Name"}; node characters = "v.characters_cache"{fontName}{size}{("T" . replace_all(text, '.', '_'))}; #echo("characters: " . node_as_string(characters)); if (!(@characters)) then ( @characters = true; ## ## GET CHARACTER INFO NODE ## string baseFont = @font{"/BaseFont"}; string baseFontWithoutSlash = substr(@font{"/BaseFont"}, 1); string baseFontNodename = replace_all(lowercase(baseFontWithoutSlash), "-", "_"); node afm = "lib.afm." . baseFontNodename; node CharMetrics = afm{"CharMetrics"}; node FontBBox = afm{"FontBBox"}; float lly = @FontBBox{"lly"}; float descent = (-lly / 1000) * size; # Create the node which contains information about the characters int charnum = 0; # Fudge width a bit to make sure that floating point rounding errors don't cause a line to wrap when it fits exactly. # width++; int pos = 0; int textlen = length(text); string c; int asciic; float charWidth; int image_width = 0; int image_height = 0; string image_pathname; # Take a pass through this line to determine where to split it while (pos < textlen) ( # Get the character c = substr(text, pos, 1); v.characters_seen++; # Handle inline HTML tags while (c eq '<') ( # Read to the end of the <> section, to get the tag name string tag = ""; pos++; c = substr(text, pos, 1); while (c ne '>') ( tag .= c; pos++; c = substr(text, pos, 1); ); pos++; c = substr(text, pos, 1); # Extract attributes string attributes_string; if (matches_regular_expression(tag, '^([^ ]+) (.*)$')) then ( tag = $1; attributes_string = $2; ); string oldBaseFont = baseFont; if (tag eq "strong") then ( if (baseFont eq "/Courier") then ( baseFont = "/Courier-Bold"; ); else if (baseFont eq "/Times-Roman") then ( baseFont = "/Times-Bold"; ); else if (baseFont eq "/Helvetica") then ( baseFont = "/Helvetica-Bold"; ); else error("Unsupported font transition: tag=" . tag . "; font=" . baseFont); ); else if (tag eq "/strong") then ( if (baseFont eq "/Courier-Bold") then ( baseFont = "/Courier"; ); else if (baseFont eq "/Times-Bold") then ( baseFont = "/Times-Roman"; ); else if (baseFont eq "/Helvetica-Bold") then ( baseFont = "/Helvetica"; ); else error("Unsupported font transition: tag=" . tag . "; font=" . baseFont); ); # Handle IMG tags else if (tag eq "img") then ( node attributes = new_node(); # Parse the IMG attributes while (attributes_string ne "") ( # Handle normal attribute if (matches_regular_expression(attributes_string, '^([a-z]+)="?([^"]*)"?/? (.*)$')) then ( string attribute_name = $1; string attribute_value = $2; attributes_string = $3; @attributes{attribute_name} = attribute_value; ); else if (matches_regular_expression(attributes_string, '^ */ *$')) then ( attributes_string = ""; ); else error("Can't parse IMG attributes: '" . attributes_string . "'"); ); # while attributes_string # Validate that we have the required attributes for IMG if (!(attributes?{"width"})) then error("IMG tags without width attributes are not supported"); if (!(attributes?{"height"})) then error("IMG tags without height attributes are not supported"); if (!(attributes?{"src"})) then error("IMG tag is missing the src attribute"); # Get information about the image image_pathname = get_local_pathname(@attributes{"src"}); image_width = @attributes{"width"}; image_height = @attributes{"height"}; delete_node(attributes); ); # if IMG # Handle A tags else if ((tag eq "a") or (tag eq "/a")) then ( ); # if A # Handle SPAN tags else if ((tag eq "span") or (tag eq "/span")) then ( ); # if SPAN else error("Unsupported tag in PDF text display: " . tag); # If the font changed, recompute the metrics if (oldBaseFont ne baseFont) then ( font = @"v.stdfonts"{baseFont}; baseFontNodename = replace_all(lowercase(baseFont), "-", "_"); baseFontNodename = replace_all(baseFontNodename, "/", ""); afm = "lib.afm." . baseFontNodename; FontBBox = afm{"FontBBox"}; lly = @FontBBox{"lly"}; descent = (-lly / 1000) * size; ); ); # while start of tag # Handle HTML entities if (c eq '&') then ( # Read to the end of the &; section, to get the entity name string entity = ""; pos++; c = substr(text, pos, 1); while (c ne ';') ( entity .= c; pos++; c = substr(text, pos, 1); ); if (entity eq 'copy') then ( c = ascii_to_char((2*64) + (5*8) + 1); charWidth = @(CharMetrics{"copyright"}){"WX"}; charWidth = (charWidth / 1000) * size; ); else if (entity eq 'nbsp') then ( c = ' '; asciic = char_to_ascii(c); charWidth = @(CharMetrics{asciic}){"WX"}; charWidth = (charWidth / 1000) * size; ); else if (entity eq 'lt') then ( c = '<'; asciic = char_to_ascii(c); charWidth = @(CharMetrics{asciic}){"WX"}; charWidth = (charWidth / 1000) * size; ); else if (entity eq 'gt') then ( c = '>'; asciic = char_to_ascii(c); charWidth = @(CharMetrics{asciic}){"WX"}; charWidth = (charWidth / 1000) * size; ); # Handle #N entities, where N is decimal ASCII code else if (matches_regular_expression(entity, '^#([0-9]+)$')) then ( asciic = $1; c = ascii_to_char(asciic); charWidth = @(CharMetrics{asciic}){"WX"}; charWidth = (charWidth / 1000) * size; ); # 2009-04-15 - If we don't know the entity, use it directly (don't generate a fatal error) else ( c = '?'; asciic = char_to_ascii(c); charWidth = @(CharMetrics{asciic}){"WX"}; charWidth = (charWidth / 1000) * size; ); ); # if HTML entity # If it's not an entity, compute its width from the normal table else ( # If it's an image, treat it as a character whose width is the image width if (image_width) then charWidth = image_width; # Get the width of this character else ( asciic = char_to_ascii(c); charWidth = @(CharMetrics{asciic}){"WX"}; charWidth = (charWidth / 1000) * size; ); ); # if not entity # Add this to the characters node charnum++; node character = characters{charnum}; # If it's a character, remember the character information if (image_width == 0) then ( @character{"c"} = c; @character{"box_width"} = charWidth; @character{"box_height"} = text_line_spacing(font, size); set_node_type(character{"font"}, 'node'); @character{"font"} = font; pos++; ); # if character # If it's an image, remember the image information else ( @character{"c"} = "image"; @character{"image_pathname"} = image_pathname; @character{"image_width"} = image_width; @character{"image_height"} = image_height; @character{"box_width"} = image_width; @character{"box_height"} = image_height + descent; image_width = 0; ); # if image ); # while pos < textlen ); # if characters node not yet computed ## ## NOW WE HAVE THE "characters" NODE FOR THIS FONT/SIZE/STRING ## node character; # If we're getting the minimum width, count the width of the longest wrapped line. if (get_minimum_width) then ( float minimumWidth = 0; float lineWidth = 0; foreach character characters ( # If this is an image, include its width in the line, and the end the line (we can wrap after an image). if (@character{"c"} eq "image") then ( lineWidth += @character{"box_width"}; if (wrap and (lineWidth > minimumWidth)) then ( minimumWidth = lineWidth; lineWidth = 0; ); ); # If this is a space, that's the end of the line (not including the space). else if (@character{"c"} eq " ") then ( if (wrap) then ( if (lineWidth > minimumWidth) then ( minimumWidth = lineWidth; ); lineWidth = 0; ); else lineWidth += @character{"box_width"}; ); # If it's another character, include it in the line else ( lineWidth += @character{"box_width"}; ); ); # foreach character if (lineWidth > minimumWidth) then minimumWidth = lineWidth; # Return the computed minimum width minimumWidth; return; ); # if get_minimum_width # Otherwise, we're computing the height, and possibly drawing. else ( float y = ymax; float lineWidth = 0; int pos = 0; int lastWrapPos = -1; int nextLinePos = -1; int lineStartPos = pos; int lineEndPos; float lineHeight = 0; float lineDescent = 0; float lineWidth = 0; float x; int drawPos; string image_handle; node pdf; string image_name; int rangePos; int rangeStartPos; int rangeEndPos; bool endOfRange; string rangeString; float rangeWidth; int drawEndPos; float maxLineWidth = 0; while (pos < num_subnodes(characters)) ( # Find the end of the wrapped line, and the last wrap position before the end lineWidth = 0; lastWrapPos = -1; nextLinePos = -1; lineStartPos = pos; while ((lineWidth < width) and (pos < num_subnodes(characters))) ( character = characters[pos]; if (@character{"c"} eq " ") then ( lastWrapPos = pos; nextLinePos = pos + 1; ); else if (@character{"c"} eq "image") then ( lastWrapPos = pos + 1; nextLinePos = pos + 1; ); lineWidth += @(characters[pos]){"box_width"}; pos++; ); # If this line was longer than any we've seen, update maxLineWidth if (lineWidth > maxLineWidth) then maxLineWidth = lineWidth; # Find the end of the wrapped line, backing up to the wrap point if possible if (pos == num_subnodes(characters)) then lineEndPos = pos; else if (lastWrapPos != -1) then ( lineEndPos = lastWrapPos; pos = nextLinePos; ); else lineEndPos = pos; # Compute the line height lineHeight = 0; lineDescent = 0; lineWidth = 0; for (int drawPos = lineStartPos; drawPos != lineEndPos; drawPos++) ( node character = characters[drawPos]; lineWidth += @character{"box_width"}; if (@character{"box_height"} > lineHeight) then lineHeight = @character{"box_height"}; if (@character{"descent"} > lineDescent) then lineDescent = @character{"descent"}; ); if (alignment eq "right") then x = xmin + width - lineWidth; else if (alignment eq "center") then x = xmin + (width / 2) - (lineWidth / 2); else x = xmin; y -= (lineHeight + lineDescent); # Don't allow 0-length lines; there must always be at least one character per line. if (lineEndPos == lineStartPos) then ( lineEndPos = lineStartPos + 1; pos = lineEndPos; ); # Draw the text if (draw) then ( drawPos = lineStartPos; while (drawPos < lineEndPos) ( rangeStartPos = drawPos; rangeEndPos = rangeStartPos + 1; # Find a character range, starting at drawPos, where the font and size never change. endOfRange = false; rangeString = @(characters[rangeStartPos]){"c"}; rangeWidth = @(characters[rangeStartPos]){"box_width"}; while (!endOfRange) ( if (rangeEndPos == lineEndPos) then endOfRange = true; else if (@character{"c"} eq "image") then endOfRange = true; else if ((@((characters[rangeEndPos]){"font"}) + 0) != (@((characters[(rangeEndPos - 1)]){"font"}) + 0)) then endOfRange = true; else ( rangeString .= @(characters[rangeEndPos]){"c"}; rangeWidth += @character{"box_width"}; rangeEndPos++; ); ); character = characters[rangeStartPos]; if (@character{"c"} eq "image") then ( #echo("pathname: " . @character{"image_pathname"}); string image_handle = read_image_from_disk(@character{"image_pathname"}); node pdf = @page{"pdf"}; string image_name = load_image_into_pdf(pdf, image_handle); draw_image(page, image_name, x, y + @character{"descent"}, @character{"image_width"}, @character{"image_height"}); ); else ( draw_text(page, @character{"font"}, size, x, y, rangeString); ); x += rangeWidth; drawPos = rangeEndPos; ); # while (drawPos < lineEndPos) ); # if draw ); # while pos ); # if compute height and possibly draw # If we're computing the width, return the width of the text we just drew (width of longest wrapped line) if (get_width) then ( v.text_height = ymax - y; maxLineWidth; ); # Otherwise, we were computing the height; return the height of the text we just drew else ymax - y; )); # draw_wrapped_text() # # draw_line() # # Purpose: This draws a line on a page # # Parameters: page: the page # x1, y1: start of line # x2, y2: end of line # subroutine(draw_line(node page, float x1, float y1, float x2, float y2), ( #echo("draw_line(); (" . x1 . ", ". y1 . ") to (" . x2 . ", " . y2 . ")"); node contents = @page{"contents"}; @contents{"stream"} .= " " . format_for_pdf(x1, '%0.6f') . " " . format_for_pdf(y1, '%0.6f') . " m\n"; @contents{"stream"} .= " " . format_for_pdf(x2, '%0.6f') . " " . format_for_pdf(y2, '%0.6f') . " l\n"; @contents{"stream"} .= " S\n"; )); # draw_line # # draw_rectangle() # # Purpose: This draws a rectangle on a page # # Parameters: page: the page to draw on # xmin, ymin: the lower left corner of the rectangle # width, height: the width and height of the rectangle # filled: true if it should be filled; false for an outline # subroutine(draw_rectangle(node page, float xmin, float ymin, float width, float height, bool filled), ( node contents = @page{"contents"}; @contents{"stream"} .= " " . format_for_pdf(xmin, '%0.6f') . " " . format_for_pdf(ymin, '%0.6f') . " " . format_for_pdf(width, '%0.6f') . " " . format_for_pdf(height, '%0.6f') . " re\n"; if (filled) then @contents{"stream"} .= " f\n"; else @contents{"stream"} .= " s\n"; )); # draw_rectangle() # # draw_circle() # # Purpose: This draws a circle on a page # # Parameters: page: the page to draw on # x, y: the position of the center of the circle # radius: the radius of the circle # filled: true if it should be filled; false for an outline # subroutine(draw_circle(node page, float x, float y, float radius, bool filled), ( node contents = @page{"contents"}; @contents{"stream"} .= " q\n"; @contents{"stream"} .= " 0.01 w\n"; # @contents{"stream"} .= x . ' ' . y . ' Td\n'; @contents{"stream"} .= " " . format_for_pdf(radius, '%0.6f') . " 0 0 " . format_for_pdf(radius, '%0.6f') . " " . format_for_pdf(x, '%0.6f') . " " . format_for_pdf(y, '0.6f') . " cm\n"; @contents{"stream"} .= ` 0.9992 0.4992 m 0.9992 0.7752 0.7752 0.9992 0.4992 0.9992 c 0.2232 0.9992 -0.0008 0.7752 -0.0008 0.4992 c -0.0008 0.2232 0.2232 -0.0008 0.4992 -0.0008 c 0.7752 -0.0008 0.9992 0.2232 0.9992 0.4992 c `; if (filled) then @contents{"stream"} .= ' B\n'; else @contents{"stream"} .= ' S\n'; @contents{"stream"} .= " Q\n"; )); # draw_circle() # # draw_polygon() # # Purpose: This draws a polygon on a page # # Parameters: page: the page to draw on # vertices: the vertices (a node with one subnode per vertex, with x,y in each subnode) # filled: true if it should be filled; false for an outline # subroutine(draw_polygon(node page, node vertices, bool filled), ( #subroutine(draw_polygon(node page, bool filled), ( node contents = @page{"contents"}; node vertex; bool firstVertex = true; foreach vertex vertices ( float x = @vertex{"x"}; float y = @vertex{"y"}; if (firstVertex) then ( @contents{"stream"} .= ' ' . format_for_pdf(x, '%0.6f') . ' ' . format_for_pdf(y, '0.6f') . ' m\n'; firstVertex = false; ); else ( @contents{"stream"} .= ' ' . format_for_pdf(x, '%0.6f') . ' ' . format_for_pdf(y, '0.6f') . ' l\n'; ); ); if (filled) then @contents{"stream"} .= ' f\n'; else @contents{"stream"} .= ' s\n'; )); # draw_rectangle() subroutine(save_graphics_state(node page), ( node contents = @page{"contents"}; @contents{"stream"} .= " q % Save graphics state \n"; )); # save_graphics_stage() subroutine(restore_graphics_state(node page), ( node contents = @page{"contents"}; @contents{"stream"} .= " Q % Restore graphics state \n"; )); # save_graphics_stage() subroutine(set_coordinate_matrix(node page, float a, float b, float c, float d, float e, float f), ( node contents = @page{"contents"}; @contents{"stream"} .= " " . format_for_pdf(a, '%0.6f') . " " . format_for_pdf(b, '%0.6f') . " " . format_for_pdf(c, '%0.6f') . " " . format_for_pdf(d, '%0.6f') . " " . format_for_pdf(e, '%0.6f') . " " . format_for_pdf(f, '%0.6f') . " cm\n"; )); # set_coordinate_matrix() # This adds an entry to the outline subroutine(add_outline_entry(node pdf, string title), ( # Create the outline if it doesn't exist node outlines = v.outlines; # int num_outline_entries = num_subnodes(pdf{"outlines"}); int num_outline_entries = num_subnodes(outlines); node outline_entry = outlines{num_outline_entries}; @outline_entry{"title"} = title; @outline_entry{"page_objnum"} = @pdf{"current_page_objnum"}; #echo("Added outline entry for " . title); )); # add_outline_entry() # # write_objects() # # Purpose: This writes all objects which have been created (with new_obj(pdf)) to the PDF # subroutine(write_objects(node pdf), ( # Iterate through all object pointers saved in pdf{"objects"} node op; foreach op (pdf{"objects"}) ( # Get the object itself node o = @op; # Add it to the PDF text add_obj(pdf, o); ); # foreach )); # write_objects # # write_pdf() # # Purpose: This writes the final PDF # # Parameters: pathname: the pathname to write the PDF to # subroutine(write_pdf(node pdf, string pathname), ( #echo("write_pdf(); pathname=" . pathname); # Add the catalog node catalog = new_obj(pdf); @catalog{"type"} = "dictionary"; @catalog{"/Type"} = "/Catalog"; @catalog{"/Outlines"} = obj_ref(@pdf{"outlines"}); @catalog{"/Pages"} = obj_ref(@pdf{"pages"}); #echo("v.outlines: " . node_as_string(v.outlines)); if (num_subnodes(v.outlines) == 0) then ( @pdf{"outlines"}{"/Count"} = 0; ); else ( @(@pdf{"outlines"}){"/Count"} = num_subnodes(v.outlines); node outline_item; int first_objnum = @pdf{"objnum"}; int last_objnum = @pdf{"objnum"}; int outlinenum = 0; foreach outline_item v.outlines ( #echo("Adding outline: " . node_as_string(outline_item)); node outline_item_obj = new_obj(pdf); @outline_item_obj{"type"} = "dictionary"; @outline_item_obj{"/Parent"} = obj_ref(@pdf{"outlines"}); @outline_item_obj{"/Title"} = "(" . @outline_item{"title"} . ")"; if (num_subnodes(v.outlines) > outlinenum + 1) then @outline_item_obj{"/Next"} = (@outline_item_obj{"objnum"} + 1) . " 0 R"; if (outlinenum > 0) then @outline_item_obj{"/Previous"} = (@outline_item_obj{"objnum"} - 1) . " 0 R"; @outline_item_obj{"/Dest"} = "[" . @outline_item{"page_objnum"} . " 0 R /XYZ null " . v.page_height . " null]"; # @outline_item_obj{"/Dest"} = "[19 0 R /XYZ null 500 null]"; #echo("outline_item_obj: " . node_as_string(outline_item_obj)); # error("OUTLINE NOT IMPLEMENTED"); outlinenum++; ); int last_objnum = @pdf{"objnum"} - 1; @(@pdf{"outlines"}){"/First"} = "" . first_objnum . " 0 R"; @(@pdf{"outlines"}){"/Last"} = "" . last_objnum . " 0 R"; #echo("@pdf{outlines}: " . node_as_string(@pdf{"outlines"})); ); # Add the pages list node pages = @pdf{"pages"}; @pages{"/Kids"} = "["; node pagep; string kids = "["; foreach pagep (pdf{"pageslist"}) ( node page = @pagep; kids .= obj_ref(page); kids .= " "; ); kids .= "]"; @pages{"/Kids"} = kids; @pages{"/Count"} = num_subnodes(pdf{"pageslist"}); # Write all the objects we have created write_objects(pdf); # Remember the offset of the xref table int xrefpos = @pdf{"objectpos"}; # Write the xref table # @pdf{"data"} .= xref(); @pdf{"data"} .= xref(pdf); # Write the trailer @pdf{"data"} .= "\n"; @pdf{"data"} .= "trailer\n"; @pdf{"data"} .= " <<\n"; @pdf{"data"} .= " /Size " . (num_subnodes(pdf{"objects"}) + 1) . "\n"; @pdf{"data"} .= " /Root " . obj_ref(catalog) . "\n"; @pdf{"data"} .= " >>\n"; # Write the position of the xref table @pdf{"data"} .= "startxref\n"; @pdf{"data"} .= xrefpos; # Write the final EOF part @pdf{"data"} .= "\n"; @pdf{"data"} .= "%%EOF\n"; # Write the PDF to a file write_file(pathname, @pdf{"data"}); # echo("Wrote PDF to " . pathname); )); # write_pdf()