001    /*
002    Copyright (c) 1996-2012, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    package org.hd.d.pg2k.svrCore.mediahandler;
030    
031    import java.awt.Dimension;
032    import java.io.DataInputStream;
033    import java.io.FilterInputStream;
034    import java.io.IOException;
035    import java.io.InputStream;
036    import java.util.zip.InflaterInputStream;
037    
038    import javax.xml.parsers.DocumentBuilder;
039    import javax.xml.parsers.DocumentBuilderFactory;
040    import javax.xml.parsers.ParserConfigurationException;
041    
042    import org.hd.d.pg2k.svrCore.Name.ExhibitFull;
043    import org.hd.d.pg2k.svrCore.MIME.AbstractHandler;
044    import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
045    import org.w3c.dom.Document;
046    import org.w3c.dom.Element;
047    import org.w3c.dom.Node;
048    
049    /**Basic handler for SWF file (MacroMedia Shockwave Flash).
050     */
051    public final class swf extends AbstractHandler
052        {
053        /**Get internal type of ExhibitMIME type; never null. */
054        public ExhibitMIME.ExhibitTypeParameters getExhibitType()
055            { return(ExhibitMIME.getParamsByType(ExhibitMIME.ET_SWF)); }
056    
057    
058        /**TWIPS to pixels scaling factor assuming 72dpi screen resolution. */
059        private static final int TWIPS_TO_PIXELS = 20;
060    
061        /**Efficiently get X,Y pixel dimensions of SWF exhibit, else null if dimensions cannot be computed.
062         * This <strong>does not close its input stream</strong> when done.
063         * <p>
064         * This should work efficiently, by directly reading (opening) bytes
065         * of the exhibit passed as an input stream.
066         * <p>
067         * This will only work correctly if the exhibit is of the correct type,
068         * eg its magic number must already have been tested.
069         *
070         * @param is the exhibit as a binary data stream
071         * @throws java.io.IOException in case of problems with corrupt data
072         *                             (or a broken exhibit)
073         */
074        @Override public Dimension get2DImageDimensions(final InputStream is)
075            throws IOException
076            {
077            // Don't allow the underlying stream to be closed.
078            final InputStream isWrapped = new FilterInputStream(is){
079                /**Ignore any attempt to close the stream. */
080                @Override public void close() { }
081                };
082            final SWFMetaData swfMetaData = new SWFMetaData(isWrapped);
083            final Dimension result = new Dimension(
084                swfMetaData.xmax / TWIPS_TO_PIXELS,
085                swfMetaData.ymax / TWIPS_TO_PIXELS);
086            return(result);
087            }
088    
089        /**Immutable meta-data for SWF exhibit. */
090        public static final class SWFMetaData
091            {
092            /**SWF version. */
093            public final int version;
094            public final long length;
095            /**True if file in compressed format. */
096            public final boolean compressed;
097            public final int xmin;
098            public final int xmax;
099            public final int ymin;
100            public final int ymax;
101            /**Frame rate. */
102            public final float rate;
103            /**Frame count. */
104            public final int count;
105    
106            /**Parse meta-data from an input stream. */
107            public SWFMetaData(final InputStream is)
108                throws IOException
109                {
110                InflaterInputStream inflaterInputStream = null;
111                try
112                    {
113                    // Starts with CWS for compressed format, FWS for uncompressed.
114                    DataInputStream dis = new DataInputStream(is);
115                    final int firstChar = dis.readUnsignedByte();
116                    dis.skip(2); // Skip the "WS"...
117    
118                    version = dis.readUnsignedByte();
119                    length = 0x0ffffffffL & dis.readInt();
120    
121                    compressed = (version >= 6) && ('C' == firstChar);
122                    if(compressed)
123                        {
124                        // Start reading from the decompressed stream instead.
125                        inflaterInputStream = new InflaterInputStream(dis);
126                        dis = new DataInputStream(inflaterInputStream);
127                        }
128    
129                    long sizeBits = dis.readLong();
130                    final int bitsPerLenValue = (int) (sizeBits >>> 59);
131                    sizeBits <<= 5;
132    
133                    xmin = (int) (sizeBits >> (64 - bitsPerLenValue));
134                    sizeBits <<= bitsPerLenValue;
135                    xmax = (int) (sizeBits >> (64 - bitsPerLenValue));
136                    sizeBits <<= bitsPerLenValue;
137                    ymin = (int) (sizeBits >> (64 - bitsPerLenValue));
138                    sizeBits <<= bitsPerLenValue;
139                    ymax = (int) (sizeBits >> (64 - bitsPerLenValue));
140                    sizeBits <<= bitsPerLenValue;
141    
142                    rate = dis.readUnsignedShort() / 256f;
143                    count = dis.readUnsignedShort();
144                    }
145                finally
146                    {
147                    // Ensure that we free any ZIP resources.
148                    if(inflaterInputStream != null)
149                        { inflaterInputStream.close(); }
150                    }
151                }
152            }
153    
154        /**Gets all available exhibit metadata as a single XML DOM tree; null if none.
155         * We do not (yet) have a schema for this and may never do so,
156         * since its structure and content will depend on various external sources
157         * and parts of (for example) the ImageIO and JAI subsystems.
158         * <p>
159         * This implementation synthesises a metadata tree from the AudioFileFormat data,
160         * if any.
161         *
162         * @param is  input stream; never null
163         * @return top-level node "metadata" with captured metadata beneath, else null
164         */
165        @Override
166        public Node getMetadata(final InputStream is, final ExhibitFull exhibitName)
167            {
168            // Result: will be set non-null if we encounter any usable data.
169            Node result = null;
170    
171            try
172                {
173                final SWFMetaData md = new SWFMetaData(is);
174    
175                final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
176                final DocumentBuilder db = dbf.newDocumentBuilder();
177                final Document doc = db.newDocument();
178    
179                // Create a provisional root node,
180                // but only assign it to the result (ie to get a non-null result)
181                // when we know that there is some real data in there.
182                final Element root = doc.createElement(TAG_NAME_METADATA_TOP);
183                doc.appendChild(root);
184    
185    
186                // The "misc" node contains misc items.
187                final Element misc = doc.createElement("misc");
188                root.appendChild(misc);
189                result = root; // Got some real data...
190    
191                final Element compNode = doc.createElement("compressed");
192                compNode.setAttribute("value", String.valueOf(md.compressed));
193                misc.appendChild(compNode);
194    
195                final Element versionNode = doc.createElement("version");
196                versionNode.setAttribute("value", String.valueOf(md.version));
197                misc.appendChild(versionNode);
198    
199                final Element lengthNode = doc.createElement("length");
200                lengthNode.setAttribute("value", String.valueOf(md.length));
201                misc.appendChild(lengthNode);
202    
203    
204                // The "audio" node contains data on any soundtrack.
205                final Element audio = doc.createElement("audio");
206                root.appendChild(audio);
207                result = root; // Got some real data...
208    
209    
210                // The "video" node contains data on any imagery.
211                final Element video = doc.createElement("video");
212                root.appendChild(video);
213                result = root; // Got some real data...
214    
215                final Element xminNode = doc.createElement("xmin");
216                xminNode.setAttribute("value", String.valueOf(md.xmin));
217                video.appendChild(xminNode);
218    
219                final Element xmaxNode = doc.createElement("xmax");
220                xmaxNode.setAttribute("value", String.valueOf(md.xmax));
221                video.appendChild(xmaxNode);
222    
223                final Element yminNode = doc.createElement("ymin");
224                yminNode.setAttribute("value", String.valueOf(md.ymin));
225                video.appendChild(yminNode);
226    
227                final Element ymaxNode = doc.createElement("ymax");
228                ymaxNode.setAttribute("value", String.valueOf(md.ymax));
229                video.appendChild(ymaxNode);
230    
231                final Element rateNode = doc.createElement("fps");
232                rateNode.setAttribute("value", String.valueOf(md.rate));
233                video.appendChild(rateNode);
234    
235                final Element countNode = doc.createElement("count");
236                countNode.setAttribute("value", String.valueOf(md.count));
237                video.appendChild(countNode);
238                }
239            catch(final ParserConfigurationException e)
240                {
241                e.printStackTrace();
242                return(null); // Cannot build document...
243                }
244            catch(final IOException e)
245                {
246                e.printStackTrace();
247                return(null); // Cannot parse exhibit file...
248                }
249    
250            return(result);
251            }
252        }