#![doc = include_str!("../README.md")]
use bitvec::bitvec;
use bitvec::vec::BitVec;
#[cfg(feature = "image")]
use image::{DynamicImage, ImageBuffer, Luma};
#[cfg(feature = "image")]
mod convert;
mod vote;
#[cfg(feature = "image")]
use convert::ToLumaZero;
pub use vote::{Vote, Votes};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("inconsistency between the raw image array and the width and height provided")]
InvalidRawDimensions,
#[error("failed to encode the original image to a 99% quality JPEG: {0}")]
Encoding(#[from] jpeg_encoder::EncodingError),
#[cfg(feature = "image")]
#[error("failed to decode the image: {0}")]
Decoding(#[from] image::ImageError),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
pub struct Grid(pub u8);
impl Grid {
const fn from_xy(x: u32, y: u32) -> Self {
Self(((x % 8) + (y % 8) * 8) as u8)
}
pub const fn x(&self) -> u8 {
self.0 % 8
}
pub const fn y(&self) -> u8 {
self.0 / 8
}
}
#[derive(Default, Clone, Debug)]
pub struct ForgedRegion {
pub start: (u32, u32),
pub end: (u32, u32),
pub grid: Grid,
pub lnfa: f64,
regions_xy: Box<[(u32, u32)]>,
}
pub struct ForeignGridAreas {
luminance: LuminanceImage,
votes: Votes,
forged_regions: Box<[ForgedRegion]>,
lnfa_grids: [f64; 64],
main_grid: Option<Grid>,
}
impl ForeignGridAreas {
pub fn votes(&self) -> &Votes {
&self.votes
}
pub fn build_forgery_mask(&self) -> ForgeryMask {
ForgeryMask::from_regions(&self.forged_regions, self.votes.width, self.votes.height)
}
pub fn forged_regions(&self) -> &[ForgedRegion] {
self.forged_regions.as_ref()
}
pub fn lnfa_grids(&self) -> [f64; 64] {
self.lnfa_grids
}
pub fn main_grid(&self) -> Option<Grid> {
self.main_grid
}
pub fn is_cropped(&self) -> bool {
self.main_grid.map_or(false, |grid| grid.0 > 0)
}
#[cfg(feature = "image")]
pub fn detect_missing_grid_areas(&self) -> Result<Option<MissingGridAreas>> {
let main_grid = if let Some(main_grid) = self.main_grid {
main_grid
} else {
return Ok(None);
};
let jpeg_99 = self.luminance.to_jpeg_99_luminance()?;
let mut jpeg_99_votes = Votes::from_luminance(&jpeg_99);
for (&vote, vote_99) in self.votes.iter().zip(jpeg_99_votes.iter_mut()) {
if vote == Vote::AlignedWith(main_grid) {
*vote_99 = Vote::Invalid;
}
}
let jpeg_99_forged_regions = jpeg_99_votes.detect_forgeries(None, Grid(0));
Ok(Some(MissingGridAreas {
votes: jpeg_99_votes,
missing_regions: jpeg_99_forged_regions,
}))
}
}
impl IntoIterator for ForeignGridAreas {
type Item = ForgedRegion;
type IntoIter = std::vec::IntoIter<ForgedRegion>;
fn into_iter(self) -> Self::IntoIter {
self.forged_regions.into_vec().into_iter()
}
}
pub struct MissingGridAreas {
votes: Votes,
missing_regions: Box<[ForgedRegion]>,
}
impl MissingGridAreas {
pub fn votes(&self) -> &Votes {
&self.votes
}
pub fn forged_regions(&self) -> &[ForgedRegion] {
self.missing_regions.as_ref()
}
pub fn build_forgery_mask(self) -> ForgeryMask {
ForgeryMask::from_regions(&self.missing_regions, self.votes.width, self.votes.height)
}
}
impl IntoIterator for MissingGridAreas {
type Item = ForgedRegion;
type IntoIter = std::vec::IntoIter<ForgedRegion>;
fn into_iter(self) -> Self::IntoIter {
self.missing_regions.into_vec().into_iter()
}
}
pub struct Zero {
luminance: LuminanceImage,
}
impl Zero {
#[cfg(feature = "image")]
pub fn from_image(image: &DynamicImage) -> Self {
let luminance = image.to_luma32f_zero();
Self { luminance }
}
pub fn from_luminance_raw(luminance: Box<[f64]>, width: u32, height: u32) -> Result<Self> {
if luminance.len() != width.saturating_mul(height) as usize {
return Err(Error::InvalidRawDimensions);
}
Ok(Self {
luminance: LuminanceImage {
image: luminance,
width,
height,
},
})
}
pub fn detect_forgeries(self) -> ForeignGridAreas {
let votes = Votes::from_luminance(&self.luminance);
let (main_grid, lnfa_grids) = votes.detect_global_grids();
let forged_regions = votes.detect_forgeries(main_grid, Grid(63));
ForeignGridAreas {
luminance: self.luminance,
votes,
forged_regions,
lnfa_grids,
main_grid,
}
}
}
#[cfg(feature = "image")]
impl IntoIterator for Zero {
type Item = ForgedRegion;
type IntoIter = Box<dyn Iterator<Item = ForgedRegion>>;
fn into_iter(self) -> Self::IntoIter {
let foreign_grid_areas = self.detect_forgeries();
let missing_grid_regions = foreign_grid_areas
.detect_missing_grid_areas()
.ok()
.flatten()
.into_iter()
.flat_map(IntoIterator::into_iter);
let forged_regions = foreign_grid_areas.into_iter().chain(missing_grid_regions);
Box::new(forged_regions)
}
}
pub struct ForgeryMask {
mask: BitVec,
width: u32,
height: u32,
}
impl ForgeryMask {
#[cfg(feature = "image")]
pub fn into_luma_image(self) -> ImageBuffer<Luma<u8>, Vec<u8>> {
ImageBuffer::from_fn(self.width, self.height, |x, y| {
let index = (x + y * self.width) as usize;
Luma([u8::from(self.mask[index]) * 255])
})
}
pub fn is_forged(&self, x: u32, y: u32) -> bool {
self.mask
.get((x + y * self.width) as usize)
.as_deref()
.copied()
.unwrap_or(false)
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
fn from_regions(regions: &[ForgedRegion], width: u32, height: u32) -> Self {
let w = 9;
let mut mask_aux = bitvec![0; width as usize * height as usize];
let mut forgery_mask = bitvec![0; width as usize * height as usize];
for forged in regions {
for &(x, y) in forged.regions_xy.iter() {
for xx in (x - w)..=(x + w) {
for yy in (y - w)..=(y + w) {
let index = (xx + yy * width) as usize;
mask_aux.set(index, true);
forgery_mask.set(index, true);
}
}
}
}
for x in w..width.saturating_sub(w) {
for y in w..height.saturating_sub(w) {
let index = (x + y * width) as usize;
if !mask_aux[index] {
for xx in (x - w)..=(x + w) {
for yy in (y - w)..=(y + w) {
let index = (xx + yy * width) as usize;
forgery_mask.set(index, false);
}
}
}
}
}
Self {
mask: forgery_mask,
width,
height,
}
}
}
pub(crate) struct LuminanceImage {
image: Box<[f64]>,
width: u32,
height: u32,
}
impl LuminanceImage {
pub(crate) fn width(&self) -> u32 {
self.width
}
pub(crate) fn height(&self) -> u32 {
self.height
}
pub(crate) fn as_raw(&self) -> &[f64] {
&self.image
}
pub(crate) unsafe fn unsafe_get_pixel(&self, x: u32, y: u32) -> &f64 {
self.image.get_unchecked((x + y * self.width) as usize)
}
}