useblocks / sphinx-simplepdf

A simple PDF builder for Sphinx documentations
https://sphinx-simplepdf.readthedocs.io
MIT License
32 stars 14 forks source link

Test framework for sphinx-simplepdf #83

Open kreuzberger opened 11 months ago

kreuzberger commented 11 months ago

Is there a chance to implement a basic test framework? I dont know if i should / could takeover these from the other repositories of useblocks "as is".

The pdf output could be testet with some python pdftotext modules, available at pypi. E.g. to count pages, or get the text from individual pages and compare if some expected text appears

Impementing a "basic" test would be good, i feel motivated to add more tests :grinning:

danwos commented 11 months ago

I agree, a test framework would be great. But just checking for certain text is not enough for me. I would like to be able o check also the layout, so the tests cover for instance:

A quick search hasn't found any promising solution for this.

@ubmarco: As PDF miner expert, do you have an idea how this could be achieved?

danwos commented 11 months ago

Maybe a solution would be to make a pixel-by-pixel comparison with a golden sample, which got checked once manually.

There is a question on PyMuPDF, which is discussing this: https://github.com/pymupdf/PyMuPDF/issues/584

technical concept (idea)

A test-case contains:

Pytest-fixtures to:

  1. Build the PDF from the Sphinx-project
  2. Extract the textual content as JSON, so that it can be used for tests

A helper function like compare_pdf(new_pdf, golden_sample), which compares PDF pixel-by-pixel to check for layout problems.

So in the end, each test case defines its own little project and therefore PDF. There is no single PDF file for all test cases, which is containing everything for testing (like our demo-pdf).

ubmarco commented 11 months ago

I think we should both:

Read back a PDF into text representation, we could check

We could use libpdf for this (a pdfplumber and pdfminer wrapper). This test targets directly where things went wrong. This can also detect whether tables wrapped. Keep in mind, PDFs have no understanding of words, sentences, tables. They just know letters, letter orientation, font and color. Tables are made of lines. So for proper table detection we need to use tables with borders.

Then we'll also need a image comparison to be sure the overall layout is still valid, colors match and to test theme updates. A quick search: perceptualdiff or a home-grown solution.

Getting all needed programs installed to the Github node that runs the test (e.g. pillow) might be a problem.

kreuzberger commented 11 months ago

The text solution would handle most of the test cases i have in mind. Maybe this handling could be used not only for sphinx-simple internal tests, also for the real document tests produced during build.

a pdf (one per test) test is also ok, but i am not sure if this is a) easy to maintain b) does not rely to much on weasyprint versions

Here is the question: The tests should not only tests against different sphinx versions, it should also maybe test against different weasyprint versions. This might also be trick to handle

danwos commented 11 months ago

The last point can be easily done by matrix tests. Which are supported by github actions. Sphinx-Needs does this by creating different test-envs based on python, sphinx and docutils versions.

One PDF per test has the advantage that the tests are isolated from each other and therefore normally easier to maintain,.

kreuzberger commented 6 months ago

I have to start with a test framework for the generated pdf's from simplepdf in my current project. I saw that libpdf is a repository in your organisation ( https://github.com/useblocks/libpdf ). So i assume work on a test framwork could start with this as there is currently no other solution available?

danwos commented 6 months ago

I think so, yes. May be the easiest solution as all other PDF libraries are more low-level.

kreuzberger commented 6 months ago

Integration of libpdf seems not to be so easy in an environment with sphinx-simplepdf and weasyprint due to pillow dependencies. libpdf seems to have a (maybe outdated) dependency to an exact pillow reference which is in conflict with the weasyprint dependency.

There seems to exist a branch in libpdf to fix this, but it is not merged in the main branch. @ubmarco : Maybe you could give me some hints how to solve this?

kreuzberger commented 6 months ago

And there seems to by a typo in the pyproject.toml in this branch "ruamel.yaml" = "^*"

kreuzberger commented 6 months ago

