[
  {
    "path": ".chglog/CHANGELOG.tpl.md",
    "content": "{{ range .Versions }}\n## Changes\n\n{{ range .CommitGroups -}}\n### {{ .Title }}\n\n{{ range .Commits -}}\n* {{ .Subject }}\n{{ end }}\n{{ end -}}\n\n{{- if .RevertCommits -}}\n### Reverts\n\n{{ range .RevertCommits -}}\n* {{ .Revert.Header }}\n{{ end }}\n{{ end -}}\n\n{{- if .MergeCommits -}}\n### Pull Requests\n\n{{ range .MergeCommits -}}\n* {{ .Header }}\n{{ end }}\n{{ end -}}\n\n{{- if .NoteGroups -}}\n{{ range .NoteGroups -}}\n### {{ .Title }}\n\n{{ range .Notes }}\n{{ .Body }}\n{{ end }}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n"
  },
  {
    "path": ".chglog/config.yml",
    "content": "style: github\ntemplate: CHANGELOG.tpl.md\ninfo:\n  title: CHANGELOG\n  repository_url: https://github.com/YOUR_NAME/REPOSITORY\noptions:\n  commits:\n    filters:\n      Type:\n        - feat\n        - fix\n        - perf\n        - refactor\n  commit_groups:\n    # title_maps:\n    #   feat: Features\n    #   fix: Bug Fixes\n    #   perf: Performance Improvements\n    #   refactor: Code Refactoring\n  header:\n    pattern: \"^(\\\\w*)\\\\:\\\\s(.*)$\"\n    pattern_maps:\n      - Type\n      - Subject\n  notes:\n    keywords:\n      - BREAKING CHANGE\n"
  },
  {
    "path": ".dockerignore",
    "content": "bin\nbuild\n.git\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: jiro4989\n\n---\n\n## Describe the bug\n\nA clear and concise description of what the bug is.\n\n## To Reproduce\n\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n## Expected behavior\n\nA clear and concise description of what you expected to happen.\n\n## Screenshots or Logs\n\nIf applicable, add screenshots to help explain your problem.\n\n## Environment (please complete the following information)\n\n- OS: [e.g. Windows10]\n- Version: [e.g. 1.6.0]\n\n## Additional context\n\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature\nassignees: jiro4989\n\n---\n\n## Is your feature request related to a problem? Please describe\n\nA clear and concise description of what the problem is. Ex. I'm always\nfrustrated when [...]\n\n## Describe the solution you'd like\n\nA clear and concise description of what you want to happen.\n\n## Describe alternatives you've considered\n\nA clear and concise description of any alternative solutions or features you've\nconsidered.\n\n## Additional context\n\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n---\nversion: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    cooldown:\n      default-days: 7\n    assignees:\n      - \"jiro4989\"\n  - package-ecosystem: \"docker\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    cooldown:\n      default-days: 7\n    assignees:\n      - \"jiro4989\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    cooldown:\n      default-days: 7\n    assignees:\n      - \"jiro4989\"\n"
  },
  {
    "path": ".github/pr-labeler.yml",
    "content": "feature: feature/*\nbug: hotfix/*\nchore: chore/*\n"
  },
  {
    "path": ".github/workflows/auto_merge.yml",
    "content": "---\nname: Dependabot auto-merge\n\"on\": pull_request\n\npermissions:\n  pull-requests: write\n  contents: write\n\njobs:\n  dependabot:\n    runs-on: ubuntu-latest\n    if: ${{ github.actor == 'dependabot[bot]' }}\n    steps:\n      - name: Dependabot metadata\n        id: metadata\n        uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0\n        with:\n          github-token: \"${{ secrets.GITHUB_TOKEN }}\"\n      - name: Enable auto-merge for Dependabot PRs\n        if: >-\n          ${{\n          steps.metadata.outputs.update-type == 'version-update:semver-patch' ||\n          steps.metadata.outputs.update-type == 'version-update:semver-minor'\n          }}\n        run: gh pr merge --auto --merge \"$PR_URL\"\n        env:\n          PR_URL: ${{github.event.pull_request.html_url}}\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/pr-labeler.yml",
    "content": "name: labeler\n\non:\n  pull_request:\n    types: [opened]\n\njobs:\n  labeler:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: TimonVS/pr-labeler-action@f9c084306ce8b3f488a8f3ee1ccedc6da131d1af # v5.0.0\n        with:\n          configuration-path: .github/pr-labeler.yml\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "---\nname: release\n\n\"on\":\n  push:\n    tags:\n      - 'v*.*.*'\n\nenv:\n  app: textimg\n  goversion: '1.25'\n  build_opts: '-ldflags=\"-s -w -extldflags \\\"-static\\\"\"'\n  description: 'textimg is command to convert from color text (ANSI or 256) to image.'\n\njobs:\n  build-artifact:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        os: [linux, windows, darwin]\n        arch: [amd64, arm64]\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: ${{ env.goversion }}\n      - name: Build\n        run: |\n          go build ${{ env.build_opts }}\n          if [[ $GOOS = windows ]]; then\n            7z a ${{ env.app }}-$GOOS-$GOARCH.zip ./${{ env.app }}.exe\n          else\n            tar czf ${{ env.app }}-$GOOS-$GOARCH.tar.gz ./${{ env.app }}\n          fi\n        env:\n          GOOS: ${{ matrix.os }}\n          GOARCH: ${{ matrix.arch }}\n\n      - name: Upload artifact (windows)\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: artifact-${{ matrix.os }}-${{ matrix.arch }}\n          path: ${{ env.app }}-${{ matrix.os }}-${{ matrix.arch }}.zip\n        if: matrix.os == 'windows'\n\n      - name: Upload artifact (unix)\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: artifact-${{ matrix.os }}-${{ matrix.arch }}\n          path: ${{ env.app }}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz\n        if: matrix.os != 'windows'\n\n  build-debian-packages:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: ${{ env.goversion }}\n      - run: go build ${{ env.build_opts }}\n      - name: Create package\n        run: |\n          mkdir -p .debpkg/usr/bin\n          cp -p ./${{ env.app }} .debpkg/usr/bin/\n      - uses: jiro4989/build-deb-action@a883c65147d80579cb359548b9a902ff0a35ae5b # v4.3.0\n        with:\n          package: ${{ env.app }}\n          package_root: .debpkg\n          maintainer: jiro4989\n          version: ${{ github.ref }}\n          arch: 'amd64'\n          desc: ${{ env.description }}\n      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: artifact-deb\n          path: |\n            ./*.deb\n\n  build-rpm-packages:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: ${{ env.goversion }}\n      - run: go build ${{ env.build_opts }}\n      - name: Create package\n        run: |\n          mkdir -p .rpmpkg/usr/bin\n          cp -p ./${{ env.app }} .rpmpkg/usr/bin/\n      - uses: jiro4989/build-rpm-action@f11474937f502aaa8bb36d8c1a8ec6f8de536a0c # v2.5.0\n        with:\n          summary: '${{ env.app }} is command to convert from color text (ANSI or 256) to image.'\n          package: ${{ env.app }}\n          package_root: .rpmpkg\n          maintainer: jiro4989\n          version: ${{ github.ref }}\n          arch: 'x86_64'\n          desc: ${{ env.description }}\n      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: artifact-rpm\n          path: |\n            ./*.rpm\n            !./*-debuginfo-*.rpm\n\n  create-release:\n    runs-on: ubuntu-latest\n    needs:\n      - build-artifact\n      - build-debian-packages\n      - build-rpm-packages\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Generate changelog\n        run: |\n          wget https://github.com/git-chglog/git-chglog/releases/download/0.9.1/git-chglog_linux_amd64\n          chmod +x git-chglog_linux_amd64\n          mv git-chglog_linux_amd64 git-chglog\n          ./git-chglog --output ./changelog $(git describe --tags $(git rev-list --tags --max-count=1))\n\n      - name: Create Release\n        id: create-release\n        uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: ${{ github.ref }}\n          body_path: ./changelog\n          draft: false\n          prerelease: false\n\n      - name: Write upload_url to file\n        run: echo '${{ steps.create-release.outputs.upload_url }}' > upload_url.txt\n\n      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: create-release\n          path: upload_url.txt\n\n  upload-release:\n    runs-on: ubuntu-latest\n    needs: create-release\n    strategy:\n      matrix:\n        os: [linux, windows, darwin]\n        arch: [amd64, arm64]\n        include:\n          - os: windows\n            asset_content_type: application/zip\n          - os: linux\n            asset_content_type: application/gzip\n          - os: darwin\n            asset_content_type: application/gzip\n    steps:\n      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: artifact-${{ matrix.os }}-${{ matrix.arch }}\n\n      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: create-release\n\n      - id: vars\n        run: |\n          echo \"::set-output name=upload_url::$(cat upload_url.txt)\"\n          echo \"::set-output name=asset_name::$(ls ${{ env.app }}-${{ matrix.os }}-${{ matrix.arch }}.* | head -n 1)\"\n\n      - name: Upload Release Asset\n        id: upload-release-asset\n        uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.vars.outputs.upload_url }}\n          asset_path: ${{ steps.vars.outputs.asset_name }}\n          asset_name: ${{ steps.vars.outputs.asset_name }}\n          asset_content_type: ${{ matrix.asset_content_type }}\n\n  upload-linux-package:\n    runs-on: ubuntu-latest\n    needs: create-release\n    strategy:\n      matrix:\n        include:\n          - pkg: deb\n            asset_content_type: application/vnd.debian.binary-package\n          - pkg: rpm\n            asset_content_type: application/x-rpm\n    steps:\n      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: artifact-${{ matrix.pkg }}\n\n      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: create-release\n\n      - id: vars\n        run: |\n          echo \"::set-output name=upload_url::$(cat upload_url.txt)\"\n          echo \"::set-output name=asset_name::$(ls *.${{ matrix.pkg }} | head -n 1)\"\n\n      - name: Upload Release Asset\n        id: upload-release-asset\n        uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.vars.outputs.upload_url }}\n          asset_path: ${{ steps.vars.outputs.asset_name }}\n          asset_name: ${{ steps.vars.outputs.asset_name }}\n          asset_content_type: ${{ matrix.asset_content_type }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "---\n\nname: test\n\n\"on\":\n  push:\n    branches:\n      - master\n    paths-ignore:\n      - README*\n      - LICENSE\n  pull_request:\n    paths-ignore:\n      - README*\n      - LICENSE\n\nenv:\n  goversion: '1.25'\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go:\n          - '1.25'\n          - '1.x'\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: ${{ matrix.go }}\n      - run: go build\n      - run: go install\n      - run: go test -cover ./...\n\n  build-arm64:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go:\n          - '1.x'\n        os:\n          - 'linux'\n          - 'darwin'\n          - 'windows'\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: ${{ matrix.go }}\n      # if use CGO_ENABLED=1 then use this code.\n      #\n      # - run: sudo apt-get install -y gcc-aarch64-linux-gnu\n      # - run: GOOS=${{ matrix.os }} GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -o textimg_${{ matrix.os }}_arm64\n      - run: GOOS=${{ matrix.os }} GOARCH=arm64 go build -o textimg_${{ matrix.os }}_arm64\n      - run: gzip textimg_${{ matrix.os }}_arm64\n      - name: Upload artifact (windows)\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: textimg_${{ matrix.os }}_arm64.gz\n          path: textimg_${{ matrix.os }}_arm64.gz\n\n  format:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: ${{ env.goversion }}\n      - name: Check code format\n        run: |\n          go mod download\n          count=\"$(go fmt ./... | wc -l)\"\n          if [[ \"$count\" -ne 0 ]]; then\n            echo \"[ERR] 'go fmt ./...' してください\" >&2\n            exit 1\n          fi\n\n  docker-test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Tests\n        run: |\n          make docker-build\n          make docker-test\n\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: ${{ env.goversion }}\n      - name: Lint\n        run: go vet .\n\n  coverage:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: ${{ env.goversion }}\n      - run: go test -coverprofile=coverage.out ./...\n      - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0\n"
  },
  {
    "path": ".gitignore",
    "content": "/bin/\n/dist/\n/testdata/out/\n/testdata/out*/\nimages/*\n!images/.gitkeep\nscripts/width/width\n\n!completions/*/textimg\ntextimg\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.26-alpine3.22 AS base\n\nRUN go version \\\n    && echo $GOPATH \\\n    && apk update \\\n    && apk add --no-cache git wget unzip fontconfig alpine-sdk bash \\\n    && wget https://github.com/tomokuni/Myrica/raw/master/product/MyricaM.zip -q -O /tmp/MyricaM.zip \\\n    && (cd /tmp && unzip MyricaM.zip) \\\n    && git clone https://github.com/googlefonts/noto-emoji /usr/local/src/noto-emoji \\\n    && wget https://www.wfonts.com/download/data/2016/04/23/symbola/symbola.zip -q -O /tmp/symbola.zip \\\n    && (cd /tmp && unzip symbola.zip)\n\n################################################################################\n\nFROM base AS builder\n\nCOPY . /app\nWORKDIR /app\nRUN go install\n\n################################################################################\n\nFROM alpine:3.23.4 AS runtime\nCOPY --from=builder /go/bin/textimg /usr/local/bin/\nCOPY --from=builder /tmp/MyricaM.TTC /usr/share/fonts/truetype/myrica/MyricaM.TTC\nCOPY --from=builder /usr/local/src/noto-emoji/png/128 /usr/share/emoji-image\nCOPY --from=builder /tmp/Symbola_hint.ttf /usr/share/fonts/truetype/symbola/\nCOPY --from=builder /tmp/Symbola_hint.ttf /usr/share/fonts/truetype/ancient-scripts/\n\nENV TEXTIMG_OUTPUT_DIR /images\nENV TEXTIMG_FONT_FILE /usr/share/fonts/truetype/myrica/MyricaM.TTC\nENV TEXTIMG_EMOJI_DIR /usr/share/emoji-image\nENV TEXTIMG_EMOJI_FONT_FILE /usr/share/fonts/truetype/symbola/Symbola_hint.ttf\nENV LANG ja_JP.UTF-8\nRUN mkdir /images\n\nENTRYPOINT [\"/usr/local/bin/textimg\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 jiro4989\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "textimg: parser/grammar.peg.go *.go */*.go\n\tgo fmt ./...\n\tgo build\n\nparser/grammar.peg.go: parser/grammar.peg\n\tpeg parser/grammar.peg\n\n.PHONY: help\nhelp: ## ドキュメントのヘルプを表示する。\n\t@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-30s\\033[0m %s\\n\", $$1, $$2}'\n\n.PHONY: test\ntest: textimg ## テストコードを実行する\n\tgo test -cover ./...\n\n.PHONY: docker-build\ndocker-build: ## Dockerイメージをビルドする\n\tdocker compose build\n\n.PHONY: docker-test\ndocker-test: ## Docker環境でgo testを実行する\n\tdocker compose run --rm base go test -tags docker -cover ./...\n\n.PHONY: docker-push\ndocker-push: ## DockerHubにイメージをPushする\n\tdocker push jiro4989/textimg\n\n.PHONY: setup-tools\nsetup-tools: ## 開発時に使うツールをインストールする\n\tgo install github.com/pointlander/peg@latest\n\twget https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc\n\tsudo install -m 0755 ./gh-md-toc /usr/local/bin/\n\t-rm -f gh-md-toc\n"
  },
  {
    "path": "README.md",
    "content": "# textimg\n\n![test](https://github.com/jiro4989/textimg/workflows/test/badge.svg)\n[![codecov](https://codecov.io/gh/jiro4989/textimg/branch/master/graph/badge.svg)](https://codecov.io/gh/jiro4989/textimg)\n\ntextimg is command to convert from color text (ANSI or 256) to image.  \nDrawn image keeps having colors of escape sequence.\n\n* [README on Japanese](./README_ja.md)\n\nTable of contents:\n\n<!--ts-->\n* [textimg](#textimg)\n  * [Usage](#usage)\n    * [Simple examples](#simple-examples)\n    * [With other commands](#with-other-commands)\n    * [Rainbow examples](#rainbow-examples)\n      * [From ANSI color](#from-ansi-color)\n      * [From 256 color](#from-256-color)\n      * [From 256 RGB color](#from-256-rgb-color)\n      * [Animation GIF](#animation-gif)\n      * [Slide animation GIF](#slide-animation-gif)\n    * [Using on Docker](#using-on-docker)\n    * [Saving shortcut](#saving-shortcut)\n  * [Installation](#installation)\n    * [Linux users (Debian base distros)](#linux-users-debian-base-distros)\n    * [Linux users (RHEL compatible distros)](#linux-users-rhel-compatible-distros)\n    * [With Go](#with-go)\n    * [Manual](#manual)\n  * [Help](#help)\n  * [Fonts](#fonts)\n    * [Default font path](#default-font-path)\n    * [Emoji font (image file path)](#emoji-font-image-file-path)\n    * [Emoji font (TTF)](#emoji-font-ttf)\n  * [Tab Completions](#tab-completions)\n    * [Bash](#bash)\n    * [Zsh](#zsh)\n    * [Fish](#fish)\n  * [Development](#development)\n    * [How to build](#how-to-build)\n    * [How to test](#how-to-test)\n  * [See also](#see-also)\n\n<!-- Added by: jiro4989, at: Sat Jun 19 17:52:14 JST 2021 -->\n\n<!--te-->\n\n## Usage\n\n### Simple examples\n\n```bash\ntextimg $'\\x1b[31mRED\\x1b[0m' > out.png\ntextimg $'\\x1b[31mRED\\x1b[0m' -o out.png\necho -e '\\x1b[31mRED\\x1b[0m' | textimg -o out.png\necho -e '\\x1b[31mRED\\x1b[0m' | textimg --background 0,255,255,255 -o out.jpg\necho -e '\\x1b[31mRED\\x1b[0m' | textimg --background black -o out.gif\n```\n\nOutput image format is PNG or JPG or GIF.\nFile extension of `-o` option defines output image format.\nDefault image format is PNG. if you write image file with `>` redirect then\nimage file will be saved as PNG file.\n\n### With other commands\n\ngrep:\n\n```bash\necho hello world | grep hello --color=always | textimg -o out.png\n```\n\n![image](https://user-images.githubusercontent.com/13825004/92329722-4e77d380-f0a4-11ea-97eb-0de316ebf6c7.png)\n\nscreenfetch:\n\n```bash\nscreenfetch | textimg -o out.png\n```\n\n[bat](https://github.com/sharkdp/bat):\n\n```bash\nbat --color=always /etc/profile | textimg -o out.png\n```\n\n![image](https://user-images.githubusercontent.com/13825004/92329806-03aa8b80-f0a5-11ea-95f4-d876c34d65d6.png)\n\nccze:\n\n```bash\nls -lah | ccze -A | textimg -o out.png\n```\n\n![image](https://user-images.githubusercontent.com/13825004/113440487-7e633b80-9427-11eb-8e03-4888308780a7.png)\n\nlolcat:\n\n```bash\nseq -f 'seq %g | xargs' 18 | bash | lolcat -f --freq=0.5 | textimg -o out.png\n```\n\n![image](https://user-images.githubusercontent.com/13825004/113440659-ce420280-9427-11eb-933b-7f9b1b618264.png)\n\n### Rainbow examples\n\n#### From ANSI color\n\ntextimg supports `\\x1b[30m` notation.\n\n```bash\ncolors=(30 31 32 33 34 35 36 37)\ni=0\nwhile read -r line; do\n  echo -e \"$line\" | sed -r 's/.*/\\x1b['\"${colors[$((i%8))]}\"'m&\\x1b[m/g'\n  i=$((i+1))\ndone <<< \"$(seq 8 | xargs -I@ echo TEST)\" | textimg -b 50,100,12,255 -o testdata/out/rainbow.png\n```\n\nOutput is here.\n\n![Rainbow example](docs/rainbow.png)\n\n#### From 256 color\n\ntextimg supports `\\x1b[38;5;255m` notation.\n\nForeground example is below.\n\n```bash\nseq 0 255 | while read -r i; do\n  echo -ne \"\\x1b[38;5;${i}m$(printf %03d $i)\"\n  if [ $(((i+1) % 16)) -eq 0 ]; then\n    echo\n  fi\ndone | textimg -o 256_fg.png\n```\n\nOutput is here.\n\n![256 foreground example](docs/256_fg.png)\n\nBackground example is below.\n\n```bash\nseq 0 255 | while read -r i; do\n  echo -ne \"\\x1b[48;5;${i}m$(printf %03d $i)\"\n  if [ $(((i+1) % 16)) -eq 0 ]; then\n    echo\n  fi\ndone | textimg -o 256_bg.png\n```\n\nOutput is here.\n\n![256 background example](docs/256_bg.png)\n\n#### From 256 RGB color\n\ntextimg supports `\\x1b[38;2;255;0;0m` notation.\n\n```bash\nseq 0 255 | while read i; do\n  echo -ne \"\\x1b[38;2;${i};0;0m$(printf %03d $i)\"\n  if [ $(((i+1) % 16)) -eq 0 ]; then\n    echo\n  fi\ndone | textimg -o extrgb_f_gradation.png\n```\n\nOutput is here.\n\n![RGB gradation example](docs/extrgb_f_gradation.png)\n\n#### Animation GIF\n\ntextimg supports animation GIF.\n\n```bash\necho -e '\\x1b[31mText\\x1b[0m\n\\x1b[32mText\\x1b[0m\n\\x1b[33mText\\x1b[0m\n\\x1b[34mText\\x1b[0m\n\\x1b[35mText\\x1b[0m\n\\x1b[36mText\\x1b[0m\n\\x1b[37mText\\x1b[0m\n\\x1b[41mText\\x1b[0m\n\\x1b[42mText\\x1b[0m\n\\x1b[43mText\\x1b[0m\n\\x1b[44mText\\x1b[0m\n\\x1b[45mText\\x1b[0m\n\\x1b[46mText\\x1b[0m\n\\x1b[47mText\\x1b[0m' | textimg -a -o ansi_fb_anime_1line.gif\n```\n\nOutput is here.\n\n![Animation GIF example](docs/ansi_fb_anime_1line.gif)\n\n#### Slide animation GIF\n\n```bash\necho -e '\\x1b[31mText\\x1b[0m\n\\x1b[32mText\\x1b[0m\n\\x1b[33mText\\x1b[0m\n\\x1b[34mText\\x1b[0m\n\\x1b[35mText\\x1b[0m\n\\x1b[36mText\\x1b[0m\n\\x1b[37mText\\x1b[0m\n\\x1b[41mText\\x1b[0m\n\\x1b[42mText\\x1b[0m\n\\x1b[43mText\\x1b[0m\n\\x1b[44mText\\x1b[0m\n\\x1b[45mText\\x1b[0m\n\\x1b[46mText\\x1b[0m\n\\x1b[47mText\\x1b[0m' | textimg -l 5 -SE -o slide_5_1_rainbow_forever.gif\n```\n\nOutput is here.\n\n![Slide Animation GIF example](docs/slide_5_1_rainbow_forever.gif)\n\n### Using on Docker\n\nYou can use textimg on Docker. ([DockerHub](https://hub.docker.com/r/jiro4989/textimg))\n\n```bash\ndocker pull jiro4989/textimg\ndocker run -v $(pwd):/images -it jiro4989/textimg -h\ndocker run -v $(pwd):/images -it jiro4989/textimg Testあいうえお😄 -o /images/a.png\ndocker run -v $(pwd):/images -it jiro4989/textimg Testあいうえお😄 -s\n```\n\nor build docker image of local Dockerfile.\n\n```bash\ndocker-compose build\ndocker-compose run textimg $'\\x1b[31mHello\\x1b[42mWorld\\x1b[m' -s\n```\n\n### Saving shortcut\n\n`textimg` saves an image as `t.png` to `$HOME/Pictures` (`%USERPROFILE%` on\nWindows) with `-s` options.  You can change this directory with\n`TEXTIMG_OUTPUT_DIR` environment variables.\n\n`textimg` adds current timestamp to the file suffix when activate `-t` options.\n\n```bash\n$ textimg 寿司 -st\n\n$ ls ~/Pictures/\nt_2021-03-21-194959.png\n```\n\nAnd, `textimg` adds number to the file suffix when activate `-n` options and\nthe file has existed.\n\n```bash\n$ textimg 寿司 -sn\n\n$ textimg 寿司 -sn\n\n$ ls ~/Pictures/\nt.png  t_2.png\n```\n\n## Installation\n\n### Linux users (Debian base distros)\n\n```bash\nwget https://github.com/jiro4989/textimg/releases/download/v3.1.9/textimg_3.1.9_amd64.deb\nsudo dpkg -i ./*.deb\n```\n\n### Linux users (RHEL compatible distros)\n\n```bash\nsudo yum install https://github.com/jiro4989/textimg/releases/download/v3.1.9/textimg-3.1.9-1.el7.x86_64.rpm\n```\n\n### With Go\n\n```bash\ngo install github.com/jiro4989/textimg/v3@latest\n```\n\n### Manual\n\nDownload binary from [Releases](https://github.com/jiro4989/textimg/releases).\n\n## Help\n\n```\ntextimg is command to convert from colored text (ANSI or 256) to image.\n\nUsage:\n  textimg [flags]\n\nExamples:\ntextimg $'\\x1b[31mRED\\x1b[0m' -o out.png\n\nFlags:\n  -g, --foreground string         foreground text color.\n                                  available color types are [black|red|green|yellow|blue|magenta|cyan|white]\n                                  or (R,G,B,A(0~255)) (default \"white\")\n  -b, --background string         background text color.\n                                  color types are same as \"foreground\" option (default \"black\")\n  -f, --fontfile string           font file path.\n                                  You can change this default value with environment variables TEXTIMG_FONT_FILE\n  -x, --fontindex int             \n  -e, --emoji-fontfile string     emoji font file\n  -X, --emoji-fontindex int       \n  -i, --use-emoji-font            use emoji font\n  -z, --shellgei-emoji-fontfile   emoji font file for shellgei-bot (path: \"/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf\")\n  -F, --fontsize int              font size (default 20)\n  -o, --out string                output image file path.\n                                  available image formats are [png | jpg | gif]\n  -t, --timestamp                 add time stamp to output image file path.\n  -n, --numbered                  add number-suffix to filename when the output file was existed.\n                                  ex: t_2.png\n  -s, --shellgei-imagedir         image directory path for shellgei-bot (path: \"/images/t.png\")\n  -a, --animation                 generate animation gif\n  -d, --delay int                 animation delay time (default 20)\n  -l, --line-count int            animation input line count (default 1)\n  -S, --slide                     use slide animation\n  -W, --slide-width int           sliding animation width (default 1)\n  -E, --forever                   sliding forever\n      --environments              print environment variables\n      --slack                     resize to slack icon size (128x128 px)\n  -h, --help                      help for textimg\n  -v, --version                   version for textimg\n```\n\n## Fonts\n\n### Default font path\n\nDefault fonts that to use are below.\n\n|OS     |Font path |\n|-------|----------|\n|Linux  |/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc |\n|Linux  |/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc |\n|MacOS  |/System/Library/Fonts/AppleSDGothicNeo.ttc |\n|iOS    |/System/Library/Fonts/Core/AppleSDGothicNeo.ttc |\n|Android|/system/fonts/NotoSansCJK-Regular.ttc |\n|Windows|C:\\Windows\\Fonts\\msgothic.ttc |\n\nYou can change this font path with environment variables `TEXTIMG_FONT_FILE` .\n\nExamples.\n\n```bash\nexport TEXTIMG_FONT_FILE=/usr/share/fonts/TTF/HackGen-Regular.ttf\n```\n\n### Emoji font (image file path)\n\ntextimg needs emoji image files to draw emoji.\nYou have to set `TEXTIMG_EMOJI_DIR` environment variables if you want to draw\none.\nFor example, run below.\n\n```bash\n# You can clone your favorite fonts here.\nsudo git clone https://github.com/googlefonts/noto-emoji /usr/local/src/noto-emoji\nexport TEXTIMG_EMOJI_DIR=/usr/local/src/noto-emoji/png/128\nexport LANG=ja_JP.UTF-8\necho Test👍 | textimg -o emoji.png\n```\n\n![Emoji example](docs/emoji.png)\n\n### Emoji font (TTF)\n\ntextimg can change emoji font with `TEXTIMG_EMOJI_FONT_FILE` environment variables and set `-i` option.\nFor example, switching emoji font to [Symbola font](https://www.wfonts.com/font/symbola).\n\n```bash\nexport TEXTIMG_EMOJI_FONT_FILE=/usr/share/fonts/TTF/Symbola.ttf\necho あ😃a👍！👀ん👄 | textimg -i -o emoji_symbola.png\n```\n\n![Symbola emoji example](docs/emoji_symbola.png)\n\n## Tab Completions\n\nYou can use TAB completions on your shell.\n\n### Bash\n\nRun below.\n\n```bash\nsudo cp -p completions/bash/textimg /usr/share/bash-completion/completions/textimg\n```\n\n### Zsh\n\nRun below.\n\n```bash\nsudo cp -p completions/zsh/textimg /usr/share/zsh/functions/Completion/_textimg\n\n# or\n# sudo cp -p completions/zsh/textimg {path to your $fpath}\n```\n\n### Fish\n\nRun below.\n\n```bash\nln -sfn completions/fish/textimg.fish $HOME/.config/fish/completions/textimg.fish\n```\n\n## Development\n\ngo version go1.17 linux/amd64\n\n### How to build\n\nYou run below.\n\n```bash\nmake setup-tools\nmake\n```\n\n**I didn't test on Windows.**\n\n### How to test\n\n```bash\nmake test\n\n# docker\nmake docker-build\nmake docker-test\n```\n\n## See also\n\n* <https://misc.flogisoft.com/bash/tip_colors_and_formatting>\n\n"
  },
  {
    "path": "color/color.go",
    "content": "package color\n\nimport (\n\tc \"image/color\"\n)\n\ntype RGBA c.RGBA\n\nvar (\n\tRGBABlack        = RGBA{0, 0, 0, 255}\n\tRGBARed          = RGBA{255, 0, 0, 255}\n\tRGBAGreen        = RGBA{0, 255, 0, 255}\n\tRGBAYellow       = RGBA{255, 255, 0, 255}\n\tRGBABlue         = RGBA{0, 0, 255, 255}\n\tRGBAMagenta      = RGBA{255, 0, 255, 255}\n\tRGBACyan         = RGBA{0, 255, 255, 255}\n\tRGBALightGray    = RGBA{211, 211, 211, 255}\n\tRGBADarkGray     = RGBA{169, 169, 169, 255}\n\tRGBALightRed     = RGBA{255, 144, 144, 255}\n\tRGBALightGreen   = RGBA{144, 238, 144, 255}\n\tRGBALightYellow  = RGBA{255, 255, 224, 255}\n\tRGBALightBlue    = RGBA{173, 216, 230, 255}\n\tRGBALightMagenta = RGBA{255, 224, 255, 255}\n\tRGBALightCyan    = RGBA{224, 255, 255, 255}\n\tRGBAWhite        = RGBA{255, 255, 255, 255}\n\n\tStringMap = map[string]RGBA{\n\t\t\"black\":   RGBABlack,\n\t\t\"red\":     RGBARed,\n\t\t\"green\":   RGBAGreen,\n\t\t\"yellow\":  RGBAYellow,\n\t\t\"blue\":    RGBABlue,\n\t\t\"magenta\": RGBAMagenta,\n\t\t\"cyan\":    RGBACyan,\n\t\t\"white\":   RGBAWhite,\n\t}\n\n\t// \\x1b[NNm とかの NN に紐づくRGBA色\n\t// 例: \\x1b[30m\n\tANSIMap = map[int]RGBA{\n\t\t// 文字色\n\t\t30: RGBABlack,\n\t\t31: RGBARed,\n\t\t32: RGBAGreen,\n\t\t33: RGBAYellow,\n\t\t34: RGBABlue,\n\t\t35: RGBAMagenta,\n\t\t36: RGBACyan,\n\t\t37: RGBALightGray,\n\t\t90: RGBADarkGray,\n\t\t91: RGBALightRed,\n\t\t92: RGBALightGreen,\n\t\t93: RGBALightYellow,\n\t\t94: RGBALightBlue,\n\t\t95: RGBALightMagenta,\n\t\t96: RGBALightCyan,\n\t\t97: RGBAWhite,\n\t\t// 背景色\n\t\t40:  RGBABlack,\n\t\t41:  RGBARed,\n\t\t42:  RGBAGreen,\n\t\t43:  RGBAYellow,\n\t\t44:  RGBABlue,\n\t\t45:  RGBAMagenta,\n\t\t46:  RGBACyan,\n\t\t47:  RGBALightGray,\n\t\t100: RGBADarkGray,\n\t\t101: RGBALightRed,\n\t\t102: RGBALightGreen,\n\t\t103: RGBALightYellow,\n\t\t104: RGBALightBlue,\n\t\t105: RGBALightMagenta,\n\t\t106: RGBALightCyan,\n\t\t107: RGBAWhite,\n\t}\n\n\t// \\x1b[38;5;NNNm とかの NNN に紐づくRGBA色\n\t// 例: \\x1b[38;5;114m\n\tMap256 = map[int]RGBA{\n\t\t0:   {0, 0, 0, 255},\n\t\t1:   {128, 0, 0, 255},\n\t\t2:   {0, 128, 0, 255},\n\t\t3:   {128, 128, 0, 255},\n\t\t4:   {0, 0, 128, 255},\n\t\t5:   {128, 0, 128, 255},\n\t\t6:   {0, 128, 128, 255},\n\t\t7:   {192, 192, 192, 255},\n\t\t8:   {128, 128, 128, 255},\n\t\t9:   {255, 0, 0, 255},\n\t\t10:  {0, 255, 0, 255},\n\t\t11:  {255, 255, 0, 255},\n\t\t12:  {0, 0, 255, 255},\n\t\t13:  {255, 0, 255, 255},\n\t\t14:  {0, 255, 255, 255},\n\t\t15:  {255, 255, 255, 255},\n\t\t16:  {0, 0, 0, 255},\n\t\t17:  {0, 0, 95, 255},\n\t\t18:  {0, 0, 135, 255},\n\t\t19:  {0, 0, 175, 255},\n\t\t20:  {0, 0, 215, 255},\n\t\t21:  {0, 0, 255, 255},\n\t\t22:  {0, 95, 0, 255},\n\t\t23:  {0, 95, 95, 255},\n\t\t24:  {0, 95, 135, 255},\n\t\t25:  {0, 95, 175, 255},\n\t\t26:  {0, 95, 215, 255},\n\t\t27:  {0, 95, 255, 255},\n\t\t28:  {0, 135, 0, 255},\n\t\t29:  {0, 135, 95, 255},\n\t\t30:  {0, 135, 135, 255},\n\t\t31:  {0, 135, 175, 255},\n\t\t32:  {0, 135, 215, 255},\n\t\t33:  {0, 135, 255, 255},\n\t\t34:  {0, 175, 0, 255},\n\t\t35:  {0, 175, 95, 255},\n\t\t36:  {0, 175, 135, 255},\n\t\t37:  {0, 175, 175, 255},\n\t\t38:  {0, 175, 215, 255},\n\t\t39:  {0, 175, 255, 255},\n\t\t40:  {0, 215, 0, 255},\n\t\t41:  {0, 215, 95, 255},\n\t\t42:  {0, 215, 135, 255},\n\t\t43:  {0, 215, 175, 255},\n\t\t44:  {0, 215, 215, 255},\n\t\t45:  {0, 215, 255, 255},\n\t\t46:  {0, 255, 0, 255},\n\t\t47:  {0, 255, 95, 255},\n\t\t48:  {0, 255, 135, 255},\n\t\t49:  {0, 255, 175, 255},\n\t\t50:  {0, 255, 215, 255},\n\t\t51:  {0, 255, 255, 255},\n\t\t52:  {95, 0, 0, 255},\n\t\t53:  {95, 0, 95, 255},\n\t\t54:  {95, 0, 135, 255},\n\t\t55:  {95, 0, 175, 255},\n\t\t56:  {95, 0, 215, 255},\n\t\t57:  {95, 0, 255, 255},\n\t\t58:  {95, 95, 0, 255},\n\t\t59:  {95, 95, 95, 255},\n\t\t60:  {95, 95, 135, 255},\n\t\t61:  {95, 95, 175, 255},\n\t\t62:  {95, 95, 215, 255},\n\t\t63:  {95, 95, 255, 255},\n\t\t64:  {95, 135, 0, 255},\n\t\t65:  {95, 135, 95, 255},\n\t\t66:  {95, 135, 135, 255},\n\t\t67:  {95, 135, 175, 255},\n\t\t68:  {95, 135, 215, 255},\n\t\t69:  {95, 135, 255, 255},\n\t\t70:  {95, 175, 0, 255},\n\t\t71:  {95, 175, 95, 255},\n\t\t72:  {95, 175, 135, 255},\n\t\t73:  {95, 175, 175, 255},\n\t\t74:  {95, 175, 215, 255},\n\t\t75:  {95, 175, 255, 255},\n\t\t76:  {95, 215, 0, 255},\n\t\t77:  {95, 215, 95, 255},\n\t\t78:  {95, 215, 135, 255},\n\t\t79:  {95, 215, 175, 255},\n\t\t80:  {95, 215, 215, 255},\n\t\t81:  {95, 215, 255, 255},\n\t\t82:  {95, 255, 0, 255},\n\t\t83:  {95, 255, 95, 255},\n\t\t84:  {95, 255, 135, 255},\n\t\t85:  {95, 255, 175, 255},\n\t\t86:  {95, 255, 215, 255},\n\t\t87:  {95, 255, 255, 255},\n\t\t88:  {135, 0, 0, 255},\n\t\t89:  {135, 0, 95, 255},\n\t\t90:  {135, 0, 135, 255},\n\t\t91:  {135, 0, 175, 255},\n\t\t92:  {135, 0, 215, 255},\n\t\t93:  {135, 0, 255, 255},\n\t\t94:  {135, 95, 0, 255},\n\t\t95:  {135, 95, 95, 255},\n\t\t96:  {135, 95, 135, 255},\n\t\t97:  {135, 95, 175, 255},\n\t\t98:  {135, 95, 215, 255},\n\t\t99:  {135, 95, 255, 255},\n\t\t100: {135, 135, 0, 255},\n\t\t101: {135, 135, 95, 255},\n\t\t102: {135, 135, 135, 255},\n\t\t103: {135, 135, 175, 255},\n\t\t104: {135, 135, 215, 255},\n\t\t105: {135, 135, 255, 255},\n\t\t106: {135, 175, 0, 255},\n\t\t107: {135, 175, 95, 255},\n\t\t108: {135, 175, 135, 255},\n\t\t109: {135, 175, 175, 255},\n\t\t110: {135, 175, 215, 255},\n\t\t111: {135, 175, 255, 255},\n\t\t112: {135, 215, 0, 255},\n\t\t113: {135, 215, 95, 255},\n\t\t114: {135, 215, 135, 255},\n\t\t115: {135, 215, 175, 255},\n\t\t116: {135, 215, 215, 255},\n\t\t117: {135, 215, 255, 255},\n\t\t118: {135, 255, 0, 255},\n\t\t119: {135, 255, 95, 255},\n\t\t120: {135, 255, 135, 255},\n\t\t121: {135, 255, 175, 255},\n\t\t122: {135, 255, 215, 255},\n\t\t123: {135, 255, 255, 255},\n\t\t124: {175, 0, 0, 255},\n\t\t125: {175, 0, 95, 255},\n\t\t126: {175, 0, 135, 255},\n\t\t127: {175, 0, 175, 255},\n\t\t128: {175, 0, 215, 255},\n\t\t129: {175, 0, 255, 255},\n\t\t130: {175, 95, 0, 255},\n\t\t131: {175, 95, 95, 255},\n\t\t132: {175, 95, 135, 255},\n\t\t133: {175, 95, 175, 255},\n\t\t134: {175, 95, 215, 255},\n\t\t135: {175, 95, 255, 255},\n\t\t136: {175, 135, 0, 255},\n\t\t137: {175, 135, 95, 255},\n\t\t138: {175, 135, 135, 255},\n\t\t139: {175, 135, 175, 255},\n\t\t140: {175, 135, 215, 255},\n\t\t141: {175, 135, 255, 255},\n\t\t142: {175, 175, 0, 255},\n\t\t143: {175, 175, 95, 255},\n\t\t144: {175, 175, 135, 255},\n\t\t145: {175, 175, 175, 255},\n\t\t146: {175, 175, 215, 255},\n\t\t147: {175, 175, 255, 255},\n\t\t148: {175, 215, 0, 255},\n\t\t149: {175, 215, 95, 255},\n\t\t150: {175, 215, 135, 255},\n\t\t151: {175, 215, 175, 255},\n\t\t152: {175, 215, 215, 255},\n\t\t153: {175, 215, 255, 255},\n\t\t154: {175, 255, 0, 255},\n\t\t155: {175, 255, 95, 255},\n\t\t156: {175, 255, 135, 255},\n\t\t157: {175, 255, 175, 255},\n\t\t158: {175, 255, 215, 255},\n\t\t159: {175, 255, 255, 255},\n\t\t160: {215, 0, 0, 255},\n\t\t161: {215, 0, 95, 255},\n\t\t162: {215, 0, 135, 255},\n\t\t163: {215, 0, 175, 255},\n\t\t164: {215, 0, 215, 255},\n\t\t165: {215, 0, 255, 255},\n\t\t166: {215, 95, 0, 255},\n\t\t167: {215, 95, 95, 255},\n\t\t168: {215, 95, 135, 255},\n\t\t169: {215, 95, 175, 255},\n\t\t170: {215, 95, 215, 255},\n\t\t171: {215, 95, 255, 255},\n\t\t172: {215, 135, 0, 255},\n\t\t173: {215, 135, 95, 255},\n\t\t174: {215, 135, 135, 255},\n\t\t175: {215, 135, 175, 255},\n\t\t176: {215, 135, 215, 255},\n\t\t177: {215, 135, 255, 255},\n\t\t178: {215, 175, 0, 255},\n\t\t179: {215, 175, 95, 255},\n\t\t180: {215, 175, 135, 255},\n\t\t181: {215, 175, 175, 255},\n\t\t182: {215, 175, 215, 255},\n\t\t183: {215, 175, 255, 255},\n\t\t184: {215, 215, 0, 255},\n\t\t185: {215, 215, 95, 255},\n\t\t186: {215, 215, 135, 255},\n\t\t187: {215, 215, 175, 255},\n\t\t188: {215, 215, 215, 255},\n\t\t189: {215, 215, 255, 255},\n\t\t190: {215, 255, 0, 255},\n\t\t191: {215, 255, 95, 255},\n\t\t192: {215, 255, 135, 255},\n\t\t193: {215, 255, 175, 255},\n\t\t194: {215, 255, 215, 255},\n\t\t195: {215, 255, 255, 255},\n\t\t196: {255, 0, 0, 255},\n\t\t197: {255, 0, 95, 255},\n\t\t198: {255, 0, 135, 255},\n\t\t199: {255, 0, 175, 255},\n\t\t200: {255, 0, 215, 255},\n\t\t201: {255, 0, 255, 255},\n\t\t202: {255, 95, 0, 255},\n\t\t203: {255, 95, 95, 255},\n\t\t204: {255, 95, 135, 255},\n\t\t205: {255, 95, 175, 255},\n\t\t206: {255, 95, 215, 255},\n\t\t207: {255, 95, 255, 255},\n\t\t208: {255, 135, 0, 255},\n\t\t209: {255, 135, 95, 255},\n\t\t210: {255, 135, 135, 255},\n\t\t211: {255, 135, 175, 255},\n\t\t212: {255, 135, 215, 255},\n\t\t213: {255, 135, 255, 255},\n\t\t214: {255, 175, 0, 255},\n\t\t215: {255, 175, 95, 255},\n\t\t216: {255, 175, 135, 255},\n\t\t217: {255, 175, 175, 255},\n\t\t218: {255, 175, 215, 255},\n\t\t219: {255, 175, 255, 255},\n\t\t220: {255, 215, 0, 255},\n\t\t221: {255, 215, 95, 255},\n\t\t222: {255, 215, 135, 255},\n\t\t223: {255, 215, 175, 255},\n\t\t224: {255, 215, 215, 255},\n\t\t225: {255, 215, 255, 255},\n\t\t226: {255, 255, 0, 255},\n\t\t227: {255, 255, 95, 255},\n\t\t228: {255, 255, 135, 255},\n\t\t229: {255, 255, 175, 255},\n\t\t230: {255, 255, 215, 255},\n\t\t231: {255, 255, 255, 255},\n\t\t232: {8, 8, 8, 255},\n\t\t233: {18, 18, 18, 255},\n\t\t234: {28, 28, 28, 255},\n\t\t235: {38, 38, 38, 255},\n\t\t236: {48, 48, 48, 255},\n\t\t237: {58, 58, 58, 255},\n\t\t238: {68, 68, 68, 255},\n\t\t239: {78, 78, 78, 255},\n\t\t240: {88, 88, 88, 255},\n\t\t241: {98, 98, 98, 255},\n\t\t242: {108, 108, 108, 255},\n\t\t243: {118, 118, 118, 255},\n\t\t244: {128, 128, 128, 255},\n\t\t245: {138, 138, 138, 255},\n\t\t246: {148, 148, 148, 255},\n\t\t247: {158, 158, 158, 255},\n\t\t248: {168, 168, 168, 255},\n\t\t249: {178, 178, 178, 255},\n\t\t250: {188, 188, 188, 255},\n\t\t251: {198, 198, 198, 255},\n\t\t252: {208, 208, 208, 255},\n\t\t253: {218, 218, 218, 255},\n\t\t254: {228, 228, 228, 255},\n\t\t255: {238, 238, 238, 255},\n\t}\n)\n"
  },
  {
    "path": "completions/fish/textimg.fish",
    "content": "complete -c textimg -x\n\ncomplete -c textimg -s g -l foreground -a 'black red green yellow blue magenta cyan white' -d 'foreground text color.'\ncomplete -c textimg -s b -l background -a 'black red green yellow blue magenta cyan white' -d 'background text color.'\ncomplete -c textimg -s f -l fontfile -d 'font file path.'\ncomplete -c textimg -s x -l fontindex\ncomplete -c textimg -s e -l emoji-fontfile -d 'emoji font file.'\ncomplete -c textimg -s X -l emoji-fontindex\ncomplete -c textimg -s i -l use-emoji-font -d 'use emoji font'\ncomplete -c textimg -s z -l shellgei-emoji-fontfile -d 'emoji font file for shellgei-bot'\ncomplete -c textimg -s F -l fontdize\ncomplete -c textimg -s o -l out -d 'output image file path.'\ncomplete -c textimg -s t -l timestamp -d 'add time stamp to output image file path.'\ncomplete -c textimg -s n -l numbered -d 'add number-suffix to filename when the output file was existed.'\ncomplete -c textimg -s s -l shellgei-imagedir -d 'image directory path'\ncomplete -c textimg -s a -l animation -d 'generate animation gif'\ncomplete -c textimg -s d -l delay -d 'animation delay time (default 20)'\ncomplete -c textimg -s l -l line-count -d 'animation input line count (default 1)'\ncomplete -c textimg -s S -l slide -d 'use slide animation'\ncomplete -c textimg -s W -l slide-width -d 'sliding animation width (default 1)'\ncomplete -c textimg -s E -l forever -d 'sliding forever'\ncomplete -c textimg      -l environments -d 'print environment variables'\ncomplete -c textimg      -l slack -d 'resize to slack icon size (128x128 px)'\ncomplete -c textimg -s h -l help -d 'help for textimg'\ncomplete -c textimg -s v -l version -d 'version for textimg'\n"
  },
  {
    "path": "completions/zsh/_textimg",
    "content": "#compdef textimg\n\n_textimg() {\n  _arguments \\\n    {-g,--foreground}'[foreground text color]: :->color' \\\n    {-b,--background}'[background text color]: :->color' \\\n    {-f,--fontfile}'[font file path]: :->etc' \\\n    {-x,--fontindex}': :->etc' \\\n    {-e,--emoji-fontfile}'[emoji font file]: :->etc' \\\n    {-X,--emoji-fontindex}': :->etc' \\\n    {-i,--use-emoji-font}': :->etc' \\\n    {-z,--shellgei-emoji-fontfile}'[emoji font file for shellgei-bot]: :->etc' \\\n    {-F,--fontsize}'[font size (default 20)]: :->etc' \\\n    {-o,--out}'[output image file path. available image formats are png or jpg or gif]: :->etc' \\\n    {-t,--timestamp}'[add time stamp to output image file path.]: :->etc' \\\n    {-n,--numbered}'[add number-suffix to filename when the output file was existed.]: :->etc' \\\n    {-s,--shellgei-imagedir}'[image directory path]: :->etc' \\\n    {-a,--animation}'[generate animation gif]: :->etc' \\\n    {-d,--delay}'[animation delay time (default 20)]: :->etc' \\\n    {-l,--line-count}'[animation input line count (default 1)]: :->etc' \\\n    {-S,--slide}'[use slide animation]: :->etc' \\\n    {-W,--slide-width}'[sliding animation width (default 1)]: :->etc' \\\n    {-E,--forever}'[sliding forever]: :->etc' \\\n    --environments'[print environment variables]: :->etc' \\\n    --slack'[resize toslack icon size (128x128x px)]: :->etc' \\\n    {-h,--help}'[help for textimg]: :->etc' \\\n    {-v,--version}'[version for textimg]: :->etc'\n\n  case \"$state\" in\n    color)\n      _values \\\n        'color' \\\n        black red green yellow blue magenta cyan white\n      ;;\n    etc)\n      # nothing to do\n      ;;\n  esac\n}\n\ncompdef _textimg textimg\n\n# vim: ft=zsh\n"
  },
  {
    "path": "config/config.go",
    "content": "package config\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/jiro4989/textimg/v3/color\"\n\t\"github.com/jiro4989/textimg/v3/log\"\n\t\"golang.org/x/image/font\"\n\t\"golang.org/x/term\"\n)\n\ntype Config struct {\n\tForeground               string // 文字色\n\tBackground               string // 背景色\n\tOutpath                  string // 画像の出力ファイルパス\n\tAddTimeStamp             bool   // ファイル名末尾にタイムスタンプ付与\n\tSaveNumberedFile         bool   // 保存しようとしたファイルがすでに存在する場合に連番を付与する\n\tFontFile                 string // フォントファイルのパス\n\tFontIndex                int    // フォントコレクションのインデックス\n\tEmojiFontFile            string // 絵文字用のフォントファイルのパス\n\tEmojiFontIndex           int    // 絵文字用のフォントコレクションのインデックス\n\tUseEmojiFont             bool   // 絵文字TTFを使う\n\tFontSize                 int    // フォントサイズ\n\tUseAnimation             bool   // アニメーションGIFを生成する\n\tDelay                    int    // アニメーションのディレイ時間\n\tLineCount                int    // 入力データのうち何行を1フレーム画像に使うか\n\tUseSlideAnimation        bool   // スライドアニメーションする\n\tSlideWidth               int    // スライドする幅\n\tSlideForever             bool   // スライドを無限にスライドするように描画する\n\tToSlackIcon              bool   // Slackのアイコンサイズにする\n\tPrintEnvironments        bool\n\tUseShellgeiImagedir      bool\n\tUseShellgeiEmojiFontfile bool\n\tResizeWidth              int // 画像の横幅\n\tResizeHeight             int // 画像の縦幅\n\n\tForegroundColor color.RGBA // 文字色\n\tBackgroundColor color.RGBA // 背景色\n\tTexts           []string\n\tFileExtension   string\n\tWriter          io.WriteCloser\n\tFontFace        font.Face\n\tEmojiFontFace   font.Face\n\tEmojiDir        string\n}\n\ntype osDefaultFont struct {\n\tfontFile  string\n\tfontIndex int\n\tisLinux   bool\n}\n\nconst ShellgeiEmojiFontPath = \"/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf\"\n\nconst (\n\tdefaultWindowsFont = `C:\\Windows\\Fonts\\msgothic.ttc`\n\tdefaultDarwinFont  = \"/System/Library/Fonts/AppleSDGothicNeo.ttc\"\n\tdefaultIOSFont     = \"/System/Library/Fonts/Core/AppleSDGothicNeo.ttc\"\n\tdefaultAndroidFont = \"/system/fonts/NotoSansCJK-Regular.ttc\"\n\tdefaultLinuxFont1  = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc\"\n\tdefaultLinuxFont2  = \"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc\"\n)\n\n// adjust はパラメータを調整する。\n// 副作用を持つ。\nfunc (a *Config) Adjust(args []string, ev EnvVars) error {\n\ta.EmojiDir = ev.EmojiDir\n\n\t// シェル芸イメージディレクトリの指定がある時はパスを変更する\n\tif a.UseShellgeiImagedir {\n\t\tvar err error\n\t\toutDir := ev.OutputDir\n\t\ta.Outpath, err = outputImageDir(outDir, a.UseAnimation)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\ta.addTimeStampToOutPath(time.Now())\n\ta.addNumberSuffixToOutPath()\n\n\tif a.UseShellgeiEmojiFontfile {\n\t\ta.EmojiFontFile = ShellgeiEmojiFontPath\n\t\ta.UseEmojiFont = true\n\t}\n\n\tif a.UseSlideAnimation {\n\t\ta.UseAnimation = true\n\t}\n\n\tvar err error\n\ta.ForegroundColor, err = optionColorStringToRGBA(a.Foreground)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ta.BackgroundColor, err = optionColorStringToRGBA(a.Background)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 引数にテキストの指定がなければ標準入力を使用する\n\ta.Texts = readInputText(args)\n\n\t// textsが空のときは警告メッセージを出力して異常終了\n\tif err := validateInputText(a.Texts); err != nil {\n\t\treturn err\n\t}\n\n\t// スライドアニメーションを使うときはテキストを加工する\n\tif a.UseSlideAnimation {\n\t\ta.Texts = toSlideStrings(a.Texts, a.LineCount, a.SlideWidth, a.SlideForever)\n\t}\n\n\t// 拡張子のみ取得\n\ta.FileExtension = filepath.Ext(strings.ToLower(a.Outpath))\n\n\tif err := a.setWriter(); err != nil {\n\t\treturn err\n\t}\n\n\tif err := validateFileExtension(a.FileExtension); err != nil {\n\t\treturn err\n\t}\n\n\ta.Texts = normalizeTexts(a.Texts)\n\n\ta.FontFace, err = readFace(a.FontFile, a.FontIndex, float64(a.FontSize))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif a.EmojiFontFile != \"\" {\n\t\ta.EmojiFontFace, err = readFace(a.EmojiFontFile, a.EmojiFontIndex, float64(a.FontSize))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif a.ToSlackIcon {\n\t\ta.ResizeWidth = 128\n\t\ta.ResizeHeight = 128\n\t}\n\n\treturn nil\n}\n\nfunc (a *Config) SetFontFileAndFontIndex(runtimeOS string) {\n\tif a.FontFile != \"\" {\n\t\treturn\n\t}\n\n\tm := map[string]osDefaultFont{\n\t\t\"linux\": {\n\t\t\tisLinux: true,\n\t\t},\n\t\t\"windows\": {\n\t\t\tfontFile:  defaultWindowsFont,\n\t\t\tfontIndex: 0,\n\t\t},\n\t\t\"darwin\": {\n\t\t\tfontFile:  defaultDarwinFont,\n\t\t\tfontIndex: 0,\n\t\t},\n\t\t\"ios\": {\n\t\t\tfontFile:  defaultIOSFont,\n\t\t\tfontIndex: 0,\n\t\t},\n\t\t\"android\": {\n\t\t\tfontFile:  defaultAndroidFont,\n\t\t\tfontIndex: 5,\n\t\t},\n\t}\n\n\tif f, ok := m[runtimeOS]; ok {\n\t\t// linux だけ特殊なので特別に分岐\n\t\tif !f.isLinux {\n\t\t\ta.FontFile = f.fontFile\n\t\t\ta.FontIndex = f.fontIndex\n\t\t\treturn\n\t\t}\n\n\t\tif _, err := os.Stat(\"/proc/sys/fs/binfmt_misc/WSLInterop\"); err == nil {\n\t\t\ta.FontFile = \"/mnt/c/Windows/Fonts/msgothic.ttc\"\n\t\t\ta.FontIndex = 0\n\t\t\treturn\n\t\t}\n\n\t\ta.FontFile = defaultLinuxFont1\n\t\tif _, err := os.Stat(a.FontFile); err != nil {\n\t\t\ta.FontFile = defaultLinuxFont2\n\t\t}\n\t\ta.FontIndex = 5\n\t\treturn\n\t}\n}\n\n// addTimeStampToOutPath はOutpathに指定日時のタイムスタンプを付与する。\nfunc (a *Config) addTimeStampToOutPath(t time.Time) {\n\tif !a.AddTimeStamp {\n\t\treturn\n\t}\n\n\text := filepath.Ext(a.Outpath)\n\tfile := strings.TrimSuffix(a.Outpath, ext)\n\ttimestamp := t.Format(\"2006-01-02-150405\")\n\ta.Outpath = file + \"_\" + timestamp + ext\n}\n\n// addTimeStampToOutPath はOutpathに指定日時のタイムスタンプを付与する。\nfunc (a *Config) addNumberSuffixToOutPath() {\n\tif !a.SaveNumberedFile {\n\t\treturn\n\t}\n\n\t// ファイルが存在しない時は何もしない\n\t// NOTE: 並列に実行されるとチェックしきれない場合があるけれど許容する\n\tif _, err := os.Stat(a.Outpath); err != nil {\n\t\treturn\n\t}\n\n\tfileExt := filepath.Ext(a.Outpath)\n\tfileName := strings.TrimSuffix(a.Outpath, fileExt)\n\ti := 2\n\tfor {\n\t\ta.Outpath = fmt.Sprintf(\"%s_%d%s\", fileName, i, fileExt)\n\t\t_, err := os.Stat(a.Outpath)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\ti++\n\t}\n}\n\nfunc (a *Config) setWriter() error {\n\tif a.Outpath == \"\" {\n\t\t// 出力先画像の指定がなく、且つ出力先がパイプならstdout + PNG/GIFと\n\t\t// して出力。なければそもそも画像処理しても意味が無いので終了\n\t\tfd := os.Stdout.Fd()\n\t\tif term.IsTerminal(int(fd)) {\n\t\t\tlog.Error(\"image data not written to a terminal. use -o, -s, pipe or redirect.\")\n\t\t\tlog.Error(\"for help, type: textimg -h\")\n\t\t\treturn fmt.Errorf(\"no output target error\")\n\t\t}\n\t\ta.Writer = os.Stdout\n\t\tif a.UseAnimation {\n\t\t\ta.FileExtension = \".gif\"\n\t\t} else {\n\t\t\ta.FileExtension = \".png\"\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif a.Writer != nil {\n\t\treturn nil\n\t}\n\n\tvar err error\n\ta.Writer, err = os.Create(a.Outpath)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// NOTE: writerは呼び出し元でクローズする\n\n\treturn nil\n}\n\nfunc validateInputText(texts []string) error {\n\tvar emptyCount int\n\tfor _, v := range texts {\n\t\tif len(v) < 1 {\n\t\t\temptyCount++\n\t\t}\n\t}\n\tif emptyCount == len(texts) {\n\t\terr := fmt.Errorf(\"must need input texts.\")\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// validateFileExtension はファイル拡張子をチェックする。\nfunc validateFileExtension(ext string) error {\n\tswitch ext {\n\tcase \".png\", \".jpg\", \".jpeg\", \".gif\":\n\t\t// 何もしない\n\tdefault:\n\t\terr := fmt.Errorf(\"%s is not supported extension.\", ext)\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// normalizeTexts はテキストを正規化する。\nfunc normalizeTexts(texts []string) []string {\n\tresult := texts\n\n\t// タブ文字は画像描画時に表示されないので暫定対応で半角スペースに置換\n\tfor i, text := range result {\n\t\tresult[i] = strings.Replace(text, \"\\t\", \"  \", -1)\n\t}\n\n\t// ゼロ幅文字を削除\n\tfor i, text := range result {\n\t\tresult[i] = removeZeroWidthCharacters(text)\n\t}\n\n\treturn result\n}\n\nfunc readInputText(args []string) []string {\n\tvar texts []string\n\tif len(args) < 1 {\n\t\ttexts = readStdin()\n\t} else {\n\t\tfor _, v := range args {\n\t\t\ttexts = append(texts, strings.Split(v, \"\\n\")...)\n\t\t}\n\t}\n\treturn texts\n}\n\n// outputImageDir は `-s` オプションで保存するさきのディレクトリパスを返す。\nfunc outputImageDir(outDir string, useAnimation bool) (string, error) {\n\tif outDir == \"\" {\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\toutDir = filepath.Join(homeDir, \"Pictures\")\n\t}\n\n\tif useAnimation {\n\t\treturn filepath.Join(outDir, \"t.gif\"), nil\n\t}\n\n\treturn filepath.Join(outDir, \"t.png\"), nil\n}\n\n// オプション引数のbackgroundは２つの書き方を許容する。\n//  1. black といった色の直接指定\n//  2. RGBAのカンマ区切り指定\n//     書式: R,G,B,A\n//     赤色の例: 255,0,0,255\nfunc optionColorStringToRGBA(colstr string) (color.RGBA, error) {\n\t// \"black\"といった色名称でマッチするものがあれば返す\n\tcolstr = strings.ToLower(colstr)\n\tcol := color.StringMap[colstr]\n\tzeroColor := color.RGBA{}\n\tif col != zeroColor {\n\t\treturn col, nil\n\t}\n\n\t// カンマ区切りでの指定があれば返す\n\trgba := strings.Split(colstr, \",\")\n\tif len(rgba) != 4 {\n\t\treturn zeroColor, fmt.Errorf(\"illegal RGBA format: %s\", colstr)\n\t}\n\n\tvar (\n\t\tr   uint64\n\t\tg   uint64\n\t\tb   uint64\n\t\ta   uint64\n\t\terr error\n\t\trs  = rgba[0]\n\t\tgs  = rgba[1]\n\t\tbs  = rgba[2]\n\t\tas  = rgba[3]\n\t)\n\tr, err = strconv.ParseUint(rs, 10, 8)\n\tif err != nil {\n\t\treturn zeroColor, err\n\t}\n\tg, err = strconv.ParseUint(gs, 10, 8)\n\tif err != nil {\n\t\treturn zeroColor, err\n\t}\n\tb, err = strconv.ParseUint(bs, 10, 8)\n\tif err != nil {\n\t\treturn zeroColor, err\n\t}\n\ta, err = strconv.ParseUint(as, 10, 8)\n\tif err != nil {\n\t\treturn zeroColor, err\n\t}\n\tc := color.RGBA{\n\t\tR: uint8(r),\n\t\tG: uint8(g),\n\t\tB: uint8(b),\n\t\tA: uint8(a),\n\t}\n\treturn c, nil\n}\n\n// toSlideStrings は文字列をスライドアニメーション用の文字列に変換する。\nfunc toSlideStrings(src []string, lineCount, slideWidth int, slideForever bool) (ret []string) {\n\tif 1 < slideWidth {\n\t\tvar loopCount int\n\t\tfor i := 0; i < len(src); i += slideWidth {\n\t\t\tloopCount++\n\t\t}\n\t\tfor i := 0; i < (loopCount*slideWidth+1)-len(src); i++ {\n\t\t\tif !slideForever {\n\t\t\t\tsrc = append(src, \"\")\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := 0; i < len(src); i += slideWidth {\n\t\tn := i + lineCount\n\t\tif len(src) < n {\n\t\t\tif slideForever {\n\t\t\t\tfor j := i; j < n; j++ {\n\t\t\t\t\tm := j\n\t\t\t\t\tif len(src) <= m {\n\t\t\t\t\t\tm -= len(src)\n\t\t\t\t\t}\n\t\t\t\t\tline := src[m]\n\t\t\t\t\tret = append(ret, line)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\t// lineCountの数ずつ行を取得して戻り値に追加\n\t\tfor j := i; j < n; j++ {\n\t\t\tline := src[j]\n\t\t\tret = append(ret, line)\n\t\t}\n\t}\n\treturn\n}\n\n// removeZeroWidthSpace はゼロ幅文字が存在したときに削除する。\n//\n// 参考\n// * ゼロ幅スペース https://ja.wikipedia.org/wiki/%E3%82%BC%E3%83%AD%E5%B9%85%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9\nfunc removeZeroWidthCharacters(s string) string {\n\tzwc := []rune{\n\t\t0x200b, // zero width space\n\t\t0x200c, // zero width joiner\n\t\t0x200d, // zero width joiner\n\t\t0xfeff, // zero width no-break-space\n\t}\n\tvar ret []rune\nchars:\n\tfor _, v := range s {\n\t\tfor _, c := range zwc {\n\t\t\tif v == c {\n\t\t\t\tcontinue chars\n\t\t\t}\n\t\t}\n\t\tret = append(ret, v)\n\t}\n\treturn string(ret)\n}\n\n// readStdin は標準入力を文字列の配列として返す。\nfunc readStdin() (ret []string) {\n\tsc := bufio.NewScanner(os.Stdin)\n\tfor sc.Scan() {\n\t\tline := sc.Text()\n\t\tret = append(ret, line)\n\t}\n\tif err := sc.Err(); err != nil {\n\t\tpanic(err)\n\t}\n\treturn\n}\n"
  },
  {
    "path": "config/config_test.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/jiro4989/textimg/v3/color\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc newDefaultConfig() Config {\n\treturn Config{\n\t\tForeground:               \"white\",\n\t\tBackground:               \"black\",\n\t\tOutpath:                  \"\",\n\t\tAddTimeStamp:             false,\n\t\tSaveNumberedFile:         false,\n\t\tFontFile:                 \"\",\n\t\tFontIndex:                0,\n\t\tEmojiFontFile:            \"\",\n\t\tEmojiFontIndex:           0,\n\t\tUseEmojiFont:             false,\n\t\tFontSize:                 20,\n\t\tUseAnimation:             false,\n\t\tDelay:                    20,\n\t\tLineCount:                1,\n\t\tUseSlideAnimation:        false,\n\t\tSlideWidth:               1,\n\t\tSlideForever:             false,\n\t\tToSlackIcon:              false,\n\t\tPrintEnvironments:        false,\n\t\tUseShellgeiImagedir:      false,\n\t\tUseShellgeiEmojiFontfile: false,\n\t\tResizeWidth:              0,\n\t\tResizeHeight:             0,\n\t\tWriter:                   NewMockWriter(false, false),\n\t}\n}\n\nfunc TestConfig_Adjust(t *testing.T) {\n\ttests := []struct {\n\t\tdesc    string\n\t\tconfig  Config\n\t\targs    []string\n\t\tev      EnvVars\n\t\twant    Config\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tdesc: \"正常系: Outpathが設定されている\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"hello\"},\n\t\t\tev:   EnvVars{},\n\t\t\twant: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.ForegroundColor = color.RGBAWhite\n\t\t\t\tc.BackgroundColor = color.RGBABlack\n\t\t\t\tc.Texts = []string{\"hello\"}\n\t\t\t\tc.FileExtension = \".png\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: UseShellgeiImagedirが有効なときはt.pngが設定される\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.UseShellgeiImagedir = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"hello\"},\n\t\t\tev: EnvVars{\n\t\t\t\tOutputDir: \"sushi\",\n\t\t\t},\n\t\t\twant: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = filepath.Join(\"sushi\", \"t.png\")\n\t\t\t\tc.UseShellgeiImagedir = true\n\t\t\t\tc.ForegroundColor = color.RGBAWhite\n\t\t\t\tc.BackgroundColor = color.RGBABlack\n\t\t\t\tc.Texts = []string{\"hello\"}\n\t\t\t\tc.FileExtension = \".png\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: UseShellgeiEmojiFontfileが有効な時は組み込みの絵文字パスが設定されて、UseEmojiFont=trueになる\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.UseShellgeiImagedir = true\n\t\t\t\tc.UseShellgeiEmojiFontfile = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"hello\"},\n\t\t\tev: EnvVars{\n\t\t\t\tOutputDir: \"sushi\",\n\t\t\t},\n\t\t\twant: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = filepath.Join(\"sushi\", \"t.png\")\n\t\t\t\tc.UseShellgeiImagedir = true\n\t\t\t\tc.ForegroundColor = color.RGBAWhite\n\t\t\t\tc.BackgroundColor = color.RGBABlack\n\t\t\t\tc.Texts = []string{\"hello\"}\n\t\t\t\tc.FileExtension = \".png\"\n\t\t\t\tc.UseShellgeiEmojiFontfile = true\n\t\t\t\tc.UseEmojiFont = true\n\t\t\t\tc.EmojiFontFile = ShellgeiEmojiFontPath\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: UseShellgeiImagedirが有効でUseAnimationが設定されているときはt.gifになる\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.UseShellgeiImagedir = true\n\t\t\t\tc.UseAnimation = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"hello\"},\n\t\t\tev: EnvVars{\n\t\t\t\tOutputDir: \"sushi\",\n\t\t\t},\n\t\t\twant: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = filepath.Join(\"sushi\", \"t.gif\")\n\t\t\t\tc.UseShellgeiImagedir = true\n\t\t\t\tc.UseAnimation = true\n\t\t\t\tc.ForegroundColor = color.RGBAWhite\n\t\t\t\tc.BackgroundColor = color.RGBABlack\n\t\t\t\tc.Texts = []string{\"hello\"}\n\t\t\t\tc.FileExtension = \".gif\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: ToSlackIconが有効なときはResizeWidthとResizeHeightが設定される\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.ToSlackIcon = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"hello\"},\n\t\t\tev:   EnvVars{},\n\t\t\twant: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.ForegroundColor = color.RGBAWhite\n\t\t\t\tc.BackgroundColor = color.RGBABlack\n\t\t\t\tc.Texts = []string{\"hello\"}\n\t\t\t\tc.FileExtension = \".png\"\n\t\t\t\tc.ToSlackIcon = true\n\t\t\t\tc.ResizeWidth = 128\n\t\t\t\tc.ResizeHeight = 128\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: UseSlideAnimationが有効なときはUseAnimationも有効になる\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.UseSlideAnimation = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"hello\"},\n\t\t\tev:   EnvVars{},\n\t\t\twant: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.ForegroundColor = color.RGBAWhite\n\t\t\t\tc.BackgroundColor = color.RGBABlack\n\t\t\t\tc.Texts = []string{\"hello\"}\n\t\t\t\tc.FileExtension = \".png\"\n\t\t\t\tc.UseSlideAnimation = true\n\t\t\t\tc.UseAnimation = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: SlideWidthが2以上の時はテキストの処理が変化する\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.UseSlideAnimation = true\n\t\t\t\tc.LineCount = 2\n\t\t\t\tc.SlideWidth = 2\n\t\t\t\tc.SlideForever = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"hello\", \"hello\", \"hello\", \"hello\"},\n\t\t\tev:   EnvVars{},\n\t\t\twant: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.ForegroundColor = color.RGBAWhite\n\t\t\t\tc.BackgroundColor = color.RGBABlack\n\t\t\t\tc.Texts = []string{\"hello\", \"hello\", \"hello\", \"hello\"}\n\t\t\t\tc.FileExtension = \".png\"\n\t\t\t\tc.UseSlideAnimation = true\n\t\t\t\tc.UseAnimation = true\n\t\t\t\tc.LineCount = 2\n\t\t\t\tc.SlideWidth = 2\n\t\t\t\tc.SlideForever = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: EmojiFontFileに存在しないファイルを指定してもエラーにはならない\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.EmojiFontFile = \"sushi.otf\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"hello\"},\n\t\t\tev:   EnvVars{},\n\t\t\twant: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.ForegroundColor = color.RGBAWhite\n\t\t\t\tc.BackgroundColor = color.RGBABlack\n\t\t\t\tc.Texts = []string{\"hello\"}\n\t\t\t\tc.FileExtension = \".png\"\n\t\t\t\tc.EmojiFontFile = \"sushi.otf\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: Foregroundに不正な色指定をした時はエラーを返す\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Foreground = \"sushi\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"hello\"},\n\t\t\tev:      EnvVars{},\n\t\t\twant:    Config{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: Backgroundに不正な色指定をした時はエラーを返す\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Background = \"sushi\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"hello\"},\n\t\t\tev:      EnvVars{},\n\t\t\twant:    Config{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: textsが空の時はエラーを返す\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{},\n\t\t\tev:      EnvVars{},\n\t\t\twant:    Config{},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\n\t\t\terr := tt.config.Adjust(tt.args, tt.ev)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(err)\n\t\t\tassert.Equal(tt.want.Foreground, tt.config.Foreground)\n\t\t\tassert.Equal(tt.want.Background, tt.config.Background)\n\t\t\tassert.Equal(tt.want.Outpath, tt.config.Outpath)\n\t\t\tassert.Equal(tt.want.AddTimeStamp, tt.config.AddTimeStamp)\n\t\t\tassert.Equal(tt.want.SaveNumberedFile, tt.config.SaveNumberedFile)\n\t\t\tassert.Equal(tt.want.FontFile, tt.config.FontFile)\n\t\t\tassert.Equal(tt.want.FontIndex, tt.config.FontIndex)\n\t\t\tassert.Equal(tt.want.EmojiFontFile, tt.config.EmojiFontFile)\n\t\t\tassert.Equal(tt.want.EmojiFontIndex, tt.config.EmojiFontIndex)\n\t\t\tassert.Equal(tt.want.UseEmojiFont, tt.config.UseEmojiFont)\n\t\t\tassert.Equal(tt.want.FontSize, tt.config.FontSize)\n\t\t\tassert.Equal(tt.want.UseAnimation, tt.config.UseAnimation)\n\t\t\tassert.Equal(tt.want.Delay, tt.config.Delay)\n\t\t\tassert.Equal(tt.want.LineCount, tt.config.LineCount)\n\t\t\tassert.Equal(tt.want.UseSlideAnimation, tt.config.UseSlideAnimation)\n\t\t\tassert.Equal(tt.want.SlideWidth, tt.config.SlideWidth)\n\t\t\tassert.Equal(tt.want.SlideForever, tt.config.SlideForever)\n\t\t\tassert.Equal(tt.want.ToSlackIcon, tt.config.ToSlackIcon)\n\t\t\tassert.Equal(tt.want.PrintEnvironments, tt.config.PrintEnvironments)\n\t\t\tassert.Equal(tt.want.UseShellgeiImagedir, tt.config.UseShellgeiImagedir)\n\t\t\tassert.Equal(tt.want.UseShellgeiEmojiFontfile, tt.config.UseShellgeiEmojiFontfile)\n\t\t\tassert.Equal(tt.want.ForegroundColor, tt.config.ForegroundColor)\n\t\t\tassert.Equal(tt.want.BackgroundColor, tt.config.BackgroundColor)\n\t\t\tassert.Equal(tt.want.Texts, tt.config.Texts)\n\t\t\tassert.Equal(tt.want.FileExtension, tt.config.FileExtension)\n\t\t\t// NOTE: ここはテストするのが難しいので無視\n\t\t\t// assert.Equal(tt.want.Writer, tt.config.Writer)\n\t\t\t// assert.Equal(tt.want.FontFace, tt.config.FontFace)\n\t\t\t// assert.Equal(tt.want.EmojiFontFace, tt.config.EmojiFontFace)\n\t\t\tassert.Equal(tt.want.EmojiDir, tt.config.EmojiDir)\n\t\t})\n\t}\n}\n\nfunc TestOptionColorStringToRGBA(t *testing.T) {\n\ttype TestData struct {\n\t\tdesc   string\n\t\tcolstr string\n\t\texpect color.RGBA\n\t}\n\ttds := []TestData{\n\t\t{desc: \"BLACK\", colstr: \"BLACK\", expect: color.RGBABlack},\n\t\t{desc: \"black\", colstr: \"black\", expect: color.RGBABlack},\n\t\t{desc: \"red\", colstr: \"red\", expect: color.RGBARed},\n\t\t{desc: \"green\", colstr: \"green\", expect: color.RGBAGreen},\n\t\t{desc: \"yellow\", colstr: \"yellow\", expect: color.RGBAYellow},\n\t\t{desc: \"blue\", colstr: \"blue\", expect: color.RGBABlue},\n\t\t{desc: \"magenta\", colstr: \"magenta\", expect: color.RGBAMagenta},\n\t\t{desc: \"cyan\", colstr: \"cyan\", expect: color.RGBACyan},\n\t\t{desc: \"white\", colstr: \"white\", expect: color.RGBAWhite},\n\t\t{desc: \"0,0,0,255\", colstr: \"0,0,0,255\", expect: color.RGBA{R: 0, G: 0, B: 0, A: 255}},\n\t\t{desc: \"255,255,255,255\", colstr: \"255,255,255,255\", expect: color.RGBA{R: 255, G: 255, B: 255, A: 255}},\n\t\t{desc: \"0,0,0,0\", colstr: \"0,0,0,0\", expect: color.RGBA{R: 0, G: 0, B: 0, A: 0}},\n\t}\n\tfor _, v := range tds {\n\t\tt.Run(v.desc, func(t *testing.T) {\n\t\t\tgot, err := optionColorStringToRGBA(v.colstr)\n\t\t\tassert.Nil(t, err, v.desc)\n\t\t\tassert.Equal(t, v.expect, got, v.desc)\n\t\t})\n\t}\n\n\t// 異常系\n\ttds = []TestData{\n\t\t{desc: \"不正な色文字列\", colstr: \"unko\"},\n\t\t{desc: \"RGBAの書式不正(値の数不足)\", colstr: \"1,2,3\"},\n\t\t{desc: \"RGBAの書式不正(値の数過多)\", colstr: \"1,2,3,4,5\"},\n\t\t{desc: \"RGBAの書式不正(値がない)\", colstr: \"1,2,3,\"},\n\t\t{desc: \"RGBAの書式不正(値に文字が混じっている)\", colstr: \"1,2,3,a\"},\n\t\t{desc: \"RGBAの書式不正(255以上の値)\", colstr: \"1,2,3,256\"},\n\t\t{desc: \"RGBAの書式不正(負の値)\", colstr: \"-1,2,3,255\"},\n\t\t{desc: \"RGBAの書式不正(空文字)\", colstr: \"\"},\n\t}\n\tfor _, v := range tds {\n\t\tt.Run(v.desc, func(t *testing.T) {\n\t\t\t_, err := optionColorStringToRGBA(v.colstr)\n\t\t\tassert.NotNil(t, err, v.desc)\n\t\t})\n\t}\n}\n\nfunc TestToSlideStrings(t *testing.T) {\n\ttype TestData struct {\n\t\tdesc                  string\n\t\tsrc, expect           []string\n\t\tlineCount, slideWidth int\n\t\tslideForever          bool\n\t}\n\ttds := []TestData{\n\t\t{\n\t\t\tdesc: \"2行描画、スライド幅1、無限なし\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\",\n\t\t\t\t\"2\", \"3\",\n\t\t\t\t\"3\", \"4\",\n\t\t\t\t\"4\", \"5\",\n\t\t\t},\n\t\t\tlineCount:    2,\n\t\t\tslideWidth:   1,\n\t\t\tslideForever: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"2行描画、スライド幅2、無限なし\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\",\n\t\t\t\t\"3\", \"4\",\n\t\t\t\t\"5\", \"\",\n\t\t\t},\n\t\t\tlineCount:    2,\n\t\t\tslideWidth:   2,\n\t\t\tslideForever: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅1、無限なし\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"2\", \"3\", \"4\",\n\t\t\t\t\"3\", \"4\", \"5\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   1,\n\t\t\tslideForever: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅2、無限なし、不足あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"3\", \"4\", \"5\",\n\t\t\t\t\"5\", \"6\", \"\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   2,\n\t\t\tslideForever: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅2、無限なし、不足なし\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"3\", \"4\", \"5\",\n\t\t\t\t\"5\", \"6\", \"7\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   2,\n\t\t\tslideForever: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅3、無限なし、不足なし\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"4\", \"5\", \"6\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   3,\n\t\t\tslideForever: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅3、無限なし、不足あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"4\", \"5\", \"6\",\n\t\t\t\t\"7\", \"\", \"\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   3,\n\t\t\tslideForever: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅3、無限なし、不足あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"4\", \"5\", \"6\",\n\t\t\t\t\"7\", \"8\", \"\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   3,\n\t\t\tslideForever: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"2行描画、スライド幅2、無限あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\",\n\t\t\t\t\"3\", \"4\",\n\t\t\t\t\"5\", \"1\",\n\t\t\t},\n\t\t\tlineCount:    2,\n\t\t\tslideWidth:   2,\n\t\t\tslideForever: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"2行描画、スライド幅2、無限あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\",\n\t\t\t\t\"3\", \"4\",\n\t\t\t\t\"5\", \"6\",\n\t\t\t},\n\t\t\tlineCount:    2,\n\t\t\tslideWidth:   2,\n\t\t\tslideForever: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅1、無限あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"2\", \"3\", \"4\",\n\t\t\t\t\"3\", \"4\", \"5\",\n\t\t\t\t\"4\", \"5\", \"1\",\n\t\t\t\t\"5\", \"1\", \"2\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   1,\n\t\t\tslideForever: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅1、無限あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"2\", \"3\", \"4\",\n\t\t\t\t\"3\", \"4\", \"5\",\n\t\t\t\t\"4\", \"5\", \"6\",\n\t\t\t\t\"5\", \"6\", \"1\",\n\t\t\t\t\"6\", \"1\", \"2\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   1,\n\t\t\tslideForever: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅2、無限あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"3\", \"4\", \"5\",\n\t\t\t\t\"5\", \"1\", \"2\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   2,\n\t\t\tslideForever: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅2、無限あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"3\", \"4\", \"5\",\n\t\t\t\t\"5\", \"6\", \"1\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   2,\n\t\t\tslideForever: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅3、無限あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"4\", \"5\", \"6\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   3,\n\t\t\tslideForever: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"3行描画、スライド幅3、無限あり\",\n\t\t\tsrc:  []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\"},\n\t\t\texpect: []string{\n\t\t\t\t\"1\", \"2\", \"3\",\n\t\t\t\t\"4\", \"5\", \"6\",\n\t\t\t\t\"7\", \"1\", \"2\",\n\t\t\t},\n\t\t\tlineCount:    3,\n\t\t\tslideWidth:   3,\n\t\t\tslideForever: true,\n\t\t},\n\t}\n\tfor _, v := range tds {\n\t\tt.Run(v.desc, func(t *testing.T) {\n\t\t\tgot := toSlideStrings(v.src, v.lineCount, v.slideWidth, v.slideForever)\n\t\t\tassert.Equal(t, v.expect, got, v.desc)\n\t\t})\n\t}\n}\n\nfunc TestRemoveZeroWidthCharacters(t *testing.T) {\n\ttype TestData struct {\n\t\tdesc   string\n\t\ts      string\n\t\texpect string\n\t}\n\ttds := []TestData{\n\t\t{desc: \"Zero width space (U+200B)が削除される\", s: \"A\\u200bB\", expect: \"AB\"},\n\t\t{desc: \"Zero width joiner (U+200C)が削除される\", s: \"A\\u200cB\", expect: \"AB\"},\n\t\t{desc: \"Zero width joiner (U+200D)が削除される\", s: \"A\\u200dB\", expect: \"AB\"},\n\t\t{desc: \"U+200B ~ U+200Dが削除される\", s: \"あ\\u200bい\\u200cう\\u200dえ\", expect: \"あいうえ\"},\n\t}\n\tfor _, v := range tds {\n\t\tt.Run(v.desc, func(t *testing.T) {\n\t\t\tgot := removeZeroWidthCharacters(v.s)\n\t\t\tassert.Equal(t, v.expect, got, v.desc)\n\t\t})\n\t}\n}\n\nfunc TestApplicationConfigSetFontFileAndFontIndex(t *testing.T) {\n\ttype TestData struct {\n\t\tdesc          string\n\t\tinFontFile    string\n\t\tinFontIndex   int\n\t\tinRuntimeOS   string\n\t\twantFontFile  string\n\t\twantFontIndex int\n\t}\n\ttests := []TestData{\n\t\t{\n\t\t\tdesc:          \"正常系: FontFileが設定済みの場合は変更なし\",\n\t\t\tinFontFile:    \"/usr/share/fonts/寿司\",\n\t\t\tinFontIndex:   0,\n\t\t\tinRuntimeOS:   \"linux\",\n\t\t\twantFontFile:  \"/usr/share/fonts/寿司\",\n\t\t\twantFontIndex: 0,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"正常系: フォント未設定でwindowsの場合はwindows用のフォントが設定される\",\n\t\t\tinRuntimeOS:   \"windows\",\n\t\t\twantFontFile:  defaultWindowsFont,\n\t\t\twantFontIndex: 0,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"正常系: フォント未設定でdarwinの場合はdarwin用のフォントが設定される\",\n\t\t\tinRuntimeOS:   \"darwin\",\n\t\t\twantFontFile:  defaultDarwinFont,\n\t\t\twantFontIndex: 0,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"正常系: フォント未設定でiosの場合はios用のフォントが設定される\",\n\t\t\tinRuntimeOS:   \"ios\",\n\t\t\twantFontFile:  defaultIOSFont,\n\t\t\twantFontIndex: 0,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"正常系: フォント未設定でandroidの場合はandroid用のフォントが設定される\",\n\t\t\tinRuntimeOS:   \"android\",\n\t\t\twantFontFile:  defaultAndroidFont,\n\t\t\twantFontIndex: 5,\n\t\t},\n\t\t// FIXME: ローカル環境で実行するとエラーになるので一旦無効化\n\t\t// {\n\t\t// \tdesc:          \"正常系: フォント未設定でlinuxの場合はlinux用のフォントが設定される。Linux用のフォントは2つ存在するが、1つ目のフォントはalpineコンテナ内にデフォルトでは存在しないため2つ目が設定される\",\n\t\t// \tinRuntimeOS:   \"linux\",\n\t\t// \twantFontFile:  defaultLinuxFont2,\n\t\t// \twantFontIndex: 5,\n\t\t// },\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\n\t\t\ta := Config{\n\t\t\t\tFontFile:  tt.inFontFile,\n\t\t\t\tFontIndex: tt.inFontIndex,\n\t\t\t}\n\t\t\ta.SetFontFileAndFontIndex(tt.inRuntimeOS)\n\n\t\t\tassert.Equal(tt.wantFontFile, a.FontFile)\n\t\t\tassert.Equal(tt.wantFontIndex, a.FontIndex)\n\t\t})\n\t}\n}\n\nfunc TestApplicationConfig_AddTimeStampToOutPath(t *testing.T) {\n\ttype TestData struct {\n\t\tdesc           string\n\t\tinOutpath      string\n\t\tinAddTimeStamp bool\n\t\tinTime         time.Time\n\t\twant           string\n\t}\n\ttests := []TestData{\n\t\t{\n\t\t\tdesc:           \"正常系: フラグfalseの場合は変更なし\",\n\t\t\tinOutpath:      \"t.png\",\n\t\t\tinAddTimeStamp: false,\n\t\t\tinTime:         time.Date(2000, 1, 1, 12, 10, 5, 2, time.Local),\n\t\t\twant:           \"t.png\",\n\t\t},\n\t\t{\n\t\t\tdesc:           \"正常系: フラグtrueの場合はタイムスタンプがつく\",\n\t\t\tinOutpath:      \"t.png\",\n\t\t\tinAddTimeStamp: true,\n\t\t\tinTime:         time.Date(2000, 1, 1, 12, 10, 5, 0, time.Local),\n\t\t\twant:           \"t_2000-01-01-121005.png\",\n\t\t},\n\t\t{\n\t\t\tdesc:           \"正常系: フルパスでも同様に動作する\",\n\t\t\tinOutpath:      \"/images/t.png\",\n\t\t\tinAddTimeStamp: true,\n\t\t\tinTime:         time.Date(2000, 1, 1, 12, 10, 5, 0, time.Local),\n\t\t\twant:           \"/images/t_2000-01-01-121005.png\",\n\t\t},\n\t\t{\n\t\t\tdesc:           \"正常系: ファイル拡張子が多重についていても動作する\",\n\t\t\tinOutpath:      \"/images/t.png.1\",\n\t\t\tinAddTimeStamp: true,\n\t\t\tinTime:         time.Date(2000, 1, 1, 12, 10, 5, 0, time.Local),\n\t\t\twant:           \"/images/t.png_2000-01-01-121005.1\",\n\t\t},\n\t\t{\n\t\t\tdesc:           \"正常系: Windowsのパス表現でも動作する\",\n\t\t\tinOutpath:      `C:\\Users\\foobar\\Pictures\\t.png`,\n\t\t\tinAddTimeStamp: true,\n\t\t\tinTime:         time.Date(2000, 1, 1, 12, 10, 5, 0, time.Local),\n\t\t\twant:           `C:\\Users\\foobar\\Pictures\\t_2000-01-01-121005.png`,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\n\t\t\ta := Config{\n\t\t\t\tOutpath:      tt.inOutpath,\n\t\t\t\tAddTimeStamp: tt.inAddTimeStamp,\n\t\t\t}\n\t\t\ta.addTimeStampToOutPath(tt.inTime)\n\n\t\t\tassert.Equal(tt.want, a.Outpath)\n\t\t})\n\t}\n}\n\nfunc TestOutputImageDir(t *testing.T) {\n\thome, err := os.UserHomeDir()\n\tassert.NoError(t, err)\n\tpictDir := filepath.Join(home, \"Pictures\")\n\n\ttype TestData struct {\n\t\tdesc           string\n\t\tinEnvDir       string\n\t\tinUseAnimation bool\n\t\twantPath       string\n\t\twantErr        bool\n\t}\n\ttests := []TestData{\n\t\t{\n\t\t\tdesc:           \"正常系: Env未設定の場合はホームディレクトリ配下のPictures配下が返る\",\n\t\t\tinEnvDir:       \"\",\n\t\t\tinUseAnimation: false,\n\t\t\twantPath:       filepath.Join(pictDir, \"t.png\"),\n\t\t\twantErr:        false,\n\t\t},\n\t\t{\n\t\t\tdesc:           \"正常系: animation trueの場合は Basenameが t.gif になる\",\n\t\t\tinEnvDir:       \"\",\n\t\t\tinUseAnimation: true,\n\t\t\twantPath:       filepath.Join(pictDir, \"t.gif\"),\n\t\t\twantErr:        false,\n\t\t},\n\t\t{\n\t\t\tdesc:           \"正常系: Envが設定されている場合は設定されている値が優先される\",\n\t\t\tinEnvDir:       filepath.Join(\".\", \"sushi\"),\n\t\t\tinUseAnimation: false,\n\t\t\twantPath:       filepath.Join(\".\", \"sushi\", \"t.png\"),\n\t\t\twantErr:        false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\n\t\t\tgot, err := outputImageDir(tt.inEnvDir, tt.inUseAnimation)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Equal(\"\", got)\n\t\t\t\tassert.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(err)\n\t\t\tassert.Equal(tt.wantPath, got)\n\t\t})\n\t}\n}\n\nfunc TestApplicationConfig_AddNumberSuffixToOutPath(t *testing.T) {\n\ttestdataDir := filepath.Join(\"..\", \"testdata\", \"in\")\n\n\texistedFile := filepath.Join(testdataDir, \"appconf_add_number_suffix_test_case1.png\")\n\texistedFileWant := filepath.Join(testdataDir, \"appconf_add_number_suffix_test_case1_2.png\")\n\n\tnotExistedFile := filepath.Join(testdataDir, \"appconf_add_number_suffix_sushi.png\")\n\n\ttype TestData struct {\n\t\tdesc               string\n\t\tinOutpath          string\n\t\tinSaveNumberedFile bool\n\t\twant               string\n\t}\n\ttests := []TestData{\n\t\t{\n\t\t\tdesc:               \"正常系: フラグfalseの場合は変更なし\",\n\t\t\tinOutpath:          existedFile,\n\t\t\tinSaveNumberedFile: false,\n\t\t\twant:               existedFile,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"正常系: フラグtrueの場合は連番を付与する\",\n\t\t\tinOutpath:          existedFile,\n\t\t\tinSaveNumberedFile: true,\n\t\t\twant:               existedFileWant,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"正常系: フラグtrueの場合でも、ファイルが存在しなければ何もしない\",\n\t\t\tinOutpath:          notExistedFile,\n\t\t\tinSaveNumberedFile: true,\n\t\t\twant:               notExistedFile,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\n\t\t\ta := Config{\n\t\t\t\tOutpath:          tt.inOutpath,\n\t\t\t\tSaveNumberedFile: tt.inSaveNumberedFile,\n\t\t\t}\n\t\t\ta.addNumberSuffixToOutPath()\n\n\t\t\tassert.Equal(tt.want, a.Outpath)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "config/envvar.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\ntype EnvVars struct {\n\tEmojiDir      string\n\tOutputDir     string\n\tFontFile      string\n\tEmojiFontFile string\n}\n\nconst (\n\tenvNameOutputDir     = \"TEXTIMG_OUTPUT_DIR\"\n\tenvNameFontFile      = \"TEXTIMG_FONT_FILE\"\n\tenvNameEmojiDir      = \"TEXTIMG_EMOJI_DIR\"\n\tenvNameEmojiFontFile = \"TEXTIMG_EMOJI_FONT_FILE\"\n)\n\nvar (\n\tenvs = map[string]string{\n\t\tenvNameOutputDir:     os.Getenv(envNameOutputDir),\n\t\tenvNameFontFile:      os.Getenv(envNameFontFile),\n\t\tenvNameEmojiDir:      os.Getenv(envNameEmojiDir),\n\t\tenvNameEmojiFontFile: os.Getenv(envNameEmojiFontFile),\n\t}\n)\n\nfunc NewEnvVars() EnvVars {\n\treturn EnvVars{\n\t\tOutputDir:     envs[envNameOutputDir],\n\t\tFontFile:      envs[envNameFontFile],\n\t\tEmojiDir:      envs[envNameEmojiDir],\n\t\tEmojiFontFile: envs[envNameEmojiFontFile],\n\t}\n}\n\nfunc PrintEnvs() {\n\tfor k, v := range envs {\n\t\ttext := fmt.Sprintf(\"%s=%s\", k, v)\n\t\tfmt.Println(text)\n\t}\n}\n"
  },
  {
    "path": "config/face.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/jiro4989/textimg/v3/log\"\n\t\"golang.org/x/image/font\"\n\t\"golang.org/x/image/font/gofont/gomono\"\n\t\"golang.org/x/image/font/opentype\"\n)\n\n// readFace はfontPathのフォントファイルからfaceを返す。\nfunc readFace(fontPath string, fontIndex int, fontSize float64) (font.Face, error) {\n\tvar ft *opentype.Font\n\n\t// ファイルが存在しなければビルトインのフォントをデフォルトとして使う\n\t_, err := os.Stat(fontPath)\n\tif err == nil {\n\t\tfontData, err := os.ReadFile(fontPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch strings.ToLower(filepath.Ext(fontPath)) {\n\t\tcase \".otc\", \".ttc\":\n\t\t\tftc, err := opentype.ParseCollection(fontData)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tft, err = ftc.Font(fontIndex)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tdefault:\n\t\t\tft, err = opentype.Parse(fontData)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tlog.Warnf(\"%s is not found. please set font path with `-f` option\", fontPath)\n\t\tft, err = opentype.Parse(gomono.TTF)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\topt := opentype.FaceOptions{\n\t\tSize:    fontSize,\n\t\tDPI:     72,\n\t\tHinting: 0,\n\t}\n\tface, err := opentype.NewFace(ft, &opt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn face, nil\n}\n"
  },
  {
    "path": "config/face_test.go",
    "content": "package config\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestReadFace(t *testing.T) {\n\ttestdataDir := filepath.Join(\"..\", \"testdata\", \"in\")\n\n\ttype TestData struct {\n\t\tdesc        string\n\t\tinFontPath  string\n\t\tinFontIndex int\n\t\twantErr     bool\n\t}\n\ttests := []TestData{\n\t\t{\n\t\t\tdesc:        \"正常系: font.Faceが取得できる\",\n\t\t\tinFontPath:  \"/tmp/MyricaM.TTC\",\n\t\t\tinFontIndex: 0,\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"正常系: 存在しないファイルの場合もエラーにはならない\",\n\t\t\tinFontPath:  \"/tmp/寿司\",\n\t\t\tinFontIndex: 0,\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"異常系: パスとしては存在するがディレクトリの場合はエラー\",\n\t\t\tinFontPath:  \"/tmp\",\n\t\t\tinFontIndex: 0,\n\t\t\twantErr:     true,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"異常系: ファイルは存在するけれど、フォントファイルじゃない時はエラー (ttc)\",\n\t\t\tinFontPath:  filepath.Join(testdataDir, \"illegal_font.ttc\"),\n\t\t\tinFontIndex: 0,\n\t\t\twantErr:     true,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"異常系: ファイルは存在するけれど、フォントファイルじゃない時はエラー (otc)\",\n\t\t\tinFontPath:  filepath.Join(testdataDir, \"illegal_font.otc\"),\n\t\t\tinFontIndex: 0,\n\t\t\twantErr:     true,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"異常系: ファイルは存在するけれど、フォントファイルじゃない時はエラー (txt)\",\n\t\t\tinFontPath:  filepath.Join(testdataDir, \"illegal_font.txt\"),\n\t\t\tinFontIndex: 0,\n\t\t\twantErr:     true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\n\t\t\tgot, err := readFace(tt.inFontPath, tt.inFontIndex, 20)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Nil(got)\n\t\t\t\tassert.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NotNil(got)\n\t\t\tassert.NoError(err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "config/writer_mock.go",
    "content": "package config\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\ntype MockWriter struct {\n\twriteErr bool\n\tcloseErr bool\n}\n\nfunc NewMockWriter(w, c bool) io.WriteCloser {\n\treturn &MockWriter{\n\t\twriteErr: w,\n\t\tcloseErr: c,\n\t}\n}\n\nfunc (m *MockWriter) Write(p []byte) (n int, err error) {\n\tif m.writeErr {\n\t\treturn -1, errors.New(\"write error\")\n\t}\n\treturn 0, nil\n}\n\nfunc (m *MockWriter) Close() error {\n\tif m.closeErr {\n\t\treturn errors.New(\"close error\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "---\n\nversion: '3.7'\n\nservices:\n  base: &common\n    build:\n      context: ./\n      dockerfile: ./Dockerfile\n      target: base\n    container_name: textimg_base\n    image: jiro4989/textimg-base\n    working_dir: /app\n    volumes:\n      - \"$PWD:/app\"\n      - \"gopkg:/go/pkg\" # 名前付きボリュームで依存パッケージを永続化\n      - \"$PWD/images:/images\"\n    environment:\n      TEXTIMG_FONT_FILE: /tmp/MyricaM.TTC\n      TEXTIMG_EMOJI_DIR: /usr/local/src/noto-emoji/png/128\n      TEXTIMG_EMOJI_FONT_FILE: /tmp/Symbola_hint.ttf\n\n  textimg:\n    build:\n      context: ./\n      dockerfile: ./Dockerfile\n    container_name: textimg\n    image: jiro4989/textimg\n    volumes:\n      - \"$PWD/images:/images\"\n\nvolumes:\n  gopkg:\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/jiro4989/textimg/v3\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/kr/pretty v0.2.0 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.23\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/image v0.39.0\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/term v0.42.0\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\nrequire github.com/oliamb/cutter v0.2.2\n\nrequire (\n\tgithub.com/clipperhouse/uax29/v2 v2.2.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/spf13/pflag v1.0.9 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=\ngithub.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=\ngithub.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k=\ngithub.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=\ngolang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=\ngolang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "image/encode.go",
    "content": "package image\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color/palette\"\n\t\"image/draw\"\n\t\"image/gif\"\n\t\"image/jpeg\"\n\t\"image/png\"\n\t\"io\"\n)\n\nfunc (i *Image) Encode(w io.Writer, ext string) error {\n\timg := i.image\n\tswitch ext {\n\tcase \".png\":\n\t\treturn png.Encode(w, img)\n\tcase \".jpg\", \".jpeg\":\n\t\treturn jpeg.Encode(w, img, nil)\n\tcase \".gif\":\n\t\tif i.useAnimation {\n\t\t\tvar delays []int\n\t\t\tfor x := 0; x < len(i.animationImages); x++ {\n\t\t\t\tdelays = append(delays, i.delay)\n\t\t\t}\n\t\t\treturn gif.EncodeAll(w, &gif.GIF{\n\t\t\t\tImage: toPalettes(i.animationImages),\n\t\t\t\tDelay: delays,\n\t\t\t})\n\t\t}\n\t\treturn gif.Encode(w, img, nil)\n\t}\n\treturn fmt.Errorf(\"%s is not supported extension.\", ext)\n}\n\nfunc toPalettes(imgs []image.Image) (ret []*image.Paletted) {\n\tfor _, v := range imgs {\n\t\tbounds := v.Bounds()\n\t\tp := image.NewPaletted(bounds, palette.Plan9)\n\t\tdraw.Draw(p, p.Rect, v, bounds.Min, draw.Over)\n\t\tret = append(ret, p)\n\t}\n\treturn\n}\n"
  },
  {
    "path": "image/image.go",
    "content": "package image\n\nimport (\n\t\"image\"\n\tc \"image/color\"\n\t\"image/draw\"\n\t\"os\"\n\n\t\"github.com/jiro4989/textimg/v3/color\"\n\t\"github.com/jiro4989/textimg/v3/token\"\n\t\"github.com/mattn/go-runewidth\"\n\t\"github.com/oliamb/cutter\"\n\txdraw \"golang.org/x/image/draw\"\n\t\"golang.org/x/image/font\"\n\t\"golang.org/x/image/math/fixed\"\n)\n\ntype (\n\tImage struct {\n\t\timage                     *image.RGBA\n\t\tanimationImages           []image.Image\n\t\tx                         int\n\t\ty                         int\n\t\tforegroundColor           c.RGBA // 文字色\n\t\tbackgroundColor           c.RGBA // 背景色\n\t\tdefaultForegroundColor    c.RGBA // 文字色\n\t\tdefaultBackgroundColor    c.RGBA // 背景色\n\t\tfontSize                  int    // フォントサイズ\n\t\tfontFace                  font.Face\n\t\temojiFontFace             font.Face\n\t\tcharWidth                 int\n\t\tcharHeight                int\n\t\temojiDir                  string\n\t\tuseEmoji                  bool\n\t\tlineCount                 int\n\t\tuseAnimation              bool\n\t\tanimationLineCount        int\n\t\tanimationImageFlameHeight int\n\t\tresizeWidth               int\n\t\tresizeHeight              int\n\t\tdelay                     int\n\t}\n\tImageParam struct {\n\t\tBaseWidth          int\n\t\tBaseHeight         int\n\t\tForegroundColor    c.RGBA // 文字色\n\t\tBackgroundColor    c.RGBA // 背景色\n\t\tFontSize           int    // フォントサイズ\n\t\tFontFace           font.Face\n\t\tEmojiFontFace      font.Face\n\t\tEmojiDir           string\n\t\tUseEmoji           bool\n\t\tUseAnimation       bool\n\t\tAnimationLineCount int\n\t\tResizeWidth        int\n\t\tResizeHeight       int\n\t\tDelay              int\n\t}\n)\n\nfunc init() {\n\t// Unicode Neutral で定義されている絵文字(例: 👁)を幅2として扱う\n\trunewidth.DefaultCondition.StrictEmojiNeutral = false\n}\n\nfunc NewImage(p *ImageParam) *Image {\n\tvar (\n\t\tcharWidth   = p.FontSize / 2\n\t\tcharHeight  = int(float64(p.FontSize) * 1.1)\n\t\timageWidth  = p.BaseWidth * charWidth\n\t\timageHeight = p.BaseHeight * charHeight\n\t)\n\n\tvar animationImageFlameHeight int\n\tif p.UseAnimation {\n\t\tanimationImageFlameHeight = imageHeight / (p.BaseHeight / p.AnimationLineCount)\n\t}\n\n\timage := newImage(imageWidth, imageHeight)\n\n\treturn &Image{\n\t\timage:                     image,\n\t\tforegroundColor:           p.ForegroundColor,\n\t\tbackgroundColor:           p.BackgroundColor,\n\t\tdefaultForegroundColor:    p.ForegroundColor,\n\t\tdefaultBackgroundColor:    p.BackgroundColor,\n\t\tfontSize:                  p.FontSize,\n\t\tfontFace:                  p.FontFace,\n\t\temojiFontFace:             p.EmojiFontFace,\n\t\tcharWidth:                 charWidth,\n\t\tcharHeight:                charHeight,\n\t\temojiDir:                  p.EmojiDir,\n\t\tuseEmoji:                  p.UseEmoji,\n\t\tuseAnimation:              p.UseAnimation,\n\t\tanimationLineCount:        p.AnimationLineCount,\n\t\tanimationImageFlameHeight: animationImageFlameHeight,\n\t\tresizeWidth:               p.ResizeWidth,\n\t\tresizeHeight:              p.ResizeHeight,\n\t\tdelay:                     p.Delay,\n\t}\n}\n\nfunc newImage(w, h int) *image.RGBA {\n\treturn image.NewRGBA(image.Rect(0, 0, w, h))\n}\n\nfunc (i *Image) Draw(tokens token.Tokens) error {\n\ti.drawBackgroundAll()\n\n\t// 背景のみ描画\n\tfor _, t := range tokens {\n\t\tswitch t.Kind {\n\t\tcase token.KindColor:\n\t\t\ti.updateColor(t.ColorType, t.Color)\n\t\tcase token.KindText:\n\t\t\ti.drawBackground(t.Text)\n\t\t\tfor _, r := range t.Text {\n\t\t\t\tif isLinefeed(r) {\n\t\t\t\t\ti.moveDown()\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\ti.moveRight(r)\n\t\t\t}\n\t\t}\n\t}\n\ti.resetColor()\n\ti.resetPosition()\n\n\t// 文字のみ描画\n\tfor _, t := range tokens {\n\t\tswitch t.Kind {\n\t\tcase token.KindColor:\n\t\t\ti.updateColor(t.ColorType, t.Color)\n\t\tcase token.KindText:\n\t\t\tfor _, r := range t.Text {\n\t\t\t\tif isLinefeed(r) {\n\t\t\t\t\ti.moveDown()\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif err := i.draw(r); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\ti.moveRight(r)\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := i.setAnimationFlames(); err != nil {\n\t\treturn err\n\t}\n\ti.scale()\n\n\treturn nil\n}\n\n// 背景色をデフォルト色で塗りつぶす。\nfunc (i *Image) drawBackgroundAll() {\n\tvar (\n\t\tbounds = i.image.Bounds().Max\n\t\twidth  = bounds.X\n\t\theight = bounds.Y\n\t)\n\tfor x := 0; x < width; x++ {\n\t\tfor y := 0; y < height; y++ {\n\t\t\ti.image.Set(x, y, c.RGBA(i.defaultBackgroundColor))\n\t\t}\n\t}\n}\n\nfunc (i *Image) updateColor(t token.ColorType, col color.RGBA) {\n\tswitch t {\n\tcase token.ColorTypeReset:\n\t\ti.resetColor()\n\tcase token.ColorTypeResetForeground:\n\t\ti.foregroundColor = i.defaultForegroundColor\n\tcase token.ColorTypeResetBackground:\n\t\ti.backgroundColor = i.defaultBackgroundColor\n\tcase token.ColorTypeReverse:\n\t\ti.foregroundColor, i.backgroundColor = i.backgroundColor, i.foregroundColor\n\tcase token.ColorTypeForeground:\n\t\ti.foregroundColor = c.RGBA(col)\n\tcase token.ColorTypeBackground:\n\t\ti.backgroundColor = c.RGBA(col)\n\t}\n}\n\nfunc (i *Image) resetColor() {\n\ti.foregroundColor = i.defaultForegroundColor\n\ti.backgroundColor = i.defaultBackgroundColor\n}\n\nfunc (i *Image) resetPosition() {\n\ti.x = 0\n\ti.y = 0\n\ti.lineCount = 0\n}\n\nfunc (i *Image) newDrawer(f font.Face) *font.Drawer {\n\t// 特殊な位置調整。なんでこんな計算式にしたのか覚えていない\n\tvar (\n\t\tx = i.x\n\t\ty = i.y + i.charHeight - (i.charHeight / 5)\n\t)\n\t// FIXME: なんか警告出てる\n\tpoint := fixed.Point26_6{fixed.Int26_6(x * 64), fixed.Int26_6(y * 64)}\n\td := &font.Drawer{\n\t\tDst:  i.image,\n\t\tSrc:  image.NewUniform(c.RGBA(i.foregroundColor)),\n\t\tFace: f,\n\t\tDot:  point,\n\t}\n\treturn d\n}\n\nfunc (i *Image) draw(r rune) error {\n\tif ok, emojiPath := isEmoji(r, i.emojiDir); ok {\n\t\tif i.useEmoji {\n\t\t\ti.drawRune(r, i.emojiFontFace)\n\t\t\treturn nil\n\t\t}\n\t\treturn i.drawEmoji(r, emojiPath)\n\t}\n\ti.drawRune(r, i.fontFace)\n\treturn nil\n}\n\nfunc (i *Image) setAnimationFlames() error {\n\tif i.useAnimation {\n\t\tb := i.image.Bounds().Max\n\t\tw, h := b.X, i.animationImageFlameHeight\n\t\tmax := b.Y / i.animationImageFlameHeight\n\t\tfor rc := 0; rc < max; rc++ {\n\t\t\tx, y := 0, rc*h\n\t\t\tpt := image.Pt(x, y)\n\t\t\tcimg, err := cutter.Crop(i.image, cutter.Config{\n\t\t\t\tWidth:   w,\n\t\t\t\tHeight:  h,\n\t\t\t\tAnchor:  pt,\n\t\t\t\tMode:    cutter.TopLeft,\n\t\t\t\tOptions: cutter.Copy,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdist := image.NewRGBA(image.Rectangle{\n\t\t\t\timage.Pt(0, 0),\n\t\t\t\timage.Pt(w, h),\n\t\t\t})\n\t\t\tdraw.Draw(dist, dist.Bounds(), cimg, pt, draw.Over)\n\t\t\ti.animationImages = append(i.animationImages, dist)\n\t\t}\n\t}\n\treturn nil\n}\n\n// rune文字を画像に書き込む。\n// 書き込み終えると座標を更新する。\nfunc (i *Image) drawRune(r rune, f font.Face) {\n\td := i.newDrawer(f)\n\td.DrawString(string(r))\n}\n\nfunc (i *Image) drawEmoji(r rune, path string) error {\n\tfp, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fp.Close()\n\n\temoji, _, err := image.Decode(fp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\td := i.newDrawer(i.fontFace)\n\t// 画像サイズをフォントサイズに合わせる\n\t// 0.9でさらに微妙に調整\n\tsize := int(float64(d.Face.Metrics().Ascent.Floor()+d.Face.Metrics().Descent.Floor()) * 0.9)\n\trect := image.Rect(0, 0, size, size)\n\tdst := image.NewRGBA(rect)\n\txdraw.ApproxBiLinear.Scale(dst, rect, emoji, emoji.Bounds(), draw.Over, nil)\n\n\tp := image.Pt(d.Dot.X.Floor(), d.Dot.Y.Floor()-d.Face.Metrics().Ascent.Floor())\n\tdraw.Draw(i.image, rect.Add(p), dst, image.Point{}, draw.Over)\n\treturn nil\n}\n\nfunc (i *Image) drawBackground(s string) {\n\tvar (\n\t\ttw     = runewidth.StringWidth(s)\n\t\twidth  = tw * i.charWidth\n\t\theight = i.charHeight\n\t\tposX   = i.x\n\t\tposY   = i.y\n\t)\n\tfor x := posX; x < posX+width; x++ {\n\t\tfor y := posY; y < posY+height; y++ {\n\t\t\ti.image.Set(x, y, c.RGBA(i.backgroundColor))\n\t\t}\n\t}\n}\n\nfunc (i *Image) moveRight(r rune) {\n\ti.x += runewidth.RuneWidth(r) * i.charWidth\n}\n\nfunc (i *Image) moveDown() {\n\ti.x = 0\n\ti.y += i.charHeight\n\ti.lineCount++\n}\n\nfunc (i *Image) newScaledImage() *image.RGBA {\n\tif i.resizeWidth == 0 && i.resizeHeight == 0 {\n\t\treturn i.image\n\t}\n\n\t// 呼び出し側で大きさを調整していること\n\tdst := scale(i.image, i.resizeWidth, i.resizeHeight)\n\treturn dst\n}\n\nfunc scale(img image.Image, w, h int) *image.RGBA {\n\trect := img.Bounds()\n\tdst := newImage(w, h)\n\txdraw.CatmullRom.Scale(dst, dst.Bounds(), img, rect, draw.Over, nil)\n\treturn dst\n}\n\nfunc (i *Image) scale() {\n\tif i.resizeWidth == 0 && i.resizeHeight == 0 {\n\t\treturn\n\t}\n\n\ti.image = i.newScaledImage()\n\tfor j, img := range i.animationImages {\n\t\tdst := scale(img, i.resizeWidth, i.resizeHeight)\n\t\ti.animationImages[j] = dst\n\t}\n}\n"
  },
  {
    "path": "image/util.go",
    "content": "package image\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nvar (\n\t// 絵文字描画の際に、普通に描画してほしいけれど絵文字としても定義されている\n\t// 文字のコードポイント\n\texRunes = []rune{\n\t\t0x0023, // #\n\t\t0x002A, // *\n\t\t0x0030, // 0\n\t\t0x0031, // 1\n\t\t0x0032, // 2\n\t\t0x0033, // 3\n\t\t0x0034, // 4\n\t\t0x0035, // 5\n\t\t0x0036, // 6\n\t\t0x0037, // 7\n\t\t0x0038, // 8\n\t\t0x0039, // 9\n\t\t0x00A9, // ©\n\t\t0x00AE, // ®️\n\t}\n)\n\n// コードポイントに対応する画像ファイルかどうかを判定する。\n// 画像ファイルだった場合は当該画像ファイルのパスを返却する。\nfunc isEmoji(r rune, emojiDir string) (bool, string) {\n\tpath := fmt.Sprintf(\"%s/emoji_u%.4x.png\", emojiDir, r)\n\t_, err := os.Stat(path)\n\tif err == nil && !isExceptionallyCodePoint(r) {\n\t\treturn true, path\n\t}\n\treturn false, \"\"\n}\n\n// r が例外的なコードポイントに存在するかを判定する。\n// http://unicode.org/Public/emoji/4.0/emoji-data.txt\n//\n// ここでtrueを返す文字は、絵文字データ的には絵文字ではあるものの、\n// シェル芸bot環境ではテキストとして表示したいので例外的に除外するために指定して\n// いる。\nfunc isExceptionallyCodePoint(r rune) bool {\n\tfor _, ex := range exRunes {\n\t\tif r == ex {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc isLinefeed(r rune) bool {\n\treturn r == '\\n'\n}\n"
  },
  {
    "path": "images/.gitkeep",
    "content": ""
  },
  {
    "path": "internal/global/env.go",
    "content": "package global\n\nconst (\n\tEnvNameOutputDir     = \"TEXTIMG_OUTPUT_DIR\"\n\tEnvNameFontFile      = \"TEXTIMG_FONT_FILE\"\n\tEnvNameEmojiDir      = \"TEXTIMG_EMOJI_DIR\"\n\tEnvNameEmojiFontFile = \"TEXTIMG_EMOJI_FONT_FILE\"\n)\n\nvar (\n\tEnvNames = []string{\n\t\tEnvNameOutputDir,\n\t\tEnvNameFontFile,\n\t\tEnvNameEmojiDir,\n\t\tEnvNameEmojiFontFile,\n\t}\n)\n"
  },
  {
    "path": "internal/global/version.go",
    "content": "package global\n\nconst (\n\tAppName = \"textimg\"\n\tVersion = `3.1.10\nCopyright (c) 2019 jiro4989\nReleased under the MIT License.\nhttps://github.com/jiro4989/textimg`\n)\n"
  },
  {
    "path": "log/log.go",
    "content": "package log\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/jiro4989/textimg/v3/internal/global\"\n)\n\nconst (\n\tdebugPrefix = \"[DEBUG]\"\n\tinfoPrefix  = \"[INFO]\"\n\twarnPrefix  = \"[WARN]\"\n\terrorPrefix = \"[ERROR]\"\n)\n\nfunc log(lvl string, msg interface{}) {\n\t_, f, l, ok := runtime.Caller(2)\n\tif !ok {\n\t\tfmt.Fprintln(os.Stderr, \"something error occurred.\")\n\t\treturn\n\t}\n\n\tnow := time.Now().Format(\"2006/01/02 03:04:05\")\n\ttext := fmt.Sprintf(\"%s %s %s %s:%d %v\", now, global.AppName, lvl, f, l, msg)\n\tfmt.Fprintln(os.Stderr, text)\n}\n\nfunc Debug(msg interface{}) {\n\tlog(debugPrefix, msg)\n}\n\nfunc Info(msg interface{}) {\n\tlog(infoPrefix, msg)\n}\n\nfunc Warn(msg interface{}) {\n\tlog(warnPrefix, msg)\n}\n\nfunc Warnf(format string, msg interface{}) {\n\ttext := fmt.Sprintf(format, msg)\n\tlog(warnPrefix, text)\n}\n\nfunc Error(msg interface{}) {\n\tlog(errorPrefix, msg)\n}\n"
  },
  {
    "path": "log/log_test.go",
    "content": "package log\n\nimport (\n\t\"testing\"\n)\n\nfunc TestDebug(t *testing.T) {\n\tDebug(\"debug\")\n\tInfo(\"debug\")\n\tWarn(\"debug\")\n\tWarnf(\"%v\", 1)\n\tError(\"debug\")\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"os\"\n)\n\nfunc main() {\n\tos.Exit(Main())\n}\n\nfunc Main() int {\n\tif err := RootCommand.Execute(); err != nil {\n\t\treturn -1\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "main_test.go",
    "content": "//go:build !docker\n\npackage main\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nconst (\n\tinDir  = \"testdata/in\"\n\toutDir = \"testdata/out\"\n)\n\nfunc TestMain(m *testing.M) {\n\ttestBefore()\n\texitCode := m.Run()\n\tos.Exit(exitCode)\n}\n\nfunc testBefore() {\n\tif err := os.RemoveAll(outDir); err != nil {\n\t\tpanic(err)\n\t}\n\t// nolint\n\tos.Mkdir(outDir, os.ModePerm)\n}\n"
  },
  {
    "path": "parser/grammar.peg",
    "content": "package parser\n\ntype Parser Peg {\n  ParserFunc\n}\n\nroot <-\n  (colors / ignore / text)*\n\nignore <-\n  prefix number? non_color_suffix\n  / escape_sequence\n\ncolors <-\n  prefix color_suffix { p.pushResetColor() }\n  / prefix color (delimiter color)* color_suffix\n\ntext <-\n  < [^\\e] + > { p.pushText(text) }\n\ncolor <-\n  standard_color\n  / extended_color\n  / text_attributes\n\nstandard_color <-\n  zero < ([349] / '10') [0-7] > { p.pushStandardColorWithCategory(text) }\n  / zero < [39] '9' >           { p.pushResetForegroundColor() }\n  / zero < ('4' / '10') '9' >   { p.pushResetBackgroundColor() }\n\nextended_color <-\n  extended_color_256 / extended_color_rgb\n\nextended_color_256 <-\n  extended_color_prefix\n  delimiter\n  zero '5'\n  delimiter\n  < number > { p.setExtendedColor256(text) }\n\nextended_color_rgb <-\n  extended_color_prefix\n  delimiter\n  zero '2'\n  delimiter\n  < number > { p.setExtendedColorR(text) }\n  delimiter\n  < number > { p.setExtendedColorG(text) }\n  delimiter\n  < number > { p.setExtendedColorB(text) }\n\nextended_color_prefix <-\n  zero\n  < [34] '8' > { p.pushExtendedColor(text) }\n\ntext_attributes <-\n  (\n  '0' { p.pushResetColor() }\n  / '7' { p.pushReverseColor() }\n  / [1458]\n  )+\n\nzero             <- '0' *\nnumber           <- [0-9]+\nprefix           <- escape_sequence '['\nescape_sequence  <- '\\e'\ncolor_suffix     <- 'm'\nnon_color_suffix <- [A-HfSTJK]\ndelimiter        <- ';'\n"
  },
  {
    "path": "parser/grammar.peg.go",
    "content": "package parser\n\n// Code generated by peg parser/grammar.peg DO NOT EDIT.\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst endSymbol rune = 1114112\n\n/* The rule types inferred from the grammar are below. */\ntype pegRule uint8\n\nconst (\n\truleUnknown pegRule = iota\n\truleroot\n\truleignore\n\trulecolors\n\truletext\n\trulecolor\n\trulestandard_color\n\truleextended_color\n\truleextended_color_256\n\truleextended_color_rgb\n\truleextended_color_prefix\n\truletext_attributes\n\trulezero\n\trulenumber\n\truleprefix\n\truleescape_sequence\n\trulecolor_suffix\n\trulenon_color_suffix\n\truledelimiter\n\truleAction0\n\trulePegText\n\truleAction1\n\truleAction2\n\truleAction3\n\truleAction4\n\truleAction5\n\truleAction6\n\truleAction7\n\truleAction8\n\truleAction9\n\truleAction10\n\truleAction11\n)\n\nvar rul3s = [...]string{\n\t\"Unknown\",\n\t\"root\",\n\t\"ignore\",\n\t\"colors\",\n\t\"text\",\n\t\"color\",\n\t\"standard_color\",\n\t\"extended_color\",\n\t\"extended_color_256\",\n\t\"extended_color_rgb\",\n\t\"extended_color_prefix\",\n\t\"text_attributes\",\n\t\"zero\",\n\t\"number\",\n\t\"prefix\",\n\t\"escape_sequence\",\n\t\"color_suffix\",\n\t\"non_color_suffix\",\n\t\"delimiter\",\n\t\"Action0\",\n\t\"PegText\",\n\t\"Action1\",\n\t\"Action2\",\n\t\"Action3\",\n\t\"Action4\",\n\t\"Action5\",\n\t\"Action6\",\n\t\"Action7\",\n\t\"Action8\",\n\t\"Action9\",\n\t\"Action10\",\n\t\"Action11\",\n}\n\ntype token32 struct {\n\tpegRule\n\tbegin, end uint32\n}\n\nfunc (t *token32) String() string {\n\treturn fmt.Sprintf(\"\\x1B[34m%v\\x1B[m %v %v\", rul3s[t.pegRule], t.begin, t.end)\n}\n\ntype node32 struct {\n\ttoken32\n\tup, next *node32\n}\n\nfunc (node *node32) print(w io.Writer, pretty bool, buffer string) {\n\tvar print func(node *node32, depth int)\n\tprint = func(node *node32, depth int) {\n\t\tfor node != nil {\n\t\t\tfor c := 0; c < depth; c++ {\n\t\t\t\tfmt.Fprintf(w, \" \")\n\t\t\t}\n\t\t\trule := rul3s[node.pegRule]\n\t\t\tquote := strconv.Quote(string(([]rune(buffer)[node.begin:node.end])))\n\t\t\tif !pretty {\n\t\t\t\tfmt.Fprintf(w, \"%v %v\\n\", rule, quote)\n\t\t\t} else {\n\t\t\t\tfmt.Fprintf(w, \"\\x1B[36m%v\\x1B[m %v\\n\", rule, quote)\n\t\t\t}\n\t\t\tif node.up != nil {\n\t\t\t\tprint(node.up, depth+1)\n\t\t\t}\n\t\t\tnode = node.next\n\t\t}\n\t}\n\tprint(node, 0)\n}\n\nfunc (node *node32) Print(w io.Writer, buffer string) {\n\tnode.print(w, false, buffer)\n}\n\nfunc (node *node32) PrettyPrint(w io.Writer, buffer string) {\n\tnode.print(w, true, buffer)\n}\n\ntype tokens32 struct {\n\ttree []token32\n}\n\nfunc (t *tokens32) Trim(length uint32) {\n\tt.tree = t.tree[:length]\n}\n\nfunc (t *tokens32) Print() {\n\tfor _, token := range t.tree {\n\t\tfmt.Println(token.String())\n\t}\n}\n\nfunc (t *tokens32) AST() *node32 {\n\ttype element struct {\n\t\tnode *node32\n\t\tdown *element\n\t}\n\ttokens := t.Tokens()\n\tvar stack *element\n\tfor _, token := range tokens {\n\t\tif token.begin == token.end {\n\t\t\tcontinue\n\t\t}\n\t\tnode := &node32{token32: token}\n\t\tfor stack != nil && stack.node.begin >= token.begin && stack.node.end <= token.end {\n\t\t\tstack.node.next = node.up\n\t\t\tnode.up = stack.node\n\t\t\tstack = stack.down\n\t\t}\n\t\tstack = &element{node: node, down: stack}\n\t}\n\tif stack != nil {\n\t\treturn stack.node\n\t}\n\treturn nil\n}\n\nfunc (t *tokens32) PrintSyntaxTree(buffer string) {\n\tt.AST().Print(os.Stdout, buffer)\n}\n\nfunc (t *tokens32) WriteSyntaxTree(w io.Writer, buffer string) {\n\tt.AST().Print(w, buffer)\n}\n\nfunc (t *tokens32) PrettyPrintSyntaxTree(buffer string) {\n\tt.AST().PrettyPrint(os.Stdout, buffer)\n}\n\nfunc (t *tokens32) Add(rule pegRule, begin, end, index uint32) {\n\ttree, i := t.tree, int(index)\n\tif i >= len(tree) {\n\t\tt.tree = append(tree, token32{pegRule: rule, begin: begin, end: end})\n\t\treturn\n\t}\n\ttree[i] = token32{pegRule: rule, begin: begin, end: end}\n}\n\nfunc (t *tokens32) Tokens() []token32 {\n\treturn t.tree\n}\n\ntype Parser struct {\n\tParserFunc\n\n\tBuffer string\n\tbuffer []rune\n\trules  [32]func() bool\n\tparse  func(rule ...int) error\n\treset  func()\n\tPretty bool\n\ttokens32\n}\n\nfunc (p *Parser) Parse(rule ...int) error {\n\treturn p.parse(rule...)\n}\n\nfunc (p *Parser) Reset() {\n\tp.reset()\n}\n\ntype textPosition struct {\n\tline, symbol int\n}\n\ntype textPositionMap map[int]textPosition\n\nfunc translatePositions(buffer []rune, positions []int) textPositionMap {\n\tlength, translations, j, line, symbol := len(positions), make(textPositionMap, len(positions)), 0, 1, 0\n\tsort.Ints(positions)\n\nsearch:\n\tfor i, c := range buffer {\n\t\tif c == '\\n' {\n\t\t\tline, symbol = line+1, 0\n\t\t} else {\n\t\t\tsymbol++\n\t\t}\n\t\tif i == positions[j] {\n\t\t\ttranslations[positions[j]] = textPosition{line, symbol}\n\t\t\tfor j++; j < length; j++ {\n\t\t\t\tif i != positions[j] {\n\t\t\t\t\tcontinue search\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak search\n\t\t}\n\t}\n\n\treturn translations\n}\n\ntype parseError struct {\n\tp   *Parser\n\tmax token32\n}\n\nfunc (e *parseError) Error() string {\n\ttokens, err := []token32{e.max}, \"\\n\"\n\tpositions, p := make([]int, 2*len(tokens)), 0\n\tfor _, token := range tokens {\n\t\tpositions[p], p = int(token.begin), p+1\n\t\tpositions[p], p = int(token.end), p+1\n\t}\n\ttranslations := translatePositions(e.p.buffer, positions)\n\tformat := \"parse error near %v (line %v symbol %v - line %v symbol %v):\\n%v\\n\"\n\tif e.p.Pretty {\n\t\tformat = \"parse error near \\x1B[34m%v\\x1B[m (line %v symbol %v - line %v symbol %v):\\n%v\\n\"\n\t}\n\tfor _, token := range tokens {\n\t\tbegin, end := int(token.begin), int(token.end)\n\t\terr += fmt.Sprintf(format,\n\t\t\trul3s[token.pegRule],\n\t\t\ttranslations[begin].line, translations[begin].symbol,\n\t\t\ttranslations[end].line, translations[end].symbol,\n\t\t\tstrconv.Quote(string(e.p.buffer[begin:end])))\n\t}\n\n\treturn err\n}\n\nfunc (p *Parser) PrintSyntaxTree() {\n\tif p.Pretty {\n\t\tp.tokens32.PrettyPrintSyntaxTree(p.Buffer)\n\t} else {\n\t\tp.tokens32.PrintSyntaxTree(p.Buffer)\n\t}\n}\n\nfunc (p *Parser) WriteSyntaxTree(w io.Writer) {\n\tp.tokens32.WriteSyntaxTree(w, p.Buffer)\n}\n\nfunc (p *Parser) SprintSyntaxTree() string {\n\tvar bldr strings.Builder\n\tp.WriteSyntaxTree(&bldr)\n\treturn bldr.String()\n}\n\nfunc (p *Parser) Execute() {\n\tbuffer, _buffer, text, begin, end := p.Buffer, p.buffer, \"\", 0, 0\n\tfor _, token := range p.Tokens() {\n\t\tswitch token.pegRule {\n\n\t\tcase rulePegText:\n\t\t\tbegin, end = int(token.begin), int(token.end)\n\t\t\ttext = string(_buffer[begin:end])\n\n\t\tcase ruleAction0:\n\t\t\tp.pushResetColor()\n\t\tcase ruleAction1:\n\t\t\tp.pushText(text)\n\t\tcase ruleAction2:\n\t\t\tp.pushStandardColorWithCategory(text)\n\t\tcase ruleAction3:\n\t\t\tp.pushResetForegroundColor()\n\t\tcase ruleAction4:\n\t\t\tp.pushResetBackgroundColor()\n\t\tcase ruleAction5:\n\t\t\tp.setExtendedColor256(text)\n\t\tcase ruleAction6:\n\t\t\tp.setExtendedColorR(text)\n\t\tcase ruleAction7:\n\t\t\tp.setExtendedColorG(text)\n\t\tcase ruleAction8:\n\t\t\tp.setExtendedColorB(text)\n\t\tcase ruleAction9:\n\t\t\tp.pushExtendedColor(text)\n\t\tcase ruleAction10:\n\t\t\tp.pushResetColor()\n\t\tcase ruleAction11:\n\t\t\tp.pushReverseColor()\n\n\t\t}\n\t}\n\t_, _, _, _, _ = buffer, _buffer, text, begin, end\n}\n\nfunc Pretty(pretty bool) func(*Parser) error {\n\treturn func(p *Parser) error {\n\t\tp.Pretty = pretty\n\t\treturn nil\n\t}\n}\n\nfunc Size(size int) func(*Parser) error {\n\treturn func(p *Parser) error {\n\t\tp.tokens32 = tokens32{tree: make([]token32, 0, size)}\n\t\treturn nil\n\t}\n}\nfunc (p *Parser) Init(options ...func(*Parser) error) error {\n\tvar (\n\t\tmax                  token32\n\t\tposition, tokenIndex uint32\n\t\tbuffer               []rune\n\t)\n\tfor _, option := range options {\n\t\terr := option(p)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tp.reset = func() {\n\t\tmax = token32{}\n\t\tposition, tokenIndex = 0, 0\n\n\t\tp.buffer = []rune(p.Buffer)\n\t\tif len(p.buffer) == 0 || p.buffer[len(p.buffer)-1] != endSymbol {\n\t\t\tp.buffer = append(p.buffer, endSymbol)\n\t\t}\n\t\tbuffer = p.buffer\n\t}\n\tp.reset()\n\n\t_rules := p.rules\n\ttree := p.tokens32\n\tp.parse = func(rule ...int) error {\n\t\tr := 1\n\t\tif len(rule) > 0 {\n\t\t\tr = rule[0]\n\t\t}\n\t\tmatches := p.rules[r]()\n\t\tp.tokens32 = tree\n\t\tif matches {\n\t\t\tp.Trim(tokenIndex)\n\t\t\treturn nil\n\t\t}\n\t\treturn &parseError{p, max}\n\t}\n\n\tadd := func(rule pegRule, begin uint32) {\n\t\ttree.Add(rule, begin, position, tokenIndex)\n\t\ttokenIndex++\n\t\tif begin != position && position > max.end {\n\t\t\tmax = token32{rule, begin, position}\n\t\t}\n\t}\n\n\tmatchDot := func() bool {\n\t\tif buffer[position] != endSymbol {\n\t\t\tposition++\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\t/*matchChar := func(c byte) bool {\n\t\tif buffer[position] == c {\n\t\t\tposition++\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}*/\n\n\t/*matchRange := func(lower byte, upper byte) bool {\n\t\tif c := buffer[position]; c >= lower && c <= upper {\n\t\t\tposition++\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}*/\n\n\t_rules = [...]func() bool{\n\t\tnil,\n\t\t/* 0 root <- <(colors / ignore / text)*> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tposition1 := position\n\t\t\tl2:\n\t\t\t\t{\n\t\t\t\t\tposition3, tokenIndex3 := position, tokenIndex\n\t\t\t\t\t{\n\t\t\t\t\t\tposition4, tokenIndex4 := position, tokenIndex\n\t\t\t\t\t\tif !_rules[rulecolors]() {\n\t\t\t\t\t\t\tgoto l5\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgoto l4\n\t\t\t\t\tl5:\n\t\t\t\t\t\tposition, tokenIndex = position4, tokenIndex4\n\t\t\t\t\t\tif !_rules[ruleignore]() {\n\t\t\t\t\t\t\tgoto l6\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgoto l4\n\t\t\t\t\tl6:\n\t\t\t\t\t\tposition, tokenIndex = position4, tokenIndex4\n\t\t\t\t\t\tif !_rules[ruletext]() {\n\t\t\t\t\t\t\tgoto l3\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tl4:\n\t\t\t\t\tgoto l2\n\t\t\t\tl3:\n\t\t\t\t\tposition, tokenIndex = position3, tokenIndex3\n\t\t\t\t}\n\t\t\t\tadd(ruleroot, position1)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 1 ignore <- <((prefix number? non_color_suffix) / escape_sequence)> */\n\t\tfunc() bool {\n\t\t\tposition7, tokenIndex7 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition8 := position\n\t\t\t\t{\n\t\t\t\t\tposition9, tokenIndex9 := position, tokenIndex\n\t\t\t\t\tif !_rules[ruleprefix]() {\n\t\t\t\t\t\tgoto l10\n\t\t\t\t\t}\n\t\t\t\t\t{\n\t\t\t\t\t\tposition11, tokenIndex11 := position, tokenIndex\n\t\t\t\t\t\tif !_rules[rulenumber]() {\n\t\t\t\t\t\t\tgoto l11\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgoto l12\n\t\t\t\t\tl11:\n\t\t\t\t\t\tposition, tokenIndex = position11, tokenIndex11\n\t\t\t\t\t}\n\t\t\t\tl12:\n\t\t\t\t\tif !_rules[rulenon_color_suffix]() {\n\t\t\t\t\t\tgoto l10\n\t\t\t\t\t}\n\t\t\t\t\tgoto l9\n\t\t\t\tl10:\n\t\t\t\t\tposition, tokenIndex = position9, tokenIndex9\n\t\t\t\t\tif !_rules[ruleescape_sequence]() {\n\t\t\t\t\t\tgoto l7\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tl9:\n\t\t\t\tadd(ruleignore, position8)\n\t\t\t}\n\t\t\treturn true\n\t\tl7:\n\t\t\tposition, tokenIndex = position7, tokenIndex7\n\t\t\treturn false\n\t\t},\n\t\t/* 2 colors <- <((prefix color_suffix Action0) / (prefix color (delimiter color)* color_suffix))> */\n\t\tfunc() bool {\n\t\t\tposition13, tokenIndex13 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition14 := position\n\t\t\t\t{\n\t\t\t\t\tposition15, tokenIndex15 := position, tokenIndex\n\t\t\t\t\tif !_rules[ruleprefix]() {\n\t\t\t\t\t\tgoto l16\n\t\t\t\t\t}\n\t\t\t\t\tif !_rules[rulecolor_suffix]() {\n\t\t\t\t\t\tgoto l16\n\t\t\t\t\t}\n\t\t\t\t\tif !_rules[ruleAction0]() {\n\t\t\t\t\t\tgoto l16\n\t\t\t\t\t}\n\t\t\t\t\tgoto l15\n\t\t\t\tl16:\n\t\t\t\t\tposition, tokenIndex = position15, tokenIndex15\n\t\t\t\t\tif !_rules[ruleprefix]() {\n\t\t\t\t\t\tgoto l13\n\t\t\t\t\t}\n\t\t\t\t\tif !_rules[rulecolor]() {\n\t\t\t\t\t\tgoto l13\n\t\t\t\t\t}\n\t\t\t\tl17:\n\t\t\t\t\t{\n\t\t\t\t\t\tposition18, tokenIndex18 := position, tokenIndex\n\t\t\t\t\t\tif !_rules[ruledelimiter]() {\n\t\t\t\t\t\t\tgoto l18\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif !_rules[rulecolor]() {\n\t\t\t\t\t\t\tgoto l18\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgoto l17\n\t\t\t\t\tl18:\n\t\t\t\t\t\tposition, tokenIndex = position18, tokenIndex18\n\t\t\t\t\t}\n\t\t\t\t\tif !_rules[rulecolor_suffix]() {\n\t\t\t\t\t\tgoto l13\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tl15:\n\t\t\t\tadd(rulecolors, position14)\n\t\t\t}\n\t\t\treturn true\n\t\tl13:\n\t\t\tposition, tokenIndex = position13, tokenIndex13\n\t\t\treturn false\n\t\t},\n\t\t/* 3 text <- <(<(!'\\x1b' .)+> Action1)> */\n\t\tfunc() bool {\n\t\t\tposition19, tokenIndex19 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition20 := position\n\t\t\t\t{\n\t\t\t\t\tposition21 := position\n\t\t\t\t\t{\n\t\t\t\t\t\tposition24, tokenIndex24 := position, tokenIndex\n\t\t\t\t\t\tif buffer[position] != rune('\\x1b') {\n\t\t\t\t\t\t\tgoto l24\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tgoto l19\n\t\t\t\t\tl24:\n\t\t\t\t\t\tposition, tokenIndex = position24, tokenIndex24\n\t\t\t\t\t}\n\t\t\t\t\tif !matchDot() {\n\t\t\t\t\t\tgoto l19\n\t\t\t\t\t}\n\t\t\t\tl22:\n\t\t\t\t\t{\n\t\t\t\t\t\tposition23, tokenIndex23 := position, tokenIndex\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tposition25, tokenIndex25 := position, tokenIndex\n\t\t\t\t\t\t\tif buffer[position] != rune('\\x1b') {\n\t\t\t\t\t\t\t\tgoto l25\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tgoto l23\n\t\t\t\t\t\tl25:\n\t\t\t\t\t\t\tposition, tokenIndex = position25, tokenIndex25\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif !matchDot() {\n\t\t\t\t\t\t\tgoto l23\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgoto l22\n\t\t\t\t\tl23:\n\t\t\t\t\t\tposition, tokenIndex = position23, tokenIndex23\n\t\t\t\t\t}\n\t\t\t\t\tadd(rulePegText, position21)\n\t\t\t\t}\n\t\t\t\tif !_rules[ruleAction1]() {\n\t\t\t\t\tgoto l19\n\t\t\t\t}\n\t\t\t\tadd(ruletext, position20)\n\t\t\t}\n\t\t\treturn true\n\t\tl19:\n\t\t\tposition, tokenIndex = position19, tokenIndex19\n\t\t\treturn false\n\t\t},\n\t\t/* 4 color <- <(standard_color / extended_color / text_attributes)> */\n\t\tfunc() bool {\n\t\t\tposition26, tokenIndex26 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition27 := position\n\t\t\t\t{\n\t\t\t\t\tposition28, tokenIndex28 := position, tokenIndex\n\t\t\t\t\tif !_rules[rulestandard_color]() {\n\t\t\t\t\t\tgoto l29\n\t\t\t\t\t}\n\t\t\t\t\tgoto l28\n\t\t\t\tl29:\n\t\t\t\t\tposition, tokenIndex = position28, tokenIndex28\n\t\t\t\t\tif !_rules[ruleextended_color]() {\n\t\t\t\t\t\tgoto l30\n\t\t\t\t\t}\n\t\t\t\t\tgoto l28\n\t\t\t\tl30:\n\t\t\t\t\tposition, tokenIndex = position28, tokenIndex28\n\t\t\t\t\tif !_rules[ruletext_attributes]() {\n\t\t\t\t\t\tgoto l26\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tl28:\n\t\t\t\tadd(rulecolor, position27)\n\t\t\t}\n\t\t\treturn true\n\t\tl26:\n\t\t\tposition, tokenIndex = position26, tokenIndex26\n\t\t\treturn false\n\t\t},\n\t\t/* 5 standard_color <- <((zero <(('3' / '4' / '9' / ('1' '0')) [0-7])> Action2) / (zero <(('3' / '9') '9')> Action3) / (zero <(('4' / ('1' '0')) '9')> Action4))> */\n\t\tfunc() bool {\n\t\t\tposition31, tokenIndex31 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition32 := position\n\t\t\t\t{\n\t\t\t\t\tposition33, tokenIndex33 := position, tokenIndex\n\t\t\t\t\tif !_rules[rulezero]() {\n\t\t\t\t\t\tgoto l34\n\t\t\t\t\t}\n\t\t\t\t\t{\n\t\t\t\t\t\tposition35 := position\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tposition36, tokenIndex36 := position, tokenIndex\n\t\t\t\t\t\t\tif buffer[position] != rune('3') {\n\t\t\t\t\t\t\t\tgoto l37\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tgoto l36\n\t\t\t\t\t\tl37:\n\t\t\t\t\t\t\tposition, tokenIndex = position36, tokenIndex36\n\t\t\t\t\t\t\tif buffer[position] != rune('4') {\n\t\t\t\t\t\t\t\tgoto l38\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tgoto l36\n\t\t\t\t\t\tl38:\n\t\t\t\t\t\t\tposition, tokenIndex = position36, tokenIndex36\n\t\t\t\t\t\t\tif buffer[position] != rune('9') {\n\t\t\t\t\t\t\t\tgoto l39\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tgoto l36\n\t\t\t\t\t\tl39:\n\t\t\t\t\t\t\tposition, tokenIndex = position36, tokenIndex36\n\t\t\t\t\t\t\tif buffer[position] != rune('1') {\n\t\t\t\t\t\t\t\tgoto l34\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tif buffer[position] != rune('0') {\n\t\t\t\t\t\t\t\tgoto l34\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t}\n\t\t\t\t\tl36:\n\t\t\t\t\t\tif c := buffer[position]; c < rune('0') || c > rune('7') {\n\t\t\t\t\t\t\tgoto l34\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tadd(rulePegText, position35)\n\t\t\t\t\t}\n\t\t\t\t\tif !_rules[ruleAction2]() {\n\t\t\t\t\t\tgoto l34\n\t\t\t\t\t}\n\t\t\t\t\tgoto l33\n\t\t\t\tl34:\n\t\t\t\t\tposition, tokenIndex = position33, tokenIndex33\n\t\t\t\t\tif !_rules[rulezero]() {\n\t\t\t\t\t\tgoto l40\n\t\t\t\t\t}\n\t\t\t\t\t{\n\t\t\t\t\t\tposition41 := position\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tposition42, tokenIndex42 := position, tokenIndex\n\t\t\t\t\t\t\tif buffer[position] != rune('3') {\n\t\t\t\t\t\t\t\tgoto l43\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tgoto l42\n\t\t\t\t\t\tl43:\n\t\t\t\t\t\t\tposition, tokenIndex = position42, tokenIndex42\n\t\t\t\t\t\t\tif buffer[position] != rune('9') {\n\t\t\t\t\t\t\t\tgoto l40\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t}\n\t\t\t\t\tl42:\n\t\t\t\t\t\tif buffer[position] != rune('9') {\n\t\t\t\t\t\t\tgoto l40\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tadd(rulePegText, position41)\n\t\t\t\t\t}\n\t\t\t\t\tif !_rules[ruleAction3]() {\n\t\t\t\t\t\tgoto l40\n\t\t\t\t\t}\n\t\t\t\t\tgoto l33\n\t\t\t\tl40:\n\t\t\t\t\tposition, tokenIndex = position33, tokenIndex33\n\t\t\t\t\tif !_rules[rulezero]() {\n\t\t\t\t\t\tgoto l31\n\t\t\t\t\t}\n\t\t\t\t\t{\n\t\t\t\t\t\tposition44 := position\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tposition45, tokenIndex45 := position, tokenIndex\n\t\t\t\t\t\t\tif buffer[position] != rune('4') {\n\t\t\t\t\t\t\t\tgoto l46\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tgoto l45\n\t\t\t\t\t\tl46:\n\t\t\t\t\t\t\tposition, tokenIndex = position45, tokenIndex45\n\t\t\t\t\t\t\tif buffer[position] != rune('1') {\n\t\t\t\t\t\t\t\tgoto l31\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tif buffer[position] != rune('0') {\n\t\t\t\t\t\t\t\tgoto l31\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t}\n\t\t\t\t\tl45:\n\t\t\t\t\t\tif buffer[position] != rune('9') {\n\t\t\t\t\t\t\tgoto l31\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tadd(rulePegText, position44)\n\t\t\t\t\t}\n\t\t\t\t\tif !_rules[ruleAction4]() {\n\t\t\t\t\t\tgoto l31\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tl33:\n\t\t\t\tadd(rulestandard_color, position32)\n\t\t\t}\n\t\t\treturn true\n\t\tl31:\n\t\t\tposition, tokenIndex = position31, tokenIndex31\n\t\t\treturn false\n\t\t},\n\t\t/* 6 extended_color <- <(extended_color_256 / extended_color_rgb)> */\n\t\tfunc() bool {\n\t\t\tposition47, tokenIndex47 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition48 := position\n\t\t\t\t{\n\t\t\t\t\tposition49, tokenIndex49 := position, tokenIndex\n\t\t\t\t\tif !_rules[ruleextended_color_256]() {\n\t\t\t\t\t\tgoto l50\n\t\t\t\t\t}\n\t\t\t\t\tgoto l49\n\t\t\t\tl50:\n\t\t\t\t\tposition, tokenIndex = position49, tokenIndex49\n\t\t\t\t\tif !_rules[ruleextended_color_rgb]() {\n\t\t\t\t\t\tgoto l47\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tl49:\n\t\t\t\tadd(ruleextended_color, position48)\n\t\t\t}\n\t\t\treturn true\n\t\tl47:\n\t\t\tposition, tokenIndex = position47, tokenIndex47\n\t\t\treturn false\n\t\t},\n\t\t/* 7 extended_color_256 <- <(extended_color_prefix delimiter zero '5' delimiter <number> Action5)> */\n\t\tfunc() bool {\n\t\t\tposition51, tokenIndex51 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition52 := position\n\t\t\t\tif !_rules[ruleextended_color_prefix]() {\n\t\t\t\t\tgoto l51\n\t\t\t\t}\n\t\t\t\tif !_rules[ruledelimiter]() {\n\t\t\t\t\tgoto l51\n\t\t\t\t}\n\t\t\t\tif !_rules[rulezero]() {\n\t\t\t\t\tgoto l51\n\t\t\t\t}\n\t\t\t\tif buffer[position] != rune('5') {\n\t\t\t\t\tgoto l51\n\t\t\t\t}\n\t\t\t\tposition++\n\t\t\t\tif !_rules[ruledelimiter]() {\n\t\t\t\t\tgoto l51\n\t\t\t\t}\n\t\t\t\t{\n\t\t\t\t\tposition53 := position\n\t\t\t\t\tif !_rules[rulenumber]() {\n\t\t\t\t\t\tgoto l51\n\t\t\t\t\t}\n\t\t\t\t\tadd(rulePegText, position53)\n\t\t\t\t}\n\t\t\t\tif !_rules[ruleAction5]() {\n\t\t\t\t\tgoto l51\n\t\t\t\t}\n\t\t\t\tadd(ruleextended_color_256, position52)\n\t\t\t}\n\t\t\treturn true\n\t\tl51:\n\t\t\tposition, tokenIndex = position51, tokenIndex51\n\t\t\treturn false\n\t\t},\n\t\t/* 8 extended_color_rgb <- <(extended_color_prefix delimiter zero '2' delimiter <number> Action6 delimiter <number> Action7 delimiter <number> Action8)> */\n\t\tfunc() bool {\n\t\t\tposition54, tokenIndex54 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition55 := position\n\t\t\t\tif !_rules[ruleextended_color_prefix]() {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\tif !_rules[ruledelimiter]() {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\tif !_rules[rulezero]() {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\tif buffer[position] != rune('2') {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\tposition++\n\t\t\t\tif !_rules[ruledelimiter]() {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\t{\n\t\t\t\t\tposition56 := position\n\t\t\t\t\tif !_rules[rulenumber]() {\n\t\t\t\t\t\tgoto l54\n\t\t\t\t\t}\n\t\t\t\t\tadd(rulePegText, position56)\n\t\t\t\t}\n\t\t\t\tif !_rules[ruleAction6]() {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\tif !_rules[ruledelimiter]() {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\t{\n\t\t\t\t\tposition57 := position\n\t\t\t\t\tif !_rules[rulenumber]() {\n\t\t\t\t\t\tgoto l54\n\t\t\t\t\t}\n\t\t\t\t\tadd(rulePegText, position57)\n\t\t\t\t}\n\t\t\t\tif !_rules[ruleAction7]() {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\tif !_rules[ruledelimiter]() {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\t{\n\t\t\t\t\tposition58 := position\n\t\t\t\t\tif !_rules[rulenumber]() {\n\t\t\t\t\t\tgoto l54\n\t\t\t\t\t}\n\t\t\t\t\tadd(rulePegText, position58)\n\t\t\t\t}\n\t\t\t\tif !_rules[ruleAction8]() {\n\t\t\t\t\tgoto l54\n\t\t\t\t}\n\t\t\t\tadd(ruleextended_color_rgb, position55)\n\t\t\t}\n\t\t\treturn true\n\t\tl54:\n\t\t\tposition, tokenIndex = position54, tokenIndex54\n\t\t\treturn false\n\t\t},\n\t\t/* 9 extended_color_prefix <- <(zero <(('3' / '4') '8')> Action9)> */\n\t\tfunc() bool {\n\t\t\tposition59, tokenIndex59 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition60 := position\n\t\t\t\tif !_rules[rulezero]() {\n\t\t\t\t\tgoto l59\n\t\t\t\t}\n\t\t\t\t{\n\t\t\t\t\tposition61 := position\n\t\t\t\t\t{\n\t\t\t\t\t\tposition62, tokenIndex62 := position, tokenIndex\n\t\t\t\t\t\tif buffer[position] != rune('3') {\n\t\t\t\t\t\t\tgoto l63\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tgoto l62\n\t\t\t\t\tl63:\n\t\t\t\t\t\tposition, tokenIndex = position62, tokenIndex62\n\t\t\t\t\t\tif buffer[position] != rune('4') {\n\t\t\t\t\t\t\tgoto l59\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t}\n\t\t\t\tl62:\n\t\t\t\t\tif buffer[position] != rune('8') {\n\t\t\t\t\t\tgoto l59\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tadd(rulePegText, position61)\n\t\t\t\t}\n\t\t\t\tif !_rules[ruleAction9]() {\n\t\t\t\t\tgoto l59\n\t\t\t\t}\n\t\t\t\tadd(ruleextended_color_prefix, position60)\n\t\t\t}\n\t\t\treturn true\n\t\tl59:\n\t\t\tposition, tokenIndex = position59, tokenIndex59\n\t\t\treturn false\n\t\t},\n\t\t/* 10 text_attributes <- <(('0' Action10) / ('7' Action11) / ('1' / '4' / '5' / '8'))+> */\n\t\tfunc() bool {\n\t\t\tposition64, tokenIndex64 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition65 := position\n\t\t\t\t{\n\t\t\t\t\tposition68, tokenIndex68 := position, tokenIndex\n\t\t\t\t\tif buffer[position] != rune('0') {\n\t\t\t\t\t\tgoto l69\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tif !_rules[ruleAction10]() {\n\t\t\t\t\t\tgoto l69\n\t\t\t\t\t}\n\t\t\t\t\tgoto l68\n\t\t\t\tl69:\n\t\t\t\t\tposition, tokenIndex = position68, tokenIndex68\n\t\t\t\t\tif buffer[position] != rune('7') {\n\t\t\t\t\t\tgoto l70\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tif !_rules[ruleAction11]() {\n\t\t\t\t\t\tgoto l70\n\t\t\t\t\t}\n\t\t\t\t\tgoto l68\n\t\t\t\tl70:\n\t\t\t\t\tposition, tokenIndex = position68, tokenIndex68\n\t\t\t\t\t{\n\t\t\t\t\t\tposition71, tokenIndex71 := position, tokenIndex\n\t\t\t\t\t\tif buffer[position] != rune('1') {\n\t\t\t\t\t\t\tgoto l72\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tgoto l71\n\t\t\t\t\tl72:\n\t\t\t\t\t\tposition, tokenIndex = position71, tokenIndex71\n\t\t\t\t\t\tif buffer[position] != rune('4') {\n\t\t\t\t\t\t\tgoto l73\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tgoto l71\n\t\t\t\t\tl73:\n\t\t\t\t\t\tposition, tokenIndex = position71, tokenIndex71\n\t\t\t\t\t\tif buffer[position] != rune('5') {\n\t\t\t\t\t\t\tgoto l74\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tgoto l71\n\t\t\t\t\tl74:\n\t\t\t\t\t\tposition, tokenIndex = position71, tokenIndex71\n\t\t\t\t\t\tif buffer[position] != rune('8') {\n\t\t\t\t\t\t\tgoto l64\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t}\n\t\t\t\tl71:\n\t\t\t\t}\n\t\t\tl68:\n\t\t\tl66:\n\t\t\t\t{\n\t\t\t\t\tposition67, tokenIndex67 := position, tokenIndex\n\t\t\t\t\t{\n\t\t\t\t\t\tposition75, tokenIndex75 := position, tokenIndex\n\t\t\t\t\t\tif buffer[position] != rune('0') {\n\t\t\t\t\t\t\tgoto l76\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tif !_rules[ruleAction10]() {\n\t\t\t\t\t\t\tgoto l76\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgoto l75\n\t\t\t\t\tl76:\n\t\t\t\t\t\tposition, tokenIndex = position75, tokenIndex75\n\t\t\t\t\t\tif buffer[position] != rune('7') {\n\t\t\t\t\t\t\tgoto l77\n\t\t\t\t\t\t}\n\t\t\t\t\t\tposition++\n\t\t\t\t\t\tif !_rules[ruleAction11]() {\n\t\t\t\t\t\t\tgoto l77\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgoto l75\n\t\t\t\t\tl77:\n\t\t\t\t\t\tposition, tokenIndex = position75, tokenIndex75\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tposition78, tokenIndex78 := position, tokenIndex\n\t\t\t\t\t\t\tif buffer[position] != rune('1') {\n\t\t\t\t\t\t\t\tgoto l79\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tgoto l78\n\t\t\t\t\t\tl79:\n\t\t\t\t\t\t\tposition, tokenIndex = position78, tokenIndex78\n\t\t\t\t\t\t\tif buffer[position] != rune('4') {\n\t\t\t\t\t\t\t\tgoto l80\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tgoto l78\n\t\t\t\t\t\tl80:\n\t\t\t\t\t\t\tposition, tokenIndex = position78, tokenIndex78\n\t\t\t\t\t\t\tif buffer[position] != rune('5') {\n\t\t\t\t\t\t\t\tgoto l81\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t\tgoto l78\n\t\t\t\t\t\tl81:\n\t\t\t\t\t\t\tposition, tokenIndex = position78, tokenIndex78\n\t\t\t\t\t\t\tif buffer[position] != rune('8') {\n\t\t\t\t\t\t\t\tgoto l67\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tposition++\n\t\t\t\t\t\t}\n\t\t\t\t\tl78:\n\t\t\t\t\t}\n\t\t\t\tl75:\n\t\t\t\t\tgoto l66\n\t\t\t\tl67:\n\t\t\t\t\tposition, tokenIndex = position67, tokenIndex67\n\t\t\t\t}\n\t\t\t\tadd(ruletext_attributes, position65)\n\t\t\t}\n\t\t\treturn true\n\t\tl64:\n\t\t\tposition, tokenIndex = position64, tokenIndex64\n\t\t\treturn false\n\t\t},\n\t\t/* 11 zero <- <'0'*> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tposition83 := position\n\t\t\tl84:\n\t\t\t\t{\n\t\t\t\t\tposition85, tokenIndex85 := position, tokenIndex\n\t\t\t\t\tif buffer[position] != rune('0') {\n\t\t\t\t\t\tgoto l85\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tgoto l84\n\t\t\t\tl85:\n\t\t\t\t\tposition, tokenIndex = position85, tokenIndex85\n\t\t\t\t}\n\t\t\t\tadd(rulezero, position83)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 12 number <- <[0-9]+> */\n\t\tfunc() bool {\n\t\t\tposition86, tokenIndex86 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition87 := position\n\t\t\t\tif c := buffer[position]; c < rune('0') || c > rune('9') {\n\t\t\t\t\tgoto l86\n\t\t\t\t}\n\t\t\t\tposition++\n\t\t\tl88:\n\t\t\t\t{\n\t\t\t\t\tposition89, tokenIndex89 := position, tokenIndex\n\t\t\t\t\tif c := buffer[position]; c < rune('0') || c > rune('9') {\n\t\t\t\t\t\tgoto l89\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tgoto l88\n\t\t\t\tl89:\n\t\t\t\t\tposition, tokenIndex = position89, tokenIndex89\n\t\t\t\t}\n\t\t\t\tadd(rulenumber, position87)\n\t\t\t}\n\t\t\treturn true\n\t\tl86:\n\t\t\tposition, tokenIndex = position86, tokenIndex86\n\t\t\treturn false\n\t\t},\n\t\t/* 13 prefix <- <(escape_sequence '[')> */\n\t\tfunc() bool {\n\t\t\tposition90, tokenIndex90 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition91 := position\n\t\t\t\tif !_rules[ruleescape_sequence]() {\n\t\t\t\t\tgoto l90\n\t\t\t\t}\n\t\t\t\tif buffer[position] != rune('[') {\n\t\t\t\t\tgoto l90\n\t\t\t\t}\n\t\t\t\tposition++\n\t\t\t\tadd(ruleprefix, position91)\n\t\t\t}\n\t\t\treturn true\n\t\tl90:\n\t\t\tposition, tokenIndex = position90, tokenIndex90\n\t\t\treturn false\n\t\t},\n\t\t/* 14 escape_sequence <- <'\\x1b'> */\n\t\tfunc() bool {\n\t\t\tposition92, tokenIndex92 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition93 := position\n\t\t\t\tif buffer[position] != rune('\\x1b') {\n\t\t\t\t\tgoto l92\n\t\t\t\t}\n\t\t\t\tposition++\n\t\t\t\tadd(ruleescape_sequence, position93)\n\t\t\t}\n\t\t\treturn true\n\t\tl92:\n\t\t\tposition, tokenIndex = position92, tokenIndex92\n\t\t\treturn false\n\t\t},\n\t\t/* 15 color_suffix <- <'m'> */\n\t\tfunc() bool {\n\t\t\tposition94, tokenIndex94 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition95 := position\n\t\t\t\tif buffer[position] != rune('m') {\n\t\t\t\t\tgoto l94\n\t\t\t\t}\n\t\t\t\tposition++\n\t\t\t\tadd(rulecolor_suffix, position95)\n\t\t\t}\n\t\t\treturn true\n\t\tl94:\n\t\t\tposition, tokenIndex = position94, tokenIndex94\n\t\t\treturn false\n\t\t},\n\t\t/* 16 non_color_suffix <- <([A-H] / 'f' / 'S' / 'T' / 'J' / 'K')> */\n\t\tfunc() bool {\n\t\t\tposition96, tokenIndex96 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition97 := position\n\t\t\t\t{\n\t\t\t\t\tposition98, tokenIndex98 := position, tokenIndex\n\t\t\t\t\tif c := buffer[position]; c < rune('A') || c > rune('H') {\n\t\t\t\t\t\tgoto l99\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tgoto l98\n\t\t\t\tl99:\n\t\t\t\t\tposition, tokenIndex = position98, tokenIndex98\n\t\t\t\t\tif buffer[position] != rune('f') {\n\t\t\t\t\t\tgoto l100\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tgoto l98\n\t\t\t\tl100:\n\t\t\t\t\tposition, tokenIndex = position98, tokenIndex98\n\t\t\t\t\tif buffer[position] != rune('S') {\n\t\t\t\t\t\tgoto l101\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tgoto l98\n\t\t\t\tl101:\n\t\t\t\t\tposition, tokenIndex = position98, tokenIndex98\n\t\t\t\t\tif buffer[position] != rune('T') {\n\t\t\t\t\t\tgoto l102\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tgoto l98\n\t\t\t\tl102:\n\t\t\t\t\tposition, tokenIndex = position98, tokenIndex98\n\t\t\t\t\tif buffer[position] != rune('J') {\n\t\t\t\t\t\tgoto l103\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t\tgoto l98\n\t\t\t\tl103:\n\t\t\t\t\tposition, tokenIndex = position98, tokenIndex98\n\t\t\t\t\tif buffer[position] != rune('K') {\n\t\t\t\t\t\tgoto l96\n\t\t\t\t\t}\n\t\t\t\t\tposition++\n\t\t\t\t}\n\t\t\tl98:\n\t\t\t\tadd(rulenon_color_suffix, position97)\n\t\t\t}\n\t\t\treturn true\n\t\tl96:\n\t\t\tposition, tokenIndex = position96, tokenIndex96\n\t\t\treturn false\n\t\t},\n\t\t/* 17 delimiter <- <';'> */\n\t\tfunc() bool {\n\t\t\tposition104, tokenIndex104 := position, tokenIndex\n\t\t\t{\n\t\t\t\tposition105 := position\n\t\t\t\tif buffer[position] != rune(';') {\n\t\t\t\t\tgoto l104\n\t\t\t\t}\n\t\t\t\tposition++\n\t\t\t\tadd(ruledelimiter, position105)\n\t\t\t}\n\t\t\treturn true\n\t\tl104:\n\t\t\tposition, tokenIndex = position104, tokenIndex104\n\t\t\treturn false\n\t\t},\n\t\t/* 19 Action0 <- <{ p.pushResetColor() }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction0, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\tnil,\n\t\t/* 21 Action1 <- <{ p.pushText(text) }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction1, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 22 Action2 <- <{ p.pushStandardColorWithCategory(text) }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction2, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 23 Action3 <- <{ p.pushResetForegroundColor() }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction3, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 24 Action4 <- <{ p.pushResetBackgroundColor() }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction4, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 25 Action5 <- <{ p.setExtendedColor256(text) }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction5, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 26 Action6 <- <{ p.setExtendedColorR(text) }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction6, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 27 Action7 <- <{ p.setExtendedColorG(text) }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction7, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 28 Action8 <- <{ p.setExtendedColorB(text) }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction8, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 29 Action9 <- <{ p.pushExtendedColor(text) }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction9, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 30 Action10 <- <{ p.pushResetColor() }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction10, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\t/* 31 Action11 <- <{ p.pushReverseColor() }> */\n\t\tfunc() bool {\n\t\t\t{\n\t\t\t\tadd(ruleAction11, position)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t}\n\tp.rules = _rules\n\treturn nil\n}\n"
  },
  {
    "path": "parser/parser.go",
    "content": "package parser\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/jiro4989/textimg/v3/color\"\n\t\"github.com/jiro4989/textimg/v3/token\"\n)\n\ntype ParserFunc struct {\n\t// pegが生成するTokensと名前が衝突するので別名にする\n\tTk token.Tokens\n}\n\nfunc Parse(s string) (token.Tokens, error) {\n\tp := &Parser{Buffer: s}\n\tif err := p.Init(); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := p.Parse(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tp.Execute()\n\treturn p.Tk, nil\n}\n\nfunc (p *ParserFunc) pushResetColor() {\n\tp.Tk = append(p.Tk, token.NewResetColor())\n}\n\nfunc (p *ParserFunc) pushResetForegroundColor() {\n\tp.Tk = append(p.Tk, token.NewResetForegroundColor())\n}\n\nfunc (p *ParserFunc) pushResetBackgroundColor() {\n\tp.Tk = append(p.Tk, token.NewResetBackgroundColor())\n}\n\nfunc (p *ParserFunc) pushReverseColor() {\n\tp.Tk = append(p.Tk, token.NewReverseColor())\n}\n\nfunc (p *ParserFunc) pushText(text string) {\n\tp.Tk = append(p.Tk, token.NewText(text))\n}\n\nfunc (p *ParserFunc) pushStandardColorWithCategory(text string) {\n\tp.Tk = append(p.Tk, token.NewStandardColorWithCategory(text))\n}\n\nfunc (p *ParserFunc) pushExtendedColor(text string) {\n\tp.Tk = append(p.Tk, token.NewExtendedColor(text))\n}\n\nfunc (p *ParserFunc) setExtendedColor256(text string) {\n\tn, _ := strconv.ParseUint(text, 10, 8)\n\tp.Tk[len(p.Tk)-1].Color = color.Map256[int(n)]\n}\n\nfunc (p *ParserFunc) setExtendedColorR(text string) {\n\tn, _ := strconv.ParseUint(text, 10, 8)\n\tp.Tk[len(p.Tk)-1].Color.R = uint8(n)\n}\n\nfunc (p *ParserFunc) setExtendedColorG(text string) {\n\tn, _ := strconv.ParseUint(text, 10, 8)\n\tp.Tk[len(p.Tk)-1].Color.G = uint8(n)\n}\n\nfunc (p *ParserFunc) setExtendedColorB(text string) {\n\tn, _ := strconv.ParseUint(text, 10, 8)\n\tp.Tk[len(p.Tk)-1].Color.B = uint8(n)\n}\n"
  },
  {
    "path": "parser/parser_test.go",
    "content": "package parser\n\nimport (\n\t\"testing\"\n\n\t\"github.com/jiro4989/textimg/v3/color\"\n\t\"github.com/jiro4989/textimg/v3/token\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttests := []struct {\n\t\tdesc    string\n\t\ts       string\n\t\twant    token.Tokens\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tdesc: \"正常系: 黒\",\n\t\t\ts:    \"\\x1b[30m\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBABlack,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 90系と100系\",\n\t\t\ts:    \"\\x1b[90;100m\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBADarkGray,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeBackground,\n\t\t\t\t\tColor:     color.RGBADarkGray,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 黒赤緑\",\n\t\t\ts:    \"\\x1b[30m\\x1b[31m\\x1b[32m\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBABlack,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBARed,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBAGreen,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 赤とテキストとリセット\",\n\t\t\ts:    \"\\x1b[31m\\n hello\\tworld \\n\\x1b[0m\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBARed,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"\\n hello\\tworld \\n\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeReset,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 39はResetForeground, 49はResetBackground\",\n\t\t\ts:    \"\\x1b[39mReset\\x1b[49mReset\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeResetForeground,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"Reset\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeResetBackground,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"Reset\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 前景色と背景色の同時指定 + リセット省略系\",\n\t\t\ts:    \"\\x1b[32;43mhello world\\x1b[m\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBAGreen,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeBackground,\n\t\t\t\t\tColor:     color.RGBAYellow,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"hello world\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeReset,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 0埋めありの指定\",\n\t\t\ts:    \"\\x1b[032;00043mhello world\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBAGreen,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeBackground,\n\t\t\t\t\tColor:     color.RGBAYellow,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"hello world\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 拡張系 256色\",\n\t\t\ts:    \"\\x1b[38;5;1m\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.Map256[1],\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 拡張系 RGB指定\",\n\t\t\ts:    \"\\x1b[48;2;1;2;3m\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeBackground,\n\t\t\t\t\tColor: color.RGBA{\n\t\t\t\t\t\tR: 1,\n\t\t\t\t\t\tG: 2,\n\t\t\t\t\t\tB: 3,\n\t\t\t\t\t\tA: 255,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 拡張系の混在\",\n\t\t\ts:    \"\\x1b[38;5;2;48;2;1;2;3mこんばんは\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.Map256[2],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeBackground,\n\t\t\t\t\tColor: color.RGBA{\n\t\t\t\t\t\tR: 1,\n\t\t\t\t\t\tG: 2,\n\t\t\t\t\t\tB: 3,\n\t\t\t\t\t\tA: 255,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"こんばんは\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 拡張系の混在 + 0埋め\",\n\t\t\ts:    \"\\x1b[038;005;002;048;002;001;002;003mx1bこんば\\nんはx1b\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.Map256[2],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeBackground,\n\t\t\t\t\tColor: color.RGBA{\n\t\t\t\t\t\tR: 1,\n\t\t\t\t\t\tG: 2,\n\t\t\t\t\t\tB: 3,\n\t\t\t\t\t\tA: 255,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"x1bこんば\\nんはx1b\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 関係ないエスケープシーケンス系無視される\",\n\t\t\ts:    \"\\x1b[1A\\x1b[A\\x1b[K寿司\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"寿司\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc:    \"正常系: 空文字列の場合は空\",\n\t\t\ts:       \"\",\n\t\t\twant:    nil,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 不完全なANSIエスケープシーケンスは無視\",\n\t\t\ts:    \"\\x1b[31helloworld\\x1b[30m\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"[31helloworld\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBABlack,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 範囲外のエスケープシーケンスの場合は無視\",\n\t\t\ts:    \"\\x1b[310mhelloworld\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"[310mhelloworld\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: text_attributes\",\n\t\t\ts:    \"\\x1b[01;31mRED\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeReset,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBARed,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: token.KindText,\n\t\t\t\t\tText: \"RED\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: 拡張系 256色で数値がuint8を超えた場合はMap256の最後の値が設定される\",\n\t\t\ts:    \"\\x1b[38;5;256m\",\n\t\t\twant: token.Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind:      token.KindColor,\n\t\t\t\t\tColorType: token.ColorTypeForeground,\n\t\t\t\t\tColor:     color.Map256[255],\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\t\t\tgot, err := Parse(tt.s)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(err)\n\t\t\t\tassert.Nil(got)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "root.go",
    "content": "package main\n\nimport (\n\t\"image/color\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/jiro4989/textimg/v3/config\"\n\t\"github.com/jiro4989/textimg/v3/image\"\n\t\"github.com/jiro4989/textimg/v3/internal/global\"\n\t\"github.com/jiro4989/textimg/v3/parser\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\tconf    config.Config\n\tenvvars config.EnvVars\n)\n\nfunc init() {\n\tenvvars = config.NewEnvVars()\n\tcobra.OnInitialize()\n\n\tRootCommand.Flags().SortFlags = false\n\tRootCommand.Flags().StringVarP(&conf.Foreground, \"foreground\", \"g\", \"white\", `foreground text color.\navailable color types are [black|red|green|yellow|blue|magenta|cyan|white]\nor (R,G,B,A(0~255))`)\n\tRootCommand.Flags().StringVarP(&conf.Background, \"background\", \"b\", \"black\", `background text color.\ncolor types are same as \"foreground\" option`)\n\n\tvar font string\n\tenvFontFile := envvars.FontFile\n\tif envFontFile != \"\" {\n\t\tfont = envFontFile\n\t}\n\tRootCommand.Flags().StringVarP(&conf.FontFile, \"fontfile\", \"f\", font, `font file path.\nYou can change this default value with environment variables TEXTIMG_FONT_FILE`)\n\tRootCommand.Flags().IntVarP(&conf.FontIndex, \"fontindex\", \"x\", 0, \"\")\n\tconf.SetFontFileAndFontIndex(runtime.GOOS)\n\n\tenvEmojiFontFile := envvars.EmojiFontFile\n\tRootCommand.Flags().StringVarP(&conf.EmojiFontFile, \"emoji-fontfile\", \"e\", envEmojiFontFile, \"emoji font file\")\n\tRootCommand.Flags().IntVarP(&conf.EmojiFontIndex, \"emoji-fontindex\", \"X\", 0, \"\")\n\n\tRootCommand.Flags().BoolVarP(&conf.UseEmojiFont, \"use-emoji-font\", \"i\", false, \"use emoji font\")\n\tRootCommand.Flags().BoolVarP(&conf.UseShellgeiEmojiFontfile, \"shellgei-emoji-fontfile\", \"z\", false, `emoji font file for shellgei-bot (path: \"`+config.ShellgeiEmojiFontPath+`\")`)\n\n\tRootCommand.Flags().IntVarP(&conf.FontSize, \"fontsize\", \"F\", 20, \"font size\")\n\tRootCommand.Flags().StringVarP(&conf.Outpath, \"out\", \"o\", \"\", `output image file path.\navailable image formats are [png | jpg | gif]`)\n\tRootCommand.Flags().BoolVarP(&conf.AddTimeStamp, \"timestamp\", \"t\", false, `add time stamp to output image file path.`)\n\tRootCommand.Flags().BoolVarP(&conf.SaveNumberedFile, \"numbered\", \"n\", false, `add number-suffix to filename when the output file was existed.\nex: t_2.png`)\n\tRootCommand.Flags().BoolVarP(&conf.UseShellgeiImagedir, \"shellgei-imagedir\", \"s\", false, `image directory path (path: \"$HOME/Pictures/t.png\" or \"$TEXTIMG_OUTPUT_DIR/t.png\")`)\n\n\tRootCommand.Flags().BoolVarP(&conf.UseAnimation, \"animation\", \"a\", false, \"generate animation gif\")\n\tRootCommand.Flags().IntVarP(&conf.Delay, \"delay\", \"d\", 20, \"animation delay time\")\n\tRootCommand.Flags().IntVarP(&conf.LineCount, \"line-count\", \"l\", 1, \"animation input line count\")\n\tRootCommand.Flags().BoolVarP(&conf.UseSlideAnimation, \"slide\", \"S\", false, \"use slide animation\")\n\tRootCommand.Flags().IntVarP(&conf.SlideWidth, \"slide-width\", \"W\", 1, \"sliding animation width\")\n\tRootCommand.Flags().BoolVarP(&conf.SlideForever, \"forever\", \"E\", false, \"sliding forever\")\n\tRootCommand.Flags().BoolVarP(&conf.PrintEnvironments, \"environments\", \"\", false, \"print environment variables\")\n\tRootCommand.Flags().BoolVarP(&conf.ToSlackIcon, \"slack\", \"\", false, \"resize to slack icon size (128x128 px)\")\n\tRootCommand.Flags().IntVarP(&conf.ResizeWidth, \"resize-width\", \"\", 0, \"resize width\")\n\tRootCommand.Flags().IntVarP(&conf.ResizeHeight, \"resize-height\", \"\", 0, \"resize height\")\n}\n\nvar RootCommand = &cobra.Command{\n\tUse:     global.AppName,\n\tShort:   global.AppName + \" is command to convert from colored text (ANSI or 256) to image.\",\n\tExample: global.AppName + ` $'\\x1b[31mRED\\x1b[0m' -o out.png`,\n\tVersion: global.Version,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\treturn RunRootCommand(conf, args, envvars)\n\t},\n}\n\nfunc RunRootCommand(c config.Config, args []string, envs config.EnvVars) error {\n\tif c.PrintEnvironments {\n\t\tconfig.PrintEnvs()\n\t\treturn nil\n\t}\n\n\tif err := c.Adjust(args, envs); err != nil {\n\t\treturn err\n\t}\n\tdefer c.Writer.Close()\n\n\ttokens, err := parser.Parse(strings.Join(c.Texts, \"\\n\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbw := tokens.MaxStringWidth()\n\tbh := len(tokens.StringLines())\n\n\tif !c.ToSlackIcon {\n\t\t// TODO: コピペコードになってるので共通化する\n\t\tvar (\n\t\t\tf          = c.FontSize\n\t\t\tcharWidth  = f / 2\n\t\t\tcharHeight = int(float64(f) * 1.1)\n\t\t\tw          = bw * charWidth\n\t\t\th          = bh * charHeight\n\t\t)\n\t\tc.ResizeWidth, c.ResizeHeight = complementWidthHeight(w, h, c.ResizeWidth, c.ResizeHeight)\n\t}\n\n\tparam := &image.ImageParam{\n\t\tBaseWidth:          bw,\n\t\tBaseHeight:         bh,\n\t\tForegroundColor:    color.RGBA(c.ForegroundColor),\n\t\tBackgroundColor:    color.RGBA(c.BackgroundColor),\n\t\tFontFace:           c.FontFace,\n\t\tEmojiFontFace:      c.EmojiFontFace,\n\t\tEmojiDir:           c.EmojiDir,\n\t\tFontSize:           c.FontSize,\n\t\tDelay:              c.Delay,\n\t\tUseAnimation:       c.UseAnimation,\n\t\tAnimationLineCount: c.LineCount,\n\t\tResizeWidth:        c.ResizeWidth,\n\t\tResizeHeight:       c.ResizeHeight,\n\t\tUseEmoji:           c.UseEmojiFont,\n\t}\n\timg := image.NewImage(param)\n\tif err := img.Draw(tokens); err != nil {\n\t\treturn err\n\t}\n\tif err := img.Encode(c.Writer, c.FileExtension); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// complementWidthHeight は width, height の片方が 0 の時、サイズを調整する。\nfunc complementWidthHeight(x, y, w, h int) (int, int) {\n\tif w == 0 {\n\t\thh := y\n\t\td := float64(h) / float64(hh)\n\t\tw = int(float64(x) * d)\n\t\treturn w, h\n\t}\n\tif h == 0 {\n\t\tww := x\n\t\td := float64(w) / float64(ww)\n\t\th = int(float64(y) * d)\n\t\treturn w, h\n\t}\n\treturn w, h\n}\n"
  },
  {
    "path": "root_common_test.go",
    "content": "package main\n\nimport \"github.com/jiro4989/textimg/v3/config\"\n\nfunc newDefaultConfig() config.Config {\n\treturn config.Config{\n\t\tForeground:               \"white\",\n\t\tBackground:               \"black\",\n\t\tOutpath:                  \"\",\n\t\tAddTimeStamp:             false,\n\t\tSaveNumberedFile:         false,\n\t\tFontFile:                 \"\",\n\t\tFontIndex:                0,\n\t\tEmojiFontFile:            \"\",\n\t\tEmojiFontIndex:           0,\n\t\tUseEmojiFont:             false,\n\t\tFontSize:                 20,\n\t\tUseAnimation:             false,\n\t\tDelay:                    20,\n\t\tLineCount:                1,\n\t\tUseSlideAnimation:        false,\n\t\tSlideWidth:               1,\n\t\tSlideForever:             false,\n\t\tToSlackIcon:              false,\n\t\tPrintEnvironments:        false,\n\t\tUseShellgeiImagedir:      false,\n\t\tUseShellgeiEmojiFontfile: false,\n\t\tResizeWidth:              0,\n\t\tResizeHeight:             0,\n\t\tWriter:                   config.NewMockWriter(false, false),\n\t}\n}\n"
  },
  {
    "path": "root_on_docker_test.go",
    "content": "//go:build docker\n\n//\n// 日本語や絵文字が使えるDocker環境上で実行する想定のテスト。\n// どうしてもDocker上でしかテストできないもののみこのファイルに記述する。\n\npackage main\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/jiro4989/textimg/v3/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRunRootCommandOnDocker(t *testing.T) {\n\tvar (\n\t\toutDockerDir  = \"testdata/out_docker\"\n\t\tfontFile      = \"/tmp/MyricaM.TTC\"\n\t\temojiDir      = \"/usr/local/src/noto-emoji/png/128\"\n\t\temojiFontFile = \"/tmp/Symbola_hint.ttf\"\n\t)\n\n\t// nolint\n\tos.Mkdir(outDockerDir, os.ModePerm)\n\n\ttests := []struct {\n\t\tdesc       string\n\t\tc          config.Config\n\t\targs       []string\n\t\tenvs       config.EnvVars\n\t\twantErr    bool\n\t\texistsFile string\n\t}{\n\t\t{\n\t\t\tdesc: \"正常系: 日本語や絵文字を描画できる\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDockerDir + \"/root_on_docker_test_japanese.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.FontFile = fontFile\n\t\t\t\t// c.EmojiDir = emojiDir\n\t\t\t\tc.EmojiFontFile = emojiFontFile\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"\\x1b[31mあいうえお\\n\\x1b[32;43mあ😃a👍！👀ん👄\"},\n\t\t\tenvs: config.EnvVars{\n\t\t\t\tEmojiDir: emojiDir,\n\t\t\t},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDockerDir + \"/root_on_docker_test_japanese.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 絵文字を連続して描画しても背景色が絵文字を上書きしない\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDockerDir + \"/root_on_docker_test_emoji.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.FontFile = fontFile\n\t\t\t\t// c.EmojiDir = emojiDir\n\t\t\t\tc.EmojiFontFile = emojiFontFile\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"😃👍👀👄\"},\n\t\t\tenvs: config.EnvVars{\n\t\t\t\tEmojiDir: emojiDir,\n\t\t\t},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDockerDir + \"/root_on_docker_test_emoji.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 特殊な絵文字を使う\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDockerDir + \"/root_on_docker_test_shellgei_emoji.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.UseEmojiFont = true\n\t\t\t\tc.FontFile = fontFile\n\t\t\t\t// c.EmojiDir = emojiDir\n\t\t\t\tc.EmojiFontFile = emojiFontFile\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs: []string{\"\\x1b[31mあいうえお\\n\\x1b[32;43mあ😃a👍！👀ん👄\"},\n\t\t\tenvs: config.EnvVars{\n\t\t\t\tEmojiDir: emojiDir,\n\t\t\t},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDockerDir + \"/root_on_docker_test_shellgei_emoji.png\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\n\t\t\terr := RunRootCommand(tt.c, tt.args, tt.envs)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.NoError(err)\n\t\t\tif tt.existsFile != \"\" {\n\t\t\t\t_, err := os.Stat(tt.existsFile)\n\t\t\t\tgot := os.IsNotExist(err)\n\t\t\t\tassert.False(got)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "root_test.go",
    "content": "//go:build !docker\n\npackage main\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/jiro4989/textimg/v3/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRunRootCommand(t *testing.T) {\n\tb, _ := os.ReadFile(inDir + \"/red_grad.txt\")\n\tgrad := string(b)\n\tb, _ = os.ReadFile(inDir + \"/255.txt\")\n\tc255 := string(b)\n\n\ttests := []struct {\n\t\tdesc       string\n\t\tc          config.Config\n\t\targs       []string\n\t\tenvs       config.EnvVars\n\t\twantErr    bool\n\t\texistsFile string\n\t}{\n\t\t{\n\t\t\tdesc: \"正常系: PrintEnvironmentsが設定されていると環境変数を出力して終了\",\n\t\t\tc: config.Config{\n\t\t\t\tPrintEnvironments: true,\n\t\t\t},\n\t\t\targs:    []string{\"hello\"},\n\t\t\tenvs:    config.EnvVars{},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 正常系がパスする。出力先はモックWriterなのでファイルは生成されない\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"hello\"},\n\t\t\tenvs:    config.EnvVars{},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: Writerがエラーを返す\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = \"t.png\"\n\t\t\t\tc.Writer = config.NewMockWriter(true, false)\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"hello\"},\n\t\t\tenvs:    config.EnvVars{},\n\t\t\twantErr: true,\n\t\t},\n\t\t// 旧 main_test.go を移行してきたもの\n\t\t{\n\t\t\tdesc: \"正常系: 画像ファイルに出力する\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_font_is_red_and_background_is_black.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"1234\\x1b[31mred\\x1b[m5678\\nabcd\\x1b[32mgreen\\x1b[0mefgh\\nあい\\x1b[33mう\\x1b[mえお\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_font_is_red_and_background_is_black.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: Foregroundのみもとにもどす、Backgroundのみもとに戻す\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_foreground_default_background_default.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"\\x1b[31;42mRedGreen\\x1b[39mRedGreen\\x1b[49mRedGreen\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_foreground_default_background_default.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 256色を使う\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_color_256.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{c255},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_color_256.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: RGB色を使う\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_color_rgb.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{grad},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_color_rgb.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: JPEGで出力する\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_jpeg.jpeg\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"jpeg\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_jpeg.jpeg\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: GIFで出力する\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_gif.gif\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"gif\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_gif.gif\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 日本語と絵文字を描画する（ただし豆腐になる）。このテストはDockerの方で実施する\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_tofu.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"あいうえお👍\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_tofu.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 前景色と背景色を反転する\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_reverse.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"\\x1b[31;42mRED\\x1b[7m\\nGREEN\\x1b[0m\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_reverse.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 文字色と背景色を変更する\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_font_is_green_and_background_is_blue.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.Foreground = \"green\"\n\t\t\t\tc.Background = \"blue\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"green\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_font_is_green_and_background_is_blue.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: カンマ区切りで指定\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_font_is_blue_and_background_is_red.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.Foreground = \"0,0,255,255\"\n\t\t\t\tc.Background = \"255,0,0,255\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"blue\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_font_is_blue_and_background_is_red.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: Slackアイコンサイズで生成する\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_font_is_blue_and_background_is_red_slack_icon_size.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.Foreground = \"0,0,255,255\"\n\t\t\t\tc.Background = \"255,0,0,255\"\n\t\t\t\tc.ToSlackIcon = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"slack\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_font_is_blue_and_background_is_red_slack_icon_size.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 明示的に幅を指定できる\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_font_is_blue_and_background_is_red_100x200.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.Foreground = \"0,0,255,255\"\n\t\t\t\tc.Background = \"255,0,0,255\"\n\t\t\t\tc.ResizeWidth = 100\n\t\t\t\tc.ResizeHeight = 200\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"100x200\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_font_is_blue_and_background_is_red_100x200.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: Widthのみを指定した場合はHeightが調整される\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_font_is_blue_and_background_is_red_100w.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.Foreground = \"0,0,255,255\"\n\t\t\t\tc.Background = \"255,0,0,255\"\n\t\t\t\tc.ResizeWidth = 100\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"100w\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_font_is_blue_and_background_is_red_100w.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: Heightのみを指定した場合はWidthが調整される\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_font_is_blue_and_background_is_red_100h.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.Foreground = \"0,0,255,255\"\n\t\t\t\tc.Background = \"255,0,0,255\"\n\t\t\t\tc.ResizeHeight = 100\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"100h\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_font_is_blue_and_background_is_red_100h.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 1行のアニメを生成できる\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_animation_1_line.gif\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.UseAnimation = true\n\t\t\t\tc.LineCount = 1\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"\\x1b[31m1\\n\\x1b[32m2\\n\\x1b[33m3\\n\\x1b[34m4\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_animation_1_line.gif\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 2行のアニメを生成できる\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_animation_2_line.gif\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.UseAnimation = true\n\t\t\t\tc.LineCount = 2\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"\\x1b[31m1\\n\\x1b[32m2\\n\\x1b[33m3\\n\\x1b[34m4\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_animation_2_line.gif\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 4行のアニメを生成できる\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_animation_4_line.gif\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.UseAnimation = true\n\t\t\t\tc.LineCount = 4\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"\\x1b[31m1\\n\\x1b[32m2\\n\\x1b[33m3\\n\\x1b[34m4\\n\\x1b[31m5\\n\\x1b[32m6\\n\\x1b[33m7\\n\\x1b[34m8\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_animation_4_line.gif\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 8行のアニメを生成できる\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_animation_8_line.gif\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.UseAnimation = true\n\t\t\t\tc.LineCount = 8\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"\\x1b[31m1\\n\\x1b[32m2\\n\\x1b[33m3\\n\\x1b[34m4\\n\\x1b[31m5\\n\\x1b[32m6\\n\\x1b[33m7\\n\\x1b[34m8\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_animation_8_line.gif\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 4行のアニメを2行ずつスライドする\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_animation_4_line_slide_2_forever.gif\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.UseAnimation = true\n\t\t\t\tc.LineCount = 4\n\t\t\t\tc.UseSlideAnimation = true\n\t\t\t\tc.SlideWidth = 2\n\t\t\t\tc.SlideForever = true\n\t\t\t\tc.Delay = 100\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"\\x1b[31m1\\n\\x1b[32m2\\n\\x1b[33m3\\n\\x1b[34m4\\n\\x1b[31m5\\n\\x1b[32m6\\n\\x1b[33m7\\n\\x1b[34m8\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_animation_4_line_slide_2_forever.gif\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: 4行のアニメを3行ずつスライドする\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_animation_4_line_slide_3_forever.gif\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.UseAnimation = true\n\t\t\t\tc.LineCount = 4\n\t\t\t\tc.UseSlideAnimation = true\n\t\t\t\tc.SlideWidth = 3\n\t\t\t\tc.SlideForever = true\n\t\t\t\tc.Delay = 100\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"\\x1b[31m1\\n\\x1b[32m2\\n\\x1b[33m3\\n\\x1b[34m4\\n\\x1b[31m5\\n\\x1b[32m6\\n\\x1b[33m7\\n\\x1b[34m8\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_animation_4_line_slide_3_forever.gif\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: SlackアイコンサイズでアニメーションGIFを生成できる\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_slack_icon_size_animation.gif\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.ToSlackIcon = true\n\t\t\t\tc.UseAnimation = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"1\\n2\\n3\\n4\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_slack_icon_size_animation.gif\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: すでに同名のファイルが存在する時、別名で保存される\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_numbering.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.SaveNumberedFile = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"number\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_numbering.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: すでに同名のファイルが存在する時、別名で保存される_2\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_numbering.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.SaveNumberedFile = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"number\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_numbering_2.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: すでに同名のファイルが存在する時、別名で保存される_3\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_numbering.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.SaveNumberedFile = true\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"number\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_numbering_3.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: フォントインデックスを指定できる\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_index.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.FontIndex = 0\n\t\t\t\tc.EmojiFontIndex = 0\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:       []string{\"index\"},\n\t\t\tenvs:       config.EnvVars{},\n\t\t\twantErr:    false,\n\t\t\texistsFile: outDir + \"/root_test_index.png\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: 空文字列は不正\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_empty_string.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"\"},\n\t\t\tenvs:    config.EnvVars{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: 改行文字のみは不正\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_only_line.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"\\n\\n\\n\"},\n\t\t\tenvs:    config.EnvVars{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: 色文字列が不正\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_numbering.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.Foreground = \"ggg\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"ggg\"},\n\t\t\tenvs:    config.EnvVars{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: 背景色が不正\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_numbering.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.Background = \"ggg\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"ggg\"},\n\t\t\tenvs:    config.EnvVars{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: 不正なフォント指定\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_numbering.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.FontFile = inDir + \"/illegal_font.ttc\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"ggg\"},\n\t\t\tenvs:    config.EnvVars{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"異常系: 不正な絵文字フォント指定\",\n\t\t\tc: func() config.Config {\n\t\t\t\tc := newDefaultConfig()\n\t\t\t\tc.Outpath = outDir + \"/root_test_numbering.png\"\n\t\t\t\tc.Writer = nil\n\t\t\t\tc.EmojiFontFile = inDir + \"/illegal_font.ttc\"\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\targs:    []string{\"ggg\"},\n\t\t\tenvs:    config.EnvVars{},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\n\t\t\terr := RunRootCommand(tt.c, tt.args, tt.envs)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.NoError(err)\n\t\t\tif tt.existsFile != \"\" {\n\t\t\t\t_, err := os.Stat(tt.existsFile)\n\t\t\t\tgot := os.IsNotExist(err)\n\t\t\t\tassert.False(got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestComplementWidthHeight(t *testing.T) {\n\ttype TestData struct {\n\t\tdesc       string\n\t\tx, y, w, h int\n\t\twantWidth  int\n\t\twantHeight int\n\t}\n\ttds := []TestData{\n\t\t{\n\t\t\tdesc:       \"正常系: wが0のときはwidthが調整される\",\n\t\t\tx:          200,\n\t\t\ty:          100,\n\t\t\tw:          0,\n\t\t\th:          200,\n\t\t\twantWidth:  400,\n\t\t\twantHeight: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"正常系: hが0のときはheightが調整される\",\n\t\t\tx:          200,\n\t\t\ty:          100,\n\t\t\tw:          100,\n\t\t\th:          0,\n\t\t\twantWidth:  100,\n\t\t\twantHeight: 50,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"正常系: hが0のときはheightが調整される\",\n\t\t\tx:          200,\n\t\t\ty:          100,\n\t\t\tw:          100,\n\t\t\th:          0,\n\t\t\twantWidth:  100,\n\t\t\twantHeight: 50,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"正常系: wとhが0出ないときはwとhが返る\",\n\t\t\tx:          200,\n\t\t\ty:          100,\n\t\t\tw:          400,\n\t\t\th:          300,\n\t\t\twantWidth:  400,\n\t\t\twantHeight: 300,\n\t\t},\n\t}\n\tfor _, v := range tds {\n\t\tt.Run(v.desc, func(t *testing.T) {\n\t\t\ta := assert.New(t)\n\t\t\tw, h := complementWidthHeight(v.x, v.y, v.w, v.h)\n\t\t\ta.Equal(v.wantWidth, w)\n\t\t\ta.Equal(v.wantHeight, h)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "scripts/fetch_color.sh",
    "content": "#!/bin/bash\n\ncurl \"https://jonasjacek.github.io/colors/\" \\\n  | grep style \\\n  | sed -r 's@.*<td>([0-9]+)</td>.*<td>(rgb[^<]+)</td>.*@\\1:\\2@g' \\\n  | sed -re 's@rgb\\(@color.RGBA{@g' -e 's/\\)/,255},/g'\n"
  },
  {
    "path": "scripts/width/main.go",
    "content": "// width は文字のrunewidthが返す文字幅を確認するためのツール。\n\n/*\n\n使い方\n\ncd tools/width\ngo build .\n./width あいうえお■漢字abcde😲\n\n*/\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/mattn/go-runewidth\"\n)\n\nfunc main() {\n\targs := os.Args\n\tfmt.Println(\"Char CodePoint Width\")\n\trunewidth.DefaultCondition.StrictEmojiNeutral = false\n\tfor _, c := range args[1] {\n\t\ttext := fmt.Sprintf(\"%v %d %d\", string(c), c, runewidth.RuneWidth(c))\n\t\tfmt.Println(text)\n\t}\n}\n"
  },
  {
    "path": "testdata/in/255.txt",
    "content": "\u001b[38;5;0m000\u001b[38;5;1m001\u001b[38;5;2m002\u001b[38;5;3m003\u001b[38;5;4m004\u001b[38;5;5m005\u001b[38;5;6m006\u001b[38;5;7m007\u001b[38;5;8m008\u001b[38;5;9m009\u001b[38;5;10m010\u001b[38;5;11m011\u001b[38;5;12m012\u001b[38;5;13m013\u001b[38;5;14m014\u001b[38;5;15m015\n\u001b[38;5;16m016\u001b[38;5;17m017\u001b[38;5;18m018\u001b[38;5;19m019\u001b[38;5;20m020\u001b[38;5;21m021\u001b[38;5;22m022\u001b[38;5;23m023\u001b[38;5;24m024\u001b[38;5;25m025\u001b[38;5;26m026\u001b[38;5;27m027\u001b[38;5;28m028\u001b[38;5;29m029\u001b[38;5;30m030\u001b[38;5;31m031\n\u001b[38;5;32m032\u001b[38;5;33m033\u001b[38;5;34m034\u001b[38;5;35m035\u001b[38;5;36m036\u001b[38;5;37m037\u001b[38;5;38m038\u001b[38;5;39m039\u001b[38;5;40m040\u001b[38;5;41m041\u001b[38;5;42m042\u001b[38;5;43m043\u001b[38;5;44m044\u001b[38;5;45m045\u001b[38;5;46m046\u001b[38;5;47m047\n\u001b[38;5;48m048\u001b[38;5;49m049\u001b[38;5;50m050\u001b[38;5;51m051\u001b[38;5;52m052\u001b[38;5;53m053\u001b[38;5;54m054\u001b[38;5;55m055\u001b[38;5;56m056\u001b[38;5;57m057\u001b[38;5;58m058\u001b[38;5;59m059\u001b[38;5;60m060\u001b[38;5;61m061\u001b[38;5;62m062\u001b[38;5;63m063\n\u001b[38;5;64m064\u001b[38;5;65m065\u001b[38;5;66m066\u001b[38;5;67m067\u001b[38;5;68m068\u001b[38;5;69m069\u001b[38;5;70m070\u001b[38;5;71m071\u001b[38;5;72m072\u001b[38;5;73m073\u001b[38;5;74m074\u001b[38;5;75m075\u001b[38;5;76m076\u001b[38;5;77m077\u001b[38;5;78m078\u001b[38;5;79m079\n\u001b[38;5;80m080\u001b[38;5;81m081\u001b[38;5;82m082\u001b[38;5;83m083\u001b[38;5;84m084\u001b[38;5;85m085\u001b[38;5;86m086\u001b[38;5;87m087\u001b[38;5;88m088\u001b[38;5;89m089\u001b[38;5;90m090\u001b[38;5;91m091\u001b[38;5;92m092\u001b[38;5;93m093\u001b[38;5;94m094\u001b[38;5;95m095\n\u001b[38;5;96m096\u001b[38;5;97m097\u001b[38;5;98m098\u001b[38;5;99m099\u001b[38;5;100m100\u001b[38;5;101m101\u001b[38;5;102m102\u001b[38;5;103m103\u001b[38;5;104m104\u001b[38;5;105m105\u001b[38;5;106m106\u001b[38;5;107m107\u001b[38;5;108m108\u001b[38;5;109m109\u001b[38;5;110m110\u001b[38;5;111m111\n\u001b[38;5;112m112\u001b[38;5;113m113\u001b[38;5;114m114\u001b[38;5;115m115\u001b[38;5;116m116\u001b[38;5;117m117\u001b[38;5;118m118\u001b[38;5;119m119\u001b[38;5;120m120\u001b[38;5;121m121\u001b[38;5;122m122\u001b[38;5;123m123\u001b[38;5;124m124\u001b[38;5;125m125\u001b[38;5;126m126\u001b[38;5;127m127\n\u001b[38;5;128m128\u001b[38;5;129m129\u001b[38;5;130m130\u001b[38;5;131m131\u001b[38;5;132m132\u001b[38;5;133m133\u001b[38;5;134m134\u001b[38;5;135m135\u001b[38;5;136m136\u001b[38;5;137m137\u001b[38;5;138m138\u001b[38;5;139m139\u001b[38;5;140m140\u001b[38;5;141m141\u001b[38;5;142m142\u001b[38;5;143m143\n\u001b[38;5;144m144\u001b[38;5;145m145\u001b[38;5;146m146\u001b[38;5;147m147\u001b[38;5;148m148\u001b[38;5;149m149\u001b[38;5;150m150\u001b[38;5;151m151\u001b[38;5;152m152\u001b[38;5;153m153\u001b[38;5;154m154\u001b[38;5;155m155\u001b[38;5;156m156\u001b[38;5;157m157\u001b[38;5;158m158\u001b[38;5;159m159\n\u001b[38;5;160m160\u001b[38;5;161m161\u001b[38;5;162m162\u001b[38;5;163m163\u001b[38;5;164m164\u001b[38;5;165m165\u001b[38;5;166m166\u001b[38;5;167m167\u001b[38;5;168m168\u001b[38;5;169m169\u001b[38;5;170m170\u001b[38;5;171m171\u001b[38;5;172m172\u001b[38;5;173m173\u001b[38;5;174m174\u001b[38;5;175m175\n\u001b[38;5;176m176\u001b[38;5;177m177\u001b[38;5;178m178\u001b[38;5;179m179\u001b[38;5;180m180\u001b[38;5;181m181\u001b[38;5;182m182\u001b[38;5;183m183\u001b[38;5;184m184\u001b[38;5;185m185\u001b[38;5;186m186\u001b[38;5;187m187\u001b[38;5;188m188\u001b[38;5;189m189\u001b[38;5;190m190\u001b[38;5;191m191\n\u001b[38;5;192m192\u001b[38;5;193m193\u001b[38;5;194m194\u001b[38;5;195m195\u001b[38;5;196m196\u001b[38;5;197m197\u001b[38;5;198m198\u001b[38;5;199m199\u001b[38;5;200m200\u001b[38;5;201m201\u001b[38;5;202m202\u001b[38;5;203m203\u001b[38;5;204m204\u001b[38;5;205m205\u001b[38;5;206m206\u001b[38;5;207m207\n\u001b[38;5;208m208\u001b[38;5;209m209\u001b[38;5;210m210\u001b[38;5;211m211\u001b[38;5;212m212\u001b[38;5;213m213\u001b[38;5;214m214\u001b[38;5;215m215\u001b[38;5;216m216\u001b[38;5;217m217\u001b[38;5;218m218\u001b[38;5;219m219\u001b[38;5;220m220\u001b[38;5;221m221\u001b[38;5;222m222\u001b[38;5;223m223\n\u001b[38;5;224m224\u001b[38;5;225m225\u001b[38;5;226m226\u001b[38;5;227m227\u001b[38;5;228m228\u001b[38;5;229m229\u001b[38;5;230m230\u001b[38;5;231m231\u001b[38;5;232m232\u001b[38;5;233m233\u001b[38;5;234m234\u001b[38;5;235m235\u001b[38;5;236m236\u001b[38;5;237m237\u001b[38;5;238m238\u001b[38;5;239m239\n\u001b[38;5;240m240\u001b[38;5;241m241\u001b[38;5;242m242\u001b[38;5;243m243\u001b[38;5;244m244\u001b[38;5;245m245\u001b[38;5;246m246\u001b[38;5;247m247\u001b[38;5;248m248\u001b[38;5;249m249\u001b[38;5;250m250\u001b[38;5;251m251\u001b[38;5;252m252\u001b[38;5;253m253\u001b[38;5;254m254\u001b[38;5;255m255\n"
  },
  {
    "path": "testdata/in/illegal_font.otc",
    "content": "2\n"
  },
  {
    "path": "testdata/in/illegal_font.ttc",
    "content": "1\n"
  },
  {
    "path": "testdata/in/illegal_font.txt",
    "content": "3\n"
  },
  {
    "path": "testdata/in/red_grad.txt",
    "content": "\u001b[38;2;0;0;0m000\u001b[38;2;1;0;0m001\u001b[38;2;2;0;0m002\u001b[38;2;3;0;0m003\u001b[38;2;4;0;0m004\u001b[38;2;5;0;0m005\u001b[38;2;6;0;0m006\u001b[38;2;7;0;0m007\u001b[38;2;8;0;0m008\u001b[38;2;9;0;0m009\u001b[38;2;10;0;0m010\u001b[38;2;11;0;0m011\u001b[38;2;12;0;0m012\u001b[38;2;13;0;0m013\u001b[38;2;14;0;0m014\u001b[38;2;15;0;0m015\n\u001b[38;2;16;0;0m016\u001b[38;2;17;0;0m017\u001b[38;2;18;0;0m018\u001b[38;2;19;0;0m019\u001b[38;2;20;0;0m020\u001b[38;2;21;0;0m021\u001b[38;2;22;0;0m022\u001b[38;2;23;0;0m023\u001b[38;2;24;0;0m024\u001b[38;2;25;0;0m025\u001b[38;2;26;0;0m026\u001b[38;2;27;0;0m027\u001b[38;2;28;0;0m028\u001b[38;2;29;0;0m029\u001b[38;2;30;0;0m030\u001b[38;2;31;0;0m031\n\u001b[38;2;32;0;0m032\u001b[38;2;33;0;0m033\u001b[38;2;34;0;0m034\u001b[38;2;35;0;0m035\u001b[38;2;36;0;0m036\u001b[38;2;37;0;0m037\u001b[38;2;38;0;0m038\u001b[38;2;39;0;0m039\u001b[38;2;40;0;0m040\u001b[38;2;41;0;0m041\u001b[38;2;42;0;0m042\u001b[38;2;43;0;0m043\u001b[38;2;44;0;0m044\u001b[38;2;45;0;0m045\u001b[38;2;46;0;0m046\u001b[38;2;47;0;0m047\n\u001b[38;2;48;0;0m048\u001b[38;2;49;0;0m049\u001b[38;2;50;0;0m050\u001b[38;2;51;0;0m051\u001b[38;2;52;0;0m052\u001b[38;2;53;0;0m053\u001b[38;2;54;0;0m054\u001b[38;2;55;0;0m055\u001b[38;2;56;0;0m056\u001b[38;2;57;0;0m057\u001b[38;2;58;0;0m058\u001b[38;2;59;0;0m059\u001b[38;2;60;0;0m060\u001b[38;2;61;0;0m061\u001b[38;2;62;0;0m062\u001b[38;2;63;0;0m063\n\u001b[38;2;64;0;0m064\u001b[38;2;65;0;0m065\u001b[38;2;66;0;0m066\u001b[38;2;67;0;0m067\u001b[38;2;68;0;0m068\u001b[38;2;69;0;0m069\u001b[38;2;70;0;0m070\u001b[38;2;71;0;0m071\u001b[38;2;72;0;0m072\u001b[38;2;73;0;0m073\u001b[38;2;74;0;0m074\u001b[38;2;75;0;0m075\u001b[38;2;76;0;0m076\u001b[38;2;77;0;0m077\u001b[38;2;78;0;0m078\u001b[38;2;79;0;0m079\n\u001b[38;2;80;0;0m080\u001b[38;2;81;0;0m081\u001b[38;2;82;0;0m082\u001b[38;2;83;0;0m083\u001b[38;2;84;0;0m084\u001b[38;2;85;0;0m085\u001b[38;2;86;0;0m086\u001b[38;2;87;0;0m087\u001b[38;2;88;0;0m088\u001b[38;2;89;0;0m089\u001b[38;2;90;0;0m090\u001b[38;2;91;0;0m091\u001b[38;2;92;0;0m092\u001b[38;2;93;0;0m093\u001b[38;2;94;0;0m094\u001b[38;2;95;0;0m095\n\u001b[38;2;96;0;0m096\u001b[38;2;97;0;0m097\u001b[38;2;98;0;0m098\u001b[38;2;99;0;0m099\u001b[38;2;100;0;0m100\u001b[38;2;101;0;0m101\u001b[38;2;102;0;0m102\u001b[38;2;103;0;0m103\u001b[38;2;104;0;0m104\u001b[38;2;105;0;0m105\u001b[38;2;106;0;0m106\u001b[38;2;107;0;0m107\u001b[38;2;108;0;0m108\u001b[38;2;109;0;0m109\u001b[38;2;110;0;0m110\u001b[38;2;111;0;0m111\n\u001b[38;2;112;0;0m112\u001b[38;2;113;0;0m113\u001b[38;2;114;0;0m114\u001b[38;2;115;0;0m115\u001b[38;2;116;0;0m116\u001b[38;2;117;0;0m117\u001b[38;2;118;0;0m118\u001b[38;2;119;0;0m119\u001b[38;2;120;0;0m120\u001b[38;2;121;0;0m121\u001b[38;2;122;0;0m122\u001b[38;2;123;0;0m123\u001b[38;2;124;0;0m124\u001b[38;2;125;0;0m125\u001b[38;2;126;0;0m126\u001b[38;2;127;0;0m127\n\u001b[38;2;128;0;0m128\u001b[38;2;129;0;0m129\u001b[38;2;130;0;0m130\u001b[38;2;131;0;0m131\u001b[38;2;132;0;0m132\u001b[38;2;133;0;0m133\u001b[38;2;134;0;0m134\u001b[38;2;135;0;0m135\u001b[38;2;136;0;0m136\u001b[38;2;137;0;0m137\u001b[38;2;138;0;0m138\u001b[38;2;139;0;0m139\u001b[38;2;140;0;0m140\u001b[38;2;141;0;0m141\u001b[38;2;142;0;0m142\u001b[38;2;143;0;0m143\n\u001b[38;2;144;0;0m144\u001b[38;2;145;0;0m145\u001b[38;2;146;0;0m146\u001b[38;2;147;0;0m147\u001b[38;2;148;0;0m148\u001b[38;2;149;0;0m149\u001b[38;2;150;0;0m150\u001b[38;2;151;0;0m151\u001b[38;2;152;0;0m152\u001b[38;2;153;0;0m153\u001b[38;2;154;0;0m154\u001b[38;2;155;0;0m155\u001b[38;2;156;0;0m156\u001b[38;2;157;0;0m157\u001b[38;2;158;0;0m158\u001b[38;2;159;0;0m159\n\u001b[38;2;160;0;0m160\u001b[38;2;161;0;0m161\u001b[38;2;162;0;0m162\u001b[38;2;163;0;0m163\u001b[38;2;164;0;0m164\u001b[38;2;165;0;0m165\u001b[38;2;166;0;0m166\u001b[38;2;167;0;0m167\u001b[38;2;168;0;0m168\u001b[38;2;169;0;0m169\u001b[38;2;170;0;0m170\u001b[38;2;171;0;0m171\u001b[38;2;172;0;0m172\u001b[38;2;173;0;0m173\u001b[38;2;174;0;0m174\u001b[38;2;175;0;0m175\n\u001b[38;2;176;0;0m176\u001b[38;2;177;0;0m177\u001b[38;2;178;0;0m178\u001b[38;2;179;0;0m179\u001b[38;2;180;0;0m180\u001b[38;2;181;0;0m181\u001b[38;2;182;0;0m182\u001b[38;2;183;0;0m183\u001b[38;2;184;0;0m184\u001b[38;2;185;0;0m185\u001b[38;2;186;0;0m186\u001b[38;2;187;0;0m187\u001b[38;2;188;0;0m188\u001b[38;2;189;0;0m189\u001b[38;2;190;0;0m190\u001b[38;2;191;0;0m191\n\u001b[38;2;192;0;0m192\u001b[38;2;193;0;0m193\u001b[38;2;194;0;0m194\u001b[38;2;195;0;0m195\u001b[38;2;196;0;0m196\u001b[38;2;197;0;0m197\u001b[38;2;198;0;0m198\u001b[38;2;199;0;0m199\u001b[38;2;200;0;0m200\u001b[38;2;201;0;0m201\u001b[38;2;202;0;0m202\u001b[38;2;203;0;0m203\u001b[38;2;204;0;0m204\u001b[38;2;205;0;0m205\u001b[38;2;206;0;0m206\u001b[38;2;207;0;0m207\n\u001b[38;2;208;0;0m208\u001b[38;2;209;0;0m209\u001b[38;2;210;0;0m210\u001b[38;2;211;0;0m211\u001b[38;2;212;0;0m212\u001b[38;2;213;0;0m213\u001b[38;2;214;0;0m214\u001b[38;2;215;0;0m215\u001b[38;2;216;0;0m216\u001b[38;2;217;0;0m217\u001b[38;2;218;0;0m218\u001b[38;2;219;0;0m219\u001b[38;2;220;0;0m220\u001b[38;2;221;0;0m221\u001b[38;2;222;0;0m222\u001b[38;2;223;0;0m223\n\u001b[38;2;224;0;0m224\u001b[38;2;225;0;0m225\u001b[38;2;226;0;0m226\u001b[38;2;227;0;0m227\u001b[38;2;228;0;0m228\u001b[38;2;229;0;0m229\u001b[38;2;230;0;0m230\u001b[38;2;231;0;0m231\u001b[38;2;232;0;0m232\u001b[38;2;233;0;0m233\u001b[38;2;234;0;0m234\u001b[38;2;235;0;0m235\u001b[38;2;236;0;0m236\u001b[38;2;237;0;0m237\u001b[38;2;238;0;0m238\u001b[38;2;239;0;0m239\n\u001b[38;2;240;0;0m240\u001b[38;2;241;0;0m241\u001b[38;2;242;0;0m242\u001b[38;2;243;0;0m243\u001b[38;2;244;0;0m244\u001b[38;2;245;0;0m245\u001b[38;2;246;0;0m246\u001b[38;2;247;0;0m247\u001b[38;2;248;0;0m248\u001b[38;2;249;0;0m249\u001b[38;2;250;0;0m250\u001b[38;2;251;0;0m251\u001b[38;2;252;0;0m252\u001b[38;2;253;0;0m253\u001b[38;2;254;0;0m254\u001b[38;2;255;0;0m255\n"
  },
  {
    "path": "token/token.go",
    "content": "package token\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/jiro4989/textimg/v3/color\"\n\t\"github.com/mattn/go-runewidth\"\n)\n\ntype (\n\tKind      int\n\tColorType int\n\tToken     struct {\n\t\tKind      Kind\n\t\tColorType ColorType\n\t\tColor     color.RGBA\n\t\tText      string\n\t}\n\tTokens []Token\n)\n\nconst (\n\tKindEmpty Kind = iota\n\tKindText\n\tKindColor\n\tKindNotColor\n\n\tColorTypeReset       ColorType = iota // \\x1b[0m 指定をリセット\n\tColorTypeBold                         // \\x1b[1m 太字\n\tColorTypeDim                          // \\x1b[2m 薄く表示\n\tColorTypeItalic                       // \\x1b[3m イタリック\n\tColorTypeUnderline                    // \\x1b[4m アンダーライン\n\tColorTypeBlink                        // \\x1b[5m ブリンク\n\tColorTypeSpeedyBlink                  // \\x1b[6m 高速ブリンク\n\tColorTypeReverse                      // \\x1b[7m 文字色と背景色の反転\n\tColorTypeHide                         // \\x1b[8m 表示を隠す\n\tColorTypeDelete                       // \\x1b[9m 取り消し\n\tColorTypeForeground\n\tColorTypeBackground\n\tColorTypeResetForeground\n\tColorTypeResetBackground\n)\n\nfunc init() {\n\t// Unicode Neutral で定義されている絵文字(例: 👁)を幅2として扱う\n\trunewidth.DefaultCondition.StrictEmojiNeutral = false\n}\n\nfunc NewResetColor() Token {\n\treturn Token{\n\t\tKind:      KindColor,\n\t\tColorType: ColorTypeReset,\n\t}\n}\n\nfunc NewResetForegroundColor() Token {\n\treturn Token{\n\t\tKind:      KindColor,\n\t\tColorType: ColorTypeResetForeground,\n\t}\n}\n\nfunc NewResetBackgroundColor() Token {\n\treturn Token{\n\t\tKind:      KindColor,\n\t\tColorType: ColorTypeResetBackground,\n\t}\n}\n\nfunc NewReverseColor() Token {\n\treturn Token{\n\t\tKind:      KindColor,\n\t\tColorType: ColorTypeReverse,\n\t}\n}\n\nfunc NewText(text string) Token {\n\treturn Token{\n\t\tKind: KindText,\n\t\tText: text,\n\t}\n}\n\nfunc NewStandardColorWithCategory(text string) Token {\n\tn, _ := strconv.Atoi(text)\n\treturn Token{\n\t\tKind:      KindColor,\n\t\tColorType: colorType(n),\n\t\tColor:     color.ANSIMap[n],\n\t}\n}\n\nfunc NewExtendedColor(text string) Token {\n\tn, _ := strconv.Atoi(text)\n\treturn Token{\n\t\tKind:      KindColor,\n\t\tColorType: colorType(n),\n\t\tColor:     color.RGBA{A: 255},\n\t}\n}\n\nfunc colorType(n int) ColorType {\n\tvar t ColorType\n\tswitch n / 10 {\n\tcase 3, 9:\n\t\tt = ColorTypeForeground\n\tcase 4, 10:\n\t\tt = ColorTypeBackground\n\t}\n\treturn t\n}\n\nfunc (t *Tokens) MaxStringWidth() int {\n\tvar strs []string\n\tfor _, tt := range *t {\n\t\tif tt.Kind != KindText {\n\t\t\tcontinue\n\t\t}\n\t\ts := tt.Text\n\t\tstrs = append(strs, s)\n\t}\n\ts := strings.Join(strs, \"\")\n\tlines := strings.Split(s, \"\\n\")\n\tvar max int\n\tfor _, line := range lines {\n\t\tw := runewidth.StringWidth(line)\n\t\tif max < w {\n\t\t\tmax = w\n\t\t}\n\t}\n\treturn max\n}\n\nfunc (t *Tokens) StringLines() []string {\n\tvar strs []string\n\tfor _, tt := range *t {\n\t\tif tt.Kind != KindText {\n\t\t\tcontinue\n\t\t}\n\t\ts := tt.Text\n\t\tstrs = append(strs, s)\n\t}\n\ts := strings.Join(strs, \"\")\n\tlines := strings.Split(s, \"\\n\")\n\treturn lines\n}\n"
  },
  {
    "path": "token/token_test.go",
    "content": "package token\n\nimport (\n\t\"testing\"\n\n\t\"github.com/jiro4989/textimg/v3/color\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestToken_MaxStringWidth(t *testing.T) {\n\ttests := []struct {\n\t\tdesc string\n\t\tt    Tokens\n\t\twant int\n\t}{\n\t\t{\n\t\t\tdesc: \"正常系: hello = 5\",\n\t\t\tt: Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"hello\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 5,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: あいうえお = 10\",\n\t\t\tt: Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"あいうえお\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 10,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: he\\nllo = 3\",\n\t\t\tt: Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"he\\nllo\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 3,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: hel\\nlo = 3\",\n\t\t\tt: Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"hel\\nlo\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 3,\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: aREDb = 5\",\n\t\t\tt: Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"a\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      KindColor,\n\t\t\t\t\tColorType: ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBARed,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"RED\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      KindColor,\n\t\t\t\t\tColorType: ColorTypeReset,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"b\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 5,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\t\t\tgot := tt.t.MaxStringWidth()\n\t\t\tassert.Equal(tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestToken_StringLines(t *testing.T) {\n\ttests := []struct {\n\t\tdesc string\n\t\tt    Tokens\n\t\twant []string\n\t}{\n\t\t{\n\t\t\tdesc: \"正常系: hello\",\n\t\t\tt: Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"hello\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{\"hello\"},\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: hel\\nlo\\nworld\",\n\t\t\tt: Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"hel\\nlo\\nworld\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{\"hel\", \"lo\", \"world\"},\n\t\t},\n\t\t{\n\t\t\tdesc: \"正常系: aREDb = 5\",\n\t\t\tt: Tokens{\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"a\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      KindColor,\n\t\t\t\t\tColorType: ColorTypeForeground,\n\t\t\t\t\tColor:     color.RGBARed,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"RED\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind:      KindColor,\n\t\t\t\t\tColorType: ColorTypeReset,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKind: KindText,\n\t\t\t\t\tText: \"b\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{\"aREDb\"},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\t\t\tgot := tt.t.StringLines()\n\t\t\tassert.Equal(tt.want, got)\n\t\t})\n\t}\n}\n"
  }
]