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