사무직에게 꼭 필요한 PDF 편집 툴 [Stirling-PDF]

개요

사무직으로 일하다보면 PDF로부터 특정 페이지만을 추출하거나, 페이지의 순서를 바꾸거나, 다른 PDF와 병합하는 등, PDF자체를 다룰 일이 빈번하게 발생합니다. 워드나 HWP등의 문서 파일의 원본이 있다면 간단하게 해결할 수 있지만, 스캔파일이거나, 문서 원본 없이 PDF만 갖고 있을 때는 그렇게 간단하게 해결할 수 없습니다.

구글에 검색하면 iLovePDF같은, 웹 호스팅을 기반으로 약간의 무료 할당량을 제공해주는 서비스도 있고, PDFSam Basic같은 부분무료 소프트웨어를 사용할 수도 있습니다.

저는 홈서버의 남는 자원을 활용해 위에 언급한 것들보다 조금 더 많은 기능을 제공하는(OCR가능!!!) Stirling-PDF을 셀프 호스팅으로 구현해보겠습니다.

해당 프로그램은 도커 이미지를 제공하고 있고 ARM 이미지도 제공하므로, 오라클 A1이나 라즈베리파이에도 올릴 수 있습니다 +_+

도커로 설치하기

Stirling-PDF Github 레포지토리에서 docker compose를 제공합니다.

services:
  stirling-pdf:
    image: frooodle/s-pdf:latest
    ports:
      - '8080:8080'
    volumes:
      - ./trainingData:/usr/share/tessdata #Required for extra OCR languages
      - ./extraConfigs:/configs
#      - ./customFiles:/customFiles/
#      - ./logs:/logs/
    environment:
      - DOCKER_ENABLE_SECURITY=false
      - INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
      - LANGS=en_GB
  • 이미지 태그는 latest-fat, latest, latest-ultra-lite 총 3가지를 사용할 수 있으며, 버전 별 차이는 이 곳에서 확인할 수 있습니다.
    적어도, OCR기능이나 PDF압축(용량 줄이기)등의 기능을 사용하기 위해선 fat버전을 사용해야 합니다.
  • DOCKER_ENABLE_SECURITY는 아래에서 설명할 OAuth를 위해서는 true가 되어야 합니다.
  • volumes에서 ./trainingData은 OCR을 위한 데이터 볼륨으로 해당 데이터는 이 곳에서 다운로드 받을 수 있습니다.
  • 그 외의 추가적인 설정은 ./extraconfigs에 settings.yml를 통해 구성할 수 있습니다.
    이를 통해 로그인 실패 시 일정 시간 차단, OAuth를 통한 로그인 구성 등을 설정할 수 있습니다.
security:
  enableLogin: false # set to 'true' to enable login
  csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
  loginAttemptCount: 5 # lock user account after 5 tries
  loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
  loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2)
  initialLogin:
    username: '' # Initial username for the first login
    password: '' # Initial password for the first login
  oauth2:
    enabled: false # set to 'true' to enable login (<strong>Note:</strong> enableLogin must also be 'true' for this to work)
    client:
      keycloak:
        issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
        clientId: '' # Client ID for Keycloak OAuth2
        clientSecret: '' # Client Secret for Keycloak OAuth2
        scopes: openid, profile, email # Scopes for Keycloak OAuth2
        useAsUsername: preferred_username # Field to use as the username for Keycloak OAuth2
      google:
        clientId: '' # Client ID for Google OAuth2
        clientSecret: '' # Client Secret for Google OAuth2
        scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # Scopes for Google OAuth2
        useAsUsername: email # Field to use as the username for Google OAuth2
      github:
        clientId: '' # Client ID for GitHub OAuth2
        clientSecret: '' # Client Secret for GitHub OAuth2
        scopes: read:user # Scope for GitHub OAuth2
        useAsUsername: login # Field to use as the username for GitHub OAuth2
    issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
    clientId: '' # Client ID from your provider
    clientSecret: '' # Client Secret from your provider
    autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
    useAsUsername: email # Default is 'email'; custom fields can be used as the username
    scopes: openid, profile, email # Specify the scopes for which the application will request permissions
    provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'

system:
  defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
  googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
  enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
  showUpdate: true # see when a new update is available
  showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
  customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files

ui:
  appName: '' # Application's visible name
  homeDescription: '' # Short description or tagline shown on homepage.
  appNameNavbar: '' # Name displayed on the navigation bar

endpoints:
  toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
  groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])

metrics:
  enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable

쿠버네티스(K8S)로 설치하기

기본 구성으로 실행하기

Stirling-PDF는 별도의 Helm Chart는 없습니다. 따라서 manifest를 직접 작성해야 합니다.

TrueNAS에서 OCR데이터와 log저장 경로를 미리 생성하고 CSI-NFS를 이용해 PV와 PVC로 사용하겠습니다.

