diff --git a/build/shared/tools/MovieMaker/src/processing/app/tools/MovieMaker.java b/build/shared/tools/MovieMaker/src/processing/app/tools/MovieMaker.java
index 347ee8911..29ecf02d8 100644
--- a/build/shared/tools/MovieMaker/src/processing/app/tools/MovieMaker.java
+++ b/build/shared/tools/MovieMaker/src/processing/app/tools/MovieMaker.java
@@ -13,13 +13,10 @@ package processing.app.tools;
import java.awt.*;
import java.awt.event.*;
-import java.awt.image.*;
import java.io.*;
-import java.util.*;
+import java.util.Arrays;
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;
@@ -28,8 +25,6 @@ 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;
/**
@@ -63,6 +58,7 @@ import ch.randelshofer.media.quicktime.QuickTimeWriter;
*/
public class MovieMaker extends JFrame implements Tool {
private Preferences prefs;
+ private QuickTimeMovieEngine engine;
public String getMenuTitle() {
@@ -76,6 +72,7 @@ public class MovieMaker extends JFrame implements Tool {
public void init(Base base) {
+ engine = new QuickTimeMovieEngine(this);
initComponents(base.getActiveEditor() == null);
((JComponent) getContentPane()).setBorder(new EmptyBorder(12, 18, 18, 18));
@@ -209,7 +206,6 @@ public class MovieMaker extends JFrame implements Tool {
}));
createMovieButton.setText(Language.text("movie_maker.create_movie_button"));
-// createMovieButton.addActionListener(formListener);
createMovieButton.addActionListener(e -> {
String lastPath = prefs.get("movie.outputFile", null);
File lastFile = lastPath == null ? null : new File(lastPath);
@@ -219,14 +215,7 @@ public class MovieMaker extends JFrame implements Tool {
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));
- }
+ createMovie(file);
}
});
});
@@ -249,13 +238,7 @@ public class MovieMaker extends JFrame implements Tool {
compressionLabel.setText(Language.text("movie_maker.compression"));
compressionBox.setFont(font);
//compressionBox.setModel(new DefaultComboBoxModel(new String[] { "None", "Animation", "JPEG", "PNG" }));
- compressionBox.setModel(new DefaultComboBoxModel<>(
- new String[]{
- Language.text("movie_maker.compression.animation"),
- Language.text("movie_maker.compression.jpeg"),
- Language.text("movie_maker.compression.png")
- }
- ));
+ compressionBox.setModel(new DefaultComboBoxModel<>(engine.getFormats()));
fpsLabel.setFont(font);
fpsLabel.setText(Language.text("movie_maker.framerate"));
@@ -313,37 +296,37 @@ public class MovieMaker extends JFrame implements Tool {
.addContainerGap())))
);
layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
- .addGroup(layout.createSequentialGroup()
- .addContainerGap()
- .addComponent(aboutLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
+ .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(imageFolderHelpLabel)
+ .addComponent(soundFileHelpLabel)
.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())
+ .addComponent(soundFileField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
+ .addComponent(chooseSoundFileButton))
+ .addGap(18, 18, 18)
+ .addComponent(createMovieButton)
+ .addContainerGap())
);
pack();
@@ -371,6 +354,19 @@ public class MovieMaker extends JFrame implements Tool {
int width, height;
private void createMovie(final File movieFile) {
+ if (movieFile == null) return;
+
+ /*
+ if (movieFile != null) {
+ String path = movieFile.getAbsolutePath();
+ if (!path.toLowerCase().endsWith(".mov")) {
+ path += ".mov";
+ }
+ prefs.put("movie.outputFile", path);
+ createMovie(new File(path));
+ }
+ */
+
createMovieButton.setEnabled(false);
// ---------------------------------
@@ -397,22 +393,24 @@ public class MovieMaker extends JFrame implements Tool {
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;
+ 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
@@ -453,13 +451,8 @@ public class MovieMaker extends JFrame implements Tool {
// 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;
+ width = 0;
+ height = 0;
}
// Delete movie file if it already exists.
@@ -469,11 +462,8 @@ public class MovieMaker extends JFrame implements Tool {
}
}
- if (soundFile != null) {
- writeVideoAndAudio(movieFile, imgFiles, soundFile, width, height, fps, videoFormat);
- } else {
- writeVideoOnlyVFR(movieFile, imgFiles, width, height, fps, videoFormat);
- }
+ String formatName = (String) compressionBox.getSelectedItem();
+ engine.write(movieFile, imgFiles, soundFile, width, height, fps, formatName);
}
return null;
@@ -482,19 +472,6 @@ public class MovieMaker extends JFrame implements Tool {
}
}
- 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;
@@ -506,10 +483,10 @@ public class MovieMaker extends JFrame implements Tool {
if (t != null) {
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);
+ 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);
}
@@ -518,481 +495,6 @@ public class MovieMaker extends JFrame implements Tool {
}//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().
- *
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) throws IOException {
- File tmpFile = 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;
- int[] data;
- int[] prevData;
- 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) {
- 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);
-
- //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 writeVideoAndAudio(File movieFile, File[] imgFiles, File audioFile,
- int width, int height, double fps, QuickTimeWriter.VideoFormat videoFormat) throws IOException {
-
- File tmpFile = 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");
-
- // 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
- imgBuffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
- g = imgBuffer.createGraphics();
- g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
- 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()));
- 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) {
- throw new IOException(e.getMessage(), e);
- } 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 {
-
- try (InputStream is = new FileInputStream(file)) {
- 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;
-
- }
- }
-
-
static public void main(String[] args) {
EventQueue.invokeLater(() -> {
MovieMaker m = new MovieMaker();
diff --git a/build/shared/tools/MovieMaker/src/processing/app/tools/QuickTimeEngine.java b/build/shared/tools/MovieMaker/src/processing/app/tools/QuickTimeEngine.java
new file mode 100644
index 000000000..54015df6f
--- /dev/null
+++ b/build/shared/tools/MovieMaker/src/processing/app/tools/QuickTimeEngine.java
@@ -0,0 +1,560 @@
+package processing.app.tools;
+
+import processing.app.Language;
+
+import javax.imageio.ImageIO;
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.UnsupportedAudioFileException;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.io.*;
+import java.util.Arrays;
+
+import ch.randelshofer.media.mp3.MP3AudioInputStream;
+import ch.randelshofer.media.quicktime.QuickTimeWriter;
+
+
+class QuickTimeMovieEngine {
+ Component parent;
+
+
+ QuickTimeMovieEngine(Component parent) {
+ this.parent = parent;
+ }
+
+
+ String[] getFormats() {
+ return new String[] {
+ "Animation", "JPEG", "PNG"
+ };
+ }
+
+
+ void write(File movieFile, File[] imgFiles, File soundFile,
+ int width, int height, double fps, String formatName) throws IOException {
+ QuickTimeWriter.VideoFormat videoFormat;
+ switch (formatName) {
+ case "JPEG":
+ videoFormat = QuickTimeWriter.VideoFormat.JPG;
+ break;
+ case "PNG":
+ videoFormat = QuickTimeWriter.VideoFormat.PNG;
+ break;
+ case "Animation":
+ default:
+ videoFormat = QuickTimeWriter.VideoFormat.RLE;
+ break;
+ }
+
+ // preserving size, check the images for their size
+ if (width == 0 || height == 0) {
+ 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;
+ }
+
+ if (soundFile != null) {
+ writeVideoAndAudio(movieFile, imgFiles, soundFile, width, height, fps, videoFormat);
+ } else {
+ writeVideoOnlyVFR(movieFile, imgFiles, width, height, fps, videoFormat);
+ }
+ }
+
+
+ /**
+ * 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().
+ *
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;
+ }
+ }
+
+
+ private 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;
+ }
+
+
+ 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) throws IOException {
+ File tmpFile = new File(movieFile.getPath() + ".tmp");
+ ProgressMonitor p = new ProgressMonitor(parent,
+ 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;
+ int[] data;
+ int[] prevData;
+ 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) {
+ 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);
+
+ //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 writeVideoAndAudio(File movieFile, File[] imgFiles, File audioFile,
+ int width, int height, double fps, QuickTimeWriter.VideoFormat videoFormat) throws IOException {
+
+ File tmpFile = new File(movieFile.getPath() + ".tmp");
+ ProgressMonitor p = new ProgressMonitor(parent,
+ 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");
+
+ // 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
+ imgBuffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ g = imgBuffer.createGraphics();
+ g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+ 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()));
+ 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) {
+ throw new IOException(e.getMessage(), e);
+ } 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 {
+
+ try (InputStream is = new FileInputStream(file)) {
+ 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;
+
+ }
+ }
+}
\ No newline at end of file