You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ImageFactory.java 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /* $Id$ */
  18. package org.apache.fop.image;
  19. // Java
  20. import java.io.InputStream;
  21. import java.lang.ref.Reference;
  22. import java.lang.ref.ReferenceQueue;
  23. import java.lang.ref.SoftReference;
  24. import java.lang.reflect.Constructor;
  25. import java.util.ArrayList;
  26. import java.util.Collections;
  27. import java.util.HashMap;
  28. import java.util.Iterator;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.Set;
  32. import java.util.Map.Entry;
  33. import javax.xml.transform.Source;
  34. import javax.xml.transform.stream.StreamSource;
  35. import org.apache.commons.logging.Log;
  36. import org.apache.commons.logging.LogFactory;
  37. import org.apache.fop.apps.FOUserAgent;
  38. import org.apache.fop.datatypes.URISpecification;
  39. import org.apache.fop.image.analyser.ImageReaderFactory;
  40. import org.apache.xmlgraphics.util.Service;
  41. /**
  42. * Create FopImage objects (with a configuration file - not yet implemented).
  43. * @author Eric SCHAEFFER
  44. */
  45. public final class ImageFactory {
  46. /**
  47. * logging instance
  48. */
  49. protected static Log log = LogFactory.getLog(FopImage.class);
  50. private HashMap imageMimeTypes = new HashMap();
  51. private ImageCache cache = new ContextImageCache(true);
  52. /**
  53. * Main constructor for the ImageFactory.
  54. */
  55. public ImageFactory() {
  56. /* @todo The mappings set up below of image mime types to implementing
  57. * classes should be made externally configurable
  58. */
  59. ImageProvider jaiImage = new ImageProvider("JAIImage", "org.apache.fop.image.JAIImage");
  60. ImageProvider jimiImage = new ImageProvider("JIMIImage", "org.apache.fop.image.JimiImage");
  61. ImageProvider imageIoImage = new ImageProvider(
  62. "ImageIOImage", "org.apache.fop.image.ImageIOImage");
  63. ImageProvider gifImage = new ImageProvider("GIFImage", "org.apache.fop.image.GifImage");
  64. ImageProvider jpegImage = new ImageProvider("JPEGImage", "org.apache.fop.image.JpegImage");
  65. ImageProvider jpegImageIOImage = new ImageProvider(
  66. "JPEGImage", "org.apache.fop.image.JpegImageIOImage");
  67. ImageProvider bmpImage = new ImageProvider("BMPImage", "org.apache.fop.image.BmpImage");
  68. ImageProvider epsImage = new ImageProvider("EPSImage", "org.apache.fop.image.EPSImage");
  69. ImageProvider pngImage = new ImageProvider("PNGImage", "org.apache.fop.image.PNGImage");
  70. ImageProvider tiffImage = new ImageProvider("TIFFImage", "org.apache.fop.image.TIFFImage");
  71. ImageProvider xmlImage = new ImageProvider("XMLImage", "org.apache.fop.image.XMLImage");
  72. ImageProvider emfImage = new ImageProvider("EMFImage", "org.apache.fop.image.EmfImage");
  73. ImageMimeType imt = new ImageMimeType("image/gif");
  74. imageMimeTypes.put(imt.getMimeType(), imt);
  75. imt.addProvider(imageIoImage);
  76. imt.addProvider(jaiImage);
  77. imt.addProvider(jimiImage);
  78. imt.addProvider(gifImage);
  79. imt = new ImageMimeType("image/jpeg");
  80. imageMimeTypes.put(imt.getMimeType(), imt);
  81. imt.addProvider(jpegImageIOImage);
  82. imt.addProvider(jpegImage);
  83. imt = new ImageMimeType("image/bmp");
  84. imageMimeTypes.put(imt.getMimeType(), imt);
  85. imt.addProvider(bmpImage);
  86. imt = new ImageMimeType("image/eps");
  87. imageMimeTypes.put(imt.getMimeType(), imt);
  88. imt.addProvider(epsImage);
  89. imt = new ImageMimeType("image/png");
  90. imageMimeTypes.put(imt.getMimeType(), imt);
  91. //Image I/O is faster and more memory-efficient than own codec for PNG
  92. imt.addProvider(imageIoImage);
  93. imt.addProvider(pngImage);
  94. imt = new ImageMimeType("image/tga");
  95. imageMimeTypes.put(imt.getMimeType(), imt);
  96. imt.addProvider(jaiImage);
  97. imt.addProvider(imageIoImage);
  98. imt.addProvider(jimiImage);
  99. imt = new ImageMimeType("image/tiff");
  100. imageMimeTypes.put(imt.getMimeType(), imt);
  101. imt.addProvider(tiffImage); //Slower but supports CCITT embedding
  102. imt.addProvider(imageIoImage); //Fast but doesn't support CCITT embedding
  103. imt.addProvider(jaiImage); //Fast but doesn't support CCITT embedding
  104. imt = new ImageMimeType("image/svg+xml");
  105. imageMimeTypes.put(imt.getMimeType(), imt);
  106. imt.addProvider(xmlImage);
  107. imt = new ImageMimeType("text/xml");
  108. imageMimeTypes.put(imt.getMimeType(), imt);
  109. imt.addProvider(xmlImage);
  110. imt = new ImageMimeType("image/emf");
  111. imageMimeTypes.put(imt.getMimeType(), imt);
  112. imt.addProvider(emfImage);
  113. Iterator iter = Service.providers(RegisterableImageProvider.class, true);
  114. while (iter.hasNext()) {
  115. RegisterableImageProvider impl = (RegisterableImageProvider)iter.next();
  116. imt = new ImageMimeType(impl.getSupportedMimeType());
  117. imageMimeTypes.put(imt.getMimeType(), imt);
  118. imt.addProvider(new ImageProvider(impl.getName(), impl.getClassName()));
  119. }
  120. }
  121. /**
  122. * Get the url string from a wrapped url.
  123. *
  124. * @param href the input wrapped url
  125. * @return the raw url
  126. */
  127. public static String getURL(String href) {
  128. return URISpecification.getURL(href);
  129. }
  130. /**
  131. * Get the image from the cache or load.
  132. * If this returns null then the image could not be loaded
  133. * due to an error. Messages should be logged.
  134. * Before calling this the getURL(url) must be used.
  135. *
  136. * @param url the url for the image
  137. * @param context the user agent context
  138. * @return the fop image instance
  139. */
  140. public FopImage getImage(String url, FOUserAgent context) {
  141. return cache.getImage(url, context);
  142. }
  143. /**
  144. * Release an image from the cache.
  145. * This can be used if the renderer has its own cache of
  146. * the image.
  147. * The image should then be put into the weak cache.
  148. *
  149. * @param url the url for the image
  150. * @param context the user agent context
  151. */
  152. public void releaseImage(String url, FOUserAgent context) {
  153. cache.releaseImage(url, context);
  154. }
  155. /**
  156. * Release the context and all images in the context.
  157. *
  158. * @param context the context to remove
  159. */
  160. public void removeContext(FOUserAgent context) {
  161. cache.removeContext(context);
  162. }
  163. /**
  164. * Create an FopImage objects.
  165. * @param href the url for the image
  166. * @param ua the user agent context
  167. * @return the fop image instance
  168. */
  169. public FopImage loadImage(String href, FOUserAgent ua) {
  170. Source source = ua.resolveURI(href);
  171. if (source == null) {
  172. return null;
  173. }
  174. // Got a valid source, obtain an InputStream from it
  175. InputStream in = null;
  176. if (source instanceof StreamSource) {
  177. in = ((StreamSource)source).getInputStream();
  178. }
  179. if (in == null) {
  180. try {
  181. in = new java.net.URL(source.getSystemId()).openStream();
  182. } catch (Exception ex) {
  183. log.error("Unable to obtain stream from id '"
  184. + source.getSystemId() + "'");
  185. }
  186. }
  187. if (in == null) {
  188. return null;
  189. }
  190. //Make sure the InputStream is decorated with a BufferedInputStream
  191. if (!(in instanceof java.io.BufferedInputStream)) {
  192. in = new java.io.BufferedInputStream(in);
  193. }
  194. // Check image type
  195. FopImage.ImageInfo imgInfo = null;
  196. try {
  197. imgInfo = ImageReaderFactory.make(source.getSystemId(), in, ua);
  198. } catch (Exception e) {
  199. log.error("Error while recovering image information ("
  200. + href + ") : " + e.getMessage(), e);
  201. return null;
  202. }
  203. if (imgInfo == null) {
  204. try {
  205. in.close();
  206. in = null;
  207. } catch (Exception e) {
  208. log.debug("Error closing the InputStream for the image", e);
  209. }
  210. log.error("No ImageReader for this type of image (" + href + ")");
  211. return null;
  212. }
  213. // Associate mime-type to FopImage class
  214. String imgMimeType = imgInfo.mimeType;
  215. Class imageClass = getImageClass(imgMimeType);
  216. if (imageClass == null) {
  217. log.error("Unsupported image type (" + href + "): " + imgMimeType);
  218. return null;
  219. } else {
  220. if (log.isDebugEnabled()) {
  221. log.debug("Loading " + imgMimeType + " with " + imageClass.getName()
  222. + ": " + href);
  223. }
  224. }
  225. // load the right image class
  226. // return new <FopImage implementing class>
  227. Object imageInstance = null;
  228. try {
  229. Class[] imageConstructorParameters = new Class[1];
  230. imageConstructorParameters[0] = org.apache.fop.image.FopImage.ImageInfo.class;
  231. Constructor imageConstructor = imageClass.getDeclaredConstructor(
  232. imageConstructorParameters);
  233. Object[] initArgs = new Object[1];
  234. initArgs[0] = imgInfo;
  235. imageInstance = imageConstructor.newInstance(initArgs);
  236. } catch (java.lang.reflect.InvocationTargetException ex) {
  237. Throwable t = ex.getTargetException();
  238. String msg;
  239. if (t != null) {
  240. msg = t.getMessage();
  241. } else {
  242. msg = ex.getMessage();
  243. }
  244. log.error("Error creating FopImage object ("
  245. + href + "): " + msg, (t == null) ? ex : t);
  246. return null;
  247. } catch (InstantiationException ie) {
  248. log.error("Error creating FopImage object ("
  249. + href + "): Could not instantiate " + imageClass.getName() + " instance");
  250. return null;
  251. } catch (Exception ex) {
  252. log.error("Error creating FopImage object ("
  253. + href + "): " + ex.getMessage(), ex);
  254. return null;
  255. }
  256. if (!(imageInstance instanceof org.apache.fop.image.FopImage)) {
  257. log.error("Error creating FopImage object (" + href + "): " + "class "
  258. + imageClass.getName()
  259. + " doesn't implement org.apache.fop.image.FopImage interface");
  260. return null;
  261. }
  262. return (FopImage) imageInstance;
  263. }
  264. private Class getImageClass(String imgMimeType) {
  265. ImageMimeType imt = (ImageMimeType)imageMimeTypes.get(imgMimeType);
  266. if (imt == null) {
  267. return null;
  268. }
  269. return imt.getFirstImplementingClass();
  270. }
  271. /**
  272. * Forces all the image caches to be cleared. This should normally only be used in
  273. * testing environments. If you happen to think that you need to call this yourself
  274. * in a production environment, please notify the development team so we can look
  275. * into the issue. A call like this shouldn't be necessary anymore like it may have
  276. * been with FOP 0.20.5.
  277. */
  278. public void clearCaches() {
  279. cache.clearAll();
  280. }
  281. }
  282. /**
  283. * Basic image cache.
  284. * This keeps track of invalid images.
  285. */
  286. class BasicImageCache implements ImageCache {
  287. private Set invalid = Collections.synchronizedSet(new java.util.HashSet());
  288. //private Map contextStore = Collections.synchronizedMap(new java.util.HashMap());
  289. public FopImage getImage(String url, FOUserAgent context) {
  290. if (invalid.contains(url)) {
  291. return null;
  292. }
  293. //TODO Doesn't seem to be fully implemented. Do we need it at all? Not referenced.
  294. return null;
  295. }
  296. public void releaseImage(String url, FOUserAgent context) {
  297. // do nothing
  298. }
  299. public void invalidateImage(String url, FOUserAgent context) {
  300. // cap size of invalid list
  301. if (invalid.size() > 100) {
  302. invalid.clear();
  303. }
  304. invalid.add(url);
  305. }
  306. public void removeContext(FOUserAgent context) {
  307. // do nothing
  308. }
  309. /** {@inheritDoc} */
  310. public void clearAll() {
  311. invalid.clear();
  312. }
  313. }
  314. /**
  315. * This is the context image cache.
  316. * This caches images on the basis of the given context.
  317. * Common images in different contexts are currently not handled.
  318. * There are two possiblities, each context handles its own images
  319. * and renderers can cache information or images are shared and
  320. * all information is retained.
  321. * Once a context is removed then all images are placed into a
  322. * weak hashmap so they may be garbage collected.
  323. */
  324. class ContextImageCache implements ImageCache {
  325. // if this cache is collective then images can be shared
  326. // among contexts, this implies that the base directory
  327. // is either the same or does not effect the images being
  328. // loaded
  329. private boolean collective;
  330. private Map contextStore = Collections.synchronizedMap(new java.util.HashMap());
  331. private Set invalid = null;
  332. private Map refStore = null;
  333. private ReferenceQueue refQueue = new ReferenceQueue();
  334. public ContextImageCache(boolean col) {
  335. collective = col;
  336. if (collective) {
  337. refStore = Collections.synchronizedMap(new java.util.HashMap());
  338. invalid = Collections.synchronizedSet(new java.util.HashSet());
  339. }
  340. }
  341. // sync around lookups and puts
  342. // another sync around load for a particular image
  343. public FopImage getImage(String url, FOUserAgent context) {
  344. ImageLoader im = null;
  345. // this protects the finding or creating of a new
  346. // ImageLoader for multi threads
  347. synchronized (this) {
  348. if (collective && invalid.contains(url)) {
  349. return null;
  350. }
  351. Context con = (Context) contextStore.get(context);
  352. if (con == null) {
  353. con = new Context(context, collective);
  354. contextStore.put(context, con);
  355. } else {
  356. if (con.invalid(url)) {
  357. return null;
  358. }
  359. im = con.getImage(url);
  360. }
  361. if (im == null && collective) {
  362. Iterator i = contextStore.values().iterator();
  363. while (i.hasNext()) {
  364. Context c = (Context)i.next();
  365. if (c != con) {
  366. im = c.getImage(url);
  367. if (im != null) {
  368. break;
  369. }
  370. }
  371. }
  372. if (im == null) {
  373. Reference ref = (Reference)refStore.get(url);
  374. if (ref != null) {
  375. im = (ImageLoader) ref.get();
  376. if (im == null) {
  377. //Remove key if its value has been garbage collected
  378. refStore.remove(url);
  379. }
  380. }
  381. }
  382. }
  383. if (im != null) {
  384. con.putImage(url, im);
  385. } else {
  386. im = con.getImage(url, this);
  387. }
  388. }
  389. // the ImageLoader is synchronized so images with the
  390. // same url will not be loaded at the same time
  391. if (im != null) {
  392. return im.loadImage();
  393. }
  394. return null;
  395. }
  396. public void releaseImage(String url, FOUserAgent context) {
  397. Context con = (Context) contextStore.get(context);
  398. if (con != null) {
  399. if (collective) {
  400. ImageLoader im = con.getImage(url);
  401. refStore.put(url, wrapInReference(im, url));
  402. }
  403. con.releaseImage(url);
  404. }
  405. }
  406. public void invalidateImage(String url, FOUserAgent context) {
  407. if (collective) {
  408. // cap size of invalid list
  409. if (invalid.size() > 100) {
  410. invalid.clear();
  411. }
  412. invalid.add(url);
  413. }
  414. Context con = (Context) contextStore.get(context);
  415. if (con != null) {
  416. con.invalidateImage(url);
  417. }
  418. }
  419. private Reference wrapInReference(Object obj, Object key) {
  420. return new SoftReferenceWithKey(obj, key, refQueue);
  421. }
  422. private static class SoftReferenceWithKey extends SoftReference {
  423. private Object key;
  424. public SoftReferenceWithKey(Object referent, Object key, ReferenceQueue q) {
  425. super(referent, q);
  426. this.key = key;
  427. }
  428. }
  429. public void removeContext(FOUserAgent context) {
  430. Context con = (Context) contextStore.get(context);
  431. if (con != null) {
  432. if (collective) {
  433. Map images = con.getImages();
  434. Iterator iter = images.entrySet().iterator();
  435. while (iter.hasNext()) {
  436. Entry entry = (Entry)iter.next();
  437. refStore.put(entry.getKey(),
  438. wrapInReference(entry.getValue(), entry.getKey()));
  439. }
  440. }
  441. contextStore.remove(context);
  442. }
  443. //House-keeping (remove cleared references)
  444. checkReferenceQueue();
  445. }
  446. /**
  447. * Checks the reference queue if any references have been cleared and removes them from the
  448. * cache.
  449. */
  450. private void checkReferenceQueue() {
  451. SoftReferenceWithKey ref;
  452. while ((ref = (SoftReferenceWithKey)refQueue.poll()) != null) {
  453. refStore.remove(ref.key);
  454. }
  455. }
  456. class Context {
  457. private Map images = Collections.synchronizedMap(new java.util.HashMap());
  458. private Set invalid = null;
  459. private FOUserAgent userAgent;
  460. public Context(FOUserAgent ua, boolean inv) {
  461. userAgent = ua;
  462. if (inv) {
  463. invalid = Collections.synchronizedSet(new java.util.HashSet());
  464. }
  465. }
  466. public ImageLoader getImage(String url, ImageCache c) {
  467. if (images.containsKey(url)) {
  468. return (ImageLoader) images.get(url);
  469. }
  470. ImageLoader loader = new ImageLoader(url, c, userAgent);
  471. images.put(url, loader);
  472. return loader;
  473. }
  474. public void putImage(String url, ImageLoader image) {
  475. images.put(url, image);
  476. }
  477. public ImageLoader getImage(String url) {
  478. return (ImageLoader) images.get(url);
  479. }
  480. public void releaseImage(String url) {
  481. images.remove(url);
  482. }
  483. public Map getImages() {
  484. return images;
  485. }
  486. public void invalidateImage(String url) {
  487. invalid.add(url);
  488. }
  489. public boolean invalid(String url) {
  490. return invalid.contains(url);
  491. }
  492. }
  493. /** {@inheritDoc} */
  494. public void clearAll() {
  495. this.refStore.clear();
  496. this.invalid.clear();
  497. //The context-sensitive caches are not cleared so there are no negative side-effects
  498. //in a multi-threaded environment. Not that it's a good idea to use this method at
  499. //all except in testing environments. If such a calls is necessary in normal environments
  500. //we need to check on memory leaks!
  501. }
  502. }
  503. /**
  504. * Encapsulates a class of type FopImage by holding its class name.
  505. * This allows dynamic loading of the class at runtime.
  506. */
  507. class ImageProvider {
  508. private String name = null;
  509. private String className = null;
  510. private boolean checked = false;
  511. private Class clazz = null;
  512. /**
  513. * Creates an ImageProvider with a given name and implementing class.
  514. * The class name should refer to a class of type {@link FopImage}.
  515. * However, this is not checked on construction.
  516. * @param name The name of the provider
  517. * @param className The full class name of the class implementing this provider
  518. */
  519. public ImageProvider(String name, String className) {
  520. setName(name);
  521. setClassName(className);
  522. }
  523. /**
  524. * Returns the provider name.
  525. * @return The provider name
  526. */
  527. public String getName() {
  528. return name;
  529. }
  530. private void setName(String name) {
  531. this.name = name;
  532. }
  533. /**
  534. * Returns the implementing class name.
  535. * @return The implementing class name
  536. */
  537. public String getClassName() {
  538. return className;
  539. }
  540. private void setClassName(String className) {
  541. this.className = className;
  542. }
  543. /**
  544. * Returns the implementing class as a {@link Class} object.
  545. * @return The implementing class or null if it couldn't be loaded.
  546. */
  547. public Class getImplementingClass() {
  548. if (!checked) {
  549. try {
  550. clazz = Class.forName(getClassName());
  551. } catch (ClassNotFoundException cnfe) {
  552. //nop
  553. } catch (LinkageError le) {
  554. // This can happen if fop was build with support for a
  555. // particular provider (e.g. a binary fop distribution)
  556. // but the required support files (e.g. jai, jimi) are not
  557. // available in the current runtime environment.
  558. ImageFactory.log.debug("Image support provider " + getName()
  559. + " could not be loaded. If " + getName() + " should be"
  560. + " available please make sure all required external libraries"
  561. + " are on the classpath.");
  562. }
  563. checked = true;
  564. }
  565. return clazz;
  566. }
  567. }
  568. /**
  569. * Holds a mime type for a particular image format plus a list of
  570. * {@link ImageProvider} objects which support the particular image format.
  571. */
  572. class ImageMimeType {
  573. private String mimeType = null;
  574. private List providers = null;
  575. /**
  576. * Constructor for a particular mime type.
  577. * @param mimeType The mime type
  578. */
  579. public ImageMimeType(String mimeType) {
  580. setMimeType(mimeType);
  581. }
  582. /**
  583. * Returns the mime type.
  584. * @return The mime type
  585. */
  586. public String getMimeType() {
  587. return mimeType;
  588. }
  589. private void setMimeType(String mimeType) {
  590. this.mimeType = mimeType;
  591. }
  592. /**
  593. * Returns the class from the first available provider.
  594. * @return The first available class or null if none can be found
  595. */
  596. public Class getFirstImplementingClass() {
  597. if (providers == null) {
  598. return null;
  599. }
  600. for (Iterator it = providers.iterator(); it.hasNext();) {
  601. ImageProvider ip = (ImageProvider)it.next();
  602. Class clazz = ip.getImplementingClass();
  603. if (clazz != null) {
  604. return clazz;
  605. }
  606. }
  607. return null;
  608. }
  609. /**
  610. * Adds a new provider.
  611. * The provider is added to the end of the current provider list.
  612. * @param The new provider to add
  613. */
  614. public void addProvider(ImageProvider provider) {
  615. if (providers == null) {
  616. providers = new ArrayList(4); // Assume we only have a few providers
  617. }
  618. if (!providers.contains(provider)) {
  619. providers.add(provider);
  620. }
  621. }
  622. }