package processing.app.tools;
/*
* Nearly all of this code is
* Copyright (c) 2010-2011 Werner Randelshofer, Immensee, Switzerland.
* All rights reserved.
* (However, he should not be held responsible for the current mess of a hack
* that it has become.)
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import java.util.prefs.Preferences;
import javax.imageio.ImageIO;
import javax.sound.sampled.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.filechooser.FileSystemView;
import processing.app.Base;
import processing.app.Language;
import ch.randelshofer.gui.datatransfer.FileTextFieldTransferHandler;
import ch.randelshofer.media.mp3.MP3AudioInputStream;
import ch.randelshofer.media.quicktime.QuickTimeWriter;
/**
* Hacked from Werner Randelshofer's QuickTimeWriter demo. The original version
* can be found <a href="http://www.randelshofer.ch/blog/2010/10/writing-quicktime-movies-in-pure-java/">here</a>.
* <p>
* A more up-to-date version of the project is
* <a href="http://www.randelshofer.ch/monte/">here</a>.
* Problem is, it's too big, so we don't want to merge it into our code.
* <p>
* Broken out as a separate project because the license (CC) probably isn't
* compatible with the rest of Processing and we don't want any confusion.
* <p>
* Added JAI ImageIO to support lots of other image file formats [131008].
* Also copied the Processing TGA implementation.
* <p>
* Added support for the gamma ('gama') atom [131008].
* <p>
* A few more notes on the implementation:
* <ul>
* <li> The dialog box is super ugly. It's a hacked up version of the previous
* interface, but I'm too scared to pull that GUI layout code apart.
* <li> The 'None' compressor seems to have bugs, so just disabled it instead.
* <li> The 'pass through' option seems to be broken, so it's been removed.
* In its place is an option to use the same width/height as the originals.
* <li> When this new 'pass through' is set, there's some nastiness with how
* the 'final' width/height variables are passed to the movie maker.
* This is an easy fix but needs a couple minutes.
* </ul>
* Ben Fry 2011-09-06, updated 2013-10-09
*/
public class MovieMaker extends JFrame implements Tool {
private Preferences prefs;
public String getMenuTitle() {
return Language.text("movie_maker");
}
public void run() {
setVisible(true);
}
public void init(Base base) {
initComponents(base.getActiveEditor() == null);
((JComponent) getContentPane()).setBorder(new EmptyBorder(12, 18, 18, 18));
imageFolderField.setTransferHandler(new FileTextFieldTransferHandler(JFileChooser.DIRECTORIES_ONLY));
soundFileField.setTransferHandler(new FileTextFieldTransferHandler());
JComponent[] smallComponents = {
compressionBox,
compressionLabel,
fpsField,
fpsLabel,
widthField,
widthLabel,
heightField,
heightLabel,
originalSizeCheckBox,
};
for (JComponent c : smallComponents) {
c.putClientProperty("JComponent.sizeVariant", "small");
}
// Get Preferences
prefs = Preferences.userNodeForPackage(MovieMaker.class);
imageFolderField.setText(prefs.get("movie.imageFolder", ""));
soundFileField.setText(prefs.get("movie.soundFile", ""));
widthField.setText("" + prefs.getInt("movie.width", 640));
heightField.setText("" + prefs.getInt("movie.height", 480));
boolean original = prefs.getBoolean("movie.originalSize", false);
originalSizeCheckBox.setSelected(original);
widthField.setEnabled(!original);
heightField.setEnabled(!original);
String fps = "" + prefs.getDouble("movie.fps", 30);
if (fps.endsWith(".0")) {
fps = fps.substring(0, fps.length() - 2);
}
fpsField.setText(fps);
compressionBox.setSelectedIndex(Math.max(0, Math.min(compressionBox.getItemCount() - 1, prefs.getInt("movie.compression", 0))));
originalSizeCheckBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
boolean enabled = !originalSizeCheckBox.isSelected();
widthField.setEnabled(enabled);
heightField.setEnabled(enabled);
}
});
// String streaming = prefs.get("movie.streaming", "fastStartCompressed");
// for (Enumeration<AbstractButton> i = streamingGroup.getElements(); i.hasMoreElements();) {
// AbstractButton btn = i.nextElement();
// if (btn.getActionCommand().equals(streaming)) {
// btn.setSelected(true);
// break;
// }
// }
// scoot everybody around
pack();
// center the frame on screen
setLocationRelativeTo(null);
}
/**
* Registers key events for a Ctrl-W and ESC with an ActionListener
* that will take care of disposing the window.
*/
static public void registerWindowCloseKeys(JRootPane root,
ActionListener disposer) {
KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
root.registerKeyboardAction(disposer, stroke,
JComponent.WHEN_IN_FOCUSED_WINDOW);
int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
stroke = KeyStroke.getKeyStroke('W', modifiers);
root.registerKeyboardAction(disposer, stroke,
JComponent.WHEN_IN_FOCUSED_WINDOW);
}
private void initComponents(final boolean standalone) {
imageFolderHelpLabel = new JLabel();
imageFolderField = new JTextField();
chooseImageFolderButton = new JButton();
soundFileHelpLabel = new JLabel();
soundFileField = new JTextField();
chooseSoundFileButton = new JButton();
createMovieButton = new JButton();
widthLabel = new JLabel();
widthField = new JTextField();
heightLabel = new JLabel();
heightField = new JTextField();
compressionLabel = new JLabel();
compressionBox = new JComboBox<String>();
fpsLabel = new JLabel();
fpsField = new JTextField();
originalSizeCheckBox = new JCheckBox();
setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
setVisible(false);
}
});
registerWindowCloseKeys(getRootPane(), new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
if (standalone) {
System.exit(0);
} else {
setVisible(false);
}
}
});
setTitle(Language.text("movie_maker.title"));
aboutLabel = new JLabel(Language.text("movie_maker.blurb"));
imageFolderHelpLabel.setText(Language.text("movie_maker.image_folder_help_label"));
chooseImageFolderButton.setText(Language.text("movie_maker.choose_button"));
//chooseImageFolderButton.addActionListener(formListener);
chooseImageFolderButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Chooser.selectFolder(MovieMaker.this,
Language.text("movie_maker.select_image_folder"),
new File(imageFolderField.getText()),
new Chooser.Callback() {
void select(File file) {
if (file != null) {
imageFolderField.setText(file.getAbsolutePath());
}
}
});
}
});
soundFileHelpLabel.setText(Language.text("movie_maker.sound_file_help_label"));
chooseSoundFileButton.setText(Language.text("movie_maker.choose_button"));
//chooseSoundFileButton.addActionListener(formListener);
chooseSoundFileButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Chooser.selectInput(MovieMaker.this,
Language.text("movie_maker.select_sound_file"),
new File(soundFileField.getText()),
new Chooser.Callback() {
void select(File file) {
if (file != null) {
soundFileField.setText(file.getAbsolutePath());
}
}
});
}
});
createMovieButton.setText(Language.text("movie_maker.create_movie_button"));
// createMovieButton.addActionListener(formListener);
createMovieButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String lastPath = prefs.get("movie.outputFile", null);
File lastFile = lastPath == null ? null : new File(lastPath);
Chooser.selectOutput(MovieMaker.this,
Language.text("movie_maker.save_dialog_prompt"),
lastFile,
new Chooser.Callback() {
@Override
void select(File file) {
if (file != null) {
String path = file.getAbsolutePath();
if (!path.toLowerCase().endsWith(".mov")) {
path += ".mov";
}
prefs.put("movie.outputFile", path);
createMovie(new File(path));
// final File target = new File(path);
// //new Thread(new Runnable() {
// EventQueue.invokeLater(new Runnable() {
//
// @Override
// public void run() {
// createMovie(target);
// }
//
// });
}
}
});
}
});
Font font = new Font("Dialog", Font.PLAIN, 11);
widthLabel.setFont(font);
widthLabel.setText(Language.text("movie_maker.width"));
widthField.setColumns(4);
widthField.setFont(font);
widthField.setText("320");
heightLabel.setFont(font);
heightLabel.setText(Language.text("movie_maker.height"));
heightField.setColumns(4);
heightField.setFont(font);
heightField.setText("240");
compressionLabel.setFont(font);
compressionLabel.setText(Language.text("movie_maker.compression"));
compressionBox.setFont(font);
//compressionBox.setModel(new DefaultComboBoxModel(new String[] { "None", "Animation", "JPEG", "PNG" }));
compressionBox.setModel(new DefaultComboBoxModel<String>(
new String[] {
Language.text("movie_maker.compression.animation"),
Language.text("movie_maker.compression.jpeg"),
Language.text("movie_maker.compression.png")
}
));
fpsLabel.setFont(font);
fpsLabel.setText(Language.text("movie_maker.framerate"));
fpsField.setColumns(4);
fpsField.setFont(font);
fpsField.setText("30");
originalSizeCheckBox.setFont(font);
originalSizeCheckBox.setText(Language.text("movie_maker.orig_size_button"));
originalSizeCheckBox.setToolTipText(Language.text("movie_maker.orig_size_tooltip"));
// streamingLabel.setText("Prepare for Internet Streaming");
//
// streamingGroup.add(noPreparationRadio);
// noPreparationRadio.setFont(font);
// noPreparationRadio.setSelected(true);
// noPreparationRadio.setText("No preparation");
// noPreparationRadio.setActionCommand("none");
// noPreparationRadio.addActionListener(formListener);
//
// streamingGroup.add(fastStartRadio);
// fastStartRadio.setFont(font);
// fastStartRadio.setText("Fast Start");
// fastStartRadio.setActionCommand("fastStart");
// fastStartRadio.addActionListener(formListener);
//
// streamingGroup.add(fastStartCompressedRadio);
// fastStartCompressedRadio.setFont(font);
// fastStartCompressedRadio.setText("Fast Start - Compressed Header");
// fastStartCompressedRadio.setActionCommand("fastStartCompressed");
// fastStartCompressedRadio.addActionListener(formListener);
GroupLayout layout = new GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGap(61, 61, 61)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(widthLabel)
.addComponent(fpsLabel))
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addComponent(fpsField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(compressionLabel)
.addGap(1, 1, 1)
.addComponent(compressionBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(originalSizeCheckBox))
.addGroup(layout.createSequentialGroup()
.addComponent(widthField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(heightLabel)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(heightField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)))
.addGap(41, 41, 41))
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addComponent(aboutLabel, GroupLayout.DEFAULT_SIZE, 484, Short.MAX_VALUE)
.addComponent(imageFolderHelpLabel)
.addComponent(soundFileHelpLabel)
.addGroup(layout.createSequentialGroup()
.addComponent(soundFileField, GroupLayout.DEFAULT_SIZE, 372, Short.MAX_VALUE)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(chooseSoundFileButton))
.addComponent(createMovieButton, GroupLayout.Alignment.TRAILING)
.addGroup(GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addComponent(imageFolderField, GroupLayout.DEFAULT_SIZE, 372, Short.MAX_VALUE)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(chooseImageFolderButton))))
.addGroup(layout.createSequentialGroup()
.addContainerGap())))
);
layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(aboutLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addGap(18, 18, 18)
.addComponent(imageFolderHelpLabel)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(imageFolderField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addComponent(chooseImageFolderButton))
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(widthLabel)
.addComponent(widthField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addComponent(heightLabel)
.addComponent(heightField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(compressionBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addComponent(fpsLabel)
.addComponent(fpsField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addComponent(compressionLabel)
.addComponent(originalSizeCheckBox))
.addGap(18, 18, 18)
.addComponent(soundFileHelpLabel)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(soundFileField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addComponent(chooseSoundFileButton))
.addGap(18, 18, 18)
.addComponent(createMovieButton)
.addContainerGap())
);
pack();
}
// Code for dispatching events from components to event handlers.
// private class FormListener implements java.awt.event.ActionListener {
// FormListener() {}
// public void actionPerformed(java.awt.event.ActionEvent evt) {
// if (evt.getSource() == chooseImageFolderButton) {
// MovieMaker.this.chooseImageFolder(evt);
// }
// else if (evt.getSource() == chooseSoundFileButton) {
// MovieMaker.this.chooseSoundFile(evt);
// }
// else if (evt.getSource() == createMovieButton) {
// MovieMaker.this.createMovie(evt);
// }
//// else if (evt.getSource() == fastStartCompressedRadio) {
//// MovieMaker.this.streamingRadioPerformed(evt);
//// }
//// else if (evt.getSource() == fastStartRadio) {
//// MovieMaker.this.streamingRadioPerformed(evt);
//// }
//// else if (evt.getSource() == noPreparationRadio) {
//// MovieMaker.this.streamingRadioPerformed(evt);
//// }
// }
// }
// private void chooseImageFolder(ActionEvent evt) {
// if (imageFolderChooser == null) {
// imageFolderChooser = new JFileChooser();
// imageFolderChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
// if (imageFolderField.getText().length() > 0) {
// imageFolderChooser.setSelectedFile(new File(imageFolderField.getText()));
// } else if (soundFileField.getText().length() > 0) {
// imageFolderChooser.setCurrentDirectory(new File(soundFileField.getText()).getParentFile());
// }
// }
// if (JFileChooser.APPROVE_OPTION == imageFolderChooser.showOpenDialog(this)) {
// imageFolderField.setText(imageFolderChooser.getSelectedFile().getPath());
// }
// }
//
// private void chooseSoundFile(ActionEvent evt) {
// if (soundFileChooser == null) {
// soundFileChooser = new JFileChooser();
// if (soundFileField.getText().length() > 0) {
// soundFileChooser.setSelectedFile(new File(soundFileField.getText()));
// } else if (imageFolderField.getText().length() > 0) {
// soundFileChooser.setCurrentDirectory(new File(imageFolderField.getText()));
// }
// }
// if (JFileChooser.APPROVE_OPTION == soundFileChooser.showOpenDialog(this)) {
// soundFileField.setText(soundFileChooser.getSelectedFile().getPath());
// }
// }
// this is super naughty, and shouldn't be out here. it's a hack to get the
// ImageIcon width/height setting to work. there are better ways to do this
// given a bit of time. you know, time? the infinite but non-renewable resource?
int width, height;
private void createMovie(final File movieFile) {
createMovieButton.setEnabled(false);
// ---------------------------------
// Check input
// ---------------------------------
final File soundFile = soundFileField.getText().trim().length() == 0 ? null : new File(soundFileField.getText().trim());
final File imageFolder = imageFolderField.getText().trim().length() == 0 ? null : new File(imageFolderField.getText().trim());
//final String streaming = prefs.get("movie.streaming", "fastStartCompressed");
final String streaming = "fastStartCompressed";
if (soundFile == null && imageFolder == null) {
JOptionPane.showMessageDialog(this, Language.text("movie_maker.error.need_input"));
return;
}
final double fps;
try {
width = Integer.parseInt(widthField.getText());
height = Integer.parseInt(heightField.getText());
fps = Double.parseDouble(fpsField.getText());
} catch (Throwable t) {
JOptionPane.showMessageDialog(this, Language.text("movie_maker.error.badnumbers"));
return;
}
if (width < 1 || height < 1 || fps < 1) {
JOptionPane.showMessageDialog(this, Language.text("movie_maker.error.badnumbers"));
return;
}
final QuickTimeWriter.VideoFormat videoFormat;
switch (compressionBox.getSelectedIndex()) {
// case 0:
// videoFormat = QuickTimeWriter.VideoFormat.RAW;
// break;
case 0://1:
videoFormat = QuickTimeWriter.VideoFormat.RLE;
break;
case 1://2:
videoFormat = QuickTimeWriter.VideoFormat.JPG;
break;
case 2://3:
default:
videoFormat = QuickTimeWriter.VideoFormat.PNG;
break;
}
// ---------------------------------
// Update Preferences
// ---------------------------------
prefs.put("movie.imageFolder", imageFolderField.getText());
prefs.put("movie.soundFile", soundFileField.getText());
prefs.putInt("movie.width", width);
prefs.putInt("movie.height", height);
prefs.putDouble("movie.fps", fps);
prefs.putInt("movie.compression", compressionBox.getSelectedIndex());
prefs.putBoolean("movie.originalSize", originalSizeCheckBox.isSelected());
final boolean originalSize = originalSizeCheckBox.isSelected();
// ---------------------------------
// Create the QuickTime movie
// ---------------------------------
new SwingWorker<Throwable, Object>() {
@Override
protected Throwable doInBackground() {
try {
// Read image files
File[] imgFiles = null;
if (imageFolder != null) {
imgFiles = imageFolder.listFiles(new FileFilter() {
FileSystemView fsv = FileSystemView.getFileSystemView();
public boolean accept(File f) {
return f.isFile() && !fsv.isHiddenFile(f) &&
!f.getName().equals("Thumbs.db");
}
});
if (imgFiles == null || imgFiles.length == 0) {
return new RuntimeException(Language.text("movie_maker.error.no_images_found"));
}
Arrays.sort(imgFiles);
}
// Get the width and height if we're preserving size.
if (originalSize) {
Dimension d = findSize(imgFiles);
if (d == null) {
// No images at all? No video then.
throw new RuntimeException(Language.text("movie_maker.error.no_images_found"));
}
width = d.width;
height = d.height;
}
// Delete movie file if it already exists.
if (movieFile.exists()) {
movieFile.delete();
}
if (imageFolder != null && soundFile != null) {
writeVideoAndAudio(movieFile, imgFiles, soundFile, width, height, fps, videoFormat, /*passThrough,*/ streaming);
} else if (imageFolder != null) {
writeVideoOnlyVFR(movieFile, imgFiles, width, height, fps, videoFormat, /*passThrough,*/ streaming);
} else {
writeAudioOnly(movieFile, soundFile, streaming);
}
return null;
} catch (Throwable t) {
return t;
}
}
Dimension findSize(File[] imgFiles) {
for (int i = 0; i < imgFiles.length; i++) {
BufferedImage temp = readImage(imgFiles[i]);
if (temp != null) {
return new Dimension(temp.getWidth(), temp.getHeight());
} else {
// Nullify bad Files so we don't get errors twice.
imgFiles[i] = null;
}
}
return null;
}
@Override
protected void done() {
Throwable t;
try {
t = get();
} catch (Exception ex) {
t = ex;
}
t.printStackTrace();
JOptionPane.showMessageDialog(MovieMaker.this,
Language.text("movie_maker.error.movie_failed") + "\n" +
(t.getMessage() == null ? t.toString() : t.getMessage()),
Language.text("movie_maker.error.sorry"),
JOptionPane.ERROR_MESSAGE);
createMovieButton.setEnabled(true);
}
}.execute();
}//GEN-LAST:event_createMovie
/**
* Read an image from a file. ImageIcon doesn't don't do well with some
* file types, so we use ImageIO. ImageIO doesn't handle TGA files
* created by Processing, so this calls our own loadImageTGA().
* <br> Prints errors itself.
* @return null on error; image only if okay.
*/
private BufferedImage readImage(File file) {
try {
Thread current = Thread.currentThread();
ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
current.setContextClassLoader(getClass().getClassLoader());
BufferedImage image;
try {
image = ImageIO.read(file);
} catch (IOException e) {
System.err.println(Language.interpolate("movie_maker.error.cannot_read",
file.getAbsolutePath()));
return null;
}
current.setContextClassLoader(origLoader);
/*
String[] loadImageFormats = ImageIO.getReaderFormatNames();
if (loadImageFormats != null) {
for (String format : loadImageFormats) {
System.out.println(format);
}
}
*/
if (image == null) {
String path = file.getAbsolutePath();
String pathLower = path.toLowerCase();
// Might be an incompatible TGA or TIFF created by Processing
if (pathLower.endsWith(".tga")) {
try {
return loadImageTGA(file);
} catch (IOException e) {
cannotRead(file);
return null;
}
} else if (pathLower.endsWith(".tif") || pathLower.endsWith(".tiff")) {
cannotRead(file);
System.err.println(Language.text("movie_maker.error.avoid_tiff"));
return null;
} else {
cannotRead(file);
return null;
}
} else {
if (image.getWidth() <= 0 || image.getHeight() <= 0) {
System.err.println(Language.interpolate("movie_maker.error.cannot_read_maybe_bad", file.getAbsolutePath()));
return null;
}
}
return image;
// Catch-all is sometimes needed.
} catch (RuntimeException e) {
cannotRead(file);
return null;
}
}
static private void cannotRead(File file) {
String path = file.getAbsolutePath();
String msg = Language.interpolate("movie_maker.error.cannot_read", path);
System.err.println(msg);
}
/** variable frame rate. */
private void writeVideoOnlyVFR(File movieFile, File[] imgFiles, int width, int height, double fps, QuickTimeWriter.VideoFormat videoFormat, /*boolean passThrough,*/ String streaming) throws IOException {
File tmpFile = streaming.equals("none") ? movieFile : new File(movieFile.getPath() + ".tmp");
ProgressMonitor p = new ProgressMonitor(MovieMaker.this,
Language.interpolate("movie_maker.progress.creating_file_name", movieFile.getName()),
Language.text("movie_maker.progress.creating_output_file"),
0, imgFiles.length);
Graphics2D g = null;
BufferedImage img = null;
BufferedImage prevImg = null;
int[] data = null;
int[] prevData = null;
QuickTimeWriter qtOut = null;
try {
int timeScale = (int) (fps * 100.0);
int duration = 100;
qtOut = new QuickTimeWriter(videoFormat == QuickTimeWriter.VideoFormat.RAW ? movieFile : tmpFile);
qtOut.addVideoTrack(videoFormat, timeScale, width, height);
qtOut.setSyncInterval(0, 30);
//if (!passThrough) {
if (true) {
img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
data = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
prevImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
prevData = ((DataBufferInt) prevImg.getRaster().getDataBuffer()).getData();
g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
}
int prevImgDuration = 0;
for (int i = 0; i < imgFiles.length && !p.isCanceled(); i++) {
File f = imgFiles[i];
if (f == null) continue;
p.setNote(Language.interpolate("movie_maker.progress.processing", f.getName()));
p.setProgress(i);
//if (passThrough) {
if (false) {
qtOut.writeSample(0, f, duration);
} else {
//BufferedImage fImg = ImageIO.read(f);
BufferedImage fImg = readImage(f);
if (fImg == null) continue;
g.drawImage(fImg, 0, 0, width, height, null);
if (i != 0 && Arrays.equals(data, prevData)) {
prevImgDuration += duration;
} else {
if (prevImgDuration != 0) {
qtOut.writeFrame(0, prevImg, prevImgDuration);
}
prevImgDuration = duration;
System.arraycopy(data, 0, prevData, 0, data.length);
}
}
}
if (prevImgDuration != 0) {
qtOut.writeFrame(0, prevImg, prevImgDuration);
}
if (streaming.equals("fastStart")) {
qtOut.toWebOptimizedMovie(movieFile, false);
tmpFile.delete();
} else if (streaming.equals("fastStartCompressed")) {
qtOut.toWebOptimizedMovie(movieFile, true);
tmpFile.delete();
}
qtOut.close();
qtOut = null;
} finally {
p.close();
if (g != null) {
g.dispose();
}
if (img != null) {
img.flush();
}
if (qtOut != null) {
qtOut.close();
}
}
}
private void writeAudioOnly(File movieFile, File audioFile, String streaming) throws IOException {
File tmpFile = streaming.equals("none") ? movieFile : new File(movieFile.getPath() + ".tmp");
int length = (int) Math.min(Integer.MAX_VALUE, audioFile.length()); // file length is used for a rough progress estimate. This will only work for uncompressed audio.
ProgressMonitor p = new ProgressMonitor(MovieMaker.this,
Language.interpolate("movie_maker.progress.creating_file_name", movieFile.getName()),
Language.text("movie_maker.progress.initializing"), 0, length);
AudioInputStream audioIn = null;
QuickTimeWriter qtOut = null;
try {
qtOut = new QuickTimeWriter(tmpFile);
if (audioFile.getName().toLowerCase().endsWith(".mp3")) {
audioIn = new MP3AudioInputStream(audioFile);
} else {
audioIn = AudioSystem.getAudioInputStream(audioFile);
}
AudioFormat audioFormat = audioIn.getFormat();
//System.out.println("QuickTimeMovieMakerMain " + audioFormat);
qtOut.addAudioTrack(audioFormat);
boolean isVBR = audioFormat.getProperty("vbr") != null && ((Boolean) audioFormat.getProperty("vbr")).booleanValue();
int asSize = audioFormat.getFrameSize();
int nbOfFramesInBuffer = isVBR ? 1 : Math.max(1, 1024 / asSize);
int asDuration = (int) (audioFormat.getSampleRate() / audioFormat.getFrameRate());
//System.out.println(" frameDuration=" + asDuration);
long count = 0;
byte[] audioBuffer = new byte[asSize * nbOfFramesInBuffer];
for (int bytesRead = audioIn.read(audioBuffer);
bytesRead != -1;
bytesRead = audioIn.read(audioBuffer)) {
if (bytesRead != 0) {
int framesRead = bytesRead / asSize;
qtOut.writeSamples(0, framesRead, audioBuffer, 0, bytesRead, asDuration);
count += bytesRead;
p.setProgress((int) count);
}
if (isVBR) {
audioFormat = audioIn.getFormat();
if (audioFormat == null) {
break;
}
asSize = audioFormat.getFrameSize();
asDuration = (int) (audioFormat.getSampleRate() / audioFormat.getFrameRate());
if (audioBuffer.length < asSize) {
audioBuffer = new byte[asSize];
}
}
}
audioIn.close();
audioIn = null;
if (streaming.equals("fastStart")) {
qtOut.toWebOptimizedMovie(movieFile, false);
tmpFile.delete();
} else if (streaming.equals("fastStartCompressed")) {
qtOut.toWebOptimizedMovie(movieFile, true);
tmpFile.delete();
}
qtOut.close();
qtOut = null;
} catch (UnsupportedAudioFileException e) {
IOException ioe = new IOException(e.getMessage());
ioe.initCause(e);
throw ioe;
} finally {
p.close();
if (audioIn != null) {
audioIn.close();
}
if (qtOut != null) {
qtOut.close();
}
}
}
private void writeVideoAndAudio(File movieFile, File[] imgFiles, File audioFile,
int width, int height, double fps, QuickTimeWriter.VideoFormat videoFormat,
/*boolean passThrough,*/ String streaming) throws IOException {
File tmpFile = streaming.equals("none") ? movieFile : new File(movieFile.getPath() + ".tmp");
ProgressMonitor p = new ProgressMonitor(MovieMaker.this,
Language.interpolate("movie_maker.progress.creating_file_name", movieFile.getName()),
Language.text("movie_maker.progress.creating_output_file"),
0, imgFiles.length);
AudioInputStream audioIn = null;
QuickTimeWriter qtOut = null;
BufferedImage imgBuffer = null;
Graphics2D g = null;
try {
// Determine audio format
if (audioFile.getName().toLowerCase().endsWith(".mp3")) {
audioIn = new MP3AudioInputStream(audioFile);
} else {
audioIn = AudioSystem.getAudioInputStream(audioFile);
}
AudioFormat audioFormat = audioIn.getFormat();
boolean isVBR = audioFormat.getProperty("vbr") != null && ((Boolean) audioFormat.getProperty("vbr")).booleanValue();
// Determine duration of a single sample
int asDuration = (int) (audioFormat.getSampleRate() / audioFormat.getFrameRate());
int vsDuration = 100;
// Create writer
qtOut = new QuickTimeWriter(videoFormat == QuickTimeWriter.VideoFormat.RAW ? movieFile : tmpFile);
qtOut.addAudioTrack(audioFormat); // audio in track 0
qtOut.addVideoTrack(videoFormat, (int) (fps * vsDuration), width, height); // video in track 1
// Create audio buffer
int asSize;
byte[] audioBuffer;
if (isVBR) {
// => variable bit rate: create audio buffer for a single frame
asSize = audioFormat.getFrameSize();
audioBuffer = new byte[asSize];
} else {
// => fixed bit rate: create audio buffer for half a second
asSize = audioFormat.getChannels() * audioFormat.getSampleSizeInBits() / 8;
audioBuffer = new byte[(int) (qtOut.getMediaTimeScale(0) / 2 * asSize)];
}
// Create video buffer
//if (!passThrough) {
if (true) {
imgBuffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
g = imgBuffer.createGraphics();
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
} // Main loop
int movieTime = 0;
int imgIndex = 0;
boolean isAudioDone = false;
while ((imgIndex < imgFiles.length || !isAudioDone) && !p.isCanceled()) {
// Advance movie time by half a second (we interleave twice per second)
movieTime += qtOut.getMovieTimeScale() / 2;
// Advance audio to movie time + 1 second (audio must be ahead of video by 1 second)
while (!isAudioDone && qtOut.getTrackDuration(0) < movieTime + qtOut.getMovieTimeScale()) {
int len = audioIn.read(audioBuffer);
if (len == -1) {
isAudioDone = true;
} else {
qtOut.writeSamples(0, len / asSize, audioBuffer, 0, len, asDuration);
}
if (isVBR) {
// => variable bit rate: format can change at any time
audioFormat = audioIn.getFormat();
if (audioFormat == null) {
break;
}
asSize = audioFormat.getFrameSize();
asDuration = (int) (audioFormat.getSampleRate() / audioFormat.getFrameRate());
if (audioBuffer.length < asSize) {
audioBuffer = new byte[asSize];
}
}
}
// Advance video to movie time
for (; imgIndex < imgFiles.length && qtOut.getTrackDuration(1) < movieTime; ++imgIndex) {
// catch up with video time
p.setProgress(imgIndex);
File f = imgFiles[imgIndex];
if (f == null) continue;
p.setNote(Language.interpolate("movie_maker.progress.processing", f.getName()));
//if (passThrough) {
if (false) {
qtOut.writeSample(1, f, vsDuration);
} else {
//BufferedImage fImg = ImageIO.read(imgFiles[imgIndex]);
BufferedImage fImg = readImage(f);
if (fImg == null) continue;
g.drawImage(fImg, 0, 0, width, height, null);
fImg.flush();
qtOut.writeFrame(1, imgBuffer, vsDuration);
}
}
}
if (streaming.equals("fastStart")) {
qtOut.toWebOptimizedMovie(movieFile, false);
tmpFile.delete();
} else if (streaming.equals("fastStartCompressed")) {
qtOut.toWebOptimizedMovie(movieFile, true);
tmpFile.delete();
}
qtOut.close();
qtOut = null;
} catch (UnsupportedAudioFileException e) {
IOException ioe = new IOException(e.getMessage());
ioe.initCause(e);
throw ioe;
} finally {
p.close();
if (qtOut != null) {
qtOut.close();
}
if (audioIn != null) {
audioIn.close();
}
if (g != null) {
g.dispose();
}
if (imgBuffer != null) {
imgBuffer.flush();
}
}
}
/**
* Targa image loader for RLE-compressed TGA files.
* Code taken from PApplet, any changes here should lead to updates there.
*/
static private BufferedImage loadImageTGA(File file) throws IOException {
InputStream is = new FileInputStream(file);
try {
byte header[] = new byte[18];
int offset = 0;
do {
int count = is.read(header, offset, header.length - offset);
if (count == -1) return null;
offset += count;
} while (offset < 18);
/*
header[2] image type code
2 (0x02) - Uncompressed, RGB images.
3 (0x03) - Uncompressed, black and white images.
10 (0x0A) - Run-length encoded RGB images.
11 (0x0B) - Compressed, black and white images. (grayscale?)
header[16] is the bit depth (8, 24, 32)
header[17] image descriptor (packed bits)
0x20 is 32 = origin upper-left
0x28 is 32 + 8 = origin upper-left + 32 bits
7 6 5 4 3 2 1 0
128 64 32 16 8 4 2 1
*/
int format = 0;
final int RGB = 1;
final int ARGB = 2;
final int ALPHA = 4;
if (((header[2] == 3) || (header[2] == 11)) && // B&W, plus RLE or not
(header[16] == 8) && // 8 bits
((header[17] == 0x8) || (header[17] == 0x28))) { // origin, 32 bit
format = ALPHA;
} else if (((header[2] == 2) || (header[2] == 10)) && // RGB, RLE or not
(header[16] == 24) && // 24 bits
((header[17] == 0x20) || (header[17] == 0))) { // origin
format = RGB;
} else if (((header[2] == 2) || (header[2] == 10)) &&
(header[16] == 32) &&
((header[17] == 0x8) || (header[17] == 0x28))) { // origin, 32
format = ARGB;
}
if (format == 0) {
throw new IOException(Language.interpolate("movie_maker.error.unknown_tga_format", file.getName()));
}
int w = ((header[13] & 0xff) << 8) + (header[12] & 0xff);
int h = ((header[15] & 0xff) << 8) + (header[14] & 0xff);
//PImage outgoing = createImage(w, h, format);
int[] pixels = new int[w * h];
// where "reversed" means upper-left corner (normal for most of
// the modernized world, but "reversed" for the tga spec)
//boolean reversed = (header[17] & 0x20) != 0;
// https://github.com/processing/processing/issues/1682
boolean reversed = (header[17] & 0x20) == 0;
if ((header[2] == 2) || (header[2] == 3)) { // not RLE encoded
if (reversed) {
int index = (h-1) * w;
switch (format) {
case ALPHA:
for (int y = h-1; y >= 0; y--) {
for (int x = 0; x < w; x++) {
pixels[index + x] = is.read();
}
index -= w;
}
break;
case RGB:
for (int y = h-1; y >= 0; y--) {
for (int x = 0; x < w; x++) {
pixels[index + x] =
is.read() | (is.read() << 8) | (is.read() << 16) |
0xff000000;
}
index -= w;
}
break;
case ARGB:
for (int y = h-1; y >= 0; y--) {
for (int x = 0; x < w; x++) {
pixels[index + x] =
is.read() | (is.read() << 8) | (is.read() << 16) |
(is.read() << 24);
}
index -= w;
}
}
} else { // not reversed
int count = w * h;
switch (format) {
case ALPHA:
for (int i = 0; i < count; i++) {
pixels[i] = is.read();
}
break;
case RGB:
for (int i = 0; i < count; i++) {
pixels[i] =
is.read() | (is.read() << 8) | (is.read() << 16) |
0xff000000;
}
break;
case ARGB:
for (int i = 0; i < count; i++) {
pixels[i] =
is.read() | (is.read() << 8) | (is.read() << 16) |
(is.read() << 24);
}
break;
}
}
} else { // header[2] is 10 or 11
int index = 0;
while (index < pixels.length) {
int num = is.read();
boolean isRLE = (num & 0x80) != 0;
if (isRLE) {
num -= 127; // (num & 0x7F) + 1
int pixel = 0;
switch (format) {
case ALPHA:
pixel = is.read();
break;
case RGB:
pixel = 0xFF000000 |
is.read() | (is.read() << 8) | (is.read() << 16);
break;
case ARGB:
pixel = is.read() |
(is.read() << 8) | (is.read() << 16) | (is.read() << 24);
break;
}
for (int i = 0; i < num; i++) {
pixels[index++] = pixel;
if (index == pixels.length) break;
}
} else { // write up to 127 bytes as uncompressed
num += 1;
switch (format) {
case ALPHA:
for (int i = 0; i < num; i++) {
pixels[index++] = is.read();
}
break;
case RGB:
for (int i = 0; i < num; i++) {
pixels[index++] = 0xFF000000 |
is.read() | (is.read() << 8) | (is.read() << 16);
}
break;
case ARGB:
for (int i = 0; i < num; i++) {
pixels[index++] = is.read() |
(is.read() << 8) | (is.read() << 16) | (is.read() << 24);
}
break;
}
}
}
if (!reversed) {
int[] temp = new int[w];
for (int y = 0; y < h/2; y++) {
int z = (h-1) - y;
System.arraycopy(pixels, y*w, temp, 0, w);
System.arraycopy(pixels, z*w, pixels, y*w, w);
System.arraycopy(temp, 0, pixels, z*w, w);
}
}
}
//is.close();
int type = (format == RGB) ?
BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
BufferedImage image = new BufferedImage(w, h, type);
WritableRaster wr = image.getRaster();
wr.setDataElements(0, 0, w, h, pixels);
return image;
} finally {
is.close();
}
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
MovieMaker m = new MovieMaker();
m.init(null);
// m.init();
m.setVisible(true);
// m.pack();
}
});
}
private JLabel aboutLabel;
private JButton chooseImageFolderButton;
private JButton chooseSoundFileButton;
private JComboBox<String> compressionBox;
private JLabel compressionLabel;
// private JRadioButton fastStartCompressedRadio;
// private JRadioButton fastStartRadio;
private JTextField fpsField;
private JLabel fpsLabel;
private JTextField heightField;
private JLabel heightLabel;
private JTextField imageFolderField;
private JLabel imageFolderHelpLabel;
// private JRadioButton noPreparationRadio;
private JCheckBox originalSizeCheckBox;
private JTextField soundFileField;
private JLabel soundFileHelpLabel;
// private ButtonGroup streamingGroup;
// private JLabel streamingLabel;
private JTextField widthField;
private JLabel widthLabel;
// private JLabel copyrightLabel;
private JButton createMovieButton;
}