|
|
|
@@ -0,0 +1,486 @@
|
|
|
|
|
import java.awt.Dimension;
|
|
|
|
|
import java.awt.GridBagConstraints;
|
|
|
|
|
import java.awt.GridBagLayout;
|
|
|
|
|
import java.awt.Image;
|
|
|
|
|
import java.awt.Insets;
|
|
|
|
|
import java.awt.event.ActionEvent;
|
|
|
|
|
import java.awt.event.ActionListener;
|
|
|
|
|
import java.awt.geom.AffineTransform;
|
|
|
|
|
import java.awt.image.AffineTransformOp;
|
|
|
|
|
import java.awt.image.BufferedImage;
|
|
|
|
|
import java.awt.image.ImageObserver;
|
|
|
|
|
import java.io.File;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
|
|
|
|
|
import javax.imageio.ImageIO;
|
|
|
|
|
import javax.swing.InputVerifier;
|
|
|
|
|
import javax.swing.JButton;
|
|
|
|
|
import javax.swing.JComponent;
|
|
|
|
|
import javax.swing.JFileChooser;
|
|
|
|
|
import javax.swing.JFrame;
|
|
|
|
|
import javax.swing.JLabel;
|
|
|
|
|
import javax.swing.JOptionPane;
|
|
|
|
|
import javax.swing.JPanel;
|
|
|
|
|
import javax.swing.JProgressBar;
|
|
|
|
|
import javax.swing.JTextField;
|
|
|
|
|
import javax.swing.SwingUtilities;
|
|
|
|
|
import javax.swing.UIManager;
|
|
|
|
|
import javax.swing.event.DocumentEvent;
|
|
|
|
|
import javax.swing.event.DocumentListener;
|
|
|
|
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
|
|
|
|
|
|
|
|
|
import org.imgscalr.Scalr.Rotation;
|
|
|
|
|
|
|
|
|
|
import com.drew.imaging.ImageMetadataReader;
|
|
|
|
|
import com.drew.metadata.Metadata;
|
|
|
|
|
import com.drew.metadata.MetadataException;
|
|
|
|
|
import com.drew.metadata.exif.ExifIFD0Directory;
|
|
|
|
|
|
|
|
|
|
public class ImageWitchGui extends JFrame {
|
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
|
public static final String[] supportedExtensions =
|
|
|
|
|
{"jpg","jpeg","png","gif"};
|
|
|
|
|
|
|
|
|
|
File[] selectedFiles = null;
|
|
|
|
|
|
|
|
|
|
JTextField heightField;
|
|
|
|
|
JTextField widthField;
|
|
|
|
|
|
|
|
|
|
Integer maxWidth = 0;
|
|
|
|
|
Integer maxHeight = 0;
|
|
|
|
|
Boolean errorNotified = false;
|
|
|
|
|
|
|
|
|
|
JButton loadButton;
|
|
|
|
|
JButton resizeButton;
|
|
|
|
|
JProgressBar progressBar;
|
|
|
|
|
volatile boolean conversionRunning;
|
|
|
|
|
|
|
|
|
|
public static void main(String[] args) {
|
|
|
|
|
//Schedule a job for the event-dispatching thread:
|
|
|
|
|
//creating and showing this application's GUI.
|
|
|
|
|
javax.swing.SwingUtilities.invokeLater(new Runnable() {
|
|
|
|
|
public void run() {
|
|
|
|
|
ImageWitchGui gui = new ImageWitchGui();
|
|
|
|
|
gui.setDefaultCloseOperation(EXIT_ON_CLOSE);
|
|
|
|
|
gui.createAndShowGui();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImageWitchGui() {
|
|
|
|
|
setTitle("ImageWitch");
|
|
|
|
|
try {
|
|
|
|
|
UIManager.setLookAndFeel(
|
|
|
|
|
UIManager.getCrossPlatformLookAndFeelClassName());
|
|
|
|
|
} catch (Exception e) {}
|
|
|
|
|
|
|
|
|
|
InputVerifier verifier = new NumberInputVerifier();
|
|
|
|
|
|
|
|
|
|
heightField = new JTextField("0");
|
|
|
|
|
heightField.setColumns(10);
|
|
|
|
|
heightField.setInputVerifier(verifier);
|
|
|
|
|
heightField.getDocument().addDocumentListener(
|
|
|
|
|
new FieldListener(heightField));
|
|
|
|
|
|
|
|
|
|
widthField = new JTextField("0");
|
|
|
|
|
widthField.setColumns(10);
|
|
|
|
|
widthField.setInputVerifier(verifier);
|
|
|
|
|
widthField.getDocument().addDocumentListener(
|
|
|
|
|
new FieldListener(widthField));
|
|
|
|
|
|
|
|
|
|
loadButton = new JButton("Load Files");
|
|
|
|
|
loadButton.addActionListener(new ActionListener() {
|
|
|
|
|
@Override
|
|
|
|
|
public void actionPerformed(ActionEvent e) {
|
|
|
|
|
JFileChooser chooser = new JFileChooser();
|
|
|
|
|
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
|
|
|
|
|
chooser.setMultiSelectionEnabled(true);
|
|
|
|
|
chooser.setFileFilter(
|
|
|
|
|
new FileNameExtensionFilter("Image Files",
|
|
|
|
|
supportedExtensions));
|
|
|
|
|
int returnVal = chooser.showOpenDialog(ImageWitchGui.this);
|
|
|
|
|
if (returnVal == JFileChooser.APPROVE_OPTION) {
|
|
|
|
|
selectedFiles = chooser.getSelectedFiles();
|
|
|
|
|
resizeButton.setEnabled(true);
|
|
|
|
|
errorNotified = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
progressBar.setString("Ready");
|
|
|
|
|
progressBar.setValue(0);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
resizeButton = new JButton("Resize");
|
|
|
|
|
resizeButton.setEnabled(false);
|
|
|
|
|
resizeButton.addActionListener(new ActionListener() {
|
|
|
|
|
@Override
|
|
|
|
|
public void actionPerformed(ActionEvent e) {
|
|
|
|
|
if (conversionRunning) {
|
|
|
|
|
conversionRunning = false;
|
|
|
|
|
loadButton.setEnabled(true);
|
|
|
|
|
resizeButton.setText("Resize");
|
|
|
|
|
} else {
|
|
|
|
|
errorNotified = false;
|
|
|
|
|
loadButton.setEnabled(false);
|
|
|
|
|
conversionRunning = true;
|
|
|
|
|
resizeButton.setText("Cancel");
|
|
|
|
|
Thread conversionThread = new Thread() {
|
|
|
|
|
public void run() {
|
|
|
|
|
convertFiles(selectedFiles);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
conversionThread.start();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
progressBar = new JProgressBar();
|
|
|
|
|
progressBar.setIndeterminate(false);
|
|
|
|
|
progressBar.setStringPainted(true);
|
|
|
|
|
progressBar.setString("Ready");
|
|
|
|
|
progressBar.setValue(0);
|
|
|
|
|
progressBar.setMaximum(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void createAndShowGui() {
|
|
|
|
|
JLabel label;
|
|
|
|
|
JPanel panel = new JPanel(new GridBagLayout());
|
|
|
|
|
getContentPane().add(panel);
|
|
|
|
|
GridBagConstraints c = new GridBagConstraints();
|
|
|
|
|
c.fill = GridBagConstraints.HORIZONTAL;
|
|
|
|
|
|
|
|
|
|
label = new JLabel("Max Width: ");
|
|
|
|
|
c.gridx = 0;
|
|
|
|
|
c.gridy = 0;
|
|
|
|
|
c.insets = new Insets(2, 2, 2, 2);
|
|
|
|
|
c.gridheight = 1;
|
|
|
|
|
c.gridwidth = 1;
|
|
|
|
|
c.weightx = 1.0;
|
|
|
|
|
c.weighty = 1.0;
|
|
|
|
|
panel.add(label, c);
|
|
|
|
|
|
|
|
|
|
c.gridx = 1;
|
|
|
|
|
c.gridy = 0;
|
|
|
|
|
panel.add(widthField, c);
|
|
|
|
|
|
|
|
|
|
label = new JLabel("Max Height: ");
|
|
|
|
|
c.gridx = 0;
|
|
|
|
|
c.gridy = 1;
|
|
|
|
|
panel.add(label, c);
|
|
|
|
|
|
|
|
|
|
c.gridx = 1;
|
|
|
|
|
c.gridy = 1;
|
|
|
|
|
panel.add(heightField, c);
|
|
|
|
|
|
|
|
|
|
c.gridx = 0;
|
|
|
|
|
c.gridy = 2;
|
|
|
|
|
panel.add(loadButton, c);
|
|
|
|
|
|
|
|
|
|
c.gridx = 1;
|
|
|
|
|
c.gridy = 2;
|
|
|
|
|
panel.add(resizeButton, c);
|
|
|
|
|
|
|
|
|
|
c.gridx = 0;
|
|
|
|
|
c.gridy = 3;
|
|
|
|
|
c.gridwidth = 2;
|
|
|
|
|
c.insets = new Insets(0, 0, 0, 0);
|
|
|
|
|
c.fill = GridBagConstraints.BOTH;
|
|
|
|
|
panel.add(progressBar, c);
|
|
|
|
|
|
|
|
|
|
this.pack();
|
|
|
|
|
this.setVisible(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String getExtension(String filename) {
|
|
|
|
|
String extension;
|
|
|
|
|
|
|
|
|
|
extension = filename.substring(filename.lastIndexOf(".")+1).toLowerCase();
|
|
|
|
|
if (extension.equals("jpeg")) {
|
|
|
|
|
extension = "jpg";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return extension;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String getNewFilename(String filename) {
|
|
|
|
|
String newFilename;
|
|
|
|
|
|
|
|
|
|
int dotIdx = filename.lastIndexOf(".");
|
|
|
|
|
newFilename = filename.substring(0, dotIdx) +
|
|
|
|
|
"_scaled." + getExtension(filename);
|
|
|
|
|
|
|
|
|
|
return newFilename;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Dimension getNewDimensions(int maxWidth, int maxHeight,
|
|
|
|
|
BufferedImage image) {
|
|
|
|
|
Dimension dim = new Dimension();
|
|
|
|
|
double sourceRatio, targetRatio;
|
|
|
|
|
int targetWidth, targetHeight;
|
|
|
|
|
|
|
|
|
|
sourceRatio = (double) image.getWidth() / (double) image.getHeight();
|
|
|
|
|
targetRatio = (double) maxWidth / (double) maxHeight;
|
|
|
|
|
|
|
|
|
|
if (maxWidth < 0 && maxHeight < 0) {
|
|
|
|
|
throw new IllegalArgumentException(
|
|
|
|
|
"At least one of width and height must be selected");
|
|
|
|
|
} else if (maxWidth <= 0 ||
|
|
|
|
|
(maxHeight > 0 && targetRatio > sourceRatio)) {
|
|
|
|
|
targetHeight = maxHeight;
|
|
|
|
|
targetWidth = (int) (targetHeight * sourceRatio);
|
|
|
|
|
|
|
|
|
|
} else if (maxHeight <= 0 ||
|
|
|
|
|
(maxWidth > 0 && targetRatio < sourceRatio)) {
|
|
|
|
|
targetWidth = maxWidth;
|
|
|
|
|
targetHeight = (int) (targetWidth / sourceRatio);
|
|
|
|
|
} else {
|
|
|
|
|
targetWidth = maxWidth;
|
|
|
|
|
targetHeight = maxHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dim.height = targetHeight;
|
|
|
|
|
dim.width = targetWidth;
|
|
|
|
|
|
|
|
|
|
return dim;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private BufferedImage rotateUpright(BufferedImage image, File file) {
|
|
|
|
|
Metadata metadata;
|
|
|
|
|
ExifIFD0Directory exifIFD0;
|
|
|
|
|
int orientation;
|
|
|
|
|
AffineTransform transform;
|
|
|
|
|
BufferedImage uprightImage;
|
|
|
|
|
AffineTransformOp transformOp;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
metadata = ImageMetadataReader.readMetadata(file);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
return image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exifIFD0 = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
orientation = exifIFD0.getInt(ExifIFD0Directory.TAG_ORIENTATION);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
return image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
transform = new AffineTransform();
|
|
|
|
|
|
|
|
|
|
switch (orientation) {
|
|
|
|
|
case 6: // [Exif IFD0] Orientation - Right side, top (Rotate 90 CW)
|
|
|
|
|
uprightImage = new BufferedImage(image.getHeight(), image.getWidth(), image.getType());
|
|
|
|
|
transform.translate(image.getHeight() / 2, image.getWidth() / 2);
|
|
|
|
|
transform.rotate(Math.PI/2);
|
|
|
|
|
transform.translate(-image.getWidth() / 2, -image.getHeight() / 2);
|
|
|
|
|
transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
|
|
|
|
|
transformOp.filter(image, uprightImage);
|
|
|
|
|
break;
|
|
|
|
|
case 3: // [Exif IFD0] Orientation - Bottom, right side (Rotate 180)
|
|
|
|
|
uprightImage = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
|
|
|
|
|
transform.translate(image.getHeight() / 2, image.getWidth() / 2);
|
|
|
|
|
transform.rotate(Math.PI);
|
|
|
|
|
transform.translate(-image.getHeight() / 2, -image.getWidth() / 2);
|
|
|
|
|
transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
|
|
|
|
|
transformOp.filter(image, uprightImage);
|
|
|
|
|
break;
|
|
|
|
|
case 8: // [Exif IFD0] Orientation - Left side, bottom (Rotate 270 CW)
|
|
|
|
|
uprightImage = new BufferedImage(image.getHeight(), image.getWidth(), image.getType());
|
|
|
|
|
transform.translate(image.getHeight() / 2, image.getWidth() / 2);
|
|
|
|
|
transform.rotate(-Math.PI/2);
|
|
|
|
|
transform.translate(-image.getWidth() / 2, -image.getHeight() / 2);
|
|
|
|
|
transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
|
|
|
|
|
transformOp.filter(image, uprightImage);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
uprightImage = image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return uprightImage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void updateProgress(int progress, int maxValue, String text) {
|
|
|
|
|
javax.swing.SwingUtilities.invokeLater(new Runnable() {
|
|
|
|
|
public void run() {
|
|
|
|
|
System.out.println(text);
|
|
|
|
|
if (maxValue != progressBar.getMaximum()) {
|
|
|
|
|
progressBar.setMaximum(maxValue);
|
|
|
|
|
}
|
|
|
|
|
progressBar.setValue(progress);
|
|
|
|
|
progressBar.setString(text);
|
|
|
|
|
|
|
|
|
|
if (progress == maxValue) {
|
|
|
|
|
loadButton.setEnabled(true);
|
|
|
|
|
resizeButton.setText("Resize");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void convertFiles(File[] files) {
|
|
|
|
|
BufferedImage oldImage;
|
|
|
|
|
String newFilename;
|
|
|
|
|
int index = 0;
|
|
|
|
|
int numFiles = files.length;
|
|
|
|
|
|
|
|
|
|
progressBar.setMaximum(numFiles);
|
|
|
|
|
for (File oldFile : files) {
|
|
|
|
|
if (!conversionRunning) {
|
|
|
|
|
updateProgress(0, 1, "Cancelled");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
oldImage = ImageIO.read(oldFile);
|
|
|
|
|
newFilename = getNewFilename(oldFile.getAbsolutePath());
|
|
|
|
|
oldImage = rotateUpright(oldImage, oldFile);
|
|
|
|
|
resizeAndSaveImage(oldImage, maxWidth, maxHeight, newFilename);
|
|
|
|
|
|
|
|
|
|
progressBar.setValue(index);
|
|
|
|
|
index++;
|
|
|
|
|
updateProgress(index, numFiles,
|
|
|
|
|
"Processing " + Integer.toString(index) +
|
|
|
|
|
" of " + Integer.toString(numFiles));
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
if (!errorNotified) {
|
|
|
|
|
errorNotified = true;
|
|
|
|
|
JOptionPane.showMessageDialog(ImageWitchGui.this,
|
|
|
|
|
"<html>Unable to read file: <br>" +
|
|
|
|
|
oldFile.getAbsolutePath() +
|
|
|
|
|
"<br><br>Other files may be affected.",
|
|
|
|
|
"Read Error",
|
|
|
|
|
JOptionPane.ERROR_MESSAGE);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
updateProgress(numFiles, numFiles, "Done");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void resizeAndSaveImage(BufferedImage image,
|
|
|
|
|
int maxWidth, int maxHeight,
|
|
|
|
|
String newFilename)
|
|
|
|
|
throws IllegalArgumentException
|
|
|
|
|
{
|
|
|
|
|
Image newImage;
|
|
|
|
|
BufferedImage renderedImage;
|
|
|
|
|
ImageSaver saver;
|
|
|
|
|
Dimension newDim;
|
|
|
|
|
|
|
|
|
|
newDim = getNewDimensions(maxWidth, maxHeight, image);
|
|
|
|
|
newImage = image.getScaledInstance(
|
|
|
|
|
newDim.width, newDim.height, Image.SCALE_SMOOTH);
|
|
|
|
|
renderedImage = new BufferedImage(
|
|
|
|
|
newDim.width, newDim.height, BufferedImage.TYPE_INT_RGB);
|
|
|
|
|
saver = new ImageSaver(newFilename, renderedImage);
|
|
|
|
|
renderedImage.getGraphics().drawImage(newImage, 0, 0, saver);
|
|
|
|
|
saver.saveImage();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class NumberInputVerifier extends InputVerifier {
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean verify(JComponent input) {
|
|
|
|
|
String text = ((JTextField) input).getText();
|
|
|
|
|
try {
|
|
|
|
|
Integer.parseInt(text);
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class ImageSaver implements ImageObserver {
|
|
|
|
|
|
|
|
|
|
private String filename;
|
|
|
|
|
private BufferedImage image;
|
|
|
|
|
private String formatName;
|
|
|
|
|
|
|
|
|
|
public ImageSaver(String filename, BufferedImage image) {
|
|
|
|
|
this.filename = filename;
|
|
|
|
|
this.image = image;
|
|
|
|
|
this.formatName = getExtension(filename);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean imageUpdate(Image img, int infoflags, int x, int y,
|
|
|
|
|
int width, int height) {
|
|
|
|
|
if (infoflags == ImageObserver.ERROR) {
|
|
|
|
|
if (!errorNotified) {
|
|
|
|
|
errorNotified = true;
|
|
|
|
|
JOptionPane.showMessageDialog(ImageWitchGui.this,
|
|
|
|
|
"<html>Unable to resize file: <br>" + filename +
|
|
|
|
|
"<br><br>Other files may be affected.",
|
|
|
|
|
"Resize Error",
|
|
|
|
|
JOptionPane.ERROR_MESSAGE);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveImage();
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void saveImage() {
|
|
|
|
|
File file = new File(filename);
|
|
|
|
|
try {
|
|
|
|
|
ImageIO.write(image, formatName, file);
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
if (!errorNotified) {
|
|
|
|
|
errorNotified = true;
|
|
|
|
|
JOptionPane.showMessageDialog(ImageWitchGui.this,
|
|
|
|
|
"<html>Unable to save file: <br>" + filename +
|
|
|
|
|
"<br><br>Other files may be affected.",
|
|
|
|
|
"Save Error",
|
|
|
|
|
JOptionPane.ERROR_MESSAGE);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class FieldListener implements DocumentListener {
|
|
|
|
|
|
|
|
|
|
private JTextField field;
|
|
|
|
|
|
|
|
|
|
public FieldListener(JTextField field) {
|
|
|
|
|
this.field = field;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void update() {
|
|
|
|
|
Integer value = 0;
|
|
|
|
|
|
|
|
|
|
if (!field.getText().isBlank()) {
|
|
|
|
|
value = Integer.parseInt(field.getText());
|
|
|
|
|
}
|
|
|
|
|
if (field == heightField) {
|
|
|
|
|
maxHeight = value;
|
|
|
|
|
} else {
|
|
|
|
|
maxWidth = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void insertUpdate(DocumentEvent e) {
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void removeUpdate(DocumentEvent e) {
|
|
|
|
|
update();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void changedUpdate(DocumentEvent e) {
|
|
|
|
|
update();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|