view src/name/blackcap/exifwasher/WashDialog.kt @ 13:a59d84674fb0

Make it seamlessly work on IPTC and XMP metadata, too.
author David Barts <davidb@stashtea.com>
date Sat, 11 Apr 2020 09:14:31 -0700
parents e52fd1a575de
children 841f711c40bd
line wrap: on
line source

/*
 * The dialog that controls washing a single file.
 */
package name.blackcap.exifwasher

import java.awt.Color
import java.awt.Dimension
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.logging.Level
import java.util.logging.Logger
import javax.swing.*
import javax.swing.table.DefaultTableModel
import javax.swing.table.TableColumn
import javax.swing.table.TableColumnModel

import name.blackcap.exifwasher.exiv2.*

class WashDialog : JDialog(Application.mainFrame) {
    private val BW = 9
    private val BW2 = BW * 2
    private val WIDTH = 640
    private val HEIGHT = 480

    private val COLUMN_NAMES = arrayOf<String>("Delete?", "Key", "Type", "Value")
    private val myTable = JTable(arrayOf<Array<Any>>(), COLUMN_NAMES).apply {
        autoCreateRowSorter = false
        border = BorderFactory.createLineBorder(Color.GRAY)
        gridColor = Color.GRAY
        rowSorter = null
        setShowGrid(true)
    }

    val impliedDeletions = mutableSetOf<String>()
    private val selectAll = JCheckBox("Select all for deletion", false).also {
        it.addActionListener(ActionListener { _ ->
            val limit = myTable.rowCount - 1
            if (it.isSelected()) {
                impliedDeletions.clear()
                for (i in 0 .. limit) {
                    if(!(myTable.getValueAt(i, 0) as Boolean)) {
                        impliedDeletions.add(myTable.getValueAt(i, 1) as String)
                        myTable.setValueAt(true, i, 0)
                    }
                }
            } else {
                for (i in 0 .. limit) {
                    if (impliedDeletions.contains(myTable.getValueAt(i, 1) as String)) {
                        myTable.setValueAt(false, i, 0)
                    }
                }
            }
        })
    }

    private val resetButton = JButton("Reset").also {
        it.addActionListener(ActionListener { doReset() })
    }

    private val cancelButton = JButton("Cancel").also {
        it.addActionListener(ActionListener { close() })
    }

    /* deliberately not the default action, because it changes a file */
    private val washButton = JButton("Wash").also {
        it.addActionListener(ActionListener { doWash() })
    }

    private lateinit var washing: File

    /* initiates the washing of the Exif data */
    fun wash(dirty: File) {
        title = "Washing: ${dirty.name}"
        selectAll.setSelected(false)
        washing = dirty
        useWaitCursor()
        swingWorker<Array<Array<Any>>?> {
            inBackground {
                try {
                    val image = Image(dirty.canonicalPath)
                    val meta = image.metadata
                    val keys = meta.keys
                    keys.sort()
                    Array<Array<Any>>(keys.size) {
                        val key = keys[it]
                        val value = meta[key]
                        arrayOf(
                            !Application.settingsDialog.whitelist.contains(key),
                            key, value.type, value.value)
                    }
                } catch (e: Exiv2Exception) {
                    LOGGER.log(Level.SEVERE, "unable to read metadata", e)
                    null
                }
            }
            whenDone {
                useNormalCursor()
                val tableData = get()
                if (tableData == null) {
                    JOptionPane.showMessageDialog(Application.mainFrame,
                        "Unable to read metadata.",
                        "Error", JOptionPane.ERROR_MESSAGE)
                } else {
                    myTable.run {
                        model = MyTableModel(tableData, COLUMN_NAMES)
                        autoResizeMode = JTable.AUTO_RESIZE_OFF
                        setColWidth(0, 0, COLUMN_NAMES[0])
                        setColWidth(1, 180, null)
                        setColWidth(2, 72, "Undefined")
                        setColWidth(3, 720, null)
                        setOverallWidth()
                    }
                    setVisible(true)
                }
            }
        }
    }

    private class MyTableModel(tData: Array<Array<Any>>, cNames: Array<String>) : DefaultTableModel(tData, cNames) {
        override fun isCellEditable(row: Int, col: Int) = col == 0
        override fun getColumnClass(col: Int) = if (col == 0) {
            java.lang.Boolean::class.java
        } else {
            java.lang.String::class.java
        }
    }

