001    /**
002     * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.image;
016    
017    import com.liferay.portal.kernel.concurrent.FutureConverter;
018    import com.liferay.portal.kernel.image.ImageBag;
019    import com.liferay.portal.kernel.image.ImageMagick;
020    import com.liferay.portal.kernel.image.ImageTool;
021    import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
022    import com.liferay.portal.kernel.log.Log;
023    import com.liferay.portal.kernel.log.LogFactoryUtil;
024    import com.liferay.portal.kernel.security.pacl.DoPrivileged;
025    import com.liferay.portal.kernel.util.ArrayUtil;
026    import com.liferay.portal.kernel.util.PropsKeys;
027    import com.liferay.portal.kernel.util.StringUtil;
028    import com.liferay.portal.model.Image;
029    import com.liferay.portal.model.impl.ImageImpl;
030    import com.liferay.portal.util.FileImpl;
031    import com.liferay.portal.util.PropsUtil;
032    import com.liferay.portal.util.PropsValues;
033    
034    import java.awt.AlphaComposite;
035    import java.awt.Graphics;
036    import java.awt.Graphics2D;
037    import java.awt.GraphicsConfiguration;
038    import java.awt.Rectangle;
039    import java.awt.image.BufferedImage;
040    import java.awt.image.ColorModel;
041    import java.awt.image.DataBuffer;
042    import java.awt.image.IndexColorModel;
043    import java.awt.image.RenderedImage;
044    import java.awt.image.SampleModel;
045    import java.awt.image.WritableRaster;
046    
047    import java.io.ByteArrayInputStream;
048    import java.io.File;
049    import java.io.IOException;
050    import java.io.InputStream;
051    import java.io.OutputStream;
052    
053    import java.util.Arrays;
054    import java.util.Hashtable;
055    import java.util.Iterator;
056    import java.util.LinkedList;
057    import java.util.Queue;
058    import java.util.concurrent.Future;
059    
060    import javax.imageio.ImageIO;
061    import javax.imageio.ImageReader;
062    import javax.imageio.spi.IIORegistry;
063    import javax.imageio.spi.ImageReaderSpi;
064    import javax.imageio.stream.ImageInputStream;
065    
066    import net.jmge.gif.Gif89Encoder;
067    
068    import org.im4java.core.IMOperation;
069    
070    import org.monte.media.jpeg.CMYKJPEGImageReaderSpi;
071    
072    /**
073     * @author Brian Wing Shun Chan
074     * @author Alexander Chow
075     * @author Shuyang Zhou
076     */
077    @DoPrivileged
078    public class ImageToolImpl implements ImageTool {
079    
080            public static ImageTool getInstance() {
081                    return _instance;
082            }
083    
084            public void afterPropertiesSet() {
085                    ClassLoader classLoader = getClass().getClassLoader();
086    
087                    try {
088                            InputStream is = classLoader.getResourceAsStream(
089                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_SPACER));
090    
091                            if (is == null) {
092                                    _log.error("Default spacer is not available");
093                            }
094    
095                            _defaultSpacer = getImage(is);
096                    }
097                    catch (Exception e) {
098                            _log.error(
099                                    "Unable to configure the default spacer: " + e.getMessage());
100                    }
101    
102                    try {
103                            InputStream is = classLoader.getResourceAsStream(
104                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_COMPANY_LOGO));
105    
106                            if (is == null) {
107                                    _log.error("Default company logo is not available");
108                            }
109    
110                            _defaultCompanyLogo = getImage(is);
111                    }
112                    catch (Exception e) {
113                            _log.error(
114                                    "Unable to configure the default company logo: " +
115                                            e.getMessage());
116                    }
117    
118                    try {
119                            InputStream is = classLoader.getResourceAsStream(
120                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_ORGANIZATION_LOGO));
121    
122                            if (is == null) {
123                                    _log.error("Default organization logo is not available");
124                            }
125    
126                            _defaultOrganizationLogo = getImage(is);
127                    }
128                    catch (Exception e) {
129                            _log.error(
130                                    "Unable to configure the default organization logo: " +
131                                            e.getMessage());
132                    }
133    
134                    try {
135                            InputStream is = classLoader.getResourceAsStream(
136                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_USER_FEMALE_PORTRAIT));
137    
138                            if (is == null) {
139                                    _log.error("Default user female portrait is not available");
140                            }
141    
142                            _defaultUserFemalePortrait = getImage(is);
143                    }
144                    catch (Exception e) {
145                            _log.error(
146                                    "Unable to configure the default user female portrait: " +
147                                            e.getMessage());
148                    }
149    
150                    try {
151                            InputStream is = classLoader.getResourceAsStream(
152                                    PropsUtil.get(PropsKeys.IMAGE_DEFAULT_USER_MALE_PORTRAIT));
153    
154                            if (is == null) {
155                                    _log.error("Default user male portrait is not available");
156                            }
157    
158                            _defaultUserMalePortrait = getImage(is);
159                    }
160                    catch (Exception e) {
161                            _log.error(
162                                    "Unable to configure the default user male portrait: " +
163                                            e.getMessage());
164                    }
165            }
166    
167            @Override
168            public Future<RenderedImage> convertCMYKtoRGB(
169                    byte[] bytes, final String type) {
170    
171                    ImageMagick imageMagick = getImageMagick();
172    
173                    if (!imageMagick.isEnabled()) {
174                            return null;
175                    }
176    
177                    File inputFile = _fileUtil.createTempFile(type);
178                    final File outputFile = _fileUtil.createTempFile(type);
179    
180                    try {
181                            _fileUtil.write(inputFile, bytes);
182    
183                            IMOperation imOperation = new IMOperation();
184    
185                            imOperation.addRawArgs("-format", "%[colorspace]");
186                            imOperation.addImage(inputFile.getPath());
187    
188                            String[] output = imageMagick.identify(imOperation.getCmdArgs());
189    
190                            if ((output.length == 1) &&
191                                    StringUtil.equalsIgnoreCase(output[0], "CMYK")) {
192    
193                                    if (_log.isInfoEnabled()) {
194                                            _log.info("The image is in the CMYK colorspace");
195                                    }
196    
197                                    imOperation = new IMOperation();
198    
199                                    imOperation.addRawArgs("-colorspace", "RGB");
200                                    imOperation.addImage(inputFile.getPath());
201                                    imOperation.addImage(outputFile.getPath());
202    
203                                    Future<Object> future = (Future<Object>)imageMagick.convert(
204                                            imOperation.getCmdArgs());
205    
206                                    return new FutureConverter<RenderedImage, Object>(future) {
207    
208                                            @Override
209                                            protected RenderedImage convert(Object obj) {
210                                                    RenderedImage renderedImage = null;
211    
212                                                    try {
213                                                            ImageBag imageBag = read(
214                                                                    _fileUtil.getBytes(outputFile));
215    
216                                                            renderedImage = imageBag.getRenderedImage();
217                                                    }
218                                                    catch (IOException ioe) {
219                                                            if (_log.isDebugEnabled()) {
220                                                                    _log.debug("Unable to convert " + type, ioe);
221                                                            }
222                                                    }
223    
224                                                    return renderedImage;
225                                            }
226    
227                                    };
228                            }
229                    }
230                    catch (Exception e) {
231                            if (_log.isErrorEnabled()) {
232                                    _log.error(e, e);
233                            }
234                    }
235                    finally {
236                            _fileUtil.delete(inputFile);
237                            _fileUtil.delete(outputFile);
238                    }
239    
240                    return null;
241            }
242    
243            @Override
244            public BufferedImage convertImageType(BufferedImage sourceImage, int type) {
245                    BufferedImage targetImage = new BufferedImage(
246                            sourceImage.getWidth(), sourceImage.getHeight(), type);
247    
248                    Graphics2D graphics = targetImage.createGraphics();
249    
250                    graphics.drawRenderedImage(sourceImage, null);
251    
252                    graphics.dispose();
253    
254                    return targetImage;
255            }
256    
257            @Override
258            public RenderedImage crop(
259                    RenderedImage renderedImage, int height, int width, int x, int y) {
260    
261                    Rectangle rectangle = new Rectangle(x, y, width, height);
262    
263                    Rectangle croppedRectangle = rectangle.intersection(
264                            new Rectangle(renderedImage.getWidth(), renderedImage.getHeight()));
265    
266                    BufferedImage bufferedImage = getBufferedImage(renderedImage);
267    
268                    return bufferedImage.getSubimage(
269                            croppedRectangle.x, croppedRectangle.y, croppedRectangle.width,
270                            croppedRectangle.height);
271            }
272    
273            @Override
274            public void encodeGIF(RenderedImage renderedImage, OutputStream os)
275                    throws IOException {
276    
277                    BufferedImage bufferedImage = getBufferedImage(renderedImage);
278    
279                    if (!(bufferedImage.getColorModel() instanceof IndexColorModel)) {
280                            bufferedImage = convertImageType(
281                                    bufferedImage, BufferedImage.TYPE_BYTE_INDEXED);
282                    }
283    
284                    Gif89Encoder encoder = new Gif89Encoder(bufferedImage);
285    
286                    encoder.encode(os);
287            }
288    
289            @Override
290            public void encodeWBMP(RenderedImage renderedImage, OutputStream os)
291                    throws IOException {
292    
293                    BufferedImage bufferedImage = getBufferedImage(renderedImage);
294    
295                    SampleModel sampleModel = bufferedImage.getSampleModel();
296    
297                    int type = sampleModel.getDataType();
298    
299                    if ((bufferedImage.getType() != BufferedImage.TYPE_BYTE_BINARY) ||
300                            (type < DataBuffer.TYPE_BYTE) || (type > DataBuffer.TYPE_INT) ||
301                            (sampleModel.getNumBands() != 1) ||
302                            (sampleModel.getSampleSize(0) != 1)) {
303    
304                            BufferedImage binaryImage = new BufferedImage(
305                                    bufferedImage.getWidth(), bufferedImage.getHeight(),
306                                    BufferedImage.TYPE_BYTE_BINARY);
307    
308                            Graphics graphics = binaryImage.getGraphics();
309    
310                            graphics.drawImage(bufferedImage, 0, 0, null);
311    
312                            renderedImage = binaryImage;
313                    }
314    
315                    if (!ImageIO.write(renderedImage, "wbmp", os)) {
316    
317                            // See http://www.jguru.com/faq/view.jsp?EID=127723
318    
319                            os.write(0);
320                            os.write(0);
321                            os.write(toMultiByte(bufferedImage.getWidth()));
322                            os.write(toMultiByte(bufferedImage.getHeight()));
323    
324                            DataBuffer dataBuffer = bufferedImage.getData().getDataBuffer();
325    
326                            int size = dataBuffer.getSize();
327    
328                            for (int i = 0; i < size; i++) {
329                                    os.write((byte)dataBuffer.getElem(i));
330                            }
331                    }
332            }
333    
334            @Override
335            public BufferedImage getBufferedImage(RenderedImage renderedImage) {
336                    if (renderedImage instanceof BufferedImage) {
337                            return (BufferedImage)renderedImage;
338                    }
339    
340                    ColorModel colorModel = renderedImage.getColorModel();
341    
342                    WritableRaster writableRaster =
343                            colorModel.createCompatibleWritableRaster(
344                                    renderedImage.getWidth(), renderedImage.getHeight());
345    
346                    Hashtable<String, Object> properties = new Hashtable<>();
347    
348                    String[] keys = renderedImage.getPropertyNames();
349    
350                    if (!ArrayUtil.isEmpty(keys)) {
351                            for (String key : keys) {
352                                    properties.put(key, renderedImage.getProperty(key));
353                            }
354                    }
355    
356                    BufferedImage bufferedImage = new BufferedImage(
357                            colorModel, writableRaster, colorModel.isAlphaPremultiplied(),
358                            properties);
359    
360                    renderedImage.copyData(writableRaster);
361    
362                    return bufferedImage;
363            }
364    
365            @Override
366            public byte[] getBytes(RenderedImage renderedImage, String contentType)
367                    throws IOException {
368    
369                    UnsyncByteArrayOutputStream baos = new UnsyncByteArrayOutputStream();
370    
371                    write(renderedImage, contentType, baos);
372    
373                    return baos.toByteArray();
374            }
375    
376            @Override
377            public Image getDefaultCompanyLogo() {
378                    return _defaultCompanyLogo;
379            }
380    
381            @Override
382            public Image getDefaultOrganizationLogo() {
383                    return _defaultOrganizationLogo;
384            }
385    
386            @Override
387            public Image getDefaultSpacer() {
388                    return _defaultSpacer;
389            }
390    
391            @Override
392            public Image getDefaultUserFemalePortrait() {
393                    return _defaultUserFemalePortrait;
394            }
395    
396            @Override
397            public Image getDefaultUserMalePortrait() {
398                    return _defaultUserMalePortrait;
399            }
400    
401            @Override
402            public Image getImage(byte[] bytes) throws IOException {
403                    if (bytes == null) {
404                            return null;
405                    }
406    
407                    ImageBag imageBag = read(bytes);
408    
409                    RenderedImage renderedImage = imageBag.getRenderedImage();
410    
411                    if (renderedImage == null) {
412                            throw new IOException("Unable to decode image");
413                    }
414    
415                    String type = imageBag.getType();
416    
417                    int height = renderedImage.getHeight();
418                    int width = renderedImage.getWidth();
419                    int size = bytes.length;
420    
421                    Image image = new ImageImpl();
422    
423                    image.setTextObj(bytes);
424                    image.setType(type);
425                    image.setHeight(height);
426                    image.setWidth(width);
427                    image.setSize(size);
428    
429                    return image;
430            }
431    
432            @Override
433            public Image getImage(File file) throws IOException {
434                    byte[] bytes = _fileUtil.getBytes(file);
435    
436                    return getImage(bytes);
437            }
438    
439            @Override
440            public Image getImage(InputStream is) throws IOException {
441                    byte[] bytes = _fileUtil.getBytes(is, -1, true);
442    
443                    return getImage(bytes);
444            }
445    
446            @Override
447            public Image getImage(InputStream is, boolean cleanUpStream)
448                    throws IOException {
449    
450                    byte[] bytes = _fileUtil.getBytes(is, -1, cleanUpStream);
451    
452                    return getImage(bytes);
453            }
454    
455            @Override
456            public boolean isNullOrDefaultSpacer(byte[] bytes) {
457                    if (ArrayUtil.isEmpty(bytes) ||
458                            Arrays.equals(bytes, getDefaultSpacer().getTextObj())) {
459    
460                            return true;
461                    }
462                    else {
463                            return false;
464                    }
465            }
466    
467            @Override
468            public ImageBag read(byte[] bytes) throws IOException {
469                    String formatName = null;
470                    ImageInputStream imageInputStream = null;
471                    Queue<ImageReader> imageReaders = new LinkedList<>();
472                    RenderedImage renderedImage = null;
473    
474                    try {
475                            imageInputStream = ImageIO.createImageInputStream(
476                                    new ByteArrayInputStream(bytes));
477    
478                            Iterator<ImageReader> iterator = ImageIO.getImageReaders(
479                                    imageInputStream);
480    
481                            while ((renderedImage == null) && iterator.hasNext()) {
482                                    ImageReader imageReader = iterator.next();
483    
484                                    imageReaders.offer(imageReader);
485    
486                                    try {
487                                            imageReader.setInput(imageInputStream);
488    
489                                            renderedImage = imageReader.read(0);
490                                    }
491                                    catch (IOException ioe) {
492                                            continue;
493                                    }
494    
495                                    formatName = StringUtil.toLowerCase(
496                                            imageReader.getFormatName());
497                            }
498    
499                            if (renderedImage == null) {
500                                    throw new IOException("Unsupported image type");
501                            }
502                    }
503                    finally {
504                            while (!imageReaders.isEmpty()) {
505                                    ImageReader imageReader = imageReaders.poll();
506    
507                                    imageReader.dispose();
508                            }
509    
510                            if (imageInputStream != null) {
511                                    imageInputStream.close();
512                            }
513                    }
514    
515                    String type = TYPE_JPEG;
516    
517                    if (formatName.contains(TYPE_BMP)) {
518                            type = TYPE_BMP;
519                    }
520                    else if (formatName.contains(TYPE_GIF)) {
521                            type = TYPE_GIF;
522                    }
523                    else if (formatName.contains("jpeg") ||
524                                     StringUtil.equalsIgnoreCase(type, "jpeg")) {
525    
526                            type = TYPE_JPEG;
527                    }
528                    else if (formatName.contains(TYPE_PNG)) {
529                            type = TYPE_PNG;
530                    }
531                    else if (formatName.contains(TYPE_TIFF)) {
532                            type = TYPE_TIFF;
533                    }
534                    else {
535                            throw new IllegalArgumentException(type + " is not supported");
536                    }
537    
538                    return new ImageBag(renderedImage, type);
539            }
540    
541            @Override
542            public ImageBag read(File file) throws IOException {
543                    return read(_fileUtil.getBytes(file));
544            }
545    
546            @Override
547            public ImageBag read(InputStream inputStream) throws IOException {
548                    return read(_fileUtil.getBytes(inputStream));
549            }
550    
551            @Override
552            public RenderedImage scale(RenderedImage renderedImage, int width) {
553                    if (width <= 0) {
554                            return renderedImage;
555                    }
556    
557                    int imageHeight = renderedImage.getHeight();
558                    int imageWidth = renderedImage.getWidth();
559    
560                    double factor = (double)width / imageWidth;
561    
562                    int scaledHeight = (int)Math.round(factor * imageHeight);
563                    int scaledWidth = width;
564    
565                    return doScale(renderedImage, scaledHeight, scaledWidth);
566            }
567    
568            @Override
569            public RenderedImage scale(
570                    RenderedImage renderedImage, int maxHeight, int maxWidth) {
571    
572                    int imageHeight = renderedImage.getHeight();
573                    int imageWidth = renderedImage.getWidth();
574    
575                    if (maxHeight == 0) {
576                            maxHeight = imageHeight;
577                    }
578    
579                    if (maxWidth == 0) {
580                            maxWidth = imageWidth;
581                    }
582    
583                    if ((imageHeight <= maxHeight) && (imageWidth <= maxWidth)) {
584                            return renderedImage;
585                    }
586    
587                    double factor = Math.min(
588                            (double)maxHeight / imageHeight, (double)maxWidth / imageWidth);
589    
590                    int scaledHeight = Math.max(1, (int)Math.round(factor * imageHeight));
591                    int scaledWidth = Math.max(1, (int)Math.round(factor * imageWidth));
592    
593                    return doScale(renderedImage, scaledHeight, scaledWidth);
594            }
595    
596            @Override
597            public void write(
598                            RenderedImage renderedImage, String contentType, OutputStream os)
599                    throws IOException {
600    
601                    if (contentType.contains(TYPE_BMP)) {
602                            ImageIO.write(renderedImage, "bmp", os);
603                    }
604                    else if (contentType.contains(TYPE_GIF)) {
605                            encodeGIF(renderedImage, os);
606                    }
607                    else if (contentType.contains(TYPE_JPEG) ||
608                                     contentType.contains("jpeg")) {
609    
610                            ImageIO.write(renderedImage, "jpeg", os);
611                    }
612                    else if (contentType.contains(TYPE_PNG)) {
613                            ImageIO.write(renderedImage, TYPE_PNG, os);
614                    }
615                    else if (contentType.contains(TYPE_TIFF) ||
616                                     contentType.contains("tif")) {
617    
618                            ImageIO.write(renderedImage, "tiff", os);
619                    }
620            }
621    
622            protected RenderedImage doScale(
623                    RenderedImage renderedImage, int scaledHeight, int scaledWidth) {
624    
625                    // See http://www.oracle.com/technetwork/java/index-137037.html
626    
627                    BufferedImage originalBufferedImage = getBufferedImage(renderedImage);
628    
629                    ColorModel originalColorModel = originalBufferedImage.getColorModel();
630    
631                    Graphics2D originalGraphics2D = originalBufferedImage.createGraphics();
632    
633                    if (originalColorModel.hasAlpha()) {
634                            originalGraphics2D.setComposite(AlphaComposite.Src);
635                    }
636    
637                    GraphicsConfiguration originalGraphicsConfiguration =
638                            originalGraphics2D.getDeviceConfiguration();
639    
640                    BufferedImage scaledBufferedImage =
641                            originalGraphicsConfiguration.createCompatibleImage(
642                                    scaledWidth, scaledHeight,
643                                    originalBufferedImage.getTransparency());
644    
645                    Graphics scaledGraphics = scaledBufferedImage.getGraphics();
646    
647                    scaledGraphics.drawImage(
648                            originalBufferedImage.getScaledInstance(
649                                    scaledWidth, scaledHeight, java.awt.Image.SCALE_SMOOTH),
650                            0, 0, null);
651    
652                    originalGraphics2D.dispose();
653    
654                    return scaledBufferedImage;
655            }
656    
657            protected ImageMagick getImageMagick() {
658                    if (_imageMagick == null) {
659                            _imageMagick = ImageMagickImpl.getInstance();
660    
661                            _imageMagick.reset();
662                    }
663    
664                    return _imageMagick;
665            }
666    
667            protected void orderImageReaderSpis() {
668                    IIORegistry defaultIIORegistry = IIORegistry.getDefaultInstance();
669    
670                    ImageReaderSpi firstImageReaderSpi = null;
671                    ImageReaderSpi secondImageReaderSpi = null;
672    
673                    Iterator<ImageReaderSpi> imageReaderSpis =
674                            defaultIIORegistry.getServiceProviders(ImageReaderSpi.class, true);
675    
676                    while (imageReaderSpis.hasNext()) {
677                            ImageReaderSpi imageReaderSpi = imageReaderSpis.next();
678    
679                            if (imageReaderSpi instanceof CMYKJPEGImageReaderSpi) {
680                                    secondImageReaderSpi = imageReaderSpi;
681                            }
682                            else {
683                                    String[] formatNames = imageReaderSpi.getFormatNames();
684    
685                                    if (ArrayUtil.contains(formatNames, TYPE_JPEG, true) ||
686                                            ArrayUtil.contains(formatNames, "jpeg", true)) {
687    
688                                            firstImageReaderSpi = imageReaderSpi;
689                                    }
690                            }
691                    }
692    
693                    if ((firstImageReaderSpi != null) && (secondImageReaderSpi != null)) {
694                            defaultIIORegistry.setOrdering(
695                                    ImageReaderSpi.class, firstImageReaderSpi,
696                                    secondImageReaderSpi);
697                    }
698            }
699    
700            protected byte[] toMultiByte(int intValue) {
701                    int numBits = 32;
702                    int mask = 0x80000000;
703    
704                    while ((mask != 0) && ((intValue & mask) == 0)) {
705                            numBits--;
706                            mask >>>= 1;
707                    }
708    
709                    int numBitsLeft = numBits;
710                    byte[] multiBytes = new byte[(numBitsLeft + 6) / 7];
711    
712                    int maxIndex = multiBytes.length - 1;
713    
714                    for (int b = 0; b <= maxIndex; b++) {
715                            multiBytes[b] = (byte)((intValue >>> ((maxIndex - b) * 7)) & 0x7f);
716    
717                            if (b != maxIndex) {
718                                    multiBytes[b] |= (byte)0x80;
719                            }
720                    }
721    
722                    return multiBytes;
723            }
724    
725            private ImageToolImpl() {
726                    ImageIO.setUseCache(PropsValues.IMAGE_IO_USE_DISK_CACHE);
727    
728                    orderImageReaderSpis();
729            }
730    
731            private static final Log _log = LogFactoryUtil.getLog(ImageToolImpl.class);
732    
733            private static final ImageTool _instance = new ImageToolImpl();
734    
735            private static final FileImpl _fileUtil = FileImpl.getInstance();
736            private static ImageMagick _imageMagick;
737    
738            private Image _defaultCompanyLogo;
739            private Image _defaultOrganizationLogo;
740            private Image _defaultSpacer;
741            private Image _defaultUserFemalePortrait;
742            private Image _defaultUserMalePortrait;
743    
744    }