Wednesday, April 25, 2012

KirSizer - Flex AIR Image Sizer app: Part 9

In this tutorial we will calculate the new width and height of pictures after resizing and log them.

Before we do that, let's add 2 buttons to the last NavigatorObject first. The buttons' labels are "Save error log" and "Return". We'll only give functionality for the second button first, and that is redirect the user to the first view state:

<s:NavigatorContent width="100%" height="100%" hideEffect="fadeOut" showEffect="fadeIn">
<s:VGroup width="280" height="440" top="10" left="10">
<s:Label width="100%" color="0xff3333" fontWeight="bold">Do not add, remove or rename selected files.</s:Label>
<mx:ProgressBar id="progressBar" width="100%" mode="manual" label="Resizing %1/%2" color="0xffffff" />
<s:TextArea id="logArea" editable="false" width="100%" height="100%" />
<s:HGroup>
<s:Button id="buttonError" label="Save error log" width="136" />
<s:Button id="buttonReturn" label="Return" click="contentStack.selectedIndex = 0;" width="136" />
</s:HGroup>
</s:VGroup>
</s:NavigatorContent>

You can see that I have given the buttons ids buttonError and buttonReturn. I set their enabled properties to false in beginResize() function in the if (canProceed) statement:

if (canProceed) {
contentStack.selectedIndex = 3;
var timer:Timer = new Timer(400, 1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimer);
timer.start();
function onTimer(evt:TimerEvent):void {
buttonError.enabled = false;
buttonReturn.enabled = false;
logArea.text = "";
logArea.appendText("Beginning resizing of " + totalFiles + " files...\n");
currentNum = 0;
totalErrors = 0;
resizeNext();
}
}

And to false in nextAction() if it was the last file:

private function nextAction():void {
if (currentNum < totalFiles) {
resizeNext();
}else {
logArea.appendText("Operation complete!");
buttonError.enabled = true;
buttonReturn.enabled = true;
}
}

Now let's go to resizeNext function and edit what we have right now. At the moment, in the if (canProceed) statement we create a timer and start it. Let's instead create a Loader object and load the current image. When the image is loaded, we start the timer:

if (canProceed) {
loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete);
loader.load(new URLRequest(file.url));
}

function onLoadComplete(evt:Event):void {
var timer:Timer = new Timer(500, 1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimer);
timer.start();
}

In the onTimer() function, let's add the code that will be responsible for changing sizes of the images. We check the actionCombo's selectedIndex to find out how exactly we are going to resize the images. We need to keep in mind that the user can choose to use pixels or percentages.

If its the first action that's selected, we simply and strictly apply the sizes. If its the second action, we create a ratio variable that stores the proportions values and after setting width strictly, we calculate height. If its the third action, we do it the other way around - set height and calculate width. If the last action option is chosen, we first set width to the limit, then apply new height value with proportions kept, and then we check if the height is greater than the limit. If it is, we set height to the limit value and set new width value using proportions.

function onTimer(evt:TimerEvent):void {
// new name
var newName:String = nameInput.text.replace("%initialName%", file.name.substr(0, file.name.lastIndexOf('.')));
newName = newName.replace("%num%", currentNum);

// new extension
var newExtension:String;
if (formatCombo.selectedIndex == 0) newExtension = file.extension;
if (formatCombo.selectedIndex == 1) newExtension = "jpg";
if (formatCombo.selectedIndex == 2) newExtension = "png";

// new size
var currentW:int = loader.content.width;
var currentH:int = loader.content.height;
var newW:int;
var newH:int;
var ratio:Number;

if (actionCombo.selectedIndex == 0) {
if (widthMeasure.selectedIndex == 0) newW = newWidth.value/100 * currentW;
if (widthMeasure.selectedIndex == 1) newW = newWidth.value;
if (heightMeasure.selectedIndex == 0) newH = newHeight.value/100 * currentH;
if (heightMeasure.selectedIndex == 1) newH = newHeight.value;
}

if (actionCombo.selectedIndex == 1) {
ratio = currentW / currentH;
if (widthMeasure.selectedIndex == 0) newW = newWidth.value/100 * currentW;
if (widthMeasure.selectedIndex == 1) newW = newWidth.value;
newH = newW / ratio;
}

if (actionCombo.selectedIndex == 2) {
ratio = currentW / currentH;
if (heightMeasure.selectedIndex == 0) newH = newHeight.value/100 * currentH;
if (heightMeasure.selectedIndex == 1) newH = newHeight.value;
newW = newH * ratio;
}

if (actionCombo.selectedIndex == 3) {
ratio = currentW / currentH;
newW = newWidth.value;
newH = newW / ratio;
if (newH > newHeight.value) {
newH = newHeight.value;
newW = ratio * newH;
}
}

// log
logArea.appendText("Resized from " + currentW + "x" + currentH + " to " + newW + "x" + newH + "\n");
logArea.appendText("Renamed to " + newName + "." + newExtension + "\n");

nextAction();
}

