Add testing system
14
.gitignore
vendored
@ -13,3 +13,17 @@
|
|||||||
|
|
||||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||||
.glide/
|
.glide/
|
||||||
|
|
||||||
|
*~
|
||||||
|
*.pyc
|
||||||
|
env
|
||||||
|
*.jpg
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
ImageHash.egg-info/
|
||||||
|
.eggs
|
||||||
|
.DS_Store
|
||||||
|
.python-version
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
tmp
|
||||||
|
52
.pre-commit-config.yaml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.4.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
args: [--markdown-linebreak-ext=.gitignore]
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: debug-statements
|
||||||
|
- id: double-quote-string-fixer
|
||||||
|
- id: name-tests-test
|
||||||
|
- id: requirements-txt-fixer
|
||||||
|
- repo: https://github.com/tekwizely/pre-commit-golang
|
||||||
|
rev: v1.0.0-rc.1
|
||||||
|
hooks:
|
||||||
|
- id: go-mod-tidy
|
||||||
|
- id: go-imports
|
||||||
|
args: [-w]
|
||||||
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
|
rev: v1.53.3
|
||||||
|
hooks:
|
||||||
|
- id: golangci-lint
|
||||||
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
|
rev: v2.5.0
|
||||||
|
hooks:
|
||||||
|
- id: setup-cfg-fmt
|
||||||
|
- repo: https://github.com/asottile/reorder-python-imports
|
||||||
|
rev: v3.12.0
|
||||||
|
hooks:
|
||||||
|
- id: reorder-python-imports
|
||||||
|
args: [--py38-plus, --add-import, 'from __future__ import annotations']
|
||||||
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
|
rev: v3.1.0
|
||||||
|
hooks:
|
||||||
|
- id: add-trailing-comma
|
||||||
|
- repo: https://github.com/asottile/dead
|
||||||
|
rev: v1.5.2
|
||||||
|
hooks:
|
||||||
|
- id: dead
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v3.15.2
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: [--py38-plus]
|
||||||
|
- repo: https://github.com/hhatto/autopep8
|
||||||
|
rev: v2.1.0
|
||||||
|
hooks:
|
||||||
|
- id: autopep8
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 7.0.0
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
BIN
_examples/images/example.png
Normal file
After Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 319 KiB |
17
go.mod
@ -1,10 +1,19 @@
|
|||||||
module github.com/corona10/goimagehash
|
module github.com/corona10/goimagehash
|
||||||
|
|
||||||
go 1.20
|
go 1.21
|
||||||
|
|
||||||
require github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
toolchain go1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anthonynsimon/bild v0.13.0 // indirect
|
github.com/anthonynsimon/bild v0.13.0
|
||||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
github.com/gen2brain/avif v0.3.1
|
||||||
|
github.com/spakin/netpbm v1.3.0
|
||||||
|
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
|
||||||
|
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ebitengine/purego v0.7.1 // indirect
|
||||||
|
github.com/tetratelabs/wazero v1.7.1 // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
)
|
)
|
||||||
|
13
go.sum
@ -7,17 +7,21 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz
|
|||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
|
||||||
|
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/gen2brain/avif v0.3.1 h1:womS2LKvhS/dSR3zIKUxtJW+riGlY48akGWqc+YgHtE=
|
||||||
|
github.com/gen2brain/avif v0.3.1/go.mod h1:s9sI2zo2cF6EdyRVCtnIfwL/Qb3k0TkOIEsz6ovK1ms=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/spakin/netpbm v1.3.0 h1:eDX7VvrkN5sHXW0luZXRA4AKDlLmu0E5sNxJ7VSTwxc=
|
||||||
|
github.com/spakin/netpbm v1.3.0/go.mod h1:Q+ep6vNv1G44qSWp0wt3Y9o1m/QXjmaXZIFC0PMVpq0=
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
@ -25,13 +29,18 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
|
|||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/tetratelabs/wazero v1.7.1 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8=
|
||||||
|
github.com/tetratelabs/wazero v1.7.1/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||||
|
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 h1:uc17S921SPw5F2gJo7slQ3aqvr2RwpL7eb3+DZncu3s=
|
||||||
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
5
hashImage/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .hashImage import main
|
||||||
|
|
||||||
|
__all__ = ('main',)
|
5
hashImage/__main__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import hashImage
|
||||||
|
|
||||||
|
print(hashImage.main())
|
27
hashImage/hashImage.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
import imagehash
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def bin_str(hash):
|
||||||
|
return ''.join(str(b) for b in 1 * hash.hash.flatten())
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: list[str] | None = None) -> str:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('-file', required=True)
|
||||||
|
opts = ap.parse_args(args)
|
||||||
|
im = Image.open(opts.file)
|
||||||
|
|
||||||
|
return (
|
||||||
|
f'ahash: {bin_str(imagehash.average_hash(im))}\n'
|
||||||
|
f'dhash: {bin_str(imagehash.dhash(im))}\n'
|
||||||
|
f'phash: {bin_str(imagehash.phash(im))}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(main())
|
77
hashImage/main.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/gif"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/spakin/netpbm"
|
||||||
|
|
||||||
|
"github.com/corona10/goimagehash"
|
||||||
|
_ "github.com/gen2brain/avif"
|
||||||
|
_ "golang.org/x/image/bmp"
|
||||||
|
_ "golang.org/x/image/tiff"
|
||||||
|
_ "golang.org/x/image/webp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
imPath := flag.String("file", "", "image file to hash")
|
||||||
|
flag.Parse()
|
||||||
|
if imPath == nil || *imPath == "" {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(*imPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to open file %s: %s", *imPath, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
im, format, err := image.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Failed to decode Image: %s", err)
|
||||||
|
log.Println(msg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == "webp" {
|
||||||
|
if ycbcr, ok := im.(*image.YCbCr); ok {
|
||||||
|
im = goimagehash.FancyUpscale(ycbcr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ahash *goimagehash.ImageHash
|
||||||
|
dhash *goimagehash.ImageHash
|
||||||
|
phash *goimagehash.ImageHash
|
||||||
|
)
|
||||||
|
|
||||||
|
ahash, err = goimagehash.AverageHash(im)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Failed to ahash Image: %s", err)
|
||||||
|
log.Println(msg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
dhash, err = goimagehash.DifferenceHash(im)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Failed to dhash Image: %s", err)
|
||||||
|
log.Println(msg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
phash, err = goimagehash.PerceptionHash(im)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Failed to phash Image: %s", err)
|
||||||
|
log.Println(msg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("ahash: %s\n", ahash.BinString())
|
||||||
|
fmt.Printf("dhash: %s\n", dhash.BinString())
|
||||||
|
fmt.Printf("phash: %s\n", phash.BinString())
|
||||||
|
}
|
26
imagehash.go
@ -142,23 +142,13 @@ func ImageHashFromString(s string) (*ImageHash, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// String returns a hex representation of the hash
|
// String returns a hex representation of the hash
|
||||||
func (h *ImageHash) String(bin bool) string {
|
func (h *ImageHash) String() string {
|
||||||
strFmt := strFmtHex
|
return fmt.Sprintf("%016x", h.hash)
|
||||||
if bin {
|
}
|
||||||
strFmt = strFmtBin
|
|
||||||
}
|
// String returns a binary representation of the hash
|
||||||
kindStr := ""
|
func (h *ImageHash) BinString() string {
|
||||||
switch h.kind {
|
return fmt.Sprintf("%064b", h.hash)
|
||||||
case AHash:
|
|
||||||
kindStr = "a"
|
|
||||||
case PHash:
|
|
||||||
kindStr = "p"
|
|
||||||
case DHash:
|
|
||||||
kindStr = "d"
|
|
||||||
case WHash:
|
|
||||||
kindStr = "w"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(strFmt, kindStr, h.hash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewExtImageHash function creates a new big hash
|
// NewExtImageHash function creates a new big hash
|
||||||
@ -166,7 +156,7 @@ func NewExtImageHash(hash []uint64, kind Kind, bits int) *ExtImageHash {
|
|||||||
return &ExtImageHash{hash: hash, kind: kind, bits: bits}
|
return &ExtImageHash{hash: hash, kind: kind, bits: bits}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bits method returns an actual hash bit size
|
// Bits method returns the hash bit size
|
||||||
func (h *ExtImageHash) Bits() int {
|
func (h *ExtImageHash) Bits() int {
|
||||||
return h.bits
|
return h.bits
|
||||||
}
|
}
|
||||||
|
20
setup.cfg
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[metadata]
|
||||||
|
name = hashImage
|
||||||
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
|
license = BSD-2-Clause
|
||||||
|
license_files = LICENSE
|
||||||
|
classifiers =
|
||||||
|
License :: OSI Approved :: BSD License
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
description = Test go imagehash
|
||||||
|
deps =
|
||||||
|
pytest>=7
|
||||||
|
imagehash
|
||||||
|
commands =
|
||||||
|
python -m pytest {posargs:.}
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 120
|
||||||
|
extend-ignore = E203, E501, A003, A005, T202, E701
|
97
test/conftest.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import _pytest.terminal
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_pytest.terminal._color_for_type['incompatible'] = 'red'
|
||||||
|
_pytest.terminal._color_for_type['passable'] = 'yellow'
|
||||||
|
_pytest.terminal._color_for_type['compatible'] = 'green'
|
||||||
|
_pytest.terminal._color_for_type['exact'] = 'green'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.hookimpl
|
||||||
|
def pytest_report_teststatus(report, config):
|
||||||
|
"""Customizes the reporting of test statuses."""
|
||||||
|
if report.when != 'call':
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(report, pytest.CollectReport) or report.outcome in ('skipped'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
props = dict(report.user_properties)
|
||||||
|
ahash = props.get('ahash:', -1)
|
||||||
|
dhash = props.get('dhash:', -1)
|
||||||
|
phash = props.get('phash:', -1)
|
||||||
|
if ahash == dhash == phash == 0:
|
||||||
|
if report.outcome == 'failed':
|
||||||
|
return 'failed', 'E', ('EXACT', {'red': True})
|
||||||
|
return 'exact', 'E', ('EXACT', {'green': True})
|
||||||
|
if ahash >= 10 or dhash >= 10 or phash >= 10:
|
||||||
|
return 'incompatible', 'I', ('INCOMPATIBLE', {'red': True})
|
||||||
|
if 4 < ahash < 10 or 4 < dhash < 10 or 4 < phash < 10:
|
||||||
|
return 'passable', 'P', ('PASSABLE', {'yellow': True})
|
||||||
|
if ahash <= 4 or dhash <= 4 or phash <= 4:
|
||||||
|
return 'compatible', 'C', ('COMPATIBLE', {'green': True})
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
||||||
|
incompatible = terminalreporter._get_reports_to_display('incompatible')
|
||||||
|
passable = terminalreporter._get_reports_to_display('passable')
|
||||||
|
compatible = terminalreporter._get_reports_to_display('compatible')
|
||||||
|
exact = terminalreporter._get_reports_to_display('exact')
|
||||||
|
total = len(incompatible) + len(passable) + len(compatible) + len(exact)
|
||||||
|
if total < 1:
|
||||||
|
return
|
||||||
|
incompatible_percent = int((len(incompatible)/total)*100)
|
||||||
|
passable_percent = int((len(passable)/total)*100)
|
||||||
|
compatible_percent = int((len(compatible)/total)*100)
|
||||||
|
exact_percent = int((len(exact)/total)*100)
|
||||||
|
final_percent = ((len(exact)+len(compatible))/total)*100
|
||||||
|
|
||||||
|
parts = (
|
||||||
|
(f'incompatible: {incompatible_percent:02d}%', {'red': True}),
|
||||||
|
(f'passable: {passable_percent:02d}%', {'yellow': True}),
|
||||||
|
(f'compatible: {compatible_percent:02d}%', {'green': True}),
|
||||||
|
(f'exact: {exact_percent:02d}%', {'green': True}),
|
||||||
|
(
|
||||||
|
f'final: {final_percent:02.2f}%', {
|
||||||
|
'green' if final_percent > 80 else 'red': True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
line_parts = []
|
||||||
|
for text, markup in parts:
|
||||||
|
with_markup = terminalreporter._tw.markup(text, **markup)
|
||||||
|
line_parts.append(with_markup)
|
||||||
|
msg = ', '.join(line_parts)
|
||||||
|
terminalreporter.write_line(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption(
|
||||||
|
'--hasher', default='go run ./hashImage -file {}', help="The commandline image hasher to test ('go run ./hashImage -file {}')",
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
'--image-dir', default='_examples/images', help='The file path to a directory of images to test',
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
'--fail-skip', action='store_true', help="skip images that can't be decoded",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_generate_tests(metafunc):
|
||||||
|
if 'file' in metafunc.fixturenames:
|
||||||
|
file = metafunc.config.getoption('--image-dir')
|
||||||
|
files = []
|
||||||
|
cwd = pathlib.Path('').absolute()
|
||||||
|
for p in pathlib.Path(file).iterdir():
|
||||||
|
if not p.is_file():
|
||||||
|
continue
|
||||||
|
p = p.absolute()
|
||||||
|
if p.is_relative_to(cwd):
|
||||||
|
files.append(str(p.relative_to(cwd)))
|
||||||
|
else:
|
||||||
|
files.append(str(p.absolute()))
|
||||||
|
metafunc.parametrize('file', files)
|
57
test/hash_test.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import hashImage
|
||||||
|
|
||||||
|
|
||||||
|
def hamming_distance(h1, h2) -> int:
|
||||||
|
if isinstance(h1, int):
|
||||||
|
n1 = h1
|
||||||
|
else:
|
||||||
|
n1 = int(h1, 2)
|
||||||
|
|
||||||
|
if isinstance(h2, int):
|
||||||
|
n2 = h2
|
||||||
|
else:
|
||||||
|
n2 = int(h2, 2)
|
||||||
|
|
||||||
|
# xor the two numbers
|
||||||
|
n = n1 ^ n2
|
||||||
|
|
||||||
|
# count up the 1's in the binary string
|
||||||
|
return sum(b == '1' for b in bin(n)[2:])
|
||||||
|
|
||||||
|
|
||||||
|
def test_hash(request, file, record_property):
|
||||||
|
hasher = request.config.getoption('--hasher')
|
||||||
|
try:
|
||||||
|
python = hashImage.main(['-file', file]) + '\n'
|
||||||
|
except Exception:
|
||||||
|
pytest.skip('python imagehash failed')
|
||||||
|
return
|
||||||
|
|
||||||
|
sh = shlex.shlex(hasher, punctuation_chars=True, posix=True)
|
||||||
|
sh.whitespace_split = True
|
||||||
|
cmd_list = list(sh)
|
||||||
|
for i, p in enumerate(cmd_list):
|
||||||
|
if p == '{}':
|
||||||
|
cmd_list[i] = file
|
||||||
|
|
||||||
|
external = subprocess.run(cmd_list, shell=None, capture_output=True)
|
||||||
|
external_stdout = str(external.stdout, encoding='utf-8')
|
||||||
|
if external.returncode != 0:
|
||||||
|
pytest.skip(str(external.stderr))
|
||||||
|
return
|
||||||
|
external_stdout_h = python_h = ''
|
||||||
|
for p, e in zip(python.split('\n'), external_stdout.splitlines()):
|
||||||
|
p_, e_ = p.split(' '), e.split(' ')
|
||||||
|
h = hamming_distance(p_[1], e_[1])
|
||||||
|
record_property(p_[0], h)
|
||||||
|
external_stdout_h += ' '.join(e_) + f' {h}\n'
|
||||||
|
python_h += ' '.join(p_) + f' {h}\n'
|
||||||
|
|
||||||
|
assert external_stdout_h == python_h
|