먼저 OCR데이터를 다운받아 TrueNAS 저장소(NFS로 마운트하려는 폴더)에 옮겨준 뒤, SSH로 접속해서 압축을 풀어줍니다.

unzip tessdata-4.1.0.zip

그러면 아래처럼 자동적으로 폴더를 생성하고 압축이 해제됩니다.

...
  inflating: tessdata-4.1.0/tel.traineddata
   creating: tessdata-4.1.0/tessconfigs/
  inflating: tessdata-4.1.0/tgk.traineddata
  inflating: tessdata-4.1.0/tgl.traineddata
  inflating: tessdata-4.1.0/tha.traineddata
  inflating: tessdata-4.1.0/tir.traineddata
  inflating: tessdata-4.1.0/ton.traineddata
  inflating: tessdata-4.1.0/tur.traineddata
  inflating: tessdata-4.1.0/uig.traineddata
  inflating: tessdata-4.1.0/ukr.traineddata
  inflating: tessdata-4.1.0/urd.traineddata
  inflating: tessdata-4.1.0/uzb.traineddata
  inflating: tessdata-4.1.0/uzb_cyrl.traineddata
  inflating: tessdata-4.1.0/vie.traineddata
  inflating: tessdata-4.1.0/yid.traineddata
  inflating: tessdata-4.1.0/yor.traineddata
finishing deferred symbolic links:
  tessdata-4.1.0/configs -> tessconfigs/configs
  tessdata-4.1.0/pdf.ttf -> tessconfigs/pdf.ttf

심볼릭 링크를 생성하기 때문에, 윈도우에서 압축해제하고 붙여넣으려는 것보단, 저장소에서 직접 해제하는 것이 낫습니다.

이제 PV와 PVC를 마운트할 yaml을 작성합니다.

#! stirlingpdf-PVC.yaml

#OCR data PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-truenas-stirlingpdf-ocr
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: # NFS 공유 폴더 경로
    server: # NFS 서버의 IP 주소
  mountOptions: # 필요한 마운트 옵션
    - rw
    - vers=4.2

---
#OCR data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-truenas-stirlingpdf-ocr
  namespace: stirlingpdf
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  volumeName: nfs-truenas-stirlingpdf-ocr

---
#logs PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-truenas-stirlingpdf-logs
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: # NFS 공유 폴더 경로
    server: # NFS 서버의 IP 주소
  mountOptions: # 필요한 마운트 옵션
    - rw
    - vers=4.2

---
#logs PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-truenas-stirlingpdf-logs
  namespace: stirlingpdf
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  volumeName: nfs-truenas-stirlingpdf-logs

컨테이너를 작성합니다.

#! stirlingpdf-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: stirlingpdf
  namespace: stirlingpdf
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: stirlingpdf
  template:
    metadata:
      labels:
        app: stirlingpdf
    spec:
      containers:
      - name: stirlingpdf
        image: frooodle/s-pdf:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        env:
        - name: DOCKER_ENABLE_SECURITY
          value: "false"
        - name: INSTALL_BOOK_AND_ADVANCED_HTML_OPS
          value: "true"
        - name: LANGS
          value: "ko_KR"
        volumeMounts:
        - name: nfs-truenas-stirlingpdf-ocr
          mountPath: /usr/share/tessdata
        - name: nfs-truenas-stirlingpdf-logs
          mountPath: /logs
        # resources:
        #   requests:
        #     memory: "256Mi"
        #     cpu: "250m"
          # limits:
          #   memory: "256Mi"
          #   cpu: "250m"
      volumes:
      - name: nfs-truenas-stirlingpdf-ocr
        persistentVolumeClaim:
          claimName: nfs-truenas-stirlingpdf-ocr
      - name: nfs-truenas-stirlingpdf-logs
        persistentVolumeClaim:
          claimName: nfs-truenas-stirlingpdf-logs

그리고 노출할 서비스와 ingress까지 작성합니다.

#! stirlingpdf-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: stirlingpdf
  namespace: stirlingpdf
spec:
  selector:
    app: stirlingpdf
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080
    name: web
  type: LoadBalancer
  externalTrafficPolicy: Local

사설망 안에서 MetalLB를 활용해서 사용하려면 여기까지만 진행하시고 워커노드에서 8080포트를 방화벽에서 열어주거나, NodePort를 사용하면 됩니다.

저는 최종적으로 Authentik과 연결하여 외부 서비스로 노출할 계획이기 때문에 Ingress도 작성했습니다.

#! stirlingpdf-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: stirlingpdf-ingress
  namespace: stirlingpdf
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
spec:
  ingressClassName: nginx
  rules:  
  - host: #사용할 외부 도메인
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: stirlingpdf
            port:
              number: 8080

