Documentation
¶
Overview ¶
Package images decodes each picture a page draws, with go-pdfkit and with an implementation that is not ours, and says how far apart the two came out.
It exists because a page is a composition, and a composition hides its parts. Drawing a whole page and comparing it answers "does this page look right", which is the question that matters to a reader — but when the answer is "mostly", it cannot say WHICH picture was wrong, and a picture that is wholly wrong can move a page's number by a few percent and pass. That happened: go-pdfkit/render v0.12.0 shipped drawing scanned pages dark because what was measured was that ink appeared and not that the right ink appeared.
So this compares picture against picture, and reports by the filter each was stored in, because "our CCITT is right and our JBIG2 is wrong" is the finding a per-page number cannot produce.
The comparison is per channel, with a magnitude gate ¶
difference reads every pixel on both sides, subtracts them CHANNEL BY CHANNEL, and counts a pixel as differing only when some channel differs by more than Gate levels of 255. Share is that count over the pixels; Peak is the largest single-channel difference anywhere in the picture; and MSE and Mean are the aggregate error, in levels squared and in levels. A picture AGREES when Peak is at most Gate, which — with the count budget at zero — is the same statement as Share being nought, so the share is a report and never a budget. That is pdfium's shape (testing/image_diff.cpp:287-288, where a percentage is computed and then required to be zero) and the field's; the survey is in the repository README.
What was here before, and why it went ¶
Until conformance#16 the per-pixel predicate was a BISECTION: a pixel was ink when its alpha was at least 128 and its luminance below 128, and Share was the fraction of pixels whose ink classification differed. That is exact for a bilevel picture and blind everywhere else. A decoder rendering every pixel of a scan at luminance 120 where poppler renders 20 scored 0.000 — perfect agreement — on an error of 100 levels at every pixel. A systematic level or chroma shift is the characteristic failure of a lossy decoder, so the instrument was blind in exactly the direction the codecs fail.
Every figure this package produced before that change was that narrower thing, so a record taken with it CANNOT be subtracted from a record taken with this one. baseline/README.md says which populations carry which.
Gate is 2, and the reason is ours rather than borrowed ¶
pdfium uses 3 (testing/utils/pixel_diff_util.h:11-12), but pdfium compares RENDERED PAGES, so its 3 buys slack for rasteriser and anti-aliasing differences. There is none to buy here: pdfimages EXTRACTS rather than renders, which is why this package asks it, so nothing sits between the codec and the pixels. What is left is codec rounding, and the standards bound that. ISO/IEC 10918-2 requires a conformant JPEG IDCT to be within one level per sample of the reference — read out of FFmpeg's implementation of that test, libavcodec/tests/dct.c:259:
spec_err = is_idct && (err_inf > 1 || omse > 0.02 || fabs(ome) > 0.0015);
Two conformant decoders sit either side of that reference, so 2 is what they may legitimately differ by. Cairo's 25 (test/buffer-diff.c:43-44) is the ceiling no per-case exception may ever cross, for the reason Cairo states: "otherwise some problems could be masked".
There is ONE gate and no per-case table, because there is no case yet. conformance#16 provides for raising Gate per population and per filter with a written reason; nothing has been measured that asks for it, and an exception mechanism with no exceptions in it is a promise rather than a measurement.
MSE and Mean are carried, and bounded by nothing ¶
pdfium bounds a mean squared error beside its per-channel delta (testing/image_diff.cpp:171-183, limit 0.05), and FFmpeg carries a SIGNED mean beside its MSE at dct.c:259. Both are recorded here, in the same units those projects use — levels squared and levels — so the published limits can be read against them directly.
Neither is a pass criterion here, and that is deliberate. MSE catches accumulated noise and Mean catches BIAS, which is the failure the bisection could not see at all; but no bound on either has been MEASURED for pictures that were extracted rather than rendered, and adopting pdfium's limit for a different operation would repeat exactly the mistake the withdrawn 1% tolerance was: a number carried onto an instrument that did not produce it. They are reported so that a bound can be chosen from evidence later.
Colour conversion is not codec error, so it is counted apart ¶
Our side is RGBA from render; theirs is a PNG that poppler wrote. A CMYK, an ICC or a Lab picture reaches those two forms through two different sets of colour arithmetic, and per channel that difference is LARGE and is not a decoder disagreeing. Widening Gate to absorb it would destroy the gate.
So each picture carries the colour space pdfimages reports for it (ImageOutputDev.cc:152-190 lists gray, rgb, cmyk, lab, icc, index, sep, devn, and "-" for a mask), and the pictures poppler had to CONVERT to reach RGB are tallied in their own bucket, with their own agreement figure and their own magnitudes — the way Remapped and Inverted are already counted apart. Only gray, rgb and "-" are treated as direct.
The listing is not enough on its own, and conformance#20 is why. poppler folds csCalGray onto "gray" and csCalRGB onto "rgb" (ImageOutputDev.cc:159-164), so a CalRGB picture is listed as "rgb" while the pixels it writes went through colorMap->getRGB (:451), which applies that space's gamma, matrix and chromatic adaptation. The direct bucket therefore admitted pictures poppler HAD converted, and measured every pixel of them against a colour conversion we did not make.
So the bucket is decided by BOTH sides. A picture is converted when the listing says so, and ALSO when its own /ColorSpace resolves to a CIE-based space — CalRGB, CalGray, ICCBased or Lab — whatever the listing says. Only the first two of those can move a picture, since poppler lists ICCBased as "icc" and Lab as "lab" and both were already converted; naming all four makes the rule a statement about the document rather than a patch over one judge's table.
Bucket.Calibrated counts how much of a bucket is CIE-tagged by its own dictionary. It is NOT the count of pictures this rule moved, and must not be read as one: most of a corpus's calibrated pictures are ICCBased or Indexed, which the listing already called converted. What the rule moves is the pictures the listing called gray or rgb, and that is a difference between two runs rather than a column in either.
index is still counted converted although its base space is often DeviceRGB, because pdfimages does not report the base and a picture that cannot be classified must not be counted as agreement.
A picture whose row could not be read at all also lands in the converted bucket, which makes a failure of pdfimages -list LOUD — every filter would read as wholly converted — rather than silently generous.
Alpha, and what a mask is compared in ¶
For an ordinary picture the compared channels are R, G and B. Alpha is not compared: pdfimages writes an opaque picture for anything that is not a mask, and writes a soft mask out as its own file, so a difference in alpha would be a difference in what the two tools chose to emit rather than in what the codecs produced.
A MASK is not comparable channel for channel, because the two sides do not put it in the same channels, and render does not put it in ONE place either. Both were read out of the buffers rather than assumed:
- a /ImageMask true STENCIL carries no colour of its own — it paints whatever colour is in force through its own shape — so render returns it black with the shape in the ALPHA channel. us-opm/SF2801PR.pdf's Im0 is one: 325x240, every RGB nought, exactly two alpha values.
- a /SMask is eight-bit greyscale, so render returns it OPAQUE with its levels in RGB. us-opm/sf2822.pdf's Im0/SMask is one: 116x73, alpha 255 at every one of its 8468 pixels.
poppler writes both as grey, with black where the mask paints. So a mask is compared in ONE derived channel, INK COVERAGE, by the same formula on both sides: alpha times 255 minus the luminance, over 255. On a stencil the luminance is nought so it reduces to the alpha; on a soft mask the alpha is 255 so it reduces to the inverted luminance; on poppler's side the alpha is always 255 and it is the inverted luminance. One formula, correct for both layouts, and it is what the old bisection was doing per bit.
Every poppler invocation is bounded, and a hang is named ¶
pdfimages can hang, on a document this corpus holds; the reason and the bound are in internal/poppler. All three of this package's invocations go through it, and a document that exceeds the bound is RECORDED BY NAME with the tool that hung — Missing.Hung, Result.Tool, and Summary.Hung in the record. It is never dropped and never retried. A named timeout is data about the corpus; a silent one is a gap a reader cannot tell from a bad score, which is the same distinction this package already makes between Ours, Neither and Theirs.
Polarity stays its own signal ¶
pdfimages writes a stencil with the opposite polarity to the samples it holds. Under a magnitude measure that reads as maximal error at every pixel, which would lose a distinction worth keeping, so the complement is tested separately and in the same pass: Inverted says the direct comparison failed the gate and the complemented one passed it. A picture that passes directly is never called inverted, so a uniform mid-grey — whose complement is within the gate of itself — is reported as agreeing and not as a complement.
Index ¶
Constants ¶
const ( PairedByObject = "object" PairedBySize = "size" )
How a picture was paired with the judge's.
const Gate = 2
Gate is D from conformance#16: a pixel counts as differing only when some compared channel differs by more than this many levels of 255.
It is 2 because ISO/IEC 10918-2 allows a conformant JPEG IDCT one level per sample either side of the reference, so two conformant decoders may differ by two. See the package comment for the derivation, and for why it is not pdfium's 3.
Variables ¶
This section is empty.
Functions ¶
func Report ¶
Report writes a tally in the order that reads best: worst first, because a filter that is right everywhere is not the one to read about.
Each filter takes a line for what could not be compared and a line for each bucket that holds anything, because the two buckets are two different questions and a reader who adds them together has exactly the number the split exists to prevent.
Types ¶
type Bucket ¶
type Bucket struct {
// Pictures is how many were judged in this bucket.
Pictures int
// Exact is how many had no channel differ by more than Gate. It is the
// criterion.
Exact int
// Identical is how many of those had no channel differ AT ALL, so a
// reader can see how much of the agreement is bit equality and how much
// the gate bought. One gate applies to every filter, which is a loosening
// for the lossless ones — they could defensibly be held to nought — and
// this is the count that says whether the loosening bought anything.
Identical int
// Inverted is how many agreed with the judge's complement instead.
Inverted int
// Calibrated is how many of this bucket's pictures name a CIE-based space
// in their own dictionary.
//
// It is nought in the direct bucket by construction, since a calibrated
// picture is converted whatever the listing said. In the converted bucket
// it is how much of that bucket the document itself accounts for — and it
// is NOT what conformance#20's rule moved, which is a much smaller number:
// most calibrated pictures are ICCBased or Indexed, which the listing
// already called converted. What moved is what the listing called gray or
// rgb, and that is the difference between two runs rather than a column
// in either.
Calibrated int
// Diffs holds the magnitude of each picture that differed.
Diffs []Difference
}
A Bucket is what one group of a filter's comparable pictures came to.
There are two, because a picture poppler had to convert to reach RGB is not evidence about a codec and must not be averaged with one that needed no conversion; see the package comment.
type BucketCounts ¶
type BucketCounts struct {
Pictures int `json:"pictures"`
Exact int `json:"exact"`
// Identical is how many of the exact ones differed by nothing at all, so
// bit equality can be told from agreement the gate bought.
Identical int `json:"identical"`
Inverted int `json:"inverted"`
Differing int `json:"differing"`
// Calibrated is how many of these pictures name a CIE-based /ColorSpace in
// their own dictionary. Nought in the direct bucket by construction, and
// not the count of what conformance#20's rule moved; see Bucket.Calibrated.
Calibrated int `json:"calibrated,omitempty"`
// Terms is the spread of each magnitude over the differing pictures,
// absent when none differed. A pointer because 0 is a real answer here —
// a bucket whose worst peak is nought is not the same as one with nothing
// to disagree about — and omitempty cannot tell those apart.
Terms *Terms `json:"terms,omitempty"`
}
BucketCounts is one bucket of one filter, as data.
type Counts ¶
type Counts struct {
// Pictures is how many were judged.
Pictures int
// SizePaired is how many were matched to the judge's picture by SIZE
// rather than by object number.
//
// PairedBy's own note says a run whose size share is large is a run whose
// numbers are worth less, "and that has to be visible". It was not: the
// field was recorded per picture and no report or record carried it, so
// the share could not be read from a run at all. conformance#13 found 144
// of 173 apparent inversions were this fallback pairing pictures that had
// nothing to do with each other, which is what makes the share worth
// printing beside the counts it qualifies rather than kept for whoever
// thinks to ask.
SizePaired int
// Unmatched is how many the other implementation had no picture for.
Unmatched int
// Remapped is how many carried a /Decode array, which we apply and
// pdfimages does not, so the two sides were not asked the same question.
// They are counted here rather than as disagreements: on a one-bit mask a
// /Decode of [1 0] inverts every pixel, which reads as total disagreement
// and is none.
Remapped int
// RawBits is how many the judge wrote as samples rather than as colour,
// which is every one-bit picture whose space is an index or a tint. They
// are counted here for the same reason as Remapped: the two sides were
// not asked the same question. 1070 pictures in 17 of the 2598 forms.
RawBits int
// Direct is the pictures whose colour space needed no conversion, which
// are the ones the agreement figure is computed over.
Direct Bucket
// Converted is the rest, tallied with their own magnitudes so that the
// claim "this is colour arithmetic and not a decoder" can be checked
// rather than assumed.
Converted Bucket
}
Counts is what a population came to, per filter.
type Difference ¶
type Difference struct {
// more than Gate, or -1 when the picture could not be judged. It is a
// REPORT and not a budget: the count budget is zero, so Share being nought
// and Peak being within Gate are the same statement.
Share float64
// Peak is the largest absolute channel difference anywhere in the
// picture, in levels of 255. This is the criterion.
Peak int
// MSE is the mean squared error over every compared channel, in levels
// squared — pdfium's units at testing/image_diff.cpp:181 and FFmpeg's
// omse at dct.c:256.
MSE float64
// Mean is the signed mean error over every compared channel, ours minus
// theirs, in levels — FFmpeg's ome. It is the term that catches BIAS,
// which is what the bisection this replaced could not see.
Mean float64
// Inverted says the direct comparison failed the gate and the picture
// against the judge's COMPLEMENT passes it.
//
// pdfimages writes a STENCIL with the opposite polarity to the samples it
// holds: an image with /ImageMask true, or a stream a picture names as its
// /Mask. That is a convention, not a disagreement — and it is worth
// telling apart from a real one, because "identical up to inversion" and
// "wrong" look the same in a magnitude and only one of them needs looking
// into.
//
// Which side is right was measured rather than assumed. On a 644-byte
// document whose 8x8 mask has its left half masked out and its right half
// painted, poppler's own pdftoppm paints the right half, which is what
// PDF 32000-1 8.9.6.2 says, pdfimages writes the left one, and ours
// agrees with the rendering.
//
// It is about being a stencil and nothing else. Not about JBIG2: 27 of
// the 28 in us-opm are CCITT, one is Flate, and the minimal document has
// no filter. Not about soft masks, which are the one kind pdfimages
// leaves alone.
//
// Not every complement this reports is that convention. When a page draws
// many uniform pictures of one size, match pairs them by size and order
// and pairs a black one with a white one; see
// https://github.com/go-pdfkit/conformance/issues/13.
Inverted bool
}
A Difference is one picture compared with the judge's, per channel.
Every term is over the CHANNELS the package comment names: three for an ordinary picture, one derived coverage channel for a stencil.
type FilterCounts ¶
type FilterCounts struct {
Filter string `json:"filter"`
Pictures int `json:"pictures"`
// Unmatched and Remapped are the pictures no comparison was made of.
Unmatched int `json:"unmatched"`
Remapped int `json:"remapped"`
// SizePaired is how many were paired by size rather than by object. A
// large share here devalues every figure on the line beside it.
SizePaired int `json:"sizePaired,omitempty"`
// RawBits is the pictures the judge wrote as samples rather than as
// colour, which is a third way of not being asked the same question.
RawBits int `json:"rawBits,omitempty"`
// Direct and Converted are the two buckets, absent when empty. The
// agreement figure is Direct's and never the two added together.
Direct *BucketCounts `json:"direct,omitempty"`
Converted *BucketCounts `json:"converted,omitempty"`
}
FilterCounts is one filter's line of a report, as data.
type Missing ¶
type Missing string
A Missing says which side produced nothing.
const ( // Judged means there was a picture on both sides and they were compared. Judged Missing = "" // Ours means ours produced no picture for a page the judge did produce // pictures for. That is the only one of these that can be a defect. // // It is NOT "would not draw the page", which this said for months. The // two are different questions and the corpus has an instance of the // difference: bulletinno38tasm.pdf is counted here, and render.Page draws // it. What refuses it is render.Images, under a per-page pixel budget that // the page renderer does not share -- afford() is reached from images.go // alone. See baseline/README.md §31 and go-pdfkit/render#100. // // So a document in this column is worth reading and is not by itself a // document the field can read and we cannot. Asking the renderer is a // separate step, and this harness never takes it: it only calls Images. Ours Missing = "ours" // Neither means no implementation produced a picture — ours refused, and // so did the judge, asked separately about the same file, or for a page, // about the same page. A judge that answers and takes nothing out of a // page ours refused is here too: judgeShots reports that as an error, and // the two are not told apart. // // This has to be told apart from Ours or the count misleads in the // direction of comfort in one direction and panic in the other. Seven of // the twelve documents of the ia-texts population are refused by ours; // all seven carry Adobe's proprietary EBX_HANDLER and poppler refuses // every one of them too. Folded together with a real refusal, that reads // as a decoder failing on 58% of a population. Counted apart, it says // what is true: that population is five documents, not twelve. Neither Missing = "neither" // Theirs means the judge took no picture out of a page ours drew pictures // for. That is not a disagreement and not a fault of ours: it is a page // this cannot get an answer about, and one that must be counted so that a // corpus getting harder is not read as a decoder getting worse. Theirs Missing = "theirs" // Unseen means the judge took a picture out whose OBJECT ours produced // nothing for. It is the mirror of Unmatched and it had no name, because // the pairing walks OUR pictures and appends one result for each: a row // of the judge's that nothing claimed left no trace in any count, so the // one direction this harness could not see was the direction that matters // most -- a picture the field gets out of a file and we do not. // // Measured over fr-cerfa's 450 forms, it is 4 153 rows and two classes, // in proportions worth knowing before anyone reads the number as a gap: // // 4 147 object 0 -- INLINE IMAGES, which render.Images does not // return at all. 2 614 of them are stencils, 2 592 of them // 16x16, and ONE document holds 2 592. go-pdfkit/render#101. // 25 pictures only an ANNOTATION reaches, counted over the whole // population: a push button's icon (/MK /I) or a widget's // appearance (/AP /N), which Images cannot see because it // walks the content stream. cerfa_10011.pdf is one. // // Neither is a decoder gap: render.Page draws annotations and draws // inline images. Both are the extraction API's scope being narrower than // the renderer's. See baseline/README.md §32, which also records that // the FIRST cause offered here was the small one, fitted to a sample // where both sides were almost always nought. Unseen Missing = "unseen" // Repeated means the judge listed a row for a picture ours produced // ONCE, under an object we did compare -- the same stream drawn again. // // It has to be apart from Unseen or the number is worthless. pdfimages // lists one row PER DRAW: page 1 of cerfa_10074.pdf gives it 729 rows // over 214 distinct objects, object 147 sixty-two times, against the 425 // pictures Images returns. Counting those 304 leftovers as pictures the // field gets and we do not would be false, and large enough to drown the // six real ones in the same sixty documents. // // Ours is one entry per object on purpose: decoding the same stream a // second time says nothing about a codec. The asymmetry is therefore // expected, and it is counted so that 729 against 425 has somewhere to // show. The package comment's claim that pdfimages gives "one" for a // repeated draw was never measured and is wrong; §32 measures it. Repeated Missing = "repeated" // Hung means a poppler tool did not finish within Timeout. // // It is its own value rather than a kind of Theirs because a judge that // would not answer and a judge that answered "nothing" are different // facts, and because this one has to be recorded BY NAME: the whole point // of bounding the call is that the document can be gone and looked at. // Result.Tool says which tool it was and Summary.Hung carries the list. Hung Missing = "hung" )
type Options ¶
type Options struct {
// Pages is how many pages of each document to take the pictures of; 0
// means the first only.
Pages int
}
Options say how much to look at.
type Result ¶
type Result struct {
Path string
Page int
// Name is the resource name the page draws it by.
Name string
// Filter is the image format it was stored in, empty for plain samples.
Filter string
// Stencil says render returned this as a MASK rather than as a picture —
// a /ImageMask true stencil, or a stream some picture named as its /SMask
// or /Mask. It decides what channel the two sides are compared in.
Stencil bool
// RawBits says pdfimages wrote this picture's SAMPLES rather than its
// colour. ImageOutputDev.cc:642 picks PNGWriter::MONOCHROME whenever the
// colour map has one component and one bit, and then writes
// `str->getChar() ^ invert_bits` -- the bits themselves, with the colour
// space never consulted. For a one-bit DeviceGray that is the same thing:
// the sample IS the level. For an index or a tint it is not, and
// cerfa_10074.pdf is the case that found it -- an Indexed palette of
// 808080 and ffffff, drawn by us as the mid grey it says and written by
// the judge as a bit.
RawBits bool
// Decoded says a /Decode array shaped our samples. pdfimages writes the
// samples as stored, so this picture and theirs are not the same question
// and are counted apart rather than as a disagreement.
Decoded bool
// PairedBy says how this picture was matched with the judge's: "object"
// when both sides named the same object number, "size" when it fell back
// to the first unclaimed picture of the same size. Empty when nothing was
// paired. A run whose size share is large is a run whose numbers are worth
// less, and that has to be visible.
PairedBy string
// Space is the colour space pdfimages reports for the judge's picture:
// gray, rgb, cmyk, lab, icc, index, sep, devn, or "-" for a mask. It is
// empty when no row could be read for it.
Space string
// Converted says poppler had to convert that space to reach RGB, so a
// per-channel difference on this picture is colour arithmetic and not
// codec error. Those are tallied apart; see the package comment.
Converted bool
// Calibrated says the picture's OWN dictionary names a CIE-based colour
// space. It is what makes Converted true for a picture the listing calls
// gray or rgb, which poppler does for CalGray and CalRGB although it
// converts them (conformance#20). It is kept beside Converted rather than
// folded into it so a bucket can say how much of itself the document
// accounts for, which is not the same as how much the rule moved.
Calibrated bool
// Size is what the picture came out as.
W, H int
// Difference is how far apart the two came out, or Share -1 when there
// was no comparison, which Note explains.
Difference
// Missing says which side had nothing, when nothing could be compared.
// It is a field rather than a reading of Note because a baseline has to
// count these apart: a run that agrees less because ours stopped opening
// documents and a run that agrees less because it decoded them wrongly
// are different events, and a total that lumps them says neither.
Missing Missing
// Tool is the poppler tool that did not finish, when Missing is Hung. It
// is empty otherwise. A hang is only useful as data if it says which of
// the three invocations it was, because they fail independently.
Tool string
Note string
}
A Result is one picture judged.
type Spread ¶
type Spread struct {
Median float64 `json:"median"`
// Worst is the value furthest from zero, WITH ITS SIGN, so that a signed
// mean says which way the bias ran and not only how big it was.
Worst float64 `json:"worst"`
}
A Spread is the middle and the far end of one term.
type Summary ¶
type Summary struct {
Population string `json:"population"`
// Documents is how many were looked at, which is not how many were
// judged: a document that draws no pictures contributes none.
Documents int `json:"documents"`
// Refused is how many documents ours would not open or draw, and the
// judge would. This is the count that is a defect.
Refused int `json:"refused"`
// Unopenable is how many neither would open. A corpus is full of these
// and they say nothing about a decoder, but a population's real size is
// its documents minus these and a rate quoted without them is quoted over
// the wrong denominator.
Unopenable int `json:"unopenable"`
// Declined is how many pages ours drew pictures for and the judge took
// none out of, so there was nothing to compare them with.
Declined int `json:"declined"`
// Unseen is how many pictures the judge took out, under an object ours
// produced nothing for -- the direction the pairing could not see at all
// until it was counted. omitempty, so records written before it keep
// their shape.
Unseen int `json:"unseen,omitempty"`
// Repeated is how many of the judge's rows name an object we compared
// once. It is the scale of a difference in UNIT, not in fidelity:
// pdfimages lists one row per draw and Images returns one entry per
// object.
Repeated int `json:"repeated,omitempty"`
// Hung names the documents a poppler tool would not finish on, with the
// tool. A document that was skipped because the judge hung is not a
// document that scored badly, and the two are indistinguishable in a
// total, so these are carried by name rather than as a count. Absent when
// nothing hung.
Hung []Hang `json:"hung,omitempty"`
Filters []FilterCounts `json:"filters"`
}
A Summary is one population's tally in a shape that keeps: named fields, a settled order, and no map. A baseline is only worth writing down if a later run can be diffed against it, and a Go map ordered by chance cannot be.