Now let's also go to actionChange() function, which is called when an action option is chosen in the combo box, and add a separate "case" block for the third selected index and here we will set the newWidth and newHeight combo boxes' enabled properties to false. We also set their selectedIndex to 1. This way the fourth action option will only work with pixel values, and that's exactly what we need.

private function actionChange():void{
switch (actionCombo.selectedIndex) {
case 0:
newWidth.enabled = true;
widthMeasure.enabled = true;
newHeight.enabled = true;
heightMeasure.enabled = true;
break;
case 1:
newWidth.enabled = true;
widthMeasure.enabled = true;
newHeight.enabled = false;
heightMeasure.enabled = false;
break;
case 2:
newWidth.enabled = false;
widthMeasure.enabled = false;
newHeight.enabled = true;
heightMeasure.enabled = true;
break;
case 3:
newWidth.enabled = true;
widthMeasure.enabled = false;
newHeight.enabled = true;
heightMeasure.enabled = false;
widthMeasure.selectedIndex = 1;
heightMeasure.selectedIndex = 1;
}
}

Full code:

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
width="300" height="460" 
showStatusBar="false" title="KirSizer" creationComplete="init();">
   
<fx:Declarations>
<mx:ArrayCollection id="measures">
<fx:String>%</fx:String>
<fx:String>px</fx:String>
</mx:ArrayCollection>
<mx:ArrayCollection id="actions">
<fx:String>Fixed width, fixed height</fx:String>
<fx:String>Fixed width, proportional height</fx:String>
<fx:String>Proportional width, fixed height</fx:String>
<fx:String>Proportional sizes to fit specified sizes</fx:String>
</mx:ArrayCollection>
<mx:ArrayCollection id="formats">
<fx:String>Same format as initial file</fx:String>
<fx:String>Convert all to JPG</fx:String>
<fx:String>Convert all to PNG</fx:String>
</mx:ArrayCollection>
<mx:Fade id="fadeIn" alphaFrom="0" alphaTo="1" duration="300"/>
<mx:Fade id="fadeOut" alphaFrom="1" alphaTo="0" duration="300"/>
<mx:TitleWindow id="waitWindow" title="Please wait" width="240" height="70" showCloseButton="false">
<s:Group width="100%" height="100%">
<s:Label top="10" left="10" id="waitLabel" width="220" color="0x000000" />
</s:Group>
</mx:TitleWindow>
</fx:Declarations>

<fx:Style>
@namespace s "library://ns.adobe.com/flex/spark";
@namespace mx "library://ns.adobe.com/flex/mx";

#contentStack{
backgroundColor: #313131;
}

s|Label{
color: #fcfcfc;
}

s|Button{
chromeColor: #636363;
}

mx|ComboBox{
chromeColor: #636363;
color: #fcfcfc;
contentBackgroundColor: #000000;
rollOverColor: #aaaaaa;
selectionColor: #ffffff;
}

#logArea{
contentBackgroundColor: #111111;
focusedTextSelectionColor: #0000ff;
fontFamily: "Courier New";
color: #aaaaaa;
}
</fx:Style>