After hacking and get it running it only runs with no_annotations, and then gets stucked internal. So stopping here and wait for further hints about how to proceed.

Hacking steps:

It then fails internaly:

  objects = libpdf.load(pdf_info["document"], verbose=2, no_annotations=True)
../../../../build/debug/pypackages/venv/lib/python3.11/site-packages/libpdf/core.py:228: in main_api
    objects = main(
../../../../build/debug/pypackages/venv/lib/python3.11/site-packages/libpdf/core.py:118: in main
    objects = extract(
../../../../build/debug/pypackages/venv/lib/python3.11/site-packages/libpdf/extract.py:131: in extract
    extract_catalog(pdf, no_annotations)
../../../../build/debug/pypackages/venv/lib/python3.11/site-packages/libpdf/catalog.py:674: in extract_catalog
    des_dict = get_named_destination(pdf)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

pdf = <pdfplumber.pdf.PDF object at 0x7f9d36a4c990>

    def get_named_destination(pdf):  # pylint: disable=too-many-branches
        """Extract Name destination catalog.

        Extracts Name destination catalog (link target) from pdf.doc.catalog['Name'] to obtain
        the coordinates (x,y) and page for the corresponding destination's name.

        PDFPlumber does not provide explict 'Named Destinations of Document Catalog' like py2pdf, so it needs to be obtained
        by resolving the hierarchical indirect objects.

        The first step in this function is to check if the name destination exist in the PDF. If it does not, no extraction
        is executed.

        :param pdf: pdf object of pdfplumber.pdf.PDF
        :return: named destination dictionary mapping reference of destination by name object
        """
        LOG.info('Catalog extraction: name destination ...')

        # check if name tree exist in catalog and extract name tree
        name_tree = {}
        named_destination = {}
        pdf_catalog = pdf.doc.catalog
        if 'Names' in pdf_catalog:
            # PDF 1.2
            if isinstance(pdf_catalog['Names'], PDFObjRef) and 'Dests' in pdf_catalog['Names'].resolve():
                name_tree = pdf_catalog['Names'].resolve()['Dests'].resolve()
            elif isinstance(pdf_catalog['Names'], dict) and 'Dests' in pdf_catalog['Names']:
>               name_tree = pdf_catalog['Names']['Dests'].resolve()
E               AttributeError: 'dict' object has no attribute 'resolve'
kreuzberger commented 6 months ago

patch_libpdf.zip

After "zero knowledge based hacking" the libpdf source code i was able to extract some content.

This helps me going further into my efforts for the "pdf" check.

May the force be with you - If you might integrate :smile:

kreuzberger commented 6 months ago

forked libpdf and applied fixes to https://github.com/procitec/libpdf/tree/upgrade. I would recommend a review on the solution for the above Problem with resolve, this could be the cricital part (e.g. better use resolve_all or other methods). i would stop discussion here and would start a PR on libpdf repo.

kreuzberger commented 6 months ago

With the PR in the libpdf i am able to parse and test the pdf, e.g. chapter, headings, page numbering etc. I still have to check tables.

Open questions currently:

ubmarco commented 5 months ago

I just released a new version 0.1.0 of libpdf. It now has a new element called Rect which you can find in the architecture diagram. The rectangle color as well as its contained text with coordinates is also exposed. Any text spilling over the rectangle boundaries is cropped. Is that feature enough to write test cases?

kreuzberger commented 5 months ago

Currently i think its enough for testing. see https://github.com/useblocks/libpdf/pull/36 for integration of tests in libpdf and a sphinx-simplepdf/weasyprint generated pdf. I think test implementation for sphinx-simplepdf could start now.

I would expect one member of useblocks to create the test framework, maybe like the others with poetry/nox. libpdf is here a litte bit different, i do not know which python test framework useblocks currently prefers

ubmarco commented 5 months ago

You're right, we need to set up testing for this repo. I vote for simple tox and pytest, just like for libpdf. nox only makes sense if we need to programmatically configure the test matrix.