Skip to content

Commit

Permalink
feat: improve diff view, introduce multiple algorithms, add fuzziness…
Browse files Browse the repository at this point in the history
… setting, add prefix option
  • Loading branch information
Curve committed Oct 3, 2024
1 parent fe87284 commit ee3417c
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 30 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.21)
project(pdfcomp LANGUAGES CXX VERSION 1.0.0)
project(pdfcomp LANGUAGES CXX VERSION 2.0.0)

# --------------------------------------------------------------------------------------------------------
# Setup Executable
Expand Down
27 changes: 24 additions & 3 deletions include/pdf.hpp
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
#pragma once

#include <string>
#include <memory>
#include <optional>

#include <filesystem>

#include <cstdint>
#include <optional>

#include <tl/expected.hpp>

namespace pdfcomp
{
namespace fs = std::filesystem;

enum class error
enum class error : std::uint8_t
{
bad_file,
bad_directory,
mismatching_pages,
};

enum class algorithm : std::uint8_t
{
highlight,
difference,
};

struct options
{
double fuzz{0};
double tolerance{0};
algorithm method{algorithm::highlight};

public:
std::string prefix;
std::optional<fs::path> output;
};

class pdf
{
struct impl;
Expand All @@ -36,7 +57,7 @@ namespace pdfcomp

public:
[[nodiscard]] std::size_t pages() const;
[[nodiscard]] tl::expected<double, error> compare(const pdf &other, std::optional<fs::path> output) const;
[[nodiscard]] tl::expected<double, error> compare(const pdf &other, const options &opts) const;

public:
[[nodiscard]] static tl::expected<pdf, error> from(const fs::path &);
Expand Down
47 changes: 41 additions & 6 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,50 @@ int main(int argc, char **argv)
public:
struct
{
fs::path path;
fs::path value;
bool enabled{};
} diff;
} output;

struct
{
std::string value;
bool enabled{};
} prefix;

struct
{
double value{0};
bool enabled{};
} fuzz;

struct
{
double value{0};
bool enabled{};
} tolerance;

struct
{
std::uint8_t value{0};
bool enabled{};
} method;
} args;

auto options = clap::Options{
"pdfcomp",
"A utility to compare PDF and image files",
"1.0.0",
"2.0.0",
};

options //
.positional(args.first, "first") //
.positional(args.second, "second") //
.optional(args.tolerance.value, args.tolerance.enabled, "t,tol", "Absolute tolerance", "<value>")
.optional(args.diff.path, args.diff.enabled, "d,diff", "Folder to save a difference image(s) to", "<path>");
.optional(args.output.value, args.output.enabled, "o,output", "Folder to save a difference image(s) to", "<path>")
.optional(args.fuzz.value, args.fuzz.enabled, "f,fuzz", "Fuzziness to use for comparison", "<path>")
.optional(args.prefix.value, args.prefix.enabled, "p,prefix", "Filename prefix to use", "<value>")
.optional(args.method.value, args.method.enabled, "m,method",
"Highlighting algorithm to use (0 = default, 1 = Only Differences)", "<value>");

auto result = options.parse(argc, argv);

Expand All @@ -63,19 +85,32 @@ int main(int argc, char **argv)
return 1;
}

auto diff = first->compare(second.value(), args.diff.enabled ? std::optional{args.diff.path} : std::nullopt);
if (args.method.value > 1)
{
std::println(stderr, "Invalid method specified ({})", args.method.value);
return 1;
}

auto diff = first->compare(second.value(), {
.fuzz = args.fuzz.value,
.tolerance = args.tolerance.value,
.method = static_cast<pdfcomp::algorithm>(args.method.value),
.prefix = args.prefix.value,
.output = args.output.value,
});

