001/*
002 * $RCSfile: J2KImageWriter.java,v $
003 *
004 * 
005 * Copyright (c) 2005 Sun Microsystems, Inc. All  Rights Reserved.
006 * 
007 * Redistribution and use in source and binary forms, with or without
008 * modification, are permitted provided that the following conditions
009 * are met: 
010 * 
011 * - Redistribution of source code must retain the above copyright 
012 *   notice, this  list of conditions and the following disclaimer.
013 * 
014 * - Redistribution in binary form must reproduce the above copyright
015 *   notice, this list of conditions and the following disclaimer in 
016 *   the documentation and/or other materials provided with the
017 *   distribution.
018 * 
019 * Neither the name of Sun Microsystems, Inc. or the names of 
020 * contributors may be used to endorse or promote products derived 
021 * from this software without specific prior written permission.
022 * 
023 * This software is provided "AS IS," without a warranty of any 
024 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND 
025 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 
026 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
027 * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL 
028 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF 
029 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
030 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR 
031 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
032 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
033 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
034 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
035 * POSSIBILITY OF SUCH DAMAGES. 
036 * 
037 * You acknowledge that this software is not designed or intended for 
038 * use in the design, construction, operation or maintenance of any 
039 * nuclear facility. 
040 *
041 * $Revision: 1.1 $
042 * $Date: 2005/02/11 05:01:34 $
043 * $State: Exp $
044 */
045package com.github.jaiimageio.jpeg2000.impl;
046
047import java.awt.image.ColorModel;
048import java.awt.image.DataBuffer;
049import java.awt.image.IndexColorModel;
050import java.awt.image.MultiPixelPackedSampleModel;
051import java.awt.image.Raster;
052import java.awt.image.RenderedImage;
053import java.awt.image.SampleModel;
054import java.io.File;
055import java.io.IOException;
056import java.util.Arrays;
057import java.util.List;
058
059import javax.imageio.IIOException;
060import javax.imageio.IIOImage;
061import javax.imageio.ImageTypeSpecifier;
062import javax.imageio.ImageWriteParam;
063import javax.imageio.ImageWriter;
064import javax.imageio.metadata.IIOInvalidTreeException;
065import javax.imageio.metadata.IIOMetadata;
066import javax.imageio.metadata.IIOMetadataFormatImpl;
067import javax.imageio.spi.ImageWriterSpi;
068import javax.imageio.stream.ImageOutputStream;
069
070import jj2000.j2k.codestream.writer.FileCodestreamWriter;
071import jj2000.j2k.codestream.writer.HeaderEncoder;
072import jj2000.j2k.entropy.encoder.EntropyCoder;
073import jj2000.j2k.entropy.encoder.PostCompRateAllocator;
074import jj2000.j2k.fileformat.writer.FileFormatWriter;
075import jj2000.j2k.image.ImgDataConverter;
076import jj2000.j2k.image.Tiler;
077import jj2000.j2k.image.forwcomptransf.ForwCompTransf;
078import jj2000.j2k.quantization.quantizer.Quantizer;
079import jj2000.j2k.roi.encoder.ROIScaler;
080import jj2000.j2k.util.CodestreamManipulator;
081import jj2000.j2k.wavelet.analysis.ForwardWT;
082
083import com.github.jaiimageio.impl.common.ImageUtil;
084import com.github.jaiimageio.jpeg2000.J2KImageWriteParam;
085
086/**
087 * The Java Image IO plugin writer for encoding a RenderedImage into
088 * a JPEG 2000 part 1 file (JP2) format.
089 *
090 * This writer has the capability to (1) Losslessly encode
091 * <code>RenderedImage</code>s with an <code>IndexColorModel</code> (for
092 * example, bi-level or color indexed images).  (2) Losslessly or lossy encode
093 * <code>RenderedImage</code> with a byte, short, ushort or integer types with
094 * band number upto 16384.  (3) Encode an image with alpha channel.
095 * (4) Write the provided metadata into the code stream.  It also can encode
096 * a raster wrapped in the provided <code>IIOImage</code>.
097 *
098 * The encoding process may re-tile image, clip, subsample, and select bands
099 * using the parameters specified in the <code>ImageWriteParam</code>.
100 *
101 * @see com.sun.media.imageio.plugins.J2KImageWriteParam
102 */
103public class J2KImageWriter extends ImageWriter {
104    /** Wrapper for the protected method <code>processImageProgress</code>
105     *  So it can be access from the classes which are not in
106     *  <code>ImageWriter</code> hierachy.
107     */
108    public void processImageProgressWrapper(float percentageDone) {
109        processImageProgress(percentageDone);
110    }
111
112
113    /** When the writing is aborted, <code>RenderedImageSrc</code> throws a
114     *  <code>RuntimeException</code>.
115     */
116    public static String WRITE_ABORTED = "Write aborted.";
117
118    /** The output stream to write into */
119    private ImageOutputStream stream = null;
120
121    /** Constructs <code>J2KImageWriter</code> based on the provided
122     *  <code>ImageWriterSpi</code>.
123     */
124    public J2KImageWriter(ImageWriterSpi originator) {
125        super(originator);
126    }
127
128    public void setOutput(Object output) {
129        super.setOutput(output); // validates output
130        if (output != null) {
131            if (!(output instanceof ImageOutputStream))
132                throw new IllegalArgumentException(I18N.getString("J2KImageWriter0"));
133            this.stream = (ImageOutputStream)output;
134        } else
135            this.stream = null;
136    }
137
138    public ImageWriteParam getDefaultWriteParam() {
139        return new J2KImageWriteParam();
140    }
141
142    public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
143        return null;
144    }
145
146    public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
147                                               ImageWriteParam param) {
148        return new J2KMetadata(imageType, param, this);
149    }
150
151    public IIOMetadata convertStreamMetadata(IIOMetadata inData,
152                                             ImageWriteParam param) {
153        return null;
154    }
155
156    public IIOMetadata convertImageMetadata(IIOMetadata inData,
157                                            ImageTypeSpecifier imageType,
158                                            ImageWriteParam param) {
159        // Check arguments.
160        if(inData == null) {
161            throw new IllegalArgumentException("inData == null!");
162        }
163        if(imageType == null) {
164            throw new IllegalArgumentException("imageType == null!");
165        }
166
167        // If it's one of ours, return a clone.
168        if (inData instanceof J2KMetadata) {
169            return (IIOMetadata)((J2KMetadata)inData).clone();
170        }
171
172        try {
173            J2KMetadata outData = new J2KMetadata();
174
175            List formats = Arrays.asList(inData.getMetadataFormatNames());
176
177            String format = null;
178            if(formats.contains(J2KMetadata.nativeMetadataFormatName)) {
179                // Initialize from native image metadata format.
180                format = J2KMetadata.nativeMetadataFormatName;
181            } else if(inData.isStandardMetadataFormatSupported()) {
182                // Initialize from standard metadata form of the input tree.
183                format = IIOMetadataFormatImpl.standardMetadataFormatName;
184            }
185
186            if(format != null) {
187                outData.setFromTree(format, inData.getAsTree(format));
188                return outData;
189            }
190        } catch(IIOInvalidTreeException e) {
191            return null;
192        }
193
194        return null;
195    }
196
197    public boolean canWriteRasters() {
198        return true;
199    }
200
201    public void write(IIOMetadata streamMetadata,
202                      IIOImage image,
203                      ImageWriteParam param) throws IOException {
204        if (stream == null) {
205            throw new IllegalStateException(I18N.getString("J2KImageWriter7"));
206        }
207        if (image == null) {
208            throw new IllegalArgumentException(I18N.getString("J2KImageWriter8"));
209        }
210
211        clearAbortRequest();
212        processImageStarted(0);
213        RenderedImage input = null;
214
215        boolean writeRaster = image.hasRaster();
216        Raster raster = null;
217
218        SampleModel sampleModel = null;
219        if (writeRaster) {
220            raster = image.getRaster();
221            sampleModel = raster.getSampleModel();
222        } else {
223            input = image.getRenderedImage();
224            sampleModel = input.getSampleModel();
225        }
226
227        checkSampleModel(sampleModel);
228        if (param == null)
229            param = getDefaultWriteParam();
230
231        J2KImageWriteParamJava j2kwparam =
232            new J2KImageWriteParamJava(image, param);
233
234        // Packet header cannot exist in two places.
235        if (j2kwparam.getPackPacketHeaderInTile() &&
236            j2kwparam.getPackPacketHeaderInMain())
237            throw new IllegalArgumentException(I18N.getString("J2KImageWriter1"));
238
239        // Lossless and encoding rate cannot be set at the same time
240        if (j2kwparam.getLossless() &&
241            j2kwparam.getEncodingRate()!=Double.MAX_VALUE)
242            throw new IllegalArgumentException(I18N.getString("J2KImageWriter2"));
243
244        // If the source image is bilevel or color-indexed, or, the
245        // encoding rate is Double.MAX_VALUE, use lossless
246        if ((!writeRaster && input.getColorModel() instanceof IndexColorModel) ||
247             (writeRaster &&
248              raster.getSampleModel() instanceof MultiPixelPackedSampleModel)) {
249            j2kwparam.setDecompositionLevel("0");
250            j2kwparam.setLossless(true);
251            j2kwparam.setEncodingRate(Double.MAX_VALUE);
252            j2kwparam.setQuantizationType("reversible");
253            j2kwparam.setFilters(J2KImageWriteParam.FILTER_53);
254        } else if (j2kwparam.getEncodingRate() == Double.MAX_VALUE) {
255            j2kwparam.setLossless(true);
256            j2kwparam.setQuantizationType("reversible");
257            j2kwparam.setFilters(J2KImageWriteParam.FILTER_53);
258        }
259
260        // Gets parameters from the write parameter
261        boolean pphTile = j2kwparam.getPackPacketHeaderInTile();
262        boolean pphMain = j2kwparam.getPackPacketHeaderInMain();
263        boolean tempSop = false;
264        boolean tempEph = false;
265
266        int[] bands = param.getSourceBands();
267        int ncomp = sampleModel.getNumBands();
268
269        if (bands != null)
270            ncomp = bands.length;
271
272        // create the encoding source recognized by jj2000 packages
273        RenderedImageSrc imgsrc = null;
274        if (writeRaster)
275            imgsrc = new RenderedImageSrc(raster, j2kwparam, this);
276        else
277            imgsrc = new RenderedImageSrc(input, j2kwparam, this);
278
279        // if the components signed
280        boolean[] imsigned = new boolean[ncomp];
281        if (bands != null) {
282            for (int i=0; i<ncomp; i++)
283                imsigned[i] = ((RenderedImageSrc)imgsrc).isOrigSigned(bands[i]);
284        } else {
285            for (int i=0; i<ncomp; i++)
286                imsigned[i] = ((RenderedImageSrc)imgsrc).isOrigSigned(i);
287        }
288
289        // Gets the tile dimensions
290        int tw = j2kwparam.getTileWidth();
291        int th = j2kwparam.getTileHeight();
292
293        //Gets the image position
294        int refx = j2kwparam.getMinX();
295        int refy = j2kwparam.getMinY();
296        if (refx < 0 || refy < 0)
297            throw new IIOException(I18N.getString("J2KImageWriter3"));
298
299        // Gets tile grid offsets and validates them
300        int trefx = j2kwparam.getTileGridXOffset();
301        int trefy = j2kwparam.getTileGridYOffset();
302        if (trefx < 0 || trefy < 0 || trefx > refx || trefy > refy)
303            throw new IIOException(I18N.getString("J2KImageWriter4"));
304
305        // Instantiate tiler
306        Tiler imgtiler = new Tiler(imgsrc,refx,refy,trefx,trefy,tw,th);
307
308        // Creates the forward component transform
309        ForwCompTransf fctransf = new ForwCompTransf(imgtiler, j2kwparam);
310
311        // Creates ImgDataConverter
312        ImgDataConverter converter = new ImgDataConverter(fctransf);
313
314        // Creates ForwardWT (forward wavelet transform)
315        ForwardWT dwt = ForwardWT.createInstance(converter, j2kwparam);
316
317        // Creates Quantizer
318        Quantizer quant = Quantizer.createInstance(dwt,j2kwparam);
319
320        // Creates ROIScaler
321        ROIScaler rois = ROIScaler.createInstance(quant, j2kwparam);
322
323        // Creates EntropyCoder
324        EntropyCoder ecoder =
325            EntropyCoder.createInstance(rois, j2kwparam,
326                j2kwparam.getCodeBlockSize(),
327                j2kwparam.getPrecinctPartition(),
328                j2kwparam.getBypass(),
329                j2kwparam.getResetMQ(),
330                j2kwparam.getTerminateOnByte(),
331                j2kwparam.getCausalCXInfo(),
332                j2kwparam.getCodeSegSymbol(),
333                j2kwparam.getMethodForMQLengthCalc(),
334                j2kwparam.getMethodForMQTermination());
335
336        // Rely on rate allocator to limit amount of data
337        File tmpFile = File.createTempFile("jiio-", ".tmp");
338        tmpFile.deleteOnExit();
339
340        // Creates CodestreamWriter
341        FileCodestreamWriter bwriter =
342            new FileCodestreamWriter(tmpFile, Integer.MAX_VALUE);
343
344        // Creates the rate allocator
345        float rate = (float)j2kwparam.getEncodingRate();
346        PostCompRateAllocator ralloc =
347            PostCompRateAllocator.createInstance(ecoder,
348                                                 rate,
349                                                 bwriter,
350                                                 j2kwparam);
351
352        // Instantiates the HeaderEncoder
353        HeaderEncoder headenc =
354            new HeaderEncoder(imgsrc, imsigned, dwt, imgtiler,
355                              j2kwparam, rois,ralloc);
356
357        ralloc.setHeaderEncoder(headenc);
358
359        // Writes header to be able to estimate header overhead
360        headenc.encodeMainHeader();
361
362        //Initializes rate allocator, with proper header
363        // overhead. This will also encode all the data
364        try {
365            ralloc.initialize();
366        } catch (RuntimeException e) {
367            if (WRITE_ABORTED.equals(e.getMessage())) {
368                bwriter.close();
369                tmpFile.delete();
370                processWriteAborted();
371                return;
372            } else throw e;
373        }
374
375        // Write header (final)
376        headenc.reset();
377        headenc.encodeMainHeader();
378
379        // Insert header into the codestream
380        bwriter.commitBitstreamHeader(headenc);
381
382        // Now do the rate-allocation and write result
383        ralloc.runAndWrite();
384
385        //Done for data encoding
386        bwriter.close();
387
388        // Calculate file length
389        int fileLength = bwriter.getLength();
390
391        // Tile-parts and packed packet headers
392        int pktspertp = j2kwparam.getPacketPerTilePart();
393        int ntiles = imgtiler.getNumTiles();
394        if (pktspertp>0 || pphTile || pphMain){
395            CodestreamManipulator cm =
396                new CodestreamManipulator(tmpFile, ntiles, pktspertp,
397                                          pphMain, pphTile, tempSop,
398                                          tempEph);
399            fileLength += cm.doCodestreamManipulation();
400        }
401
402        // File Format
403        int nc= imgsrc.getNumComps() ;
404        int[] bpc = new int[nc];
405        for(int comp = 0; comp<nc; comp++)
406            bpc[comp]=imgsrc.getNomRangeBits(comp);
407
408        ColorModel colorModel = (input != null) ? input.getColorModel() : null;
409        if (bands != null) {
410            ImageTypeSpecifier type= param.getDestinationType();
411            if (type != null)
412                colorModel = type.getColorModel();
413            //XXX: other wise should create proper color model based
414            // on the selected bands
415        }
416        if(colorModel == null) {
417            colorModel = ImageUtil.createColorModel(sampleModel);
418        }
419
420        J2KMetadata metadata = null;
421
422        if (param instanceof J2KImageWriteParam &&
423            !((J2KImageWriteParam)param).getWriteCodeStreamOnly()) {
424            IIOMetadata inMetadata = image.getMetadata();
425
426            J2KMetadata metadata1 = new J2KMetadata(colorModel,
427                                                    sampleModel,
428                                                    imgsrc.getImgWidth(),
429                                                    imgsrc.getImgHeight(),
430                                                    param,
431                                                    this);
432
433            if (inMetadata == null) {
434                metadata = metadata1;
435            } else {
436                // Convert the input metadata tree to a J2KMetadata.
437                if(colorModel != null) {
438                    ImageTypeSpecifier imageType = 
439                        new ImageTypeSpecifier(colorModel, sampleModel);
440                    metadata =
441                        (J2KMetadata)convertImageMetadata(inMetadata,
442                                                          imageType,
443                                                          param);
444                } else {
445                    String metaFormat = null;
446                    List metaFormats =
447                        Arrays.asList(inMetadata.getMetadataFormatNames());
448                    if(metaFormats.contains(J2KMetadata.nativeMetadataFormatName)) {
449                        // Initialize from native image metadata format.
450                        metaFormat = J2KMetadata.nativeMetadataFormatName;
451                    } else if(inMetadata.isStandardMetadataFormatSupported()) {
452                        // Initialize from standard metadata form of the
453                        // input tree.
454                        metaFormat = 
455                            IIOMetadataFormatImpl.standardMetadataFormatName;
456                    }
457
458                    metadata = new J2KMetadata();
459                    if(metaFormat != null) {
460                        metadata.setFromTree(metaFormat,
461                                             inMetadata.getAsTree(metaFormat));
462                    }
463                }
464
465                metadata.mergeTree(J2KMetadata.nativeMetadataFormatName,
466                                   metadata1.getAsTree(J2KMetadata.nativeMetadataFormatName));
467            }
468        }
469
470        FileFormatWriter ffw =
471            new FileFormatWriter(tmpFile, stream,
472                                 imgsrc.getImgHeight(),
473                                 imgsrc.getImgWidth(), nc, bpc,
474                                 fileLength,
475                                 colorModel,
476                                 sampleModel,
477                                 metadata);
478        fileLength += ffw.writeFileFormat();
479        tmpFile.delete();
480
481        processImageComplete();
482    }
483
484    public synchronized void abort() {
485        super.abort();
486    }
487
488    public void reset() {
489        // reset local Java structures
490        super.reset();
491        stream = null;
492    }
493
494    /** This method wraps the protected method <code>abortRequested</code>
495     *  to allow the abortions be monitored by <code>J2KRenderedImage</code>.
496     */
497    public boolean getAbortRequest() {
498        return abortRequested();
499    }
500
501    private void checkSampleModel(SampleModel sm) {
502        int type = sm.getDataType();
503
504        if (type < DataBuffer.TYPE_BYTE || type > DataBuffer.TYPE_INT)
505            throw new IllegalArgumentException(I18N.getString("J2KImageWriter5"));
506        if (sm.getNumBands() > 16384)
507            throw new IllegalArgumentException(I18N.getString("J2KImageWriter6"));
508    }
509}