SeekquarrySeekquarry - Yioop Repo - Seekquarry

/ tests / PdfProcessorTest.php⤓
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\tests;

use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\CrawlConstants;
use seekquarry\yioop\library\processors\JpgProcessor;
use seekquarry\yioop\library\processors\PdfProcessor;
use seekquarry\yioop\library\CrawlConstants as CC;
use seekquarry\yioop\library\UnitTest;

/**
 * UnitTest for the PdfProcessor class. A PdfProcessor is used to process
 * a .pdf file and extract summary from it. This
 * class tests the processing of an .pdf file.
 *
 * @author Chris Pollett
 */
class PdfProcessorTest extends UnitTest implements CrawlConstants
{
    /**
     * Creates a new PdfProcessor object using the test.pdf
     * file made from the seekquarry landing page
     */
    public function setUp()
    {
    }
    /**
     * Delete any files associated with our test on PdfProcessor (in this case
     * none)
     */
    public function tearDown()
    {
    }
    /**
     * Test case to check whether words known to be in the PDF were extracted
     * is retrieved correctly.
     */
    public function wordExtractionTestCase()
    {
        $pdf_object = new PdfProcessor();
        $url = "http://www.yioop.com/test.pdf";
        $filename = C\PARENT_DIR . "/tests/test_files/test.pdf";
        $page = file_get_contents($filename);
        $summary = $pdf_object->process($page, $url);
        $words = explode(" ", $summary[self::DESCRIPTION]);
        /* The page's menu word Documentation left the description once the
           second page's text was read too; crawling is from that page. */
        $this->assertTrue(in_array("crawling", $words),
            "Word Extraction 1");
        $this->assertTrue(in_array("Yioop", $words),
            "Word Extraction 2");
        $this->assertTrue(in_array("Open", $words),
            "Word Extraction 3");
    }
    /**
     * faxCodedPictureIsWrittenAsShadesTestCase writes a picture packed by
     * fax coding, as a page scanned in black and white often is, to a
     * file tesseract can read. The picture is 16 points across and 2 down;
     * its coding gives each row 8 points of white then 8 of black. Read a
     * byte at a time, as before, its bits made a blank picture, and the
     * words of such a page were never read.
     */
    public function faxCodedPictureIsWrittenAsShadesTestCase()
    {
        $said = "<< /Type /XObject /Subtype /Image /Width 16 /Height 2 " .
            "/BitsPerComponent 1 /Filter /CCITTFaxDecode " .
            "/DecodeParms << /K -1 /Columns 16 >> >>";
        $object = $said . "\nstream\n" . hex2bin("3317001001") .
            "\nendstream";
        $path = PdfProcessor::pictureForReading($said, $object,
            C\TEST_DIR . "/test_files/fax_test");
        $written = ($path === "") ? "" : file_get_contents($path);
        @unlink($path);
        $row = str_repeat("\xff", 8) . str_repeat("\x00", 8);
        $this->assertEqual($written, "P5\n16 2\n255\n" . $row . $row,
            "each row is 8 white points then 8 black ones");
    }
    /**
     * pictureInInksIsWrittenAsGrayTestCase writes a picture kept in the
     * four inks of a press, two points across: one with no ink, one with
     * full black ink. Such pictures were not written for tesseract at
     * all; now the first point comes out white and the second black.
     */
    public function pictureInInksIsWrittenAsGrayTestCase()
    {
        $said = "<< /Type /XObject /Subtype /Image /Width 2 /Height 1 " .
            "/ColorSpace /DeviceCMYK /BitsPerComponent 8 >>";
        $object = $said . "\nstream\n\x00\x00\x00\x00\x00\x00\x00\xff" .
            "\nendstream";
        $path = PdfProcessor::pictureForReading($said, $object,
            C\TEST_DIR . "/test_files/inks_test");
        $written = ($path === "") ? "" : file_get_contents($path);
        @unlink($path);
        $this->assertEqual($written, "P5\n2 1\n255\n\xff\x00",
            "no ink is white and full black ink is black");
    }
    /**
     * Where a case that writes files puts them.
     * @var string
     */
    public $where = "";
    /**
     * The jpeg reader written here reads an ordinary jpeg the same as the
     * one PHP comes with, which is known right. That is what says the
     * reading is sound; the four-color case then differs from it only in
     * how the values are turned into colors.
     */
    public function readsAnOrdinaryJpegTheSameWayTestCase()
    {
        $made = imagecreatetruecolor(64, 48);
        for ($down = 0; $down < 48; $down++) {
            for ($across = 0; $across < 64; $across++) {
                imagesetpixel($made, $across, $down,
                    imagecolorallocate($made, $across * 4, $down * 5, 128));
            }
        }
        ob_start();
        imagejpeg($made, null, 100);
        $jpeg = ob_get_clean();
        $read = JpgProcessor::readJpeg($jpeg);
        $this->assertTrue($read !== false, "the jpeg is read");
        $this->assertEqual($read["width"], 64, "at the right width");
        $this->assertEqual(count($read["planes"]), 3, "with three colors");
        $theirs = imagecreatefromstring($jpeg);
        $worst = 0;
        for ($down = 0; $down < 48; $down += 3) {
            for ($across = 0; $across < 64; $across += 3) {
                $said = imagecolorat($theirs, $across, $down);
                $at = $down * 64 + $across;
                $light = $read["planes"][0][$at];
                $blueness = $read["planes"][1][$at] - 128;
                $redness = $read["planes"][2][$at] - 128;
                $mine = [max(0, min(255,
                    (int)round($light + 1.402 * $redness))),
                    max(0, min(255, (int)round($light -
                    0.344136 * $blueness - 0.714136 * $redness))),
                    max(0, min(255,
                    (int)round($light + 1.772 * $blueness)))];
                $theirs_now = [($said >> 16) & 255, ($said >> 8) & 255,
                    $said & 255];
                for ($which = 0; $which < 3; $which++) {
                    $worst = max($worst,
                        abs($mine[$which] - $theirs_now[$which]));
                }
            }
        }
        $this->assertTrue($worst <= 3,
            "every color within rounding of what PHP's reader gives, " .
            "worst was $worst");
    }
    /**
     * makePicture draws a plain jpeg of the width and height asked for,
     * for a case that needs a picture whose size it knows.
     *
     * @param int $width how wide
     * @param int $height how tall
     * @return string the picture's bytes as a jpeg
     */
    public function makePicture($width, $height)
    {
        $made = imagecreatetruecolor($width, $height);
        imagefill($made, 0, 0, imagecolorallocate($made, 30, 90, 200));
        ob_start();
        imagejpeg($made);
        $bytes = ob_get_clean();
        return $bytes;
    }
    /**
     * The first picture kept whole inside a portable document is found,
     * which is what a scanned page is, and a document holding none says
     * so rather than handing back something that is not a picture.
     */
    public function pictureInsideADocumentIsFoundTestCase()
    {
        $picture = $this->makePicture(300, 200);
        $document = "%PDF-1.4\n5 0 obj<</Filter/DCTDecode>>stream\n" .
            $picture . "\nendstream endobj";
        $found = PdfProcessor::pictureInDocument($document);
        $this->assertTrue($found !== false, "the picture is found");
        $this->assertEqual(substr($found, 0, 2), "\xFF\xD8",
            "and it is the picture rather than the words around it");
        $this->assertTrue(PdfProcessor::pictureInDocument(
            "%PDF-1.4\n5 0 obj<</Filter/FlateDecode>>stream\nxx\nendstream"
            ) === false, "a document with no such picture says so");
    }
    /**
     * nameIsHandedBackWithTheWordThatUsesItTestCase checks that a name
     * a page writes comes back from the walk with its slash, so the word
     * that follows it knows which thing it works on. The name used to be
     * read back out of the characters before that word, which failed
     * wherever the word sat near the start of a drawing: a document of
     * two pages that placed its cover thirty five characters in lost the
     * cover.
     */
    public function nameIsHandedBackWithTheWordThatUsesItTestCase()
    {
        $drawing = "q 576 0 0 783 0 0 cm /X3 Do Q";
        $at = 0;
        $seen = [];
        while ($at < strlen($drawing)) {
            $token = PdfProcessor::nextToken($drawing, $at);
            if ($token === false) {
                break;
            }
            if ($token !== "") {
                $seen[] = $token;
            }
        }
        $this->assertTrue(in_array("/X3", $seen),
            "the name of the picture is one of the words read");
        $where = array_search("/X3", $seen);
        $this->assertEqual("Do", $seen[$where + 1] ?? "",
            "and the word that places it follows the name");
        $at = 0;
        $token = PdfProcessor::nextToken("/X3 Do", $at);
        $this->assertEqual("/X3", $token,
            "a name at the very start of a drawing is read as well");
    }
    /**
     * pointGoesThroughTheFrameInForceTestCase checks that a point of a
     * page's own measure lands where the frame in force puts it. A page
     * draws inside frames that may be moved, scaled, turned and leaned,
     * and keeping only a scale and an offset put a turned drawing in the
     * wrong place.
     */
    public function pointGoesThroughTheFrameInForceTestCase()
    {
        $plain = PdfProcessor::plainFrame();
        $where = PdfProcessor::throughFrame($plain, 7, 11);
        $this->assertEqual(7, $where[0],
            "a plain frame leaves a point where it is across");
        $this->assertEqual(11, $where[1],
            "a plain frame leaves it where it is up the page");
        $turned = ["a" => 0.0, "b" => 1.0, "c" => -1.0, "d" => 0.0,
            "e" => 0.0, "f" => 0.0];
        $where = PdfProcessor::throughFrame($turned, 3, 0);
        $this->assertEqual(0, round($where[0]),
            "a quarter turn takes it off the across line");
        $this->assertEqual(3, round($where[1]),
            "and puts it that far up the page instead");
    }
    /**
     * frameInsideAnotherStandsOnItTestCase checks that opening a frame
     * inside another gives the frame that does the work of both, so what
     * is drawn in the innermost is placed by all of them together.
     */
    public function frameInsideAnotherStandsOnItTestCase()
    {
        $outer = ["a" => 2.0, "b" => 0.0, "c" => 0.0, "d" => 2.0,
            "e" => 10.0, "f" => 20.0];
        $inner = ["a" => 1.0, "b" => 0.0, "c" => 0.0, "d" => 1.0,
            "e" => 5.0, "f" => 0.0];
        $both = PdfProcessor::frameWithin($inner, $outer);
        $where = PdfProcessor::throughFrame($both, 0, 0);
        $this->assertEqual(20, round($where[0]),
            "the inner move is scaled by the outer frame");
        $this->assertEqual(20, round($where[1]),
            "and the outer move still stands");
    }
    /**
     * lettersWrittenAsHexDigitsAreReadTestCase checks that letters a
     * page writes as pairs of hex digits come back as those letters. A
     * page setting a headline in a font that numbers its own letters
     * writes them that way, and reading only the ones between brackets
     * left such a headline undrawn.
     */
    public function lettersWrittenAsHexDigitsAreReadTestCase()
    {
        $this->assertEqual("AB", PdfProcessor::lettersFromHex("4142"),
            "two pairs of digits are two letters");
        $this->assertEqual("AB", PdfProcessor::lettersFromHex("41 42"),
            "spaces between the pairs are passed over");
        $this->assertEqual(3, strlen(PdfProcessor::lettersFromHex(
            "41424")), "an odd digit is filled out rather than dropped");
    }
    /**
     * tableOfDifferencesIsReadTestCase checks that the table a page
     * carries beside a font, saying which number stands for which
     * letter, is read. A font made for one document numbers its letters
     * itself, and without the table a headline comes back as control
     * characters.
     */
    public function tableOfDifferencesIsReadTestCase()
    {
        $entry = "<< /Type /Font /Encoding 9 0 R >>";
        $document = "%PDF-1.4\n9 0 obj\n<< /BaseEncoding " .
            "/WinAnsiEncoding /Differences [ 14 /four /one /hyphen " .
            "30 /O /M ] /Type /Encoding >>\nendobj\n";
        $names = PdfProcessor::namesByCode($document, [], $entry);
        $this->assertEqual("four", $names[14] ?? "",
            "the first name goes with the number beside it");
        $this->assertEqual("hyphen", $names[16] ?? "",
            "the names after it follow one number at a time");
        $this->assertEqual("O", $names[30] ?? "",
            "a number in the middle starts the counting again");
    }
    /**
     * runThisCannotReadIsNotDrawnTestCase checks that a run of words
     * holding no letters a reader could recognize is turned away.
     * Drawing such a run laid a solid box in the headline's color over
     * the picture beneath it.
     */
    public function runThisCannotReadIsNotDrawnTestCase()
    {
        $this->assertTrue(PdfProcessor::wordsCanBeDrawn("TRUMP WAS"),
            "ordinary words are drawn");
        $this->assertTrue(!PdfProcessor::wordsCanBeDrawn("\x0c\x0c\x0c"),
            "a run of control characters is not");
        $this->assertTrue(!PdfProcessor::wordsCanBeDrawn(""),
            "and neither is a run with nothing in it");
    }
    /**
     * loopInsideAnotherIsTheMiddleOfALetterTestCase checks that the
     * inner loop of a letter such as an o is told from a letter of its
     * own. Filling both left the o solid where its middle should show
     * what lies beneath.
     */
    public function loopInsideAnotherIsTheMiddleOfALetterTestCase()
    {
        $outside = [0, 0, 100, 0, 100, 100, 0, 100];
        $middle = [30, 30, 70, 30, 70, 70, 30, 70];
        $beside = [200, 0, 300, 0, 300, 100, 200, 100];
        $this->assertTrue(PdfProcessor::loopWithinLoop($middle, $outside),
            "a loop within the bounds of another is its middle");
        $this->assertTrue(!PdfProcessor::loopWithinLoop($beside,
            $outside), "a loop beside another is a letter of its own");
        $this->assertTrue(!PdfProcessor::loopWithinLoop($outside,
            $middle), "and the larger of two is never the middle");
    }
    /**
     * tintOfOneInkIsDarkAtFullTestCase checks that a color said as one
     * number in a space of the page's own is read as ink laid down
     * rather than as a shade of gray. Read as gray, a barcode's bars
     * came out white on a white box.
     */
    public function tintOfOneInkIsDarkAtFullTestCase()
    {
        $full = PdfProcessor::inkFromTint([1]);
        $none = PdfProcessor::inkFromTint([0]);
        $this->assertEqual(0, $full[0], "the ink at full is dark");
        $this->assertEqual(255, $none[0], "none of it is white");
    }
    /**
     * onlyAPieceThatSaysSoIsWrittenInTestCase checks that a page's
     * drawing takes in the drawing of a piece it hands off to, and that
     * a picture whose settings happen to carry the word is left alone. A
     * picture read as a piece has its bytes spliced into the drawing and
     * is lost.
     */
    public function onlyAPieceThatSaysSoIsWrittenInTestCase()
    {
        $page = "<< /Resources << /XObject << /X2 2 0 R /X3 3 0 R >> " .
            ">> >>";
        $document = "%PDF-1.4\n" .
            "2 0 obj\n<< /Subtype /Form /Length 8 >>\nstream\n" .
            "1 0 0 rg\nendstream\nendobj\n" .
            "3 0 obj\n<< /Subtype /Image /Filter /DCTDecode " .
            "/Name /Form >>\nstream\nxxxx\nendstream\nendobj\n";
        $drawing = PdfProcessor::piecesWrittenIn($document, [], $page,
            "q /X2 Do Q q /X3 Do Q");
        $this->assertTrue(strpos($drawing, "1 0 0 rg") !== false,
            "the drawing of a piece is written in");
        $this->assertTrue(strpos($drawing, "/X3 Do") !== false,
            "a picture that only carries the word is left where it is");
    }
    /**
     * fontCarriesOverAndPropertyStringsStayOutTestCase
     * checks two things a magazine cover showed. A block that names no
     * font of its own writes in the one the block before it set, so its
     * words come back at the size they are drawn at; starting the block
     * over at the plain size multiplied by the text matrix made a
     * one-point font in a forty-one-fold frame read as four hundred
     * ninety-two, and two giant letters covered the thumbnail. And a
     * marked-content property list like /Lang (en-GB) is a note about
     * the page, not words on it, so nothing of it may ride into the
     * words.
     */
    public function fontCarriesOverAndPropertyStringsStayOutTestCase()
    {
        $drawing = "/P <</Lang (en-GB)/MCID 1 >>BDC\n" .
            "BT\n/T1_0 1 Tf\n41 0 0 41 194 402 Tm\n" .
            "[(Could )1 (A)7 (I)5 (s)]TJ\nET\nEMC\n" .
            "BT\n/P <</Lang (en-GB)/MCID 2 >>BDC\n" .
            "41 0 0 41 214 364 Tm\n[(bec)7 (ome)]TJ\nEMC\nET\n";
        $runs = PdfProcessor::wordsOnPage($drawing);
        $sizes = [];
        foreach ($runs as $run) {
            $this->assertTrue(
                strpos($run["words"], "en-GB") === false,
                "no property-list string rides into the words");
            $sizes[trim($run["words"])] = $run["size"];
        }
        $this->assertTrue(isset($sizes["become"]),
            "the block with no font of its own still yields its words");
        $this->assertEqual(41.0, $sizes["become"] ?? 0,
            "and they carry the size the block before set, " .
            "not the plain size multiplied by the frame");
    }
    /**
     * base85LetteringOpensBackIntoItsBytesTestCase checks the
     * base-85 lettering a stream may be written out as is turned back
     * into its bytes: the known five letters that carry the word Man
     * and a space, the lone z that stands for four zero bytes, and a
     * short last group carrying one byte fewer than its letters.
     */
    public function base85LetteringOpensBackIntoItsBytesTestCase()
    {
        $this->assertEqual("Man ",
            PdfProcessor::bytesFromBase85("9jqo^~>"),
            "five letters open into their four bytes");
        $this->assertEqual("\x00\x00\x00\x00",
            PdfProcessor::bytesFromBase85("z~>"),
            "a lone z stands for four zero bytes");
        $this->assertEqual("hi",
            PdfProcessor::bytesFromBase85("BP@~>"),
            "a short group gives one byte fewer than its letters");
    }
    /**
     * sortedCatalogAndBase85DrawingStillReadTestCase walks the two
     * faults a newspaper front page showed. Its catalog writes Pages
     * before Type, so reading only what follows the type missed the
     * pages reference and the page tree went unread; and its drawing
     * is written as base-85 lettering around the packed bytes, which
     * inflating as it stood read as damage and gave back nothing. The
     * page here is built the same two ways, and its one word has to
     * come back at its drawn size.
     */
    public function sortedCatalogAndBase85DrawingStillReadTestCase()
    {
        $lettering =
            "Garg^;:'MC<%q8W8Q.;t<%n;=+D#3d66WlH)Sr7q.>a=Y\$6`OZXs!f\$`X7h";
        $document = "%PDF-1.4\n" .
            "1 0 obj\n<< /Pages 2 0 R /Type /Catalog >>\nendobj\n" .
            "2 0 obj\n<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>\n" .
            "endobj\n" .
            "3 0 obj\n<< /Contents 4 0 R /MediaBox [ 0 0 612 792 ]" .
            " /Parent 2 0 R /Type /Page >>\nendobj\n" .
            "4 0 obj\n<< /Filter [ /ASCII85Decode /FlateDecode ]" .
            " /Length " . strlen($lettering) . " >>\nstream\n" .
            $lettering . "~>\nendstream\nendobj\n" .
            "trailer\n<< /Root 1 0 R >>\n%%EOF\n";
        $page = PdfProcessor::firstPage($document);
        $this->assertTrue($page !== false &&
            strpos($page["drawing"], "Sunday") !== false,
            "the sorted catalog is walked and the lettering opens " .
            "into the drawing");
        $runs = PdfProcessor::wordsOnPage($page["drawing"]);
        $found_size = 0;
        foreach ($runs as $run) {
            if (trim($run["words"]) === "Sunday") {
                $found_size = $run["size"];
            }
        }
        $this->assertEqual(41.0, $found_size,
            "and the page's one word comes back at its drawn size");
    }
    /**
     * A font that numbers its letters writes each as two bytes, and the
     * first byte of a low numbered letter is zero. A run of such words
     * keeps that zero byte, since cutting it read every letter one byte
     * out of step and left a cover's headline undrawn.
     */
    public function runKeepsTheZeroByteOfANumberedLetterTestCase()
    {
        $runs = PdfProcessor::wordsOnPage(
            "BT /F6 52 Tf 23 491 Td <001C0002> Tj ET");
        $this->assertEqual("001c0002", bin2hex($runs[0]["words"] ?? ""),
            "a run keeps the zero byte that begins a numbered letter");
    }
    /**
     * A color set inside a saved state ends when that state is restored,
     * so words drawn after the restore take the color from before it. A
     * cover set its masthead's blue inside such a state and its headline
     * in white after, and reading the blue on through drew the headline
     * blue.
     */
    public function restoredStateBringsBackItsColorTestCase()
    {
        $runs = PdfProcessor::wordsOnPage("0 0 0 0 k q .736 .432 0 .074 k " .
            "q Q Q BT /F6 52 Tf 23 491 Td <001C0002> Tj ET");
        $this->assertEqual([255, 255, 255], $runs[0]["ink"] ?? null,
            "words after a restore are in the color set before the save");
    }
    /**
     * A composite font lists how wide its letters are as a table of runs
     * and ranges, and both forms read into the face.
     */
    public function widthTableReadsRunsAndRangesTestCase()
    {
        $face = new \stdClass();
        $face->stated_widths = [];
        PdfProcessor::widthsFromLetterTable($face,
            " 1 [ 647 603 727 ] 10 12 500 ");
        $this->assertEqual([1 => 647.0, 2 => 603.0, 3 => 727.0,
            10 => 500.0, 11 => 500.0, 12 => 500.0], $face->stated_widths,
            "a run of widths and a range with one width both read");
    }
    /**
     * A page may set its letters tighter or looser than their own widths
     * with a spacing operator, and the run carries that spacing so the
     * letters are drawn as far apart as the page shows them.
     */
    public function runCarriesTheLetterSpacingSetTestCase()
    {
        $runs = PdfProcessor::wordsOnPage(
            "BT /F6 52 Tf 23 491 Td -13 Tc <001C0002> Tj ET");
        $this->assertEqual(-13.0, $runs[0]["spacing"] ?? null,
            "a run carries the letter spacing in force when it was set");
    }
    /**
     * A page may draw a form that keeps its own resources, and a picture
     * named only inside that form is still one of the page's pictures. A
     * cover that was one photograph inside one form came out blank when
     * only the page's own list was read.
     */
    public function pictureNamedInsideAFormIsFoundTestCase()
    {
        $document = "1 0 obj << /Type /Page /Resources << /XObject << " .
            "/Fm1 2 0 R >> >> >> endobj\n" .
            "2 0 obj << /Type /XObject /Subtype /Form /Resources << " .
            "/XObject << /X2 3 0 R >> >> /Length 0 >> stream\n" .
            "endstream endobj\n";
        $page = "<< /Type /Page /Resources << /XObject << /Fm1 2 0 R >> " .
            ">> >>";
        $listing = PdfProcessor::pageResourceEntries($document, [], $page,
            "XObject");
        $this->assertTrue(strpos($listing, "/X2 3 0 R") !== false,
            "a picture named inside a form the page draws is listed");
    }
    /**
     * spacedTitleIsReadTestCase checks that a title written with a space
     * before its parenthesis, as /Title (Annual Report) is in the sample,
     * is read, and that the sample's page text, written in base-85
     * lettering, is read too.
     */
    public function spacedTitleIsReadTestCase()
    {
        $processor = new PdfProcessor();
        $summary = $processor->process(file_get_contents(__DIR__ .
            "/test_files/spaced_title.pdf"),
            "https://example.com/spaced_title.pdf");
        $this->assertTrue($summary[CC::TITLE] == "Annual Report",
            "the title is read");
        $this->assertTrue(str_contains($summary[CC::DESCRIPTION],
            "Nitrate levels fell"), "the text of a page written in " .
            "base-85 lettering after a /Type /Pages object is read");
    }
    /**
     * embeddedFontIsNotReadAsTextTestCase checks that the bytes of an
     * embedded font program, marked by /Length1, are not read as words,
     * and that the text of both pages is read. The file's made-up font
     * program is text drawing "fontnoise" if it were read as a page.
     */
    public function embeddedFontIsNotReadAsTextTestCase()
    {
        $processor = new PdfProcessor();
        $summary = $processor->process(file_get_contents(__DIR__ .
            "/test_files/embedded_font.pdf"),
            "https://example.com/embedded_font.pdf");
        $this->assertTrue(str_contains($summary[CC::DESCRIPTION],
            "First page about rivers"), "the first page's text is read");
        $this->assertTrue(str_contains($summary[CC::DESCRIPTION],
            "Second page about glaciers"), "the second page's text is read");
        $this->assertFalse(str_contains($summary[CC::DESCRIPTION],
            "fontnoise"), "the font program is not read as words");
    }
    /**
     * subsetFontLettersAreReadTestCase checks that text shown in a font
     * that numbers its own letters is read through the font's ToUnicode
     * map, both its single codes and its ranges, one range listing each
     * letter and one counting up from a first. The sample shows Crème as
     * 01 02 03 04 05, мир as 06 07 08, and abc as 09 0A 0B.
     */
    public function subsetFontLettersAreReadTestCase()
    {
        $processor = new PdfProcessor();
        $summary = $processor->process(file_get_contents(__DIR__ .
            "/test_files/subset_font.pdf"),
            "https://example.com/subset_font.pdf");
        foreach (["Crème", "мир", "abc"] as $word) {
            $this->assertTrue(str_contains($summary[CC::DESCRIPTION], $word),
                "$word is read");
        }
    }
    /**
     * thinPictureIsNotReadTestCase checks that getText does not hand
     * tesseract a picture one pixel high, which is how a slide deck keeps
     * the strips of a shaded background. A deck from a crawl held 843 of
     * them, and reading them took 34 seconds and gave no words.
     */
    public function thinPictureIsNotReadTestCase()
    {
        $document = "%PDF-1.4\n";
        for ($i = 1; $i <= 3; $i++) {
            $document .= "$i 0 obj\n<< /Type /XObject /Subtype /Image " .
                "/Width 300 /Height 1 /ColorSpace /DeviceGray " .
                "/BitsPerComponent 8 /Length 300 >>\nstream\n" .
                str_repeat("\x80", 300) . "\nendstream\nendobj\n";
        }
        $before = L\ComputerVision::$ocr_time;
        PdfProcessor::getText($document, "https://example.com/thin.pdf");
        $this->assertEqual($before, L\ComputerVision::$ocr_time,
            "no time is spent reading pictures one pixel high");
    }
    /**
     * runAfterAChangeOfFontFollowsTestCase checks that words a page writes
     * on from where the pen stopped, after a change of font with no move
     * between, are marked as following the run before them, and that
     * words after a move are not. A cover set its title's S, I and G in
     * one font and its A in another, and the A was drawn over the S.
     */
    public function runAfterAChangeOfFontFollowsTestCase()
    {
        $runs = PdfProcessor::wordsOnPage("BT /T1_1 1 Tf " .
            "62 0 0 62 320 674 Tm (SIG)Tj /T1_2 1 Tf (A)Tj " .
            "2.079 0 Td (C)Tj ET");
        $follows = [];
        foreach ($runs as $run) {
            $follows[$run["words"]] = $run["follows"] ?? null;
        }
        $this->assertEqual(["SIG" => false, "A" => true, "C" => false],
            $follows, "only the run written on from the pen follows");
    }
    /**
     * colorSetInABlockCarriesToTheNextTestCase checks that a color set
     * inside one block of text is still in force in the next. A table of
     * contents set an orange page number, then black inside the same
     * block for its title, and the title's next block came out orange.
     */
    public function colorSetInABlockCarriesToTheNextTestCase()
    {
        $runs = PdfProcessor::wordsOnPage("0 0.64 0.95 0 k " .
            "BT /F1 10 Tf 1 0 0 1 10 10 Tm (1)Tj 0 0 0 1 k (Title)Tj ET " .
            "BT 1 0 0 1 50 10 Tm (rest)Tj ET");
        $inks = [];
        foreach ($runs as $run) {
            $inks[$run["words"]] = $run["ink"];
        }
        $this->assertEqual([255, 92, 13], $inks["1"] ?? null,
            "the page number is orange");
        $this->assertEqual([0, 0, 0], $inks["rest"] ?? null,
            "the next block keeps the black set before it");
    }
    /**
     * letterCodedAsALineEndIsKeptTestCase checks that a run whose letter
     * is coded as a carriage return is kept. A font that numbers its own
     * letters used that code for t, and cutting it as blank space left a
     * gap wherever a t stood alone between two moves.
     */
    public function letterCodedAsALineEndIsKeptTestCase()
    {
        $runs = PdfProcessor::wordsOnPage("BT /T1_0 10 Tf 10 10 Td " .
            "(\\r)Tj ET");
        $this->assertEqual("\r", $runs[0]["words"] ?? "",
            "the letter coded as a carriage return is kept");
    }
    /**
     * pageWithNoPicturesIsDrawnFromItsShapesTestCase checks that a page
     * naming no pictures among its resources is still drawn from its
     * shapes. Such a page was refused, and the thumbnail was made from a
     * photograph on a later page with the cover's words laid over it.
     */
    public function pageWithNoPicturesIsDrawnFromItsShapesTestCase()
    {
        $page = ["drawing" => "1 0 0 rg 0 0 100 100 re f", "wide" => 100,
            "high" => 100, "object" => "<< /Type /Page >>",
            "shapes" => [["corners" => [[0, 0], [100, 0], [100, 100],
            [0, 100]], "holes" => [], "ink" => [255, 0, 0]]]];
        $picture = PdfProcessor::pageDrawnFromItsParts("%PDF-1.4\n", [],
            $page, 20, 20);
        $this->assertTrue($picture !== false, "the page is drawn");
        if ($picture === false) {
            return;
        }
        $middle = imagecolorat($picture, 10, 10);
        $this->assertEqual([255, 0, 0], [($middle >> 16) & 0xff,
            ($middle >> 8) & 0xff, $middle & 0xff],
            "in the color its shape is filled with");
    }
    /**
     * pageWhoseWordsCannotBeDrawnGoesBackTestCase checks that a page with
     * no pictures, whose words can be drawn neither in the document's
     * letters nor in our own, is handed back for the older way. A slide
     * turned on its side, in fonts that could not be read, came out as a
     * blank white page.
     */
    public function pageWhoseWordsCannotBeDrawnGoesBackTestCase()
    {
        $page = ["drawing" => "BT /TT2 1 Tf 0 44 -44 0 226 105 Tm " .
            "(Source)Tj ET", "wide" => 612, "high" => 792,
            "object" => "<< /Type /Page >>", "shapes" => [["corners" =>
            [[0, 0], [612, 0], [612, 792], [0, 792]], "holes" => [],
            "ink" => [255, 255, 255]]]];
        $this->assertTrue(PdfProcessor::pageDrawnFromItsParts(
            "%PDF-1.4\n", [], $page, 20, 20) === false,
            "the page is handed back rather than drawn blank");
    }
    /**
     * letterWithAMiddleKeepsItsMiddleClearTestCase checks a letter with a
     * middle, drawn on a sheet the size of its box, against points of the
     * picture: the ring is in the letter's ink, the middle shows what was
     * there before, and what lies outside the letter is untouched. The
     * letter runs off the left of the picture, so the box is cut to the
     * picture there.
     */
    public function letterWithAMiddleKeepsItsMiddleClearTestCase()
    {
        $canvas = imagecreatetruecolor(20, 20);
        imagefill($canvas, 0, 0, imagecolorallocate($canvas, 0, 0, 255));
        $page = ["wide" => 20, "high" => 20];
        PdfProcessor::fillShapeOnPicture($canvas, ["corners" => [[-4, 4],
            [12, 4], [12, 16], [-4, 16]], "holes" => [[[2, 8], [8, 8],
            [8, 12], [2, 12]]], "ink" => [255, 0, 0]], $page, 1, 1);
        $shade = function ($across, $down) use ($canvas) {
            $color = imagecolorat($canvas, $across, $down);
            return [($color >> 16) & 0xff, ($color >> 8) & 0xff,
                $color & 0xff];
        };
        $this->assertEqual([255, 0, 0], $shade(0, 10),
            "the ring at the picture's edge is in the letter's ink");
        $this->assertEqual([255, 0, 0], $shade(10, 10),
            "the ring on the other side is in the letter's ink");
        $this->assertEqual([0, 0, 255], $shade(5, 10),
            "the middle shows what was beneath");
        $this->assertEqual([0, 0, 255], $shade(15, 10),
            "past the letter the picture is untouched");
        $this->assertEqual([0, 0, 255], $shade(5, 1),
            "above the letter the picture is untouched");
    }
    /**
     * bundleReadsOnlyItsOwnDictionaryTestCase checks that two bundles of
     * objects written one right after the other are each cut by their
     * own /First and /N. The second bundle's look back reached the first
     * one's dictionary, whose numbers were read in place of its own, and
     * a magazine's page tree came apart so that a later page was taken
     * for the cover.
     */
    public function bundleReadsOnlyItsOwnDictionaryTestCase()
    {
        $first_bundle = gzcompress("7 0 8 5 (one)(two)");
        $second_list = "9 0 10 22 ";
        $second_bundle = gzcompress($second_list .
            "<</Type/Pages/Kids[]>><</Type/Page>>");
        $document = "%PDF-1.5\n1 0 obj\n<</Filter/FlateDecode/First 8" .
            "/Length " . strlen($first_bundle) . "/N 2/Type/ObjStm>>" .
            "stream\n" . $first_bundle . "\nendstream\nendobj\n" .
            "2 0 obj\n<</Filter/FlateDecode/First " . strlen($second_list) .
            "/Length " . strlen($second_bundle) . "/N 2/Type/ObjStm>>" .
            "stream\n" . $second_bundle . "\nendstream\nendobj\n";
        $held = PdfProcessor::packedObjects($document);
        $this->assertEqual("(one)", $held[7] ?? "",
            "the first bundle's first object is read whole");
        $this->assertEqual("<</Type/Pages/Kids[]>>", $held[9] ?? "",
            "the second bundle's first object is cut by its own /First");
        $this->assertEqual("<</Type/Page>>", $held[10] ?? "",
            "and so is its second");
    }
}