if (!diff.has_value())
{
switch (diff.error())
{
case pdfcomp::error::bad_directory:
std::println(stderr, "Given output directory ('{}') is not valid", args.diff.path.string());
std::println(stderr, "Given output directory ('{}') is not valid", args.output.value.string());
break;
case pdfcomp::error::mismatching_pages:
std::println(stderr, "Given PDFs have differing page count ({}/{})", first->pages(), second->pages());
break;
default:
std::unreachable();
break;
}

Expand Down
104 changes: 84 additions & 20 deletions src/pdf.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "pdf.hpp"

#include <numeric>
#include <utility>

#include <mutex>
#include <print>
Expand All @@ -13,9 +14,19 @@

namespace pdfcomp
{
using Magick::ColorRGB;
using Magick::CompositeOperator;
using Magick::Geometry;
using Magick::Image;
using Magick::MetricType;

struct pdf::impl
{
std::vector<Magick::Image> pages;
std::vector<Image> pages;

public:
template <algorithm Algorithm>
static std::pair<Image, Image> compare(Image &, Image &);
};

pdf::pdf(impl data) : m_impl(std::make_unique<impl>(std::move(data))) {}
Expand All @@ -31,53 +42,106 @@ namespace pdfcomp
return m_impl->pages.size();
}

tl::expected<double, error> pdf::compare(const pdf &other, std::optional<fs::path> output) const
template <>
std::pair<Image, Image> pdf::impl::compare<algorithm::highlight>(Image &first, Image &second)
{
second.lowlightColor(ColorRGB{0, 0, 0, 0});
second.highlightColor(ColorRGB{0, 0, 255});

[[maybe_unused]] double distortion{};

return {
first.compare(second, MetricType::AbsoluteErrorMetric, &distortion),
second.compare(first, MetricType::AbsoluteErrorMetric, &distortion),
};
}

template <>
std::pair<Image, Image> pdf::impl::compare<algorithm::difference>(Image &first, Image &second)
{
auto diff = first;
diff.composite(second, 0, 0, CompositeOperator::ChangeMaskCompositeOp);

first.lowlightColor(ColorRGB{0, 0, 0, 0});

[[maybe_unused]] double distortion{};
auto highlight = first.compare(second, MetricType::AbsoluteErrorMetric, &distortion);

return {diff, highlight};
}

tl::expected<double, error> pdf::compare(const pdf &other, const options &opts) const
{
if (m_impl->pages.size() != other.m_impl->pages.size())
{
return tl::unexpected{error::mismatching_pages};
}

std::vector<std::pair<Magick::Image, double>> comp;
std::vector<double> diffs;

for (const auto &[first, second] : std::views::zip(m_impl->pages, other.m_impl->pages))
{
double distortion{};
auto image = first.compare(second, Magick::AbsoluteErrorMetric, &distortion);

comp.emplace_back(image, distortion);
first.colorFuzz(opts.fuzz);
diffs.emplace_back(first.compare(second, MetricType::AbsoluteErrorMetric));
}

const auto values = comp | std::views::values;
const auto differences = std::accumulate(values.begin(), values.end(), 0.0);
const auto total = std::accumulate(diffs.begin(), diffs.end(), 0.0);

if (!output)
if (!opts.output)
{
return differences;
return total;
}

const auto output = opts.output.value();

std::error_code ec{};
fs::create_directories(output.value());
fs::create_directories(output);

if (!fs::is_directory(output.value()))
if (!fs::is_directory(output))
{
return tl::unexpected{error::bad_directory};
}

for (auto [index, elem] : comp | std::views::enumerate)
for (const auto &[index, difference] : diffs | std::views::enumerate)
{
auto &[image, difference] = elem;

if (difference <= 0)
if (difference <= opts.tolerance)
{
continue;
}

const auto path = output.value() / std::format("{}.png", index);
image.write(path.string());
auto &first = m_impl->pages[index];
auto &second = other.m_impl->pages[index];

std::pair<Image, Image> result;

switch (opts.method)
{
case algorithm::highlight:
result = impl::compare<algorithm::highlight>(first, second);
break;
case algorithm::difference:
result = impl::compare<algorithm::difference>(first, second);
break;
}

const auto &[middle, right] = result;
auto canvas = first;

auto extent = Geometry{
first.size().width() + middle.size().width() + right.size().width(),
middle.size().height(),
};

canvas.extent(extent);

canvas.composite(middle, static_cast<ssize_t>(first.size().width()), 0);
canvas.composite(right, static_cast<ssize_t>(first.size().width() + middle.size().width()), 0);

const auto path = output / std::format("{}{}.png", opts.prefix, index);
canvas.write(path.string());
}

return differences;
return total;
}

tl::expected<pdf, error> pdf::from(const fs::path &document)
Expand Down

0 comments on commit ee3417c

Please sign in to comment.