-
Notifications
You must be signed in to change notification settings - Fork 0
/
tagUtils.js
293 lines (253 loc) · 11.4 KB
/
tagUtils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// copywrite 2023 Richard R. Lyman
function htmlEntities(str) {
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
/**
* If an entry contains tagged or giffed information, then skip it
* @param {*} entry
* @returns true if the entry should be skipped.
*/
function skipThisEntry(entry) {
let illegalName =
entry.name.includes(gifSuffix + "_") ||
entry.name.endsWith(gifSuffix) ||
entry.name.includes(labeledSuffix + "_") ||
entry.name.endsWith(labeledSuffix) ||
entry.name.startsWith(".");
let legalType = true;
if (!entry.isFolder) {
legalType = false;
let goodTypes = ['.bmp', /*'gif', */'.jpg', '.jpeg', '.png', '.psb', '.psd', '.tif', '.tiff'];
let eName = entry.name.toLowerCase();
goodTypes.forEach((fType) => {
if (eName.endsWith(fType)) {
if (eName.replace(fType, "").length > 0)
legalType = true;
}
});
}
return (!legalType) || illegalName;
};
/** Add text and and exiftool command to the html and cmd arrays
*
* @param {Array} htmlArray The array of text lines to be placed in the .html file
* @param {Array} exiftoolArray The array of text lines to be placed in the exiftool file
* @param {string} txt The text to be concatenated to the html array.
* @param {string} exiftoolText The tex to be cancatenated tot he exiftool array.
*/
function errorLog(html, cmd, txt, exiftoolText) {
html.push({ "txt": txt, "exif": exiftoolText });
if (txt.length > 0)
cmd.push("echo " + txt);
if (exiftoolText.length > 0)
cmd.push("echo " + exiftoolText);
cmd.push(exiftoolText);
}
const tabs = " "
/** make a dictionary of unique Warnings containing an array of files and exif commands for that warning
*
* @param {*} savedMetaData
* @returns a dictionary indexed by the warning text containing an array of files and exif commands for that warning
*/
function queryWarning(savedMetaData) {
let warningDB = {};
for (let nativePath in savedMetaData) {
savedMetaData[nativePath][meta_html].forEach((x) => {
// console.log(x.txt);
// console.log(" " + x.exif);
if (warningDB[x.txt] == undefined) warningDB[x.txt] = [];
warningDB[x.txt].push({ "nativePath": nativePath, "exif": x.exif });
});
}
return warningDB;
};
/** Create an errors.html and recommended.bat files
*
* @param {entry} errorFolder Folder entry to place the files into
* @param {{}} savedMetaData Persons, errors and such read from the metadata of each file.
*/
async function writeErrors(errorFolder, savedMetaData) {
// const errorFolder = await resultsFolder.createFolder("Error Results");
const pluginFolder = await fs.getPluginFolder();
const configFile = await pluginFolder.getEntry("facetags.config");
const text = await configFile.read();
const newConfigFile = await errorFolder.createEntry("facetags.config");
await newConfigFile.write(text), { append: false };
let html = [];
html.push('<!DOCTYPE html><html id="html"><head>');
html.push("<title>Metatdata Warnings</title>");
html.push("</head > <body>");
const warning = [
"MAJOR ALERT: The \"Recommendations.bat\" file will make changes to the metadata in your photos. " +
"Recommendations.bat\" uses \"exiftool\" that must be installed somewhere in the path in your system. " +
"For instance, " +
"<ol>" +
" <li>download exiftool(-k)</li>" +
"<li>rename it as exiftool.exe</li>" +
"<li>stick it in your Windows folder.</li>" +
"</ol>",
"The program, \"exiftool\", will leave the original files untouched with an added extension \"_original\" so you can always get back the originals.",
"However, the \"exiftool\" commands should only be tried on a temporary copy of your photo tree, until you are 100% satisfied with the results " +
"before trying them on your original photos. ",
"The recommendations fix up the following:" +
"<ul>" +
"<li>Convert from the Microsoft face recognition metadata format to the Metadata Working Group standard used by Adobe.</li>" +
"<li>Fix people keywords that have inadvertantly been left on photos without that person in the photo.</li>" +
"<li>Fix non person keywords, such as \"cars\" that has been mistakenly placed in a face recognition rectangle.</li>" +
"<li>Fix photos, where there is a face recognized, i.e. it has a face detection rectangle and a name entered for that person, but the keyword is missing from the \"keywording\" list</li>" +
"<li>Make a list of the non person keywords, shown in this file, so you can check it for accuracy. If there are no recommendations shown at the bottom of this file, " +
"then there were no global keywords detected and all keywords and face data are in agreement. </li>" +
"</ul>" +
"This plugin and Lightroom Classic communicate through the metadata in the files on the hard drive. Lightroom Classic has a database that must be transferred to the metadata " +
"on the hard drive. The metadata is then read by this Photoshop plugin to create the \"recommendations.bat\" file. " +
"The metatdata is updated by the exiftool commands in \"recommendations.bat\" and then is reread back into Lightroom Classic.",
"<ol>" +
"<li>Make sure that metadata on the hard drive corresponds to the Lightroom Classic database. Sync on Lightroom Classic does not work for face detection rectangles. " +
"Save Metadata to files does not work on face detection rectangles. \"Export as catalog\" does not work on face detection rectangles. Exporting a single file does work. " +
"The easiest workaround is in Lightroom Classic:</li>" +
"<ol type=\"A\">" +
"<li>Very carefully, working only on a backup copy of your photos, add a keyword such as \"x\" at the bottom of the keywords in \"Keywording\" in the top right of the screen. " +
" If the setting to automatically update xmp metadata has been enabled, Lightroom Classic will save to disk all the metadata whenever a keyword changes.</li> " +
"<li>In the keyword list, select \"x\" and choose \"delete\". Lightroom Classic will again write all metadata to the hard drive.</li> " +
"</ol> " +
"<li>Run the Recommendations.bat. To get a record of the results, in a terminal window type \"./recommendations.bat > results.log\"." +
"<li>Get the metadata back into Lightroom Classic. Since Sync does not work, select all the photos and from the menu, \"Read Metadata from files\" </li>" +
"</ol>"];
html.push("<h1> Metadata Warnings </h1>");
html.push("<p>" + warning.join("</p><p>") + "</p>");
// create a database indexed by the warning message.
let warningDB = queryWarning(savedMetaData);
for (let warning in warningDB) {
html.push("<div> " + htmlEntities(warning) + "</div>");
html.push('<p style="margin-left: 25px;">');
warningDB[warning].forEach((x) => {
let fileName = x.nativePath;
let fname = fileName.replaceAll("\"", "/").replaceAll(" ", "%20");
html.push("<a href=\"file://" + fname + "\" >" + fileName + "</a><br> ");
if (x.exif.length > 0)
html.push("Recommend: " + htmlEntities(x.exif) + "<br>");
});
html.push("</p>");
}
html.push("</body> </html>");
const errorFile = await errorFolder.createFile("Readme1st.html");
await errorFile.write(html.join(" "), { append: true });
let pc = ["echo " + warning.join(" ")];
for (let fileName in savedMetaData)
savedMetaData[fileName][meta_cmd].forEach((x) => { pc.push(x.replaceAll("<", "^<")) });
const exifFile = await errorFolder.createFile("recommendations.bat");
await exifFile.write(pc.join("\r\n"), { append: false });
};
/**
* Determine the suffix of the labeledDirectory folder by finding previous versions and adding 1 to the _n suffix of the folder name.
*/
/**
*
* @param {*} ents List of files and folders of the top level folder
* @returns The name of the output labeledDirectory top level folder
*/
function getFaceTagsTreeName(originalName, ents, suffix) {
let iMax = 0;
let targetFolder = originalName + suffix;
for (let i1 = 0; i1 < ents.length; i1++) {
if (ents[i1].name.toLowerCase().startsWith(targetFolder.toLowerCase())) {
let results = ents[i1].name.split("_");
if (results.length > 1) {
let x = Number(results.pop())
if (x > iMax) iMax = x;
}
}
}
iMax++;
return targetFolder + "_" + iMax.toString();
};
/**for the progress bar, get an overall file ocount.
*
* @param {*} folder The root file folder
* @returns the number of file entries under the root
*/
async function countFiles(folder) {
let ents = await folder.getEntries();
let iCount = 0;
for (let i = 0; i < ents.length && !stopFlag; i++) {
if (skipThisEntry(ents[i]))
continue;
iCount++;
if (ents[i].isFolder)
iCount += await countFiles(ents[i]);
}
return iCount;
};
function isAlpha(str) {
return ((("a" <= str) && ("z" >= str)) || ((str <= "Z") && (str >= "A")))
}
/** Given a string, capitalize the words
*
* @param {*} str String that may contain illegal characters
* @returns capitalized string
*/
function capitalize(str) {
if (str == undefined || str == null)
return str;
if (str.includes("florence")) {
let k = 5;
}
let newStr = "";
let notAlpha = true;
for (let i = 0; i < str.length; i++) {
if (isAlpha(str[i])) {
if (notAlpha) {
notAlpha = false;
newStr += str[i].toUpperCase();;
} else {
newStr += str[i].toLowerCase();
}
} else {
newStr += str[i];
notAlpha = true;
}
}
return newStr;
};
/** Given a string, remove any characters that will cause trouble later when the string is used to create a file name
* And standardize capitalization
*
* @param {*} str String that may contain illegal characters
* @returns sanitized string
*/
function removeIllegalFilenameCharacters(str) {
let fileName = str.toString();
// while (fileName.includes(" ")) {
// fileName = fileName.replaceAll(" ", " "); // remove double spaces
// }
["/", "\\", "\"", ":", "<", ">", "\|", "?", "*", "#"].forEach((x) => { fileName = fileName.replaceAll(x, ""); });
return fileName;
};
function addSetFunctions(set) {
Set.prototype.union = function (set2) {
let newSet = this;
set2.forEach((x) => { newSet.add(x) });
return newSet
}
Set.prototype.intersection = function (set2) {
let newSet = new Set();
this.forEach((x) => {
if (set2.has(x)) {
// console.log("intersect " + x)
newSet.add(x);
}
});
return newSet;
};
Set.prototype.difference = function (set2) {
let newSet = new Set();
this.forEach((x) => {
if (!set2.has(x))
newSet.add(x);
});
return newSet;
};
}
module.exports = {
getFaceTagsTreeName, skipThisEntry, countFiles, writeErrors, removeIllegalFilenameCharacters, capitalize, errorLog, capitalize, addSetFunctions
};