Issues with Panorama and TidyUp


Our university uses Canvas as it LMS. They have installed Panorama to check accessibility, and have installed TidyUp to help find unused files that can be deleted. But there are issues.

First, TidyUp does not know about images used in quizzes. So if you delete a file based on its recommendation, you may actually break a quiz. My workaround is to just not use TidyUp

Next, Panorama remembers scores of deleted files and factors them into your overall accessibility score. I typically import my Canvas course from a previous semester and then delete previous class presentations, announcements, etc. But Panorama was still holding onto their scores. Contacting OTL, it seems they know about this, and are eventually planning on telling us all about it. But you don't have to wait. Here's the solution. After deleting things, go to the panorama menu item, click on "Configuration/General Settings" and click on "Reprocess Course." It says it won't change any of your files, but it also makes you type a word to make sure you really want to do this, like it is a dangerous thing to do. But that's what OTL told me to do. Then you wait for a long time (possibly days) for it to finish.

Another problem is that if you put a journal article on your Canvas page, you will be judged on its accessiblity. In one of my classes I have them read an article. But I can't send them to the journal's page because it is not publically accessible. I am allowed to post it onto my class site. But it is not accessibile, and there is nothing I can do about that. OTL says they know about this, but they don't have any process for you to deal with it.

Making Accessible PDFs with LaTeX


There are many sources on how to make accessible PDF files with LaTeX, but they are often contradicting. Here's what worked for me --- take it for what it is worth.

In the past people would import various packages to implement accessibility in PDF files generated with LaTeX. But now there are early versions of accessibility available in latex-lab, a package with new or experimental files that are not yet part of the standard distribution. To use these files, you must install the latex-lab package. Then you need to do the following to make them work.

  1. latex-lab relies on luatex, so you must build your pdf file by running lualatex (not pdflatex, xetex, etc.).
  2. I'm told that not all document classes are supported. So if you have problems, try a different one. The standard article, report, and book are supposed to work, and it has worked for me using extbook.
  3. At the very top of your document, before documentclass, you need to include the \DocumentMetadata command. It should include the option testphase = {phase-II, math} as shown below.
    		\DocumentMetadata{
    			lang = en-US,
    			tagging=on,
    			testphase = {phase-II, math}
    		}
    		
  4. All images should have an "alt" tag which is text describing the image for someone who cannot see it. An example is below.
    		\includegraphics[width=0.98\textwidth,alt={A picture of a caveman saying, "What Zog mean rocks is fingers?  Rocks no taste like fingers!"}]{VectorFigs/NeanderthalRocks}
    		
  5. If you have tables, they need to have a header row which describes what is in each column. Then, before the table, add the code
    		\tagpdfsetup{table/header-rows={1}}
    		
    The {1} indicates that the first row is the header row. If the header takes up the first two or three rows, you would change this to {2} or {3}, etc.

Note that I get a lot of "Package tagpdf" warnings when I run lualatex, but the documents seem to be OK and pass the accessibility tests.

Overleaf has some useful info on making accessible pdfs which can be found here.

Making Accessible PDFs with LyX


I use LyX, kind of a visual front end for LaTeX, to write a lot of things. If you use LyX, here are some pointers on making accessible documents in addtion to the ones above.

First, rather than putting it in the preamble, there is a special place for the metadata under the menu

Document/Settings/PDF Properties

You don't type \DocumentMetadata{, you just put the stuff inside of that block.

You should disable listings so it isn't loaded. This is done by going to the menu

Document/Settings/Local Layout
and adding the line
Provides listings false

To add alt tags to images, click on the image to bring up the Graphics box. Click on the "LaTeX and LyX options" tab. Then on the LaTeX options line, type your alt tag following the example below.

			alt={A picture of a caveman saying, "What Zog mean rocks is fingers?  Rocks no taste like fingers!"}
			

To create a pdf, you need to export your project as a LuaLaTeX file by using the menu item

File/Export/LaTeX (LuaTeX)

If your document uses smcode, run the code below to process the file/files. Then run lualatex on the processed file as described in the comments at the beginning of the code below. If not, run lualatex on the file exported from LyX. Note that directly exporting to pdf from LyX doesn't seem to put the accessibility structure into the file. I've found I have to export to tex and then run lualatex outisde of LyX.

Pretty LaTeX Code Listings with Accessibility


The "listings" package is not compatible with the accessibility code in LaTeX. So after hours trying to find a work-around, and more hours trying to define my own environment, I decided just to write some Python code to parse my file and format my code listings for me.

