Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to the documentation of the Gutenberg project!

The home of the Gutenberg project is its GitHub repository: KSIUJ/gutenberg. It hosts the project's source code, the source of this book and the list of issues and planned features.

Book structure

This book is divided into two sections Admin documentation and Internals.

The Admin documentation section is intended for system administrators who manage a self-hosted Gutenberg instance.

The API documentation section documents Gutenberg's REST API and Internet Printing Protocol (IPP) implementation.

The Internals section is mainly intended for Gutenberg's contributors. It explains the codebase structure, describes the design choices and their rationale. It also describes the suggested implementation and design considerations for planned features.

Contributing to this book

This source of this book is in the same GitHub repository as Gutenberg's code, in the docs directory. The book has been created using mdBook. These mdBook plugins are also installed:

To contribute, fork this repository and create a pull request with your changes.

Instance setup

note

This document is incomplete

Requirements

  • Printer: make printing available for server network

  • Linux server: install drivers, configure CUPS

  • Linux server: test lp command

  • Check if you have the following commands available:

    • libreoffice for printing .docx and .odt. A no-GUI version is enough,
    • convert (package imagemagick) for printing image files,
    • gs (package ghostscript),
    • bbwrap (package bubblewrap).

    To install them:

    • Debian/Ubuntu: sudo apt install libreoffice-core-nogui libreoffice-writer-nogui imagemagick ghostscript bubblewrap
    • Arch Linux: sudo pacman -S libreoffice-still imagemagick ghostscript bubblewrap
  • Gutenberg uses uv as the Python project manager. See https://docs.astral.sh/uv/getting-started/installation/ for installation instructions.

  • You will also need to have yarn or npm to build the web interface.

Setting up the app

First, set the temporary GUTENBERG_ENV environment variable to one of these two values:

export GUTENBERG_ENV=local # local development
export GUTENBERG_ENV=production # production settings

And, if you haven't done it yet, set your $EDITOR variable:

export EDITOR=vim # flamewar starting in 3, 2, 1...

Now, execute the following commands:

export DJANGO_SETTINGS_MODULE=gutenberg.settings.${GUTENBERG_ENV}_settings
git clone https://github.com/KSIUJ/gutenberg.git
cd gutenberg

# Setup the Python virtual environment in .venv and install required packages.
# uv will also download the correct Python version based on pyproject.toml,
# if the version installed on your machine is different.
cd backend
uv sync
cd ../

cd backend
cp ${GUTENBERG_ENV}_settings.py.example ${GUTENBERG_ENV}_settings.py
$EDITOR ${GUTENBERG_ENV}_settings.py # edit the values appropriately
cd ../

# Build the webapp
cd webapp
pnpm install
pnpm run build
cd ../

# Execute all Python commands through uv
cd backend
uv run manage.py migrate
uv run manage.py runserver 0.0.0.0:11111
cd ../

# visit localhost:11111 and check if everything works

While developing the webapp, you can start the Nuxt development server after starting the Django development server:

export GUTENBERG_DEV_DJANGO_URL=http://localhost:11111/
cd webapp
pnpm run dev

You can now access the webapp at http://localhost:3000/. API endpoints are proxied by the Nuxt dev server (based on the GUTENBERG_DEV_DJANGO_URL env variable).

You will also need to start at least one worker. In the main directory after activating the virtual environment:

cd backend
uv run celery -A gutenberg worker -B -l INFO

For proper deployment (instead of uv run manage.py runserver), see the uWSGI documentation.

IPP features might not work with runserver- proper front webserver is required (works with eg. nginx + uwsgi). This is due to an error in Django (or one of its dependencies) - the Expect: 100-continue HTTP header is not handled properly by the development server (IPP standard requires it).

Please remember to add both uwsgi (or your server of choice) AND celery worker (including celery beat) to systemd (or the init server you use).

Exemplary production configs for systemd, uwsgi and nginx setup are available in the /examples/ directory.

Printer management

This document is intended for Gutenberg instance admins; it explains how to configure printers and manage printing permissions.

note

This document is incomplete

Admin interface

Management actions are performed in the Django admin interface, which can be accessed by appending /admin/ to the instance URL.

The simplest way to access it is to create a superuser account:

uv run manage.py createsuperuser

Printer list

A new printer can be created in the Control > Printers section of the admin interface.

Adding a printer via CUPS

CUPS is the standard printing system on Linux operating systems. CUPS provides a web interface for managing printers, which can be accessed at http://localhost:631/ on the server where the Gutenberg Celery worker is running.

When adding a printer a CUPS, the Printer type field should be set to local cups.

You can use the web interface or the command below to find the list of available printers:

lpstat -v

Use the name (not the URL) from the output of the command above as the value of the Cups printer name field.

Most other fields are optional.

Managing printing permissions

Only users who are in a group listed in the Printer permissions list can access the printer.

important

This restriction also applies to superuser accounts.

OpenID Connect

Gutenberg supports Single Sign-On with OpenID Connect using an OpenID Connect client library developed at KSI: https://github.com/KSIUJ/ksi-oidc-python.

