001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.reading.imageop;
018:
019: import java.awt.image.BufferedImage;
020: import java.awt.image.ColorModel;
021: import java.awt.image.WritableRaster;
022: import javax.imageio.ImageIO;
023: import javax.imageio.ImageTypeSpecifier;
024: import javax.imageio.ImageWriter;
025: import javax.imageio.stream.ImageOutputStream;
026: import javax.imageio.spi.ImageWriterSpi;
027: import java.io.IOException;
028: import java.io.InputStream;
029: import java.io.Serializable;
030: import java.util.ArrayList;
031: import java.util.Hashtable;
032: import java.util.Iterator;
033: import java.util.Map;
034:
035: import org.apache.avalon.framework.activity.Disposable;
036: import org.apache.avalon.framework.configuration.Configurable;
037: import org.apache.avalon.framework.configuration.Configuration;
038: import org.apache.avalon.framework.configuration.ConfigurationException;
039: import org.apache.avalon.framework.parameters.Parameters;
040: import org.apache.avalon.framework.service.ServiceException;
041: import org.apache.avalon.framework.service.ServiceManager;
042: import org.apache.avalon.framework.service.ServiceSelector;
043: import org.apache.avalon.framework.service.Serviceable;
044: import org.apache.cocoon.ProcessingException;
045: import org.apache.cocoon.environment.SourceResolver;
046: import org.apache.cocoon.reading.ResourceReader;
047: import org.xml.sax.SAXException;
048:
049: /**
050: * The <code>ImageOpReader</code> component is used to serve binary image data
051: * in a sitemap pipeline. It makes use of HTTP Headers to determine if
052: * the requested resource should be written to the <code>OutputStream</code>
053: * or if it can signal that it hasn't changed.
054: */
055: final public class ImageOpReader extends ResourceReader implements
056: Configurable, Serviceable, Disposable {
057:
058: private final static String FORMAT_DEFAULT = "png";
059:
060: private String format;
061: private ArrayList effectsStack;
062: private ServiceSelector operationSelector;
063: private ServiceManager manager;
064:
065: /**
066: * Read reader configuration
067: */
068: public void configure(Configuration configuration)
069: throws ConfigurationException {
070: super .configure(configuration);
071: Configuration effects = configuration.getChild("effects");
072: try {
073: configureEffects(effects);
074: } catch (ServiceException e) {
075: throw new ConfigurationException(
076: "Unable to configure ImageOperations", e);
077: }
078: }
079:
080: /**
081: * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
082: */
083: public void service(ServiceManager man) throws ServiceException {
084: this .manager = man;
085: operationSelector = (ServiceSelector) man
086: .lookup(ImageOperation.ROLE + "Selector");
087: }
088:
089: /**
090: * @see org.apache.avalon.framework.activity.Disposable#dispose()
091: */
092: public void dispose() {
093: if (this .manager != null) {
094: this .manager.release(this .operationSelector);
095: this .operationSelector = null;
096: this .manager = null;
097: }
098: }
099:
100: /**
101: * @see org.apache.cocoon.reading.ResourceReader#setup(org.apache.cocoon.environment.SourceResolver, java.util.Map, java.lang.String, org.apache.avalon.framework.parameters.Parameters)
102: */
103: public void setup(SourceResolver resolver, Map objectModel,
104: String src, Parameters par) throws ProcessingException,
105: SAXException, IOException {
106: super .setup(resolver, objectModel, src, par);
107: format = par.getParameter("output-format", FORMAT_DEFAULT);
108: if (getLogger().isInfoEnabled()) {
109: getLogger().info(src + " --> " + format);
110: }
111: setupEffectsStack(par);
112: }
113:
114: protected void processStream(InputStream inputStream)
115: throws IOException, ProcessingException {
116: if (effectsStack.size() > 0) {
117: // since we create the image on the fly
118: response.setHeader("Accept-Ranges", "none");
119:
120: BufferedImage image = ImageIO.read(inputStream);
121: if (image == null) {
122: throw new ProcessingException(
123: "Unable to decode the InputStream. Possibly an unknown format.");
124: }
125: image = applyEffectsStack(image);
126:
127: write(image);
128: } else {
129: // only read the resource - no modifications requested
130: if (getLogger().isDebugEnabled()) {
131: getLogger().debug("passing original resource");
132: }
133: super .processStream(inputStream);
134: }
135: }
136:
137: /**
138: * Generate the unique key.
139: * This key must be unique inside the space of this component.
140: *
141: * @return The generated key consists of the src and width and height, and the color transform
142: * parameters
143: */
144: public Serializable getKey() {
145: StringBuffer b = new StringBuffer(200);
146: b.append(this .inputSource.getURI());
147: b.append(':');
148: b.append(format);
149: b.append(':');
150: b.append(super .getKey());
151: b.append(':');
152: Iterator list = effectsStack.iterator();
153: while (list.hasNext()) {
154: ImageOperation op = (ImageOperation) list.next();
155: b.append(op.getKey());
156: b.append(':');
157: }
158: String key = b.toString();
159: b.setLength(0); // Seems to be something odd (memory leak?)
160: // going on if this isn't done. (JDK1.4.2)
161: return key;
162: }
163:
164: private void configureEffects(Configuration conf)
165: throws ConfigurationException, ServiceException {
166: effectsStack = new ArrayList();
167:
168: Configuration[] ops = conf.getChildren("op");
169: for (int i = 0; i < ops.length; i++) {
170: String type = ops[i].getAttribute("type");
171: String prefix = ops[i].getAttribute("prefix", type + "-");
172: ImageOperation op = (ImageOperation) operationSelector
173: .select(type);
174: op.setPrefix(prefix);
175: effectsStack.add(op);
176: }
177: }
178:
179: private void setupEffectsStack(Parameters params)
180: throws ProcessingException {
181: Iterator list = effectsStack.iterator();
182: while (list.hasNext()) {
183: ImageOperation op = (ImageOperation) list.next();
184: op.setup(params);
185: }
186: }
187:
188: private BufferedImage applyEffectsStack(BufferedImage image) {
189: if (effectsStack.size() == 0) {
190: return image;
191: }
192: Iterator list = effectsStack.iterator();
193: WritableRaster src = image.getRaster();
194: while (list.hasNext()) {
195: ImageOperation op = (ImageOperation) list.next();
196: WritableRaster r = op.apply(src);
197: if (getLogger().isDebugEnabled()) {
198: getLogger().debug("In Bounds: " + r.getBounds());
199: }
200: src = r.createWritableTranslatedChild(0, 0);
201: }
202: ColorModel cm = image.getColorModel();
203: if (getLogger().isDebugEnabled()) {
204: getLogger().debug("Out Bounds: " + src.getBounds());
205: }
206: BufferedImage newImage = new BufferedImage(cm, src, true,
207: new Hashtable());
208: // Not sure what this should really be --------------^^^^^
209:
210: int minX = newImage.getMinX();
211: int minY = newImage.getMinY();
212: int width = newImage.getWidth();
213: int height = newImage.getHeight();
214: if (getLogger().isInfoEnabled()) {
215: getLogger().info(
216: "Image: " + minX + ", " + minY + ", " + width
217: + ", " + height);
218: }
219:
220: return newImage;
221: }
222:
223: private void write(BufferedImage image) throws ProcessingException,
224: IOException {
225: ImageTypeSpecifier its = ImageTypeSpecifier
226: .createFromRenderedImage(image);
227: Iterator writers = ImageIO.getImageWriters(its, format);
228: ImageWriter writer = null;
229: if (writers.hasNext()) {
230: writer = (ImageWriter) writers.next();
231: }
232: if (writer == null) {
233: throw new ProcessingException(
234: "Unable to find a ImageWriter: " + format);
235: }
236:
237: ImageWriterSpi spi = writer.getOriginatingProvider();
238: String[] mimetypes = spi.getMIMETypes();
239: if (getLogger().isInfoEnabled()) {
240: getLogger().info("Setting content-type: " + mimetypes[0]);
241: }
242: response.setHeader("Content-Type", mimetypes[0]);
243: ImageOutputStream output = ImageIO.createImageOutputStream(out);
244: try {
245: writer.setOutput(output);
246: writer.write(image);
247: } finally {
248: writer.dispose();
249: output.close();
250: out.flush();
251: // Niclas Hedhman: Stream is closed in superclass.
252: }
253: }
254: /*
255: private void printRaster( WritableRaster r )
256: {
257: DataBuffer data = r.getDataBuffer();
258: int numBanks = data.getNumBanks();
259: int size = data.getSize();
260: for( int i=0 ; i < size ; i++ )
261: {
262: long value = 0;
263: for( int j=0 ; j < numBanks ; j++ )
264: {
265: int v = data.getElem( j, i );
266: if( v < 256 )
267: value = value << 8 ;
268: else
269: value = value << 16;
270: value = value + v;
271: }
272: if(getLogger().isDebugEnabled()) {
273: getLogger().debug( Long.toHexString( value ) );
274: }
275: }
276: }
277: */
278: }
|