To use this code, you use a "fake" environment called "smcode" on your code listings. (The sm stands for "Slendermath," a textbook I wrote which you can read here.)

In addition to doing code listings, it also looks for any missing alt tags in images and makes you aware of them. And it can optionally change all bold math symbols to pmb symbols and convert \[ \] equations to \begin{equation} form with enforced equation numbers.

If you don't need any of the above, you can still use it to get a list of images which are missing alt tags. More instructions are given in the code below.

The code

You can read the code below. You can download it here

r"""
ProcessTexV3.py
@author: Dallin Durfee  (c)  2026

This code was written to help implement accessibility for the vision
impared when writing LaTeX documents.  The accessibility features 
currently in LaTeX are not compatible with the listings package.
After hours of trying to write an environment that was compatible and
would do what I want, I gave up and instead wrote this code so that
I could insert my code listings in a "fake" environment, and then have
this Python code parse the .tex files and replace the things in the
environment with code that formats the stuff inside the environment
to produce a nice looking code listing.  This fake environment 
called "smcode" (SM for Slendermath, the book I was working on when
I wrote this code). It is discussed below.

In addition to doing code listings, it also looks for any missing
alt tags in images and makes you aware of them.  You can set some
boolean variables below to have the code change bold math symbols
to pmb symbols, and to "upgrade" \[ \] equations to \begin{equation}
equations labelled with numbers.

If you use pmb symbols, you need to put

\usepackage{bm}

in your preable after all fonts have been defined.

This code notices include files, and will process each included file
In your project.  It will write each processed file
with the original filename but with _PyProc.tex added to the end 
of the name (so your original files are not overwritten).
The references in the processed files will point to the processed
versions of the files referenced.

To use this code:
    
    1.  Make your tex files compatible with LuaLaTex.
    
    2.  Make sure that you add the lines to your preamble:
        
        \usepackage{pdfmanagement-testphase}
        \usepackage{unicode-math}
        
    3.  If you are using smcode, you also need to add the following
        lines to your preamble:
            
        \usepackage{tcolorbox}
        \tcbuselibrary{breakable}
        \usepackage{longtable}
        \definecolor{listingbackground}{gray}{0.9}
        \definecolor{commentcolor}{rgb}{0.0, 0.478, 0.182}
        \definecolor{changecolor}{rgb}{0.89, 0.02, 0.17}
        \definecolor{linenumbercolor}{gray}{0.6}
    
        You can change the colors defined above to your liking.
    
    4.  In the code below, change "topfilename" to the name of the 
        latex file to be processed.  For example, if my file name 
        was "TheBook.tex" I would change that line in the code below to read  
        
        topfilename = "TheBook.tex"
        
        If your project uses includes to import other files, they will be
        processed as well.  New files will be created with "_PyProc.tex" 
        tacked on the end (such that your original files are not overwritten).
        
    4.  Run this code
    
    5.  Open a command prompt, navagate to the folder containing the top file, 
        and run 

        lualatex topfilename_PyProc.tex
        
        Change "topfilename" to the name of your file.
        For example, if my file was named "TheBook.tex" I would run
        lualatex TheBook_PyProc.tex


To use the "fake" listing environment, enclose your code listings with 

\begin{smcode}[n]

and

\end{smcode}

where n is the line number with which to start.  Normally this would be one:
    
    \begin{smcode}[1]
    
But if you are continuing a listing started previously, or changing part of
a listing, you can put in a different starting number.
If you set n zero, no line numbers will be printed.

NOTE: \begin{smcode}[n] and \{end{smcode} must each be on a line by itself.

Python single-line comments will automatically be set to the "comment"
color.  But you can also make code the comment color by putting it
inside a \cmt{  } environment.

To change the color of some code to the "change" color (like, for
example to highlight something that has changed) put it inside
a \chng{  } environment.

\cmt and \chng  are "fake" environments inside of the "fake" smcode
environment, and are formatted with the code below.

If you place \dbs in your LaTeX, this will be changed to a
double slash by this code.  


NOTE: \verb does not seem to work in a minipage.  This means that
\begin{smcode}[1] won't work either.

The "fake" environment supports line numbering, and it makes lines 
beginning with # (Python comments) green in color.  This code can be
modified to do other types of formatting, but that is all that I needed.

"""


topfilename = "SlendermathV0p9p9Y.tex"