Check out the README of ksi-oidc-django for more information.

The library was tested with Keycloak, other OpenID Connect providers should work as well, but the role sync features might not work.

Configuration steps

To configure OpenID Connect within Gutenberg, you need to:

Django settings

  1. Set the value of OIDC_APP_BASE_URL in the appropriate settings file (likely local_setting.py, production_settings.py or docker_settings.py) to the public URL of your Gutenberg instance. This URL will be used by the OpenID Connect provider to communicate with Gutenberg's auth backend and to redirect users to after authentication.

  2. Set the values of OIDC_SYNC_ROLES_AS_GROUPS, OIDC_STAFF_ROLE and OIDC_SUPERUSER_ROLE.

    The recommended way to do this is to enable OIDC_SYNC_ROLES_AS_GROUPS - the group synchronization will be useful for specifying printing permissions.

    For OIDC_STAFF_ROLE and OIDC_SUPERUSER_ROLE, the default configuration is:

    OIDC_STAFF_ROLE = ('client', 'gutenberg-staff')
    OIDC_SUPERUSER_ROLE = ('client', 'gutenberg-superuser')
    

    This will let the users with the Keycloak client role gutenberg-staff access the admin panel and with the role gutenberg-superuser get all Django permissions.

  3. Add kdi_oidc_django.backends.OidcAuthBackend to AUTHENTICATION_BACKENDS. This will enable signing in with OpenID Connect and replace the default login view with a redirect to the OpenID Connect provider.

Creating a client in the OpenID Connect provider

ksi-oidc-django supports OpenID Connect Dynamic Client Registration, so only minimal configuration in the OpenID Provider is needed.

Dynamic client registration (also for existing clients)

To use it, first run the command:

uv run manage.py oidc_set_issuer

It will display a prompt for the issuer URI. When using Keycloak, the issuer URI is https://<keycloak-host>/realms/<realm-name>.

The oidc_set_issuer command will verify that the provided URL is correct and accessible.

If your OpenID Connect Provider supports dynamic registration, use the oidc_init_dynamic command to create the client:

uv run manage.py oidc_init_dynamic

This command can be used both for new and existing clients. When creating a new client with Keycloak, you need to create an Initial access token in the client list of the appropriate realm.

The command will use this token to register a new client with the redirect URIs, name, logo and other setting configured for your deployment.

After updating Gutenberg or changing its Django settings, run the command below to update the client configuration in the OpenID Connect Provider. You should also run this command after configuring dynamic registration for an existing client.

uv run manage.py oidc_update_config

The issuer URI, Client ID, Client Secret, registration access token and configuration URI are stored in the default database configured in Django. You can see and verify the stored setting using the command:

uv run manage.py oidc_info

Static client configuration

If you are unable to use dynamic client registration, ksi-oidc-django also allows to set the Client ID and Client Secret manually. Use the oidc_init_static command for this:

uv run manage.py oidc_init_static

You need to specify the Valid redirect URIs and Valid post logout redirect URIs settings manually. Assuming https://myapp.com/ is the public URL of your Gutenberg instance (the value of the OIDC_APP_BASE_URL setting):

  • Add https://myapp.com/oidc/callback/ to Valid redirect URIs.
  • Add https://myapp.com/ to Valid post logout redirect URIs.

Creating client roles

If you've configured role sync like described above in the Django settings section, you should also create the gutenberg-staff and gutenberg-superuser client roles in the Keycloak settings for your client.

If you have an admin realm role in your Keycloak realm, you can add the gutenberg-staff and gutenberg-superuser roles in the Associated roles section of the realm role settings, so that all admins automatically inherit these roles.

Deploying a Gutenberg instance with Docker

The Gutenberg project provides a Docker configuration to simplify the deployment on your server.

Dockerfile modifications

We intend to publish container images based on the Dockerfile in the future. If your use-case requires modifications to the Dockerfile we encourage you to create an issue in the Gutenberg's GitHub issue tracker. This way we can ensure that the published images are suitable for customized setups.

Required images

Gutenberg requires configuring a PostgreSQL database, a Redis instance, and a CUPS server. All of them need to be accessible from the Django server and from the Celery worker. They can be deployed as Docker containers or as standalone instances. On top of that, three more containers are required to run Gutenberg: the Django backend server, the Celery worker for executing background tasks, and the Nginx proxy that routes incoming HTTP requests and serves static files.

In summary:

Container nameDescription
gutenberg-dbPostgreSQL database for storing application data
gutenberg-redisRedis instance for caching and task queue
gutenberg-backendDjango application server
gutenberg-celeryCelery worker for background tasks
gutenberg-proxyNginx for routing requests

Configuration

To run Gutenberg in Docker, you need to create your own version of the settings:

  cp backend/gutenberg/settings/docker_settings.py.example backend/gutenberg/settings/docker_settings.py

In docker_settings.py, fill in the following fields properly:

  • SECRET_KEY - unique random string
  • ALLOWED_HOSTS - list of hosts that can connect to the app
  • CSRF_TRUSTED_ORIGINS - list of trusted origins for CSRF protection

