# BST-TIMEOUT: 900 # 272 cases; measured 111s on the BRINTOS-1997 batch image # # BRINTOS-2017 -- leptonica 1.82.0 on wasm32-hwjs. # # THIS PORT IS A LIBRARY WITH TEN CLI PROGRAMS IN FRONT OF IT, and the two halves # are covered by two different things for a reason. The ten programs convert # formats and report headers; they binarise nothing, erode nothing, scale nothing # and measure no skew. So the processing layer -- which is the half an OCR engine # is built on and the half this port exists for -- is driven from # data/leptonica-probe, one process, 198 of the 272 cases below. # # UPSTREAM'S OWN REGRESSION SUITE IS NOT WHAT RUNS HERE, and that is a judgement # about what it would measure rather than a limitation. prog/reg_wrapper.sh drives # each of the 142 `*_reg' drivers as `foo_reg generate && foo_reg compare', and the # tarball ships no golden directory -- so `generate' writes the reference that # `compare' then reads back, seconds later, from the same binary. On target that # asserts self-consistency and cannot catch a codec that is wrong the same way # twice. It also wants prog/'s 284 fixture files, 16 MB. The port's bbappend keeps # those drivers out of `make all' for the link cost; this suite covers the # operations they cover, against expectations that did not come from a previous run # of the code under test. # # WHERE EACH EXPECTATION COMES FROM, because they are not all the same kind: # # * CLOSED FORM. The fixtures are built by an integer recipe (see # src/leptonica_probe.c and src/leptonica_mkfixture.py) chosen so the answers # are derivable on paper: 277 ON pixels; a threshold at 128 on a `4*x' ramp # keeps 32 columns of 64 rows = 2048; a 3x3 erosion of a 20x12 rectangle, a 6x6 # and one isolated pixel leaves 18x10 + 4x4 + 0 = 196; three components with # bounding boxes the recipe states outright. # * ALGEBRA. Opening is idempotent and a subset of its input; four quarter turns # are the identity; a half turn twice is the identity. These hold for ANY # correct implementation and need no reference at all. # * AN INDEPENDENT ENCODER. data/leptonica-fixture.png is written by # src/leptonica_mkfixture.py from the PNG spec and zlib -- leptonica never # touched it -- and data/leptonica-fixture.rgb is the decode any correct reader # must produce. That is what makes `convertformat f.png -> PPM, compare the # payload' a claim about leptonica rather than a round trip with itself. # * MEASURED ON x86-64, and confined to the cases named `*-vs-x86'. The # interpolating operations -- pixScale's linear arm, Sauvola, the shear # rotation -- have no closed form worth writing down, so their values come from # an x86-64 build of the SAME source with the SAME codec set (giflib on, # openjpeg and webp off) and are pinned here. A divergence in one of those says # the arithmetic moved and says nothing about the rest of the suite. # # THE FORMAT LAYER IS ASSERTED FIRST AND SEPARATELY because it is where a # cross-build goes wrong: every lossless format is gated on bit-identical round # trip, and JPEG -- the only lossy one -- on a bounded per-channel maximum # difference instead. PackBits is in twice on purpose: for RGB, leptonica's TIFF # writer DOWNGRADES it to Deflate and says so, and the header code coming back as # ZIP rather than PackBits pins that; on 1 bpp it is honoured and the code comes # back PackBits. # # THE TWO DECODERS THAT LEAVE THROUGH longjmp ARE DRIVEN ON PURPOSE. libjpeg's # error_exit and libpng's png_error never return, so leptonica installs a setjmp # landing pad for each (src/jpegio.c, src/pngio.c) -- 19 sites, and the only # setjmp in the tarball. A truncated file reaches the pad from inside the decoder, # which is the cross-frame shape HwjsCoroutinize has to survive; the assertion is # that pixRead returns NULL and the process is still running to say so. . "$BST_LIB" require fileinfo leptonica/present tmp=${TMPDIR:-/tmp}/bst-leptonica.$$ rm -rf "$tmp"; mkdir -p "$tmp" || { bst_notok leptonica/tmpdir "cannot mkdir $tmp"; exit 0; } FIXPNG="$BST_DATA/leptonica-fixture.png" FIXRGB="$BST_DATA/leptonica-fixture.rgb" size() { wc -c < "$1" 2>/dev/null | tr -d ' '; } # usage -- a syntax line and the status beside # it. check_out cannot express this: every one of leptonica's ten programs prints # its syntax and exits 1, and check_out requires exit 0. usage() { _id=$1; _wrc=$2; _needle=$3; shift 3 _out=$("$@" 2>&1); _rc=$? if [ "$_rc" != "$_wrc" ]; then bst_notok "$_id" "rc=$_rc want $_wrc"; return; fi case "$_out" in *"$_needle"*) bst_ok "$_id" ;; *) bst_notok "$_id" "$(_bst_1line "$_out")" ;; esac } # magic -- the first n bytes of a file, as lowercase hex. magic() { got=$(dd if="$2" bs=1 count="$3" 2>/dev/null | od -An -tx1 | tr -d ' \n') if [ "$got" = "$4" ]; then bst_ok "$1"; else bst_notok "$1" "got $got want $4"; fi } # ----------------------------------------------------- 1. what the port ships for t in convertfilestopdf convertfilestops convertformat convertsegfilestopdf \ convertsegfilestops converttopdf converttops fileinfo imagetops \ xtractprotos; do if command -v "$t" >/dev/null 2>&1; then bst_ok "leptonica/prog-$t" else bst_notok "leptonica/prog-$t" "not installed (upstream bin_PROGRAMS names ten)"; fi done # The 282 noinst programs upstream builds and throws away are kept out of `make # all' by the port's patch. None of them may appear on the image; a re-appearance # is a patch that stopped applying, which is otherwise silent. for t in alltests_reg binarize_reg conncomp_reg skew_reg scale_reg rotate1_reg \ ioformats_reg pageseg_reg; do if command -v "$t" >/dev/null 2>&1; then bst_notok "leptonica/noinst-$t" "$t is installed; it is a noinst regression driver" else bst_ok "leptonica/noinst-$t"; fi done check leptonica/staticlib test -f /usr/lib/liblept.a check leptonica/pkgconfig test -f /usr/lib/pkgconfig/lept.pc for h in allheaders.h alltypes.h pix.h environ.h imageio.h morph.h; do check "leptonica/header-$h" test -f "/usr/include/leptonica/$h" done # Shape, not absence: this arm converts to shared libraries per recipe # (BRINTOS-1740), so a .so here would be a conversion rather than a breach. bst_lib_shape leptonica/no-shared "/usr/lib/liblept.so*" # The .pc a consumer reads. `-llept' rather than `-lleptonica' is upstream's # leptonica_OUTPUT_NAME (configure.ac:269), and the header dir has the package # name appended, which is why an #include is "leptonica/allheaders.h". pc=$(cat /usr/lib/pkgconfig/lept.pc 2>/dev/null) case "$pc" in *"Version: 1.82.0"*) bst_ok leptonica/pc-version ;; *) bst_notok leptonica/pc-version "$(_bst_1line "$pc")" ;; esac case "$pc" in *"-llept"*) bst_ok leptonica/pc-llept ;; *) bst_notok leptonica/pc-llept "no -llept in Libs" ;; esac case "$pc" in *"/leptonica"*) bst_ok leptonica/pc-incdir ;; *) bst_notok leptonica/pc-incdir "includedir is not .../leptonica" ;; esac # ------------------------------------------ 2. the fixture leptonica did not write check leptonica/fixture-png test -f "$FIXPNG" check leptonica/fixture-rgb test -f "$FIXRGB" check_eq leptonica/fixture-rgb-size 12288 "$(size "$FIXRGB")" cp "$FIXPNG" "$tmp/f.png" 2>/dev/null cp "$FIXRGB" "$tmp/f.rgb" 2>/dev/null check leptonica/fixture-staged test -f "$tmp/f.png" # --------------------------------------------------- 3. the CLI layer, end to end # fileinfo prints its report on stderr and returns 1 with the wrong argc. # EVERY ONE OF THE TEN PRINTS ITS SYNTAX AND EXITS 1 WITH NO ARGUMENTS, which is # worth asserting for all ten together: it is the cheapest statement that each is # the real program, that it started, that its argv parsing ran, and that it # returned rather than trapping. Two of them -- imagetops and fileinfo -- route # the message through leptonica's own ERROR_INT and so carry an `Error in' prefix # the other eight do not. usage leptonica/fileinfo-usage 1 "Syntax: fileinfo filein" fileinfo usage leptonica/convertformat-usage 1 "Syntax: convertformat filein" convertformat usage leptonica/converttops-usage 1 "Syntax: converttops filein" converttops usage leptonica/converttopdf-usage 1 "Syntax: converttopdf dir" converttopdf usage leptonica/convertfilestopdf-usage 1 "Syntax: convertfilestopdf dirin" convertfilestopdf usage leptonica/convertfilestops-usage 1 "Syntax: convertfilestops dirin" convertfilestops usage leptonica/convertsegfilestopdf-usage 1 "Syntax: convertsegfilestopdf" convertsegfilestopdf usage leptonica/convertsegfilestops-usage 1 "Syntax: convertsegfilestops" convertsegfilestops usage leptonica/imagetops-usage 1 "Syntax: imagetops " imagetops usage leptonica/xtractprotos-usage 1 "xtractprotos [-prestring=" xtractprotos info=$(fileinfo "$tmp/f.png" 2>&1); rc=$? if [ $rc -eq 0 ]; then bst_ok leptonica/fileinfo-rc else bst_notok leptonica/fileinfo-rc "rc=$rc"; fi for pair in \ "fmt:input image format type: png" \ "hdr:w = 64, h = 64, bps = 8, spp = 3, iscmap = 0" \ "full:w = 64, h = 64, d = 32, spp = 3, wpl = 64" \ "nocmap:colormap does not exist" ; do id=${pair%%:*}; needle=${pair#*:} case "$info" in *"$needle"*) bst_ok "leptonica/fileinfo-$id" ;; *) bst_notok "leptonica/fileinfo-$id" "no '$needle'" ;; esac done # THE DECODE ORACLE. The PPM leptonica writes carries a comment line, so the # payload is taken from the end rather than at a fixed offset; 64*64*3 bytes of # it must be exactly what mkfixture put in the PNG. if convertformat "$tmp/f.png" "$tmp/o.pnm" >/dev/null 2>&1; then bst_ok leptonica/cf-png-pnm else bst_notok leptonica/cf-png-pnm "convertformat png -> pnm failed"; fi tail -c 12288 "$tmp/o.pnm" > "$tmp/o.rgb" 2>/dev/null check leptonica/png-decode-vs-independent cmp "$tmp/o.rgb" "$tmp/f.rgb" # BMP and GIF, each back out through PPM, so the comparison is against the same # independent bytes rather than against leptonica's other encoder. if convertformat "$tmp/f.png" "$tmp/o.bmp" >/dev/null 2>&1; then bst_ok leptonica/cf-png-bmp else bst_notok leptonica/cf-png-bmp "convertformat png -> bmp failed"; fi convertformat "$tmp/o.bmp" "$tmp/b.pnm" >/dev/null 2>&1 tail -c 12288 "$tmp/b.pnm" > "$tmp/b.rgb" 2>/dev/null check leptonica/bmp-roundtrip-exact cmp "$tmp/b.rgb" "$tmp/f.rgb" if convertformat "$tmp/f.png" "$tmp/o.gif" >/dev/null 2>&1; then bst_ok leptonica/cf-png-gif else bst_notok leptonica/cf-png-gif "convertformat png -> gif failed"; fi convertformat "$tmp/o.gif" "$tmp/g.pnm" >/dev/null 2>&1 tail -c 12288 "$tmp/g.pnm" > "$tmp/g.rgb" 2>/dev/null check leptonica/gif-roundtrip-exact cmp "$tmp/g.rgb" "$tmp/f.rgb" hdr=$(fileinfo "$tmp/o.bmp" 2>&1) case "$hdr" in *"format type: bmp"*) bst_ok leptonica/hdr-bmp ;; *) bst_notok leptonica/hdr-bmp "$(_bst_1line "$hdr")" ;; esac hdr=$(fileinfo "$tmp/o.gif" 2>&1) case "$hdr" in *"format type: gif"*) bst_ok leptonica/hdr-gif ;; *) bst_notok leptonica/hdr-gif "$(_bst_1line "$hdr")" ;; esac # The GIF comes back 4 bpp colormapped: sixteen colours is what the fixture has, # so a wider bps here means the palette path widened rather than mapped. case "$hdr" in *"bps = 4, spp = 1, iscmap = 1"*) bst_ok leptonica/hdr-gif-cmap ;; *) bst_notok leptonica/hdr-gif-cmap "$(_bst_1line "$hdr")" ;; esac # A .tif EXTENSION MEANS TIFF_G4, WHICH IS BILEVEL, so an RGB input cannot use it # -- and upstream's documented answer is to transcode to PNG and rename the # output, not to fail. The named output does not appear; a .png does. ( cd "$tmp" && convertformat f.png ext.tif ) >/dev/null 2>&1 check leptonica/cf-ext-tif-fallback test ! -f "$tmp/ext.tif" check leptonica/cf-ext-tif-wrote-png test -f "$tmp/ext.png" # Asked explicitly, TIFF is honoured and stays 8 bps RGB. convertformat "$tmp/f.png" "$tmp/e.tif" TIFF >/dev/null 2>&1 hdr=$(fileinfo "$tmp/e.tif" 2>&1) case "$hdr" in *"format type: tif"*) bst_ok leptonica/cf-explicit-tiff ;; *) bst_notok leptonica/cf-explicit-tiff "$(_bst_1line "$hdr")" ;; esac case "$hdr" in *"bps = 8, spp = 3"*) bst_ok leptonica/cf-explicit-tiff-depth ;; *) bst_notok leptonica/cf-explicit-tiff-depth "$(_bst_1line "$hdr")" ;; esac # PostScript and PDF: leptonica writes both and reads neither, so the assertion # is the signature and a plausible size. if converttops "$tmp/f.png" "$tmp/one.ps" >/dev/null 2>&1; then bst_ok leptonica/converttops-rc else bst_notok leptonica/converttops-rc "converttops failed"; fi magic leptonica/converttops-magic "$tmp/one.ps" 4 "25215053" if [ "$(size "$tmp/one.ps")" -gt 500 ] 2>/dev/null; then bst_ok leptonica/converttops-size else bst_notok leptonica/converttops-size "$(size "$tmp/one.ps") bytes"; fi # imagetops takes a PostScript LEVEL, not a compression percentage, and refuses # anything outside 1..3 by name -- which is worth pinning because the syntax line # calls it a "compression level". if imagetops "$tmp/f.png" 2 "$tmp/two.ps" >/dev/null 2>&1; then bst_ok leptonica/imagetops-rc else bst_notok leptonica/imagetops-rc "imagetops level 2 failed"; fi magic leptonica/imagetops-magic "$tmp/two.ps" 4 "25215053" usage leptonica/imagetops-level0 1 "valid levels are: 1, 2, 3" \ imagetops "$tmp/f.png" 0 "$tmp/bad.ps" mkdir -p "$tmp/d" && cp "$tmp/f.png" "$tmp/d/" 2>/dev/null if converttopdf "$tmp/d" "$tmp/one.pdf" >/dev/null 2>&1; then bst_ok leptonica/converttopdf-rc else bst_notok leptonica/converttopdf-rc "converttopdf failed"; fi magic leptonica/converttopdf-magic "$tmp/one.pdf" 5 "255044462d" if convertfilestopdf "$tmp/d" allfiles 300 1.0 0 0 none "$tmp/many.pdf" \ >/dev/null 2>&1; then bst_ok leptonica/convertfilestopdf-rc else bst_notok leptonica/convertfilestopdf-rc "convertfilestopdf failed"; fi magic leptonica/convertfilestopdf-magic "$tmp/many.pdf" 5 "255044462d" if convertfilestops "$tmp/d" png 300 "$tmp/many.ps" >/dev/null 2>&1; then bst_ok leptonica/convertfilestops-rc else bst_notok leptonica/convertfilestops-rc "convertfilestops failed"; fi magic leptonica/convertfilestops-magic "$tmp/many.ps" 4 "25215053" # The two segmented-page converters are not driven any further than the usage # block above. Each wants a mask directory, a boxa file and a page-numbering # prefix this suite has no business synthesising, and a fixture invented to # satisfy the argument list would assert that the fixture was well formed rather # than anything about the port. # --------------------------------------------------------------- 4. the probe probe=$tmp/leptonica-probe OUT=$tmp/probe.out check leptonica/probe-payload test -f "$BST_DATA/leptonica-probe" if cp "$BST_DATA/leptonica-probe" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then bst_ok leptonica/probe-stage "$probe" "$tmp" > "$OUT" 2>"$tmp/probe.err"; prc=$? if [ $prc -eq 0 ]; then bst_ok leptonica/probe-rc else bst_notok leptonica/probe-rc "rc=$prc $(_bst_1line "$(cat "$tmp/probe.err")")"; fi else bst_notok leptonica/probe-stage "cannot stage $BST_DATA/leptonica-probe" : > "$OUT" fi # v -- one probe value, or the empty string. Every assertion below goes # through it, so a probe that printed nothing lands on the whole of section 4 as # named `not ok' lines rather than contributing nothing to the count. v() { awk -v k="$1" '$1=="LEPT" && $2==k {print $3; exit}' "$OUT"; } # eq eq() { check_eq "leptonica/$1" "$2" "$(v "$3")"; } # The probe reached its last line: everything above it ran. eq probe-end ok end # 4a. what the library says it is, and which codecs it was built against. eq version leptonica-1.82.0 version libs=$(awk '$1=="LEPT" && $2=="imagelibs" {$1="";$2="";print;exit}' "$OUT") for want in libgif libjpeg libpng libtiff zlib; do case "$libs" in *"$want"*) bst_ok "leptonica/imagelib-$want" ;; *) bst_notok "leptonica/imagelib-$want" "not in: $(_bst_1line "$libs")" ;; esac done # webp is off by the stock recipe's --without-libwebp and openjpeg by upstream's # default PACKAGECONFIG. Both are on this image for other ports, so their absence # here is the recipe's decision and not a missing dependency. for no in libwebp jp2k; do case "$libs" in *"$no"*) bst_notok "leptonica/imagelib-no-$no" "$no is linked in: $(_bst_1line "$libs")" ;; *) bst_ok "leptonica/imagelib-no-$no" ;; esac done # giflib and libtiff are pinned by their own ports on this image, so the version # leptonica reports for each is a statement about the link, not about leptonica. case "$libs" in *"libgif 5.2.2"*) bst_ok leptonica/imagelib-gif-version ;; *) bst_notok leptonica/imagelib-gif-version "$(_bst_1line "$libs")" ;; esac case "$libs" in *"libtiff 4.6.0"*) bst_ok leptonica/imagelib-tiff-version ;; *) bst_notok leptonica/imagelib-tiff-version "$(_bst_1line "$libs")" ;; esac # 4b. the fixtures are what the integer recipe says -- closed form throughout. eq fixtures ok fixtures eq rgb-w 64 rgb-w eq rgb-h 64 rgb-h eq rgb-d 32 rgb-d eq rgb-p00-r 0 rgb-p00-r eq rgb-p00-g 255 rgb-p00-g eq rgb-p00-b 0 rgb-p00-b eq rgb-p63-r 255 rgb-p63-r eq rgb-p63-g 0 rgb-p63-g eq rgb-p63-b 255 rgb-p63-b eq gray-p0 0 gray-p0 eq gray-p63 252 gray-p63 eq bin-on 277 bin-on eq text-on 39360 text-on # 4c. every format the enabled PACKAGECONFIG admits. # # Format codes are IFF_* from src/imageio.h: 1 BMP, 2 JFIF_JPEG, 3 PNG, 4 TIFF, # 5 TIFF_PACKBITS, 8 TIFF_G4, 9 TIFF_LZW, 10 TIFF_ZIP, 11 PNM, 13 GIF. They are # what pixReadHeader made of a file it did not write, so they assert the reader's # own sniffing rather than the writer's intent -- which is how the PackBits # downgrade below becomes visible at all. for spec in "png:3:3" "tiff:4:3" "tzip:10:3" "tlzw:9:3" \ "gif:13:1" "bmp:1:3" "pnm:11:3" \ "gpng:3:1" "gpnm:11:1" "bpng:3:1" "bg4:8:1" "bpbm:11:1" "bpb:5:1"; do tag=$(printf '%s' "$spec" | cut -d: -f1) wfmt=$(printf '%s' "$spec" | cut -d: -f2) wspp=$(printf '%s' "$spec" | cut -d: -f3) eq "rt-$tag-write" ok "rt-$tag-write" eq "rt-$tag-read" ok "rt-$tag-read" eq "rt-$tag-fmt" "$wfmt" "rt-$tag-hdr-fmt" eq "rt-$tag-w" 64 "rt-$tag-hdr-w" eq "rt-$tag-h" 64 "rt-$tag-hdr-h" eq "rt-$tag-spp" "$wspp" "rt-$tag-hdr-spp" eq "rt-$tag-same" 1 "rt-$tag-same" done # The GIF path is the one that goes through a colormap, so it is the one whose # cmap flag is worth asserting; every other format here comes back direct. eq rt-gif-cmap 1 rt-gif-hdr-cmap eq rt-png-cmap 0 rt-png-hdr-cmap # PACKBITS ON RGB IS SILENTLY DEFLATE. pixWriteStreamTiffWA refuses PackBits for # bpp > 1 -- "invalid compression type 5 for bpp > 1; using TIFF_ZIP" -- so the # request is honoured as ZIP, and the header code coming back 10 rather than 5 is # what says so. On 1 bpp (rt-bpb above) it is honoured and the code is 5. eq rt-tpb-write ok rt-tpb-write eq rt-tpb-read ok rt-tpb-read eq rt-tpb-fmt 10 rt-tpb-hdr-fmt eq rt-tpb-same 1 rt-tpb-same # The lossless formats are bit-identical, which the `-same' cases above already # say; these pin the per-channel difference at zero so a decode that is equal by # some other definition still lands on a named case. for tag in png tiff tzip tlzw tpb gif bmp pnm; do eq "rt-$tag-maxdiff" 0 "rt-$tag-maxdiff" done # JPEG IS THE ONLY LOSSY FORMAT HERE AND IS BOUNDED RATHER THAN PINNED. The # fixture's 16x16 blocks of flat colour are the worst case for a DCT at quality # 75 -- every block boundary is a step -- so a difference is expected; what is # asserted is that it stays inside the band an x86-64 build of the same source # measured (26), with headroom for a different libjpeg-turbo build. eq rt-jpeg-write ok rt-jpeg-write eq rt-jpeg-read ok rt-jpeg-read eq rt-jpeg-fmt 2 rt-jpeg-hdr-fmt eq rt-jpeg-w 64 rt-jpeg-hdr-w eq rt-jpeg-h 64 rt-jpeg-hdr-h eq rt-jpeg-spp 3 rt-jpeg-hdr-spp eq rt-jpeg-same 0 rt-jpeg-same jd=$(v rt-jpeg-maxdiff) if [ -n "$jd" ] && [ "$jd" -ge 1 ] && [ "$jd" -le 40 ] 2>/dev/null; then bst_ok leptonica/rt-jpeg-maxdiff-band else bst_notok leptonica/rt-jpeg-maxdiff-band "maxdiff=$jd, want 1..40 (x86-64 measured 26)" fi eq ps-write ok ps-write eq ps-magic 25214164 ps-magic eq pdf-write ok pdf-write eq pdf-magic 255044462d pdf-magic # 4d. the two decoders that must leave through longjmp. eq corrupt-png null corrupt-png eq corrupt-jpeg null corrupt-jpeg # 4e. binarisation. # Closed form: the ramp is 4*x, so `< 128' is x < 32 -- 32 columns, 64 rows. eq thresh128-on 2048 thresh128-on eq otsu-rc 0 otsu-rc # Otsu on a uniform ramp has no interesting maximum, so the threshold is reported # and banded rather than pinned: what matters is that it lands inside the data's # range and splits it, not exactly where. ot=$(v otsu-thresh) if [ -n "$ot" ] && [ "$ot" -gt 20 ] && [ "$ot" -lt 230 ] 2>/dev/null; then bst_ok leptonica/otsu-thresh-band else bst_notok leptonica/otsu-thresh-band "thresh=$ot, want 20..230 (x86-64 measured 86)" fi on=$(v otsu-on) if [ -n "$on" ] && [ "$on" -gt 0 ] && [ "$on" -lt 4096 ] 2>/dev/null; then bst_ok leptonica/otsu-on-band else bst_notok leptonica/otsu-on-band "on=$on, want 1..4095 (x86-64 measured 1408)"; fi eq sauvola-rc 0 sauvola-rc eq sauvola-on-vs-x86 192 sauvola-on # 4f. morphology with a 3x3 brick -- every count closed form (see the header). eq erode-on 196 erode-on eq dilate-on 381 dilate-on eq open-on 276 open-on eq close-on 277 close-on # Algebra, true of any correct implementation and of no particular one. eq open-idempotent 1 open-idempotent eq open-subset 0 open-subset # 4g. scaling. eq scale2-w 128 scale2-w eq scale2-h 128 scale2-h eq scale15-w 96 scale15-w eq scale15-h 96 scale15-h eq sample05-w 32 sample05-w eq sample05-h 32 sample05-h eq areamap05-w 32 areamap05-w eq areamap05-h 32 areamap05-h eq smooth025-w 16 smooth025-w eq smooth025-h 16 smooth025-h # Sampling and area-mapping have closed forms on this ramp and are asserted as # such: sampling takes column 2x, worth 8x, so a row sums to 8*(0+..+31) = 3968 # and the image to 32*3968 = 126976. Area mapping averages each pair, (8x + 8x+4) # / 2 = 8x + 2, so a row is 3968 + 64 = 4032 and the image 129024. eq sample05-sum 126976 sample05-sum eq areamap05-sum 129024 areamap05-sum # The linear-interpolation arm has no such form; these are the x86-64 values. eq scale2-sum-vs-x86 2080 scale2-sum eq scale15-sum-vs-x86 1167 scale15-sum # 4h. rotation. eq rot90-w 64 rot90-w eq rot90-h 64 rot90-h # A quarter turn maps source (0,63) to destination (0,0); block index there is # k = 0 + 4*3 = 12, so (r,g,b) = (204, 51, 0). Closed form, not measured. eq rot90-p00-r 204 rot90-p00-r eq rot90-p00-g 51 rot90-p00-g eq rot90-p00-b 0 rot90-p00-b eq rot180-twice-same 1 rot180-twice-same eq rot90-x4-same 1 rot90-x4-same eq rotam-w 64 rotam-w eq rotam-h 64 rotam-h # Rotation is about the centre, so the centre pixel is the one interpolation # cannot move: column 32 of the ramp is 4*32 = 128. eq rotam-centre 128 rotam-centre eq rotshear-w 64 rotshear-w eq rotshear-h 64 rotshear-h eq rotshear-on-vs-x86 272 rotshear-on # 4i. skew. The bars go in level and come out tilted by exactly 2 degrees, so # there is a right answer no previous run of this code supplied. leptonica # reports the angle that would UNDO the skew, and its sweep step bounds how close # it can get, so the assertion is a band around -2 rather than a value. eq skew-fixture ok skew-fixture eq skew-rc 0 skew-rc ang=$(v skew-angle) case "$ang" in -1.[6-9]*|-2.[0-3]*) bst_ok leptonica/skew-angle-band ;; *) bst_notok leptonica/skew-angle-band "angle=$ang, want about -2 (x86-64 measured -2.0938)" ;; esac # Below 3.0 confidence pixDeskewGeneral declines to rotate at all (skew.c:123), # so the fixture's geometry was chosen to clear that floor with room to spare -- # and this case is what says it still does. conf=$(v skew-conf) conf_i=$(printf '%s' "$conf" | cut -d. -f1) if [ -n "$conf_i" ] && [ "$conf_i" -ge 5 ] 2>/dev/null; then bst_ok leptonica/skew-conf-floor else bst_notok leptonica/skew-conf-floor "conf=$conf, want >= 5 (x86-64 measured 12.5011)"; fi # A page with no skew must read as zero, which is the control the measurement # needs, and the deskewed page must read the same way -- that is the whole claim. for pair in "flat:skew-flat-angle" "residual:deskew-residual"; do id=${pair%%:*}; key=${pair#*:} a=$(v "$key") case "$a" in 0.0*|-0.0*|0.1*|-0.1*|0.2*|-0.2*) bst_ok "leptonica/skew-$id" ;; *) bst_notok "leptonica/skew-$id" "$key=$a, want within 0.3 of zero" ;; esac done # 4j. connected components -- closed form, boxes and all. eq cc8-count 3 cc8-count eq cc4-count 3 cc4-count check_eq leptonica/cc8-box0 "58,4,1,1" "$(v cc8-box0)" check_eq leptonica/cc8-box1 "10,8,20,12" "$(v cc8-box1)" check_eq leptonica/cc8-box2 "44,40,6,6" "$(v cc8-box2)" # A 3x3 opening removes the isolated pixel and nothing else, so the count drops # by exactly one -- a relation between two operations rather than a fact about # either, and the one case here that a consistently wrong morphology still fails. eq cc8-after-open 2 cc8-after-open rm -rf "$tmp"