<fx:Script>
<![CDATA[
import flash.display.Loader;
import flash.events.Event;
import flash.events.FileListEvent;
import flash.events.KeyboardEvent;
import flash.events.TimerEvent;
import flash.filesystem.File;
import flash.net.FileFilter;
import flash.net.URLRequest;
import flash.utils.Timer;
import mx.collections.ArrayCollection;
import mx.effects.easing.Linear;
import mx.controls.Alert;
import mx.events.CloseEvent;
import mx.events.FlexEvent;
import mx.events.StateChangeEvent;
import mx.managers.PopUpManager;

[Bindable]
private var selectedFiles:ArrayCollection = new ArrayCollection([]);
private var totalFiles:int;
private var currentNum:int;
private var totalErrors:int;

private function init():void {
addEventListener(KeyboardEvent.KEY_DOWN, keybDown);
}

private function keybDown(evt:KeyboardEvent):void {
if (evt.ctrlKey && evt.keyCode == 65) {
var arr:Array = [];
for (var i:int = 0; i < selectedFiles.length; i++) {
arr.push(i);
}
tileList.selectedIndices = arr;
}
}

private function actionChange():void{
switch (actionCombo.selectedIndex) {
case 0:
newWidth.enabled = true;
widthMeasure.enabled = true;
newHeight.enabled = true;
heightMeasure.enabled = true;
break;
case 1:
newWidth.enabled = true;
widthMeasure.enabled = true;
newHeight.enabled = false;
heightMeasure.enabled = false;
break;
case 2:
newWidth.enabled = false;
widthMeasure.enabled = false;
newHeight.enabled = true;
heightMeasure.enabled = true;
break;
case 3:
newWidth.enabled = true;
widthMeasure.enabled = false;
newHeight.enabled = true;
heightMeasure.enabled = false;
widthMeasure.selectedIndex = 1;
heightMeasure.selectedIndex = 1;
}
}

private function addFiles():void {
var file:File = new File();
file.browseForOpenMultiple("Select JPG or PNG files", [new FileFilter("Pictures", "*.jpg;*.jpeg;*.png")]);
file.addEventListener(FileListEvent.SELECT_MULTIPLE, filesSelected);
}

private function filesSelected(evt:FileListEvent):void {
PopUpManager.addPopUp(waitWindow, this);
PopUpManager.centerPopUp(waitWindow);
waitLabel.text = "Selecting files...";
var timer:Timer = new Timer(100, 1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE, onWait);
timer.start();
function onWait(ev:TimerEvent):void {
doFiles(evt.files);
PopUpManager.removePopUp(waitWindow);
}
}

private function addFolder():void {
var file:File = new File();
file.browseForDirectory("Select a directory");
file.addEventListener(Event.SELECT, folderSelected);
}

private function folderSelected(evt:Event):void {
var file:File = evt.currentTarget as File;
Alert.show("Do you want to select subfolders too?", "Recursive selection?", Alert.YES | Alert.NO, null, warningClose);

function warningClose(ev:CloseEvent):void {
if (ev.detail == Alert.YES) {
startFolder(file, true);
}
if (ev.detail == Alert.NO) {
startFolder(file, false);
}
}
}

private function startFolder(file:File, recurs:Boolean):void {
PopUpManager.addPopUp(waitWindow, this);
PopUpManager.centerPopUp(waitWindow);
waitLabel.text = "Selecting folders...";
var timer:Timer = new Timer(100, 1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE, onWait);
timer.start();
function onWait(ev:TimerEvent):void {
doFolder(file, recurs);
PopUpManager.removePopUp(waitWindow);
}
}

private function doFiles(files:Array):void {
for (var i:int = 0; i < files.length; i++) {
var alreadySelected:Boolean = false;
for (var u:int = 0; u < selectedFiles.length; u++) {
if (selectedFiles[u].type == 0 && selectedFiles[u].path == files[i].nativePath) {
alreadySelected = true;
}
}
if (!alreadySelected) selectedFiles.addItem({type:0, path:files[i].nativePath});
}
updateTotalFiles();
}

private function doFolder(file:File, isRecursive:Boolean):void {
var picturesInFolder:int = 0;
var childFiles:Array = file.getDirectoryListing();
for (var i:int = 0; i < childFiles.length; i++) {
if (childFiles[i].extension == "png" || childFiles[i].extension == "jpg" || childFiles[i].extension == "jpeg") {
picturesInFolder++;
}
if (childFiles[i].isDirectory && isRecursive) {
doFolder(childFiles[i], true);
}
}
if (picturesInFolder > 0) {
var alreadySelected:Boolean = false;
for (var a:int = 0; a < selectedFiles.length; a++) {
if (selectedFiles[a].type == 1 && selectedFiles[a].path == file.nativePath) {
alreadySelected = true;
}
}
if (!alreadySelected) selectedFiles.addItem( { type:1, path:file.nativePath, name:file.name, num:picturesInFolder } );
updateTotalFiles();
}
}

private function updateTotalFiles():void {
totalFiles = 0;
for (var i:int = 0; i < selectedFiles.length; i++) {
if (selectedFiles[i].type==1) {
totalFiles += selectedFiles[i].num;
}else {
totalFiles++;
}
}
labelSelected.text = totalFiles + " files selected";
}

private function removeSelected():void {
while (tileList.selectedItems.length > 0) {
selectedFiles.removeItemAt(tileList.selectedIndices[0]);
}
updateTotalFiles();
}

private function beginResize():void {
var canProceed:Boolean = true;
if (selectedFiles.length == 0) {
canProceed = false;
Alert.show("No files or folders are selected.", "Can't start resizing!");
}
if (nameInput.text.indexOf('%initialName%') < 0 && nameInput.text.indexOf('%num%') < 0) {
canProceed = false;
Alert.show("No wildcards were used in the name field! They are essential for each output file to have an unique name.", "Can't start resizing!");
}
var testName:String = nameInput.text.replace("%initialName%", "");
testName = testName.replace("%num%", "");
if(testName.indexOf('/')>=0 || testName.indexOf("\\")>=0 || testName.indexOf('?')>=0 || testName.indexOf('%')>=0 ||
testName.indexOf('*')>=0 || testName.indexOf(':')>=0 || testName.indexOf('|')>=0 || testName.indexOf('"')>=0 ||
testName.indexOf('<') >= 0 || testName.indexOf('>') >= 0 || testName.indexOf('.') >= 0) {
canProceed = false;
Alert.show("Illegal characters in the name field! Make sure file name field does not contain these characters: / \ ? % * : | \" < > . ", "Can't start resizing!");
}
if (canProceed) {
contentStack.selectedIndex = 3;
var timer:Timer = new Timer(400, 1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimer);
timer.start();
function onTimer(evt:TimerEvent):void {
buttonError.enabled = false;
buttonReturn.enabled = false;
logArea.text = "";
logArea.appendText("Beginning resizing of " + totalFiles + " files...\n");
currentNum = 0;
totalErrors = 0;
resizeNext();
}
}
}

private function resizeNext():void {
currentNum++;
progressBar.setProgress(currentNum, totalFiles);
var file:File = new File(selectedFiles[currentNum - 1].path);
logArea.appendText("#" + currentNum + " (" + file.name + ")\n");
var canProceed:Boolean = true;
var loader:Loader;

if (!file.exists) {
canProceed = false;
logArea.appendText("ERROR: File not found\n");
totalErrors++;
nextAction();
}

if (file.exists && file.extension.toLowerCase() != "png" && file.extension.toLowerCase() != "jpg" && file.extension.toLowerCase() != "jpeg") {
canProceed = false;
logArea.appendText("ERROR: Incorrect extension\n");
totalErrors++;
nextAction();
}

if (canProceed) {
loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete);
loader.load(new URLRequest(file.url));
}

function onLoadComplete(evt:Event):void {
var timer:Timer = new Timer(500, 1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimer);
timer.start();
}

function onTimer(evt:TimerEvent):void {
// new name
var newName:String = nameInput.text.replace("%initialName%", file.name.substr(0, file.name.lastIndexOf('.')));
newName = newName.replace("%num%", currentNum);

// new extension
var newExtension:String;
if (formatCombo.selectedIndex == 0) newExtension = file.extension;
if (formatCombo.selectedIndex == 1) newExtension = "jpg";
if (formatCombo.selectedIndex == 2) newExtension = "png";

// new size
var currentW:int = loader.content.width;
var currentH:int = loader.content.height;
var newW:int;
var newH:int;
var ratio:Number;

if (actionCombo.selectedIndex == 0) {
if (widthMeasure.selectedIndex == 0) newW = newWidth.value/100 * currentW;
if (widthMeasure.selectedIndex == 1) newW = newWidth.value;
if (heightMeasure.selectedIndex == 0) newH = newHeight.value/100 * currentH;
if (heightMeasure.selectedIndex == 1) newH = newHeight.value;
}

if (actionCombo.selectedIndex == 1) {
ratio = currentW / currentH;
if (widthMeasure.selectedIndex == 0) newW = newWidth.value/100 * currentW;
if (widthMeasure.selectedIndex == 1) newW = newWidth.value;
newH = newW / ratio;
}

if (actionCombo.selectedIndex == 2) {
ratio = currentW / currentH;
if (heightMeasure.selectedIndex == 0) newH = newHeight.value/100 * currentH;
if (heightMeasure.selectedIndex == 1) newH = newHeight.value;
newW = newH * ratio;
}

if (actionCombo.selectedIndex == 3) {
ratio = currentW / currentH;
newW = newWidth.value;
newH = newW / ratio;
if (newH > newHeight.value) {
newH = newHeight.value;
newW = ratio * newH;
}
}

// log
logArea.appendText("Resized from " + currentW + "x" + currentH + " to " + newW + "x" + newH + "\n");
logArea.appendText("Renamed to " + newName + "." + newExtension + "\n");

nextAction();
}

}

private function nextAction():void {
if (currentNum < totalFiles) {
resizeNext();
}else {
logArea.appendText("Operation complete!");
buttonError.enabled = true;
buttonReturn.enabled = true;
}
}
]]>
</fx:Script>
   
