diff --git a/.github/workflows/push-actions.yml b/.github/workflows/push-actions.yml index 32f094ad..6ab5d60d 100644 --- a/.github/workflows/push-actions.yml +++ b/.github/workflows/push-actions.yml @@ -45,14 +45,14 @@ jobs: architecture: x64 - name: Install Lynx dependency run: sudo apt install lynx - - name: Install PHP8.1 - run: sudo apt install php8.1 + - name: Install PHP + run: sudo apt install php - name: Install latest Chrome to match chromedriver package version # Note: you should see if the version of Chrome matches the latest listed on # https://mirror.cs.uchicago.edu/google-chrome/pool/main/g/google-chrome-stable/ run: | apt search '^google-chrome.*' \ - && wget -q -O /tmp/chrome.deb http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_130.0.6723.58-1_amd64.deb \ + && wget -q -O /tmp/chrome.deb https://mirror.cs.uchicago.edu/google-chrome/pool/main/g/google-chrome-stable/google-chrome-stable_132.0.6834.110-1_amd64.deb \ && sudo apt install -y /tmp/chrome.deb --allow-downgrades \ && rm /tmp/chrome.deb - name: Log system details diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 00000000..d5c40744 --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,55 @@ +module.exports = { + "customSyntax": "postcss-less", + "extends": [ "stylelint-config-standard"], + "ignoreFiles": [ + "**/*.js", + "enable-node-libs/**/*.css", + "js/out/**/*.css", + "js/enable-libs/**/*.css" + ], + "plugins": ["@double-great/stylelint-a11y"], + "rules": { + "a11y/no-text-align-justify": true, + "alpha-value-notation": null, + "at-rule-empty-line-before": null, + "at-rule-no-unknown": null, + "color-function-notation": null, + "color-hex-length": null, + "comment-empty-line-before": null, + "comment-whitespace-inside": null, + "custom-property-empty-line-before": null, + "custom-property-pattern": null, + "declaration-empty-line-before": null, + "declaration-block-no-duplicate-properties": null, + "declaration-block-no-redundant-longhand-properties": null, + "declaration-block-no-shorthand-property-overrides": null, + "declaration-block-single-line-max-declarations": null, + "font-family-name-quotes": null, + "font-family-no-missing-generic-family-keyword": null, + "function-calc-no-unspaced-operator": null, + "function-name-case": null, + "function-no-unknown": null, + "function-url-quotes": null, + "hue-degree-notation": null, + "import-notation": null, + "keyframes-name-pattern": null, + "length-zero-no-unit": null, + "media-feature-range-notation": null, + "media-query-no-invalid": null, + "no-empty-source": null, + "no-descending-specificity": null, + "no-duplicate-selectors": null, + "number-max-precision": null, + "property-no-unknown": null, + "property-no-vendor-prefix": null, + "rule-empty-line-before": null, + "selector-class-pattern": null, + "selector-id-pattern": null, + "selector-no-vendor-prefix": null, + "selector-pseudo-element-colon-notation": null, + "shorthand-property-no-redundant-values": null, + "string-no-newline": null, + "value-keyword-case": null, + "value-no-vendor-prefix": null + } +} diff --git a/.stylelintrc.json b/.stylelintrc.json index 315b554b..c017f0c6 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -7,7 +7,9 @@ "js/out/**/*.css", "js/enable-libs/**/*.css" ], + "plugins": ["@double-great/stylelint-a11y"], "rules": { + "a11y/no-text-align-justify": true, "alpha-value-notation": null, "at-rule-empty-line-before": null, "at-rule-no-unknown": null, diff --git a/bin/checkHTML.sh b/bin/checkHTML.sh index 90f85252..7216100d 100755 --- a/bin/checkHTML.sh +++ b/bin/checkHTML.sh @@ -86,6 +86,12 @@ If that does not work, you may need to do a global update of @axe-core/cli: sudo npm update -g @axe-core/cli +******************************************************************************** +* NOTE: If you are seeing this message in GitHub Automated Tests, you need to +* update push-actions.yml to ensure the version of Chrome being downloaded there +* matches the one in your package.json +******************************************************************************** + ' 1>&2 exit 1; diff --git a/content/body/accessible-pdf-generation.php b/content/body/accessible-pdf-generation.php new file mode 100644 index 00000000..c44c2c9c --- /dev/null +++ b/content/body/accessible-pdf-generation.php @@ -0,0 +1,361 @@ +
+ There are many times where a website may want to generate on-the-fly PDF documents. A few examples are: +
++ Unfortunately, the average person tasked to generate these reports may not realize that screen readers have problems parsing the vast majority of PDFs, due to them not being tagged. Tagging a PDF is similar to marking up an HTML document with semantic HTML tags — it allows screen readers to travese the document landmarks, navigate through interactive elements with a keyboard, and so forth. In order to be considered accessible, Tagged PDFs must conform to the PDF/UA standard. +
+ ++ We have been searching a long time for a solution for creating PDF/UA compliant PDFs that was both inexpensive and open source. In 2023, we stumbled upon Open HTML to PDF, which claimed it could create PDF/UA compliant accessible PDFs. After testing it out ourselves and confirming it can work, we wanted to share how we implemented this ourselves for those of you who want to as well. This guide will demonstrate creating a SpringBoot app with Open HTML to PDF, and will also cover how an HTML author can troubleshoot common problems using the tool. +
+ + ++ The application that we wanted to integrate the PDF generator into was written in node. Since Open HTML to PDF is a Java library, we decided to build a separate SpringBoot app that our node application could connect to via HTTP to create these accessible PDFs. This article will walk you through how you too can create a Java SpringBoot app that integrates the OpenHTMLToPDF library.
+ + +You will need the following installed on your machine:
+We will be using SpringBoot, a Java Framework, to develop this app. If you are unfamiliar with this technology, +please see here for more information about SpringBoot. +We will be using the Open HTML to PDF library to create an accessible PDF. +The repository for Open HTML to PDF for can be found here. +We will provide references to specific pages that are of interest in the tutorial as applicable.
+ + +We will use the Spring initializer to create a new SpringBoot project. +The Spring initializer tool can be found here. +This will generate a new SpringBoot project that we will use as the basis for our accessible PDF app. Open HTML to PDF is injected as a maven dependency, so select “Maven” under project. +We will be using Java as the language. Select the latest version of SpringBoot. In the metadata, add a name, artifact, name, and description relevant to your project (package name is automatically generated, but you can change this to suit your needs). +Under packaging, we will be selecting Jar, as it works best for our deployment configuration, but please select the packaging that is relevant to your project. Open HTML to PDF is compatible with, at latest, Java 17, so select Java 17. The screenshot below describes the configuration that we used. +Once you have selected your desired configuration, click on generate, and uncompress the generated zip file. Open this folder in an IDE.
+Navigate to the pom.xml file. This file controls the dependencies in the code. This is where we will be adding in the dependency for the Open HTML to PDF. Following the +implementation guide in the Open HTML to PDF repository, +we will add the necessary dependencies to the pom.xml file. If your PDF does not contain images, right to left test, SVGs, or MathML, you will only need to add the first 2 dependencies found under the “MAVEN ARTIFACTS” section of the article. We will also need to add the Open HTML to PDF version under the properties section. +After adding the necessary dependencies and properties for our project, the properties and dependencies tags will similar to below.
+ + + +... +<properties> + <java.version>17</java.version> + <openhtml.version>1.0.10</openhtml.version> +</properties> +<dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <!-- ALWAYS required, usually included transitively. --> + <groupId>com.openhtmltopdf</groupId> + <artifactId>openhtmltopdf-core</artifactId> + <version>${openhtml.version}</version> + </dependency> + + <dependency> + <!-- Required for PDF output. --> + <groupId>com.openhtmltopdf</groupId> + <artifactId>openhtmltopdf-pdfbox</artifactId> + <version>${openhtml.version}</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> +</dependencies> +... + + + +We are going to begin to build out a Java file to act as our endpoint to generate a PDF. To do this, first create a new file at the same level as the automatically generated java file under src/main/java/[your package name]. In our case, it is at the same level as “AccessiblePDFSpringApplication.java”. +We will call our new file AccessiblePDFController.java. We will be creating a Controller file, would marks a class as a web request handler.
+Then, we will start to add to this file to build out our endpoint. First, we will mark the class as a REST controller by adding @RestController above the class declaration.
+ + +@RestController +public class AccessiblePDFController { + +} + + + +Then, we will add a method and have it return a ResponseEntity<byte[]>, since we will be sending the response body as byte array. This method will need to throw an exception. You will see an error as we do not have a ResponseEntity as a return value yet, but we will add this in a later step.
+ + +@RestController +public ResponseEntity<byte[]> genratePDF() throws Exception { +} + + + +We will now create a new file so that we can properly parse the body that is sent to the endpoint in the request. This will also be at the same level as the Controller file. We have named ours “HTMLBody.java”. See below for the structure.
+For our purposes, we will send the HTML to the SpringBoot app as a string, and we will also send the filename, which will be the name of the file that we want to generate. To consume this information properly, we will create a class that has 2 properties, a String html and a String filename. The code will look like this:
+ + +public class HTMLBody { + public String html; + public String filename; +} + + + +We will now add the request body as an argument to our method. This will use the class that we created in the previous step. In addition to this, we will add the @CrossOrigin annotation, which enables cross-origin resource sharing only for this specific method. We will also be adding the @PostMapping annotation, which binds the method to an endpoint. +In the @PostMapping annotation, we will be adding the destination that we want the endpoint at, as well as the format of the body of the request, which in our case is JSON.
+ + +@CrossOrigin +@PostMapping(value = "/yourEndpointHere", consumes = MediaType.APPLICATION_JSON_VALUE) +public ResponseEntity<byte[]> genratePDF(@RequestBody HTMLBody html) throws Exception { +} + + + +Now, we will add fonts to our project. Note that this is an extremely important step as the PDF will not generate if you do not provide fonts. First, download the fonts that you would like. We found ours on +Google Fonts. +Be sure that the fonts that you select are free for commercial use. We downloaded 2 fonts, one for normal text and another for code snippets. These files should be in .ttf format. After downloading these fonts, we will add them to the resources folder.
+We will add these fonts as resources in our pom.xml file so that we can access them in our Java code. In your pom.xml add the fonts under <build>, then <plugins>. You will add a <resources> tag, with a <resource> tag, then a <directory> tag, with the <includes> and <include> tag that specifies the name of the files that you would like to access in your Java code.
+ + +... + <build> + ... + <resources> + <resource> + <directory>src/main/resources</directory> + <includes> + <include>Your-Font.ttf</include> + <include>Your-Other-Font.ttf</include> + </includes> + </resource> + </resources> + ... + </build> +... + + + +We will now add code to deal with the font files. The Open HTML to PDF library needs the files to be in Java File format, so we will add code to do this. Since the files are under the resource folder, they will be loaded as resources rather than files. We will convert the resource into a steam and then write that to a file that is accessible from our controller class. +We will delete the temporary font files that we’ve generated from the resource stream after we’re done converting the PDF, so we will add code to handle this as well. The code below is added to our controller file to handle the fonts.
+ + +ClassLoader classloader = Thread.currentThread().getContextClassLoader(); + +InputStream font1 = classloader.getResourceAsStream("Your-Font.ttf"); +InputStream font2 = classloader.getResourceAsStream("Your-Other-Font.ttf"); + +File fontFile1 = new File("OpenSans-Regular.ttf"); +copyInputStreamToFile(font1, fontFile1); +File fontFile2 = new File("SourceCodePro-VariableFont_wght.ttf"); +copyInputStreamToFile(font2, fontFile2); + +With the copyInputStreamToFile helper class:
+ + +// https://mkyong.com/java/how-to-convert-inputstream-to-file-in-java/ +private static void copyInputStreamToFile(InputStream inputStream, File file) throws IOException { + try (FileOutputStream outputStream = new FileOutputStream(file, false)) { + int read; + byte[] bytes = new byte[DEFAULT_BUFFER_SIZE]; + while ((read = inputStream.read(bytes)) != -1) { + outputStream.write(bytes, 0, read); + } + } +} + + + +We will now implement the Open HTML to PDF library in our controller file to convert the HTML we have sent as a string in the request body to PDF format. We will be following the article in the repository called +PDF Accessibility (PDF UA, WCAG, Section 508 Support. +In the section “Builder Example”, you will see the code that we will need to add to our project. Adapting this to the steps before, we will add the following to our code:
+ + +try (FileOutputStream os = new FileOutputStream("./" + html.filename + ".pdf")) { + + PdfRendererBuilder builder = new PdfRendererBuilder(); + builder.useFastMode(); + builder.usePdfUaAccessbility(true); + // may be required: select the level of conformance + builder.usePdfAConformance(PdfRendererBuilder.PdfAConformance.PDFA_3_U); + // Remember to add one or more fonts. Only .ttf fonts are supported. + //For font families do not use "serif", "sans-serif", or "monospace" as this causes an error. + builder.useFont(fontFile1, "BodyFont"); + builder.useFont(fontFile2, "CodeFont"); + builder.withHtmlContent(html.html, ""); + builder.toStream(os); + builder.run(); + +} catch (Exception e) { + +} + +IMPORTANT: please see the lines with builder.useFont(…). The second argument in this method MUST match the font family that you have in the CSS that you send in the HTML string, and this MUST NOT match any of the font families that you would typically expect to see (such as serif, sans-serif, or monospace), but rather must be unique. +If this font convention is not followed, your PDF will not generate properly, and you will likely get an extremely vague error message, usually from the Open HTML to PDF library saying that you did not provide fonts. If you get that error message, come back to this step and ensure that you have added the fonts properly and that the font families specified in your CSS match those that you have specified in your Java code.
+ + +We will now add the code to convert the PDF to a byte array, as this is the format that we will be sending back in the response.
+ + +byte[] inFileBytes = Files.readAllBytes(Paths.get("./" + html.filename + ".pdf")); +byte[] contents = java.util.Base64.getEncoder().encode(inFileBytes); + + + +We will now add the code needed to send a response. This will involve creating the headers needed and then setting the ResponseEntity. We will add this for both the case where we are successful in generating a PDF, sending an OK status, and in the catch block that we will be in if we are not successful in generating a PDF, in which case we will send a 400 error. We will also send that the content form of the response is a PDF. See how we implemented this in the code below.
+ + +try { + // code for generating PDF omitted +} catch (Exception e) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_PDF); + headers.setContentDispositionFormData(html.filename, html.filename); + headers.setCacheControl("must-revalidate, post-check=0, pre-check=0"); + // set response + ResponseEntity<byte[]> response = new ResponseEntity<>(null, headers, HttpStatus.BAD_REQUEST); + return response; +} + +// code for generating byte array omitted + +// set headers for the response +HttpHeaders headers = new HttpHeaders(); +headers.setContentType(MediaType.APPLICATION_PDF); +headers.setContentDispositionFormData(html.filename, html.filename); +headers.setCacheControl("must-revalidate, post-check=0, pre-check=0"); + +// set reponse +ResponseEntity<byte[]> response = new ResponseEntity<>(contents, headers, HttpStatus.OK); +return response; + + + +We are now able to run this app locally. Use the command mvn spring-boot:run to run the program. The endpoint will usually be at http://localhost/yourEndpointName.
+ + +Integrate the endpoint into your existing code. We have included a JavaScript example to convert a byte array into a PDF and to automatically save the resulting PDF.
+ + +function base64ToArrayBuffer(base64) { + var binaryString = window.atob(base64); + var binaryLen = binaryString.length; + var bytes = new Uint8Array(binaryLen); + for (var i = 0; i < binaryLen; i++) { + var ascii = binaryString.charCodeAt(i); + bytes[i] = ascii; + } + return bytes; +} + +function saveByteArray(reportName, byte) { + var blob = new Blob([byte], { type: "application/pdf" }); + var link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + var fileName = reportName; + link.download = fileName; + link.click(); +}; + + + +You may run into some issues once you have integrated the new endpoint in with your code. This is most likely because you have elements in your HTML that the Open HTML to PDF library is not able to parse. Though you may get an error indicating that you did not provide a font or that there is an element that the library does not recognize or is malformed, we found that most of these errors were caused by having elements in our HTML that the library could not parse properly. +To fix this, we removed all buttons, all images (there is a way to keep images, the +documentation for SVG images can be found here +and +more documentation for Java 2D images here +, but the only images in our PDF were indicators that a link would open in a new tab, so we elected to remove them), all lists (both ordered and unordered), which were replaced with paragraph tags in the order that we wanted the information displayed, and all code tags which we replaced with <p> tags with corresponding CSS that had the font family set to the code font that we provided the Spring app with. +There may be other tags that are not recognized that we have not yet come across.
+We also had to replace any tags that we wanted to display as text with "<" and ">", and any spaces within link tags (<a>) with " ".
+Most errors from the library are unfortunately extremely vague. We have found that all the ones that we have found are fixed by following the advice above, as well as double checking the work that you did in Step 10 in adding the fonts. To troubleshoot further, we have found that, though slow, the best method to find what HTML is causing the error is to start with small amounts of HTML and then slowly add more in until you see where the problem is. For example, we would start with the following HTML string:
+ + +let html = `<html lang="en"> + <head> + <title>${title}</title> + <meta name="subject" content="Subject"></meta> + <meta name="description" content="Description"></meta> + <meta name="author" content="Author"></meta> + <style>${yourCSS}</style> + </head> + <body></body></html>`; + +And then slowly add body elements until we found one that caused the bug. So, for the next test we would add in the header to our report, then the table, then the column headers, then a single row, etc., until we isolated where the error was coming from. For us, this was usually a tag that was not able to be parsed by the library, so we would either remove this tag if it was not necessary or find a way to express the information using other tags if it needed to be included.
+ + +Vishnu Ramchandani, an experienced screen reader user, was kind enough to help us test the PDFs that we generated with Open HTML to PDF. Vishnu confirmed that the generated PDFs were accessible for screen reader users but provided valuable UX feedback to improve the user experience. Below is his feedback and the actions we took to address the issues:
+ +- Originally, Enable started out as a small personal website to help me show other developers how accessible code is - structured. - Some of the solutions are my own, and some I have borrowed from others (because why reinvent the wheel, especially when - you have already learned from the best?) + Originally, Enable started out as a small personal website made by Zoltan Hawryluk to show other developers how to make web-related code accessible to people with disabalities. Some of the solutions were his own, and some he have borrowed, with citations, from others. Zoltan is still the lead developer of this project and often contributes his own code to it, as well as reviewing all contributions from the community.
+Today, Enable is now sponsored by The Publicis Sapient Accessibility Center of Excellence, to help not only its developers make accessible things, but also to give back to the accessibility and front-end web development communities. It contains contributions from developers within Publicis Sapient as well as others, and we hope to continue to grow this ongoing collaboration.
+- What follows are not just acknowledgements to existing accessible code examples used in Enable, but also to other code I have + What follows are not just acknowledgements to existing accessible code examples used in Enable, but also to other code we have built on that I have accessibility features to.
- The following people have contributed directly to the Enable project by adding code/content via pull requests. + The following people have contributed directly to the Enable project by adding code/content via pull requests or by direct collaboration.
accessibility.getAlTabbableEls()
is based on.This is an example of a image gallery where there are captions underneath the image. Note that in some image galleries on the web, captions don't always describe what is inside the image completely and may just have some additional information (a great example of this is Instagram). For this reason, it is usually a great idea for screen readers to announce both the image alt text and the caption together when the user elects to display a new image in the gallery.
+ true]); ?> + false]); ?> + true]); ?> + +A space for developers to learn and collaborate on making the web accessible.
+A space for developers to learn and collaborate on making the web accessible.
+ +
+ Proudly sponsored by The
+
+
+