MediaImageItem.java revision 75159247d81943ba99935fe02c38383c7c380304
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.media.videoeditor;
18
19import java.io.IOException;
20import java.util.List;
21
22import android.graphics.Bitmap;
23import android.graphics.BitmapFactory;
24import android.graphics.Canvas;
25import android.graphics.Paint;
26import android.graphics.Rect;
27import android.util.Log;
28import android.util.Pair;
29
30/**
31 * This class represents an image item on the storyboard. Note that images are
32 * scaled down to the maximum supported resolution by preserving the native
33 * aspect ratio. To learn the scaled image dimensions use
34 * {@link #getScaledWidth()} and {@link #getScaledHeight()} respectively.
35 *
36 * {@hide}
37 */
38public class MediaImageItem extends MediaItem {
39    // Logging
40    private static final String TAG = "MediaImageItem";
41
42    // The resize paint
43    private static final Paint sResizePaint = new Paint(Paint.FILTER_BITMAP_FLAG);
44
45    // Instance variables
46    private final int mWidth;
47    private final int mHeight;
48    private final int mAspectRatio;
49    private long mDurationMs;
50    private int mScaledWidth, mScaledHeight;
51
52    /**
53     * This class cannot be instantiated by using the default constructor
54     */
55    @SuppressWarnings("unused")
56    private MediaImageItem() throws IOException {
57        this(null, null, null, 0, RENDERING_MODE_BLACK_BORDER);
58    }
59
60    /**
61     * Constructor
62     *
63     * @param editor The video editor reference
64     * @param mediaItemId The media item id
65     * @param filename The image file name
66     * @param durationMs The duration of the image on the storyboard
67     * @param renderingMode The rendering mode
68     *
69     * @throws IOException
70     */
71    public MediaImageItem(VideoEditor editor, String mediaItemId, String filename, long durationMs,
72            int renderingMode)
73            throws IOException {
74        super(editor, mediaItemId, filename, renderingMode);
75
76        // Determine the dimensions of the image
77        final BitmapFactory.Options dbo = new BitmapFactory.Options();
78        dbo.inJustDecodeBounds = true;
79        BitmapFactory.decodeFile(filename, dbo);
80
81        mWidth = dbo.outWidth;
82        mHeight = dbo.outHeight;
83        mDurationMs = durationMs;
84
85        // TODO: Determine the aspect ratio from the width and height
86        mAspectRatio = MediaProperties.ASPECT_RATIO_4_3;
87
88        // Images are stored in memory scaled to the maximum resolution to
89        // save memory.
90        final Pair<Integer, Integer>[] resolutions =
91            MediaProperties.getSupportedResolutions(mAspectRatio);
92        // Get the highest resolution
93        final Pair<Integer, Integer> maxResolution = resolutions[resolutions.length - 1];
94        if (mHeight > maxResolution.second) {
95            // We need to scale the image
96            scaleImage(filename, maxResolution.first, maxResolution.second);
97            mScaledWidth = maxResolution.first;
98            mScaledHeight = maxResolution.second;
99        } else {
100            mScaledWidth = mWidth;
101            mScaledHeight = mHeight;
102        }
103    }
104
105    /*
106     * {@inheritDoc}
107     */
108    @Override
109    public int getFileType() {
110        if (mFilename.endsWith(".jpg") || mFilename.endsWith(".jpeg")) {
111            return MediaProperties.FILE_JPEG;
112        } else if (mFilename.endsWith(".png")) {
113            return MediaProperties.FILE_PNG;
114        } else {
115            return MediaProperties.FILE_UNSUPPORTED;
116        }
117    }
118
119    /*
120     * {@inheritDoc}
121     */
122    @Override
123    public int getWidth() {
124        return mWidth;
125    }
126
127    /*
128     * {@inheritDoc}
129     */
130    @Override
131    public int getHeight() {
132        return mHeight;
133    }
134
135    /**
136     * @return The scaled width of the image.
137     */
138    public int getScaledWidth() {
139        return mScaledWidth;
140    }
141
142    /**
143     * @return The scaled height of the image.
144     */
145    public int getScaledHeight() {
146        return mScaledHeight;
147    }
148
149    /*
150     * {@inheritDoc}
151     */
152    @Override
153    public int getAspectRatio() {
154        return mAspectRatio;
155    }
156
157    /**
158     * This method will adjust the duration of bounding transitions, effects
159     * and overlays if the current duration of the transactions become greater
160     * than the maximum allowable duration.
161     *
162     * @param durationMs The duration of the image in the storyboard timeline
163     */
164    public void setDuration(long durationMs) {
165        if (durationMs == mDurationMs) {
166            return;
167        }
168
169        // Invalidate the end transitions if necessary.
170        // This invalidation is necessary for the case in which an effect or
171        // an overlay is overlapping with the end transition
172        // (before the duration is changed) and it no longer overlaps with the
173        // transition after the duration is increased.
174
175        // The beginning transition does not need to be invalidated at this time
176        // because an effect or an overlay overlaps with the beginning
177        // transition, the begin transition is unaffected by a media item
178        // duration change.
179        invalidateEndTransition();
180
181        final long oldDurationMs = mDurationMs;
182        mDurationMs = durationMs;
183
184        adjustTransitions();
185        adjustOverlays();
186        adjustEffects();
187
188        // Invalidate the beginning and end transitions after adjustments.
189        // This invalidation is necessary for the case in which an effect or
190        // an overlay was not overlapping with the beginning or end transitions
191        // before the setDuration reduces the duration of the media item and
192        // causes an overlap of the beginning and/or end transition with the
193        // effect.
194        // If the duration is growing, the begin transition does not need to
195        // be invalidated since the effects, overlays are not adjusted.
196        if (mDurationMs < oldDurationMs) {
197            invalidateBeginTransition();
198        }
199        invalidateEndTransition();
200    }
201
202    /*
203     * {@inheritDoc}
204     */
205    @Override
206    public long getDuration() {
207        return mDurationMs;
208    }
209
210    /*
211     * {@inheritDoc}
212     */
213    @Override
214    public long getTimelineDuration() {
215        return mDurationMs;
216    }
217
218    /*
219     * {@inheritDoc}
220     */
221    @Override
222    public Bitmap getThumbnail(int width, int height, long timeMs) throws IOException {
223        return scaleImage(mFilename, width, height);
224    }
225
226    /*
227     * {@inheritDoc}
228     */
229    @Override
230    public Bitmap[] getThumbnailList(int width, int height, long startMs, long endMs,
231            int thumbnailCount) throws IOException {
232        final Bitmap thumbnail = scaleImage(mFilename, width, height);
233        final Bitmap[] thumbnailArray = new Bitmap[thumbnailCount];
234        for (int i = 0; i < thumbnailCount; i++) {
235            thumbnailArray[i] = thumbnail;
236        }
237        return thumbnailArray;
238    }
239
240    /*
241     * {@inheritDoc}
242     */
243    @Override
244    void invalidateTransitions(long startTimeMs, long durationMs) {
245        // Check if the effect overlaps with the beginning and end transitions
246        if (mBeginTransition != null) {
247            if (startTimeMs < mBeginTransition.getDuration()) {
248                mBeginTransition.invalidate();
249            }
250        }
251
252        if (mEndTransition != null) {
253            if (startTimeMs + durationMs > mDurationMs - mEndTransition.getDuration()) {
254                mEndTransition.invalidate();
255            }
256        }
257    }
258
259    /**
260     * Invalidate the begin transition if any effects and overlays overlap
261     * with the begin transition.
262     */
263    private void invalidateBeginTransition() {
264        if (mBeginTransition != null && mBeginTransition.isGenerated()) {
265            final long transitionDurationMs = mBeginTransition.getDuration();
266
267            // The begin transition must be invalidated if it overlaps with
268            // an effect.
269            final List<Effect> effects = getAllEffects();
270            for (Effect effect : effects) {
271                // Check if the effect overlaps with the begin transition
272                if (effect.getStartTime() < transitionDurationMs) {
273                    mBeginTransition.invalidate();
274                    break;
275                }
276            }
277
278            if (mBeginTransition.isGenerated()) {
279                // The end transition must be invalidated if it overlaps with
280                // an overlay.
281                final List<Overlay> overlays = getAllOverlays();
282                for (Overlay overlay : overlays) {
283                    // Check if the overlay overlaps with the end transition
284                    if (overlay.getStartTime() < transitionDurationMs) {
285                        mBeginTransition.invalidate();
286                        break;
287                    }
288                }
289            }
290        }
291    }
292
293    /**
294     * Invalidate the end transition if any effects and overlays overlap
295     * with the end transition.
296     */
297    private void invalidateEndTransition() {
298        if (mEndTransition != null && mEndTransition.isGenerated()) {
299            final long transitionDurationMs = mEndTransition.getDuration();
300
301            // The end transition must be invalidated if it overlaps with
302            // an effect.
303            final List<Effect> effects = getAllEffects();
304            for (Effect effect : effects) {
305                // Check if the effect overlaps with the end transition
306                if (effect.getStartTime() + effect.getDuration() >
307                            mDurationMs - transitionDurationMs) {
308                    mEndTransition.invalidate();
309                    break;
310                }
311            }
312
313            if (mEndTransition.isGenerated()) {
314                // The end transition must be invalidated if it overlaps with
315                // an overlay.
316                final List<Overlay> overlays = getAllOverlays();
317                for (Overlay overlay : overlays) {
318                    // Check if the overlay overlaps with the end transition
319                    if (overlay.getStartTime() + overlay.getDuration() >
320                                mDurationMs - transitionDurationMs) {
321                        mEndTransition.invalidate();
322                        break;
323                    }
324                }
325            }
326        }
327    }
328
329    /**
330     * Adjust the start time and/or duration of effects.
331     */
332    private void adjustEffects() {
333        final List<Effect> effects = getAllEffects();
334        for (Effect effect : effects) {
335            // Adjust the start time if necessary
336            final long effectStartTimeMs;
337            if (effect.getStartTime() > getDuration()) {
338                effectStartTimeMs = 0;
339            } else {
340                effectStartTimeMs = effect.getStartTime();
341            }
342
343            // Adjust the duration if necessary
344            final long effectDurationMs;
345            if (effectStartTimeMs + effect.getDuration() > getDuration()) {
346                effectDurationMs = getDuration() - effectStartTimeMs;
347            } else {
348                effectDurationMs = effect.getDuration();
349            }
350
351            if (effectStartTimeMs != effect.getStartTime() ||
352                    effectDurationMs != effect.getDuration()) {
353                effect.setStartTimeAndDuration(effectStartTimeMs, effectDurationMs);
354            }
355        }
356    }
357
358    /**
359     * Adjust the start time and/or duration of overlays.
360     */
361    private void adjustOverlays() {
362        final List<Overlay> overlays = getAllOverlays();
363        for (Overlay overlay : overlays) {
364            // Adjust the start time if necessary
365            final long overlayStartTimeMs;
366            if (overlay.getStartTime() > getDuration()) {
367                overlayStartTimeMs = 0;
368            } else {
369                overlayStartTimeMs = overlay.getStartTime();
370            }
371
372            // Adjust the duration if necessary
373            final long overlayDurationMs;
374            if (overlayStartTimeMs + overlay.getDuration() > getDuration()) {
375                overlayDurationMs = getDuration() - overlayStartTimeMs;
376            } else {
377                overlayDurationMs = overlay.getDuration();
378            }
379
380            if (overlayStartTimeMs != overlay.getStartTime() ||
381                    overlayDurationMs != overlay.getDuration()) {
382                overlay.setStartTimeAndDuration(overlayStartTimeMs, overlayDurationMs);
383            }
384        }
385    }
386
387    /**
388     * Resize a bitmap to the specified width and height
389     *
390     * @param filename The filename
391     * @param width The thumbnail width
392     * @param height The thumbnail height
393     *
394     * @return The resized bitmap
395     */
396    private Bitmap scaleImage(String filename, int width, int height) throws IOException {
397        final BitmapFactory.Options dbo = new BitmapFactory.Options();
398        dbo.inJustDecodeBounds = true;
399        BitmapFactory.decodeFile(filename, dbo);
400
401        final int nativeWidth = dbo.outWidth;
402        final int nativeHeight = dbo.outHeight;
403        if (Log.isLoggable(TAG, Log.DEBUG)) {
404            Log.d(TAG, "generateThumbnail: Input: " + nativeWidth + "x" + nativeHeight
405                    + ", resize to: " + width + "x" + height);
406        }
407
408        final Bitmap srcBitmap;
409        float bitmapWidth, bitmapHeight;
410        if (nativeWidth > width || nativeHeight > height) {
411            float dx = ((float)nativeWidth) / ((float)width);
412            float dy = ((float)nativeHeight) / ((float)height);
413            if (dx > dy) {
414                bitmapWidth = width;
415                bitmapHeight = nativeHeight / dx;
416            } else {
417                bitmapWidth = nativeWidth / dy;
418                bitmapHeight = height;
419            }
420            // Create the bitmap from file
421            if (nativeWidth / bitmapWidth > 1) {
422                final BitmapFactory.Options options = new BitmapFactory.Options();
423                options.inSampleSize = nativeWidth / (int)bitmapWidth;
424                srcBitmap = BitmapFactory.decodeFile(filename, options);
425            } else {
426                srcBitmap = BitmapFactory.decodeFile(filename);
427            }
428        } else {
429            bitmapWidth = width;
430            bitmapHeight = height;
431            srcBitmap = BitmapFactory.decodeFile(filename);
432        }
433
434        if (srcBitmap == null) {
435            Log.e(TAG, "generateThumbnail: Cannot decode image bytes");
436            throw new IOException("Cannot decode file: " + mFilename);
437        }
438
439        // Create the canvas bitmap
440        final Bitmap bitmap = Bitmap.createBitmap((int)bitmapWidth, (int)bitmapHeight,
441                Bitmap.Config.ARGB_8888);
442        final Canvas canvas = new Canvas(bitmap);
443        canvas.drawBitmap(srcBitmap, new Rect(0, 0, srcBitmap.getWidth(), srcBitmap.getHeight()),
444                new Rect(0, 0, (int)bitmapWidth, (int)bitmapHeight), sResizePaint);
445        // Release the source bitmap
446        srcBitmap.recycle();
447        return bitmap;
448    }
449}
450