<mx:ViewStack id="contentStack" width="100%" height="100%" >
<s:NavigatorContent width="100%" height="100%" hideEffect="fadeOut" showEffect="fadeIn">
<s:VGroup width="100%" height="100%" paddingLeft="10" paddingTop="10" paddingRight="10" paddingBottom="10">
<s:Label id="labelSelected">0 files selected</s:Label>
<mx:TileList id="tileList" width="282" height="100%" dataProvider="{selectedFiles}" itemRenderer="TileRenderer" columnWidth="60" rowHeight="60" columnCount="4" allowMultipleSelection="true" selectionColor="0xff0000" rollOverColor="0xff8888" />
<s:Button label="Add folder" width="100%" click="addFolder();" />
<s:Button label="Add files" width="100%" click="addFiles();" />
<s:Button label="{'Remove ' + tileList.selectedItems.length + ' items'}" width="100%" enabled="{tileList.selectedItems.length>0}" click="removeSelected();" />
<s:Button label="Continue" width="100%" click="contentStack.selectedIndex = 1;" />
</s:VGroup>
</s:NavigatorContent>
<s:NavigatorContent width="100%" height="100%" hideEffect="fadeOut" showEffect="fadeIn">
<s:VGroup width="100%" height="100%" paddingLeft="10" paddingTop="10" paddingRight="10" paddingBottom="10">
<s:Button label="Return to file selection" width="100%" click="contentStack.selectedIndex = 0;" />