여기까지만 진행하고 파드를 띄워주면 로그인 절차 없이 바로 영어로 된 메인화면을 만날 수 있습니다.

상단의 지구본을 클릭해 언어를 ‘한국어’로 변경하면 대부분의 항목이 한글로 번역되어 보입니다.

서비스를 외부에 노출하지 않고 내부에서만 사용한다면 이 상태만으로도 충분히 사용할 수 있습니다.

그렇지만, 저는 이 기능을 외부에서 사용하는 것이 목적이므로, 로그인 기능을 구현하고 Authentik과 연동하는 것까지 진행해 보겠습니다.

추가환경 구성하기

custom_settings 작성

Stirling-PDF에서 env로 지정할 수 있는 것은 딱 5개 뿐입니다. 그 외의 값은 yaml파일에 지정하여 사용합니다.

여기에는 2가지 방법이 있는데, settings.yml 파일을 작성하여 사용하는 방법과 custom_settings.yml을 작성하는 방법이 있습니다.

전자의 경우, 모든 설정을 사용자가 작성한 파일 값으로 사용하기 때문에, 빈 설정값이 있으면 안됩니다.

후자의 경우, 사용자가 설정한 값은 override하고, 없는 값은 기본 값을 사용합니다.

이 글에선 후자를 선택하겠습니다.

먼저 custom_settings.yml을 생성하고 필요한 부분을 가져다가 정의합니다. 아래의 내용은 제가 사용하는 예시입니다.

security:
  enableLogin: 'true' # set to 'true' to enable login
  csrfDisabled: 'false' # Set to 'true' to disable CSRF protection (not recommended for production)
  loginMethod: 'oauth2' # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2
  initialLogin:
    username: 'username' # Initial username for the first login
    password: 'password' # Initial password for the first login
  oauth2:
    enabled: 'true' # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
    issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
    clientId: '' # Client ID from your provider
    clientSecret: '' # Client Secret from your provider
    autoCreateUser: 'true' # set to 'true' to allow auto-creation of non-existing users
    useAsUsername: 'name' # Default is 'email'; custom fields can be used as the username
    scopes: openid, profile, email, name # Specify the scopes for which the application will request permissions
    provider: 'One-Click Login' # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'

system:
  defaultLocale: 'ko-KR' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)

ui:
  appName: 'e-PDF' # Application's visible name
  homeDescription: 'Stirling-PDF hosted by Fentanest' # Short description or tagline shown on homepage.
  appNameNavbar: 'e-PDF' # Name displayed on the navigation bar

