/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.ebixio.virtmus.actions;
import com.ebixio.util.EDT;
import com.ebixio.virtmus.MainApp;
import com.ebixio.virtmus.MusicPage;
import com.ebixio.virtmus.PlayList;
import com.ebixio.virtmus.PlayListSet;
import com.ebixio.virtmus.Song;
import com.ebixio.virtmus.Utils;
import com.ebixio.virtmus.imgsrc.ImgSrc;
import com.ebixio.virtmus.imgsrc.PdfImg;
import java.awt.Frame;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileFilter;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.ActionRegistration;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.HelpCtx;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.actions.CookieAction;
import org.openide.windows.WindowManager;
/**
* This action converts a song with PDF pages into one with JPG pages.
*
* If we try to fully automate this action, it is not clear what the right thing
* to do is in certain cases:
*
* 1. The song contains a mixture of PDF and non-PDF pages. Do we move the
* non-PDF pages to the new directory? They may be referenced from other songs.
*
* 2. The song is from a PDF book. Do we move the PDF to the new directory? If
* we do then the other songs from the same book are left in a strange state.
*
* To simplify the action:
*
* Only allow conversion of all-PDF songs.
*
* Ask the user to provide a destination directory for the JPGs.
*
* If different from the current PDF directory, ask the user if we should move
* the song+pdf to the new directory.
*
* @author Gabriel Burca <gburca dash virtmus at ebixio dot com>
*/
@ActionID(id = "com.ebixio.virtmus.actions.SongPdf2JpgAction", category = "Song")
@ActionRegistration(displayName = "#CTL_SongPdf2JpgAction", lazy = false)
@ActionReferences(value = {
@ActionReference(path = "Menu/Song", position = 600, separatorBefore = 599),
@ActionReference(path = "Toolbars/Song", name = "SongPdf2Jpg", position = 700)
})
public class SongPdf2JpgAction extends CookieAction {
final ImageIcon virtmusIcon = new ImageIcon(ImageUtilities.loadImage(
"com/ebixio/virtmus/resources/VirtMus32x32.png", true));
@Override
protected void performAction(Node[] nodes) {
if (PlayListSet.findInstance().isDirty()) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JOptionPane.showMessageDialog(null,
"Unsaved changes exist. Please save all changes first.",
"Unsaved changes", JOptionPane.INFORMATION_MESSAGE, virtmusIcon);
}
});
} else {
final Song s = nodes[0].getLookup().lookup(Song.class);
final PlayList pl = nodes[0].getLookup().lookup(PlayList.class);
RequestProcessor.getDefault().post(new SongConverter(s, pl));
}
}
class SongConverter implements Runnable {
Song s;
PlayList pl;
int maxProgress;
final AtomicInteger progressI = new AtomicInteger(1);
final ProgressHandle handle = ProgressHandleFactory.createHandle("PDF to JPG converter");
public SongConverter(Song s, PlayList pl) {
this.s = s;
this.pl = pl;
// Not all pages are unique. Need to account for that below.
maxProgress = s.pageOrder.size();
}
@Override
public void run() {
File curSongF = s.getSourceFile();
final File curDir = curSongF.getParentFile();
/* Consider what happens when the song file is named "Foo.pdf.song.xml".
* There's a good chance the song pages come from Foo.pdf in the same
* directory. We can't just use "songFile - songExt" (i.e. Foo.pdf)
* as the destination directory. So let the user choose the directory. */
File destDir = chooseDestDirOnEDT(curDir);
if (destDir == null) return;
File curPdfF = s.pageOrder.get(0).imgSrc.getSourceFile();
String imgStem = Utils.trimExtension(curPdfF.getName(), null);
List<File> jpgMusicPages = new ArrayList<>();
// The "1"s below are to show a little progress before the heavy lifting
// starts, otherwise no progress is shown until after the 1st batch
// finishes (for newWorkStealingPool() executors).
handle.start(maxProgress + 1);
handle.progress(1);
// This executor may exhaust the java heap, causing exceptions, etc...
// especially on devices with lots of cores.
//ExecutorService executor = Executors.newWorkStealingPool();
// For more consistent progress (but slower?) use:
ExecutorService executor = Executors.newSingleThreadExecutor();
int returnVal;
for (final MusicPage mp: s.pageOrder) {
PdfImg pdfImg = (PdfImg)mp.imgSrc;
final File newMusicPageF = new File(destDir.getAbsolutePath() + File.separator
+ String.format("%s-%03d.jpg", imgStem, pdfImg.pageNum));
boolean duplicated = jpgMusicPages.contains(newMusicPageF);
jpgMusicPages.add(newMusicPageF);
if (duplicated) {
synchronized (progressI) {
handle.progress(progressI.incrementAndGet());
}
continue;
}
if (newMusicPageF.exists()) {
try {
returnVal = EDT.invokeAndWait(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return JOptionPane.showConfirmDialog(null,
"" + newMusicPageF + " already exists. Overwrite?",
"Overwrite file?", JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE, virtmusIcon);
}
});
} catch (InterruptedException | InvocationTargetException ex) {
Exceptions.printStackTrace(ex);
returnVal = JOptionPane.CANCEL_OPTION;
}
switch (returnVal) {
case JOptionPane.CANCEL_OPTION:
handle.finish();
return;
case JOptionPane.NO_OPTION:
synchronized (progressI) {
handle.progress(progressI.incrementAndGet());
}
continue;
case JOptionPane.YES_OPTION:
default:
}
}
executor.execute(new MusicPageSaver(mp, newMusicPageF));
}
// While conversion is going on, prompt user...
boolean movePdf = false;
if (!destDir.equals(curDir)) {
try {
returnVal = EDT.invokeAndWait(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return JOptionPane.showConfirmDialog(null,
"Move PDF+Song to new dir?", "Move?",
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, virtmusIcon);
}
});
} catch (InterruptedException | InvocationTargetException ex) {
Exceptions.printStackTrace(ex);
returnVal = JOptionPane.NO_OPTION;
}
movePdf = returnVal == JOptionPane.YES_OPTION;
}
// Wait for conversion to finish before we move the source PDF
try {
executor.shutdown();
executor.awaitTermination(30 * maxProgress, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
handle.finish();
// Create the new JPG-based song.
File newJpgSongF = new File(destDir + File.separator +
Utils.trimExtension(curSongF.getName(), Song.SONG_FILE_EXT)
+ "-jpg" + Song.SONG_FILE_EXT);
boolean saveNewSong = true;
if (newJpgSongF.exists()) {
try {
returnVal = EDT.invokeAndWait(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return JOptionPane.showConfirmDialog(null,
"Overwrite existing song file?", "Overwrite?",
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, virtmusIcon);
}
});
} catch (InterruptedException | InvocationTargetException ex) {
Exceptions.printStackTrace(ex);
returnVal = JOptionPane.NO_OPTION;
}
saveNewSong = returnVal == JOptionPane.YES_OPTION;
}
if (saveNewSong) {
Song jpgSong = new Song();
jpgSong.setName("" + s.getName() + " JPG");
jpgSong.setNotes(s.getNotes());
jpgSong.setTags(s.getTags());
for (File jpg: jpgMusicPages) {
jpgSong.addPage(jpg);
}
jpgSong.setSourceFile(newJpgSongF);
jpgSong.serialize();
// Add it right after the PDF-based song to the current PlayList
if (pl != null) {
int oldIdx = pl.songs.indexOf(s);
pl.addSong(jpgSong, oldIdx + 1);
}
}
if (movePdf) {
File newPdfF = new File(destDir.getPath() + File.separator + curPdfF.getName());
int cnt = PlayListSet.findInstance().movedPdf(curPdfF, newPdfF);
curPdfF.renameTo(newPdfF);
File newPdfSongF = new File(destDir + File.separator + curSongF.getName());
curSongF.renameTo(newPdfSongF);
cnt = PlayListSet.findInstance().movedSong(curSongF, newPdfSongF);
}
PlayListSet.findInstance().saveAll();
MainApp.setStatusText("Finished the PDF to JPG conversion for: " + s.getName());
}
class MusicPageSaver implements Runnable {
MusicPage mp;
File newMusicPageF;
public MusicPageSaver(MusicPage m, File f) {
mp = m;
newMusicPageF = f;
}
@Override
public void run() {
try {
MainApp.setStatusText("Writing " + newMusicPageF);
mp.saveImg(newMusicPageF, "jpg");
} catch (Throwable ex) {
// Possibly out of heap space, etc...
Exceptions.printStackTrace(ex);
} finally {
/*
* It's possible for progress to be called with out-of-order values
* unless we synchronize
* - progress(3) in threadA is pre-empted before it finishes
* - progress(4) in threadB runs to completion
* - threadA resumes and finds it was called with 3 (< 4)
*/
synchronized (progressI) {
handle.progress(progressI.incrementAndGet());
}
}
}
}
}
/**
* Wrapper around {@link chooseDestDir()} that calls it on the EDT.
* @param origDir
* @return
*/
private File chooseDestDirOnEDT(final File origDir) {
try {
return EDT.invokeAndWait(new Callable<File>() {
@Override
public File call() throws Exception {
return chooseDestDir(origDir);
}
});
} catch (InterruptedException | InvocationTargetException ex) {
Exceptions.printStackTrace(ex);
return null;
}
}
private File chooseDestDir(File origDir) {
final Frame mainWindow = WindowManager.getDefault().getMainWindow();
final JFileChooser fc = new JFileChooser();
if (origDir != null && origDir.exists()) {
fc.setCurrentDirectory(origDir);
}
fc.setApproveButtonToolTipText("Save JPGs to the selected directory");
fc.setDialogTitle("Choose destination directory for JPGs");
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
fc.setMultiSelectionEnabled(false);
fc.addChoosableFileFilter(new FileFilter() {
@Override
public boolean accept(File f) {
return f.isDirectory();
}
@Override
public String getDescription() {
return "JPG destination directory";
}
});
if (fc.showDialog(mainWindow, "Select Directory") == JFileChooser.APPROVE_OPTION) {
File file = fc.getSelectedFile();
if (file.exists()) {
if (file.isDirectory()) {
return file;
}
} else {
if (file.mkdirs()) return file;
}
}
return null;
}
/**
* Checks to see if the song is convertible.
*
* That means it has at least 1 page, all the pages are PDF, and they all
* come from the same PDF.
*
* @param s
* @return
*/
private boolean isConvertible(Song s) {
if (s.pageOrder.isEmpty()) return false;
File f = s.pageOrder.get(0).getSourceFile();
if (f == null || !f.exists()) return false;
for (MusicPage mp: s.pageOrder) {
if (mp.imgSrc.getImgType() != ImgSrc.ImgType.PDF) return false;
if (! f.equals(mp.imgSrc.sourceFile)) return false;
}
return true;
}
// <editor-fold defaultstate="collapsed" desc=" Cookie Action ">
@Override
protected boolean enable(Node[] nodes) {
if (nodes.length == 1) {
Song s = nodes[0].getLookup().lookup(Song.class);
return s != null && isConvertible(s);
}
return false;
}
@Override
protected String iconResource() {
return "com/ebixio/virtmus/resources/Pdf2Jpg.png";
}
@Override
public String getName() {
return NbBundle.getMessage(SongPdf2JpgAction.class, "CTL_SongPdf2JpgAction");
}
@Override
public HelpCtx getHelpCtx() {
return HelpCtx.DEFAULT_HELP;
}
@Override
protected int mode() {
return CookieAction.MODE_EXACTLY_ONE;
}
@Override
protected Class<?>[] cookieClasses() {
return new Class[] { Song.class };
}
@Override
protected boolean asynchronous() {
// If this is set to true, we need to make sure the dialogs use the EDT.
return false;
}
// </editor-fold>
}