<s:Label>Resize options:</s:Label>

<mx:ComboBox width="100%" id="actionCombo" height="22" dataProvider="{actions}" selectedIndex="0" editable="false" change="actionChange();"
openEasingFunction="Linear.easeOut" closeEasingFunction="Linear.easeIn" openDuration="300" closeDuration="300"/>
<s:HGroup verticalAlign="middle">
<s:Label width="50">Width:</s:Label>
<s:NumericStepper id="newWidth" height="22" width="150" minimum="1" value="100" maximum="{(widthMeasure.selectedIndex==0)?(100):(4000)}" />
<mx:ComboBox id="widthMeasure" height="22" width="50" dataProvider="{measures}" selectedIndex="0" editable="false" 
openEasingFunction="Linear.easeOut" closeEasingFunction="Linear.easeIn" openDuration="300" closeDuration="300"/>
</s:HGroup>

<s:HGroup verticalAlign="middle">
<s:Label width="50">Height:</s:Label>
<s:NumericStepper id="newHeight" height="22" width="150" minimum="1" value="100" maximum="{(heightMeasure.selectedIndex==0)?(100):(4000)}"/>
<mx:ComboBox id="heightMeasure" height="22" width="50" dataProvider="{measures}" selectedIndex="0" editable="false" 
openEasingFunction="Linear.easeOut" closeEasingFunction="Linear.easeIn" openDuration="300" closeDuration="300"/>
</s:HGroup>