# Set to True to output all images missing alts to the console.
# This can be tedious if there are many of them.
printMissingAlt = False
# Set to True to output all images missing alts to a log file.
# This is the better option if there are many missing alt tags.
# The log file will be named the same as the topfile, but with
# _altlot.txt stuck onto the end.
logMissingAlt = True
# Set to true to replace \boldsymbol and \mathbf with \pmb.
# I found that the bold math symbols I made showed up blank in some
# of my students' pdf readers.  "Poor man's bold" or \pmb is a 
# workaround for that.
usePMB = True
# Set this to true to replace \[ and \] with \begin{equation} so
# that all of the euquations I lazily wrote using \[ and \]
# will get numbered.
UpdootMath = True     


#########################################################################
##
##  Do not edit below this line unless you know what you are doing.
##
#########################################################################



def findBracketPair(line, istart, brackets=("{","}")):
    i = istart
    iopen = -1
    iclose = -1
    icount = -1000
    while(True):
        if(line[i] == brackets[0]):
            iopen = i
            icount = 1
        if(line[i] == brackets[1]):
            icount -= 1
            if(icount == 0):
                iclose = i
                break
        i += 1
        if(i >= len(line)):
            return(-1,-1)
    return(iopen, iclose)


dotpos = topfilename.find('.')
topfileroot = topfilename[:dotpos]




