module plant_data; import dom_utils; import std.stdio; import std.array; import std.algorithm; import std.path; import std.file; import std.datetime; import std.typecons; import dxml.dom; struct Species { string id; string name; string scientificName; string description; string referenceLink; } struct Plant { string speciesScientificName; string identifier; uint generation; string plantingInfo; string description; } string speciesId(string scientificName) { import std.string; import std.regex; return scientificName .replaceAll(ctRegex!(`\s+`), "-") .replaceAll(ctRegex!(`ñ`), "n") .replaceAll(ctRegex!(`["“”\.‘’']`), "") .toLower; } struct PlantData { Species[] species; Plant[] plants; Plant[] plantsInSpecies(Species speciesItem) { Appender!(Plant[]) app; foreach (plant; plants) { if (plant.speciesScientificName == speciesItem.scientificName) { app ~= plant; } } Plant[] results = app[]; sort!((a, b) => a.identifier < b.identifier)(results); return results; } Species getSpecies(string name) { foreach (s; species) { if (s.scientificName == name) return s; } throw new Exception("No species with name " ~ name); } Plant getPlant(string identifier) { foreach (p; plants) { if (p.identifier == identifier) return p; } throw new Exception("No plant with identifier " ~ identifier); } } struct ImageFilePair { string filename; uint width, height; string thumbnailFilename; uint thumbnailWidth, thumbnailHeight; } PlantData parsePlantData(string filename) { import archive.zip; ZipArchive zip = new ZipArchive(std.file.read(filename)); auto contentZipEntry = zip.getFile("content.xml"); if (contentZipEntry is null) throw new Exception("Couldn't find content.xml in " ~ filename); DOMEntity!string dom = parseDOM(cast(string) contentZipEntry.data()); DOMEntity!string spreadsheet = dom.findDOMChild("office:document-content") .findDOMChild("office:body") .findDOMChild("office:spreadsheet"); DOMEntity!string speciesTable = spreadsheet.findDOMChild("table:table", ["table:name": "Species"]); DOMEntity!string[] speciesRows = speciesTable.findDOMChildren("table:table-row")[1..$]; DOMEntity!string plantsTable = spreadsheet.findDOMChild("table:table", ["table:name": "Plants"]); DOMEntity!string[] plantRows = plantsTable.findDOMChildren("table:table-row")[1..$]; PlantData result; auto speciesAppender = appender(&result.species); foreach (row; speciesRows) { if (row.children.length < 4) continue; Species species; species.name = readTableCellText(row.children[0]); species.scientificName = readTableCellText(row.children[1]); species.description = readTableCellText(row.children[2]); species.referenceLink = readTableCellText(row.children[3]); species.id = speciesId(species.scientificName); speciesAppender ~= species; } sort!((a, b) => a.name < b.name)(result.species); auto plantAppender = appender(&result.plants); foreach (row; plantRows) { import std.string : strip; if (row.children.length < 4) continue; Plant plant; plant.speciesScientificName = readTableCellText(row.children[0]); if (plant.speciesScientificName.strip.length == 0) continue; plant.identifier = readTableCellText(row.children[1]); string fGenStr = readTableCellText(row.children[2]); import std.conv : to; plant.generation = fGenStr[1..$].to!uint; plant.plantingInfo = readTableCellText(row.children[3]); if (row.children.length > 4) { plant.description = readTableCellText(row.children[4]); } plantAppender ~= plant; } sort!((a, b) => a.identifier < b.identifier)(result.plants); return result; } void ensureDirectories(PlantData data) { string basePath = buildPath("images", "garden"); if (!exists(basePath)) mkdirRecurse(basePath); string speciesDir = buildPath(basePath, "species"); if (!exists(speciesDir)) mkdir(speciesDir); foreach (s; data.species) { string thisSpeciesDir = buildPath(speciesDir, s.id); if (!exists(thisSpeciesDir)) mkdir(thisSpeciesDir); } string plantsDir = buildPath(basePath, "plants"); if (!exists(plantsDir)) mkdir(plantsDir); foreach (p; data.plants) { string thisPlantDir = buildPath(plantsDir, p.identifier); if (!exists(thisPlantDir)) mkdir(thisPlantDir); } } ImageFilePair[] getPlantImages(string identifier) { string plantDir = buildPath("images", "garden", "plants", identifier); if (!exists(plantDir)) return []; Appender!(ImageFilePair[]) app; foreach (entry; dirEntries(plantDir, SpanMode.shallow, false)) { if (entry.name.endsWith(".jpg") && !entry.name.endsWith(".thumb.jpg")) { ImageFilePair pair; pair.filename = entry.name; getImageSize(entry.name, pair.width, pair.height); string thumbnailFilename = buildPath(plantDir, baseName(entry.name, ".jpg") ~ ".thumb.jpg"); if (exists(thumbnailFilename)) { pair.thumbnailFilename = thumbnailFilename; getImageSize(thumbnailFilename, pair.thumbnailWidth, pair.thumbnailHeight); } app ~= pair; } } ImageFilePair[] images = app[]; sort!((a, b) { Nullable!DateTime tsA = getImageTimestamp(a.filename); Nullable!DateTime tsB = getImageTimestamp(b.filename); if (tsA.isNull && tsB.isNull) return a.filename < b.filename; if (tsA.isNull) return true; if (tsB.isNull) return false; return tsA.get < tsB.get; })(images); return images; } ImageFilePair[] getSpeciesImages(string speciesId) { string speciesDir = buildPath("images", "garden", "species", speciesId); if (!exists(speciesDir)) return []; Appender!(ImageFilePair[]) app; foreach (entry; dirEntries(speciesDir, SpanMode.shallow, false)) { if (entry.name.endsWith(".jpg") && !entry.name.endsWith(".thumb.jpg")) { ImageFilePair pair; pair.filename = entry.name; getImageSize(entry.name, pair.width, pair.height); string thumbnailFilename = buildPath(speciesDir, baseName(entry.name, ".jpg") ~ ".thumb.jpg"); if (exists(thumbnailFilename)) { pair.thumbnailFilename = thumbnailFilename; getImageSize(thumbnailFilename, pair.thumbnailWidth, pair.thumbnailHeight); } app ~= pair; } } ImageFilePair[] images = app[]; sort!((a, b) { Nullable!DateTime tsA = getImageTimestamp(a.filename); Nullable!DateTime tsB = getImageTimestamp(b.filename); if (tsA.isNull && tsB.isNull) return a.filename < b.filename; if (tsA.isNull) return true; if (tsB.isNull) return false; return tsA.get < tsB.get; })(images); return images; } void getImageSize(string filePath, out uint width, out uint height) { import std.process; import std.format; auto result = execute(["identify", "-ping", "-format", "'%w %h'", filePath]); if (result.status != 0) throw new Exception("Failed to get image size of " ~ filePath); formattedRead!"'%d %d'"(result.output, width, height); } Nullable!DateTime getImageTimestamp(string filePath) { import std.regex; import std.conv; auto r = ctRegex!(`\d{8}_\d{6}`); auto cap = matchFirst(baseName(filePath), r); if (cap.empty) return Nullable!DateTime.init; string text = cap[0]; return nullable(DateTime( text[0..4].to!int, text[4..6].to!int, text[6..8].to!int, text[9..11].to!int, text[11..13].to!int, text[13..15].to!int )); } void generateAllThumbnails(bool regen = false) { string plantsDir = buildPath("images", "garden", "plants"); foreach (entry; dirEntries(plantsDir, SpanMode.shallow, false)) { generateThumbnails(entry.name, regen); } string speciesDir = buildPath("images", "garden", "species"); foreach (entry; dirEntries(speciesDir, SpanMode.shallow, false)) { generateThumbnails(entry.name, regen); } } void generateThumbnails(string dir, bool regen) { import std.process; if (regen) { // Remove all thumbnails first. foreach (entry; dirEntries(dir, SpanMode.shallow, false)) { if (entry.name.endsWith(".thumb.jpg")) { std.file.remove(entry.name); } } } foreach (entry; dirEntries(dir, SpanMode.shallow, false)) { if (entry.name.endsWith(".jpg") && !entry.name.endsWith(".thumb.jpg")) { string filenameWithoutExt = baseName(entry.name, ".jpg"); string outputFilePath = buildPath(dir, filenameWithoutExt ~ ".thumb.jpg"); if (exists(outputFilePath)) continue; Pid pid = spawnProcess( [ "convert", entry.name, "-strip", "-interlace", "JPEG", "-sampling-factor", "4:2:0", "-colorspace", "RGB", "-quality", "85%", "-geometry", "x200", outputFilePath ] ); int exitCode = wait(pid); if (exitCode != 0) throw new Exception("Thumbnail generation process failed."); } } }