mirror of
https://github.com/processing/processing4.git
synced 2026-06-16 04:26:26 +02:00
moving QuickTime-specific code into a separate class
This commit is contained in:
@@ -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().
|
||||
* <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) 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();
|
||||
|
||||
@@ -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().
|
||||
* <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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user