def processFile(filename):
    global filenumber

    dotpos = filename.find('.')
    fileroot = filename[:dotpos]
    savefile = fileroot + "_PyProc.tex"
    #print(savefile)
    fin = open(filename, encoding="utf-8")
    lines = fin.readlines()
    fin.close()
    fout = open(savefile,"wt",  encoding="utf-8")
    i = 0
    N = len(lines)
    
    eqnum = 0
    
    print(f"Processing file {filenumber}: {filename}")
    
    
    while(i < N):
        line = lines[i]
        
        # Look for includes
        iinclude = line.find("\\include{")
        if(iinclude >= 0):
            istart, iend = findBracketPair(line, iinclude)
            fname = line[istart+1:iend]
            line = line[:istart+1]+fname+"_PyProc"+line[iend:]
            filenames.append(fname+".tex")
        
        # Replace \boldsymbol and \mathbf with \bm
        if(usePMB):
            line = line.replace('\\boldsymbol{', '\\pmb{')
            line = line.replace('\\mathbf{', '\\pmb{')
       
        
        # Check for alt tags in graphics
        ifig = line.find("\includegraphics")
        if(ifig >= 0):
            tagmissing = False
            istart, iend = findBracketPair(line, ifig, brackets=("[","]"))
            # If there are no square brackets, then there is definitely
            # not alt tag inside the nonexistant square brackets!
            if(istart == -1):
                tagmissing = True
            else:
                params = line[istart+1:iend]
                if(params.find("alt=") < 0):
                    tagmissing = True
            if(tagmissing):
                inamestart, inameend = findBracketPair(line, iend)
                if(printMissingAlt):
                    print(f"Missing alt for {line[inamestart+1:inameend]}")
                if(logMissingAlt):
                    altLogfile.write(f"Missing alt for {filename}:  {line[inamestart+1:inameend]}\n")                                                   
                
        # Turn \[ equations into \begin{equation} equations
        # so that every equation gets numbered.
        if(UpdootMath):
            line = line.replace("\\[", "\\begin{equation}")
            line = line.replace("\\]", "\\end{equation}")
        
       
        
        # Check for equation labels, add dummy if needed
        b = line.find("\\begin{equation}")
        if(b >= 0):
            haslabel = False
            while(True):
                # Replace \boldsymbol and \mathbf with \bm
                # This is because there is a bug which causes
                # bold math symbols to not appear in some pdf
                # readers.  So to fix this we must include
                # \usepackage{bm} 
                # in the preamble, and then us \bm instead
                # of \boldsymbol and \mathbf.
                # This is probably redundant old code, since this 
                # function dealt with this previously.
                # But the code works, so I'll leave it in until
                # I have time to test and make sure this is not needed.
                if(usePMB):
                    line = line.replace('\\boldsymbol{', '\\pmb{')
                    line = line.replace('\\mathbf{', '\\pmb{')
                # If we encounter \] and it hasn't already been
                # replaced with \end{equation}, do that now.
                line = line.replace("\\]","\\end{equation}")
                # If we reach the end of the equation, finish
                # up by adding the label.  Then break
                # out of this while loop.
                b = line.find("\\end{equation}")
                if(b >= 0):
                    if((UpdootMath) and (not haslabel)):
                        fout.write("\\label{eq:"+fileroot+str(eqnum)+"}\n")
                        eqnum += 1
                    break
                if(line.find("\\label") >= 0):
                    haslabel = True
                fout.write(line)
                i += 1
                line = lines[i]
            
        
        # Process code listings
        # The should start with a line containing ONLY
        # \begin{smcode}[x]
        # Anything else on the line will be ignored (dropped).
        # where x is the line number of the first line.
        # Set x to zero for no line numbers.
        # The end of the code listing should be a line 
        # containing ONLY
        # \end{smcode}
        # Anything else on the line will be ignored (dropped).
        b = line.find("\\begin{smcode}")
        if(b >= 0):                        
            # Find the argument with the starting line number
            b = line.find("[")
            c = line.find("]")
            
            linenum = int(line[b+1:c])
            fout.write("\\begin{tcolorbox}[breakable, size=fbox, boxrule=0pt,  boxsep=5pt, colback=listingbackground]\n")
            fout.write("\\makeatletter\n")
            fout.write("\\setlength\\LTpre{0cm}%\n")
            fout.write("\\setlength\\LTleft{0cm}%\n")
            fout.write("\\begin{longtable}{ r l }\n")
            fout.write("\\@math@level=0\\relax\n")
            i += 1
            
            # Keep going through lines until we reach the end
            # of the listing.
            while(True):
                line = lines[i]
                # I use the \dbs command to insert a double 
                # slash in code listings.
                # Here I convert that to \\\\
                line = line.replace("{\dbs}",'\\\\')
                line = line.replace("\dbs",'\\\\')                
                
                # Check to see if we reached the end
                b = line.find("\\end{smcode}")
                if(b >= 0):
                    line = "\\end{longtable}\n\\makeatother\n\\end{tcolorbox}\n"
                    break
                
                # Add line numbers if needed
                if(linenum > 0):
                    fout.write("{\\scriptsize \\color{linenumbercolor} \\verb| %d |} & " % linenum)
                    linenum += 1
                else:
                    fout.write("  & ")

                # I previously used \cmt{} to demark comments
                # so I could change their color.  But now that is
                # done automatically, so I'll just get rid of 
                # \cmt{} statements.
                while(True):
                    icmt = line.find("\\cmt{")
                    if(icmt < 0):
                        break
                    iopen, iclose = findBracketPair(line, icmt)
                    line = line[:icmt]+line[iopen+1:iclose]+line[iclose+1:]
                
                # I use the markers @r and r@ to indicate
                # parts of the listings that should be highlighted
                # as code that has been changed from a previous
                # listing.
                # I also decided to use the command \chng{} 
                # as an option to do this to make things
                # more readable.
                # Here I replace any \chng{} commands to a pair
                # of @r and r@ markers so that they will be dealt
                # with properly when I deal with change markers
                # later in this code.
                while(True):
                    ichng = line.find("\\chng{")
                    if(ichng < 0):
                        break
                    iopen, iclose = findBracketPair(line, ichng)
                    line = line[:ichng] + "@r" + line[iopen+1:iclose] +"r@" + line[iclose+1:]
                    
                # Look for single line comments and change color
                icmt = line.find("#")
                # First deal with comments at start of line
                if(icmt == 0):
                    line = "{\\color{commentcolor}\\verb|" + line[:-1] + "|} \\\\\n"
                elif(icmt > 0):
                    line = "\\verb|"+line[:icmt]+"|{\\color{commentcolor}\\verb|"+line[icmt:-1]+"|} \\\\\n"
                else:
                    line = "\\verb|" + line[:-1] + "| \\\\\n"                                
                
                # Look for change markers and set the color
                # between the markers.
                while(True):
                    ichng = line.find("@r")
                    if(ichng < 0):
                        break                    
                    iend = ichng+2 + line[ichng+2:].find("r@")
                    line = line[:ichng] + "|{\\color{changecolor}\\verb|" + line[ichng+2:iend] + "|}\\verb|" + line[iend+2:]
                    
                fout.write(line)
                i += 1 
                                  
        fout.write(line)
        i += 1
    fout.close()
    filenumber += 1
    
    
#topsavefilename = topfileroot + "_PyProc.tex"
if(logMissingAlt):
    altLogfile = open(topfileroot + "_altlog.txt", "wt")

filenames = [topfilename]


filenumber = 1

while(len(filenames) > 0):
    filename = filenames.pop(0)    
    processFile(filename)
    
