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