1// Verbatim.java - Xalan extensions supporting DocBook verbatim environments 2 3package com.nwalsh.xalan; 4 5import java.util.Hashtable; 6 7import org.xml.sax.Attributes; 8import org.xml.sax.SAXException; 9import org.xml.sax.helpers.AttributesImpl; 10 11import org.w3c.dom.Attr; 12import org.w3c.dom.Document; 13import org.w3c.dom.DocumentFragment; 14import org.w3c.dom.Element; 15import org.w3c.dom.NamedNodeMap; 16import org.w3c.dom.Node; 17import org.w3c.dom.traversal.NodeIterator; 18 19import javax.xml.transform.TransformerException; 20 21import org.apache.xpath.objects.XObject; 22import org.apache.xpath.XPathContext; 23import org.apache.xalan.extensions.ExpressionContext; 24import org.apache.xml.utils.DOMBuilder; 25import javax.xml.parsers.DocumentBuilder; 26import javax.xml.parsers.DocumentBuilderFactory; 27import javax.xml.parsers.ParserConfigurationException; 28import org.apache.xml.utils.QName; 29import org.apache.xml.utils.AttList; 30 31/** 32 * <p>Xalan extensions supporting Tables</p> 33 * 34 * <p>$Id: Table.java,v 1.2 2006/05/15 11:14:03 nwalsh Exp $</p> 35 * 36 * <p>Copyright (C) 2000 Norman Walsh.</p> 37 * 38 * <p>This class provides a 39 * <a href="http://xml.apache.org/xalan-j/">Xalan</a> 40 * implementation of some code to adjust CALS Tables to HTML 41 * Tables.</p> 42 * 43 * <p><b>Column Widths</b></p> 44 * <p>The <tt>adjustColumnWidths</tt> method takes a result tree 45 * fragment (assumed to contain the colgroup of an HTML Table) 46 * and returns the result tree fragment with the column widths 47 * adjusted to HTML terms.</p> 48 * 49 * <p><b>Convert Lengths</b></p> 50 * <p>The <tt>convertLength</tt> method takes a length specification 51 * of the form 9999.99xx (where "xx" is a unit) and returns that length 52 * as an integral number of pixels. For convenience, percentage lengths 53 * are returned unchanged.</p> 54 * <p>The recognized units are: inches (in), centimeters (cm), 55 * millimeters (mm), picas (pc, 1pc=12pt), points (pt), and pixels (px). 56 * A number with no units is assumed to be pixels.</p> 57 * 58 * <p><b>Change Log:</b></p> 59 * <dl> 60 * <dt>1.0</dt> 61 * <dd><p>Initial release.</p></dd> 62 * </dl> 63 * 64 * @author Norman Walsh 65 * <a href="mailto:ndw@nwalsh.com">ndw@nwalsh.com</a> 66 * 67 * @version $Id: Table.java,v 1.2 2006/05/15 11:14:03 nwalsh Exp $ 68 * 69 */ 70public class Table { 71 /** The number of pixels per inch */ 72 private static int pixelsPerInch = 96; 73 74 /** The hash used to associate units with a length in pixels. */ 75 protected static Hashtable unitHash = null; 76 77 /** The FO namespace name. */ 78 protected static String foURI = "http://www.w3.org/1999/XSL/Format"; 79 80 /** 81 * <p>Constructor for Verbatim</p> 82 * 83 * <p>All of the methods are static, so the constructor does nothing.</p> 84 */ 85 public Table() { 86 } 87 88 /** Initialize the internal hash table with proper values. */ 89 protected static void initializeHash() { 90 unitHash = new Hashtable(); 91 unitHash.put("in", new Float(pixelsPerInch)); 92 unitHash.put("cm", new Float(pixelsPerInch / 2.54)); 93 unitHash.put("mm", new Float(pixelsPerInch / 25.4)); 94 unitHash.put("pc", new Float((pixelsPerInch / 72) * 12)); 95 unitHash.put("pt", new Float(pixelsPerInch / 72)); 96 unitHash.put("px", new Float(1)); 97 } 98 99 /** Set the pixels-per-inch value. Only positive values are legal. */ 100 public static void setPixelsPerInch(int value) { 101 if (value > 0) { 102 pixelsPerInch = value; 103 initializeHash(); 104 } 105 } 106 107 /** Return the current pixels-per-inch value. */ 108 public int getPixelsPerInch() { 109 return pixelsPerInch; 110 } 111 112 /** 113 * <p>Convert a length specification to a number of pixels.</p> 114 * 115 * <p>The specified length should be of the form [+/-]999.99xx, 116 * where xx is a valid unit.</p> 117 */ 118 public static int convertLength(String length) { 119 // The format of length should be 999.999xx 120 int sign = 1; 121 String digits = ""; 122 String units = ""; 123 char lench[] = length.toCharArray(); 124 float flength = 0; 125 boolean done = false; 126 int pos = 0; 127 float factor = 1; 128 int pixels = 0; 129 130 if (unitHash == null) { 131 initializeHash(); 132 } 133 134 if (lench[pos] == '+' || lench[pos] == '-') { 135 if (lench[pos] == '-') { 136 sign = -1; 137 } 138 pos++; 139 } 140 141 while (!done) { 142 if (pos >= lench.length) { 143 done = true; 144 } else { 145 if ((lench[pos] > '9' || lench[pos] < '0') && lench[pos] != '.') { 146 done = true; 147 units = length.substring(pos); 148 } else { 149 digits += lench[pos++]; 150 } 151 } 152 } 153 154 try { 155 flength = Float.parseFloat(digits); 156 } catch (NumberFormatException e) { 157 System.out.println(digits + " is not a number; 1 used instead."); 158 flength = 1; 159 } 160 161 Float f = null; 162 163 if (!units.equals("")) { 164 f = (Float) unitHash.get(units); 165 if (f == null) { 166 System.out.println(units + " is not a known unit; 1 used instead."); 167 factor = 1; 168 } else { 169 factor = f.floatValue(); 170 } 171 } else { 172 factor = 1; 173 } 174 175 f = new Float(flength * factor); 176 177 pixels = f.intValue() * sign; 178 179 return pixels; 180 } 181 182 /** 183 * <p>Adjust column widths in an HTML table.</p> 184 * 185 * <p>The specification of column widths in CALS (a relative width 186 * plus an optional absolute width) are incompatible with HTML column 187 * widths. This method adjusts CALS column width specifiers in an 188 * attempt to produce equivalent HTML specifiers.</p> 189 * 190 * <p>In order for this method to work, the CALS width specifications 191 * should be placed in the "width" attribute of the <col>s within 192 * a <colgroup>. Then the colgroup result tree fragment is passed 193 * to this method.</p> 194 * 195 * <p>This method makes use of two parameters from the XSL stylesheet 196 * that calls it: <code>nominal.table.width</code> and 197 * <code>table.width</code>. The value of <code>nominal.table.width</code> 198 * must be an absolute distance. The value of <code>table.width</code> 199 * can be either absolute or relative.</p> 200 * 201 * <p>Presented with a mixture of relative and 202 * absolute lengths, the table width is used to calculate 203 * appropriate values. If the <code>table.width</code> is relative, 204 * the nominal width is used for this calculation.</p> 205 * 206 * <p>There are three possible combinations of values:</p> 207 * 208 * <ol> 209 * <li>There are no relative widths; in this case the absolute widths 210 * are used in the HTML table.</li> 211 * <li>There are no absolute widths; in this case the relative widths 212 * are used in the HTML table.</li> 213 * <li>There are a mixture of absolute and relative widths: 214 * <ol> 215 * <li>If the table width is absolute, all widths become absolute.</li> 216 * <li>If the table width is relative, make all the widths absolute 217 * relative to the nominal table width then turn them all 218 * back into relative widths.</li> 219 * </ol> 220 * </li> 221 * </ol> 222 * 223 * @param context The stylesheet context; supplied automatically by Xalan 224 * @param xalanNI 225 * 226 * @return The result tree fragment containing the adjusted colgroup. 227 * 228 */ 229 230 public DocumentFragment adjustColumnWidths (ExpressionContext context, 231 NodeIterator xalanNI) { 232 233 int nominalWidth = convertLength(Params.getString(context, 234 "nominal.table.width")); 235 String tableWidth = Params.getString(context, "table.width"); 236 String styleType = Params.getString(context, "stylesheet.result.type"); 237 boolean foStylesheet = styleType.equals("fo"); 238 239 DocumentFragment xalanRTF = (DocumentFragment) xalanNI.nextNode(); 240 Element colgroup = (Element) xalanRTF.getFirstChild(); 241 242 // N.B. ...stree.ElementImpl doesn't implement getElementsByTagName() 243 244 Node firstCol = null; 245 // If this is an FO tree, there might be no colgroup... 246 if (colgroup.getLocalName().equals("colgroup")) { 247 firstCol = colgroup.getFirstChild(); 248 } else { 249 firstCol = colgroup; 250 } 251 252 // Count the number of columns... 253 Node child = firstCol; 254 int numColumns = 0; 255 while (child != null) { 256 if (child.getNodeType() == Node.ELEMENT_NODE 257 && (child.getNodeName().equals("col") 258 || (child.getNamespaceURI().equals(foURI) 259 && child.getLocalName().equals("table-column")))) { 260 numColumns++; 261 } 262 263 child = child.getNextSibling(); 264 } 265 266 String widths[] = new String[numColumns]; 267 Element columns[] = new Element[numColumns]; 268 int colnum = 0; 269 270 child = firstCol; 271 while (child != null) { 272 if (child.getNodeType() == Node.ELEMENT_NODE 273 && (child.getNodeName().equals("col") 274 || (child.getNamespaceURI().equals(foURI) 275 && child.getLocalName().equals("table-column")))) { 276 Element col = (Element) child; 277 278 columns[colnum] = col; 279 280 if (foStylesheet) { 281 if ("".equals(col.getAttribute("column-width"))) { 282 widths[colnum] = "1*"; 283 } else { 284 widths[colnum] = col.getAttribute("column-width"); 285 } 286 } else { 287 if ("".equals(col.getAttribute("width"))) { 288 widths[colnum] = "1*"; 289 } else { 290 widths[colnum] = col.getAttribute("width"); 291 } 292 } 293 294 colnum++; 295 } 296 child = child.getNextSibling(); 297 } 298 299 float relTotal = 0; 300 float relParts[] = new float[numColumns]; 301 302 float absTotal = 0; 303 float absParts[] = new float[numColumns]; 304 305 for (int count = 0; count < numColumns; count++) { 306 String width = widths[count]; 307 int pos = width.indexOf("*"); 308 if (pos >= 0) { 309 String relPart = width.substring(0, pos); 310 String absPart = width.substring(pos+1); 311 312 try { 313 float rel = Float.parseFloat(relPart); 314 relTotal += rel; 315 relParts[count] = rel; 316 } catch (NumberFormatException e) { 317 System.out.println(relPart + " is not a valid relative unit."); 318 } 319 320 int pixels = 0; 321 if (absPart != null && !absPart.equals("")) { 322 pixels = convertLength(absPart); 323 } 324 325 absTotal += pixels; 326 absParts[count] = pixels; 327 } else { 328 relParts[count] = 0; 329 330 int pixels = 0; 331 if (width != null && !width.equals("")) { 332 pixels = convertLength(width); 333 } 334 335 absTotal += pixels; 336 absParts[count] = pixels; 337 } 338 } 339 340 // Ok, now we have the relative widths and absolute widths in 341 // two parallel arrays. 342 // 343 // - If there are no relative widths, output the absolute widths 344 // - If there are no absolute widths, output the relative widths 345 // - If there are a mixture of relative and absolute widths, 346 // - If the table width is absolute, turn these all into absolute 347 // widths. 348 // - If the table width is relative, turn these all into absolute 349 // widths in the nominalWidth and then turn them back into 350 // percentages. 351 352 if (relTotal == 0) { 353 for (int count = 0; count < numColumns; count++) { 354 Float f = new Float(absParts[count]); 355 if (foStylesheet) { 356 int pixels = f.intValue(); 357 float inches = (float) pixels / pixelsPerInch; 358 widths[count] = inches + "in"; 359 } else { 360 widths[count] = Integer.toString(f.intValue()); 361 } 362 } 363 } else if (absTotal == 0) { 364 for (int count = 0; count < numColumns; count++) { 365 float rel = relParts[count] / relTotal * 100; 366 Float f = new Float(rel); 367 widths[count] = Integer.toString(f.intValue()); 368 } 369 widths = correctRoundingError(widths); 370 } else { 371 int pixelWidth = nominalWidth; 372 373 if (tableWidth.indexOf("%") <= 0) { 374 pixelWidth = convertLength(tableWidth); 375 } 376 377 if (pixelWidth <= absTotal) { 378 System.out.println("Table is wider than table width."); 379 } else { 380 pixelWidth -= absTotal; 381 } 382 383 absTotal = 0; 384 for (int count = 0; count < numColumns; count++) { 385 float rel = relParts[count] / relTotal * pixelWidth; 386 relParts[count] = rel + absParts[count]; 387 absTotal += rel + absParts[count]; 388 } 389 390 if (tableWidth.indexOf("%") <= 0) { 391 for (int count = 0; count < numColumns; count++) { 392 Float f = new Float(relParts[count]); 393 if (foStylesheet) { 394 int pixels = f.intValue(); 395 float inches = (float) pixels / pixelsPerInch; 396 widths[count] = inches + "in"; 397 } else { 398 widths[count] = Integer.toString(f.intValue()); 399 } 400 } 401 } else { 402 for (int count = 0; count < numColumns; count++) { 403 float rel = relParts[count] / absTotal * 100; 404 Float f = new Float(rel); 405 widths[count] = Integer.toString(f.intValue()); 406 } 407 widths = correctRoundingError(widths); 408 } 409 } 410 411 // Now rebuild the colgroup with the right widths 412 413 DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); 414 DocumentBuilder docBuilder = null; 415 416 try { 417 docBuilder = docFactory.newDocumentBuilder(); 418 } catch (ParserConfigurationException e) { 419 System.out.println("PCE!"); 420 return xalanRTF; 421 } 422 Document doc = docBuilder.newDocument(); 423 DocumentFragment df = doc.createDocumentFragment(); 424 DOMBuilder rtf = new DOMBuilder(doc, df); 425 426 try { 427 String ns = colgroup.getNamespaceURI(); 428 String localName = colgroup.getLocalName(); 429 String name = colgroup.getTagName(); 430 431 if (colgroup.getLocalName().equals("colgroup")) { 432 rtf.startElement(ns, localName, name, 433 copyAttributes(colgroup)); 434 } 435 436 for (colnum = 0; colnum < numColumns; colnum++) { 437 Element col = columns[colnum]; 438 439 NamedNodeMap domAttr = col.getAttributes(); 440 441 AttributesImpl attr = new AttributesImpl(); 442 for (int acount = 0; acount < domAttr.getLength(); acount++) { 443 Node a = domAttr.item(acount); 444 String a_ns = a.getNamespaceURI(); 445 String a_localName = a.getLocalName(); 446 447 if ((foStylesheet && !a_localName.equals("column-width")) 448 || !a_localName.equalsIgnoreCase("width")) { 449 attr.addAttribute(a.getNamespaceURI(), 450 a.getLocalName(), 451 a.getNodeName(), 452 "CDATA", 453 a.getNodeValue()); 454 } 455 } 456 457 if (foStylesheet) { 458 attr.addAttribute("", "column-width", "column-width", "CDATA", widths[colnum]); 459 } else { 460 attr.addAttribute("", "width", "width", "CDATA", widths[colnum]); 461 } 462 463 rtf.startElement(col.getNamespaceURI(), 464 col.getLocalName(), 465 col.getTagName(), 466 attr); 467 rtf.endElement(col.getNamespaceURI(), 468 col.getLocalName(), 469 col.getTagName()); 470 } 471 472 if (colgroup.getLocalName().equals("colgroup")) { 473 rtf.endElement(ns, localName, name); 474 } 475 } catch (SAXException se) { 476 System.out.println("SE!"); 477 return xalanRTF; 478 } 479 480 return df; 481 } 482 483 private Attributes copyAttributes(Element node) { 484 AttributesImpl attrs = new AttributesImpl(); 485 NamedNodeMap nnm = node.getAttributes(); 486 for (int count = 0; count < nnm.getLength(); count++) { 487 Attr attr = (Attr) nnm.item(count); 488 String name = attr.getName(); 489 if (name.startsWith("xmlns:") || name.equals("xmlns")) { 490 // Skip it; (don't ya just love it!!) 491 } else { 492 attrs.addAttribute(attr.getNamespaceURI(), attr.getName(), 493 attr.getName(), "CDATA", attr.getValue()); 494 } 495 } 496 return attrs; 497 } 498 499 /** 500 * Correct rounding errors introduced in calculating the width of each 501 * column. Make sure they sum to 100% in the end. 502 */ 503 protected String[] correctRoundingError(String widths[]) { 504 int totalWidth = 0; 505 506 for (int count = 0; count < widths.length; count++) { 507 try { 508 int width = Integer.parseInt(widths[count]); 509 totalWidth += width; 510 } catch (NumberFormatException nfe) { 511 // nop; "can't happen" 512 } 513 } 514 515 float totalError = 100 - totalWidth; 516 float columnError = totalError / widths.length; 517 float error = 0; 518 519 for (int count = 0; count < widths.length; count++) { 520 try { 521 int width = Integer.parseInt(widths[count]); 522 error = error + columnError; 523 if (error >= 1.0) { 524 int adj = (int) Math.round(Math.floor(error)); 525 error = error - (float) Math.floor(error); 526 width = width + adj; 527 widths[count] = Integer.toString(width) + "%"; 528 } else { 529 widths[count] = Integer.toString(width) + "%"; 530 } 531 } catch (NumberFormatException nfe) { 532 // nop; "can't happen" 533 } 534 } 535 536 return widths; 537 } 538} 539