/* * Open Source Physics software is free software as described near the bottom of this code file. * * For additional information and documentation on Open Source Physics please see: * <http://www.opensourcephysics.org/> */ /* * The org.opensourcephysics.media.core package defines the Open Source Physics * media framework for working with video and other media. * * Copyright (c) 2014 Douglas Brown and Wolfgang Christian. * * This is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA * or view the license online at http://www.gnu.org/copyleft/gpl.html * * For additional information and documentation on Open Source Physics, * please see <http://www.opensourcephysics.org/>. */ package org.opensourcephysics.media.core; import java.awt.Component; import java.awt.Dimension; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import javax.swing.JOptionPane; import javax.swing.JPanel; import org.opensourcephysics.controls.XML; import org.opensourcephysics.controls.XMLControl; import org.opensourcephysics.tools.Resource; import org.opensourcephysics.tools.ResourceLoader; /** * This is a Video assembled from one or more still images. * * @author Douglas Brown * @version 1.0 */ public class ImageVideo extends VideoAdapter { // instance fields protected Component observer = new JPanel(); // image observer protected BufferedImage[] images = new BufferedImage[0]; // image array protected String[] paths = new String[0]; // relative image paths protected boolean readOnly = false; // true if images are only loaded from files as needed protected double deltaT = 100; // frame duration in milliseconds /** * Creates an ImageVideo and loads a named image or image sequence. * * @param imageName the name of the image file * @throws IOException */ public ImageVideo(String imageName) throws IOException { readOnly = true; append(imageName); } /** * Creates an ImageVideo and loads a named image or image sequence. * * @param imageName the name of the image file * @param sequence true to automatically load image sequence, if any * @throws IOException */ public ImageVideo(String imageName, boolean sequence) throws IOException { this(imageName, sequence, true); } /** * Creates an ImageVideo and loads a named image or image sequence. * * @param imageName the name of the image file * @param sequence true to automatically load image sequence, if any * @param fileBased true if images will be loaded from files only as needed * @throws IOException */ public ImageVideo(String imageName, boolean sequence, boolean fileBased) throws IOException { readOnly = fileBased; append(imageName, sequence); } /** * Creates an ImageVideo from an image. * * @param image the image */ public ImageVideo(Image image) { if(image!=null) { insert(new Image[] {image}, 0, null); } } /** * Creates an ImageVideo from an image array. * * @param images the image array */ public ImageVideo(Image[] images) { if((images!=null)&&(images.length>0)&&(images[0]!=null)) { insert(images, 0, null); } } /** * Overrides VideoAdapter setFrameNumber method. * * @param n the desired frame number */ public void setFrameNumber(int n) { super.setFrameNumber(n); rawImage = getImageAtFrame(getFrameNumber(), rawImage); isValidImage = false; isValidFilteredImage = false; firePropertyChange("framenumber", null, new Integer(getFrameNumber())); //$NON-NLS-1$ } /** * Gets the current video time in milliseconds. * * @return the current time in milliseconds, or -1 if not known */ public double getTime() { return getFrameNumber()*deltaT; } /** * Sets the frame duration in milliseconds. * * @param millis the desired frame duration in milliseconds */ public void setFrameDuration(double millis) { deltaT = millis; } /** * Sets the video time in milliseconds. * * @param millis the desired time in milliseconds */ public void setTime(double millis) { int frameNum = (int)(millis/deltaT); frameNum = Math.max(frameNum, 0); frameNum = Math.min(frameNum, getFrameCount()-1); setFrameNumber(frameNum); } /** * Gets the start time in milliseconds. * * @return the start time in milliseconds, or -1 if not known */ public double getStartTime() { return 0; } /** * Sets the start time in milliseconds. NOTE: the actual start time * is normally set to the beginning of a frame. * * @param millis the desired start time in milliseconds */ public void setStartTime(double millis) { /** not implemented */ } /** * Gets the end time in milliseconds. * * @return the end time in milliseconds, or -1 if not known */ public double getEndTime() { return getDuration(); } /** * Sets the end time in milliseconds. NOTE: the actual end time * is set to the end of a frame. * * @param millis the desired end time in milliseconds */ public void setEndTime(double millis) { /** not implemented */ } /** * Gets the duration of the video. * * @return the duration of the video in milliseconds, or -1 if not known */ @Override public double getDuration() { return length()*deltaT; } /** * Gets the start time of the specified frame in milliseconds. * * @param n the frame number * @return the start time of the frame in milliseconds, or -1 if not known */ public double getFrameTime(int n) { return n*deltaT; } /** * Gets the image array. * * @return the image array */ public Image[] getImages() { return images; } /** * Appends the named image or image sequence to the end of this video. * This method will ask user whether to load sequences, if any. * * @param imageName the image name * @throws IOException */ public void append(String imageName) throws IOException { insert(imageName, length()); } /** * Appends the named image or image sequence to the end of this video. * * @param imageName the image name * @param sequence true to automatically load image sequence, if any * @throws IOException */ public void append(String imageName, boolean sequence) throws IOException { insert(imageName, length(), sequence); } /** * Inserts the named image or image sequence at the specified index. * This method will ask user whether to load sequences, if any. * * @param imageName the image name * @param index the index * @throws IOException */ public void insert(String imageName, int index) throws IOException { Object[] array = loadImages(imageName, true, // ask user for confirmation true); // allow sequences, if any Image[] images = (Image[]) array[0]; if(images.length>0) { String[] paths = (String[]) array[1]; insert(images, index, paths); } } /** * Inserts the named image or image sequence at the specified index. * * @param imageName the image name * @param index the index * @param sequence true to automatically load image sequence, if any * @throws IOException */ public void insert(String imageName, int index, boolean sequence) throws IOException { Object[] array = loadImages(imageName, false, // don't ask user for confirmation sequence); Image[] images = (Image[]) array[0]; if(images.length>0) { String[] paths = (String[]) array[1]; insert(images, index, paths); } } /** * Inserts an image at the specified index. * * @param image the image * @param index the index */ public void insert(Image image, int index) { if(image==null) { return; } insert(new Image[] {image}, index, null); } /** * Removes the image at the specified index. * * @param index the index * @return the path of the image, or null if none removed */ public String remove(int index) { if (readOnly) return null; int len = images.length; if((len==1)||(len<=index)) { return null; // don't remove the only image } String removed = paths[index]; BufferedImage[] newArray = new BufferedImage[len-1]; System.arraycopy(images, 0, newArray, 0, index); System.arraycopy(images, index+1, newArray, index, len-1-index); images = newArray; String[] newPaths = new String[len-1]; System.arraycopy(paths, 0, newPaths, 0, index); System.arraycopy(paths, index+1, newPaths, index, len-1-index); paths = newPaths; if(index<len-1) { rawImage = getImageAtFrame(index, rawImage); } else { rawImage = getImageAtFrame(index-1, rawImage); } frameCount = images.length; endFrameNumber = frameCount-1; Dimension newDim = getSize(); if((newDim.height!=size.height)||(newDim.width!=size.width)) { this.firePropertyChange("size", size, newDim); //$NON-NLS-1$ size = newDim; refreshBufferedImage(); } return removed; } /** * Gets the size of this video. * * @return the maximum size of the images */ public Dimension getSize() { int w = images[0].getWidth(observer); int h = images[0].getHeight(observer); for(int i = 1; i<images.length; i++) { w = Math.max(w, images[i].getWidth(observer)); h = Math.max(h, images[i].getHeight(observer)); } return new Dimension(w, h); } /** * Returns true if all of the images are associated with files. * * @return true if all images are file-based */ public boolean isFileBased() { return getValidPaths().length==paths.length; } /** * Returns true if all images are loaded into memory. * * @return true if editable */ public boolean isEditable() { return !readOnly; } /** * Sets the editable property. * @param edit true to edit * @throws IOException */ public void setEditable(boolean edit) throws IOException { if (edit && isEditable()) return; // already editable if (!edit && !isEditable()) return; // already uneditable // get path to first image String imagePath = paths[0]; readOnly = !edit; // reset arrays if (readOnly) paths = new String[0]; images = new BufferedImage[0]; System.gc(); append(imagePath, true); } /** * Allows user to save invalid images, if any. * * @return true if saved */ public boolean saveInvalidImages() { // collect invalid paths and images ArrayList<String> pathList = new ArrayList<String>(); ArrayList<BufferedImage> imageList = new ArrayList<BufferedImage>(); for(int i = 0; i<paths.length; i++) { if(paths[i].equals("")) { //$NON-NLS-1$ pathList.add(paths[i]); imageList.add(images[i]); } } if(pathList.isEmpty()) { return true; } // offer to save invalid paths int approved = JOptionPane.showConfirmDialog(null, MediaRes.getString("ImageVideo.Dialog.UnsavedImages.Message1")+XML.NEW_LINE //$NON-NLS-1$ +MediaRes.getString("ImageVideo.Dialog.UnsavedImages.Message2"), //$NON-NLS-1$ MediaRes.getString("ImageVideo.Dialog.UnsavedImages.Title"), //$NON-NLS-1$ JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); // if approved, use chooser to select file, save images and return true if(approved==JOptionPane.YES_OPTION) { try { ImageVideoRecorder recorder = new ImageVideoRecorder(); recorder.setExpectedFrameCount(imageList.size()); File file = recorder.selectFile(); if(file==null) { return false; } String fileName = file.getAbsolutePath(); BufferedImage[] imagesToSave = imageList.toArray(new BufferedImage[0]); String[] pathArray = ImageVideoRecorder.saveImages(fileName, imagesToSave); int j = 0; for(int i = 0; i<paths.length; i++) { if(paths[i].equals("")) { //$NON-NLS-1$ paths[i] = pathArray[j++]; } } if (getProperty("name")==null) { //$NON-NLS-1$ setProperty("name", XML.getName(fileName)); //$NON-NLS-1$ setProperty("path", fileName); //$NON-NLS-1$ setProperty("absolutePath", fileName); //$NON-NLS-1$ } return true; } catch(IOException ex) { ex.printStackTrace(); } } // if declined, return false return false; } //_______________________ private/protected methods ____________________________ /** * Gets the image for a specified frame number. * * @param frameNumber the frame number * @param defaultImage the image to return if no other found */ private Image getImageAtFrame(int frameNumber, Image defaultImage) { if (readOnly && frameNumber<paths.length) { if(!paths[frameNumber].equals("")) {//$NON-NLS-1$ Image image = ResourceLoader.getImage(paths[frameNumber]); if (image!=null) return image; } } else if (frameNumber<images.length && images[frameNumber]!=null) { return images[frameNumber]; } return defaultImage; } private int length() { if (readOnly) return paths.length; return images.length; } /** * Loads an image or image sequence specified by name. This returns * an Object[] containing an Image[] at index 0 and a String[] at index 1. * * @param imagePath the image path * @param alwaysAsk true to always ask for sequence confirmation * @param sequence true to automatically load sequences (if not alwaysAsk) * @return an array of loaded images and their corresponding paths * @throws IOException */ private Object[] loadImages(String imagePath, boolean alwaysAsk, boolean sequence) throws IOException { Resource res = ResourceLoader.getResource(imagePath); if(res==null) { throw new IOException("Image "+imagePath+" not found"); //$NON-NLS-1$ //$NON-NLS-2$ } Image image = res.getImage(); if(image==null) { throw new IOException("\""+imagePath+"\" is not an image"); //$NON-NLS-1$ //$NON-NLS-2$ } if(getProperty("name")==null) { //$NON-NLS-1$ setProperty("name", XML.getName(imagePath)); //$NON-NLS-1$ setProperty("path", imagePath); //$NON-NLS-1$ setProperty("absolutePath", res.getAbsolutePath()); //$NON-NLS-1$ } if(!alwaysAsk&&!sequence) { Image[] images = new Image[] {image}; String[] paths = new String[] {imagePath}; return new Object[] {images, paths}; } ArrayList<String> pathList = new ArrayList<String>(); pathList.add(imagePath); // look for image sequence (numbered image names) String name = XML.getName(imagePath); String extension = ""; //$NON-NLS-1$ int i = imagePath.lastIndexOf('.'); if((i>0)&&(i<imagePath.length()-1)) { extension = imagePath.substring(i).toLowerCase(); imagePath = imagePath.substring(0, i); // now free of extension } int len = imagePath.length(); int n = 0; // first find the number of digits in name end int digits = 1; for(; digits<len; digits++) { try { n = Integer.parseInt(imagePath.substring(len-digits)); } catch(NumberFormatException ex) { break; } } digits--; // failed at digits, so go back one if(digits==0) { // no number found, so load single image Image[] images = new Image[] {image}; String[] paths = new String[] {imagePath+extension}; return new Object[] {images, paths}; } // image name ends with number, so look for sequence ArrayList<Image> imageList = new ArrayList<Image>(); imageList.add(image); int limit = 10; digits = Math.min(digits, 4); switch(digits) { case 1 : limit = 10; break; case 2 : limit = 100; break; case 3 : limit = 1000; break; case 4 : limit = 10000; } String root = imagePath.substring(0, len-digits); try { boolean asked = false; while(n<limit-1) { n++; // fill with leading zeros if nec String num = String.valueOf(n); int zeros = digits-num.length(); for(int k = 0; k<zeros; k++) { num = "0"+num; //$NON-NLS-1$ } imagePath = root+num+extension; // load images only if not loading as needed if (!readOnly || imageList.isEmpty()) { image = ResourceLoader.getImage(imagePath); if(image==null) { break; } } // if loading as needed, just check that the resource exists else if(ResourceLoader.getResource(imagePath)==null) break; if(!asked&&alwaysAsk) { asked = true; // strip path from image name int response = JOptionPane.showOptionDialog(null, "\""+name+"\" "+MediaRes.getString("ImageVideo.Dialog.LoadSequence.Message")+XML.NEW_LINE+ //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ MediaRes.getString("ImageVideo.Dialog.LoadSequence.Query"), //$NON-NLS-1$ MediaRes.getString("ImageVideo.Dialog.LoadSequence.Title"), //$NON-NLS-1$ JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, new String[] {MediaRes.getString("ImageVideo.Dialog.LoadSequence.Button.SingleImage"), MediaRes.getString("ImageVideo.Dialog.LoadSequence.Button.AllImages")}, //$NON-NLS-1$ //$NON-NLS-2$ MediaRes.getString("ImageVideo.Dialog.LoadSequence.Button.AllImages")); //$NON-NLS-1$ if(response==JOptionPane.YES_OPTION) { break; } } // always add first image to list, but later images only if not loading as needed if (!readOnly || imageList.isEmpty()) { imageList.add(image); } pathList.add(imagePath); } } catch(NumberFormatException ex) { ex.printStackTrace(); } Image[] images = imageList.toArray(new Image[0]); String[] paths = pathList.toArray(new String[0]); return new Object[] {images, paths}; } /** * Returns the valid paths (i.e., those that are not ""). * Invalid paths are associated with pasted images rather than files. * * @return the valid paths */ public String[] getValidPaths() { ArrayList<String> pathList = new ArrayList<String>(); for(int i = 0; i<paths.length; i++) { if(!paths[i].equals("")) {//$NON-NLS-1$ pathList.add(paths[i]); } } return pathList.toArray(new String[0]); } /** * Returns the valid paths (i.e., those that are not "") relative to a bae path. * Invalid paths are associated with pasted images rather than files. * * @param base a base path * @return the valid relative paths */ protected String[] getValidPathsRelativeTo(String base) { ArrayList<String> pathList = new ArrayList<String>(); for(int i = 0; i<paths.length; i++) { if(!paths[i].equals("")) { //$NON-NLS-1$ pathList.add(XML.getPathRelativeTo(paths[i], base)); } } return pathList.toArray(new String[0]); } /** * Inserts images starting at the specified index. * * @param newImages an array of images * @param index the insertion index * @param imagePaths array of image file paths. */ protected void insert(Image[] newImages, int index, String[] imagePaths) { if (readOnly && imagePaths==null) return; int len = length(); index = Math.min(index, len); // in case some prev images not successfully loaded int n = newImages.length; // convert new images to BufferedImage if nec BufferedImage[] buf = new BufferedImage[n]; for(int i = 0; i<newImages.length; i++) { Image im = newImages[i]; if(im instanceof BufferedImage) { buf[i] = (BufferedImage) im; } else { int w = im.getWidth(null); int h = im.getHeight(null); buf[i] = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); buf[i].createGraphics().drawImage(im, 0, 0, null); } } // insert new images BufferedImage[] newArray = new BufferedImage[len+n]; System.arraycopy(images, 0, newArray, 0, index); System.arraycopy(buf, 0, newArray, index, n); System.arraycopy(images, index, newArray, index+n, len-index); images = newArray; // create empty paths if null if(imagePaths==null) { imagePaths = new String[newImages.length]; for(int i = 0; i<imagePaths.length; i++) { imagePaths[i] = ""; //$NON-NLS-1$ } } // insert new paths n = imagePaths.length; String[] newPaths = new String[len+n]; System.arraycopy(paths, 0, newPaths, 0, index); System.arraycopy(imagePaths, 0, newPaths, index, n); System.arraycopy(paths, index, newPaths, index+n, len-index); paths = newPaths; rawImage = getImageAtFrame(index, rawImage); frameCount = length(); endFrameNumber = frameCount-1; if(coords==null) { size = new Dimension(rawImage.getWidth(observer), rawImage.getHeight(observer)); refreshBufferedImage(); // create coordinate system and relativeAspects coords = new ImageCoordSystem(frameCount); coords.addPropertyChangeListener(this); aspects = new DoubleArray(frameCount, 1); } else { coords.setLength(frameCount); aspects.setLength(frameCount); } Dimension newDim = getSize(); if((newDim.height!=size.height)||(newDim.width!=size.width)) { this.firePropertyChange("size", size, newDim); //$NON-NLS-1$ size = newDim; refreshBufferedImage(); } } //______________________________ static XML.Loader_________________________ /** * Returns an XML.ObjectLoader to save and load ImageVideo data. * * @return the object loader */ public static XML.ObjectLoader getLoader() { return new Loader(); } /** * A class to save and load ImageVideo data. */ static class Loader implements XML.ObjectLoader { /** * Saves ImageVideo data to an XMLControl. * * @param control the control to save to * @param obj the ImageVideo object to save */ public void saveObject(XMLControl control, Object obj) { ImageVideo video = (ImageVideo) obj; String base = (String) video.getProperty("base"); //$NON-NLS-1$ String[] paths = video.getValidPathsRelativeTo(base); if(paths.length>0) { control.setValue("paths", paths); //$NON-NLS-1$ control.setValue("path", paths[0]); //$NON-NLS-1$ } if(!video.getFilterStack().isEmpty()) { control.setValue("filters", video.getFilterStack().getFilters()); //$NON-NLS-1$ } control.setValue("delta_t", video.deltaT); //$NON-NLS-1$ } /** * Creates a new ImageVideo. * * @param control the control * @return the new ImageVideo */ public Object createObject(XMLControl control) { String[] paths = (String[]) control.getObject("paths"); //$NON-NLS-1$ // legacy code that opens single image or sequence if(paths==null) { try { String path = control.getString("path"); //$NON-NLS-1$ boolean seq = control.getBoolean("sequence"); //$NON-NLS-1$ if(path!=null) { ImageVideo vid = new ImageVideo(path, seq); return vid; } } catch(IOException ex) { ex.printStackTrace(); return null; } } // pre-2007 code boolean[] sequences = (boolean[]) control.getObject("sequences"); //$NON-NLS-1$ if(sequences!=null) { try { ImageVideo vid = new ImageVideo(paths[0], sequences[0]); for(int i = 1; i<paths.length; i++) { vid.append(paths[i], sequences[i]); } return vid; } catch(Exception ex) { ex.printStackTrace(); return null; } } // 2007+ code if(paths.length==0) { return null; } ImageVideo vid = null; ArrayList<String> badPaths = null; for(int i = 0; i<paths.length; i++) { try { if(vid==null) { vid = new ImageVideo(paths[i], false); } else { vid.append(paths[i], false); } } catch(Exception ex) { if(badPaths==null) { badPaths = new ArrayList<String>(); } badPaths.add("\""+paths[i]+"\""); //$NON-NLS-1$ //$NON-NLS-2$ } } if(badPaths!=null) { String s = badPaths.get(0); for(int i = 1; i<badPaths.size(); i++) { s += ", "+badPaths.get(i); //$NON-NLS-1$ } JOptionPane.showMessageDialog(null, MediaRes.getString("ImageVideo.Dialog.MissingImages.Message") //$NON-NLS-1$ +":\n"+s, //$NON-NLS-1$ MediaRes.getString("ImageVideo.Dialog.MissingImages.Title"), //$NON-NLS-1$ JOptionPane.WARNING_MESSAGE); } if(vid==null) { return null; } vid.rawImage = vid.images[0]; Collection<?> filters = Collection.class.cast(control.getObject("filters")); //$NON-NLS-1$ if(filters!=null) { vid.getFilterStack().clear(); Iterator<?> it = filters.iterator(); while(it.hasNext()) { Filter filter = (Filter) it.next(); vid.getFilterStack().addFilter(filter); } } String path = paths[0]; String ext = XML.getExtension(path); VideoType type = VideoIO.getVideoType("image", ext); //$NON-NLS-1$ if (type!=null) vid.setProperty("video_type", type); //$NON-NLS-1$ vid.deltaT = control.getDouble("delta_t"); //$NON-NLS-1$ return vid; } /** * This does nothing, but is required by the XML.ObjectLoader interface. * * @param control the control * @param obj the ImageVideo object * @return the loaded object */ public Object loadObject(XMLControl control, Object obj) { return obj; } } } /* * Open Source Physics software is free software; you can redistribute * it and/or modify it under the terms of the GNU General Public License (GPL) as * published by the Free Software Foundation; either version 2 of the License, * or(at your option) any later version. * Code that uses any portion of the code in the org.opensourcephysics package * or any subpackage (subdirectory) of this package must must also be be released * under the GNU GPL license. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA * or view the license online at http://www.gnu.org/copyleft/gpl.html * * Copyright (c) 2007 The Open Source Physics project * http://www.opensourcephysics.org */