1// Verbatim.java - Saxon extensions supporting DocBook verbatim environments 2 3package com.nwalsh.saxon; 4 5import java.util.Hashtable; 6import org.xml.sax.*; 7import org.w3c.dom.*; 8import javax.xml.transform.TransformerException; 9import com.icl.saxon.Controller; 10import com.icl.saxon.expr.*; 11import com.icl.saxon.om.*; 12import com.icl.saxon.pattern.*; 13import com.icl.saxon.Context; 14import com.icl.saxon.tree.*; 15import com.icl.saxon.functions.Extensions; 16 17/** 18 * <p>Saxon extensions supporting Tables</p> 19 * 20 * <p>$Id: Table.java,v 1.3 2006/04/27 08:26:47 xmldoc Exp $</p> 21 * 22 * <p>Copyright (C) 2000 Norman Walsh.</p> 23 * 24 * <p>This class provides a 25 * <a href="http://saxon.sourceforge.net/">Saxon</a> 26 * implementation of some code to adjust CALS Tables to HTML 27 * Tables.</p> 28 * 29 * <p><b>Column Widths</b></p> 30 * <p>The <tt>adjustColumnWidths</tt> method takes a result tree 31 * fragment (assumed to contain the colgroup of an HTML Table) 32 * and returns the result tree fragment with the column widths 33 * adjusted to HTML terms.</p> 34 * 35 * <p><b>Convert Lengths</b></p> 36 * <p>The <tt>convertLength</tt> method takes a length specification 37 * of the form 9999.99xx (where "xx" is a unit) and returns that length 38 * as an integral number of pixels. For convenience, percentage lengths 39 * are returned unchanged.</p> 40 * <p>The recognized units are: inches (in), centimeters (cm), 41 * millimeters (mm), picas (pc, 1pc=12pt), points (pt), and pixels (px). 42 * A number with no units is assumed to be pixels.</p> 43 * 44 * <p><b>Change Log:</b></p> 45 * <dl> 46 * <dt>1.0</dt> 47 * <dd><p>Initial release.</p></dd> 48 * </dl> 49 * 50 * @author Norman Walsh 51 * <a href="mailto:ndw@nwalsh.com">ndw@nwalsh.com</a> 52 * 53 * @version $Id: Table.java,v 1.3 2006/04/27 08:26:47 xmldoc Exp $ 54 * 55 */ 56public class Table { 57 /** The number of pixels per inch */ 58 private static int pixelsPerInch = 96; 59 60 /** The nominal table width (6in by default). */ 61 private static int nominalWidth = 6 * pixelsPerInch; 62 63 /** The default table width (100% by default). */ 64 private static String tableWidth = "100%"; 65 66 /** Is this an FO stylesheet? */ 67 private static boolean foStylesheet = false; 68 69 /** The hash used to associate units with a length in pixels. */ 70 protected static Hashtable unitHash = null; 71 72 /** 73 * <p>Constructor for Verbatim</p> 74 * 75 * <p>All of the methods are static, so the constructor does nothing.</p> 76 */ 77 public Table() { 78 } 79 80 /** Initialize the internal hash table with proper values. */ 81 protected static void initializeHash() { 82 unitHash = new Hashtable(); 83 unitHash.put("in", new Float(pixelsPerInch)); 84 unitHash.put("cm", new Float(pixelsPerInch / 2.54)); 85 unitHash.put("mm", new Float(pixelsPerInch / 25.4)); 86 unitHash.put("pc", new Float((pixelsPerInch / 72) * 12)); 87 unitHash.put("pt", new Float(pixelsPerInch / 72)); 88 unitHash.put("px", new Float(1)); 89 } 90 91 /** Set the pixels-per-inch value. Only positive values are legal. */ 92 public static void setPixelsPerInch(int value) { 93 if (value > 0) { 94 pixelsPerInch = value; 95 initializeHash(); 96 } 97 } 98 99 /** Return the current pixels-per-inch value. */ 100 public int getPixelsPerInch() { 101 return pixelsPerInch; 102 } 103 104 /** 105 * <p>Convert a length specification to a number of pixels.</p> 106 * 107 * <p>The specified length should be of the form [+/-]999.99xx, 108 * where xx is a valid unit.</p> 109 */ 110 public static int convertLength(String length) { 111 // The format of length should be 999.999xx 112 int sign = 1; 113 String digits = ""; 114 String units = ""; 115 char lench[] = length.toCharArray(); 116 float flength = 0; 117 boolean done = false; 118 int pos = 0; 119 float factor = 1; 120 int pixels = 0; 121 122 if (unitHash == null) { 123 initializeHash(); 124 } 125 126 if (lench[pos] == '+' || lench[pos] == '-') { 127 if (lench[pos] == '-') { 128 sign = -1; 129 } 130 pos++; 131 } 132 133 while (!done) { 134 if (pos >= lench.length) { 135 done = true; 136 } else { 137 if ((lench[pos] > '9' || lench[pos] < '0') && lench[pos] != '.') { 138 done = true; 139 units = length.substring(pos); 140 } else { 141 digits += lench[pos++]; 142 } 143 } 144 } 145 146 try { 147 flength = Float.parseFloat(digits); 148 } catch (NumberFormatException e) { 149 System.out.println(digits + " is not a number; 1 used instead."); 150 flength = 1; 151 } 152 153 Float f = null; 154 155 if (!units.equals("")) { 156 f = (Float) unitHash.get(units); 157 if (f == null) { 158 System.out.println(units + " is not a known unit; 1 used instead."); 159 factor = 1; 160 } else { 161 factor = f.floatValue(); 162 } 163 } else { 164 factor = 1; 165 } 166 167 f = new Float(flength * factor); 168 169 pixels = f.intValue() * sign; 170 171 return pixels; 172 } 173 174 /** 175 * <p>Find the string value of a stylesheet variable or parameter</p> 176 * 177 * <p>Returns the string value of <code>varName</code> in the current 178 * <code>context</code>. Returns the empty string if the variable is 179 * not defined.</p> 180 * 181 * @param context The current stylesheet context 182 * @param varName The name of the variable (without the dollar sign) 183 * 184 * @return The string value of the variable 185 */ 186 protected static String getVariable(Context context, String varName) 187 throws TransformerException { 188 Value variable = null; 189 String varString = null; 190 191 try { 192 variable = Extensions.evaluate(context, "$" + varName); 193 varString = variable.asString(); 194 return varString; 195 } catch (IllegalArgumentException e) { 196 System.out.println("Undefined variable: " + varName); 197 return ""; 198 } 199 } 200 201 /** 202 * <p>Setup the parameters associated with column width calculations</p> 203 * 204 * <p>This method queries the stylesheet for the variables 205 * associated with table column widths. It is called automatically before 206 * column widths are adjusted. The context is used to retrieve the values, 207 * this allows templates to redefine these variables.</p> 208 * 209 * <p>The following variables are queried. If the variables do not 210 * exist, builtin defaults will be used (but you may also get a bunch 211 * of messages from the Java interpreter).</p> 212 * 213 * <dl> 214 * <dt><code>nominal.table.width</code></dt> 215 * <dd>The "normal" width for tables. This must be an absolute length.</dd> 216 * <dt><code>table.width</code></dt> 217 * <dd>The width for tables. This may be either an absolute 218 * length or a percentage.</dd> 219 * </dl> 220 * 221 * @param context The current stylesheet context 222 * 223 */ 224 private static void setupColumnWidths(Context context) { 225 // Hardcoded defaults 226 nominalWidth = 6 * pixelsPerInch; 227 tableWidth = "100%"; 228 229 String varString = null; 230 231 try { 232 // Get the stylesheet type 233 varString = getVariable(context, "stylesheet.result.type"); 234 foStylesheet = varString.equals("fo"); 235 236 // Get the nominal table width 237 varString = getVariable(context, "nominal.table.width"); 238 nominalWidth = convertLength(varString); 239 240 // Get the table width 241 varString = getVariable(context, "table.width"); 242 tableWidth = varString; 243 } catch (TransformerException e) { 244 //nop, can't happen 245 } 246 } 247 248 /** 249 * <p>Adjust column widths in an HTML table.</p> 250 * 251 * <p>The specification of column widths in CALS (a relative width 252 * plus an optional absolute width) are incompatible with HTML column 253 * widths. This method adjusts CALS column width specifiers in an 254 * attempt to produce equivalent HTML specifiers.</p> 255 * 256 * <p>In order for this method to work, the CALS width specifications 257 * should be placed in the "width" attribute of the <col>s within 258 * a <colgroup>. Then the colgroup result tree fragment is passed 259 * to this method.</p> 260 * 261 * <p>This method makes use of two parameters from the XSL stylesheet 262 * that calls it: <code>nominal.table.width</code> and 263 * <code>table.width</code>. The value of <code>nominal.table.width</code> 264 * must be an absolute distance. The value of <code>table.width</code> 265 * can be either absolute or relative.</p> 266 * 267 * <p>Presented with a mixture of relative and 268 * absolute lengths, the table width is used to calculate 269 * appropriate values. If the <code>table.width</code> is relative, 270 * the nominal width is used for this calculation.</p> 271 * 272 * <p>There are three possible combinations of values:</p> 273 * 274 * <ol> 275 * <li>There are no relative widths; in this case the absolute widths 276 * are used in the HTML table.</li> 277 * <li>There are no absolute widths; in this case the relative widths 278 * are used in the HTML table.</li> 279 * <li>There are a mixture of absolute and relative widths: 280 * <ol> 281 * <li>If the table width is absolute, all widths become absolute.</li> 282 * <li>If the table width is relative, make all the widths absolute 283 * relative to the nominal table width then turn them all 284 * back into relative widths.</li> 285 * </ol> 286 * </li> 287 * </ol> 288 * 289 * @param context The stylesheet context; supplied automatically by Saxon 290 * @param rtf_ns The result tree fragment containing the colgroup. 291 * 292 * @return The result tree fragment containing the adjusted colgroup. 293 * 294 */ 295 public static NodeSetValue adjustColumnWidths (Context context, 296 NodeSetValue rtf_ns) { 297 298 FragmentValue rtf = (FragmentValue) rtf_ns; 299 300 setupColumnWidths(context); 301 302 try { 303 Controller controller = context.getController(); 304 NamePool namePool = controller.getNamePool(); 305 306 ColumnScanEmitter csEmitter = new ColumnScanEmitter(namePool); 307 rtf.replay(csEmitter); 308 309 int numColumns = csEmitter.columnCount(); 310 String widths[] = csEmitter.columnWidths(); 311 312 float relTotal = 0; 313 float relParts[] = new float[numColumns]; 314 315 float absTotal = 0; 316 float absParts[] = new float[numColumns]; 317 318 for (int count = 0; count < numColumns; count++) { 319 String width = widths[count]; 320 321 int pos = width.indexOf("*"); 322 if (pos >= 0) { 323 String relPart = width.substring(0, pos); 324 String absPart = width.substring(pos+1); 325 326 try { 327 float rel = Float.parseFloat(relPart); 328 relTotal += rel; 329 relParts[count] = rel; 330 } catch (NumberFormatException e) { 331 System.out.println(relPart + " is not a valid relative unit."); 332 } 333 334 int pixels = 0; 335 if (absPart != null && !absPart.equals("")) { 336 pixels = convertLength(absPart); 337 } 338 339 absTotal += pixels; 340 absParts[count] = pixels; 341 } else { 342 relParts[count] = 0; 343 344 int pixels = 0; 345 if (width != null && !width.equals("")) { 346 pixels = convertLength(width); 347 } 348 349 absTotal += pixels; 350 absParts[count] = pixels; 351 } 352 } 353 354 // Ok, now we have the relative widths and absolute widths in 355 // two parallel arrays. 356 // 357 // - If there are no relative widths, output the absolute widths 358 // - If there are no absolute widths, output the relative widths 359 // - If there are a mixture of relative and absolute widths, 360 // - If the table width is absolute, turn these all into absolute 361 // widths. 362 // - If the table width is relative, turn these all into absolute 363 // widths in the nominalWidth and then turn them back into 364 // percentages. 365 366 if (relTotal == 0) { 367 for (int count = 0; count < numColumns; count++) { 368 Float f = new Float(absParts[count]); 369 if (foStylesheet) { 370 int pixels = f.intValue(); 371 float inches = (float) pixels / pixelsPerInch; 372 widths[count] = inches + "in"; 373 } else { 374 widths[count] = Integer.toString(f.intValue()); 375 } 376 } 377 } else if (absTotal == 0) { 378 for (int count = 0; count < numColumns; count++) { 379 float rel = relParts[count] / relTotal * 100; 380 Float f = new Float(rel); 381 widths[count] = Integer.toString(f.intValue()); 382 } 383 widths = correctRoundingError(widths); 384 } else { 385 int pixelWidth = nominalWidth; 386 387 if (tableWidth.indexOf("%") <= 0) { 388 pixelWidth = convertLength(tableWidth); 389 } 390 391 if (pixelWidth <= absTotal) { 392 System.out.println("Table is wider than table width."); 393 } else { 394 pixelWidth -= absTotal; 395 } 396 397 absTotal = 0; 398 for (int count = 0; count < numColumns; count++) { 399 float rel = relParts[count] / relTotal * pixelWidth; 400 relParts[count] = rel + absParts[count]; 401 absTotal += rel + absParts[count]; 402 } 403 404 if (tableWidth.indexOf("%") <= 0) { 405 for (int count = 0; count < numColumns; count++) { 406 Float f = new Float(relParts[count]); 407 if (foStylesheet) { 408 int pixels = f.intValue(); 409 float inches = (float) pixels / pixelsPerInch; 410 widths[count] = inches + "in"; 411 } else { 412 widths[count] = Integer.toString(f.intValue()); 413 } 414 } 415 } else { 416 for (int count = 0; count < numColumns; count++) { 417 float rel = relParts[count] / absTotal * 100; 418 Float f = new Float(rel); 419 widths[count] = Integer.toString(f.intValue()); 420 } 421 widths = correctRoundingError(widths); 422 } 423 } 424 425 ColumnUpdateEmitter cuEmitter = new ColumnUpdateEmitter(controller, 426 namePool, 427 widths); 428 429 rtf.replay(cuEmitter); 430 return cuEmitter.getResultTreeFragment(); 431 } catch (TransformerException e) { 432 // This "can't" happen. 433 System.out.println("Transformer Exception in adjustColumnWidths"); 434 return rtf; 435 } 436 } 437 438 /** 439 * Correct rounding errors introduced in calculating the width of each 440 * column. Make sure they sum to 100% in the end. 441 */ 442 protected static String[] correctRoundingError(String widths[]) { 443 int totalWidth = 0; 444 445 for (int count = 0; count < widths.length; count++) { 446 try { 447 int width = Integer.parseInt(widths[count]); 448 totalWidth += width; 449 } catch (NumberFormatException nfe) { 450 // nop; "can't happen" 451 } 452 } 453 454 float totalError = 100 - totalWidth; 455 float columnError = totalError / widths.length; 456 float error = 0; 457 458 for (int count = 0; count < widths.length; count++) { 459 try { 460 int width = Integer.parseInt(widths[count]); 461 error = error + columnError; 462 if (error >= 1.0) { 463 int adj = (int) Math.round(Math.floor(error)); 464 error = error - (float) Math.floor(error); 465 width = width + adj; 466 widths[count] = Integer.toString(width) + "%"; 467 } else { 468 widths[count] = Integer.toString(width) + "%"; 469 } 470 } catch (NumberFormatException nfe) { 471 // nop; "can't happen" 472 } 473 } 474 475 return widths; 476 } 477} 478