    private fun doReset() {
        myTable.model.run {
            for (i in 0 .. rowCount - 1) {
                val key = getValueAt(i, 1) as String
                setValueAt(!Application.settingsDialog.whitelist.contains(key), i, 0)
            }
        }
        myTable.validate()
    }

    private fun doWash() {
        setVisible(false)

        /* get path to the directory we create */
        val outDir = if (Application.settingsDialog.outputToInputDir) {
            washing.canonicalFile.parent
        } else {
            Application.settingsDialog.outputTo
        }

        /* get new file name */
        val (name, ext) = splitext(washing.name)
        var newFile = File(outDir, "${name}_washed${ext}")

        /* warn (and allow user to back out) if overwriting */
        if (newFile.exists()) {
            val answer = JOptionPane.showConfirmDialog(Application.mainFrame,
                "File ${newFile.name} already exists. Overwrite?",
                "Confirm overwriting file",
                JOptionPane.YES_NO_OPTION,
                JOptionPane.WARNING_MESSAGE)
            if (answer != JOptionPane.YES_OPTION) {
                close()
                return
            }
        }

        /* copy the file, then edit the Exif in the copy */
        useWaitCursor()
        swingWorker<Boolean> {
            inBackground {
                try {
                    FileInputStream(washing).use { source ->
                        FileOutputStream(newFile).use { target ->
                            source.copyTo(target)
                        }
                    }
                    val image = Image(newFile.canonicalPath)
                    val meta = image.metadata
                    meta.keys.forEach {
                        if (!Application.settingsDialog.whitelist.contains(it)) {
                            meta.erase(it)
                        }
                    }
                    image.store()
                    true
                } catch (e: IOException) {
                    LOGGER.log(Level.SEVERE, "unable to copy input", e)
                    false
                } catch (e: Exiv2Exception) {
                    LOGGER.log(Level.SEVERE, "unable to edit metadata", e)
                    false
                }
            }
            whenDone {
                useNormalCursor()
                close()
                /* if all went well, show the Exif in the new file */
                if (get()) {
                    ShowDialog().show(newFile)
                } else {
                    try {
                        if (newFile.exists()) { newFile.delete() }
                    } catch (e: IOException) {
                        LOGGER.log(Level.SEVERE, "unable to delete", e)
                    }
                    JOptionPane.showMessageDialog(Application.mainFrame,
                        "Error\nUnable to wash: ${washing.canonicalPath}\nTo: ${newFile.canonicalPath}",
                        "Error", JOptionPane.ERROR_MESSAGE)
                }
            }
        }
    }

    private fun splitext(s: String): Pair<String, String> {
        val pos = s.lastIndexOf('.')
        if (pos == -1) {
            return Pair(s, "")
        }
        return Pair(s.substring(0, pos), s.substring(pos))
    }

    init {
        defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE /* delete if reusing */
        title = "Untitled"
        contentPane.apply {
            layout = BoxLayout(this, BoxLayout.Y_AXIS)
            add(Box(BoxLayout.Y_AXIS).apply {
                alignmentX = Box.CENTER_ALIGNMENT
                border = BorderFactory.createEmptyBorder(BW, BW, BW, BW)
                add(JScrollPane(myTable).apply {
                    alignmentX = JScrollPane.LEFT_ALIGNMENT
                    border = BorderFactory.createEmptyBorder(BW, BW, BW, BW)
                    verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS
                    horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS
                    preferredSize = Dimension(WIDTH, HEIGHT)
                    background = Application.mainFrame.background
                })
                add(selectAll.apply {
                    alignmentX = JCheckBox.LEFT_ALIGNMENT
                    border = BorderFactory.createEmptyBorder(BW, BW, 0, BW)
                })
            })
            add(Box(BoxLayout.X_AXIS).apply {
                alignmentX = Box.CENTER_ALIGNMENT
                border = BorderFactory.createEmptyBorder(BW, BW, BW2, BW)
                add(resetButton)
                add(Box.createHorizontalGlue())
                add(cancelButton)
                add(Box.createHorizontalStrut(BW2))
                add(washButton)
            })
        }
        pack()
    }
}