J. Duke | 319a3b9 | 2007-12-01 00:00:00 +0000 | [diff] [blame^] | 1 | /* |
| 2 | * Copyright 2003-2006 Sun Microsystems, Inc. All Rights Reserved. |
| 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| 4 | * |
| 5 | * This code is free software; you can redistribute it and/or modify it |
| 6 | * under the terms of the GNU General Public License version 2 only, as |
| 7 | * published by the Free Software Foundation. Sun designates this |
| 8 | * particular file as subject to the "Classpath" exception as provided |
| 9 | * by Sun in the LICENSE file that accompanied this code. |
| 10 | * |
| 11 | * This code is distributed in the hope that it will be useful, but WITHOUT |
| 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 14 | * version 2 for more details (a copy is included in the LICENSE file that |
| 15 | * accompanied this code). |
| 16 | * |
| 17 | * You should have received a copy of the GNU General Public License version |
| 18 | * 2 along with this work; if not, write to the Free Software Foundation, |
| 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | * |
| 21 | * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, |
| 22 | * CA 95054 USA or visit www.sun.com if you need additional information or |
| 23 | * have any questions. |
| 24 | */ |
| 25 | |
| 26 | package sun.font; |
| 27 | |
| 28 | import java.awt.Font; |
| 29 | import java.awt.font.FontRenderContext; |
| 30 | import java.awt.geom.AffineTransform; |
| 31 | import java.lang.ref.Reference; |
| 32 | import java.lang.ref.SoftReference; |
| 33 | import java.util.concurrent.ConcurrentHashMap; |
| 34 | import java.util.Locale; |
| 35 | |
| 36 | public abstract class Font2D { |
| 37 | |
| 38 | /* Note: JRE and FONT_CONFIG ranks are identical. I don't know of a reason |
| 39 | * to distingish these. Possibly if a user adds fonts to the JRE font |
| 40 | * directory that are the same font as the ones specified in the font |
| 41 | * configuration but that is more likely to be the legitimate intention |
| 42 | * than a problem. One reason why these should be the same is that on |
| 43 | * Linux the JRE fonts ARE the font configuration fonts, and although I |
| 44 | * believe all are assigned FONT_CONFIG rank, it is conceivable that if |
| 45 | * this were not so, that some JRE font would not be allowed to joint the |
| 46 | * family of its siblings which were assigned FONT_CONFIG rank. Giving |
| 47 | * them the same rank is the easy solution for now at least. |
| 48 | */ |
| 49 | public static final int FONT_CONFIG_RANK = 2; |
| 50 | public static final int JRE_RANK = 2; |
| 51 | public static final int TTF_RANK = 3; |
| 52 | public static final int TYPE1_RANK = 4; |
| 53 | public static final int NATIVE_RANK = 5; |
| 54 | public static final int UNKNOWN_RANK = 6; |
| 55 | public static final int DEFAULT_RANK = 4; |
| 56 | |
| 57 | private static final String[] boldNames = { |
| 58 | "bold", "demibold", "demi-bold", "demi bold", "negreta", "demi", }; |
| 59 | |
| 60 | private static final String[] italicNames = { |
| 61 | "italic", "cursiva", "oblique", "inclined", }; |
| 62 | |
| 63 | private static final String[] boldItalicNames = { |
| 64 | "bolditalic", "bold-italic", "bold italic", |
| 65 | "boldoblique", "bold-oblique", "bold oblique", |
| 66 | "demibold italic", "negreta cursiva","demi oblique", }; |
| 67 | |
| 68 | private static final FontRenderContext DEFAULT_FRC = |
| 69 | new FontRenderContext(null, false, false); |
| 70 | |
| 71 | public Font2DHandle handle; |
| 72 | protected String familyName; /* Family font name (english) */ |
| 73 | protected String fullName; /* Full font name (english) */ |
| 74 | protected int style = Font.PLAIN; |
| 75 | protected FontFamily family; |
| 76 | protected int fontRank = DEFAULT_RANK; |
| 77 | |
| 78 | /* |
| 79 | * A mapper can be independent of the strike. |
| 80 | * Perhaps the reference to the mapper ought to be held on the |
| 81 | * scaler, as it may be implemented via scaler functionality anyway |
| 82 | * and so the mapper would be useless if its native portion was |
| 83 | * freed when the scaler was GC'd. |
| 84 | */ |
| 85 | protected CharToGlyphMapper mapper; |
| 86 | |
| 87 | /* |
| 88 | * The strike cache is maintained per "Font2D" as that is the |
| 89 | * principal object by which you look up fonts. |
| 90 | * It means more Hashmaps, but look ups can be quicker because |
| 91 | * the map will have fewer entries, and there's no need to try to |
| 92 | * make the Font2D part of the key. |
| 93 | */ |
| 94 | protected ConcurrentHashMap<FontStrikeDesc, Reference> |
| 95 | strikeCache = new ConcurrentHashMap<FontStrikeDesc, Reference>(); |
| 96 | |
| 97 | /* Store the last Strike in a Reference object. |
| 98 | * Similarly to the strike that was stored on a C++ font object, |
| 99 | * this is an optimisation which helps if multiple clients (ie |
| 100 | * typically SunGraphics2D instances) are using the same font, then |
| 101 | * as may be typical of many UIs, they are probably using it in the |
| 102 | * same style, so it can be a win to first quickly check if the last |
| 103 | * strike obtained from this Font2D satifies the needs of the next |
| 104 | * client too. |
| 105 | * This pre-supposes that a FontStrike is a shareable object, which |
| 106 | * it should. |
| 107 | */ |
| 108 | protected Reference lastFontStrike = new SoftReference(null); |
| 109 | |
| 110 | /* |
| 111 | * POSSIBLE OPTIMISATION: |
| 112 | * Array of length 1024 elements of 64 bits indicating if a font |
| 113 | * contains these. This kind of information can be shared between |
| 114 | * all point sizes. |
| 115 | * if corresponding bit in knownBitmaskMap is set then canDisplayBitmaskMap |
| 116 | * is valid. This is 16Kbytes of data per composite font style. |
| 117 | * What about UTF-32 and surrogates? |
| 118 | * REMIND: This is too much storage. Probably can only cache this |
| 119 | * information for latin range, although possibly OK to store all |
| 120 | * for just the "logical" fonts. |
| 121 | * Or instead store arrays of subranges of 1024 bits (128 bytes) in |
| 122 | * the range below surrogate pairs. |
| 123 | */ |
| 124 | // protected long[] knownBitmaskMap; |
| 125 | // protected long[] canDisplayBitmaskMap; |
| 126 | |
| 127 | /* Returns the "real" style of this Font2D. Eg the font face |
| 128 | * Lucida Sans Bold" has a real style of Font.BOLD, even though |
| 129 | * it may be able to used to simulate bold italic |
| 130 | */ |
| 131 | public int getStyle() { |
| 132 | return style; |
| 133 | } |
| 134 | protected void setStyle() { |
| 135 | |
| 136 | String fName = fullName.toLowerCase(); |
| 137 | |
| 138 | for (int i=0; i < boldItalicNames.length; i++) { |
| 139 | if (fName.indexOf(boldItalicNames[i]) != -1) { |
| 140 | style = Font.BOLD|Font.ITALIC; |
| 141 | return; |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | for (int i=0; i < italicNames.length; i++) { |
| 146 | if (fName.indexOf(italicNames[i]) != -1) { |
| 147 | style = Font.ITALIC; |
| 148 | return; |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | for (int i=0; i < boldNames.length; i++) { |
| 153 | if (fName.indexOf(boldNames[i]) != -1 ) { |
| 154 | style = Font.BOLD; |
| 155 | return; |
| 156 | } |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | |
| 161 | int getRank() { |
| 162 | return fontRank; |
| 163 | } |
| 164 | |
| 165 | void setRank(int rank) { |
| 166 | fontRank = rank; |
| 167 | } |
| 168 | |
| 169 | abstract CharToGlyphMapper getMapper(); |
| 170 | |
| 171 | |
| 172 | |
| 173 | /* This isn't very efficient but its infrequently used. |
| 174 | * StandardGlyphVector uses it when the client assigns the glyph codes. |
| 175 | * These may not be valid. This validates them substituting the missing |
| 176 | * glyph elsewhere. |
| 177 | */ |
| 178 | protected int getValidatedGlyphCode(int glyphCode) { |
| 179 | if (glyphCode < 0 || glyphCode >= getMapper().getNumGlyphs()) { |
| 180 | glyphCode = getMapper().getMissingGlyphCode(); |
| 181 | } |
| 182 | return glyphCode; |
| 183 | } |
| 184 | |
| 185 | /* |
| 186 | * Creates an appropriate strike for the Font2D subclass |
| 187 | */ |
| 188 | abstract FontStrike createStrike(FontStrikeDesc desc); |
| 189 | |
| 190 | /* this may be useful for APIs like canDisplay where the answer |
| 191 | * is dependent on the font and its scaler, but not the strike. |
| 192 | * If no strike has ever been returned, then create a one that matches |
| 193 | * this font with the default FRC. It will become the lastStrike and |
| 194 | * there's a good chance that the next call will be to get exactly that |
| 195 | * strike. |
| 196 | */ |
| 197 | public FontStrike getStrike(Font font) { |
| 198 | FontStrike strike = (FontStrike)lastFontStrike.get(); |
| 199 | if (strike != null) { |
| 200 | return strike; |
| 201 | } else { |
| 202 | return getStrike(font, DEFAULT_FRC); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | /* SunGraphics2D has font, tx, aa and fm. From this info |
| 207 | * can get a Strike object from the cache, creating it if necessary. |
| 208 | * This code is designed for multi-threaded access. |
| 209 | * For that reason it creates a local FontStrikeDesc rather than filling |
| 210 | * in a shared one. Up to two AffineTransforms and one FontStrikeDesc will |
| 211 | * be created by every lookup. This appears to perform more than |
| 212 | * adequately. But it may make sense to expose FontStrikeDesc |
| 213 | * as a parameter so a caller can use its own. |
| 214 | * In such a case if a FontStrikeDesc is stored as a key then |
| 215 | * we would need to use a private copy. |
| 216 | * |
| 217 | * Note that this code doesn't prevent two threads from creating |
| 218 | * two different FontStrike instances and having one of the threads |
| 219 | * overwrite the other in the map. This is likely to be a rare |
| 220 | * occurrence and the only consequence is that these callers will have |
| 221 | * different instances of the strike, and there'd be some duplication of |
| 222 | * population of the strikes. However since users of these strikes are |
| 223 | * transient, then the one that was overwritten would soon be freed. |
| 224 | * If there is any problem then a small synchronized block would be |
| 225 | * required with its attendant consequences for MP scaleability. |
| 226 | */ |
| 227 | public FontStrike getStrike(Font font, AffineTransform devTx, |
| 228 | int aa, int fm) { |
| 229 | |
| 230 | /* Create the descriptor which is used to identify a strike |
| 231 | * in the strike cache/map. A strike is fully described by |
| 232 | * the attributes of this descriptor. |
| 233 | */ |
| 234 | /* REMIND: generating garbage and doing computation here in order |
| 235 | * to include pt size in the tx just for a lookup! Figure out a |
| 236 | * better way. |
| 237 | */ |
| 238 | double ptSize = font.getSize2D(); |
| 239 | AffineTransform glyphTx = (AffineTransform)devTx.clone(); |
| 240 | glyphTx.scale(ptSize, ptSize); |
| 241 | if (font.isTransformed()) { |
| 242 | glyphTx.concatenate(font.getTransform()); |
| 243 | } |
| 244 | FontStrikeDesc desc = new FontStrikeDesc(devTx, glyphTx, |
| 245 | font.getStyle(), aa, fm); |
| 246 | return getStrike(desc, false); |
| 247 | } |
| 248 | |
| 249 | public FontStrike getStrike(Font font, AffineTransform devTx, |
| 250 | AffineTransform glyphTx, |
| 251 | int aa, int fm) { |
| 252 | |
| 253 | /* Create the descriptor which is used to identify a strike |
| 254 | * in the strike cache/map. A strike is fully described by |
| 255 | * the attributes of this descriptor. |
| 256 | */ |
| 257 | FontStrikeDesc desc = new FontStrikeDesc(devTx, glyphTx, |
| 258 | font.getStyle(), aa, fm); |
| 259 | return getStrike(desc, false); |
| 260 | } |
| 261 | |
| 262 | public FontStrike getStrike(Font font, FontRenderContext frc) { |
| 263 | |
| 264 | AffineTransform at = frc.getTransform(); |
| 265 | double ptSize = font.getSize2D(); |
| 266 | at.scale(ptSize, ptSize); |
| 267 | if (font.isTransformed()) { |
| 268 | at.concatenate(font.getTransform()); |
| 269 | } |
| 270 | int aa = FontStrikeDesc.getAAHintIntVal(this, font, frc); |
| 271 | int fm = FontStrikeDesc.getFMHintIntVal(frc.getFractionalMetricsHint()); |
| 272 | FontStrikeDesc desc = new FontStrikeDesc(frc.getTransform(), |
| 273 | at, font.getStyle(), |
| 274 | aa, fm); |
| 275 | return getStrike(desc, false); |
| 276 | } |
| 277 | |
| 278 | FontStrike getStrike(FontStrikeDesc desc) { |
| 279 | return getStrike(desc, true); |
| 280 | } |
| 281 | |
| 282 | private FontStrike getStrike(FontStrikeDesc desc, boolean copy) { |
| 283 | /* Before looking in the map, see if the descriptor matches the |
| 284 | * last strike returned from this Font2D. This should often be a win |
| 285 | * since its common for the same font, in the same size to be |
| 286 | * used frequently, for example in many parts of a UI. |
| 287 | * |
| 288 | * If its not the same then we use the descriptor to locate a |
| 289 | * Reference to the strike. If it exists and points to a strike, |
| 290 | * then we update the last strike to refer to that and return it. |
| 291 | * |
| 292 | * If the key isn't in the map, or its reference object has been |
| 293 | * collected, then we create a new strike, put it in the map and |
| 294 | * set it to be the last strike. |
| 295 | */ |
| 296 | FontStrike strike = (FontStrike)lastFontStrike.get(); |
| 297 | if (strike != null && desc.equals(strike.desc)) { |
| 298 | //strike.lastlookupTime = System.currentTimeMillis(); |
| 299 | return strike; |
| 300 | } else { |
| 301 | Reference strikeRef = strikeCache.get(desc); |
| 302 | if (strikeRef != null) { |
| 303 | strike = (FontStrike)strikeRef.get(); |
| 304 | if (strike != null) { |
| 305 | //strike.lastlookupTime = System.currentTimeMillis(); |
| 306 | lastFontStrike = new SoftReference(strike); |
| 307 | StrikeCache.refStrike(strike); |
| 308 | return strike; |
| 309 | } else { |
| 310 | /* We have found a cleared reference that has not yet |
| 311 | * been removed by the disposer. |
| 312 | * If we make this reference unreachable by removing it |
| 313 | * from the map,or overwriting it with a new reference to |
| 314 | * a new strike, then it is possible it may never be |
| 315 | * enqueued for disposal. That is the implication of |
| 316 | * the docs for java.lang.ref. So on finding a cleared |
| 317 | * reference, we need to dispose the native resources, |
| 318 | * if they haven't already been freed. |
| 319 | * The reference object needs to have a reference to |
| 320 | * the disposer instance for this to occur. |
| 321 | */ |
| 322 | ((StrikeCache.DisposableStrike)strikeRef) |
| 323 | .getDisposer().dispose(); |
| 324 | } |
| 325 | } |
| 326 | /* When we create a new FontStrike instance, we *must* |
| 327 | * ask the StrikeCache for a reference. We must then ensure |
| 328 | * this reference remains reachable, by storing it in the |
| 329 | * Font2D's strikeCache map. |
| 330 | * So long as the Reference is there (reachable) then if the |
| 331 | * reference is cleared, it will be enqueued for disposal. |
| 332 | * If for some reason we explicitly remove this reference, it |
| 333 | * must only be done when holding a strong reference to the |
| 334 | * referent (the FontStrike), or if the reference is cleared, |
| 335 | * then we must explicitly "dispose" of the native resources. |
| 336 | * The only place this currently happens is in this same method, |
| 337 | * where we find a cleared reference and need to overwrite it |
| 338 | * here with a new reference. |
| 339 | * Clearing the whilst holding a strong reference, should only |
| 340 | * be done if the |
| 341 | */ |
| 342 | if (copy) { |
| 343 | desc = new FontStrikeDesc(desc); |
| 344 | } |
| 345 | strike = createStrike(desc); |
| 346 | //StrikeCache.addStrike(); |
| 347 | strikeRef = StrikeCache.getStrikeRef(strike); |
| 348 | strikeCache.put(desc, strikeRef); |
| 349 | //strike.lastlookupTime = System.currentTimeMillis(); |
| 350 | lastFontStrike = new SoftReference(strike); |
| 351 | StrikeCache.refStrike(strike); |
| 352 | return strike; |
| 353 | } |
| 354 | } |
| 355 | |
| 356 | void removeFromCache(FontStrikeDesc desc) { |
| 357 | Reference ref = strikeCache.get(desc); |
| 358 | if (ref != null) { |
| 359 | Object o = ref.get(); |
| 360 | if (o == null) { |
| 361 | strikeCache.remove(desc); |
| 362 | } |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | /** |
| 367 | * The length of the metrics array must be >= 8. This method will |
| 368 | * store the following elements in that array before returning: |
| 369 | * metrics[0]: ascent |
| 370 | * metrics[1]: descent |
| 371 | * metrics[2]: leading |
| 372 | * metrics[3]: max advance |
| 373 | * metrics[4]: strikethrough offset |
| 374 | * metrics[5]: strikethrough thickness |
| 375 | * metrics[6]: underline offset |
| 376 | * metrics[7]: underline thickness |
| 377 | */ |
| 378 | public void getFontMetrics(Font font, AffineTransform at, |
| 379 | Object aaHint, Object fmHint, |
| 380 | float metrics[]) { |
| 381 | /* This is called in just one place in Font with "at" == identity. |
| 382 | * Perhaps this can be eliminated. |
| 383 | */ |
| 384 | int aa = FontStrikeDesc.getAAHintIntVal(aaHint, this, font.getSize()); |
| 385 | int fm = FontStrikeDesc.getFMHintIntVal(fmHint); |
| 386 | FontStrike strike = getStrike(font, at, aa, fm); |
| 387 | StrikeMetrics strikeMetrics = strike.getFontMetrics(); |
| 388 | metrics[0] = strikeMetrics.getAscent(); |
| 389 | metrics[1] = strikeMetrics.getDescent(); |
| 390 | metrics[2] = strikeMetrics.getLeading(); |
| 391 | metrics[3] = strikeMetrics.getMaxAdvance(); |
| 392 | |
| 393 | getStyleMetrics(font.getSize2D(), metrics, 4); |
| 394 | } |
| 395 | |
| 396 | /** |
| 397 | * The length of the metrics array must be >= offset+4, and offset must be |
| 398 | * >= 0. Typically offset is 4. This method will |
| 399 | * store the following elements in that array before returning: |
| 400 | * metrics[off+0]: strikethrough offset |
| 401 | * metrics[off+1]: strikethrough thickness |
| 402 | * metrics[off+2]: underline offset |
| 403 | * metrics[off+3]: underline thickness |
| 404 | * |
| 405 | * Note that this implementation simply returns default values; |
| 406 | * subclasses can override this method to provide more accurate values. |
| 407 | */ |
| 408 | public void getStyleMetrics(float pointSize, float[] metrics, int offset) { |
| 409 | metrics[offset] = -metrics[0] / 2.5f; |
| 410 | metrics[offset+1] = pointSize / 12; |
| 411 | metrics[offset+2] = metrics[offset+1] / 1.5f; |
| 412 | metrics[offset+3] = metrics[offset+1]; |
| 413 | } |
| 414 | |
| 415 | /** |
| 416 | * The length of the metrics array must be >= 4. This method will |
| 417 | * store the following elements in that array before returning: |
| 418 | * metrics[0]: ascent |
| 419 | * metrics[1]: descent |
| 420 | * metrics[2]: leading |
| 421 | * metrics[3]: max advance |
| 422 | */ |
| 423 | public void getFontMetrics(Font font, FontRenderContext frc, |
| 424 | float metrics[]) { |
| 425 | StrikeMetrics strikeMetrics = getStrike(font, frc).getFontMetrics(); |
| 426 | metrics[0] = strikeMetrics.getAscent(); |
| 427 | metrics[1] = strikeMetrics.getDescent(); |
| 428 | metrics[2] = strikeMetrics.getLeading(); |
| 429 | metrics[3] = strikeMetrics.getMaxAdvance(); |
| 430 | } |
| 431 | |
| 432 | /* Currently the layout code calls this. May be better for layout code |
| 433 | * to check the font class before attempting to run, rather than needing |
| 434 | * to promote this method up from TrueTypeFont |
| 435 | */ |
| 436 | byte[] getTableBytes(int tag) { |
| 437 | return null; |
| 438 | } |
| 439 | |
| 440 | /* for layout code */ |
| 441 | protected long getUnitsPerEm() { |
| 442 | return 2048; |
| 443 | } |
| 444 | |
| 445 | boolean supportsEncoding(String encoding) { |
| 446 | return false; |
| 447 | } |
| 448 | |
| 449 | public boolean canDoStyle(int style) { |
| 450 | return (style == this.style); |
| 451 | } |
| 452 | |
| 453 | /* |
| 454 | * All the important subclasses override this which is principally for |
| 455 | * the TrueType 'gasp' table. |
| 456 | */ |
| 457 | public boolean useAAForPtSize(int ptsize) { |
| 458 | return true; |
| 459 | } |
| 460 | |
| 461 | public boolean hasSupplementaryChars() { |
| 462 | return false; |
| 463 | } |
| 464 | |
| 465 | /* The following methods implement public methods on java.awt.Font */ |
| 466 | public String getPostscriptName() { |
| 467 | return fullName; |
| 468 | } |
| 469 | |
| 470 | public String getFontName(Locale l) { |
| 471 | return fullName; |
| 472 | } |
| 473 | |
| 474 | public String getFamilyName(Locale l) { |
| 475 | return familyName; |
| 476 | } |
| 477 | |
| 478 | public int getNumGlyphs() { |
| 479 | return getMapper().getNumGlyphs(); |
| 480 | } |
| 481 | |
| 482 | public int charToGlyph(int wchar) { |
| 483 | return getMapper().charToGlyph(wchar); |
| 484 | } |
| 485 | |
| 486 | public int getMissingGlyphCode() { |
| 487 | return getMapper().getMissingGlyphCode(); |
| 488 | } |
| 489 | |
| 490 | public boolean canDisplay(char c) { |
| 491 | return getMapper().canDisplay(c); |
| 492 | } |
| 493 | |
| 494 | public boolean canDisplay(int cp) { |
| 495 | return getMapper().canDisplay(cp); |
| 496 | } |
| 497 | |
| 498 | public byte getBaselineFor(char c) { |
| 499 | return Font.ROMAN_BASELINE; |
| 500 | } |
| 501 | |
| 502 | public float getItalicAngle(Font font, AffineTransform at, |
| 503 | Object aaHint, Object fmHint) { |
| 504 | /* hardwire psz=12 as that's typical and AA vs non-AA for 'gasp' mode |
| 505 | * isn't important for the caret slope of this rarely used API. |
| 506 | */ |
| 507 | int aa = FontStrikeDesc.getAAHintIntVal(aaHint, this, 12); |
| 508 | int fm = FontStrikeDesc.getFMHintIntVal(fmHint); |
| 509 | FontStrike strike = getStrike(font, at, aa, fm); |
| 510 | StrikeMetrics metrics = strike.getFontMetrics(); |
| 511 | if (metrics.ascentY == 0 || metrics.ascentX == 0) { |
| 512 | return 0f; |
| 513 | } else { |
| 514 | /* ascent is "up" from the baseline so its typically |
| 515 | * a negative value, so we need to compensate |
| 516 | */ |
| 517 | return metrics.ascentX/-metrics.ascentY; |
| 518 | } |
| 519 | } |
| 520 | |
| 521 | } |