For example:

SECRET_KEY = 'n7+3u12_59wy_kzvecb^w^jrpi(m#(gl8^qe92kvclkd9!=-h)'
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
CSRF_TRUSTED_ORIGINS = [
    'http://127.0.0.1:3000',
    'http://localhost:3000',
]

After saving the file, you can run all the containers with:

docker compose up --build	

docker-compose.yml

The docker-compose.yml file provides an example Docker Compose configuration, which references the local Dockerfile to build the required Docker images. You might need to modify it to fit your deployment.

Two secrets need to be provided for Docker Compose: gutenberg_postgres_password and gutenberg_django_secret_key. They should be randomly generated strings and should be kept secret. Please make sure to never commit them in a Git repository. The openssl command can be used to generate the secrets:

# Create a secrets directory with a `.gitignore` file
mkdir -p secrets
printf "# Avoid publishing any secrets stored in this folder\n*\n" > secrets/.gitignore

# Generate the secrets
openssl rand -base64 32 > ./secrets/postgres_password.txt
openssl rand -base64 32 > ./secrets/django_secret_key.txt

Creating a superuser account

After starting all Docker containers, the command below can be used to create a superuser account. gutenberg-backend is the name of the container running the Django server.

docker exec -it gutenberg-backend ./manage.py createsuperuser

NGINX config files

The run_nginx target describes an NGINX Docker image with configuration required for running Gutenberg itself. The default configuration file for NGINX, /etc/nginx/nginx.conf contains an include directive:

