///|
/// Basic decoded-image facts used for inventory and human review.
pub(all) struct ImageInfo {
width : Int32
height : Int32
luminance : Int32
red : Int32
green : Int32
blue : Int32
}
///|
/// Facts about one regular JPEG/PNG file. Missing fingerprint means decoding failed.
pub(all) struct Photo {
path : String
name : String
digest : String
fingerprint : String?
size : Int32
image_info : ImageInfo?
}
///|
pub(all) enum Kind {
Exact
Similar
Conflict
New
Unreadable
} derive(Eq)
///|
pub extend Kind with Eq::{not_equal, equal}
///|
pub(all) struct ReviewRow {
path : String
name : String
digest : String
kind : Kind
match_path : String?
similarity_distance : Int32?
destination : String?
size : Int32
image_info : ImageInfo?
}
///|
pub fn Kind::label(self : Kind) -> String {
match self {
Exact => "完全重复"
Similar => "疑似相似"
Conflict => "同名冲突"
New => "可新增"
Unreadable => "图片无法解码"
}
}
///|
pub fn Kind::code(self : Kind) -> String {
match self {
Exact => "exact"
Similar => "similar"
Conflict => "conflict"
New => "new"
Unreadable => "unreadable"
}
}
///|
pub fn hamming_distance(a : String, b : String) -> Int32 {
if a.length() != b.length() || (a.length() != 64 && a.length() != 176) {
return 177
}
let mut count = 0
for i = 0; i < a.length(); i = i + 1 {
if a[i] != b[i] {
count = count + 1
}
}
count
}
///|
fn unique_name(name : String, reserved : Map[String, String]) -> String {
if reserved.get(name.to_lower()) is None {
return name
}
let (stem, ext) = match name.rev_split_once(".") {
Some((head, tail)) if head.length() > 0 && tail.length() > 0 =>
(head.to_owned(), "." + tail.to_owned())
Some((head, _)) if head.length() > 0 => (head.to_owned(), "")
_ => (name, "")
}
for number = 1; number <= 100000; number = number + 1 {
let candidate = "\{stem}-import-\{number}\{ext}"
if reserved.get(candidate.to_lower()) is None {
return candidate
}
}
name + "-import-overflow"
}
///|
/// Similarity is a review hint; only identical bytes are auto-skipped.
pub fn build_plan(
existing : Array[Photo],
incoming : Array[Photo],
similar_limit~ : Int32 = 5,
) -> Array[ReviewRow] {
let effective_limit = similar_limit.max(0).min(176)
let digests : Map[String, String] = Map([])
let names : Map[String, String] = Map([])
let index = new_similarity_index(effective_limit)
for photo in existing {
if names.get(photo.name.to_lower()) is None {
names.set(photo.name.to_lower(), photo.path)
}
if photo.fingerprint is Some(_) {
if digests.get(photo.digest) is None {
digests.set(photo.digest, photo.path)
}
index.add(photo)
}
}
let rows : Array[ReviewRow] = []
for photo in incoming {
if photo.fingerprint is None {
rows.push({
path: photo.path,
name: photo.name,
digest: photo.digest,
kind: Unreadable,
match_path: None,
similarity_distance: None,
destination: None,
size: photo.size,
image_info: photo.image_info,
})
continue
}
match digests.get(photo.digest) {
Some(first) => {
rows.push({
path: photo.path,
name: photo.name,
digest: photo.digest,
kind: Exact,
match_path: Some(first),
similarity_distance: None,
destination: None,
size: photo.size,
image_info: photo.image_info,
})
continue
}
None => ()
}
let conflicting = names.get(photo.name.to_lower())
let best = match photo.fingerprint {
Some(fingerprint) => index.nearest(fingerprint, effective_limit)
None => None
}
let closest = match best {
Some(hit) => Some(hit.path)
None => None
}
let closest_distance = match best {
Some(hit) => hit.distance
None => 177
}
let kind = if photo.fingerprint is None {
Unreadable
} else if conflicting is Some(_) {
Conflict
} else if closest_distance <= effective_limit {
Similar
} else {
New
}
let match_path = match kind {
Conflict => conflicting
Similar => closest
_ => None
}
let destination = match kind {
Unreadable => None
_ => Some(unique_name(photo.name, names))
}
rows.push({
path: photo.path,
name: photo.name,
digest: photo.digest,
kind,
match_path,
similarity_distance: if kind == Similar {
Some(closest_distance)
} else {
None
},
destination,
size: photo.size,
image_info: photo.image_info,
})
if photo.fingerprint is Some(_) {
digests.set(photo.digest, photo.path)
index.add(photo)
match destination {
Some(target) => names.set(target.to_lower(), photo.path)
None => ()
}
}
}
rows
}