├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── docker-publish.yml │ ├── dockerhub-description.yml │ ├── release-please.yml │ └── remove-docker-tag.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── biome.json ├── bun.lock ├── compose.yaml ├── eslint.config.ts ├── images ├── logo.png └── preview.png ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── results.js ├── robots.txt ├── script.js └── site.webmanifest ├── renovate.json ├── reset.d.ts ├── src ├── components │ ├── base.tsx │ └── header.tsx ├── converters │ ├── assimp.ts │ ├── calibre.ts │ ├── ffmpeg.ts │ ├── graphicsmagick.ts │ ├── imagemagick.ts │ ├── inkscape.ts │ ├── libheif.ts │ ├── libjxl.ts │ ├── main.ts │ ├── pandoc.ts │ ├── potrace.ts │ ├── resvg.ts │ ├── vips.ts │ └── xelatex.ts ├── helpers │ ├── normalizeFiletype.ts │ ├── printVersions.ts │ └── tailwind.ts ├── index.tsx └── main.css └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .editorconfig 3 | .env 4 | .git 5 | .idea 6 | .vscode 7 | CHANGELOG.md 8 | coverage* 9 | data 10 | docker-compose* 11 | Dockerfile* 12 | eslint.config.js 13 | helm-charts 14 | LICENSE 15 | Makefile 16 | node_modules 17 | prettier.config.js 18 | README.md 19 | renovate.json -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [C4illin] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # thanks to https://github.com/sredevopsorg/multi-arch-docker-github-workflow 4 | 5 | on: 6 | push: 7 | branches: ["main"] 8 | tags: ["v*.*.*"] 9 | pull_request: 10 | branches: ["main"] 11 | workflow_dispatch: 12 | env: 13 | GHCR_IMAGE: ghcr.io/c4illin/convertx 14 | IMAGE_NAME: ${{ github.repository }} 15 | DOCKERHUB_USERNAME: c4illin 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | # The build job builds the Docker image for each platform specified in the matrix. 23 | build: 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | platform: 28 | - linux/amd64 29 | - linux/arm64 30 | 31 | permissions: 32 | contents: write 33 | packages: write 34 | attestations: write 35 | checks: write 36 | actions: read 37 | 38 | runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }} 39 | 40 | name: Build Docker image for ${{ matrix.platform }} 41 | 42 | steps: 43 | - name: Prepare environment for current platform 44 | # This step sets up the environment for the current platform being built. 45 | # It replaces the '/' character in the platform name with '-' and sets it as an environment variable. 46 | # This is useful for naming artifacts and other resources that cannot contain '/'. 47 | # The environment variable PLATFORMS_PAIR will be used later in the workflow. 48 | id: prepare 49 | run: | 50 | platform=${{ matrix.platform }} 51 | echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV 52 | 53 | - name: Checkout repository 54 | uses: actions/checkout@v4 55 | 56 | - name: Docker meta default 57 | id: meta 58 | uses: docker/metadata-action@v5 59 | with: 60 | images: ${{ env.GHCR_IMAGE }} 61 | 62 | - name: Set up Docker Buildx 63 | uses: docker/setup-buildx-action@v3.10.0 64 | with: 65 | platforms: ${{ matrix.platform }} 66 | 67 | - name: Login to GitHub Container Registry 68 | # here we only login to ghcr.io since the this only pushes internal images 69 | uses: docker/login-action@v3.4.0 70 | with: 71 | registry: ghcr.io 72 | username: ${{ github.actor }} 73 | password: ${{ secrets.GITHUB_TOKEN }} 74 | 75 | - name: Build and push by digest 76 | id: build 77 | uses: docker/build-push-action@v6.18.0 78 | env: 79 | DOCKER_BUILDKIT: 1 80 | with: 81 | context: . 82 | platforms: ${{ matrix.platform }} 83 | labels: ${{ steps.meta.outputs.labels }} 84 | annotations: ${{ steps.meta.outputs.annotations }} 85 | outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true 86 | cache-from: type=gha,scope=${{ matrix.platform }} 87 | cache-to: type=gha,mode=max,scope=${{ matrix.platform }} 88 | 89 | - name: Export digest 90 | run: | 91 | mkdir -p /tmp/digests 92 | digest="${{ steps.build.outputs.digest }}" 93 | touch "/tmp/digests/${digest#sha256:}" 94 | 95 | - name: Upload digest 96 | uses: actions/upload-artifact@v4 97 | with: 98 | name: digests-${{ env.PLATFORM_PAIR }} 99 | path: /tmp/digests/* 100 | if-no-files-found: error 101 | retention-days: 1 102 | 103 | merge: 104 | name: Merge Docker manifests 105 | runs-on: ubuntu-latest 106 | 107 | permissions: 108 | attestations: write 109 | contents: read 110 | packages: write 111 | 112 | needs: 113 | - build 114 | steps: 115 | - name: Download digests 116 | uses: actions/download-artifact@v4 117 | with: 118 | path: /tmp/digests 119 | pattern: digests-* 120 | merge-multiple: true 121 | 122 | - name: Extract Docker metadata 123 | id: meta 124 | uses: docker/metadata-action@v5 125 | with: 126 | images: | 127 | ${{ env.GHCR_IMAGE }} 128 | ${{ env.IMAGE_NAME }} 129 | 130 | - name: Set up Docker Buildx 131 | uses: docker/setup-buildx-action@v3 132 | 133 | - name: Login to GitHub Container Registry 134 | uses: docker/login-action@v3 135 | with: 136 | registry: ghcr.io 137 | username: ${{ github.actor }} 138 | password: ${{ secrets.GITHUB_TOKEN }} 139 | 140 | - name: Login to Docker Hub 141 | uses: docker/login-action@v3 142 | with: 143 | username: ${{ env.DOCKERHUB_USERNAME }} 144 | password: ${{ secrets.DOCKERHUB_TOKEN }} 145 | 146 | - name: Get execution timestamp with RFC3339 format 147 | id: timestamp 148 | run: | 149 | echo "timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT 150 | 151 | - name: Create manifest list and push 152 | working-directory: /tmp/digests 153 | run: | 154 | docker buildx imagetools create \ 155 | $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 156 | --annotation='index:org.opencontainers.image.description=${{ github.event.repository.description }}' \ 157 | --annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \ 158 | --annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \ 159 | --annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \ 160 | $(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *) 161 | 162 | - name: Inspect image 163 | run: | 164 | docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}' 165 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-description.yml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub Description 2 | 3 | env: 4 | IMAGE_NAME: ${{ github.repository }} 5 | DOCKERHUB_USERNAME: c4illin 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | paths: 12 | - README.md 13 | - .github/workflows/dockerhub-description.yml 14 | jobs: 15 | dockerHubDescription: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Docker Hub Description 21 | uses: peter-evans/dockerhub-description@v4 22 | with: 23 | username: ${{ env.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | repository: ${{ env.IMAGE_NAME }} 26 | short-description: ${{ github.event.repository.description }} 27 | enable-url-completion: true 28 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | name: release-please 11 | 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: googleapis/release-please-action@v4 17 | with: 18 | # this assumes that you have created a personal access token 19 | # (PAT) and configured it as a GitHub action secret named 20 | # `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important). 21 | token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }} 22 | # token: ${{ secrets.GITHUB_TOKEN }} 23 | # this is a built-in strategy in release-please, see "Action Inputs" 24 | # for more options 25 | release-type: node 26 | -------------------------------------------------------------------------------- /.github/workflows/remove-docker-tag.yml: -------------------------------------------------------------------------------- 1 | name: Remove Docker Tag 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | remove-docker-tag: 8 | runs-on: ubuntu-latest 9 | 10 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 11 | # (required) 12 | permissions: 13 | contents: read 14 | packages: write 15 | 16 | steps: 17 | - name: Remove Docker Tag 18 | uses: ArchieAtkinson/remove-dockertag-action@v0.0 19 | with: 20 | tag_name: master 21 | github_token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | **/*.trace 37 | **/*.zip 38 | **/*.tar.gz 39 | **/*.tgz 40 | **/*.log 41 | package-lock.json 42 | **/*.bun 43 | /src/uploads 44 | /uploads 45 | /mydb.sqlite 46 | /output 47 | /db 48 | /data 49 | /Bruno 50 | /tsconfig.tsbuildinfo 51 | /public/generated.css 52 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14) 4 | 5 | 6 | ### Features 7 | 8 | * add HIDE_HISTORY option to control visibility of history page ([9d1c931](https://github.com/C4illin/ConvertX/commit/9d1c93155cc33ed6c83f9e5122afff8f28d0e4bf)) 9 | * add potrace converter ([bdbd4a1](https://github.com/C4illin/ConvertX/commit/bdbd4a122c09559b089b985ea12c5f3e085107da)) 10 | * Add support for .HIF files ([70705c1](https://github.com/C4illin/ConvertX/commit/70705c1850d470296df85958c02a01fb5bc3a25f)) 11 | * add support for drag/drop of images ([ff2ef74](https://github.com/C4illin/ConvertX/commit/ff2ef7413542cf10ba7a6e246763bcecd6829ec1)) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * add timezone support ([4b5c732](https://github.com/C4illin/ConvertX/commit/4b5c732380bc844dccf340ea1eb4f8bfe3bb44a5)), closes [#258](https://github.com/C4illin/ConvertX/issues/258) 17 | 18 | ## [0.12.1](https://github.com/C4illin/ConvertX/compare/v0.12.0...v0.12.1) (2025-03-20) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * rollback to bun 1.2.2 ([cdae798](https://github.com/C4illin/ConvertX/commit/cdae798fcf5879e4adea87386a38748b9a1e1ddc)) 24 | 25 | ## [0.12.0](https://github.com/C4illin/ConvertX/compare/v0.11.1...v0.12.0) (2025-03-06) 26 | 27 | 28 | ### Features 29 | 30 | * added progress bar for file upload ([db60f35](https://github.com/C4illin/ConvertX/commit/db60f355b2973f43f8e5990e6fe4e351b959b659)) 31 | * made every upload file independent ([cc54bdc](https://github.com/C4illin/ConvertX/commit/cc54bdcbe764c41cc3273485d072fd3178ad2dca)) 32 | * replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3)) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * add libheif ([6b92540](https://github.com/C4illin/ConvertX/commit/6b9254047c0598963aee1d99e20ba1650a0368bf)) 38 | * add libheif ([decfea5](https://github.com/C4illin/ConvertX/commit/decfea5dc9627b216bb276a9e1578c32cfa1deb6)), closes [#202](https://github.com/C4illin/ConvertX/issues/202) 39 | * added onerror log ([ae4bbc8](https://github.com/C4illin/ConvertX/commit/ae4bbc8baacbaf67763c62ea44140bb21cc17230)) 40 | * refactored uploadFile to only accept a single file instead of multiple ([dc82a43](https://github.com/C4illin/ConvertX/commit/dc82a438d4104b79ff423d502a6779a43928968a)) 41 | * update libheif to 1.19.5 ([fba5e21](https://github.com/C4illin/ConvertX/commit/fba5e212e8d0eaba8971e239e35aeb521f3cd813)), closes [#202](https://github.com/C4illin/ConvertX/issues/202) 42 | 43 | ## [0.11.1](https://github.com/C4illin/ConvertX/compare/v0.11.0...v0.11.1) (2025-02-07) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * mobile view overflow ([bec58ac](https://github.com/C4illin/ConvertX/commit/bec58ac59f9600e35385b9e21d174f3ab1b42b1d)) 49 | 50 | ## [0.11.0](https://github.com/C4illin/ConvertX/compare/v0.10.1...v0.11.0) (2025-02-05) 51 | 52 | 53 | ### Features 54 | 55 | * add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * don't crash if file is not found ([16f27c1](https://github.com/C4illin/ConvertX/commit/16f27c13bbc1c0e5fa2316f3db11d0918524053b)) 61 | * install numpy for inkscape ([0e61051](https://github.com/C4illin/ConvertX/commit/0e61051fc6be188164c3865b4fb579c140859fdc)) 62 | 63 | ## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * ffmpeg works without ffmpeg_args ([3b7ea88](https://github.com/C4illin/ConvertX/commit/3b7ea88b7382f7c21b120bdc9bda5bb10547f55d)), closes [#212](https://github.com/C4illin/ConvertX/issues/212) 69 | 70 | ## [0.10.0](https://github.com/C4illin/ConvertX/compare/v0.9.0...v0.10.0) (2025-01-18) 71 | 72 | 73 | ### Features 74 | 75 | * add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * add FFMPEG_ARGS env variable ([f537c81](https://github.com/C4illin/ConvertX/commit/f537c81db7815df8017f834e3162291197e1c40f)), closes [#190](https://github.com/C4illin/ConvertX/issues/190) 81 | * add qt6-qtbase-private-dev from community repo ([95dbc9f](https://github.com/C4illin/ConvertX/commit/95dbc9f678bec7e6e2c03587e1473fb8ff708ea3)) 82 | * skip account setup when ALLOW_UNAUTHENTICATED is true ([538c5b6](https://github.com/C4illin/ConvertX/commit/538c5b60c9e27a8184740305475245da79bae143)) 83 | 84 | ## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21) 85 | 86 | 87 | ### Features 88 | 89 | * add inkscape for vector images ([f3740e9](https://github.com/C4illin/ConvertX/commit/f3740e9ded100b8500f3613517960248bbd3c210)) 90 | * Allow to chose webroot ([36cb6cc](https://github.com/C4illin/ConvertX/commit/36cb6cc589d80d0a87fa8dbe605db71a9a2570f9)), closes [#180](https://github.com/C4illin/ConvertX/issues/180) 91 | * disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * treat unknown as m4a ([1a442d6](https://github.com/C4illin/ConvertX/commit/1a442d6e69606afef63b1e7df36aa83d111fa23d)), closes [#178](https://github.com/C4illin/ConvertX/issues/178) 97 | * wait for both upload and selection ([4c05fd7](https://github.com/C4illin/ConvertX/commit/4c05fd72bbbf91ee02327f6fcbf749b78272376b)), closes [#177](https://github.com/C4illin/ConvertX/issues/177) 98 | 99 | ## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * disable convert button when input is empty ([78844d7](https://github.com/C4illin/ConvertX/commit/78844d7bd55990789ed07c81e49043e688cbe656)), closes [#151](https://github.com/C4illin/ConvertX/issues/151) 105 | * resize to fit for ico ([b4e53db](https://github.com/C4illin/ConvertX/commit/b4e53dbb8e70b3a95b44e5b756759d16117a87e1)), closes [#157](https://github.com/C4illin/ConvertX/issues/157) 106 | * treat jfif as jpeg ([339b79f](https://github.com/C4illin/ConvertX/commit/339b79f786131deb93f0d5683e03178fdcab1ef5)), closes [#163](https://github.com/C4illin/ConvertX/issues/163) 107 | 108 | ## [0.8.0](https://github.com/C4illin/ConvertX/compare/v0.7.0...v0.8.0) (2024-09-30) 109 | 110 | 111 | ### Features 112 | 113 | * add light theme, fixes [#156](https://github.com/C4illin/ConvertX/issues/156) ([72636c5](https://github.com/C4illin/ConvertX/commit/72636c5059ebf09c8fece2e268293650b2f8ccf6)) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * add support for usd for assimp, [#144](https://github.com/C4illin/ConvertX/issues/144) ([2057167](https://github.com/C4illin/ConvertX/commit/20571675766209ad1251f07e687d29a6791afc8b)) 119 | * cleanup formats and add opus, fixes [#159](https://github.com/C4illin/ConvertX/issues/159) ([ae1dfaf](https://github.com/C4illin/ConvertX/commit/ae1dfafc9d9116a57b08c2f7fc326990e00824b0)) 120 | * support .awb and clean up, fixes [#153](https://github.com/C4illin/ConvertX/issues/153), [#92](https://github.com/C4illin/ConvertX/issues/92) ([1c9e67f](https://github.com/C4illin/ConvertX/commit/1c9e67fc3201e0e5dee91e8981adf34daaabf33a)) 121 | 122 | ## [0.7.0](https://github.com/C4illin/ConvertX/compare/v0.6.0...v0.7.0) (2024-09-26) 123 | 124 | 125 | ### Features 126 | 127 | * Add support for 3d assets through assimp converter ([63a4328](https://github.com/C4illin/ConvertX/commit/63a4328d4a1e01df3e0ec4a877bad8c8ffe71129)) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * wrong layout on search with few options ([8817389](https://github.com/C4illin/ConvertX/commit/88173891ba2d69da46eda46f3f598a9b54f26f96)) 133 | 134 | ## [0.6.0](https://github.com/C4illin/ConvertX/compare/v0.5.0...v0.6.0) (2024-09-25) 135 | 136 | 137 | ### Features 138 | 139 | * ui remake with tailwind ([22f823c](https://github.com/C4illin/ConvertX/commit/22f823c535b20382981f86a13616b830a1f3392f)) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * rename css file to force update cache, fixes [#141](https://github.com/C4illin/ConvertX/issues/141) ([47139a5](https://github.com/C4illin/ConvertX/commit/47139a550bd3d847da288c61bf8f88953b79c673)) 145 | 146 | ## [0.5.0](https://github.com/C4illin/ConvertX/compare/v0.4.1...v0.5.0) (2024-09-20) 147 | 148 | 149 | ### Features 150 | 151 | * add option to customize how often files are automatically deleted ([317c932](https://github.com/C4illin/ConvertX/commit/317c932c2a26280bf37ed3d3bf9b879413590f5a)) 152 | 153 | 154 | ### Bug Fixes 155 | 156 | * improve file name replacement logic ([60ba7c9](https://github.com/C4illin/ConvertX/commit/60ba7c93fbdc961f3569882fade7cc13dee7a7a5)) 157 | 158 | ## [0.4.1](https://github.com/C4illin/ConvertX/compare/v0.4.0...v0.4.1) (2024-09-15) 159 | 160 | 161 | ### Bug Fixes 162 | 163 | * allow non lowercase true and false values, fixes [#122](https://github.com/C4illin/ConvertX/issues/122) ([bef1710](https://github.com/C4illin/ConvertX/commit/bef1710e3376baa7e25c107ded20a40d18b8c6b0)) 164 | 165 | ## [0.4.0](https://github.com/C4illin/ConvertX/compare/v0.3.3...v0.4.0) (2024-08-26) 166 | 167 | 168 | ### Features 169 | 170 | * add option for unauthenticated file conversions [#114](https://github.com/C4illin/ConvertX/issues/114) ([f0d0e43](https://github.com/C4illin/ConvertX/commit/f0d0e4392983c3e4c530304ea88e023fda9bcac0)) 171 | * add resvg converter ([d5eeef9](https://github.com/C4illin/ConvertX/commit/d5eeef9f6884b2bb878508bed97ea9ceaa662995)) 172 | * add robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7)) 173 | * Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58)) 174 | 175 | 176 | ### Bug Fixes 177 | 178 | * keep unauthenticated user logged in if allowed [#114](https://github.com/C4illin/ConvertX/issues/114) ([bc4ad49](https://github.com/C4illin/ConvertX/commit/bc4ad492852fad8cb832a0c03485cccdd7f7b117)) 179 | * pdf support in vips ([8ca4f15](https://github.com/C4illin/ConvertX/commit/8ca4f1587df7f358893941c656d78d75f04dac93)) 180 | * Slow click on conversion popup does not work ([4d9c4d6](https://github.com/C4illin/ConvertX/commit/4d9c4d64aa0266f3928935ada68d91ac81f638aa)) 181 | 182 | ## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30) 183 | 184 | 185 | ### Bug Fixes 186 | 187 | * downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c)) 188 | 189 | ## [0.3.2](https://github.com/C4illin/ConvertX/compare/v0.3.1...v0.3.2) (2024-07-09) 190 | 191 | 192 | ### Bug Fixes 193 | 194 | * increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64) 195 | 196 | ## [0.3.1](https://github.com/C4illin/ConvertX/compare/v0.3.0...v0.3.1) (2024-06-27) 197 | 198 | 199 | ### Bug Fixes 200 | 201 | * release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf)) 202 | 203 | ## [0.3.0](https://github.com/C4illin/ConvertX/compare/v0.2.0...v0.3.0) (2024-06-27) 204 | 205 | 206 | ### Features 207 | 208 | * add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44) 209 | * change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b)) 210 | * print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a)) 211 | 212 | ## [0.2.0](https://github.com/C4illin/ConvertX/compare/v0.1.2...v0.2.0) (2024-06-20) 213 | 214 | 215 | ### Features 216 | 217 | * add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482)) 218 | * change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34) 219 | 220 | ## [0.1.2](https://github.com/C4illin/ConvertX/compare/v0.1.1...v0.1.2) (2024-06-10) 221 | 222 | 223 | ### Bug Fixes 224 | 225 | * fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23) 226 | 227 | ## [0.1.1](https://github.com/C4illin/ConvertX/compare/v0.1.0...v0.1.1) (2024-05-30) 228 | 229 | 230 | ### Bug Fixes 231 | 232 | * :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12) 233 | 234 | ## 0.1.0 (2024-05-30) 235 | 236 | 237 | ### Features 238 | 239 | * remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b)) 240 | 241 | 242 | ### Miscellaneous Chores 243 | 244 | * release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431)) 245 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:trixie-slim AS base 2 | LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX" 3 | WORKDIR /app 4 | 5 | # install bun 6 | ENV BUN_INSTALL=/etc/.bun 7 | ENV PATH=$BUN_INSTALL/bin:$PATH 8 | ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=0 9 | RUN apt-get update && apt-get install -y \ 10 | curl \ 11 | unzip \ 12 | && rm -rf /var/lib/apt/lists/* 13 | RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.2.2" 14 | 15 | # install dependencies into temp directory 16 | # this will cache them and speed up future builds 17 | FROM base AS install 18 | RUN mkdir -p /temp/dev 19 | COPY package.json bun.lock /temp/dev/ 20 | RUN cd /temp/dev && bun install --frozen-lockfile 21 | 22 | # install with --production (exclude devDependencies) 23 | RUN mkdir -p /temp/prod 24 | COPY package.json bun.lock /temp/prod/ 25 | RUN cd /temp/prod && bun install --frozen-lockfile --production 26 | 27 | FROM base AS prerelease 28 | WORKDIR /app 29 | COPY --from=install /temp/dev/node_modules node_modules 30 | COPY . . 31 | 32 | # ENV NODE_ENV=production 33 | RUN bun run build 34 | 35 | # copy production dependencies and source code into final image 36 | FROM base AS release 37 | 38 | # install additional dependencies 39 | RUN apt-get update && apt-get install -y \ 40 | assimp-utils \ 41 | calibre \ 42 | dcraw \ 43 | ffmpeg \ 44 | ghostscript \ 45 | graphicsmagick \ 46 | inkscape \ 47 | libheif-examples \ 48 | libjxl-tools \ 49 | libva2 \ 50 | libvips-tools \ 51 | imagemagick-7.q16 \ 52 | pandoc \ 53 | poppler-utils \ 54 | potrace \ 55 | python3-numpy \ 56 | resvg \ 57 | texlive \ 58 | texlive-latex-extra \ 59 | texlive-xetex \ 60 | --no-install-recommends \ 61 | && rm -rf /var/lib/apt/lists/* 62 | 63 | COPY --from=install /temp/prod/node_modules node_modules 64 | COPY --from=prerelease /app/public/generated.css /app/public/ 65 | COPY . . 66 | 67 | EXPOSE 3000/tcp 68 | ENV NODE_ENV=production 69 | ENTRYPOINT [ "bun", "run", "./src/index.tsx" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ConvertX](images/logo.png) 2 | 3 | # ConvertX 4 | 5 | [![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml) 6 | [![ghcr.io Pulls](https://img.shields.io/badge/dynamic/json?logo=github&url=https%3A%2F%2Fipitio.github.io%2Fbackage%2FC4illin%2FConvertX%2Fconvertx.json&query=%24.downloads&label=ghcr.io%20pulls&cacheSeconds=14400)](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) 7 | [![Docker Pulls](https://img.shields.io/docker/pulls/c4illin/convertx?style=flat&logo=docker&label=dockerhub%20pulls&link=https%3A%2F%2Fhub.docker.com%2Frepository%2Fdocker%2Fc4illin%2Fconvertx%2Fgeneral)](https://hub.docker.com/r/c4illin/convertx) 8 | [![GitHub Release](https://img.shields.io/github/v/release/C4illin/ConvertX)](https://github.com/C4illin/ConvertX/pkgs/container/convertx) 9 | ![GitHub commits since latest release](https://img.shields.io/github/commits-since/C4illin/ConvertX/latest) 10 | ![GitHub repo size](https://img.shields.io/github/repo-size/C4illin/ConvertX) 11 | ![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=) 12 | 13 | C4illin%2FConvertX | Trendshift 14 | 15 | 16 | A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia. 17 | 18 | ## Features 19 | 20 | - Convert files to different formats 21 | - Process multiple files at once 22 | - Password protection 23 | - Multiple accounts 24 | 25 | ## Converters supported 26 | 27 | | Converter | Use case | Converts from | Converts to | 28 | |------------------------------------------------------------------------------|---------------|---------------|-------------| 29 | | [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 | 30 | | [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 | 31 | | [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 | 32 | | [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 | 33 | | [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 | 34 | | [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 | 35 | | [Pandoc](https://pandoc.org/) | Documents | 43 | 65 | 36 | | [ImageMagick](https://imagemagick.org/) | Images | 245 | 183 | 37 | | [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 | 38 | | [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 | 39 | | [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 | 40 | | [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 | 41 | | [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 | 42 | 43 | 44 | 45 | 46 | Any missing converter? Open an issue or pull request! 47 | 48 | ## Deployment 49 | 50 | > [!WARNING] 51 | > If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true 52 | 53 | ```yml 54 | # docker-compose.yml 55 | services: 56 | convertx: 57 | image: ghcr.io/c4illin/convertx 58 | container_name: convertx 59 | restart: unless-stopped 60 | ports: 61 | - "3000:3000" 62 | environment: 63 | - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset 64 | volumes: 65 | - ./data:/app/data 66 | ``` 67 | 68 | or 69 | 70 | ```bash 71 | docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx 72 | ``` 73 | 74 | Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account. 75 | 76 | If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose. 77 | 78 | ### Environment variables 79 | 80 | All are optional, JWT_SECRET is recommended to be set. 81 | 82 | | Name | Default | Description | 83 | |---------------------------|---------|-------------| 84 | | JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token | 85 | | ACCOUNT_REGISTRATION | false | Allow users to register accounts | 86 | | HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally | 87 | | ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally | 88 | | AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable | 89 | | WEBROOT | | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" | 90 | | FFMPEG_ARGS | | Arguments to pass to ffmpeg, e.g. `-preset veryfast` | 91 | | HIDE_HISTORY | false | Hide the history page | 92 | 93 | ### Docker images 94 | 95 | There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use. 96 | 97 | The image is available on [GitHub Container Registry](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) and [Docker Hub](https://hub.docker.com/r/c4illin/convertx). 98 | 99 | | Image | What it is | 100 | |-------|------------| 101 | | `image: ghcr.io/c4illin/convertx` | The latest release on ghcr | 102 | | `image: ghcr.io/c4illin/convertx:main` | The latest commit on ghcr | 103 | | `image: c4illin/convertx` | The latest release on docker hub | 104 | | `image: c4illin/convertx:main` | The latest commit on docker hub | 105 | 106 | ![Release image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=release+image&trim=) 107 | ![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=) 108 | 109 | 110 | ### Tutorial 111 | 112 | > [!NOTE] 113 | > These are written by other people, and may be outdated, incorrect or wrong. 114 | 115 | Tutorial in french: 116 | 117 | Tutorial in chinese: 118 | 119 | ## Screenshots 120 | 121 | ![ConvertX Preview](images/preview.png) 122 | 123 | ## Development 124 | 125 | 0. Install [Bun](https://bun.sh/) and Git 126 | 1. Clone the repository 127 | 2. `bun install` 128 | 3. `bun run dev` 129 | 130 | Pull requests are welcome! See below and open issues for the list of todos. 131 | 132 | Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages. 133 | 134 | ## Todo 135 | 136 | - [x] Add messages for errors in converters 137 | - [x] Add searchable list of formats 138 | - [ ] Add options for converters 139 | - [ ] Divide index.tsx into smaller components 140 | - [ ] Add tests 141 | - [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible. 142 | - [ ] Make errors logs visible from the web ui 143 | - [ ] Add more converters: 144 | - [ ] [deark](https://github.com/jsummers/deark) 145 | - [ ] LibreOffice 146 | - [ ] [dvisvgm](https://github.com/mgieseki/dvisvgm) 147 | 148 | ## Contributors 149 | 150 | 151 | Image with all contributors 152 | 153 | 154 | ## Star History 155 | 156 | 157 | 158 | 159 | 160 | Star History Chart 161 | 162 | 163 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest release is supported 6 | 7 | ## Reporting a Vulnerability 8 | 9 | To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/C4illin/ConvertX/security/advisories/new) tab. 10 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "formatWithErrors": true, 6 | "indentStyle": "space", 7 | "indentWidth": 2, 8 | "lineEnding": "lf", 9 | "lineWidth": 80, 10 | "attributePosition": "auto" 11 | }, 12 | "files": { 13 | "ignore": [ 14 | "**/node_modules/**", 15 | "**/pico.lime.min.css" 16 | ] 17 | }, 18 | "organizeImports": { 19 | "enabled": true 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true, 25 | "complexity": { 26 | "noBannedTypes": "error", 27 | "noUselessThisAlias": "error", 28 | "noUselessTypeConstraint": "error", 29 | "useArrowFunction": "off", 30 | "useLiteralKeys": "error", 31 | "useOptionalChain": "error" 32 | }, 33 | "correctness": { 34 | "noPrecisionLoss": "error", 35 | "noUnusedVariables": "off", 36 | "useJsxKeyInIterable": "off" 37 | }, 38 | "style": { 39 | "noInferrableTypes": "error", 40 | "noNamespace": "error", 41 | "useAsConstAssertion": "error", 42 | "useBlockStatements": "off", 43 | "useConsistentArrayType": "error", 44 | "useForOf": "error", 45 | "useImportType": "error", 46 | "useShorthandFunctionType": "error" 47 | }, 48 | "suspicious": { 49 | "noEmptyBlockStatements": "error", 50 | "noEmptyInterface": "error", 51 | "noExplicitAny": "warn", 52 | "noExtraNonNullAssertion": "error", 53 | "noMisleadingInstantiator": "error", 54 | "noUnsafeDeclarationMerging": "error", 55 | "useAwait": "error", 56 | "useNamespaceKeyword": "error" 57 | }, 58 | "nursery": { 59 | "useSortedClasses": "error" 60 | } 61 | } 62 | }, 63 | "javascript": { 64 | "formatter": { 65 | "jsxQuoteStyle": "double", 66 | "quoteProperties": "asNeeded", 67 | "semicolons": "always", 68 | "arrowParentheses": "always", 69 | "bracketSpacing": true, 70 | "bracketSameLine": true, 71 | "quoteStyle": "double", 72 | "attributePosition": "auto" 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | convertx: 3 | build: 4 | context: . 5 | # dockerfile: Debian.Dockerfile 6 | volumes: 7 | - ./data:/app/data 8 | environment: # Defaults are listed below. All are optional. 9 | - ACCOUNT_REGISTRATION=true # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account) 10 | - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default 11 | - HTTP_ALLOWED=true # setting this to true is unsafe, only set this to true locally 12 | - ALLOW_UNAUTHENTICATED=true # allows anyone to use the service without logging in, only set this to true locally 13 | - AUTO_DELETE_EVERY_N_HOURS=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable 14 | # - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg 15 | # - WEBROOT=/convertx # the root path of the web interface, leave empty to disable 16 | # - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false 17 | ports: 18 | - 3000:3000 19 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintParserTypeScript from "@typescript-eslint/parser"; 3 | import type { Linter } from "eslint"; 4 | import eslintPluginReadableTailwind from "eslint-plugin-readable-tailwind"; 5 | import simpleImportSortPlugin from "eslint-plugin-simple-import-sort"; 6 | import globals from "globals"; 7 | import tseslint from "typescript-eslint"; 8 | 9 | export default [ 10 | js.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | // ...tailwind.configs["flat/recommended"], 13 | { 14 | plugins: { 15 | "simple-import-sort": simpleImportSortPlugin, 16 | "readable-tailwind": eslintPluginReadableTailwind, 17 | }, 18 | ignores: ["**/node_modules/**"], 19 | languageOptions: { 20 | parser: eslintParserTypeScript, 21 | parserOptions: { 22 | project: true, 23 | ecmaFeatures: { 24 | jsx: true, 25 | }, 26 | }, 27 | globals: { 28 | ...globals.node, 29 | ...globals.browser, 30 | }, 31 | }, 32 | files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"], 33 | rules: { 34 | ...eslintPluginReadableTailwind.configs.warning.rules, 35 | // "tailwindcss/classnames-order": "off", 36 | "readable-tailwind/multiline": [ 37 | "warn", 38 | { 39 | group: "newLine", 40 | printWidth: 100, 41 | }, 42 | ], 43 | // "tailwindcss/no-custom-classname": [ 44 | // "warn", 45 | // { 46 | // whitelist: [ 47 | // "select_container", 48 | // "convert_to_popup", 49 | // "convert_to_group", 50 | // "target", 51 | // "convert_to_target", 52 | // ], 53 | // }, 54 | // ], 55 | }, 56 | }, 57 | ] as Linter.Config[]; 58 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4illin/ConvertX/2490c3a7e7d0a627787edf1ce8aedeb0335938fb/images/logo.png -------------------------------------------------------------------------------- /images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4illin/ConvertX/2490c3a7e7d0a627787edf1ce8aedeb0335938fb/images/preview.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convertx-frontend", 3 | "version": "0.13.0", 4 | "scripts": { 5 | "dev": "bun run --watch src/index.tsx", 6 | "hot": "bun run --hot src/index.tsx", 7 | "format": "eslint --fix .", 8 | "build": "bunx @tailwindcss/cli -i ./src/main.css -o ./public/generated.css", 9 | "lint": "run-p 'lint:*'", 10 | "lint:tsc": "tsc --noEmit", 11 | "lint:knip": "knip", 12 | "lint:eslint": "eslint ." 13 | }, 14 | "dependencies": { 15 | "@elysiajs/html": "^1.3.0", 16 | "@elysiajs/jwt": "^1.3.0", 17 | "@elysiajs/static": "^1.3.0", 18 | "@kitajs/html": "^4.2.9", 19 | "elysia": "^1.3.1", 20 | "sanitize-filename": "^1.6.3" 21 | }, 22 | "module": "src/index.tsx", 23 | "type": "module", 24 | "bun-create": { 25 | "start": "bun run src/index.tsx" 26 | }, 27 | "devDependencies": { 28 | "@eslint/js": "^9.27.0", 29 | "@ianvs/prettier-plugin-sort-imports": "^4.4.1", 30 | "@kitajs/ts-html-plugin": "^4.1.1", 31 | "@tailwindcss/cli": "^4.1.7", 32 | "@tailwindcss/postcss": "^4.1.7", 33 | "@total-typescript/ts-reset": "^0.6.1", 34 | "@types/bun": "^1.2.14", 35 | "@types/eslint-plugin-tailwindcss": "^3.17.0", 36 | "@types/node": "^22.15.21", 37 | "autoprefixer": "^10.4.21", 38 | "cssnano": "^7.0.7", 39 | "eslint": "^9.27.0", 40 | "eslint-plugin-readable-tailwind": "^2.1.2", 41 | "eslint-plugin-simple-import-sort": "^12.1.1", 42 | "eslint-plugin-tailwindcss": "4.0.0-alpha.0", 43 | "globals": "^16.1.0", 44 | "knip": "^5.57.2", 45 | "npm-run-all2": "^8.0.3", 46 | "postcss": "^8.5.3", 47 | "postcss-cli": "^11.0.1", 48 | "prettier": "^3.5.3", 49 | "tailwind-scrollbar": "^4.0.2", 50 | "tailwindcss": "^4.1.7", 51 | "typescript": "^5.8.3", 52 | "typescript-eslint": "^8.32.1" 53 | } 54 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Config & import("@ianvs/prettier-plugin-sort-imports").PluginConfig} 3 | */ 4 | const config = { 5 | arrowParens: "always", 6 | printWidth: 80, 7 | singleQuote: false, 8 | semi: true, 9 | tabWidth: 2, 10 | plugins: ["@ianvs/prettier-plugin-sort-imports"], 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4illin/ConvertX/2490c3a7e7d0a627787edf1ce8aedeb0335938fb/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4illin/ConvertX/2490c3a7e7d0a627787edf1ce8aedeb0335938fb/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4illin/ConvertX/2490c3a7e7d0a627787edf1ce8aedeb0335938fb/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4illin/ConvertX/2490c3a7e7d0a627787edf1ce8aedeb0335938fb/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4illin/ConvertX/2490c3a7e7d0a627787edf1ce8aedeb0335938fb/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4illin/ConvertX/2490c3a7e7d0a627787edf1ce8aedeb0335938fb/public/favicon.ico -------------------------------------------------------------------------------- /public/results.js: -------------------------------------------------------------------------------- 1 | const webroot = document.querySelector("meta[name='webroot']").content; 2 | 3 | window.downloadAll = function () { 4 | // Get all download links 5 | const downloadLinks = document.querySelectorAll("a[download]"); 6 | 7 | // Trigger download for each link 8 | downloadLinks.forEach((link, index) => { 9 | // We add a delay for each download to prevent them from starting at the same time 10 | setTimeout(() => { 11 | const event = new MouseEvent("click"); 12 | link.dispatchEvent(event); 13 | }, index * 100); 14 | }); 15 | }; 16 | const jobId = window.location.pathname.split("/").pop(); 17 | const main = document.querySelector("main"); 18 | let progressElem = document.querySelector("progress"); 19 | 20 | const refreshData = () => { 21 | // console.log("Refreshing data...", progressElem.value, progressElem.max); 22 | if (progressElem.value !== progressElem.max) { 23 | fetch(`${webroot}/progress/${jobId}`, { 24 | method: "POST", 25 | }) 26 | .then((res) => res.text()) 27 | .then((html) => { 28 | main.innerHTML = html; 29 | }) 30 | .catch((err) => console.log(err)); 31 | 32 | setTimeout(refreshData, 1000); 33 | } 34 | 35 | progressElem = document.querySelector("progress"); 36 | }; 37 | 38 | refreshData(); 39 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /public/script.js: -------------------------------------------------------------------------------- 1 | const webroot = document.querySelector("meta[name='webroot']").content; 2 | const fileInput = document.querySelector('input[type="file"]'); 3 | const dropZone = document.getElementById("dropzone"); 4 | const convertButton = document.querySelector("input[type='submit']"); 5 | const fileNames = []; 6 | let fileType; 7 | let pendingFiles = 0; 8 | let formatSelected = false; 9 | 10 | dropZone.addEventListener("dragover", (e) => { 11 | e.preventDefault(); 12 | dropZone.classList.add("dragover"); 13 | }); 14 | 15 | dropZone.addEventListener("dragleave", () => { 16 | dropZone.classList.remove("dragover"); 17 | }); 18 | 19 | dropZone.addEventListener("drop", (e) => { 20 | e.preventDefault(); 21 | dropZone.classList.remove("dragover"); 22 | 23 | const files = e.dataTransfer.files; 24 | 25 | if (files.length === 0) { 26 | console.warn("No files dropped — likely a URL or unsupported source."); 27 | return; 28 | } 29 | 30 | for (const file of files) { 31 | console.log("Handling dropped file:", file.name); 32 | handleFile(file); 33 | } 34 | }); 35 | 36 | // Extracted handleFile function for reusability in drag-and-drop and file input 37 | function handleFile(file) { 38 | const fileList = document.querySelector("#file-list"); 39 | 40 | const row = document.createElement("tr"); 41 | row.innerHTML = ` 42 | ${file.name} 43 | 44 | ${(file.size / 1024).toFixed(2)} kB 45 | Remove 46 | `; 47 | 48 | if (!fileType) { 49 | fileType = file.name.split(".").pop(); 50 | fileInput.setAttribute("accept", `.${fileType}`); 51 | setTitle(); 52 | 53 | fetch(`${webroot}/conversions`, { 54 | method: "POST", 55 | body: JSON.stringify({ fileType }), 56 | headers: { "Content-Type": "application/json" }, 57 | }) 58 | .then((res) => res.text()) 59 | .then((html) => { 60 | selectContainer.innerHTML = html; 61 | updateSearchBar(); 62 | }) 63 | .catch(console.error); 64 | } 65 | 66 | fileList.appendChild(row); 67 | file.htmlRow = row; 68 | fileNames.push(file.name); 69 | uploadFile(file); 70 | } 71 | 72 | const selectContainer = document.querySelector("form .select_container"); 73 | 74 | const updateSearchBar = () => { 75 | const convertToInput = document.querySelector( 76 | "input[name='convert_to_search']", 77 | ); 78 | const convertToPopup = document.querySelector(".convert_to_popup"); 79 | const convertToGroupElements = document.querySelectorAll(".convert_to_group"); 80 | const convertToGroups = {}; 81 | const convertToElement = document.querySelector("select[name='convert_to']"); 82 | 83 | const showMatching = (search) => { 84 | for (const [targets, groupElement] of Object.values(convertToGroups)) { 85 | let matchingTargetsFound = 0; 86 | for (const target of targets) { 87 | if (target.dataset.target.includes(search)) { 88 | matchingTargetsFound++; 89 | target.classList.remove("hidden"); 90 | target.classList.add("flex"); 91 | } else { 92 | target.classList.add("hidden"); 93 | target.classList.remove("flex"); 94 | } 95 | } 96 | 97 | if (matchingTargetsFound === 0) { 98 | groupElement.classList.add("hidden"); 99 | groupElement.classList.remove("flex"); 100 | } else { 101 | groupElement.classList.remove("hidden"); 102 | groupElement.classList.add("flex"); 103 | } 104 | } 105 | }; 106 | 107 | for (const groupElement of convertToGroupElements) { 108 | const groupName = groupElement.dataset.converter; 109 | 110 | const targetElements = groupElement.querySelectorAll(".target"); 111 | const targets = Array.from(targetElements); 112 | 113 | for (const target of targets) { 114 | target.onmousedown = () => { 115 | convertToElement.value = target.dataset.value; 116 | convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`; 117 | formatSelected = true; 118 | if (pendingFiles === 0 && fileNames.length > 0) { 119 | convertButton.disabled = false; 120 | } 121 | showMatching(""); 122 | }; 123 | } 124 | 125 | convertToGroups[groupName] = [targets, groupElement]; 126 | } 127 | 128 | convertToInput.addEventListener("input", (e) => { 129 | showMatching(e.target.value.toLowerCase()); 130 | }); 131 | 132 | convertToInput.addEventListener("search", () => { 133 | // when the user clears the search bar using the 'x' button 134 | convertButton.disabled = true; 135 | formatSelected = false; 136 | }); 137 | 138 | convertToInput.addEventListener("blur", (e) => { 139 | // Keep the popup open even when clicking on a target button 140 | // for a split second to allow the click to go through 141 | if (e?.relatedTarget?.classList?.contains("target")) { 142 | convertToPopup.classList.add("hidden"); 143 | convertToPopup.classList.remove("flex"); 144 | return; 145 | } 146 | 147 | convertToPopup.classList.add("hidden"); 148 | convertToPopup.classList.remove("flex"); 149 | }); 150 | 151 | convertToInput.addEventListener("focus", () => { 152 | convertToPopup.classList.remove("hidden"); 153 | convertToPopup.classList.add("flex"); 154 | }); 155 | }; 156 | 157 | // Add a 'change' event listener to the file input element 158 | fileInput.addEventListener("change", (e) => { 159 | const files = e.target.files; 160 | for (const file of files) { 161 | handleFile(file); 162 | } 163 | }); 164 | 165 | const setTitle = () => { 166 | const title = document.querySelector("h1"); 167 | title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`; 168 | }; 169 | 170 | // Add a onclick for the delete button 171 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 172 | const deleteRow = (target) => { 173 | const filename = target.parentElement.parentElement.children[0].textContent; 174 | const row = target.parentElement.parentElement; 175 | row.remove(); 176 | 177 | // remove from fileNames 178 | const index = fileNames.indexOf(filename); 179 | fileNames.splice(index, 1); 180 | 181 | // reset fileInput 182 | fileInput.value = ""; 183 | 184 | // if fileNames is empty, reset fileType 185 | if (fileNames.length === 0) { 186 | fileType = null; 187 | fileInput.removeAttribute("accept"); 188 | convertButton.disabled = true; 189 | setTitle(); 190 | } 191 | 192 | fetch(`${webroot}/delete`, { 193 | method: "POST", 194 | body: JSON.stringify({ filename: filename }), 195 | headers: { 196 | "Content-Type": "application/json", 197 | }, 198 | }) 199 | .catch((err) => console.log(err)); 200 | }; 201 | 202 | const uploadFile = (file) => { 203 | convertButton.disabled = true; 204 | convertButton.textContent = "Uploading..."; 205 | pendingFiles += 1; 206 | 207 | const formData = new FormData(); 208 | formData.append("file", file, file.name); 209 | 210 | let xhr = new XMLHttpRequest(); 211 | 212 | xhr.open("POST", `${webroot}/upload`, true); 213 | 214 | xhr.onload = () => { 215 | let data = JSON.parse(xhr.responseText); 216 | 217 | pendingFiles -= 1; 218 | if (pendingFiles === 0) { 219 | if (formatSelected) { 220 | convertButton.disabled = false; 221 | } 222 | convertButton.textContent = "Convert"; 223 | } 224 | 225 | //Remove the progress bar when upload is done 226 | let progressbar = file.htmlRow.getElementsByTagName("progress"); 227 | progressbar[0].parentElement.remove(); 228 | console.log(data); 229 | }; 230 | 231 | xhr.upload.onprogress = (e) => { 232 | let sent = e.loaded; 233 | let total = e.total; 234 | console.log(`upload progress (${file.name}):`, (100 * sent) / total); 235 | 236 | let progressbar = file.htmlRow.getElementsByTagName("progress"); 237 | progressbar[0].value = ((100 * sent) / total); 238 | }; 239 | 240 | xhr.onerror = (e) => { 241 | console.log(e); 242 | }; 243 | 244 | xhr.send(formData); 245 | }; 246 | 247 | const formConvert = document.querySelector(`form[action='${webroot}/convert']`); 248 | 249 | formConvert.addEventListener("submit", () => { 250 | const hiddenInput = document.querySelector("input[name='file_names']"); 251 | hiddenInput.value = JSON.stringify(fileNames); 252 | }); 253 | 254 | updateSearchBar(); 255 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ConvertX | Self Hosted File Converter", 3 | "short_name": "ConvertX", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#a5d601", 17 | "background_color": "#13171f", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":disableDependencyDashboard" 6 | ], 7 | "lockFileMaintenance": { 8 | "enabled": true, 9 | "automerge": true 10 | } 11 | } -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | import "@total-typescript/ts-reset"; -------------------------------------------------------------------------------- /src/components/base.tsx: -------------------------------------------------------------------------------- 1 | import { Html } from "@elysiajs/html"; 2 | import { version } from "../../package.json"; 3 | 4 | export const BaseHtml = ({ 5 | children, 6 | title = "ConvertX", 7 | webroot = "", 8 | }: { 9 | children: JSX.Element; 10 | title?: string; 11 | webroot?: string; 12 | }) => ( 13 | 14 | 15 | 16 | 17 | 18 | {title} 19 | 20 | 25 | 31 | 37 | 38 | 39 | 40 | {children} 41 | 56 | 57 | 58 | ); 59 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { Html } from "@kitajs/html"; 2 | 3 | export const Header = ({ 4 | loggedIn, 5 | accountRegistration, 6 | allowUnauthenticated, 7 | hideHistory, 8 | webroot = "", 9 | }: { 10 | loggedIn?: boolean; 11 | accountRegistration?: boolean; 12 | allowUnauthenticated?: boolean; 13 | hideHistory?: boolean; 14 | webroot?: string; 15 | }) => { 16 | let rightNav: JSX.Element; 17 | if (loggedIn) { 18 | rightNav = ( 19 | 60 | ); 61 | } else { 62 | rightNav = ( 63 | 89 | ); 90 | } 91 | 92 | return ( 93 |
94 | 104 |
105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/converters/assimp.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | export const properties = { 4 | from: { 5 | object: [ 6 | "3d", 7 | "3ds", 8 | "3mf", 9 | "ac", 10 | "ac3d", 11 | "acc", 12 | "amf", 13 | "amj", 14 | "ase", 15 | "ask", 16 | "assbin", 17 | "b3d", 18 | "blend", 19 | "bsp", 20 | "bvh", 21 | "cob", 22 | "csm", 23 | "dae", 24 | "dxf", 25 | "enff", 26 | "fbx", 27 | "glb", 28 | "gltf", 29 | "hmb", 30 | "hmp", 31 | "ifc", 32 | "ifczip", 33 | "iqm", 34 | "irr", 35 | "irrmesh", 36 | "lwo", 37 | "lws", 38 | "lxo", 39 | "m3d", 40 | "md2", 41 | "md3", 42 | "md5anim", 43 | "md5camera", 44 | "md5mesh", 45 | "mdc", 46 | "mdl", 47 | "mesh.xml", 48 | "mesh", 49 | "mot", 50 | "ms3d", 51 | "ndo", 52 | "nff", 53 | "obj", 54 | "off", 55 | "ogex", 56 | "pk3", 57 | "ply", 58 | "pmx", 59 | "prj", 60 | "q3o", 61 | "q3s", 62 | "raw", 63 | "scn", 64 | "sib", 65 | "smd", 66 | "step", 67 | "stl", 68 | "stp", 69 | "ter", 70 | "uc", 71 | "usd", 72 | "usda", 73 | "usdc", 74 | "usdz", 75 | "vta", 76 | "x", 77 | "x3d", 78 | "x3db", 79 | "xgl", 80 | "xml", 81 | "zae", 82 | "zgl", 83 | ], 84 | }, 85 | to: { 86 | object: [ 87 | "3ds", 88 | "3mf", 89 | "assbin", 90 | "assjson", 91 | "assxml", 92 | "collada", 93 | "dae", 94 | "fbx", 95 | "fbxa", 96 | "glb", 97 | "glb2", 98 | "gltf", 99 | "gltf2", 100 | "json", 101 | "obj", 102 | "objnomtl", 103 | "pbrt", 104 | "ply", 105 | "plyb", 106 | "stl", 107 | "stlb", 108 | "stp", 109 | "x", 110 | ], 111 | }, 112 | }; 113 | 114 | export async function convert( 115 | filePath: string, 116 | fileType: string, 117 | convertTo: string, 118 | targetPath: string, 119 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 120 | options?: unknown, 121 | ): Promise { 122 | return new Promise((resolve, reject) => { 123 | execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => { 124 | if (error) { 125 | reject(`error: ${error}`); 126 | } 127 | 128 | if (stdout) { 129 | console.log(`stdout: ${stdout}`); 130 | } 131 | 132 | if (stderr) { 133 | console.error(`stderr: ${stderr}`); 134 | } 135 | 136 | resolve("Done"); 137 | }); 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /src/converters/calibre.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | export const properties = { 4 | from: { 5 | document: [ 6 | "azw4", 7 | "chm", 8 | "cbr", 9 | "cbz", 10 | "cbt", 11 | "cba", 12 | "cb7", 13 | "djvu", 14 | "docx", 15 | "epub", 16 | "fb2", 17 | "htlz", 18 | "html", 19 | "lit", 20 | "lrf", 21 | "mobi", 22 | "odt", 23 | "pdb", 24 | "pdf", 25 | "pml", 26 | "rb", 27 | "rtf", 28 | "recipe", 29 | "snb", 30 | "tcr", 31 | "txt", 32 | ], 33 | }, 34 | to: { 35 | document: [ 36 | "azw3", 37 | "docx", 38 | "epub", 39 | "fb2", 40 | "html", 41 | "htmlz", 42 | "lit", 43 | "lrf", 44 | "mobi", 45 | "oeb", 46 | "pdb", 47 | "pdf", 48 | "pml", 49 | "rb", 50 | "rtf", 51 | "snb", 52 | "tcr", 53 | "txt", 54 | "txtz", 55 | ], 56 | }, 57 | }; 58 | 59 | export async function convert( 60 | filePath: string, 61 | fileType: string, 62 | convertTo: string, 63 | targetPath: string, 64 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 65 | options?: unknown, 66 | ): Promise { 67 | return new Promise((resolve, reject) => { 68 | execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => { 69 | if (error) { 70 | reject(`error: ${error}`); 71 | } 72 | 73 | if (stdout) { 74 | console.log(`stdout: ${stdout}`); 75 | } 76 | 77 | if (stderr) { 78 | console.error(`stderr: ${stderr}`); 79 | } 80 | 81 | resolve("Done"); 82 | }); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /src/converters/ffmpeg.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | // This could be done dynamically by running `ffmpeg -formats` and parsing the output 4 | export const properties = { 5 | from: { 6 | muxer: [ 7 | "264", 8 | "265", 9 | "266", 10 | "302", 11 | "3dostr", 12 | "3g2", 13 | "3gp", 14 | "4xm", 15 | "669", 16 | "722", 17 | "aa", 18 | "aa3", 19 | "aac", 20 | "aax", 21 | "ac3", 22 | "ac4", 23 | "ace", 24 | "acm", 25 | "act", 26 | "adf", 27 | "adp", 28 | "ads", 29 | "adx", 30 | "aea", 31 | "afc", 32 | "aiff", 33 | "aix", 34 | "al", 35 | "alaw", 36 | "alias_pix", 37 | "alp", 38 | "alsa", 39 | "amf", 40 | "amr", 41 | "amrnb", 42 | "amrwb", 43 | "ams", 44 | "anm", 45 | "ans", 46 | "apc", 47 | "ape", 48 | "apl", 49 | "apm", 50 | "apng", 51 | "aptx", 52 | "aptxhd", 53 | "aqt", 54 | "aqtitle", 55 | "argo_asf", 56 | "argo_brp", 57 | "art", 58 | "asc", 59 | "asf", 60 | "asf_o", 61 | "ass", 62 | "ast", 63 | "au", 64 | "av1", 65 | "avc", 66 | "avi", 67 | "avif", 68 | "avr", 69 | "avs", 70 | "avs2", 71 | "avs3", 72 | "awb", 73 | "bcstm", 74 | "bethsoftvid", 75 | "bfi", 76 | "bfstm", 77 | "bin", 78 | "bink", 79 | "binka", 80 | "bit", 81 | "bitpacked", 82 | "bmv", 83 | "bmp", 84 | "bonk", 85 | "boa", 86 | "brender_pix", 87 | "brstm", 88 | "c2", 89 | "c93", 90 | "caf", 91 | "cavsvideo", 92 | "cdata", 93 | "cdg", 94 | "cdxl", 95 | "cgi", 96 | "cif", 97 | "cine", 98 | "codec2", 99 | "codec2raw", 100 | "concat", 101 | "cri", 102 | "dash", 103 | "dat", 104 | "data", 105 | "daud", 106 | "dav", 107 | "dbm", 108 | "dcstr", 109 | "dds", 110 | "derf", 111 | "dfpwm", 112 | "dfa", 113 | "dhav", 114 | "dif", 115 | "digi", 116 | "dirac", 117 | "diz", 118 | "dmf", 119 | "dnxhd", 120 | "dpx_pipe", 121 | "dsf", 122 | "dsicin", 123 | "dsm", 124 | "dss", 125 | "dtk", 126 | "dtm", 127 | "dts", 128 | "dtshd", 129 | "dv", 130 | "dvbsub", 131 | "dvbtxt", 132 | "dxa", 133 | "ea", 134 | "eac3", 135 | "ea_cdata", 136 | "epaf", 137 | "exr_pipe", 138 | "f32be", 139 | "f32le", 140 | "ec3", 141 | "evc", 142 | "f4v", 143 | "f64be", 144 | "f64le", 145 | "fap", 146 | "far", 147 | "fbdev", 148 | "ffmetadata", 149 | "filmstrip", 150 | "film_cpk", 151 | "fits", 152 | "flac", 153 | "flic", 154 | "flm", 155 | "flv", 156 | "frm", 157 | "fsb", 158 | "fwse", 159 | "g722", 160 | "g723_1", 161 | "g726", 162 | "g726le", 163 | "g729", 164 | "gdm", 165 | "gdv", 166 | "genh", 167 | "gif", 168 | "gsm", 169 | "gxf", 170 | "h261", 171 | "h263", 172 | "h264", 173 | "h265", 174 | "h266", 175 | "h26l", 176 | "hca", 177 | "hcom", 178 | "hevc", 179 | "hls", 180 | "hnm", 181 | "ice", 182 | "ico", 183 | "idcin", 184 | "idf", 185 | "idx", 186 | "iec61883", 187 | "iff", 188 | "ifv", 189 | "ilbc", 190 | "image2", 191 | "imf", 192 | "imx", 193 | "ingenient", 194 | "ipmovie", 195 | "ipu", 196 | "ircam", 197 | "ism", 198 | "isma", 199 | "ismv", 200 | "iss", 201 | "it", 202 | "iv8", 203 | "ivf", 204 | "ivr", 205 | "j2b", 206 | "j2k", 207 | "jack", 208 | "jacosub", 209 | "jv", 210 | "jpegls", 211 | "jpeg", 212 | "jxl", 213 | "kmsgrab", 214 | "kux", 215 | "kvag", 216 | "lavfi", 217 | "laf", 218 | "lmlm4", 219 | "loas", 220 | "lrc", 221 | "luodat", 222 | "lvf", 223 | "lxf", 224 | "m15", 225 | "m2a", 226 | "m4a", 227 | "m4b", 228 | "m4v", 229 | "mac", 230 | "mca", 231 | "mcc", 232 | "mdl", 233 | "med", 234 | "microdvd", 235 | "mj2", 236 | "mjpeg", 237 | "mjpg", 238 | "mk3d", 239 | "mka", 240 | "mks", 241 | "mkv", 242 | "mlp", 243 | "mlv", 244 | "mm", 245 | "mmcmp", 246 | "mmf", 247 | "mms", 248 | "mo3", 249 | "mod", 250 | "mods", 251 | "moflex", 252 | "mov", 253 | "mp2", 254 | "mp3", 255 | "mp4", 256 | "mpa", 257 | "mpc", 258 | "mpc8", 259 | "mpeg", 260 | "mpg", 261 | "mpjpeg", 262 | "mpl2", 263 | "mpo", 264 | "mpsub", 265 | "mptm", 266 | "msbc", 267 | "msf", 268 | "msnwctcp", 269 | "msp", 270 | "mt2", 271 | "mtaf", 272 | "mtm", 273 | "mtv", 274 | "mulaw", 275 | "musx", 276 | "mv", 277 | "mvi", 278 | "mxf", 279 | "mxg", 280 | "nc", 281 | "nfo", 282 | "nist", 283 | "nistsphere", 284 | "nsp", 285 | "nst", 286 | "nsv", 287 | "nut", 288 | "nuv", 289 | "obu", 290 | "ogg", 291 | "okt", 292 | "oma", 293 | "omg", 294 | "opus", 295 | "openal", 296 | "oss", 297 | "osq", 298 | "paf", 299 | "pdv", 300 | "pam", 301 | "pbm", 302 | "pcx", 303 | "pgmyuv", 304 | "pgm", 305 | "pgx", 306 | "photocd", 307 | "pictor", 308 | "pjs", 309 | "plm", 310 | "pmp", 311 | "png", 312 | "ppm", 313 | "pp", 314 | "psd", 315 | "psm", 316 | "psp", 317 | "psxstr", 318 | "pt36", 319 | "ptm", 320 | "pulse", 321 | "pva", 322 | "pvf", 323 | "qcif", 324 | "qcp", 325 | "qdraw", 326 | "r3d", 327 | "rawvideo", 328 | "rco", 329 | "rcv", 330 | "realtext", 331 | "redspark", 332 | "rgb", 333 | "rl2", 334 | "rm", 335 | "roq", 336 | "rpl", 337 | "rka", 338 | "rsd", 339 | "rso", 340 | "rt", 341 | "rtp", 342 | "rtsp", 343 | "s16be", 344 | "s16le", 345 | "s24be", 346 | "s24le", 347 | "s32be", 348 | "s32le", 349 | "s337m", 350 | "s3m", 351 | "s8", 352 | "sami", 353 | "sap", 354 | "sb", 355 | "sbc", 356 | "sbg", 357 | "scc", 358 | "sdns", 359 | "sdp", 360 | "sdr2", 361 | "sds", 362 | "sdx", 363 | "ser", 364 | "sf", 365 | "sfx", 366 | "sfx2", 367 | "sga", 368 | "sgi", 369 | "shn", 370 | "siff", 371 | "sln", 372 | "smi", 373 | "smjpeg", 374 | "smk", 375 | "smush", 376 | "sndio", 377 | "sol", 378 | "son", 379 | "sox", 380 | "spdif", 381 | "sph", 382 | "srt", 383 | "ss2", 384 | "ssa", 385 | "st26", 386 | "stk", 387 | "stl", 388 | "stm", 389 | "stp", 390 | "str", 391 | "sub", 392 | "sup", 393 | "svag", 394 | "svg", 395 | "svs", 396 | "sw", 397 | "swf", 398 | "tak", 399 | "tco", 400 | "tedcaptions", 401 | "thd", 402 | "thp", 403 | "tiertexseq", 404 | "tif", 405 | "tiff", 406 | "tmv", 407 | "truehd", 408 | "tta", 409 | "tty", 410 | "txd", 411 | "txt", 412 | "ty", 413 | "ty+", 414 | "u16be", 415 | "u16le", 416 | "u24be", 417 | "u24le", 418 | "u32be", 419 | "u32le", 420 | "u8", 421 | "ub", 422 | "ul", 423 | "ult", 424 | "umx", 425 | "usm", 426 | "uw", 427 | "v", 428 | "v210", 429 | "v210x", 430 | "vag", 431 | "vb", 432 | "vc1", 433 | "vc1test", 434 | "vidc", 435 | "video4linux2", 436 | "viv", 437 | "vividas", 438 | "vivo", 439 | "vmd", 440 | "vobsub", 441 | "voc", 442 | "vpk", 443 | "vplayer", 444 | "vqe", 445 | "vqf", 446 | "vql", 447 | "vt", 448 | "vtt", 449 | "vvc", 450 | "w64", 451 | "wa", 452 | "wav", 453 | "way", 454 | "wc3movie", 455 | "webm", 456 | "webp", 457 | "webvtt", 458 | "wow", 459 | "wsaud", 460 | "wsd", 461 | "wsvqa", 462 | "wtv", 463 | "wv", 464 | "wve", 465 | "x11grab", 466 | "xa", 467 | "xbin", 468 | "xl", 469 | "xm", 470 | "xmd", 471 | "xmv", 472 | "xpk", 473 | "xvag", 474 | "xwma", 475 | "y4m", 476 | "yop", 477 | "yuv", 478 | "yuv10", 479 | ], 480 | }, 481 | to: { 482 | muxer: [ 483 | "264", 484 | "265", 485 | "266", 486 | "302", 487 | "3g2", 488 | "3gp", 489 | "a64", 490 | "aac", 491 | "ac3", 492 | "ac4", 493 | "adts", 494 | "adx", 495 | "afc", 496 | "aif", 497 | "aifc", 498 | "aiff", 499 | "al", 500 | "amr", 501 | "amv", 502 | "apm", 503 | "apng", 504 | "aptx", 505 | "aptxhd", 506 | "asf", 507 | "ass", 508 | "ast", 509 | "au", 510 | "aud", 511 | "av1.mkv", 512 | "av1.mp4", 513 | "avi", 514 | "avif", 515 | "avs", 516 | "avs2", 517 | "avs3", 518 | "bit", 519 | "bmp", 520 | "c2", 521 | "caf", 522 | "cavs", 523 | "chk", 524 | "cpk", 525 | "cvg", 526 | "dfpwm", 527 | "dnxhd", 528 | "dnxhr", 529 | "dpx", 530 | "drc", 531 | "dts", 532 | "dv", 533 | "dvd", 534 | "eac3", 535 | "ec3", 536 | "evc", 537 | "exr", 538 | "f4v", 539 | "ffmeta", 540 | "fits", 541 | "flac", 542 | "flm", 543 | "flv", 544 | "g722", 545 | "gif", 546 | "gsm", 547 | "gxf", 548 | "h261", 549 | "h263", 550 | "h264.mkv", 551 | "h264.mp4", 552 | "h265.mkv", 553 | "h265.mp4", 554 | "h266.mkv", 555 | "hdr", 556 | "hevc", 557 | "ico", 558 | "im1", 559 | "im24", 560 | "im8", 561 | "ircam", 562 | "isma", 563 | "ismv", 564 | "ivf", 565 | "j2c", 566 | "j2k", 567 | "jls", 568 | "jp2", 569 | "jpeg", 570 | "jpg", 571 | "js", 572 | "jss", 573 | "jxl", 574 | "latm", 575 | "lbc", 576 | "ljpg", 577 | "loas", 578 | "lrc", 579 | "m1v", 580 | "m2a", 581 | "m2t", 582 | "m2ts", 583 | "m2v", 584 | "m3u8", 585 | "m4a", 586 | "m4b", 587 | "m4v", 588 | "mjpeg", 589 | "mjpg", 590 | "mkv", 591 | "mlp", 592 | "mmf", 593 | "mov", 594 | "mp2", 595 | "mp3", 596 | "mp4", 597 | "mpa", 598 | "mpd", 599 | "mpeg", 600 | "mpg", 601 | "msbc", 602 | "mts", 603 | "mxf", 604 | "nut", 605 | "obu", 606 | "oga", 607 | "ogg", 608 | "ogv", 609 | "oma", 610 | "opus", 611 | "pam", 612 | "pbm", 613 | "pcm", 614 | "pcx", 615 | "pfm", 616 | "pgm", 617 | "pgmyuv", 618 | "phm", 619 | "pix", 620 | "png", 621 | "ppm", 622 | "psp", 623 | "qoi", 624 | "ra", 625 | "ras", 626 | "rco", 627 | "rcv", 628 | "rgb", 629 | "rm", 630 | "roq", 631 | "rs", 632 | "rso", 633 | "sb", 634 | "sbc", 635 | "scc", 636 | "sf", 637 | "sgi", 638 | "sox", 639 | "spdif", 640 | "spx", 641 | "srt", 642 | "ssa", 643 | "sub", 644 | "sun", 645 | "sunras", 646 | "sup", 647 | "sw", 648 | "swf", 649 | "tco", 650 | "tga", 651 | "thd", 652 | "tif", 653 | "tiff", 654 | "ts", 655 | "tta", 656 | "ttml", 657 | "tun", 658 | "ub", 659 | "ul", 660 | "uw", 661 | "vag", 662 | "vbn", 663 | "vc1", 664 | "vc2", 665 | "vob", 666 | "voc", 667 | "vtt", 668 | "vvc", 669 | "w64", 670 | "wav", 671 | "wbmp", 672 | "webm", 673 | "webp", 674 | "wma", 675 | "wmv", 676 | "wtv", 677 | "wv", 678 | "xbm", 679 | "xface", 680 | "xml", 681 | "xwd", 682 | "y", 683 | "y4m", 684 | "yuv", 685 | ], 686 | }, 687 | }; 688 | 689 | export async function convert( 690 | filePath: string, 691 | fileType: string, 692 | convertTo: string, 693 | targetPath: string, 694 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 695 | options?: unknown, 696 | ): Promise { 697 | let extraArgs: string[] = []; 698 | let message = "Done"; 699 | 700 | if (convertTo === "ico") { 701 | // make sure image is 256x256 or smaller 702 | extraArgs = [ 703 | "-filter:v", 704 | "scale='min(256,iw)':min'(256,ih)':force_original_aspect_ratio=decrease", 705 | ]; 706 | message = "Done: resized to 256x256"; 707 | } 708 | 709 | if (convertTo.split(".").length > 1) { 710 | // support av1.mkv and av1.mp4 and h265.mp4 etc. 711 | const split = convertTo.split("."); 712 | const codec_short = split[0]; 713 | 714 | switch (codec_short) { 715 | case "av1": 716 | extraArgs.push("-c:v", "libaom-av1"); 717 | break; 718 | case "h264": 719 | extraArgs.push("-c:v", "libx264"); 720 | break; 721 | case "h265": 722 | extraArgs.push("-c:v", "libx265"); 723 | break; 724 | case "h266": 725 | extraArgs.push("-c:v", "libx266"); 726 | break; 727 | } 728 | } 729 | 730 | // Parse FFMPEG_ARGS environment variable into array 731 | const ffmpegArgs = process.env.FFMPEG_ARGS 732 | ? process.env.FFMPEG_ARGS.split(/\s+/) 733 | : []; 734 | 735 | return new Promise((resolve, reject) => { 736 | execFile( 737 | "ffmpeg", 738 | [...ffmpegArgs, "-i", filePath, ...extraArgs, targetPath], 739 | (error, stdout, stderr) => { 740 | if (error) { 741 | reject(`error: ${error}`); 742 | } 743 | 744 | if (stdout) { 745 | console.log(`stdout: ${stdout}`); 746 | } 747 | 748 | if (stderr) { 749 | console.error(`stderr: ${stderr}`); 750 | } 751 | 752 | resolve(message); 753 | }, 754 | ); 755 | }); 756 | } 757 | -------------------------------------------------------------------------------- /src/converters/graphicsmagick.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | export const properties = { 4 | from: { 5 | image: [ 6 | "3fr", 7 | "8bim", 8 | "8bimtext", 9 | "8bimwtext", 10 | "app1", 11 | "app1jpeg", 12 | "art", 13 | "arw", 14 | "avs", 15 | "b", 16 | "bie", 17 | "bigtiff", 18 | "bmp", 19 | "c", 20 | "cals", 21 | "caption", 22 | "cin", 23 | "cmyk", 24 | "cmyka", 25 | "cr2", 26 | "crw", 27 | "cur", 28 | "cut", 29 | "dcm", 30 | "dcr", 31 | "dcx", 32 | "dng", 33 | "dpx", 34 | "epdf", 35 | "epi", 36 | "eps", 37 | "epsf", 38 | "epsi", 39 | "ept", 40 | "ept2", 41 | "ept3", 42 | "erf", 43 | "exif", 44 | "fax", 45 | "file", 46 | "fits", 47 | "fractal", 48 | "ftp", 49 | "g", 50 | "gif", 51 | "gif87", 52 | "gradient", 53 | "gray", 54 | "graya", 55 | "heic", 56 | "heif", 57 | "hrz", 58 | "http", 59 | "icb", 60 | "icc", 61 | "icm", 62 | "ico", 63 | "icon", 64 | "identity", 65 | "image", 66 | "iptc", 67 | "iptctext", 68 | "iptcwtext", 69 | "jbg", 70 | "jbig", 71 | "jng", 72 | "jnx", 73 | "jpeg", 74 | "jpg", 75 | "k", 76 | "k25", 77 | "kdc", 78 | "label", 79 | "m", 80 | "mac", 81 | "map", 82 | "mat", 83 | "mef", 84 | "miff", 85 | "mng", 86 | "mono", 87 | "mpc", 88 | "mrw", 89 | "msl", 90 | "mtv", 91 | "mvg", 92 | "nef", 93 | "null", 94 | "o", 95 | "orf", 96 | "otb", 97 | "p7", 98 | "pal", 99 | "palm", 100 | "pam", 101 | "pbm", 102 | "pcd", 103 | "pcds", 104 | "pct", 105 | "pcx", 106 | "pdb", 107 | "pdf", 108 | "pef", 109 | "pfa", 110 | "pfb", 111 | "pgm", 112 | "picon", 113 | "pict", 114 | "pix", 115 | "plasma", 116 | "png", 117 | "png00", 118 | "png24", 119 | "png32", 120 | "png48", 121 | "png64", 122 | "png8", 123 | "pnm", 124 | "ppm", 125 | "ps", 126 | "ptif", 127 | "pwp", 128 | "r", 129 | "raf", 130 | "ras", 131 | "rgb", 132 | "rgba", 133 | "rla", 134 | "rle", 135 | "sct", 136 | "sfw", 137 | "sgi", 138 | "sr2", 139 | "srf", 140 | "stegano", 141 | "sun", 142 | "svg", 143 | "svgz", 144 | "text", 145 | "tga", 146 | "tif", 147 | "tiff", 148 | "tile", 149 | "tim", 150 | "topol", 151 | "ttf", 152 | "txt", 153 | "uyvy", 154 | "vda", 155 | "vicar", 156 | "vid", 157 | "viff", 158 | "vst", 159 | "wbmp", 160 | "webp", 161 | "wmf", 162 | "wpg", 163 | "x3f", 164 | "xbm", 165 | "xc", 166 | "xcf", 167 | "xmp", 168 | "xpm", 169 | "xv", 170 | "xwd", 171 | "y", 172 | "yuv", 173 | ], 174 | }, 175 | to: { 176 | image: [ 177 | "8bim", 178 | "8bimtext", 179 | "8bimwtext", 180 | "app1", 181 | "app1jpeg", 182 | "art", 183 | "avs", 184 | "b", 185 | "bie", 186 | "bigtiff", 187 | "bmp", 188 | "bmp2", 189 | "bmp3", 190 | "brf", 191 | "c", 192 | "cals", 193 | "cin", 194 | "cmyk", 195 | "cmyka", 196 | "dcx", 197 | "dpx", 198 | "epdf", 199 | "epi", 200 | "eps", 201 | "eps2", 202 | "eps3", 203 | "epsf", 204 | "epsi", 205 | "ept", 206 | "ept2", 207 | "ept3", 208 | "exif", 209 | "fax", 210 | "fits", 211 | "g", 212 | "gif", 213 | "gif87", 214 | "gray", 215 | "graya", 216 | "histogram", 217 | "html", 218 | "icb", 219 | "icc", 220 | "icm", 221 | "info", 222 | "iptc", 223 | "iptctext", 224 | "iptcwtext", 225 | "isobrl", 226 | "isobrl6", 227 | "jbg", 228 | "jbig", 229 | "jng", 230 | "jpeg", 231 | "k", 232 | "m", 233 | "m2v", 234 | "map", 235 | "mat", 236 | "matte", 237 | "miff", 238 | "mng", 239 | "mono", 240 | "mpc", 241 | "mpeg", 242 | "mpg", 243 | "msl", 244 | "mtv", 245 | "mvg", 246 | "null", 247 | "o", 248 | "otb", 249 | "p7", 250 | "pal", 251 | "pam", 252 | "pbm", 253 | "pcd", 254 | "pcds", 255 | "pcl", 256 | "pct", 257 | "pcx", 258 | "pdb", 259 | "pdf", 260 | "pgm", 261 | "picon", 262 | "pict", 263 | "png", 264 | "png00", 265 | "png24", 266 | "png32", 267 | "png48", 268 | "png64", 269 | "png8", 270 | "pnm", 271 | "ppm", 272 | "preview", 273 | "ps", 274 | "ps2", 275 | "ps3", 276 | "ptif", 277 | "r", 278 | "ras", 279 | "rgb", 280 | "rgba", 281 | "sgi", 282 | "shtml", 283 | "sun", 284 | "text", 285 | "tga", 286 | "tiff", 287 | "txt", 288 | "ubrl", 289 | "ubrl6", 290 | "uil", 291 | "uyvy", 292 | "vda", 293 | "vicar", 294 | "vid", 295 | "viff", 296 | "vst", 297 | "wbmp", 298 | "webp", 299 | "x", 300 | "xbm", 301 | "xmp", 302 | "xpm", 303 | "xv", 304 | "xwd", 305 | "y", 306 | "yuv", 307 | ], 308 | }, 309 | }; 310 | 311 | export function convert( 312 | filePath: string, 313 | fileType: string, 314 | convertTo: string, 315 | targetPath: string, 316 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 317 | options?: unknown, 318 | ): Promise { 319 | return new Promise((resolve, reject) => { 320 | execFile( 321 | "gm", 322 | ["convert", filePath, targetPath], 323 | (error, stdout, stderr) => { 324 | if (error) { 325 | reject(`error: ${error}`); 326 | } 327 | 328 | if (stdout) { 329 | console.log(`stdout: ${stdout}`); 330 | } 331 | 332 | if (stderr) { 333 | console.error(`stderr: ${stderr}`); 334 | } 335 | 336 | resolve("Done"); 337 | }, 338 | ); 339 | }); 340 | } 341 | -------------------------------------------------------------------------------- /src/converters/imagemagick.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | // declare possible conversions 4 | export const properties = { 5 | from: { 6 | images: [ 7 | "3fr", 8 | "3g2", 9 | "3gp", 10 | "aai", 11 | "ai", 12 | "apng", 13 | "art", 14 | "arw", 15 | "avci", 16 | "avi", 17 | "avif", 18 | "avs", 19 | "bayer", 20 | "bayera", 21 | "bgr", 22 | "bgra", 23 | "bgro", 24 | "bmp", 25 | "bmp2", 26 | "bmp3", 27 | "cal", 28 | "cals", 29 | "canvas", 30 | "caption", 31 | "cin", 32 | "clip", 33 | "clipboard", 34 | "cmyk", 35 | "cmyka", 36 | "cr2", 37 | "cr3", 38 | "crw", 39 | "cube", 40 | "cur", 41 | "cut", 42 | "data", 43 | "dcm", 44 | "dcr", 45 | "dcraw", 46 | "dcx", 47 | "dds", 48 | "dfont", 49 | "dng", 50 | "dpx", 51 | "dxt1", 52 | "dxt5", 53 | "emf", 54 | "epdf", 55 | "epi", 56 | "eps", 57 | "epsf", 58 | "epsi", 59 | "ept", 60 | "ept2", 61 | "ept3", 62 | "erf", 63 | "exr", 64 | "farbfeld", 65 | "fax", 66 | "ff", 67 | "fff", 68 | "file", 69 | "fits", 70 | "fl32", 71 | "flif", 72 | "flv", 73 | "fractal", 74 | "ftp", 75 | "fts", 76 | "ftxt", 77 | "g3", 78 | "g4", 79 | "gif", 80 | "gif87", 81 | "gradient", 82 | "gray", 83 | "graya", 84 | "group4", 85 | "hald", 86 | "hdr", 87 | "heic", 88 | "heif", 89 | "hrz", 90 | "http", 91 | "https", 92 | "icb", 93 | "ico", 94 | "icon", 95 | "iiq", 96 | "inline", 97 | "ipl", 98 | "j2c", 99 | "j2k", 100 | "jng", 101 | "jnx", 102 | "jp2", 103 | "jpc", 104 | "jpe", 105 | "jpeg", 106 | "jpg", 107 | "jpm", 108 | "jps", 109 | "jpt", 110 | "jxl", 111 | "k25", 112 | "kdc", 113 | "label", 114 | "m2v", 115 | "m4v", 116 | "mac", 117 | "map", 118 | "mask", 119 | "mat", 120 | "mdc", 121 | "mef", 122 | "miff", 123 | "mkv", 124 | "mng", 125 | "mono", 126 | "mos", 127 | "mov", 128 | "mp4", 129 | "mpc", 130 | "mpeg", 131 | "mpg", 132 | "mpo", 133 | "mrw", 134 | "msl", 135 | "msvg", 136 | "mtv", 137 | "mvg", 138 | "nef", 139 | "nrw", 140 | "null", 141 | "ora", 142 | "orf", 143 | "otb", 144 | "otf", 145 | "pal", 146 | "palm", 147 | "pam", 148 | "pango", 149 | "pattern", 150 | "pbm", 151 | "pcd", 152 | "pcds", 153 | "pcl", 154 | "pct", 155 | "pcx", 156 | "pdb", 157 | "pdf", 158 | "pdfa", 159 | "pef", 160 | "pes", 161 | "pfa", 162 | "pfb", 163 | "pfm", 164 | "pgm", 165 | "pgx", 166 | "phm", 167 | "picon", 168 | "pict", 169 | "pix", 170 | "pjpeg", 171 | "plasma", 172 | "png", 173 | "png00", 174 | "png24", 175 | "png32", 176 | "png48", 177 | "png64", 178 | "png8", 179 | "pnm", 180 | "pocketmod", 181 | "ppm", 182 | "ps", 183 | "psb", 184 | "psd", 185 | "ptif", 186 | "pwp", 187 | "qoi", 188 | "radial", 189 | "raf", 190 | "ras", 191 | "raw", 192 | "rgb", 193 | "rgb565", 194 | "rgba", 195 | "rgbo", 196 | "rgf", 197 | "rla", 198 | "rle", 199 | "rmf", 200 | "rsvg", 201 | "rw2", 202 | "rwl", 203 | "scr", 204 | "screenshot", 205 | "sct", 206 | "sfw", 207 | "sgi", 208 | "six", 209 | "sixel", 210 | "sr2", 211 | "srf", 212 | "srw", 213 | "stegano", 214 | "sti", 215 | "strimg", 216 | "sun", 217 | "svg", 218 | "svgz", 219 | "text", 220 | "tga", 221 | "tiff", 222 | "tiff64", 223 | "tile", 224 | "tim", 225 | "tm2", 226 | "ttc", 227 | "ttf", 228 | "txt", 229 | "uyvy", 230 | "vda", 231 | "vicar", 232 | "vid", 233 | "viff", 234 | "vips", 235 | "vst", 236 | "wbmp", 237 | "webm", 238 | "webp", 239 | "wmf", 240 | "wmv", 241 | "wpg", 242 | "x3f", 243 | "xbm", 244 | "xc", 245 | "xcf", 246 | "xpm", 247 | "xps", 248 | "xv", 249 | "ycbcr", 250 | "ycbcra", 251 | "yuv", 252 | ], 253 | }, 254 | to: { 255 | images: [ 256 | "aai", 257 | "ai", 258 | "apng", 259 | "art", 260 | "ashlar", 261 | "avif", 262 | "avs", 263 | "bayer", 264 | "bayera", 265 | "bgr", 266 | "bgra", 267 | "bgro", 268 | "bmp", 269 | "bmp2", 270 | "bmp3", 271 | "brf", 272 | "cal", 273 | "cals", 274 | "cin", 275 | "cip", 276 | "clip", 277 | "clipboard", 278 | "cmyk", 279 | "cmyka", 280 | "cur", 281 | "data", 282 | "dcx", 283 | "dds", 284 | "dpx", 285 | "dxt1", 286 | "dxt5", 287 | "epdf", 288 | "epi", 289 | "eps", 290 | "eps2", 291 | "eps3", 292 | "epsf", 293 | "epsi", 294 | "ept", 295 | "ept2", 296 | "ept3", 297 | "exr", 298 | "farbfeld", 299 | "fax", 300 | "ff", 301 | "fits", 302 | "fl32", 303 | "flif", 304 | "flv", 305 | "fts", 306 | "ftxt", 307 | "g3", 308 | "g4", 309 | "gif", 310 | "gif87", 311 | "gray", 312 | "graya", 313 | "group4", 314 | "hdr", 315 | "histogram", 316 | "hrz", 317 | "htm", 318 | "html", 319 | "icb", 320 | "ico", 321 | "icon", 322 | "info", 323 | "inline", 324 | "ipl", 325 | "isobrl", 326 | "isobrl6", 327 | "j2c", 328 | "j2k", 329 | "jng", 330 | "jp2", 331 | "jpc", 332 | "jpe", 333 | "jpeg", 334 | "jpg", 335 | "jpm", 336 | "jps", 337 | "jpt", 338 | "json", 339 | "jxl", 340 | "m2v", 341 | "m4v", 342 | "map", 343 | "mask", 344 | "mat", 345 | "matte", 346 | "miff", 347 | "mkv", 348 | "mng", 349 | "mono", 350 | "mov", 351 | "mp4", 352 | "mpc", 353 | "mpeg", 354 | "mpg", 355 | "msl", 356 | "msvg", 357 | "mtv", 358 | "mvg", 359 | "null", 360 | "otb", 361 | "pal", 362 | "palm", 363 | "pam", 364 | "pbm", 365 | "pcd", 366 | "pcds", 367 | "pcl", 368 | "pct", 369 | "pcx", 370 | "pdb", 371 | "pdf", 372 | "pdfa", 373 | "pfm", 374 | "pgm", 375 | "pgx", 376 | "phm", 377 | "picon", 378 | "pict", 379 | "pjpeg", 380 | "png", 381 | "png00", 382 | "png24", 383 | "png32", 384 | "png48", 385 | "png64", 386 | "png8", 387 | "pnm", 388 | "pocketmod", 389 | "ppm", 390 | "ps", 391 | "ps2", 392 | "ps3", 393 | "psb", 394 | "psd", 395 | "ptif", 396 | "qoi", 397 | "ras", 398 | "rgb", 399 | "rgba", 400 | "rgbo", 401 | "rgf", 402 | "rsvg", 403 | "sgi", 404 | "shtml", 405 | "six", 406 | "sixel", 407 | "sparse", 408 | "strimg", 409 | "sun", 410 | "svg", 411 | "svgz", 412 | "tga", 413 | "thumbnail", 414 | "tiff", 415 | "tiff64", 416 | "txt", 417 | "ubrl", 418 | "ubrl6", 419 | "uil", 420 | "uyvy", 421 | "vda", 422 | "vicar", 423 | "vid", 424 | "viff", 425 | "vips", 426 | "vst", 427 | "wbmp", 428 | "webm", 429 | "webp", 430 | "wmv", 431 | "wpg", 432 | "xbm", 433 | "xpm", 434 | "xv", 435 | "yaml", 436 | "ycbcr", 437 | "ycbcra", 438 | "yuv", 439 | ], 440 | }, 441 | }; 442 | 443 | export function convert( 444 | filePath: string, 445 | fileType: string, 446 | convertTo: string, 447 | targetPath: string, 448 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 449 | options?: unknown, 450 | ): Promise { 451 | let outputArgs: string[] = []; 452 | let inputArgs: string[] = []; 453 | 454 | if (convertTo === "ico") { 455 | outputArgs = [ 456 | "-define", 457 | "icon:auto-resize=256,128,64,48,32,16", 458 | "-background", 459 | "none", 460 | ]; 461 | 462 | if (fileType === "svg") { 463 | // this might be a bit too much, but it works 464 | inputArgs = ["-background", "none", "-density", "512"]; 465 | } 466 | } 467 | 468 | return new Promise((resolve, reject) => { 469 | execFile( 470 | "magick", 471 | [...inputArgs, filePath, ...outputArgs, targetPath], 472 | (error, stdout, stderr) => { 473 | if (error) { 474 | reject(`error: ${error}`); 475 | } 476 | 477 | if (stdout) { 478 | console.log(`stdout: ${stdout}`); 479 | } 480 | 481 | if (stderr) { 482 | console.error(`stderr: ${stderr}`); 483 | } 484 | 485 | resolve("Done"); 486 | }, 487 | ); 488 | }); 489 | } 490 | -------------------------------------------------------------------------------- /src/converters/inkscape.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | export const properties = { 4 | from: { 5 | images: ["svg", "pdf", "eps", "ps", "wmf", "emf", "png"], 6 | }, 7 | to: { 8 | images: [ 9 | "dxf", 10 | "emf", 11 | "eps", 12 | "fxg", 13 | "gpl", 14 | "hpgl", 15 | "html", 16 | "odg", 17 | "pdf", 18 | "png", 19 | "pov", 20 | "ps", 21 | "sif", 22 | "svg", 23 | "svgz", 24 | "tex", 25 | "wmf", 26 | ], 27 | }, 28 | }; 29 | 30 | export function convert( 31 | filePath: string, 32 | fileType: string, 33 | convertTo: string, 34 | targetPath: string, 35 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 36 | options?: unknown, 37 | ): Promise { 38 | return new Promise((resolve, reject) => { 39 | execFile( 40 | "inkscape", 41 | [filePath, "-o", targetPath], 42 | (error, stdout, stderr) => { 43 | if (error) { 44 | reject(`error: ${error}`); 45 | } 46 | 47 | if (stdout) { 48 | console.log(`stdout: ${stdout}`); 49 | } 50 | 51 | if (stderr) { 52 | console.error(`stderr: ${stderr}`); 53 | } 54 | 55 | resolve("Done"); 56 | }, 57 | ); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/converters/libheif.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "child_process"; 2 | 3 | export const properties = { 4 | from: { 5 | images: [ 6 | "avci", 7 | "avcs", 8 | "avif", 9 | "h264", 10 | "heic", 11 | "heics", 12 | "heif", 13 | "heifs", 14 | "hif", 15 | "mkv", 16 | "mp4", 17 | ], 18 | }, 19 | to: { 20 | images: ["jpeg", "png", "y4m"], 21 | }, 22 | }; 23 | 24 | export function convert( 25 | filePath: string, 26 | fileType: string, 27 | convertTo: string, 28 | targetPath: string, 29 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 30 | options?: unknown, 31 | ): Promise { 32 | return new Promise((resolve, reject) => { 33 | execFile( 34 | "heif-convert", 35 | [filePath, targetPath], 36 | (error, stdout, stderr) => { 37 | if (error) { 38 | reject(`error: ${error}`); 39 | } 40 | 41 | if (stdout) { 42 | console.log(`stdout: ${stdout}`); 43 | } 44 | 45 | if (stderr) { 46 | console.error(`stderr: ${stderr}`); 47 | } 48 | 49 | resolve("Done"); 50 | }, 51 | ); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/converters/libjxl.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | // declare possible conversions 4 | export const properties = { 5 | from: { 6 | jxl: ["jxl"], 7 | images: [ 8 | "apng", 9 | "exr", 10 | "gif", 11 | "jpeg", 12 | "pam", 13 | "pfm", 14 | "pgm", 15 | "pgx", 16 | "png", 17 | "ppm", 18 | ], 19 | }, 20 | to: { 21 | jxl: [ 22 | "apng", 23 | "exr", 24 | "gif", 25 | "jpeg", 26 | "pam", 27 | "pfm", 28 | "pgm", 29 | "pgx", 30 | "png", 31 | "ppm", 32 | ], 33 | images: ["jxl"], 34 | }, 35 | }; 36 | 37 | export function convert( 38 | filePath: string, 39 | fileType: string, 40 | convertTo: string, 41 | targetPath: string, 42 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 43 | options?: unknown, 44 | ): Promise { 45 | let tool = ""; 46 | if (fileType === "jxl") { 47 | tool = "djxl"; 48 | } 49 | 50 | if (convertTo === "jxl") { 51 | tool = "cjxl"; 52 | } 53 | 54 | return new Promise((resolve, reject) => { 55 | execFile(tool, [filePath, targetPath], (error, stdout, stderr) => { 56 | if (error) { 57 | reject(`error: ${error}`); 58 | } 59 | 60 | if (stdout) { 61 | console.log(`stdout: ${stdout}`); 62 | } 63 | 64 | if (stderr) { 65 | console.error(`stderr: ${stderr}`); 66 | } 67 | 68 | resolve("Done"); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /src/converters/main.ts: -------------------------------------------------------------------------------- 1 | import { normalizeFiletype } from "../helpers/normalizeFiletype"; 2 | import { convert as convertassimp, properties as propertiesassimp } from "./assimp"; 3 | import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg"; 4 | import { convert as convertGraphicsmagick, properties as propertiesGraphicsmagick } from "./graphicsmagick"; 5 | import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape"; 6 | import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl"; 7 | import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc"; 8 | import { convert as convertresvg, properties as propertiesresvg } from "./resvg"; 9 | import { convert as convertImage, properties as propertiesImage } from "./vips"; 10 | import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex"; 11 | import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre"; 12 | import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif"; 13 | import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace"; 14 | import { convert as convertImagemagick, properties as propertiesImagemagick } from "./imagemagick"; 15 | 16 | 17 | // This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular 18 | 19 | const properties: Record< 20 | string, 21 | { 22 | properties: { 23 | from: Record; 24 | to: Record; 25 | options?: Record< 26 | string, 27 | Record< 28 | string, 29 | { 30 | description: string; 31 | type: string; 32 | default: number; 33 | } 34 | > 35 | >; 36 | }; 37 | converter: ( 38 | filePath: string, 39 | fileType: string, 40 | convertTo: string, 41 | targetPath: string, 42 | 43 | options?: unknown, 44 | ) => unknown; 45 | } 46 | > = { 47 | libjxl: { 48 | properties: propertiesLibjxl, 49 | converter: convertLibjxl, 50 | }, 51 | resvg: { 52 | properties: propertiesresvg, 53 | converter: convertresvg, 54 | }, 55 | vips: { 56 | properties: propertiesImage, 57 | converter: convertImage, 58 | }, 59 | libheif: { 60 | properties: propertiesLibheif, 61 | converter: convertLibheif, 62 | }, 63 | xelatex: { 64 | properties: propertiesxelatex, 65 | converter: convertxelatex, 66 | }, 67 | calibre: { 68 | properties: propertiesCalibre, 69 | converter: convertCalibre, 70 | }, 71 | pandoc: { 72 | properties: propertiesPandoc, 73 | converter: convertPandoc, 74 | }, 75 | imagemagick: { 76 | properties: propertiesImagemagick, 77 | converter: convertImagemagick, 78 | }, 79 | graphicsmagick: { 80 | properties: propertiesGraphicsmagick, 81 | converter: convertGraphicsmagick, 82 | }, 83 | inkscape: { 84 | properties: propertiesInkscape, 85 | converter: convertInkscape, 86 | }, 87 | assimp: { 88 | properties: propertiesassimp, 89 | converter: convertassimp, 90 | }, 91 | ffmpeg: { 92 | properties: propertiesFFmpeg, 93 | converter: convertFFmpeg, 94 | }, 95 | potrace: { 96 | properties: propertiesPotrace, 97 | converter: convertPotrace, 98 | }, 99 | }; 100 | 101 | export async function mainConverter( 102 | inputFilePath: string, 103 | fileTypeOriginal: string, 104 | convertTo: string, 105 | targetPath: string, 106 | options?: unknown, 107 | converterName?: string, 108 | ) { 109 | const fileType = normalizeFiletype(fileTypeOriginal); 110 | 111 | let converterFunc: typeof properties["libjxl"]["converter"] | undefined; 112 | 113 | if (converterName) { 114 | converterFunc = properties[converterName]?.converter; 115 | } else { 116 | // Iterate over each converter in properties 117 | for (converterName in properties) { 118 | const converterObj = properties[converterName]; 119 | 120 | if (!converterObj) { 121 | break; 122 | } 123 | 124 | for (const key in converterObj.properties.from) { 125 | if ( 126 | converterObj?.properties?.from[key]?.includes(fileType) && 127 | converterObj?.properties?.to[key]?.includes(convertTo) 128 | ) { 129 | converterFunc = converterObj.converter; 130 | break; 131 | } 132 | } 133 | } 134 | } 135 | 136 | if (!converterFunc) { 137 | console.log( 138 | `No available converter supports converting from ${fileType} to ${convertTo}.`, 139 | ); 140 | return "File type not supported"; 141 | } 142 | 143 | try { 144 | const result = await converterFunc( 145 | inputFilePath, 146 | fileType, 147 | convertTo, 148 | targetPath, 149 | options, 150 | ); 151 | 152 | console.log( 153 | `Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`, 154 | result, 155 | ); 156 | 157 | if (typeof result === "string") { 158 | return result; 159 | } 160 | 161 | return "Done"; 162 | } catch (error) { 163 | console.error( 164 | `Failed to convert ${inputFilePath} from ${fileType} to ${convertTo} using ${converterName}.`, 165 | error, 166 | ); 167 | return "Failed, check logs"; 168 | } 169 | } 170 | 171 | const possibleTargets: Record> = {}; 172 | 173 | for (const converterName in properties) { 174 | const converterProperties = properties[converterName]?.properties; 175 | 176 | if (!converterProperties) { 177 | continue; 178 | } 179 | 180 | for (const key in converterProperties.from) { 181 | if (converterProperties.from[key] === undefined) { 182 | continue; 183 | } 184 | 185 | for (const extension of converterProperties.from[key] ?? []) { 186 | if (!possibleTargets[extension]) { 187 | possibleTargets[extension] = {}; 188 | } 189 | 190 | possibleTargets[extension][converterName] = 191 | converterProperties.to[key] || []; 192 | } 193 | } 194 | } 195 | 196 | export const getPossibleTargets = (from: string): Record => { 197 | const fromClean = normalizeFiletype(from); 198 | 199 | return possibleTargets[fromClean] || {}; 200 | }; 201 | 202 | const possibleInputs: string[] = []; 203 | for (const converterName in properties) { 204 | const converterProperties = properties[converterName]?.properties; 205 | 206 | if (!converterProperties) { 207 | continue; 208 | } 209 | 210 | for (const key in converterProperties.from) { 211 | for (const extension of converterProperties.from[key] ?? []) { 212 | if (!possibleInputs.includes(extension)) { 213 | possibleInputs.push(extension); 214 | } 215 | } 216 | } 217 | } 218 | possibleInputs.sort(); 219 | 220 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 221 | const getPossibleInputs = () => { 222 | return possibleInputs; 223 | }; 224 | 225 | const allTargets: Record = {}; 226 | 227 | for (const converterName in properties) { 228 | const converterProperties = properties[converterName]?.properties; 229 | 230 | if (!converterProperties) { 231 | continue; 232 | } 233 | 234 | for (const key in converterProperties.to) { 235 | if (allTargets[converterName]) { 236 | allTargets[converterName].push(...(converterProperties.to[key] || [])); 237 | } else { 238 | allTargets[converterName] = converterProperties.to[key] || []; 239 | } 240 | } 241 | } 242 | 243 | export const getAllTargets = () => { 244 | return allTargets; 245 | }; 246 | 247 | const allInputs: Record = {}; 248 | for (const converterName in properties) { 249 | const converterProperties = properties[converterName]?.properties; 250 | 251 | if (!converterProperties) { 252 | continue; 253 | } 254 | 255 | for (const key in converterProperties.from) { 256 | if (allInputs[converterName]) { 257 | allInputs[converterName].push(...(converterProperties.from[key] || [])); 258 | } else { 259 | allInputs[converterName] = converterProperties.from[key] || []; 260 | } 261 | } 262 | } 263 | 264 | export const getAllInputs = (converter: string) => { 265 | return allInputs[converter] || []; 266 | }; 267 | 268 | // // count the number of unique formats 269 | // const uniqueFormats = new Set(); 270 | 271 | // for (const converterName in properties) { 272 | // const converterProperties = properties[converterName]?.properties; 273 | 274 | // if (!converterProperties) { 275 | // continue; 276 | // } 277 | 278 | // for (const key in converterProperties.from) { 279 | // for (const extension of converterProperties.from[key] ?? []) { 280 | // uniqueFormats.add(extension); 281 | // } 282 | // } 283 | 284 | // for (const key in converterProperties.to) { 285 | // for (const extension of converterProperties.to[key] ?? []) { 286 | // uniqueFormats.add(extension); 287 | // } 288 | // } 289 | // } 290 | 291 | // // print the number of unique Inputs and Outputs 292 | // console.log(`Unique Formats: ${uniqueFormats.size}`); -------------------------------------------------------------------------------- /src/converters/pandoc.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | export const properties = { 4 | from: { 5 | text: [ 6 | "textile", 7 | "tikiwiki", 8 | "tsv", 9 | "twiki", 10 | "typst", 11 | "vimwiki", 12 | "biblatex", 13 | "bibtex", 14 | "bits", 15 | "commonmark", 16 | "commonmark_x", 17 | "creole", 18 | "csljson", 19 | "csv", 20 | "djot", 21 | "docbook", 22 | "docx", 23 | "dokuwiki", 24 | "endnotexml", 25 | "epub", 26 | "fb2", 27 | "gfm", 28 | "haddock", 29 | "html", 30 | "ipynb", 31 | "jats", 32 | "jira", 33 | "json", 34 | "latex", 35 | "man", 36 | "markdown", 37 | "markdown_mmd", 38 | "markdown_phpextra", 39 | "markdown_strict", 40 | "mediawiki", 41 | "muse", 42 | "pandoc native", 43 | "opml", 44 | "org", 45 | "ris", 46 | "rst", 47 | "rtf", 48 | "t2t", 49 | ], 50 | }, 51 | to: { 52 | text: [ 53 | "tei", 54 | "texinfo", 55 | "textile", 56 | "typst", 57 | "xwiki", 58 | "zimwiki", 59 | "asciidoc", 60 | "asciidoc_legacy", 61 | "asciidoctor", 62 | "beamer", 63 | "biblatex", 64 | "bibtex", 65 | "chunkedhtml", 66 | "commonmark", 67 | "commonmark_x", 68 | "context", 69 | "csljson", 70 | "djot", 71 | "docbook", 72 | "docbook4", 73 | "docbook5", 74 | "docx", 75 | "dokuwiki", 76 | "dzslides", 77 | "epub", 78 | "epub2", 79 | "epub3", 80 | "fb2", 81 | "gfm", 82 | "haddock", 83 | "html", 84 | "html4", 85 | "html5", 86 | "icml", 87 | "ipynb", 88 | "jats", 89 | "jats_archiving", 90 | "jats_articleauthoring", 91 | "jats_publishing", 92 | "jira", 93 | "json", 94 | "latex", 95 | "man", 96 | "markdown", 97 | "markdown_mmd", 98 | "markdown_phpextra", 99 | "markdown_strict", 100 | "markua", 101 | "mediawiki", 102 | "ms", 103 | "muse", 104 | "pandoc native", 105 | "odt", 106 | "opendocument", 107 | "opml", 108 | "org", 109 | "pdf", 110 | "plain", 111 | "pptx", 112 | "revealjs", 113 | "rst", 114 | "rtf", 115 | "s5", 116 | "slideous", 117 | "slidy", 118 | ], 119 | }, 120 | }; 121 | 122 | export function convert( 123 | filePath: string, 124 | fileType: string, 125 | convertTo: string, 126 | targetPath: string, 127 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 128 | options?: unknown, 129 | ): Promise { 130 | // set xelatex here 131 | const xelatex = ["pdf", "latex"]; 132 | 133 | // Build arguments array 134 | const args: string[] = []; 135 | 136 | if (xelatex.includes(convertTo)) { 137 | args.push("--pdf-engine=xelatex"); 138 | } 139 | 140 | args.push(filePath); 141 | args.push("-f", fileType); 142 | args.push("-t", convertTo); 143 | args.push("-o", targetPath); 144 | 145 | return new Promise((resolve, reject) => { 146 | execFile("pandoc", args, (error, stdout, stderr) => { 147 | if (error) { 148 | reject(`error: ${error}`); 149 | } 150 | 151 | if (stdout) { 152 | console.log(`stdout: ${stdout}`); 153 | } 154 | 155 | if (stderr) { 156 | console.error(`stderr: ${stderr}`); 157 | } 158 | 159 | resolve("Done"); 160 | }); 161 | }); 162 | } 163 | -------------------------------------------------------------------------------- /src/converters/potrace.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | export const properties = { 4 | from: { 5 | images: ["pnm", "pbm", "pgm", "bmp"], 6 | }, 7 | to: { 8 | images: ["svg", "pdf", "pdfpage", "eps", "postscript", "ps", "dxf", "geojson", "pgm", "gimppath", "xfig"], 9 | }, 10 | }; 11 | 12 | export function convert( 13 | filePath: string, 14 | fileType: string, 15 | convertTo: string, 16 | targetPath: string, 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | options?: unknown, 19 | ): Promise { 20 | return new Promise((resolve, reject) => { 21 | execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => { 22 | if (error) { 23 | reject(`error: ${error}`); 24 | } 25 | 26 | if (stdout) { 27 | console.log(`stdout: ${stdout}`); 28 | } 29 | 30 | if (stderr) { 31 | console.error(`stderr: ${stderr}`); 32 | } 33 | 34 | resolve("Done"); 35 | }); 36 | }); 37 | } -------------------------------------------------------------------------------- /src/converters/resvg.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | export const properties = { 4 | from: { 5 | images: ["svg"], 6 | }, 7 | to: { 8 | images: ["png"], 9 | }, 10 | }; 11 | 12 | export function convert( 13 | filePath: string, 14 | fileType: string, 15 | convertTo: string, 16 | targetPath: string, 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | options?: unknown, 19 | ): Promise { 20 | return new Promise((resolve, reject) => { 21 | execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => { 22 | if (error) { 23 | reject(`error: ${error}`); 24 | } 25 | 26 | if (stdout) { 27 | console.log(`stdout: ${stdout}`); 28 | } 29 | 30 | if (stderr) { 31 | console.error(`stderr: ${stderr}`); 32 | } 33 | 34 | resolve("Done"); 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/converters/vips.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | // declare possible conversions 4 | export const properties = { 5 | from: { 6 | images: [ 7 | "avif", 8 | "bif", 9 | "csv", 10 | "exr", 11 | "fits", 12 | "gif", 13 | "hdr.gz", 14 | "hdr", 15 | "heic", 16 | "heif", 17 | "img.gz", 18 | "img", 19 | "j2c", 20 | "j2k", 21 | "jp2", 22 | "jpeg", 23 | "jpx", 24 | "jxl", 25 | "mat", 26 | "mrxs", 27 | "ndpi", 28 | "nia.gz", 29 | "nia", 30 | "nii.gz", 31 | "nii", 32 | "pdf", 33 | "pfm", 34 | "pgm", 35 | "pic", 36 | "png", 37 | "ppm", 38 | "raw", 39 | "scn", 40 | "svg", 41 | "svs", 42 | "svslide", 43 | "szi", 44 | "tif", 45 | "tiff", 46 | "v", 47 | "vips", 48 | "vms", 49 | "vmu", 50 | "webp", 51 | "zip", 52 | ], 53 | }, 54 | to: { 55 | images: [ 56 | "avif", 57 | "dzi", 58 | "fits", 59 | "gif", 60 | "hdr.gz", 61 | "heic", 62 | "heif", 63 | "img.gz", 64 | "j2c", 65 | "j2k", 66 | "jp2", 67 | "jpeg", 68 | "jpx", 69 | "jxl", 70 | "mat", 71 | "nia.gz", 72 | "nia", 73 | "nii.gz", 74 | "nii", 75 | "png", 76 | "tiff", 77 | "vips", 78 | "webp", 79 | ], 80 | }, 81 | options: { 82 | svg: { 83 | scale: { 84 | description: "Scale the image up or down", 85 | type: "number", 86 | default: 1, 87 | }, 88 | }, 89 | }, 90 | }; 91 | 92 | export function convert( 93 | filePath: string, 94 | fileType: string, 95 | convertTo: string, 96 | targetPath: string, 97 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 98 | options?: unknown, 99 | ): Promise { 100 | // if (fileType === "svg") { 101 | // const scale = options.scale || 1; 102 | // const metadata = await sharp(filePath).metadata(); 103 | 104 | // if (!metadata || !metadata.width || !metadata.height) { 105 | // throw new Error("Could not get metadata from image"); 106 | // } 107 | 108 | // const newWidth = Math.round(metadata.width * scale); 109 | // const newHeight = Math.round(metadata.height * scale); 110 | 111 | // return await sharp(filePath) 112 | // .resize(newWidth, newHeight) 113 | // .toFormat(convertTo) 114 | // .toFile(targetPath); 115 | // } 116 | let action = "copy"; 117 | if (fileType === "pdf") { 118 | action = "pdfload"; 119 | } 120 | 121 | return new Promise((resolve, reject) => { 122 | execFile( 123 | "vips", 124 | [action, filePath, targetPath], 125 | (error, stdout, stderr) => { 126 | if (error) { 127 | reject(`error: ${error}`); 128 | } 129 | 130 | if (stdout) { 131 | console.log(`stdout: ${stdout}`); 132 | } 133 | 134 | if (stderr) { 135 | console.error(`stderr: ${stderr}`); 136 | } 137 | 138 | resolve("Done"); 139 | }, 140 | ); 141 | }); 142 | } 143 | -------------------------------------------------------------------------------- /src/converters/xelatex.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | 3 | export const properties = { 4 | from: { 5 | text: ["tex", "latex"], 6 | }, 7 | to: { 8 | text: ["pdf"], 9 | }, 10 | }; 11 | 12 | export function convert( 13 | filePath: string, 14 | fileType: string, 15 | convertTo: string, 16 | targetPath: string, 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | options?: unknown, 19 | ): Promise { 20 | return new Promise((resolve, reject) => { 21 | // const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "") 22 | const outputPath = targetPath 23 | .split("/") 24 | .slice(0, -1) 25 | .join("/") 26 | .replace("./", ""); 27 | 28 | execFile( 29 | "latexmk", 30 | [ 31 | "-xelatex", 32 | "-interaction=nonstopmode", 33 | `-output-directory=${outputPath}`, 34 | filePath, 35 | ], 36 | (error, stdout, stderr) => { 37 | if (error) { 38 | reject(`error: ${error}`); 39 | } 40 | 41 | if (stdout) { 42 | console.log(`stdout: ${stdout}`); 43 | } 44 | 45 | if (stderr) { 46 | console.error(`stderr: ${stderr}`); 47 | } 48 | 49 | resolve("Done"); 50 | }, 51 | ); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/helpers/normalizeFiletype.ts: -------------------------------------------------------------------------------- 1 | export const normalizeFiletype = (filetype: string): string => { 2 | const lowercaseFiletype = filetype.toLowerCase(); 3 | 4 | switch (lowercaseFiletype) { 5 | case "jfif": 6 | case "jpg": 7 | return "jpeg"; 8 | case "htm": 9 | return "html"; 10 | case "tex": 11 | return "latex"; 12 | case "md": 13 | return "markdown"; 14 | case "unknown": 15 | return "m4a"; 16 | default: 17 | return lowercaseFiletype; 18 | } 19 | }; 20 | 21 | export const normalizeOutputFiletype = (filetype: string): string => { 22 | const lowercaseFiletype = filetype.toLowerCase(); 23 | 24 | switch (lowercaseFiletype) { 25 | case "jpeg": 26 | return "jpg"; 27 | case "latex": 28 | return "tex"; 29 | case "markdown_phpextra": 30 | case "markdown_strict": 31 | case "markdown_mmd": 32 | case "markdown": 33 | return "md"; 34 | default: 35 | return lowercaseFiletype; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/helpers/printVersions.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "node:child_process"; 2 | import { version } from "../../package.json"; 3 | 4 | console.log(`ConvertX v${version}`); 5 | 6 | if (process.env.NODE_ENV === "production") { 7 | exec("cat /etc/os-release", (error, stdout) => { 8 | if (error) { 9 | console.error("Not running on docker, this is not supported."); 10 | } 11 | 12 | if (stdout) { 13 | console.log(stdout.split('PRETTY_NAME="')[1]?.split('"')[0]); 14 | } 15 | }); 16 | 17 | exec("pandoc -v", (error, stdout) => { 18 | if (error) { 19 | console.error("Pandoc is not installed."); 20 | } 21 | 22 | if (stdout) { 23 | console.log(stdout.split("\n")[0]); 24 | } 25 | }); 26 | 27 | exec("ffmpeg -version", (error, stdout) => { 28 | if (error) { 29 | console.error("FFmpeg is not installed."); 30 | } 31 | 32 | if (stdout) { 33 | console.log(stdout.split("\n")[0]); 34 | } 35 | }); 36 | 37 | exec("vips -v", (error, stdout) => { 38 | if (error) { 39 | console.error("Vips is not installed."); 40 | } 41 | 42 | if (stdout) { 43 | console.log(stdout.split("\n")[0]); 44 | } 45 | }); 46 | 47 | exec("magick --version", (error, stdout) => { 48 | if (error) { 49 | console.error("ImageMagick is not installed."); 50 | } 51 | 52 | if (stdout) { 53 | console.log(stdout.split("\n")[0]?.replace("Version: ", "")); 54 | } 55 | }); 56 | 57 | exec("gm version", (error, stdout) => { 58 | if (error) { 59 | console.error("GraphicsMagick is not installed."); 60 | } 61 | 62 | if (stdout) { 63 | console.log(stdout.split("\n")[0]); 64 | } 65 | }); 66 | 67 | exec("inkscape --version", (error, stdout) => { 68 | if (error) { 69 | console.error("Inkscape is not installed."); 70 | } 71 | 72 | if (stdout) { 73 | console.log(stdout.split("\n")[0]); 74 | } 75 | }); 76 | 77 | exec("djxl --version", (error, stdout) => { 78 | if (error) { 79 | console.error("libjxl-tools is not installed."); 80 | } 81 | 82 | if (stdout) { 83 | console.log(stdout.split("\n")[0]); 84 | } 85 | }); 86 | 87 | exec("xelatex -version", (error, stdout) => { 88 | if (error) { 89 | console.error("Tex Live with XeTeX is not installed."); 90 | } 91 | 92 | if (stdout) { 93 | console.log(stdout.split("\n")[0]); 94 | } 95 | }); 96 | 97 | exec("resvg -V", (error, stdout) => { 98 | if (error) { 99 | console.error("resvg is not installed"); 100 | } 101 | 102 | if (stdout) { 103 | console.log(`resvg v${stdout.split("\n")[0]}`); 104 | } 105 | }); 106 | 107 | exec("assimp version", (error, stdout) => { 108 | if (error) { 109 | console.error("assimp is not installed"); 110 | } 111 | 112 | if (stdout) { 113 | console.log(`assimp ${stdout.split("\n")[5]}`); 114 | } 115 | }); 116 | 117 | exec("ebook-convert --version", (error, stdout) => { 118 | if (error) { 119 | console.error("ebook-convert (calibre) is not installed"); 120 | } 121 | 122 | if (stdout) { 123 | console.log(stdout.split("\n")[0]); 124 | } 125 | }); 126 | 127 | exec("heif-info -v", (error, stdout) => { 128 | if (error) { 129 | console.error("libheif is not installed"); 130 | } 131 | 132 | if (stdout) { 133 | console.log(`libheif v${stdout.split("\n")[0]}`); 134 | } 135 | }); 136 | 137 | exec("potrace -v", (error, stdout) => { 138 | if (error) { 139 | console.error("potrace is not installed"); 140 | } 141 | 142 | if (stdout) { 143 | console.log(stdout.split("\n")[0]); 144 | } 145 | }); 146 | 147 | exec("bun -v", (error, stdout) => { 148 | if (error) { 149 | console.error("Bun is not installed. wait what"); 150 | } 151 | 152 | if (stdout) { 153 | console.log(`Bun v${stdout.split("\n")[0]}`); 154 | } 155 | }); 156 | } 157 | -------------------------------------------------------------------------------- /src/helpers/tailwind.ts: -------------------------------------------------------------------------------- 1 | import tailwind from "@tailwindcss/postcss"; 2 | import postcss from "postcss"; 3 | 4 | export const generateTailwind = async () => { 5 | const result = await Bun.file("./src/main.css") 6 | .text() 7 | .then((sourceText) => { 8 | return postcss([tailwind]).process(sourceText, { 9 | from: "./src/main.css", 10 | to: "./public/generated.css", 11 | }); 12 | }); 13 | 14 | return result; 15 | }; 16 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin 'tailwind-scrollbar'; 4 | 5 | @theme { 6 | --color-contrast: rgba(var(--contrast)); 7 | --color-neutral-900: rgba(var(--neutral-900)); 8 | --color-neutral-800: rgba(var(--neutral-800)); 9 | --color-neutral-700: rgba(var(--neutral-700)); 10 | --color-neutral-600: rgba(var(--neutral-600)); 11 | --color-neutral-500: rgba(var(--neutral-500)); 12 | --color-neutral-400: rgba(var(--neutral-400)); 13 | --color-neutral-300: rgba(var(--neutral-300)); 14 | --color-neutral-200: rgba(var(--neutral-200)); 15 | --color-neutral-100: rgba(var(--neutral-100)); 16 | --color-accent-600: rgba(var(--accent-600)); 17 | --color-accent-500: rgba(var(--accent-500)); 18 | --color-accent-400: rgba(var(--accent-400)); 19 | } 20 | 21 | /* 22 | The default border color has changed to `currentColor` in Tailwind CSS v4, 23 | so we've added these compatibility styles to make sure everything still 24 | looks the same as it did with Tailwind CSS v3. 25 | 26 | If we ever want to remove these styles, we need to add an explicit border 27 | color utility to any element that depends on these defaults. 28 | */ 29 | @layer base { 30 | *, 31 | ::after, 32 | ::before, 33 | ::backdrop, 34 | ::file-selector-button { 35 | border-color: var(--color-gray-200, currentColor); 36 | } 37 | } 38 | 39 | @utility article { 40 | @apply px-2 sm:px-4 py-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded-sm; 41 | } 42 | 43 | @utility btn-primary { 44 | @apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors; 45 | } 46 | 47 | @utility btn-secondary { 48 | @apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors; 49 | } 50 | 51 | :root { 52 | --contrast: 255, 255, 255; 53 | --neutral-900: 243, 244, 246; 54 | --neutral-800: 229, 231, 235; 55 | --neutral-700: 209, 213, 219; 56 | --neutral-600: 156, 163, 175; 57 | --neutral-500: 180, 180, 180; 58 | --neutral-400: 75, 85, 99; 59 | --neutral-300: 55, 65, 81; 60 | --neutral-200: 31, 41, 55; 61 | --neutral-100: 17, 24, 39; 62 | --accent-400: 132, 204, 22; 63 | --accent-500: 101, 163, 13; 64 | --accent-600: 77, 124, 15; 65 | } 66 | 67 | @media (prefers-color-scheme: dark) { 68 | :root { 69 | --contrast: 0, 0, 0; 70 | --neutral-900: 17, 24, 39; 71 | --neutral-800: 31, 41, 55; 72 | --neutral-700: 55, 65, 81; 73 | --neutral-600: 75, 85, 99; 74 | --neutral-500: 107, 114, 128; 75 | --neutral-300: 209, 213, 219; 76 | --neutral-400: 156, 163, 175; 77 | --neutral-200: 229, 231, 235; 78 | --accent-600: 101, 163, 13; 79 | --accent-500: 132, 204, 22; 80 | --accent-400: 163, 230, 53; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "ESNext", 5 | "target": "ES2021", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "react", 15 | "jsxFactory": "Html.createElement", 16 | "jsxFragmentFactory": "Html.Fragment", 17 | "allowSyntheticDefaultImports": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "allowJs": true, 20 | // non bun init 21 | "plugins": [{ "name": "@kitajs/ts-html-plugin" }], 22 | "noUncheckedIndexedAccess": true, 23 | // "noUnusedLocals": true, 24 | // "noUnusedParameters": true, 25 | "exactOptionalPropertyTypes": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "noImplicitOverride": true 28 | // "noImplicitReturns": true 29 | } 30 | } 31 | --------------------------------------------------------------------------------