http {
    # ...
    include /etc/nginx/conf.d/*.conf```
    #...
}

Gutenberg adds a single file in the conf.d directory: /etc/nginx/conf.d/gutenberg.conf. It defines an HTTP server which contains another include directive:

server {
    # ...
    include /etc/nginx/gutenberg-locations.d/*.conf;
    # ...
}

The files in the gutenberg-locations.d define location directives for different endpoints which will be available under the Gutenberg domain.

Gutenberg adds two files to this folder:

  • gutenberg-app.conf which defines the handlers for the endpoints /static/, /@webapp-html/ for internal use and a catch-all location / directive which proxies all requests to the Django application server.
  • gutenberg-docs.conf which defines the handlers for the /docs/ endpoint which serves the mdbook documentation.

Extending the NGINX configuration

You can make use of the include directives described above to extend Gutenberg's default NGINX image with your own config.

As an example, this is how you would add a custom /myapp/ endpoint proxied to https://example.com/myapp/:

Create a new file myapp.conf with the contents:

location /myapp/ {
    proxy_pass https://example.com/myapp/;
}

And your own Dockerfile with:

# Put the name Gutenberg's default NGINX image here:
FROM run_nginx

COPY path/to/myapp.conf /etc/nginx/gutenberg-locations.d/myapp.conf

Configuring CUPS access

The CUPS_SERVERNAME setting controls how Gutenberg connects to CUPS. It can be a path to a CUPS socket file, an IP address, or a hostname. It will be used as the -f argument to commands provided by cups-client (lp, cancel, etc.).

To use the CUPS server running on the host machine, you can mount the /run/cups directory from the host machine to the Docker containers for the backend and the Celery worker. The example docker-compose.yml file does this. The CUPS_SERVERNAME can then be set to /run/cups/cups.sock.

warning

Docker Desktop might not allow mounting any files from the /run directory, even if it is listed in Resources > File sharing > Virtual file shares. Failing to bind the /run/cups/cups.sock socket will not result in an error, Docker will silently create a new directory in that path.

This issue might be hard to overcome when using Docker Desktop, so we recommend installing the Docker engine directly.

CUPS performs permission checking when accessing CUPS via the socket file. It requires the name and UID of the system user calling the lp command in the Docker container to match a user on the host machine. The run_backend and run_celery targets in the Dockerfile use the GUTENBERG_USERNAME, GUTENBERG_UID, and GUTENBERG_GID environment variables to set the username, UID, and GID of the user in the Docker container. If they are not specified, the default username gutenberg-docker is used and the UID and GID are set to 659.

The same group and user need to be created on the host machine. This can be achieved using the commands:

sudo groupadd --system --gid 659 gutenberg-docker
sudo useradd --system --groups lp,lpadmin gutenberg-docker --uid 659 --gid 659

tip

If the Gutenberg user is not configured correctly, attempting to print a document might result in an error like:

lp: Unauthorized

In such cases it can be helpful to inspect the host's CUPS server error logs:

less +G /var/log/cups/error_log

IPP and REST API overview

Gutenberg provides two APIs for interacting with it:

See their documentation pages for more details.

The REST API is intended for use in the webapp (UI) component of Gutenberg. In the future a token-based authentication scheme might be implemented for use by other API clients.

Most existing REST API endpoints map to corresponding IPP operations and have similar semantics. This design reduces code duplication in the IPP and REST API modules. The page IPP and REST API comparison contains a table which lists the matching REST API endpoints and IPP operations.

Standard sequences of operations for printing documents

Printing single-document print jobs in a single request

When a print job consists of only a single document, both the REST API and IPP provide a simple way to select print job attributes and upload the file in a single request. In IPP the Print-Job operation is used for this, the REST API endpoint is POST /api/jobs/submit/. The print job is started immediately after the upload is complete.

Printing multi-document print jobs

  1. A new, empty print job is created using the Create-Job IPP operation or the POST /api/jobs/create_job/ endpoint. The print job attributes are supplied in this request, and they are used to print all the files. The server's response includes the job id, which is used for subsequent requests.
  2. Documents are uploaded sequentially using the Send-Document operation or the POST /api/jobs/:id/upload_artefact/ endpoint.
  3. To complete the print job and to enqueue it, the client must do one of the following:
    • Set the last-document (IPP) or last (REST API) flag in the last artifact upload request in the previous step.
    • IPP only: execute an additional Send-Document operation with last-document set to true and no document data in the request body.
    • Execute the Close-Job operation or make a request to the POST /api/jobs/:id/run_job/ endpoint.

Internet Printing Protocol (IPP)

The Internet Printing Protocol is an extensible protocol maintained by the Printer Working Group. Check the IPP Guide for an overview of IPP.

Gutenberg implements an IPP server. The IPP operations are not proxied directly to the physical printer but are handled by Gutenberg. Gutenberg verifies printing permissions, manages accounting (print stats and quotas) and processes the supplied documents. This has the implications of:

  1. Gutenberg might support some IPP operations, attributes or formats that the physical printer does not. (E.g., it might support submitting .docx files, even if the physical printer can only accept PDFs. In this case Gutenberg will convert the document to PDF, which it will then send to the printer).
  2. Some operations, attributes or formats supported by the physical printer might not be supported when printing via Gutenberg. (E.g., the printer might support stapling the media sheets after a print job is complete, but there is no way to use this feature via Gutenberg).

Supported IPP standards and versions

note

This section is incomplete

Supported IPP operations

note

This section is incomplete

Supported job attributes

note

This section is incomplete

Supported file formats

note

This section is incomplete

IPP endpoint and authentication

note

This section is incomplete

REST API

Gutenberg implements a REST API using Django REST framework. The endpoint for the REST API is <GUTENBERG_INSTANCE_URL>/api/.

You can explore the API by browsing it. DRF generates interactive HTML views for all routes.

note

The auto-generated documentation is currently incomplete and in some cases displays incorrect schemas.

Authentication

The REST API supports only cookie-based session authentication. This makes it unsuitable for uses other than the Gutenberg's webapp. Support for other authentication schemes might be added in the future.

IPP and REST API comparison

This table lists the REST API endpoints and implemented IPP operations, which have similar semantics.

REST API endpointIPP operationNotes
GET /api/printers/not applicableIn Gutenberg IPP itself is not used for printer discovery. An ipp: (or ipps:) URI is specific to one printer. More recent updates to IPP add support for the output-device attribute, but it's not used in Gutenberg.
GET /api/printers/:printer_id/Get-Printer-Attributes (RFC 8011)
nonePrint-Job (RFC 8011)
GET /api/jobs/Get-Jobs (RFC 8011)
GET /api/jobs/:id/Get-Job-Attributes (RFC 8011)
POST /api/jobs/:id/cancel/Cancel-Job (RFC 8011)
POST /api/jobs/create_job/Create-Job (RFC 8011)
POST /api/jobs/:id/upload_artefact/Send-Document (RFC 8011)
POST /api/jobs/:id/run_job/Close-Job (PWG 5100.7-2023)This endpoint also maps to the Send-Document operation, but with no file provided. This used to be the standard way of finishing a job in IPP v1.1 when the document count is not known in advance.
POST /api/jobs/:id/change_properties/Set-Job-Attributes (RFC 3380)
POST /api/jobs/:id/change_artefact_order/not directly mapped
DELETE /api/jobs/:id/delete_artefact/not directly mapped
GET /api/jobs/:id/validate_properties/Validate-Job (RFC 8011)
GET /api/jobs/:id/artefacts/Get-Documents
noneIdentify-Printer (PWG 5100.13)Implemented as a no-op.

UX and UI design goals

Goals

Printing and config

  • Simple printing should be as easy as possible. File upload should be possible from the main page.
  • There should be an indication of what file types are allowed.
  • The printing logic should be ready for manual duplex printing, which involves a two-stage printing process.
  • The UI should support settings specific for a given format.
  • If printing multiple files at once is possible, it should be possible to edit some settings separately for each file.
  • The UI should not be overwhelming, irrelevant settings should be hidden.
  • If possible, settings should include a visual explanation. If the preview option is good enough, this might not be necessary.

Preview

  • The user should be able to tell exactly what the printed pages will look like.
  • It should be easy to tell the order of the resulting pages and the backsides of the pages. For duplex printing, the user should be able to clearly see the difference between the "Two-sided (long edge)" and "Two-sided (short edge)" options.
  • The user should never see a not up-to-date preview.

IPP

  • The IPP feature should be easily discoverable from the main page.
  • This feature should be easy to understand for non-technical users.
  • If there are documents in the user's print queue, an indicator for that should be visible on all/most pages.
  • It should be possible to cancel a print job from the print queue.

General UI

  • The page should be responsive.
    • The UX on mobile and desktop can be different. For example, some options that are always visible on desktop can on mobile appear only after uploading the first file.
  • A button with the text Print should only be used to start the print job.

UI

Main page

On desktop, the main page will use a two-column layout with a header: The header contains the logo and a user menu.

The left column contains only a card with file upload elements and configuration options. The card will be prominent.

The right column will describe other ways to print: IPP and the REST API. It could also describe what Gutenberg is and how to use to self-host it.

After uploading files, the user should be presented with the buttons Preview and Print. Clicking Preview takes the user to the preview mode:

On desktop the upload + config card will remain visible and will be moved to the left screen edge, the right side will transform to show the print preview.

On mobile the preview will take the whole screen, there should be a button to close it and go back to modify the print config.

The preview updates automatically after the user changes the configuration. While a new preview is loading, the previous preview is grayed out.

The preview can have multiple display modes:

2D mode

This view shows all the pages in a reasonable order. It should be decided if this order respects the "Reverse order" setting. The user can change the preview orientation (independently of the print settings).

If duplex printing is enabled, the preview shows the backsides next to the front sides. The front pages are always on the left, the back pages on the right. The back page should bo oriented as if the page was flipped along the common, vertical edge (in the selected preview orientation) between the front and back pages. In particular this means that if the pages are displayed in landscape mode, plus the "Two-sided (long edge)" option is selected, or portrait mode + "Two-sided (short edge)", the right page is rotated 180 degrees.

The rationale for this is that if the user selects the display orientation that places the front page upright, they will be able to see if the back page is upside down.

3D mode

This view shows the pages in an isometric 3D view as if they were printed on paper and stacked. The order should match the order in the stack of printed pages. This requires admin configuration. The backsides are revealed by a 3D rotation on hover.

Planned API extensions

The first two extensions are designed with the #63 Printing Previews feature in mind. The previews will be generated on the server (likely in a Celery worker), not on the client device. This requires uploading documents before the request to start the print job is made. As the user might want to modify the print settings after generating a preview, new endpoints and operations for modifying a print job are needed. Without them, the client would have to create new jobs for each preview, which would require reuploading the documents each time.

important

The URLs for the endpoints below are not final, defaults generated by the Django REST Framework's ViewSet feature should be used where possible.

Job modifications

The goal of this extension is to allow the client to perform the following actions on a job:

  1. Modify job attributes
  2. Get the document (file) list
  3. Delete an uploaded document
  4. Change the document print order

The first two actions can be implemented using standard IPP operations.

Modify job attributes

RFC 3380 defines the Set-Job-Attributes IPP operation, which does exactly what we need for this action.

The REST API endpoint for this action could be PATCH /api/jobs/:id/. Alternatively a POST action could be added. While a PATCH request is not required to be idempotent, our implementation of this endpoint probably should be. See the MDN docs for the PATCH request method.

The IPP operation must be atomic, which means it must either change all requested attributes or none. The REST API endpoint should also be atomic.

The IPP operation is also sparse, meaning the client only has to provide the attributes which it wishes to change. This REST API endpoint should also allow sparse updates so that the addition of a new attribute is not a breaking change.

Unsupported job configuration handling

The IPP operations Print-Job, Validate-Job, Create-Job and Set-Job-Attributes should verify if the selected print configuration is valid and reject the operation if it is not (e.g., if some pair of provided attribute values is conflicting).

The same behavior might not be the optimal solution for the REST API. When the user is modifying the print configuration, it is desirable to store it on the server after each change in the UI (with proper throttling/debouncing on the webapp side). This way refreshing the page will not cause data loss, as the web app can retrieve the stored configuration after the reload.

The suggested behavior in this case is to allow setting syntactically valid attributes that result in a configuration not supported by the selected printer in requests to POST /api/jobs/create_job/ and PATCH /api/jobs/:id/. If the selected job configuration is invalid, the responses to these requests should indicate operation success (a 2xx status code) but should include a errors field in the response body, indicating the errors in the selected configuration. A human-readable warning message should be included for displaying in the webapp UI. The output could also include the warnings in a structured form, just like it would be returned from the IPP operations.

The server should return a failure response for calls to POST /api/jobs/submit/ and POST /api/jobs/:id/run_job/ if the current job configuration is invalid. The same validation should also happen when executing IPP operations which start the print job (Print-Job, Send-Document with last-document set to true and Close-Job) and the Validate-Job operation, as the configuration might be invalid if it has been created via IPP but modified using the REST API.

The list of errors could also be retrievable using a new endpoint or could be included in the response to calls to the GET /api/job/:id/ endpoint.

Get the document list

The IPP operations suitable for this action are Get-Documents and Get-Document-Attributes.

In the REST API the file list could be returned either in the response from GET /api/job/:id/ or using a new ViewSet endpoint supporting the requests to GET /api/job/:id/documents/ and GET /api/job/:id/documents/:doc_id/. The ViewSet solution follows the convention of providing a 1 to 1 mapping of the REST API endpoints to IPP operations.

Delete document

A simple DELETE /api/job/:id/documents/:doc_id/ endpoint could accomplish this action in the REST API.

There is no IPP operation suitable to accomplish this action:

  • The Delete-Document action defined in the PWG 5100.5-2003 Standard for IPP Document Object standard has since been obsoleted and must not be implemented. Even if it wasn't, it could not be used for this purpose, as it can only be used by the printer's operators and administrators, not end-users.
  • The Cancel-Document operation has undesirable semantics. If the Resubmit-Job operation or another way to rerun jobs is implemented, the previously canceled documents should get printed again.

As such, this endpoint should not be marked as mapping to Delete-Document or Cancel-Document.

Modify document order

For this action a POST /api/job/:id/documents/reorder endpoint could be provided accepting the new document print order in the request body, for example:

{
  "order": ["B", "A", "C"]
}

The server should verify that all documents are included exactly once.

There does not exist a standard IPP operation for this action.

Printing previews in the REST API

This feature is described in PR #63.

The preview request generates (low-quality) images for each page in the PDF, and any additional metadata needed to display the preview. Techniques like CSS Sprites could be used to load all images in a single request.

As both the print and preview requests create the same PDF file, job-scoped caching could be used to avoid generating the same PDF multiple times if the settings and input files have not changed.

Please note that the preprocessing job might take a while to complete, and it is done asynchronously in a Celery worker. The API design should account for this:

  • when a new preview is requested, the previous request generation celery task should probably be canceled.
  • the behavior when a print is requested while the preview is being generated should be specified.
  • there needs to be a way for the server to notify the client that the preview is ready. Long-running HTTP requests or server-side events could be used. Consider separating the endpoints for a preview request and preview image retrieval.

Printing previews via IPP

See PR #69.

Per-document print attribute overrides

This feature is described in PR #94.

This could be an opportunity to implement the PWG 5100.5-2024 – IPP Document Object v1.2 standard.

Issues with the IPP operations for document management actions

The Get-Documents, Get-Document-Attributes map directly to the GET /api/job/:id/documents/ and GET /api/job/:id/documents/:doc_id/ operations described in the Job modifications section above.

This IPP standard uses sequential document numbers for identifying the documents. The obsoleted Delete-Document operation creates a gap in the numbering. This numbering also represents the order in which the documents will be processed. This presents an issue for the modify document order action, as the document numbers used by IPP will need to be changed after modifying the document order.

One possible solution to this issue would be to assign new numbers to all the documents if the order gets changed. For example, assume there are three documents A, B, C initially in this order identified in IPP by the numbers: A:1, B:2, C:3. When the REST API client changes the order to C, B, A, these documents will be assigned the numbers C:4, D:5, E:6. Since in this scenario the number of a document can change, the REST API should use different, persistent document IDs.

The Cancel-Document operation required by this standard is semantically different from the proposed document delete endpoint. A canceled document should remain in the document list and will get printed if the job is resubmitted. The webapp should display this canceled status in the job file list.

Web app overview

The web app is created using:

  • Nuxt 4
  • PrimeVue
  • TailwindCSS v4

Static site generation

The web app is a single-page application (SPA). Although Nuxt comes with its own web server with server-side rendering support, Gutenberg does not use it. The app is rendered during the build step using the static preset of Nitro (the server component of Nuxt) with some modifications. The build command is:

cd webapp && pnpm run build

The result of the build is placed in the .output directory. Inside it, there are two important directories:

  • html contains the HTML files 200.html and 404.html. As the time of writing this document, the 404.html file is not used anywhere.
  • public contains other static files necessary for running the app: JavaScript, CSS, image and JSON files.

The public directory is designed to be served by Django or NGINX directly under yoursite.example.com/static/. It should be included in the STATICFILES_DIRS in Django settings. Please note that the files from .output/public are not the only files that need to be served under the /static/ endpoint, as other Django apps (like Django REST Framework) include their own static files.

When the DEBUG setting of Django is True, the command manage.py runserver serves all files from the directories specified in STATICFILES_DIRS, including the .output/public folder generated by Nuxt 4 and in other app directories (see also STATICFILES_FINDERS).

When it's set to False, Django does not serve the static files; a separate web server like NGINX is required. To gather all the static files, the Django command manage.py collectstatic is used. It copies all static files from STATICFILES_DIRS and installed Django apps to the directory specified by the STATIC_ROOT Django setting. It's this static root directory that should be served using NGINX under the /static/ endpoint.

The html is not included in the static files, because they should not be accessed under the /static/ endpoint. Django will serve them directly from the .output/html directory (specified in the custom GUTENBERG_SPA_HTML_DIR Django setting). See the file backend/printing/urls.py (included from backend/gutenberg/urls.py) for the details on how Django serves these files. Django will serve the 200.html file for any webapp request.

PR #91 introduces a new NGINX_ACCEL_ENABLED setting, which moves the responsibility of serving the HTML files also to NGINX.

The generation of the html files is altered in the prerender:generate Nitro hook in webapp/nuxt.config.js. It disables the rendering of index.html and changes the output directory for HTML files to .output/html.

Routing

The web app uses Vue Router for navigation. Since the same 200.html file is served for all routes (yoursite.example.com/, yoursite.example.com/print/setup-ipp/, etc.), Vue Router reads the URL from the window.location.pathname and determines which page to render based on that. Navigation between different web app routes is done internally by Vue Router using the History API, which modifies the URL in the browser's address bar but does not reload the page.

important

An exception to that is the /login/ route. Depending on the configuration, Django might either serve the 200.html file to render the web app login form or redirect to an Open ID Connect authentication URL. For this reason all links to the /login/ route should use standard HTML <a> links, not Vue Router links.

Trailing slashes

Django automatically redirects all requests without a trailing slash to the corresponding URL with a trailing slash. For example, a user trying to access yoursite.example.com/print/setup-ipp will be redirected to yoursite.example.com/print/setup-ipp/.

The default Vue Router configuration generated by Nuxt prefers routes without a trailing slash, so a custom pages:extend hook in webapp/nuxt.config.js changes the route matching logic to only handle routes with a trailing slash. Due to this, internal navigation to yoursite.example.com/print/setup-ipp will show a Vue Router "404" page, so care must be taken to append trailing slashes in Vue Router links. If a user tried to access yoursite.example.com/print/setup-ipp from the address bar, Django will redirect them to the correct URL (with a trailing slash) before serving the web app.

Authentication

Some web app routes are publicly accessible, while others require authentication. The login route can only be accessed by unauthenticated users.

Two mechanisms are used for automatic redirection if the user cannot access a route:

  • In Django the backend/printing/urls.py specifies the view function to use for a given route:
    • webapp_public for public routes;
    • webapp_require_auth for routes that require authentication;
    • login for the login route.
  • In Nuxt the login route uses a custom middleware and authenticated routes use a shared require-auth middleware. The middleware used for a given route is specified in the definePageMeta composable.

The Django views and Nuxt middlewares have the same job of redirecting:

  • unauthenticated users trying to access a page that requires authentication: to the login page with the next search query parameter pointing to the route which they tried to access;
  • authenticated users who are trying to access the /login/ page: to the route specified in the next parameter.

Django handles the redirection if the user makes a request to the server to access a page, Nuxt middleware is triggered when users are using Vue Router internal navigation.

important

When adding or modifying routes, care should be taken to keep the Django and Nuxt authentication logic in sync.

The auth Nuxt plugin

A custom webapp/app/plugins/auth.ts plugin handles the authentication logic in the web app. The plugin uses the async setup syntax, so no components will be rendered until the auth state is loaded from the API.

For almost all requests made to the API using the custom webapp/app/plugins/api.ts plugin, an onResponseError hook is installed to handle the 401/403 responses from the API. If an API call fails because the user is not authenticated according to the API, but the user was authenticated in the app, they are immediately redirected to the login page (with the next query parameter set to the path of the route which they tried to access and an expired flag to display a "session expired" message on the login screen).

Page layouts

A few components were created for standard page layouts used in the web app. The layouts are plain components, not Nuxt layouts. Future updates might change that.

The standard layouts are sidebar-layout.vue and single-column-layout.vue. Only the app-panel.vue and app-content.vue components should be used as their direct children. This design keeps the logic for handling different screen sizes centralized in these components.

PrimeVue components

Currently, all the PrimeVue components used in the app need to be specified in webapp/nuxt.config.js. A comment in that file explains why.

Development server

cd webapp && pnpm run dev # (runs "nuxt dev")

This script starts a Nuxt development server with live updates and integrated Nuxt devtools. The server is configured to proxy API endpoints to the Django development server (the host of the Django dev server is specified using the GUTENBERG_DEV_DJANGO_URL env variable, or http://localhost:11111/ if unspecified). The proxied endpoints are specified in webapp/nuxt.config.js.

warning

This dev proxy has some issues — for example, the redirect to the logout page makes Nuxt display a 404 page until the site is reloaded.

The Nuxt server also watches for changes in the code and generates type definitions in the .nuxt directory. Without the dev server running, the type definitions might not be up to date, and your IDE might not resolve automatic imports correctly.

Document processing

note

This document describes how this feature will eventually work. Some features are not yet implemented and the design might change.

Document processing steps and relevant IPP Job attributes

  1. Preprocess the documents:
    • Optionally convert to PDF with a page size taken from the document data
    • Determine the orientation of the document data
  2. Determine the Input Page size based on the available Media Sheet size and the settings:
  3. Place the preprocessed data on the Input Pages based on the determined Input Page size:
  4. Filter pages and add required blank pages to the Input Pages:
  5. Place the Input Pages on the Final Pages and convert to a standard PDF version:
  6. Place the Final Pages on the Media Sheet pages (rotate if necessary):

Other relevant IPP Job attributes:

Borderless printing:

Other:

Determining input page size and final layout

Final page orientation is the same as orientation-requested if number-up is 1, 4, 16, etc. and is rotated by 90 degrees clockwise if number-up is 2, 8 etc.

Docker configuration

Dockerfile overview

The Dockerfile defines three final and some intermediate targets. The final targets are run_nginx, run_backend and run_celery. All build targets are described in comments in the Dockerfile itself. The graph below visualizes the build-time layer dependencies.

The images are based on Alpine Linux or on Debian. The variables DEBIAN_VER and ALPINE_VER are used to select versions of the base images. The same versions should albo be used when specifying image versions in docker-compose.yml. Using common versions let's Docker reduce the disk space used by the images.

Targets in Dockerfile

  • A solid line from a to b represents the FROM a AS b instruction.
  • A dashed line from a to b represents COPY --from a as a layer of target b.
flowchart TD
    nginx{{"`_nginx..._
        (alpine)`"}}
    rust{{"`_rust..._
        (alpine)`"}}
    node{{"`_node..._
        (alpine)`"}}
    uv{{"`_uv:python..._
        (debian)`"}}
        
    rust --> build_docs
    
    node --> build_webapp

    uv --> setup_base
    setup_base --> setup_django

    setup_django --> collect_static

    setup_django ---> run_backend([run_backend])

    setup_base ----> run_celery([run_celery])
    setup_django -. copy /app/backend .-> run_celery

    nginx --> run_nginx
    build_docs -. "copy /app/docs/book" ..-> run_nginx([run_nginx])
    build_webapp -. "copy /app/webapp/.output/html" .-> run_nginx([run_nginx])
    build_webapp -. "copy /app/webapp/.output/public" .-> collect_static
    collect_static -. copy /app/staticroot .-> run_nginx([run_nginx])

Django setting modules used by each target

The Django setting files used by Docker are split into several layers to allow the server admins to customize some of the settings. All the settings are part of the backend/gutenberg/settings module.

flowchart LR

base[base.py]
base ---> local(["`local_settings.py
         _(user provided_,
         _not used in Docker)_`"])
base ---> production(["`production_settings.py
         _(user provided_,
         _not used in Docker)_`"])
base --> docker_base[docker_base.py]
docker_base --> docker_settings(["`docker_settings.py
                _(user provided)_`"])
docker_settings --> docker_server_overrides[docker_server_overrides.py]

The base.py, docker_base.py and docker_server_overrides.py are all included in the repository and should not be changed by the admins.

The docker_settings.py file is not stored in the GitHub repo, it should copied by the admin from the docker_settings.py.example file and customized. docker_settings.py is ignored in .dockerignore and is not used in any build steps in the Dockerfile. It should be mounted by the admin at /etc/gutenberg/docker_settings.py and it is symlinked to be accessible from /app/backend/gutenberg/settings/docker_settings.py.

warning

The provided docker_settings.py file should start with the from .docker_base import * line.

Build-time steps (like running the collectstatic command) use docker_base.py as the settings module. Run-time commands (starting the Django server and Celery worker) use docker_server_overrides.py. See the ENV DJANGO_SETTINGS_MODULE= commands in the Dockerfile.

Creating new releases

Updating the changelog during development

The CHANGELOG.md file lists the changes in each release in the keep a changelog style. Please update the CHANGELOG.md file in the same PR, which includes the changes that will be mentioned in the changelog. Changes should be placed in the [Unreleased] section.

Semantic versioning of Gutenberg

This project uses semantic versioning. In particular:

  • The project version has the format major.minor.patch (e.g., 4.1.0), in some places prefixed with v (e.g., in the Git release tags).
  • Release candidate versions have the format major.minor.patch-rcN (e.g., 4.1.0-rc1 is the first candidate for the version 4.1.0).
  • The major version is incremented when the app has changes which require configuration changes or other actions from the system administrator when upgrading. When a new major version is released, the minor and patch versions are reset to 0.
  • The minor version is incremented if the release adds new features which don't break existing configurations. In this case the patch version is reset to 0.
  • The patch version is incremented if the release includes only bug fixes, and the upgrade does not require any actions from the system administrator.

Please pay extra attention to documenting the breaking changes in the changelog. Describe the changes that need to be made in the deployment when upgrading from the previous stable version, consider linking to relevant Gutenberg documentation.

Creating a new release or release candidate

Create a new pull request with the name Release vX.Y.Z-rcN, which contains the following changes:

  • Update the package version in backend/pyproject.toml.
  • Run:
    cd backend && uv sync --upgrade
    
  • In the CHANGELOG.md file:
    • Move the changes from the [Unreleased] section to a new section with the header ## [X.Y.Z] - YYYY-MM-DD or ## [X.Y.Z-rcN] - YYYY-MM-DD [Release candidate] where YYYY-MM-DD is the date of the release.
    • If there already exists a header for a previous release candidate for the new version, update it instead.
    • Add an appropriate URL for the [X.Y.Z] or [X.Y.Z-rcN] link at the end of the file.
    • Update the [unreleased] link at the end of the file to compare only changes made since the release candidate tag.

After merging the pull request, create a new GitHub Release with the tag vX.Y.Z or vX.Y.Z-rcN.