<s:Label/>

<s:Label>Output file names:</s:Label>
<s:HGroup verticalAlign="middle">
<s:TextInput id="nameInput" width="240" text="%initialName%" />
<s:Button width="35" label="?" click="contentStack.selectedIndex = 2;" />
</s:HGroup>

<s:Label/>

<s:Label>Output destination:</s:Label>
<s:HGroup verticalAlign="middle">
<s:RadioButton id="oldDestination" label="Same directory" groupName="destinationGroup" selected="true" />
<s:RadioButton id="newDestination" label="Specified directory" groupName="destinationGroup" />
</s:HGroup>
<s:HGroup verticalAlign="middle" width="100%">
<s:TextInput width="100%" enabled="{newDestination.selected}" text="Select destination..." editable="false" />
<s:Button width="80" label="Browse" enabled="{newDestination.selected}"/>
</s:HGroup>

<s:Label/>

<s:Label>Output format:</s:Label>
<mx:ComboBox width="100%" height="22" id="formatCombo" dataProvider="{formats}" selectedIndex="0" editable="false" 
openEasingFunction="Linear.easeOut" closeEasingFunction="Linear.easeIn" openDuration="300" closeDuration="300"/>

<s:Label/>

<s:Label>Output JPG quality:</s:Label>
<s:HSlider width="100%" minimum="1" maximum="100" value="100" />

<s:Label/>

<s:Button label="Resize" width="100%" click="beginResize();" />
</s:VGroup>
</s:NavigatorContent>
<s:NavigatorContent width="100%" height="100%" hideEffect="fadeOut" showEffect="fadeIn">
<s:VGroup width="280" top="10" left="10">
<s:VGroup width="100%" height="410" gap="20">
<s:Label fontSize="20" width="100%" fontWeight="bold">Output file names</s:Label>
<s:Label width="100%">You can build names for output files using provided wildcards and combining them with text.</s:Label>
<s:Label width="100%">For example, "%initialName%_new" will generate names like "pic_new.jpg", "img_new.png", etc.</s:Label>
<s:Label width="100%">Available wildcards are:</s:Label>
<s:Label fontSize="18" width="100%" fontWeight="bold">%initialName%</s:Label>
<s:Label width="100%">Puts the initial name of the file that's being resized.</s:Label>
<s:Label fontSize="18" width="100%" fontWeight="bold">%num%</s:Label>
<s:Label width="100%">Gives each file a unique id number starting from 1.</s:Label>
</s:VGroup>
<s:Button label="Back" width="100%" click="contentStack.selectedIndex = 1;" />
</s:VGroup>
</s:NavigatorContent>
<s:NavigatorContent width="100%" height="100%" hideEffect="fadeOut" showEffect="fadeIn">
<s:VGroup width="280" height="440" top="10" left="10">
<s:Label width="100%" color="0xff3333" fontWeight="bold">Do not add, remove or rename selected files.</s:Label>
<mx:ProgressBar id="progressBar" width="100%" mode="manual" label="Resizing %1/%2" color="0xffffff" />
<s:TextArea id="logArea" editable="false" width="100%" height="100%" />
<s:HGroup>
<s:Button id="buttonError" label="Save error log" width="136" />
<s:Button id="buttonReturn" label="Return" click="contentStack.selectedIndex = 0;" width="136" />
</s:HGroup>
</s:VGroup>
</s:NavigatorContent>
</mx:ViewStack>
</s:WindowedApplication>

Thanks for reading!

No comments:

Post a Comment