1 module app;
2 
3 version (app){
4 	import std.stdio;
5 	import std.conv : to;
6 	import core.stdc.stdlib;
7 	import utils.misc;
8 	import std.file;
9 	import pngtext.pngtext;
10 	// QUI for the text editor
11 	import editor : Editor;
12 
13 	/// stores the version
14 	const VERSION = "0.2.1";
15 
16 	/// stores the default density
17 	const DEFAULT_DENSITY = 1;
18 
19 	/// help text
20 	const string HELP_TEXT = "pngtext - hides data inside png images
21 usage:
22 pngtext [command] [options]
23 pngtext [pngFile.png] - to open basic text editor
24 commands:
25  write         write to a png file, if --file is not specified, stdin is used to
26                input data
27  read          read data from a png file.
28  size          calculate how many bytes a png image can store.
29  editor        opens a text editor in terminal to edit hidden text.
30 options:
31  --file -f     specify file containing data to write into png image
32  --input -i    specify original png image to write to, or read from
33  --ouput -o    specify file to write output to, for write, and read. Default is
34                same as --input, will overwrite.
35 --quality -q   specify quality, for use with size command. 
36                1 - Highest, 2 - High, 3 - Low, 4 - Zero quality. Default: 1
37  --version -v  display this program's version
38  --help -h     display this message";
39 
40 	void main(string[] args){
41 		if (args.length >= 2){
42 			if (args[1] == "--version" || args[1] == "-v"){
43 				writeln(VERSION);
44 			}else if (args[1] == "--help" || args[1] == "-h"){
45 				writeln(HELP_TEXT);
46 			}else{
47 				string command;
48 				string[string] options;
49 				string[] errors;
50 				options = readArgs(args[1 .. args.length].dup, command, errors);
51 				foreach (error; errors){
52 					stderr.writeln ("error: "~error);
53 				}
54 				if (errors.length > 0){
55 					exit(1);
56 				}
57 				errors = validateOptions(options.dup, command);
58 				foreach (error; errors){
59 					stderr.writeln ("error: "~error);
60 				}
61 				if (errors.length > 0){
62 					exit(1);
63 				}
64 				if (command == "write"){
65 					string inputFile = options["input"];
66 					string outputFile = "output" in options ? options["output"] : inputFile;
67 					string text;
68 					if ("file" in options){
69 						text = cast(string)cast(char[])read(options["file"]);
70 					}else{
71 						text = "";
72 						while (!stdin.eof){
73 							char c;
74 							readf ("%s", c);
75 							text ~= c;
76 						}
77 						if (text[$-1] == 0xFF)
78 							text.length--; // remove the 0xFF from end
79 					}
80 					errors = writeDataToPng(inputFile, outputFile, cast(ubyte[])cast(char[])text);
81 					foreach (error; errors){
82 						stderr.writeln (error);
83 					}
84 					if (errors.length > 0){
85 						exit(1);
86 					}
87 				}else if (command == "read"){
88 					string inputFile = options["input"];
89 					ubyte[] text;
90 					try{
91 						text = readDataFromPng(inputFile);
92 					}catch (Exception e){
93 						stderr.writeln ("Failed to read from png image:\n",e.msg);
94 					}
95 					if ("output" in options){
96 						try{
97 							File outputFile = File(options["output"], "w");
98 							outputFile.write(cast(char[])text);
99 							outputFile.close();
100 						}catch (Exception e){
101 							stderr.writeln ("Failed to write to output file:\n",e.msg);
102 						}
103 					}else{
104 						write (cast(string)cast(char[])text);
105 					}
106 				}else if (command == "size"){
107 					string inputFile = options["input"];
108 					string quality = "quality" in options ? options["quality"] : "1";
109 					if (["1","2","3","4"].hasElement(quality)){
110 						try{
111 							writeln (calculatePngCapacity(inputFile, quality=="1"?1 : (quality == "2" ? 2 : (quality == "3" ? 4 : 8))));
112 						}catch (Exception e){
113 							stderr.writeln ("Failed to read png image:\n",e.msg);
114 						}
115 					}else{
116 						stderr.writeln ("Invalid value for --quality provided");
117 						exit (1);
118 					}
119 				}else if (command == "editor"){
120 					Editor editorInstance = new Editor(options["input"], "output" in options ? options["output"] : options["input"]);
121 					if (!editorInstance.run)
122 						exit(1);
123 					.destroy(editorInstance);
124 				}
125 			}
126 		}else{
127 			stderr.writeln("usage:\n pngtext [command] [options]");
128 			stderr.writeln("or, to open basic text editor:\n pngtext [pngFile.png]");
129 			stderr.writeln("or enter following for help:\n pngtext --help");
130 		}
131 	}
132 
133 	/// reads arguments, returns values for options in assoc array
134 	/// `args` is the array containing arguments. arg0 (executable name) should not be included in this
135 	/// `command` is the string in which the provided command will be "returned"
136 	/// `errors` is the array to put any errors in
137 	private string[string] readArgs(string[] args, ref string command, ref string[] errors){
138 		/// stores list of possible options
139 		string[] optionNames = [
140 			"file", "input", "quality", "output"
141 		];
142 		/// returns option name from the option provided in arg
143 		/// returns zero length string if invalid
144 		static string getOptionName(string option){
145 			/// stores full option names for short names
146 			const string[string] completeOptionNames = [
147 				"-f" : "file",
148 				"-i" : "input",
149 				"-q" : "quality",
150 				"-o" : "output"
151 			];
152 			if (option.length >= 3 && option[0 .. 2] == "--"){
153 				option = option[2 .. option.length];
154 			}else if (option.length == 2 && option[0] == '-' && option in completeOptionNames){
155 				option = completeOptionNames[option];
156 			}else{
157 				option = "";
158 			}
159 			return option;
160 		}
161 		string[string] r;
162 		errors = [];
163 		if (args.length == 0){
164 			errors ~= "no arguments provided";
165 		}
166 		if (["read","write","size","editor"].indexOf(args[0]) == -1){
167 			// beware ugly hack below
168 			command = "editor";
169 			args = [command, "-i", args[0]];
170 		}else{
171 			command = args[0];
172 		}
173 		args = args[1 .. args.length];
174 		for (uinteger i = 0; i < args.length; i ++){
175 			if (args[i][0] == '-'){
176 				// is an option, add error if is !valid
177 				string optionName = getOptionName(args[i]);
178 				if (optionName.length == 0 || optionNames.indexOf(optionName) == -1){
179 					errors ~= args[i]~" is not a valid option, use --help";
180 					break;
181 				}
182 				// get value
183 				if (args.length > i+1){
184 					r[optionName] = args[i+1];
185 					i += 1;
186 				}else{
187 					errors ~= "value for "~optionName~" not provided";
188 				}
189 			}
190 		}
191 		if (errors.length > 0){
192 			r.clear;
193 		}
194 		return r;
195 	}
196 
197 	/// validates if all the values provided for options are correct
198 	/// Returns: array containing errors, [] if no errors found
199 	private string[] validateOptions(string[string] options, string command){
200 		// assume that only correct options were passed, no "option does not exist" checks are here
201 		string[] errors = [];
202 		if (command == "write"){
203 			foreach (option; ["input"]){
204 				if (option !in options){
205 					errors ~= "--"~option~" not specified";
206 				}
207 			}
208 		}else if (command == "read" || command == "size"){
209 			if ("input" !in options){
210 				errors ~= "--input not specified";
211 			}
212 		}else if (command == "editor"){
213 			if ("input" !in options){
214 				errors ~= "--input not specified";
215 			}
216 		}
217 		// check if provided files exist
218 		string[] filesToCheck = [];
219 		if ("input" in options)
220 			filesToCheck ~= options["input"];
221 		if ("file" in options)
222 			filesToCheck ~= options["file"];
223 		foreach (toCheck; filesToCheck){
224 			if (!exists(toCheck) || !isFile(toCheck)){
225 				errors ~= "file "~toCheck~" does not exist";
226 			}
227 		}
228 		return errors;
229 	}
230 }