print()
print(f"Finished Processing {topfilename} and its includes")

Tips to speed up accessibility tasks on Canvas


Setting alt tags in Canvas is tedious. At first I tried going through quizzes, finding each image, and adding an alt tag. But this involves many clicks and much waiting for pages to refresh. I was shown that if you go to the images in your "Assignment Images" and "Uploaded Media" folders in the Files page, after it finishes loading each image will have a little circle next to the filename with the word ALT in it. Click on that and you can set the alt text for the image, and then save the alt text to any quiz, etc., that uses that image.

Note, however, that it does not give you a view of the image while you write the alt tag. So its a good idea to have the folder open in another window so you can open the image while writing the alt tag in the other window. Also, it doesn't tell you where the image is in your Canvas page. So it's kind of hard to check that the tag was applied properly. But, you know, Canvas.

Generating video captions with AI


I tried a few AI captions solutions that didn't work well, but Google's Gemini AI at https://gemini.google.com/ is pretty good at making captions, even when you have science and math, etc. But the free version won't cut it. I signed up for the Google AI Pro plan ($19.99 a month). They have a $4.99 paid plan, and if you try it and find it works for this, please let me know!

You can't just give Gemini a link to the video online, you have to upload a video. So if you have a video on YouTube, you will need to log in, go to the video, and click on the "Edit video" button below the video. This takes you to YouTube Studio. Here, the first thing to do is to scroll all the way down, click on "more" and make sure the video and title language is set appropriately. If you choose English, I recommend just plain English, not English (United States), etc., because I think (but I am not sure) that this is what caused me a problem when I was figuring things out. After setting the language, be sure to click the "Save" button at the top.

Next, if your video is on YouTube and you don't have ready access to the original video you uploaded, click on the three dots at the top, and select Download.

Once the video has downloaded, go to gemini.google.com. When you do, you will notice a "+" sign on the left side of the input prompt. Click on it and select "Upload files" and upload your video. Once it has finished uploading, paste the following prompt into the input and hit enter.

		Listen to the audio and observe the video in this file and generate a precise .srt caption file with timestamps that I can upload to YouTube.  Break up the captions such that there are no more than 100 characters on the screen at any time.
		
It will take Gemini a few minutes. If it says that it's produced the output, but the output is not there, just wait a bit.

If it gives you a little icon of a circle with a downward arrow, click on it to download the file. Otherwise, select all of the output, copy, paste it into a text editor like notepad, and save it. Find the file, and rename it to the name of your video with the extension .srt.

If your video is on YouTube, you can upload the caption file by going to your Studio window, scrolling up to the top, and clicking on "Subtitles" on the right. When the subtitle pop-up window appears, click on the three dots and select "Upload file." It will give you a window where you tell it whether your file has timing or not. Select "with timing," click "continue," and select your .srt file.

Then you need to check that everything is correct. I like to put the video on 2x speed and read along as it plays. Stop the video and correct any errors (there should not be many, or maybe even none!).

I found that most often the file Gemini produced was just a little fast in the timing and I could not get it to do it right consistantly. However, even though YouTube is terrible at generating captions, it seems to do well fixing timing once you have proper captions broken up into segments. To get YouTube to fix the timing, after uploading the .srt file, just click on "Edit as text," then select all of the text, cut (ctrl-x), and then paste it back in (ctrl-v). Then click on "Edit timings" to go back to the standard timing view. That's all you have to do! Just check to make sure the timings are correct. Then click on "Done" and you are done!

Exporting Accessible PowerPoint Slides


When saving a PowerPoint presentation using the File/Save As Adobe PDF function in PowerPoint, I ended up with PDF files that had problems with the reading order. So I wrote some macros to export to pdf with accessibility options enabled. These fixed the problem for me, but added the problem that the pdf is not saved with a "title" tag. This is easy to fix in Canvas (easier than fixing reading order). In case you have the same problem and want to try my macros, you can download the file here: MacrosV2.pptm.

There are two public macros in the file. "SaveAsTaggedPDF" saves your powerpoint file as an accessible pdf (assuming you have everything tagged, such as alt text for images). "ProcessAndSavePDF" does the same thing, but it does some processing first. In particular it deletes any empty slides (I often put empty slides to write on, and I don't want to keep the ones I left blank in the presentation) and it deletes any slide with a text element that reads "deletethisslide" so that I can not include any slides with things like copyrighted images when saving the PDF (I put the text outside of the view area of the slide I don't want to include in the PDF).