metrics:
  enabled: 'false' # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
  • enableLogin : 활성화하지 않으면 로그인 기능을 사용할 수 없으므로 true
  • csrfDisabled : 환경 상 일부러 비활성화할 이유가 없어 false
  • loginMethod : ID/PW를 이용한 로그인만 사용할 경우 normal, OAuth만 사용할 경우 oauth2, 모두 사용할 경우 all
  • initialLogin : 이 부분을 정의하지 않으면 파드를 재시작할 때마다 admin/stirling 이라는 관리자 계정이 새로 생성되고 로그인을 할 수 있게 됩니다. 해당 계정은 로그인할 때마다 아래처럼 새로운 비밀번호를 설정하고 관리자를 부여받기 때문에, 필수적으로 설정해야 합니다
  • oauth2 : Authentik을 이용하여 OAuth/OpenID를 구성하는 방법을 참조하여 구성하면 됩니다.
    (생성 시 리디렉션 URI는 “http://{외부 도메인 주소}/login/oauth2/code/oidc” 사용)
    issuer경로는 Authentik에서 공급자를 생성한 뒤 ‘OpenID 구성 발급자’를 기입하면 됩니다.
    autoCreateUser는 true로 해서 Authentik을 통해 접근한 유저가 막힘 없이 서비스에 접근할 수 있도록 하고
    useAsUsername은 계정이름으로 사용될 scope를 지정하는 것으로, 저는 name을 사용하고 있습니다.
    이 경우 해당 scope값을 가져올 수 있도록 그 아래의 scopes: 에다가 name을 추가해야 합니다.
    provider의 경우 아래처럼 SSO로그인 시 나타날 이름을 정하는 곳입니다.
  • appname, homeDescription 등은 브라우저의 탭, 메인 페이지의 아이콘 옆에 표시될 앱 이름, 짧은 한 줄 내용 등을 변경하는 것으로 아래처럼 변경할 수 있습니다.

작성이 끝났다면, 파일로부터 configmap을 생성합니다.

kubectl create configmap custom-settings --from-file=custom_settings.yml --namespace=stirlingpdf

manifest 수정

여러 가지 설정이 변경되었으니 deploy에서 이를 반영해 주어야 합니다.

우선, 제일 먼저 configmap으로 생성된 custom_settings.yml을 마운트해야 합니다.

...
    ...
        volumeMounts:
          ...
        - name: custom-settings
          mountPath: /configs/custom_settings.yml
          subPath: custom_settings.yml
      volumes:
      ...
      - name: custom-settings
        configMap :
          name: custom-settings

또한, 로그인 기능을 구현했기 때문에 env값의 DOCKER_ENABLE_SECURITY를 true로 변경해야 할 필요가 있습니다.

그리고 SYSTEM_CONNECTIONTIMEOUTMINUTES값을 정의해 너무 자주 로그아웃되지 않도록 변경하겠습니다.

        env:
        - name: DOCKER_ENABLE_SECURITY
          value: "true"
        - name: SYSTEM_CONNECTIONTIMEOUTMINUTES
          value: "10"

변경된 부분을 모두 반영하고 하나의 yaml로 합쳐 manifest를 아래와 같이 만들었습니다.

#! stirlingpdf-manifest.yaml

#OCR data PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-truenas-stirlingpdf-ocr
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: # NFS 공유 폴더 경로
    server: # NFS 서버의 IP 주소
  mountOptions:
    - rw
    - vers=4.2

---
#OCR data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-truenas-stirlingpdf-ocr
  namespace: stirlingpdf
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  volumeName: nfs-truenas-stirlingpdf-ocr

---
#logs PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-truenas-stirlingpdf-logs
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: # NFS 공유 폴더 경로
    server: # NFS 서버의 IP 주소
  mountOptions:
    - rw
    - vers=4.2

---
#logs PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-truenas-stirlingpdf-logs
  namespace: stirlingpdf
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  volumeName: nfs-truenas-stirlingpdf-logs

---
#deploy
apiVersion: apps/v1
kind: Deployment
metadata:
  name: stirlingpdf
  namespace: stirlingpdf
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: stirlingpdf
  template:
    metadata:
      labels:
        app: stirlingpdf
    spec:
      containers:
      - name: stirlingpdf
        image: frooodle/s-pdf:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        env:
        - name: DOCKER_ENABLE_SECURITY
          value: "true"
        - name: INSTALL_BOOK_AND_ADVANCED_HTML_OPS
          value: "true"
        - name: LANGS
          value: "ko_KR"
        - name: SYSTEM_CONNECTIONTIMEOUTMINUTES
          value: "10"
        volumeMounts:
        - name: nfs-truenas-stirlingpdf-ocr
          mountPath: /usr/share/tessdata
        - name: nfs-truenas-stirlingpdf-logs
          mountPath: /logs
        - name: custom-settings
          mountPath: /configs/custom_settings.yml
          subPath: custom_settings.yml
        # resources:
        #   requests:
        #     memory: "256Mi"
        #     cpu: "250m"
          # limits:
          #   memory: "256Mi"
          #   cpu: "250m"
      volumes:
      - name: nfs-truenas-stirlingpdf-ocr
        persistentVolumeClaim:
          claimName: nfs-truenas-stirlingpdf-ocr
      - name: nfs-truenas-stirlingpdf-logs
        persistentVolumeClaim:
          claimName: nfs-truenas-stirlingpdf-logs
      - name: custom-settings
        configMap :
          name: custom-settings

---
#stirlingpdf-service
apiVersion: v1
kind: Service
metadata:
  name: stirlingpdf
  namespace: stirlingpdf
spec:
  selector:
    app: stirlingpdf
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080
    name: web
  type: LoadBalancer
  externalTrafficPolicy: Local

---
#stirlingpdf-ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: stirlingpdf-ingress
  namespace: stirlingpdf
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
spec:
  ingressClassName: nginx
  rules:  
  - host: #사용할 외부 도메인명
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: stirlingpdf
            port:
              number: 8080

이제 해당 manifest를 통해 파드를 띄우면 아래처럼 로그인 기능이 구현된 첫 페이지를 볼 수 있고,

custom_settings.yml에 정의된 initialLogin값을 통해 로그인하거나 싱글사인온으로 Authentik을 경유해 로그인할 수 있게 되었습니다.

알려진 문제점(?)

맨 처음 Stirling-PDF에 접근하는 Authentik계정은 아래처럼 오류가 발생합니다.

로그를 살펴보면, 사전에 정의되어 있던 provider(keycloak, google, github) 외의 custom provider와의 호환 문제가 있는 것 같습니다.

이 상태에서 다시 한 번 싱글사인온을 통한 로그인을 진행하면 문제없이 로그인 되긴 합니다…=ㅅ=


관련 글

Authentik으로 홈서버 SSO 구현하기 (3) – OAuth/OpenID


출처

https://hub.docker.com/r/frooodle/s-pdf

https://github.com/Stirling-Tools/Stirling-PDF?tab=readme-ov-file

https://github.com/tesseractocr/tessdata

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