diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..99ef59d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.swp
diff --git a/README.md b/README.md
index 52a8898..6d554dd 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,32 @@
-# gitea_external_renderers
+# External rendering scripts for Gitea
 
-External rendering scripts for various file types
\ No newline at end of file
+These scripts attempt to render several documents so viewing them in Gitea is actually informative. Most are a work in progress. As some files can take a very long time to render, caching is used by leaving the generated images in the Gitea custom assets directory (if present and writable). You'd probably want to clean that out now and then.
+
+List of renderers:
+- LibreOffice
+- KiCad
+
+TODO:
+- 3D models (STL, STEP, etc)
+- Gerber files
+
+## Prerequists
+
+The KiCad renderers assume KiCad (9 or newer) is installed. They also need Inkscape (to properly fit the images). Libreoffice is required for the LO renderers.
+
+The SVG Pan & Zoom library is loaded from a CDN.
+
+`mktemp` is used to create a temporary directory.
+
+## Installation & Configuration
+
+1. Link the scripts in some place Gitea can execute them
+2. Add the content of the `.ini` files to your `app.ini` file
+3. Edit the new script path entries in `app.ini` to match your system
+4. Copy / merge the contents of the `custom` dir with the custom dir in your Gitea root
+
+## Repo and License
+
+Feel free to copy and share this under the terms of the Apache 2.0 license.
+Original author: Stefan Hamminga <stefan@rbts.co>
+Original repo: https://git.rbts.co/rbts.co/gitea_external_renderers
diff --git a/config/kicad_pcb.ini b/config/kicad_pcb.ini
new file mode 100644
index 0000000..2b53b84
--- /dev/null
+++ b/config/kicad_pcb.ini
@@ -0,0 +1,25 @@
+[markup.kicad_pcb]
+ENABLED = true
+FILE_EXTENSIONS = .kicad_pcb,.kicad_pcb.1,.kicad_pcb.2,.kicad_pcb.3,.kicad_pcb.4,.kicad_pcb.5,.kicad_pcb.6,.kicad_pcb.7,.kicad_pcb.8,.kicad_pcb.9
+RENDER_COMMAND = /usr/local/bin/gitea_render_kicad_pcb.sh
+IS_INPUT_FILE = true
+NEED_POSTPROCESS = false
+;RENDER_CONTENT_MODE = no-sanitizer
+
+[markup.sanitizer.kicad_pcb.1]
+ALLOW_DATA_URI_IMAGES = true
+ELEMENT = div
+ALLOW_ATTR = class
+REGEXP =
+
+[markup.sanitizer.kicad_pcb.2]
+ALLOW_DATA_URI_IMAGES = true
+ELEMENT = img
+ALLOW_ATTR = src
+REGEXP =
+
+[markup.sanitizer.kicad_pcb.3]
+ALLOW_DATA_URI_IMAGES = true
+ELEMENT = img
+ALLOW_ATTR = class
+REGEXP =
\ No newline at end of file
diff --git a/config/kicad_sch.ini b/config/kicad_sch.ini
new file mode 100644
index 0000000..018f535
--- /dev/null
+++ b/config/kicad_sch.ini
@@ -0,0 +1,25 @@
+[markup.kicad_sch]
+ENABLED = true
+FILE_EXTENSIONS = .kicad_sch,.kicad_sch.1,.kicad_sch.2,.kicad_sch.3,.kicad_sch.4,.kicad_sch.5,.kicad_sch.6,.kicad_sch.7,.kicad_sch.8,.kicad_sch.9
+RENDER_COMMAND = /usr/local/bin/gitea_render_kicad_sch.sh
+IS_INPUT_FILE = true
+NEED_POSTPROCESS = false
+;RENDER_CONTENT_MODE = no-sanitizer
+
+[markup.sanitizer.kicad_sch.1]
+ALLOW_DATA_URI_IMAGES = true
+ELEMENT = div
+ALLOW_ATTR = class
+REGEXP =
+
+[markup.sanitizer.kicad_sch.2]
+ALLOW_DATA_URI_IMAGES = true
+ELEMENT = img
+ALLOW_ATTR = src
+REGEXP =
+
+[markup.sanitizer.kicad_sch.3]
+ALLOW_DATA_URI_IMAGES = true
+ELEMENT = img
+ALLOW_ATTR = class
+REGEXP =
\ No newline at end of file
diff --git a/config/libreoffice.ini b/config/libreoffice.ini
new file mode 100644
index 0000000..51f96b4
--- /dev/null
+++ b/config/libreoffice.ini
@@ -0,0 +1,23 @@
+[markup.libreoffice]
+ENABLED = true
+FILE_EXTENSIONS = .odt,.ods,.odg,.docx,.ppt
+RENDER_COMMAND = /usr/local/bin/gitea_render_libreoffice.sh
+IS_INPUT_FILE = true
+NEED_POSTPROCESS = false
+
+[markup.sanitizer.libreoffice.1]
+ALLOW_DATA_URI_IMAGES = true
+ELEMENT = img
+ALLOW_ATTR = class
+REGEXP =   
+
+[markup.sanitizer.libreoffice.2]
+ALLOW_DATA_URI_IMAGES = true
+ELEMENT = src
+ALLOW_ATTR = class
+REGEXP =   
+
+[markup.sanitizer.libreoffice.3]
+ELEMENT = div
+ALLOW_ATTR = class
+REGEXP =     
diff --git a/custom/public/assets/css/renderers.css b/custom/public/assets/css/renderers.css
new file mode 100644
index 0000000..7782f81
--- /dev/null
+++ b/custom/public/assets/css/renderers.css
@@ -0,0 +1,20 @@
+.file-view.markup .full_width {
+  width: 100%;
+  display: block;
+}
+
+.file-view.markup.kicad_sch,
+.file-view.markup.kicad_pcb,
+.file-view.markup.libreoffice
+{
+  background-color: rgba(127, 127, 127, 0.05);
+}
+
+.file-view.markup .page {
+  background-color: white;
+  padding: 1em;
+}
+
+.file-view.markup .page + .page {
+  margin-top: 1em;
+}
diff --git a/custom/public/assets/img/.gitkeep b/custom/public/assets/img/.gitkeep
new file mode 100644
index 0000000..473a0f4
diff --git a/custom/templates/custom/footer.tmpl b/custom/templates/custom/footer.tmpl
new file mode 100644
index 0000000..f8ccc5c
--- /dev/null
+++ b/custom/templates/custom/footer.tmpl
@@ -0,0 +1,15 @@
+<script src="https://unpkg.com/@panzoom/panzoom@4.6.0/dist/panzoom.min.js"></script>
+<script>
+  try {
+    document.querySelectorAll('img.pan_zoom').forEach((img) => {
+      const panzoom = Panzoom(img, {
+        maxScale: 5
+      });
+      img.parentElement.addEventListener('wheel', evt => evt.preventDefault());
+      img.parentElement.addEventListener('wheel', panzoom.zoomWithWheel)
+    });
+  } catch (e) {
+    console.error('Error loading Panzoom:', e);
+  }
+</script>
+
diff --git a/custom/templates/custom/header.tmpl b/custom/templates/custom/header.tmpl
new file mode 100644
index 0000000..33f591f
--- /dev/null
+++ b/custom/templates/custom/header.tmpl
@@ -0,0 +1 @@
+<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/renderers.css" />
diff --git a/scripts/kicad_pcb.sh b/scripts/kicad_pcb.sh
new file mode 100644
index 0000000..0acffb4
--- /dev/null
+++ b/scripts/kicad_pcb.sh
@@ -0,0 +1,67 @@
+#!/bin/bash
+#set -x
+INPUT="$*"
+OUTDIR=$(mktemp -d)
+IMGDIR="$GITEA_WORK_DIR/custom/public/assets/img"
+
+KIOPTS=(
+    # --subtract-soldermask
+    --mode-single
+    --exclude-drawing-sheet
+    --page-size-mode 2
+    --sketch-pads-on-fab-layers
+)
+
+ln -s "$INPUT" "$OUTDIR"/pcb.kicad_pcb
+
+if [[ -d "$IMGDIR" && -w "$IMGDIR" ]]; then
+    # We can use caching
+    HASH=$(sha256sum "$INPUT" | cut -c1-64)
+
+    shopt -s nullglob
+    IMAGES=("${IMGDIR}"/${HASH}.*.svg)
+    shopt -u nullglob
+
+    if [ ${#IMAGES[@]} -le 0 ]; then
+        kicad-cli pcb export svg "${KIOPTS[@]}" --layers Edge.Cuts,F.Cu,F.Mask,F.Silkscreen,F.Adhesive,F.Paste --output "${OUTDIR}/${HASH}.1.svg" "$OUTDIR"/pcb.kicad_pcb >/dev/null
+        kicad-cli pcb export svg "${KIOPTS[@]}" --mirror --layers Edge.Cuts,B.Cu,B.Mask,B.Silkscreen,B.Adhesive,B.Paste --output "${OUTDIR}/${HASH}.2.svg" "$OUTDIR"/pcb.kicad_pcb >/dev/null
+
+        inkscape --actions="page-fit-to-selection" -o "${IMGDIR}/${HASH}.1.svg" "${OUTDIR}/${HASH}.1.svg"
+        inkscape --actions="page-fit-to-selection" -o "${IMGDIR}/${HASH}.2.svg" "${OUTDIR}/${HASH}.2.svg"
+
+        shopt -s nullglob
+        IMAGES=("${IMGDIR}"/${HASH}.*.svg)
+        shopt -u nullglob
+    fi
+
+    echo '<div class="file">'
+    for IMG in "${IMAGES[@]}"; do
+        echo '<div class="page">'
+        # echo "bla"
+        echo -n '<img src="/assets/img/'
+        echo -n "$(basename "${IMG}")"
+        echo -n '"'
+        echo -n ' class="full_width kicad_pcb pan_zoom"'
+        echo '/>'
+        echo '</div>'
+    done
+    echo '</div></div>'
+
+else
+    echo "<p>Error: Unable to write to $IMGDIR. Please check permissions.</p>"
+    # TODO: Implement base64 method
+    # We'll be generating the images on the fly, including them as base64
+    # mapfile -t FILES \
+    #     < <(kicad-cli sch export svg --exclude-drawing-sheet --no-background-color --output "$OUTDIR" "$OUTDIR/pcb.kicad_pcb" | grep -E 'Plotted to' | sed -r "s/^[^']+'|'[^']+$//g" )
+    # for FILE in "${FILES[@]}"; do
+    #     echo -n '<div class="file"><div class="page">'
+    #     echo -n '<img src="data:image/svg+xml;base64,'
+    #     inkscape --actions="page-fit-to-selection" -o - "$FILE" | base64 --wrap=0
+    #     echo -n '"'
+    #     echo -n ' class="full_width kicad_pcb pan_zoom"'
+    #     echo -n '/>'
+    #     echo '</div></div>'
+    # done
+fi
+
+rm -R "$OUTDIR"
diff --git a/scripts/kicad_sch.sh b/scripts/kicad_sch.sh
new file mode 100644
index 0000000..7667a41
--- /dev/null
+++ b/scripts/kicad_sch.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+#set -x
+INPUT="$*"
+OUTDIR=$(mktemp -d)
+IMGDIR="$GITEA_WORK_DIR/custom/public/assets/img"
+
+ln -s "$INPUT" "$OUTDIR"/schematic.kicad_sch
+
+if [[ -d "$IMGDIR" && -w "$IMGDIR" ]]; then
+    # We can use caching
+    HASH=$(sha256sum "$INPUT" | cut -c1-64)
+
+    shopt -s nullglob
+    IMAGES=("${IMGDIR}"/${HASH}.*.svg)
+    shopt -u nullglob
+
+    if [ ${#IMAGES[@]} -le 0 ]; then
+        mapfile -t FILES \
+            < <(kicad-cli sch export svg --exclude-drawing-sheet --no-background-color --output "$OUTDIR" "$OUTDIR/schematic.kicad_sch" | grep -E 'Plotted to' | sed -r "s/^[^']+'|'[^']+$//g" )
+
+        file_num=1
+        for FILE in "${FILES[@]}"; do
+            FN="${IMGDIR}/${HASH}.${file_num}.svg"
+            inkscape --actions="page-fit-to-selection" -o "$FN" "$FILE"
+            file_num=$((file_num + 1))
+            break # TODO: find some way to handle related (hierarchical) schematic files, as currently only the active file is made available
+        done
+
+        shopt -s nullglob
+        IMAGES=("${IMGDIR}"/${HASH}.*.svg)
+        shopt -u nullglob
+    fi
+
+    for FILE in "${IMAGES[@]}"; do
+        echo '<div class="file"><div class="page">'
+        # echo "bla"
+        echo -n '<img src="/assets/img/'
+        echo -n "$(basename "${FILE}")"
+        echo -n '"'
+        echo -n ' class="full_width kicad_sch pan_zoom"'
+        echo '/>'
+        echo '</div></div>'
+    done
+
+else
+    # We'll be generating the images on the fly, including them as base64
+    mapfile -t FILES \
+        < <(kicad-cli sch export svg --exclude-drawing-sheet --no-background-color --output "$OUTDIR" "$OUTDIR/schematic.kicad_sch" | grep -E 'Plotted to' | sed -r "s/^[^']+'|'[^']+$//g" )
+    for FILE in "${FILES[@]}"; do
+        echo -n '<div class="file"><div class="page">'
+        echo -n '<img src="data:image/svg+xml;base64,'
+        inkscape --actions="page-fit-to-selection" -o - "$FILE" | base64 --wrap=0
+        echo -n '"'
+        echo -n ' class="full_width kicad_sch pan_zoom"'
+        echo -n '/>'
+        echo '</div></div>'
+
+        break # TODO: find some way to handle related (hierarchical) schematic files, as currently only the active file is made available
+    done
+fi
+
+rm -R "$OUTDIR"
diff --git a/scripts/libreoffice.sh b/scripts/libreoffice.sh
new file mode 100644
index 0000000..7880b22
--- /dev/null
+++ b/scripts/libreoffice.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+#set -x
+INPUT="$*"
+OUTDIR=$(mktemp -d)
+IMGDIR="$GITEA_WORK_DIR/custom/public/assets/img"
+
+if [[ -d "$IMGDIR" && -w "$IMGDIR" ]]; then
+    # We can use caching
+    HASH=$(sha256sum "$INPUT" | cut -c1-64)
+
+    shopt -s nullglob
+    IMAGES=("${IMGDIR}"/${HASH}.*.svg)
+    shopt -u nullglob
+
+    if [ ${#IMAGES[@]} -le 0 ]; then
+        libreoffice --headless --convert-to pdf --outdir "$OUTDIR" $INPUT >/dev/null
+
+        PDF_FILE="$OUTDIR/$(basename "${INPUT%.*}".pdf)"
+
+        NUM_PAGES=$(pdfinfo "$PDF_FILE" | grep Pages | awk '{print $2}')
+
+        for (( i=1; i<=$NUM_PAGES; i++ )); do
+            FN="${IMGDIR}/${HASH}.${i}.svg"
+            pdftocairo -svg -f $i -l $i "$PDF_FILE" "${FN}"
+        done
+
+        shopt -s nullglob
+        IMAGES=("${IMGDIR}"/${HASH}.*.svg)
+        shopt -u nullglob
+    fi
+
+    echo '<div class="file">'
+    for IMG in "${IMAGES[@]}"; do
+        echo '<div class="page">'
+        echo -n '<img src="/assets/img/'
+        echo -n "$(basename "${IMG}")"
+        echo -n '"'
+        echo -n ' class="full_width libreoffice pan_zoom"'
+        echo '/>'
+        echo '</div>'
+    done
+    echo '</div>'
+
+else
+    # We'll be generating the images on the fly, including them as base64
+    libreoffice --headless --convert-to pdf --outdir "$OUTDIR" $INPUT >/dev/null
+    PDF_FILE="$OUTDIR/$(basename "${INPUT%.*}".pdf)"
+    NUM_PAGES=$(pdfinfo "$PDF_FILE" | grep Pages | awk '{print $2}')
+
+    for (( i=1; i<=$NUM_PAGES; i++ )); do
+        FN="${IMGDIR}/${HASH}.${i}.svg"
+        pdftocairo -svg -f $i -l $i "$PDF_FILE" "${FN}"
+    done
+
+    shopt -s nullglob
+    IMAGES=("${IMGDIR}"/${HASH}.*.svg)
+    shopt -u nullglob
+
+    echo '<div class="file">'
+    for IMG in "${IMAGES[@]}"; do
+        echo '<div class="page">'
+        echo -n '<img src="/assets/img/'
+        base64 --wrap=0 < "${IMG}"
+        echo -n '"'
+        echo -n ' class="full_width libreoffice pan_zoom"'
+        echo '/>'
+        echo '</div>'
+    done
+    echo '</div>'
+fi
+
+rm -R "$OUTDIR"