Full Code of jiro4989/textimg for AI

master 88be915c4918 cached
55 files
164.8 KB
62.5k tokens
216 symbols
1 requests
Download .txt
Repository: jiro4989/textimg
Branch: master
Commit: 88be915c4918
Files: 55
Total size: 164.8 KB

Directory structure:
gitextract_1f4t7hds/

├── .chglog/
│   ├── CHANGELOG.tpl.md
│   └── config.yml
├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   ├── pr-labeler.yml
│   └── workflows/
│       ├── auto_merge.yml
│       ├── pr-labeler.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── color/
│   └── color.go
├── completions/
│   ├── fish/
│   │   └── textimg.fish
│   └── zsh/
│       └── _textimg
├── config/
│   ├── config.go
│   ├── config_test.go
│   ├── envvar.go
│   ├── face.go
│   ├── face_test.go
│   └── writer_mock.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── image/
│   ├── encode.go
│   ├── image.go
│   └── util.go
├── images/
│   └── .gitkeep
├── internal/
│   └── global/
│       ├── env.go
│       └── version.go
├── log/
│   ├── log.go
│   └── log_test.go
├── main.go
├── main_test.go
├── parser/
│   ├── grammar.peg
│   ├── grammar.peg.go
│   ├── parser.go
│   └── parser_test.go
├── root.go
├── root_common_test.go
├── root_on_docker_test.go
├── root_test.go
├── scripts/
│   ├── fetch_color.sh
│   └── width/
│       └── main.go
├── testdata/
│   └── in/
│       ├── 255.txt
│       ├── illegal_font.otc
│       ├── illegal_font.ttc
│       ├── illegal_font.txt
│       └── red_grad.txt
└── token/
    ├── token.go
    └── token_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .chglog/CHANGELOG.tpl.md
================================================
{{ range .Versions }}
## Changes

{{ range .CommitGroups -}}
### {{ .Title }}

{{ range .Commits -}}
* {{ .Subject }}
{{ end }}
{{ end -}}

{{- if .RevertCommits -}}
### Reverts

{{ range .RevertCommits -}}
* {{ .Revert.Header }}
{{ end }}
{{ end -}}

{{- if .MergeCommits -}}
### Pull Requests

{{ range .MergeCommits -}}
* {{ .Header }}
{{ end }}
{{ end -}}

{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}

{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}


================================================
FILE: .chglog/config.yml
================================================
style: github
template: CHANGELOG.tpl.md
info:
  title: CHANGELOG
  repository_url: https://github.com/YOUR_NAME/REPOSITORY
options:
  commits:
    filters:
      Type:
        - feat
        - fix
        - perf
        - refactor
  commit_groups:
    # title_maps:
    #   feat: Features
    #   fix: Bug Fixes
    #   perf: Performance Improvements
    #   refactor: Code Refactoring
  header:
    pattern: "^(\\w*)\\:\\s(.*)$"
    pattern_maps:
      - Type
      - Subject
  notes:
    keywords:
      - BREAKING CHANGE


================================================
FILE: .dockerignore
================================================
bin
build
.git


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: jiro4989

---

## Describe the bug

A clear and concise description of what the bug is.

## To Reproduce

Steps to reproduce the behavior:

1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

## Expected behavior

A clear and concise description of what you expected to happen.

## Screenshots or Logs

If applicable, add screenshots to help explain your problem.

## Environment (please complete the following information)

- OS: [e.g. Windows10]
- Version: [e.g. 1.6.0]

## Additional context

Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: jiro4989

---

## Is your feature request related to a problem? Please describe

A clear and concise description of what the problem is. Ex. I'm always
frustrated when [...]

## Describe the solution you'd like

A clear and concise description of what you want to happen.

## Describe alternatives you've considered

A clear and concise description of any alternative solutions or features you've
considered.

## Additional context

Add any other context or screenshots about the feature request here.


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
---
version: 2
updates:
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "daily"
    cooldown:
      default-days: 7
    assignees:
      - "jiro4989"
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "daily"
    cooldown:
      default-days: 7
    assignees:
      - "jiro4989"
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"
    cooldown:
      default-days: 7
    assignees:
      - "jiro4989"


================================================
FILE: .github/pr-labeler.yml
================================================
feature: feature/*
bug: hotfix/*
chore: chore/*


================================================
FILE: .github/workflows/auto_merge.yml
================================================
---
name: Dependabot auto-merge
"on": pull_request

permissions:
  pull-requests: write
  contents: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - name: Enable auto-merge for Dependabot PRs
        if: >-
          ${{
          steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
          steps.metadata.outputs.update-type == 'version-update:semver-minor'
          }}
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}


================================================
FILE: .github/workflows/pr-labeler.yml
================================================
name: labeler

on:
  pull_request:
    types: [opened]

jobs:
  labeler:
    runs-on: ubuntu-latest
    steps:
      - uses: TimonVS/pr-labeler-action@f9c084306ce8b3f488a8f3ee1ccedc6da131d1af # v5.0.0
        with:
          configuration-path: .github/pr-labeler.yml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/release.yml
================================================
---
name: release

"on":
  push:
    tags:
      - 'v*.*.*'

env:
  app: textimg
  goversion: '1.25'
  build_opts: '-ldflags="-s -w -extldflags \"-static\""'
  description: 'textimg is command to convert from color text (ANSI or 256) to image.'

jobs:
  build-artifact:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [linux, windows, darwin]
        arch: [amd64, arm64]
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
        with:
          go-version: ${{ env.goversion }}
      - name: Build
        run: |
          go build ${{ env.build_opts }}
          if [[ $GOOS = windows ]]; then
            7z a ${{ env.app }}-$GOOS-$GOARCH.zip ./${{ env.app }}.exe
          else
            tar czf ${{ env.app }}-$GOOS-$GOARCH.tar.gz ./${{ env.app }}
          fi
        env:
          GOOS: ${{ matrix.os }}
          GOARCH: ${{ matrix.arch }}

      - name: Upload artifact (windows)
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: artifact-${{ matrix.os }}-${{ matrix.arch }}
          path: ${{ env.app }}-${{ matrix.os }}-${{ matrix.arch }}.zip
        if: matrix.os == 'windows'

      - name: Upload artifact (unix)
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: artifact-${{ matrix.os }}-${{ matrix.arch }}
          path: ${{ env.app }}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz
        if: matrix.os != 'windows'

  build-debian-packages:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
        with:
          go-version: ${{ env.goversion }}
      - run: go build ${{ env.build_opts }}
      - name: Create package
        run: |
          mkdir -p .debpkg/usr/bin
          cp -p ./${{ env.app }} .debpkg/usr/bin/
      - uses: jiro4989/build-deb-action@a883c65147d80579cb359548b9a902ff0a35ae5b # v4.3.0
        with:
          package: ${{ env.app }}
          package_root: .debpkg
          maintainer: jiro4989
          version: ${{ github.ref }}
          arch: 'amd64'
          desc: ${{ env.description }}
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: artifact-deb
          path: |
            ./*.deb

  build-rpm-packages:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
        with:
          go-version: ${{ env.goversion }}
      - run: go build ${{ env.build_opts }}
      - name: Create package
        run: |
          mkdir -p .rpmpkg/usr/bin
          cp -p ./${{ env.app }} .rpmpkg/usr/bin/
      - uses: jiro4989/build-rpm-action@f11474937f502aaa8bb36d8c1a8ec6f8de536a0c # v2.5.0
        with:
          summary: '${{ env.app }} is command to convert from color text (ANSI or 256) to image.'
          package: ${{ env.app }}
          package_root: .rpmpkg
          maintainer: jiro4989
          version: ${{ github.ref }}
          arch: 'x86_64'
          desc: ${{ env.description }}
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: artifact-rpm
          path: |
            ./*.rpm
            !./*-debuginfo-*.rpm

  create-release:
    runs-on: ubuntu-latest
    needs:
      - build-artifact
      - build-debian-packages
      - build-rpm-packages
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Generate changelog
        run: |
          wget https://github.com/git-chglog/git-chglog/releases/download/0.9.1/git-chglog_linux_amd64
          chmod +x git-chglog_linux_amd64
          mv git-chglog_linux_amd64 git-chglog
          ./git-chglog --output ./changelog $(git describe --tags $(git rev-list --tags --max-count=1))

      - name: Create Release
        id: create-release
        uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: ${{ github.ref }}
          body_path: ./changelog
          draft: false
          prerelease: false

      - name: Write upload_url to file
        run: echo '${{ steps.create-release.outputs.upload_url }}' > upload_url.txt

      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: create-release
          path: upload_url.txt

  upload-release:
    runs-on: ubuntu-latest
    needs: create-release
    strategy:
      matrix:
        os: [linux, windows, darwin]
        arch: [amd64, arm64]
        include:
          - os: windows
            asset_content_type: application/zip
          - os: linux
            asset_content_type: application/gzip
          - os: darwin
            asset_content_type: application/gzip
    steps:
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: artifact-${{ matrix.os }}-${{ matrix.arch }}

      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: create-release

      - id: vars
        run: |
          echo "::set-output name=upload_url::$(cat upload_url.txt)"
          echo "::set-output name=asset_name::$(ls ${{ env.app }}-${{ matrix.os }}-${{ matrix.arch }}.* | head -n 1)"

      - name: Upload Release Asset
        id: upload-release-asset
        uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.vars.outputs.upload_url }}
          asset_path: ${{ steps.vars.outputs.asset_name }}
          asset_name: ${{ steps.vars.outputs.asset_name }}
          asset_content_type: ${{ matrix.asset_content_type }}

  upload-linux-package:
    runs-on: ubuntu-latest
    needs: create-release
    strategy:
      matrix:
        include:
          - pkg: deb
            asset_content_type: application/vnd.debian.binary-package
          - pkg: rpm
            asset_content_type: application/x-rpm
    steps:
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: artifact-${{ matrix.pkg }}

      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: create-release

      - id: vars
        run: |
          echo "::set-output name=upload_url::$(cat upload_url.txt)"
          echo "::set-output name=asset_name::$(ls *.${{ matrix.pkg }} | head -n 1)"

      - name: Upload Release Asset
        id: upload-release-asset
        uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.vars.outputs.upload_url }}
          asset_path: ${{ steps.vars.outputs.asset_name }}
          asset_name: ${{ steps.vars.outputs.asset_name }}
          asset_content_type: ${{ matrix.asset_content_type }}


================================================
FILE: .github/workflows/test.yml
================================================
---

name: test

"on":
  push:
    branches:
      - master
    paths-ignore:
      - README*
      - LICENSE
  pull_request:
    paths-ignore:
      - README*
      - LICENSE

env:
  goversion: '1.25'

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go:
          - '1.25'
          - '1.x'
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
        with:
          go-version: ${{ matrix.go }}
      - run: go build
      - run: go install
      - run: go test -cover ./...

  build-arm64:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go:
          - '1.x'
        os:
          - 'linux'
          - 'darwin'
          - 'windows'
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
        with:
          go-version: ${{ matrix.go }}
      # if use CGO_ENABLED=1 then use this code.
      #
      # - run: sudo apt-get install -y gcc-aarch64-linux-gnu
      # - run: GOOS=${{ matrix.os }} GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -o textimg_${{ matrix.os }}_arm64
      - run: GOOS=${{ matrix.os }} GOARCH=arm64 go build -o textimg_${{ matrix.os }}_arm64
      - run: gzip textimg_${{ matrix.os }}_arm64
      - name: Upload artifact (windows)
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: textimg_${{ matrix.os }}_arm64.gz
          path: textimg_${{ matrix.os }}_arm64.gz

  format:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
        with:
          go-version: ${{ env.goversion }}
      - name: Check code format
        run: |
          go mod download
          count="$(go fmt ./... | wc -l)"
          if [[ "$count" -ne 0 ]]; then
            echo "[ERR] 'go fmt ./...' してください" >&2
            exit 1
          fi

  docker-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Tests
        run: |
          make docker-build
          make docker-test

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
        with:
          go-version: ${{ env.goversion }}
      - name: Lint
        run: go vet .

  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
        with:
          go-version: ${{ env.goversion }}
      - run: go test -coverprofile=coverage.out ./...
      - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0


================================================
FILE: .gitignore
================================================
/bin/
/dist/
/testdata/out/
/testdata/out*/
images/*
!images/.gitkeep
scripts/width/width

!completions/*/textimg
textimg


================================================
FILE: Dockerfile
================================================
FROM golang:1.26-alpine3.22 AS base

RUN go version \
    && echo $GOPATH \
    && apk update \
    && apk add --no-cache git wget unzip fontconfig alpine-sdk bash \
    && wget https://github.com/tomokuni/Myrica/raw/master/product/MyricaM.zip -q -O /tmp/MyricaM.zip \
    && (cd /tmp && unzip MyricaM.zip) \
    && git clone https://github.com/googlefonts/noto-emoji /usr/local/src/noto-emoji \
    && wget https://www.wfonts.com/download/data/2016/04/23/symbola/symbola.zip -q -O /tmp/symbola.zip \
    && (cd /tmp && unzip symbola.zip)

################################################################################

FROM base AS builder

COPY . /app
WORKDIR /app
RUN go install

################################################################################

FROM alpine:3.23.4 AS runtime
COPY --from=builder /go/bin/textimg /usr/local/bin/
COPY --from=builder /tmp/MyricaM.TTC /usr/share/fonts/truetype/myrica/MyricaM.TTC
COPY --from=builder /usr/local/src/noto-emoji/png/128 /usr/share/emoji-image
COPY --from=builder /tmp/Symbola_hint.ttf /usr/share/fonts/truetype/symbola/
COPY --from=builder /tmp/Symbola_hint.ttf /usr/share/fonts/truetype/ancient-scripts/

ENV TEXTIMG_OUTPUT_DIR /images
ENV TEXTIMG_FONT_FILE /usr/share/fonts/truetype/myrica/MyricaM.TTC
ENV TEXTIMG_EMOJI_DIR /usr/share/emoji-image
ENV TEXTIMG_EMOJI_FONT_FILE /usr/share/fonts/truetype/symbola/Symbola_hint.ttf
ENV LANG ja_JP.UTF-8
RUN mkdir /images

ENTRYPOINT ["/usr/local/bin/textimg"]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 jiro4989

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
textimg: parser/grammar.peg.go *.go */*.go
	go fmt ./...
	go build

parser/grammar.peg.go: parser/grammar.peg
	peg parser/grammar.peg

.PHONY: help
help: ## ドキュメントのヘルプを表示する。
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

.PHONY: test
test: textimg ## テストコードを実行する
	go test -cover ./...

.PHONY: docker-build
docker-build: ## Dockerイメージをビルドする
	docker compose build

.PHONY: docker-test
docker-test: ## Docker環境でgo testを実行する
	docker compose run --rm base go test -tags docker -cover ./...

.PHONY: docker-push
docker-push: ## DockerHubにイメージをPushする
	docker push jiro4989/textimg

.PHONY: setup-tools
setup-tools: ## 開発時に使うツールをインストールする
	go install github.com/pointlander/peg@latest
	wget https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc
	sudo install -m 0755 ./gh-md-toc /usr/local/bin/
	-rm -f gh-md-toc


================================================
FILE: README.md
================================================
# textimg

![test](https://github.com/jiro4989/textimg/workflows/test/badge.svg)
[![codecov](https://codecov.io/gh/jiro4989/textimg/branch/master/graph/badge.svg)](https://codecov.io/gh/jiro4989/textimg)

textimg is command to convert from color text (ANSI or 256) to image.  
Drawn image keeps having colors of escape sequence.

* [README on Japanese](./README_ja.md)

Table of contents:

<!--ts-->
* [textimg](#textimg)
  * [Usage](#usage)
    * [Simple examples](#simple-examples)
    * [With other commands](#with-other-commands)
    * [Rainbow examples](#rainbow-examples)
      * [From ANSI color](#from-ansi-color)
      * [From 256 color](#from-256-color)
      * [From 256 RGB color](#from-256-rgb-color)
      * [Animation GIF](#animation-gif)
      * [Slide animation GIF](#slide-animation-gif)
    * [Using on Docker](#using-on-docker)
    * [Saving shortcut](#saving-shortcut)
  * [Installation](#installation)
    * [Linux users (Debian base distros)](#linux-users-debian-base-distros)
    * [Linux users (RHEL compatible distros)](#linux-users-rhel-compatible-distros)
    * [With Go](#with-go)
    * [Manual](#manual)
  * [Help](#help)
  * [Fonts](#fonts)
    * [Default font path](#default-font-path)
    * [Emoji font (image file path)](#emoji-font-image-file-path)
    * [Emoji font (TTF)](#emoji-font-ttf)
  * [Tab Completions](#tab-completions)
    * [Bash](#bash)
    * [Zsh](#zsh)
    * [Fish](#fish)
  * [Development](#development)
    * [How to build](#how-to-build)
    * [How to test](#how-to-test)
  * [See also](#see-also)

<!-- Added by: jiro4989, at: Sat Jun 19 17:52:14 JST 2021 -->

<!--te-->

## Usage

### Simple examples

```bash
textimg $'\x1b[31mRED\x1b[0m' > out.png
textimg $'\x1b[31mRED\x1b[0m' -o out.png
echo -e '\x1b[31mRED\x1b[0m' | textimg -o out.png
echo -e '\x1b[31mRED\x1b[0m' | textimg --background 0,255,255,255 -o out.jpg
echo -e '\x1b[31mRED\x1b[0m' | textimg --background black -o out.gif
```

Output image format is PNG or JPG or GIF.
File extension of `-o` option defines output image format.
Default image format is PNG. if you write image file with `>` redirect then
image file will be saved as PNG file.

### With other commands

grep:

```bash
echo hello world | grep hello --color=always | textimg -o out.png
```

![image](https://user-images.githubusercontent.com/13825004/92329722-4e77d380-f0a4-11ea-97eb-0de316ebf6c7.png)

screenfetch:

```bash
screenfetch | textimg -o out.png
```

[bat](https://github.com/sharkdp/bat):

```bash
bat --color=always /etc/profile | textimg -o out.png
```

![image](https://user-images.githubusercontent.com/13825004/92329806-03aa8b80-f0a5-11ea-95f4-d876c34d65d6.png)

ccze:

```bash
ls -lah | ccze -A | textimg -o out.png
```

![image](https://user-images.githubusercontent.com/13825004/113440487-7e633b80-9427-11eb-8e03-4888308780a7.png)

lolcat:

```bash
seq -f 'seq %g | xargs' 18 | bash | lolcat -f --freq=0.5 | textimg -o out.png
```

![image](https://user-images.githubusercontent.com/13825004/113440659-ce420280-9427-11eb-933b-7f9b1b618264.png)

### Rainbow examples

#### From ANSI color

textimg supports `\x1b[30m` notation.

```bash
colors=(30 31 32 33 34 35 36 37)
i=0
while read -r line; do
  echo -e "$line" | sed -r 's/.*/\x1b['"${colors[$((i%8))]}"'m&\x1b[m/g'
  i=$((i+1))
done <<< "$(seq 8 | xargs -I@ echo TEST)" | textimg -b 50,100,12,255 -o testdata/out/rainbow.png
```

Output is here.

![Rainbow example](docs/rainbow.png)

#### From 256 color

textimg supports `\x1b[38;5;255m` notation.

Foreground example is below.

```bash
seq 0 255 | while read -r i; do
  echo -ne "\x1b[38;5;${i}m$(printf %03d $i)"
  if [ $(((i+1) % 16)) -eq 0 ]; then
    echo
  fi
done | textimg -o 256_fg.png
```

Output is here.

![256 foreground example](docs/256_fg.png)

Background example is below.

```bash
seq 0 255 | while read -r i; do
  echo -ne "\x1b[48;5;${i}m$(printf %03d $i)"
  if [ $(((i+1) % 16)) -eq 0 ]; then
    echo
  fi
done | textimg -o 256_bg.png
```

Output is here.

![256 background example](docs/256_bg.png)

#### From 256 RGB color

textimg supports `\x1b[38;2;255;0;0m` notation.

```bash
seq 0 255 | while read i; do
  echo -ne "\x1b[38;2;${i};0;0m$(printf %03d $i)"
  if [ $(((i+1) % 16)) -eq 0 ]; then
    echo
  fi
done | textimg -o extrgb_f_gradation.png
```

Output is here.

![RGB gradation example](docs/extrgb_f_gradation.png)

#### Animation GIF

textimg supports animation GIF.

```bash
echo -e '\x1b[31mText\x1b[0m
\x1b[32mText\x1b[0m
\x1b[33mText\x1b[0m
\x1b[34mText\x1b[0m
\x1b[35mText\x1b[0m
\x1b[36mText\x1b[0m
\x1b[37mText\x1b[0m
\x1b[41mText\x1b[0m
\x1b[42mText\x1b[0m
\x1b[43mText\x1b[0m
\x1b[44mText\x1b[0m
\x1b[45mText\x1b[0m
\x1b[46mText\x1b[0m
\x1b[47mText\x1b[0m' | textimg -a -o ansi_fb_anime_1line.gif
```

Output is here.

![Animation GIF example](docs/ansi_fb_anime_1line.gif)

#### Slide animation GIF

```bash
echo -e '\x1b[31mText\x1b[0m
\x1b[32mText\x1b[0m
\x1b[33mText\x1b[0m
\x1b[34mText\x1b[0m
\x1b[35mText\x1b[0m
\x1b[36mText\x1b[0m
\x1b[37mText\x1b[0m
\x1b[41mText\x1b[0m
\x1b[42mText\x1b[0m
\x1b[43mText\x1b[0m
\x1b[44mText\x1b[0m
\x1b[45mText\x1b[0m
\x1b[46mText\x1b[0m
\x1b[47mText\x1b[0m' | textimg -l 5 -SE -o slide_5_1_rainbow_forever.gif
```

Output is here.

![Slide Animation GIF example](docs/slide_5_1_rainbow_forever.gif)

### Using on Docker

You can use textimg on Docker. ([DockerHub](https://hub.docker.com/r/jiro4989/textimg))

```bash
docker pull jiro4989/textimg
docker run -v $(pwd):/images -it jiro4989/textimg -h
docker run -v $(pwd):/images -it jiro4989/textimg Testあいうえお😄 -o /images/a.png
docker run -v $(pwd):/images -it jiro4989/textimg Testあいうえお😄 -s
```

or build docker image of local Dockerfile.

```bash
docker-compose build
docker-compose run textimg $'\x1b[31mHello\x1b[42mWorld\x1b[m' -s
```

### Saving shortcut

`textimg` saves an image as `t.png` to `$HOME/Pictures` (`%USERPROFILE%` on
Windows) with `-s` options.  You can change this directory with
`TEXTIMG_OUTPUT_DIR` environment variables.

`textimg` adds current timestamp to the file suffix when activate `-t` options.

```bash
$ textimg 寿司 -st

$ ls ~/Pictures/
t_2021-03-21-194959.png
```

And, `textimg` adds number to the file suffix when activate `-n` options and
the file has existed.

```bash
$ textimg 寿司 -sn

$ textimg 寿司 -sn

$ ls ~/Pictures/
t.png  t_2.png
```

## Installation

### Linux users (Debian base distros)

```bash
wget https://github.com/jiro4989/textimg/releases/download/v3.1.9/textimg_3.1.9_amd64.deb
sudo dpkg -i ./*.deb
```

### Linux users (RHEL compatible distros)

```bash
sudo yum install https://github.com/jiro4989/textimg/releases/download/v3.1.9/textimg-3.1.9-1.el7.x86_64.rpm
```

### With Go

```bash
go install github.com/jiro4989/textimg/v3@latest
```

### Manual

Download binary from [Releases](https://github.com/jiro4989/textimg/releases).

## Help

```
textimg is command to convert from colored text (ANSI or 256) to image.

Usage:
  textimg [flags]

Examples:
textimg $'\x1b[31mRED\x1b[0m' -o out.png

Flags:
  -g, --foreground string         foreground text color.
                                  available color types are [black|red|green|yellow|blue|magenta|cyan|white]
                                  or (R,G,B,A(0~255)) (default "white")
  -b, --background string         background text color.
                                  color types are same as "foreground" option (default "black")
  -f, --fontfile string           font file path.
                                  You can change this default value with environment variables TEXTIMG_FONT_FILE
  -x, --fontindex int             
  -e, --emoji-fontfile string     emoji font file
  -X, --emoji-fontindex int       
  -i, --use-emoji-font            use emoji font
  -z, --shellgei-emoji-fontfile   emoji font file for shellgei-bot (path: "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf")
  -F, --fontsize int              font size (default 20)
  -o, --out string                output image file path.
                                  available image formats are [png | jpg | gif]
  -t, --timestamp                 add time stamp to output image file path.
  -n, --numbered                  add number-suffix to filename when the output file was existed.
                                  ex: t_2.png
  -s, --shellgei-imagedir         image directory path for shellgei-bot (path: "/images/t.png")
  -a, --animation                 generate animation gif
  -d, --delay int                 animation delay time (default 20)
  -l, --line-count int            animation input line count (default 1)
  -S, --slide                     use slide animation
  -W, --slide-width int           sliding animation width (default 1)
  -E, --forever                   sliding forever
      --environments              print environment variables
      --slack                     resize to slack icon size (128x128 px)
  -h, --help                      help for textimg
  -v, --version                   version for textimg
```

## Fonts

### Default font path

Default fonts that to use are below.

|OS     |Font path |
|-------|----------|
|Linux  |/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc |
|Linux  |/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc |
|MacOS  |/System/Library/Fonts/AppleSDGothicNeo.ttc |
|iOS    |/System/Library/Fonts/Core/AppleSDGothicNeo.ttc |
|Android|/system/fonts/NotoSansCJK-Regular.ttc |
|Windows|C:\Windows\Fonts\msgothic.ttc |

You can change this font path with environment variables `TEXTIMG_FONT_FILE` .

Examples.

```bash
export TEXTIMG_FONT_FILE=/usr/share/fonts/TTF/HackGen-Regular.ttf
```

### Emoji font (image file path)

textimg needs emoji image files to draw emoji.
You have to set `TEXTIMG_EMOJI_DIR` environment variables if you want to draw
one.
For example, run below.

```bash
# You can clone your favorite fonts here.
sudo git clone https://github.com/googlefonts/noto-emoji /usr/local/src/noto-emoji
export TEXTIMG_EMOJI_DIR=/usr/local/src/noto-emoji/png/128
export LANG=ja_JP.UTF-8
echo Test👍 | textimg -o emoji.png
```

![Emoji example](docs/emoji.png)

### Emoji font (TTF)

textimg can change emoji font with `TEXTIMG_EMOJI_FONT_FILE` environment variables and set `-i` option.
For example, switching emoji font to [Symbola font](https://www.wfonts.com/font/symbola).

```bash
export TEXTIMG_EMOJI_FONT_FILE=/usr/share/fonts/TTF/Symbola.ttf
echo あ😃a👍!👀ん👄 | textimg -i -o emoji_symbola.png
```

![Symbola emoji example](docs/emoji_symbola.png)

## Tab Completions

You can use TAB completions on your shell.

### Bash

Run below.

```bash
sudo cp -p completions/bash/textimg /usr/share/bash-completion/completions/textimg
```

### Zsh

Run below.

```bash
sudo cp -p completions/zsh/textimg /usr/share/zsh/functions/Completion/_textimg

# or
# sudo cp -p completions/zsh/textimg {path to your $fpath}
```

### Fish

Run below.

```bash
ln -sfn completions/fish/textimg.fish $HOME/.config/fish/completions/textimg.fish
```

## Development

go version go1.17 linux/amd64

### How to build

You run below.

```bash
make setup-tools
make
```

**I didn't test on Windows.**

### How to test

```bash
make test

# docker
make docker-build
make docker-test
```

## See also

* <https://misc.flogisoft.com/bash/tip_colors_and_formatting>



================================================
FILE: color/color.go
================================================
package color

import (
	c "image/color"
)

type RGBA c.RGBA

var (
	RGBABlack        = RGBA{0, 0, 0, 255}
	RGBARed          = RGBA{255, 0, 0, 255}
	RGBAGreen        = RGBA{0, 255, 0, 255}
	RGBAYellow       = RGBA{255, 255, 0, 255}
	RGBABlue         = RGBA{0, 0, 255, 255}
	RGBAMagenta      = RGBA{255, 0, 255, 255}
	RGBACyan         = RGBA{0, 255, 255, 255}
	RGBALightGray    = RGBA{211, 211, 211, 255}
	RGBADarkGray     = RGBA{169, 169, 169, 255}
	RGBALightRed     = RGBA{255, 144, 144, 255}
	RGBALightGreen   = RGBA{144, 238, 144, 255}
	RGBALightYellow  = RGBA{255, 255, 224, 255}
	RGBALightBlue    = RGBA{173, 216, 230, 255}
	RGBALightMagenta = RGBA{255, 224, 255, 255}
	RGBALightCyan    = RGBA{224, 255, 255, 255}
	RGBAWhite        = RGBA{255, 255, 255, 255}

	StringMap = map[string]RGBA{
		"black":   RGBABlack,
		"red":     RGBARed,
		"green":   RGBAGreen,
		"yellow":  RGBAYellow,
		"blue":    RGBABlue,
		"magenta": RGBAMagenta,
		"cyan":    RGBACyan,
		"white":   RGBAWhite,
	}

	// \x1b[NNm とかの NN に紐づくRGBA色
	// 例: \x1b[30m
	ANSIMap = map[int]RGBA{
		// 文字色
		30: RGBABlack,
		31: RGBARed,
		32: RGBAGreen,
		33: RGBAYellow,
		34: RGBABlue,
		35: RGBAMagenta,
		36: RGBACyan,
		37: RGBALightGray,
		90: RGBADarkGray,
		91: RGBALightRed,
		92: RGBALightGreen,
		93: RGBALightYellow,
		94: RGBALightBlue,
		95: RGBALightMagenta,
		96: RGBALightCyan,
		97: RGBAWhite,
		// 背景色
		40:  RGBABlack,
		41:  RGBARed,
		42:  RGBAGreen,
		43:  RGBAYellow,
		44:  RGBABlue,
		45:  RGBAMagenta,
		46:  RGBACyan,
		47:  RGBALightGray,
		100: RGBADarkGray,
		101: RGBALightRed,
		102: RGBALightGreen,
		103: RGBALightYellow,
		104: RGBALightBlue,
		105: RGBALightMagenta,
		106: RGBALightCyan,
		107: RGBAWhite,
	}

	// \x1b[38;5;NNNm とかの NNN に紐づくRGBA色
	// 例: \x1b[38;5;114m
	Map256 = map[int]RGBA{
		0:   {0, 0, 0, 255},
		1:   {128, 0, 0, 255},
		2:   {0, 128, 0, 255},
		3:   {128, 128, 0, 255},
		4:   {0, 0, 128, 255},
		5:   {128, 0, 128, 255},
		6:   {0, 128, 128, 255},
		7:   {192, 192, 192, 255},
		8:   {128, 128, 128, 255},
		9:   {255, 0, 0, 255},
		10:  {0, 255, 0, 255},
		11:  {255, 255, 0, 255},
		12:  {0, 0, 255, 255},
		13:  {255, 0, 255, 255},
		14:  {0, 255, 255, 255},
		15:  {255, 255, 255, 255},
		16:  {0, 0, 0, 255},
		17:  {0, 0, 95, 255},
		18:  {0, 0, 135, 255},
		19:  {0, 0, 175, 255},
		20:  {0, 0, 215, 255},
		21:  {0, 0, 255, 255},
		22:  {0, 95, 0, 255},
		23:  {0, 95, 95, 255},
		24:  {0, 95, 135, 255},
		25:  {0, 95, 175, 255},
		26:  {0, 95, 215, 255},
		27:  {0, 95, 255, 255},
		28:  {0, 135, 0, 255},
		29:  {0, 135, 95, 255},
		30:  {0, 135, 135, 255},
		31:  {0, 135, 175, 255},
		32:  {0, 135, 215, 255},
		33:  {0, 135, 255, 255},
		34:  {0, 175, 0, 255},
		35:  {0, 175, 95, 255},
		36:  {0, 175, 135, 255},
		37:  {0, 175, 175, 255},
		38:  {0, 175, 215, 255},
		39:  {0, 175, 255, 255},
		40:  {0, 215, 0, 255},
		41:  {0, 215, 95, 255},
		42:  {0, 215, 135, 255},
		43:  {0, 215, 175, 255},
		44:  {0, 215, 215, 255},
		45:  {0, 215, 255, 255},
		46:  {0, 255, 0, 255},
		47:  {0, 255, 95, 255},
		48:  {0, 255, 135, 255},
		49:  {0, 255, 175, 255},
		50:  {0, 255, 215, 255},
		51:  {0, 255, 255, 255},
		52:  {95, 0, 0, 255},
		53:  {95, 0, 95, 255},
		54:  {95, 0, 135, 255},
		55:  {95, 0, 175, 255},
		56:  {95, 0, 215, 255},
		57:  {95, 0, 255, 255},
		58:  {95, 95, 0, 255},
		59:  {95, 95, 95, 255},
		60:  {95, 95, 135, 255},
		61:  {95, 95, 175, 255},
		62:  {95, 95, 215, 255},
		63:  {95, 95, 255, 255},
		64:  {95, 135, 0, 255},
		65:  {95, 135, 95, 255},
		66:  {95, 135, 135, 255},
		67:  {95, 135, 175, 255},
		68:  {95, 135, 215, 255},
		69:  {95, 135, 255, 255},
		70:  {95, 175, 0, 255},
		71:  {95, 175, 95, 255},
		72:  {95, 175, 135, 255},
		73:  {95, 175, 175, 255},
		74:  {95, 175, 215, 255},
		75:  {95, 175, 255, 255},
		76:  {95, 215, 0, 255},
		77:  {95, 215, 95, 255},
		78:  {95, 215, 135, 255},
		79:  {95, 215, 175, 255},
		80:  {95, 215, 215, 255},
		81:  {95, 215, 255, 255},
		82:  {95, 255, 0, 255},
		83:  {95, 255, 95, 255},
		84:  {95, 255, 135, 255},
		85:  {95, 255, 175, 255},
		86:  {95, 255, 215, 255},
		87:  {95, 255, 255, 255},
		88:  {135, 0, 0, 255},
		89:  {135, 0, 95, 255},
		90:  {135, 0, 135, 255},
		91:  {135, 0, 175, 255},
		92:  {135, 0, 215, 255},
		93:  {135, 0, 255, 255},
		94:  {135, 95, 0, 255},
		95:  {135, 95, 95, 255},
		96:  {135, 95, 135, 255},
		97:  {135, 95, 175, 255},
		98:  {135, 95, 215, 255},
		99:  {135, 95, 255, 255},
		100: {135, 135, 0, 255},
		101: {135, 135, 95, 255},
		102: {135, 135, 135, 255},
		103: {135, 135, 175, 255},
		104: {135, 135, 215, 255},
		105: {135, 135, 255, 255},
		106: {135, 175, 0, 255},
		107: {135, 175, 95, 255},
		108: {135, 175, 135, 255},
		109: {135, 175, 175, 255},
		110: {135, 175, 215, 255},
		111: {135, 175, 255, 255},
		112: {135, 215, 0, 255},
		113: {135, 215, 95, 255},
		114: {135, 215, 135, 255},
		115: {135, 215, 175, 255},
		116: {135, 215, 215, 255},
		117: {135, 215, 255, 255},
		118: {135, 255, 0, 255},
		119: {135, 255, 95, 255},
		120: {135, 255, 135, 255},
		121: {135, 255, 175, 255},
		122: {135, 255, 215, 255},
		123: {135, 255, 255, 255},
		124: {175, 0, 0, 255},
		125: {175, 0, 95, 255},
		126: {175, 0, 135, 255},
		127: {175, 0, 175, 255},
		128: {175, 0, 215, 255},
		129: {175, 0, 255, 255},
		130: {175, 95, 0, 255},
		131: {175, 95, 95, 255},
		132: {175, 95, 135, 255},
		133: {175, 95, 175, 255},
		134: {175, 95, 215, 255},
		135: {175, 95, 255, 255},
		136: {175, 135, 0, 255},
		137: {175, 135, 95, 255},
		138: {175, 135, 135, 255},
		139: {175, 135, 175, 255},
		140: {175, 135, 215, 255},
		141: {175, 135, 255, 255},
		142: {175, 175, 0, 255},
		143: {175, 175, 95, 255},
		144: {175, 175, 135, 255},
		145: {175, 175, 175, 255},
		146: {175, 175, 215, 255},
		147: {175, 175, 255, 255},
		148: {175, 215, 0, 255},
		149: {175, 215, 95, 255},
		150: {175, 215, 135, 255},
		151: {175, 215, 175, 255},
		152: {175, 215, 215, 255},
		153: {175, 215, 255, 255},
		154: {175, 255, 0, 255},
		155: {175, 255, 95, 255},
		156: {175, 255, 135, 255},
		157: {175, 255, 175, 255},
		158: {175, 255, 215, 255},
		159: {175, 255, 255, 255},
		160: {215, 0, 0, 255},
		161: {215, 0, 95, 255},
		162: {215, 0, 135, 255},
		163: {215, 0, 175, 255},
		164: {215, 0, 215, 255},
		165: {215, 0, 255, 255},
		166: {215, 95, 0, 255},
		167: {215, 95, 95, 255},
		168: {215, 95, 135, 255},
		169: {215, 95, 175, 255},
		170: {215, 95, 215, 255},
		171: {215, 95, 255, 255},
		172: {215, 135, 0, 255},
		173: {215, 135, 95, 255},
		174: {215, 135, 135, 255},
		175: {215, 135, 175, 255},
		176: {215, 135, 215, 255},
		177: {215, 135, 255, 255},
		178: {215, 175, 0, 255},
		179: {215, 175, 95, 255},
		180: {215, 175, 135, 255},
		181: {215, 175, 175, 255},
		182: {215, 175, 215, 255},
		183: {215, 175, 255, 255},
		184: {215, 215, 0, 255},
		185: {215, 215, 95, 255},
		186: {215, 215, 135, 255},
		187: {215, 215, 175, 255},
		188: {215, 215, 215, 255},
		189: {215, 215, 255, 255},
		190: {215, 255, 0, 255},
		191: {215, 255, 95, 255},
		192: {215, 255, 135, 255},
		193: {215, 255, 175, 255},
		194: {215, 255, 215, 255},
		195: {215, 255, 255, 255},
		196: {255, 0, 0, 255},
		197: {255, 0, 95, 255},
		198: {255, 0, 135, 255},
		199: {255, 0, 175, 255},
		200: {255, 0, 215, 255},
		201: {255, 0, 255, 255},
		202: {255, 95, 0, 255},
		203: {255, 95, 95, 255},
		204: {255, 95, 135, 255},
		205: {255, 95, 175, 255},
		206: {255, 95, 215, 255},
		207: {255, 95, 255, 255},
		208: {255, 135, 0, 255},
		209: {255, 135, 95, 255},
		210: {255, 135, 135, 255},
		211: {255, 135, 175, 255},
		212: {255, 135, 215, 255},
		213: {255, 135, 255, 255},
		214: {255, 175, 0, 255},
		215: {255, 175, 95, 255},
		216: {255, 175, 135, 255},
		217: {255, 175, 175, 255},
		218: {255, 175, 215, 255},
		219: {255, 175, 255, 255},
		220: {255, 215, 0, 255},
		221: {255, 215, 95, 255},
		222: {255, 215, 135, 255},
		223: {255, 215, 175, 255},
		224: {255, 215, 215, 255},
		225: {255, 215, 255, 255},
		226: {255, 255, 0, 255},
		227: {255, 255, 95, 255},
		228: {255, 255, 135, 255},
		229: {255, 255, 175, 255},
		230: {255, 255, 215, 255},
		231: {255, 255, 255, 255},
		232: {8, 8, 8, 255},
		233: {18, 18, 18, 255},
		234: {28, 28, 28, 255},
		235: {38, 38, 38, 255},
		236: {48, 48, 48, 255},
		237: {58, 58, 58, 255},
		238: {68, 68, 68, 255},
		239: {78, 78, 78, 255},
		240: {88, 88, 88, 255},
		241: {98, 98, 98, 255},
		242: {108, 108, 108, 255},
		243: {118, 118, 118, 255},
		244: {128, 128, 128, 255},
		245: {138, 138, 138, 255},
		246: {148, 148, 148, 255},
		247: {158, 158, 158, 255},
		248: {168, 168, 168, 255},
		249: {178, 178, 178, 255},
		250: {188, 188, 188, 255},
		251: {198, 198, 198, 255},
		252: {208, 208, 208, 255},
		253: {218, 218, 218, 255},
		254: {228, 228, 228, 255},
		255: {238, 238, 238, 255},
	}
)


================================================
FILE: completions/fish/textimg.fish
================================================
complete -c textimg -x

complete -c textimg -s g -l foreground -a 'black red green yellow blue magenta cyan white' -d 'foreground text color.'
complete -c textimg -s b -l background -a 'black red green yellow blue magenta cyan white' -d 'background text color.'
complete -c textimg -s f -l fontfile -d 'font file path.'
complete -c textimg -s x -l fontindex
complete -c textimg -s e -l emoji-fontfile -d 'emoji font file.'
complete -c textimg -s X -l emoji-fontindex
complete -c textimg -s i -l use-emoji-font -d 'use emoji font'
complete -c textimg -s z -l shellgei-emoji-fontfile -d 'emoji font file for shellgei-bot'
complete -c textimg -s F -l fontdize
complete -c textimg -s o -l out -d 'output image file path.'
complete -c textimg -s t -l timestamp -d 'add time stamp to output image file path.'
complete -c textimg -s n -l numbered -d 'add number-suffix to filename when the output file was existed.'
complete -c textimg -s s -l shellgei-imagedir -d 'image directory path'
complete -c textimg -s a -l animation -d 'generate animation gif'
complete -c textimg -s d -l delay -d 'animation delay time (default 20)'
complete -c textimg -s l -l line-count -d 'animation input line count (default 1)'
complete -c textimg -s S -l slide -d 'use slide animation'
complete -c textimg -s W -l slide-width -d 'sliding animation width (default 1)'
complete -c textimg -s E -l forever -d 'sliding forever'
complete -c textimg      -l environments -d 'print environment variables'
complete -c textimg      -l slack -d 'resize to slack icon size (128x128 px)'
complete -c textimg -s h -l help -d 'help for textimg'
complete -c textimg -s v -l version -d 'version for textimg'


================================================
FILE: completions/zsh/_textimg
================================================
#compdef textimg

_textimg() {
  _arguments \
    {-g,--foreground}'[foreground text color]: :->color' \
    {-b,--background}'[background text color]: :->color' \
    {-f,--fontfile}'[font file path]: :->etc' \
    {-x,--fontindex}': :->etc' \
    {-e,--emoji-fontfile}'[emoji font file]: :->etc' \
    {-X,--emoji-fontindex}': :->etc' \
    {-i,--use-emoji-font}': :->etc' \
    {-z,--shellgei-emoji-fontfile}'[emoji font file for shellgei-bot]: :->etc' \
    {-F,--fontsize}'[font size (default 20)]: :->etc' \
    {-o,--out}'[output image file path. available image formats are png or jpg or gif]: :->etc' \
    {-t,--timestamp}'[add time stamp to output image file path.]: :->etc' \
    {-n,--numbered}'[add number-suffix to filename when the output file was existed.]: :->etc' \
    {-s,--shellgei-imagedir}'[image directory path]: :->etc' \
    {-a,--animation}'[generate animation gif]: :->etc' \
    {-d,--delay}'[animation delay time (default 20)]: :->etc' \
    {-l,--line-count}'[animation input line count (default 1)]: :->etc' \
    {-S,--slide}'[use slide animation]: :->etc' \
    {-W,--slide-width}'[sliding animation width (default 1)]: :->etc' \
    {-E,--forever}'[sliding forever]: :->etc' \
    --environments'[print environment variables]: :->etc' \
    --slack'[resize toslack icon size (128x128x px)]: :->etc' \
    {-h,--help}'[help for textimg]: :->etc' \
    {-v,--version}'[version for textimg]: :->etc'

  case "$state" in
    color)
      _values \
        'color' \
        black red green yellow blue magenta cyan white
      ;;
    etc)
      # nothing to do
      ;;
  esac
}

compdef _textimg textimg

# vim: ft=zsh


================================================
FILE: config/config.go
================================================
package config

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/jiro4989/textimg/v3/color"
	"github.com/jiro4989/textimg/v3/log"
	"golang.org/x/image/font"
	"golang.org/x/term"
)

type Config struct {
	Foreground               string // 文字色
	Background               string // 背景色
	Outpath                  string // 画像の出力ファイルパス
	AddTimeStamp             bool   // ファイル名末尾にタイムスタンプ付与
	SaveNumberedFile         bool   // 保存しようとしたファイルがすでに存在する場合に連番を付与する
	FontFile                 string // フォントファイルのパス
	FontIndex                int    // フォントコレクションのインデックス
	EmojiFontFile            string // 絵文字用のフォントファイルのパス
	EmojiFontIndex           int    // 絵文字用のフォントコレクションのインデックス
	UseEmojiFont             bool   // 絵文字TTFを使う
	FontSize                 int    // フォントサイズ
	UseAnimation             bool   // アニメーションGIFを生成する
	Delay                    int    // アニメーションのディレイ時間
	LineCount                int    // 入力データのうち何行を1フレーム画像に使うか
	UseSlideAnimation        bool   // スライドアニメーションする
	SlideWidth               int    // スライドする幅
	SlideForever             bool   // スライドを無限にスライドするように描画する
	ToSlackIcon              bool   // Slackのアイコンサイズにする
	PrintEnvironments        bool
	UseShellgeiImagedir      bool
	UseShellgeiEmojiFontfile bool
	ResizeWidth              int // 画像の横幅
	ResizeHeight             int // 画像の縦幅

	ForegroundColor color.RGBA // 文字色
	BackgroundColor color.RGBA // 背景色
	Texts           []string
	FileExtension   string
	Writer          io.WriteCloser
	FontFace        font.Face
	EmojiFontFace   font.Face
	EmojiDir        string
}

type osDefaultFont struct {
	fontFile  string
	fontIndex int
	isLinux   bool
}

const ShellgeiEmojiFontPath = "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf"

const (
	defaultWindowsFont = `C:\Windows\Fonts\msgothic.ttc`
	defaultDarwinFont  = "/System/Library/Fonts/AppleSDGothicNeo.ttc"
	defaultIOSFont     = "/System/Library/Fonts/Core/AppleSDGothicNeo.ttc"
	defaultAndroidFont = "/system/fonts/NotoSansCJK-Regular.ttc"
	defaultLinuxFont1  = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
	defaultLinuxFont2  = "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"
)

// adjust はパラメータを調整する。
// 副作用を持つ。
func (a *Config) Adjust(args []string, ev EnvVars) error {
	a.EmojiDir = ev.EmojiDir

	// シェル芸イメージディレクトリの指定がある時はパスを変更する
	if a.UseShellgeiImagedir {
		var err error
		outDir := ev.OutputDir
		a.Outpath, err = outputImageDir(outDir, a.UseAnimation)
		if err != nil {
			return err
		}
	}

	a.addTimeStampToOutPath(time.Now())
	a.addNumberSuffixToOutPath()

	if a.UseShellgeiEmojiFontfile {
		a.EmojiFontFile = ShellgeiEmojiFontPath
		a.UseEmojiFont = true
	}

	if a.UseSlideAnimation {
		a.UseAnimation = true
	}

	var err error
	a.ForegroundColor, err = optionColorStringToRGBA(a.Foreground)
	if err != nil {
		return err
	}

	a.BackgroundColor, err = optionColorStringToRGBA(a.Background)
	if err != nil {
		return err
	}

	// 引数にテキストの指定がなければ標準入力を使用する
	a.Texts = readInputText(args)

	// textsが空のときは警告メッセージを出力して異常終了
	if err := validateInputText(a.Texts); err != nil {
		return err
	}

	// スライドアニメーションを使うときはテキストを加工する
	if a.UseSlideAnimation {
		a.Texts = toSlideStrings(a.Texts, a.LineCount, a.SlideWidth, a.SlideForever)
	}

	// 拡張子のみ取得
	a.FileExtension = filepath.Ext(strings.ToLower(a.Outpath))

	if err := a.setWriter(); err != nil {
		return err
	}

	if err := validateFileExtension(a.FileExtension); err != nil {
		return err
	}

	a.Texts = normalizeTexts(a.Texts)

	a.FontFace, err = readFace(a.FontFile, a.FontIndex, float64(a.FontSize))
	if err != nil {
		return err
	}

	if a.EmojiFontFile != "" {
		a.EmojiFontFace, err = readFace(a.EmojiFontFile, a.EmojiFontIndex, float64(a.FontSize))
		if err != nil {
			return err
		}
	}

	if a.ToSlackIcon {
		a.ResizeWidth = 128
		a.ResizeHeight = 128
	}

	return nil
}

func (a *Config) SetFontFileAndFontIndex(runtimeOS string) {
	if a.FontFile != "" {
		return
	}

	m := map[string]osDefaultFont{
		"linux": {
			isLinux: true,
		},
		"windows": {
			fontFile:  defaultWindowsFont,
			fontIndex: 0,
		},
		"darwin": {
			fontFile:  defaultDarwinFont,
			fontIndex: 0,
		},
		"ios": {
			fontFile:  defaultIOSFont,
			fontIndex: 0,
		},
		"android": {
			fontFile:  defaultAndroidFont,
			fontIndex: 5,
		},
	}

	if f, ok := m[runtimeOS]; ok {
		// linux だけ特殊なので特別に分岐
		if !f.isLinux {
			a.FontFile = f.fontFile
			a.FontIndex = f.fontIndex
			return
		}

		if _, err := os.Stat("/proc/sys/fs/binfmt_misc/WSLInterop"); err == nil {
			a.FontFile = "/mnt/c/Windows/Fonts/msgothic.ttc"
			a.FontIndex = 0
			return
		}

		a.FontFile = defaultLinuxFont1
		if _, err := os.Stat(a.FontFile); err != nil {
			a.FontFile = defaultLinuxFont2
		}
		a.FontIndex = 5
		return
	}
}

// addTimeStampToOutPath はOutpathに指定日時のタイムスタンプを付与する。
func (a *Config) addTimeStampToOutPath(t time.Time) {
	if !a.AddTimeStamp {
		return
	}

	ext := filepath.Ext(a.Outpath)
	file := strings.TrimSuffix(a.Outpath, ext)
	timestamp := t.Format("2006-01-02-150405")
	a.Outpath = file + "_" + timestamp + ext
}

// addTimeStampToOutPath はOutpathに指定日時のタイムスタンプを付与する。
func (a *Config) addNumberSuffixToOutPath() {
	if !a.SaveNumberedFile {
		return
	}

	// ファイルが存在しない時は何もしない
	// NOTE: 並列に実行されるとチェックしきれない場合があるけれど許容する
	if _, err := os.Stat(a.Outpath); err != nil {
		return
	}

	fileExt := filepath.Ext(a.Outpath)
	fileName := strings.TrimSuffix(a.Outpath, fileExt)
	i := 2
	for {
		a.Outpath = fmt.Sprintf("%s_%d%s", fileName, i, fileExt)
		_, err := os.Stat(a.Outpath)
		if err != nil {
			return
		}
		i++
	}
}

func (a *Config) setWriter() error {
	if a.Outpath == "" {
		// 出力先画像の指定がなく、且つ出力先がパイプならstdout + PNG/GIFと
		// して出力。なければそもそも画像処理しても意味が無いので終了
		fd := os.Stdout.Fd()
		if term.IsTerminal(int(fd)) {
			log.Error("image data not written to a terminal. use -o, -s, pipe or redirect.")
			log.Error("for help, type: textimg -h")
			return fmt.Errorf("no output target error")
		}
		a.Writer = os.Stdout
		if a.UseAnimation {
			a.FileExtension = ".gif"
		} else {
			a.FileExtension = ".png"
		}

		return nil
	}

	if a.Writer != nil {
		return nil
	}

	var err error
	a.Writer, err = os.Create(a.Outpath)
	if err != nil {
		return err
	}
	// NOTE: writerは呼び出し元でクローズする

	return nil
}

func validateInputText(texts []string) error {
	var emptyCount int
	for _, v := range texts {
		if len(v) < 1 {
			emptyCount++
		}
	}
	if emptyCount == len(texts) {
		err := fmt.Errorf("must need input texts.")
		return err
	}
	return nil
}

// validateFileExtension はファイル拡張子をチェックする。
func validateFileExtension(ext string) error {
	switch ext {
	case ".png", ".jpg", ".jpeg", ".gif":
		// 何もしない
	default:
		err := fmt.Errorf("%s is not supported extension.", ext)
		return err
	}
	return nil
}

// normalizeTexts はテキストを正規化する。
func normalizeTexts(texts []string) []string {
	result := texts

	// タブ文字は画像描画時に表示されないので暫定対応で半角スペースに置換
	for i, text := range result {
		result[i] = strings.Replace(text, "\t", "  ", -1)
	}

	// ゼロ幅文字を削除
	for i, text := range result {
		result[i] = removeZeroWidthCharacters(text)
	}

	return result
}

func readInputText(args []string) []string {
	var texts []string
	if len(args) < 1 {
		texts = readStdin()
	} else {
		for _, v := range args {
			texts = append(texts, strings.Split(v, "\n")...)
		}
	}
	return texts
}

// outputImageDir は `-s` オプションで保存するさきのディレクトリパスを返す。
func outputImageDir(outDir string, useAnimation bool) (string, error) {
	if outDir == "" {
		homeDir, err := os.UserHomeDir()
		if err != nil {
			return "", err
		}
		outDir = filepath.Join(homeDir, "Pictures")
	}

	if useAnimation {
		return filepath.Join(outDir, "t.gif"), nil
	}

	return filepath.Join(outDir, "t.png"), nil
}

// オプション引数のbackgroundは2つの書き方を許容する。
//  1. black といった色の直接指定
//  2. RGBAのカンマ区切り指定
//     書式: R,G,B,A
//     赤色の例: 255,0,0,255
func optionColorStringToRGBA(colstr string) (color.RGBA, error) {
	// "black"といった色名称でマッチするものがあれば返す
	colstr = strings.ToLower(colstr)
	col := color.StringMap[colstr]
	zeroColor := color.RGBA{}
	if col != zeroColor {
		return col, nil
	}

	// カンマ区切りでの指定があれば返す
	rgba := strings.Split(colstr, ",")
	if len(rgba) != 4 {
		return zeroColor, fmt.Errorf("illegal RGBA format: %s", colstr)
	}

	var (
		r   uint64
		g   uint64
		b   uint64
		a   uint64
		err error
		rs  = rgba[0]
		gs  = rgba[1]
		bs  = rgba[2]
		as  = rgba[3]
	)
	r, err = strconv.ParseUint(rs, 10, 8)
	if err != nil {
		return zeroColor, err
	}
	g, err = strconv.ParseUint(gs, 10, 8)
	if err != nil {
		return zeroColor, err
	}
	b, err = strconv.ParseUint(bs, 10, 8)
	if err != nil {
		return zeroColor, err
	}
	a, err = strconv.ParseUint(as, 10, 8)
	if err != nil {
		return zeroColor, err
	}
	c := color.RGBA{
		R: uint8(r),
		G: uint8(g),
		B: uint8(b),
		A: uint8(a),
	}
	return c, nil
}

// toSlideStrings は文字列をスライドアニメーション用の文字列に変換する。
func toSlideStrings(src []string, lineCount, slideWidth int, slideForever bool) (ret []string) {
	if 1 < slideWidth {
		var loopCount int
		for i := 0; i < len(src); i += slideWidth {
			loopCount++
		}
		for i := 0; i < (loopCount*slideWidth+1)-len(src); i++ {
			if !slideForever {
				src = append(src, "")
			}
		}
	}

	for i := 0; i < len(src); i += slideWidth {
		n := i + lineCount
		if len(src) < n {
			if slideForever {
				for j := i; j < n; j++ {
					m := j
					if len(src) <= m {
						m -= len(src)
					}
					line := src[m]
					ret = append(ret, line)
				}
				continue
			}
			return
		}
		// lineCountの数ずつ行を取得して戻り値に追加
		for j := i; j < n; j++ {
			line := src[j]
			ret = append(ret, line)
		}
	}
	return
}

// removeZeroWidthSpace はゼロ幅文字が存在したときに削除する。
//
// 参考
// * ゼロ幅スペース 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
func removeZeroWidthCharacters(s string) string {
	zwc := []rune{
		0x200b, // zero width space
		0x200c, // zero width joiner
		0x200d, // zero width joiner
		0xfeff, // zero width no-break-space
	}
	var ret []rune
chars:
	for _, v := range s {
		for _, c := range zwc {
			if v == c {
				continue chars
			}
		}
		ret = append(ret, v)
	}
	return string(ret)
}

// readStdin は標準入力を文字列の配列として返す。
func readStdin() (ret []string) {
	sc := bufio.NewScanner(os.Stdin)
	for sc.Scan() {
		line := sc.Text()
		ret = append(ret, line)
	}
	if err := sc.Err(); err != nil {
		panic(err)
	}
	return
}


================================================
FILE: config/config_test.go
================================================
package config

import (
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/jiro4989/textimg/v3/color"
	"github.com/stretchr/testify/assert"
)

func newDefaultConfig() Config {
	return Config{
		Foreground:               "white",
		Background:               "black",
		Outpath:                  "",
		AddTimeStamp:             false,
		SaveNumberedFile:         false,
		FontFile:                 "",
		FontIndex:                0,
		EmojiFontFile:            "",
		EmojiFontIndex:           0,
		UseEmojiFont:             false,
		FontSize:                 20,
		UseAnimation:             false,
		Delay:                    20,
		LineCount:                1,
		UseSlideAnimation:        false,
		SlideWidth:               1,
		SlideForever:             false,
		ToSlackIcon:              false,
		PrintEnvironments:        false,
		UseShellgeiImagedir:      false,
		UseShellgeiEmojiFontfile: false,
		ResizeWidth:              0,
		ResizeHeight:             0,
		Writer:                   NewMockWriter(false, false),
	}
}

func TestConfig_Adjust(t *testing.T) {
	tests := []struct {
		desc    string
		config  Config
		args    []string
		ev      EnvVars
		want    Config
		wantErr bool
	}{
		{
			desc: "正常系: Outpathが設定されている",
			config: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				return c
			}(),
			args: []string{"hello"},
			ev:   EnvVars{},
			want: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.ForegroundColor = color.RGBAWhite
				c.BackgroundColor = color.RGBABlack
				c.Texts = []string{"hello"}
				c.FileExtension = ".png"
				return c
			}(),
			wantErr: false,
		},
		{
			desc: "正常系: UseShellgeiImagedirが有効なときはt.pngが設定される",
			config: func() Config {
				c := newDefaultConfig()
				c.UseShellgeiImagedir = true
				return c
			}(),
			args: []string{"hello"},
			ev: EnvVars{
				OutputDir: "sushi",
			},
			want: func() Config {
				c := newDefaultConfig()
				c.Outpath = filepath.Join("sushi", "t.png")
				c.UseShellgeiImagedir = true
				c.ForegroundColor = color.RGBAWhite
				c.BackgroundColor = color.RGBABlack
				c.Texts = []string{"hello"}
				c.FileExtension = ".png"
				return c
			}(),
			wantErr: false,
		},
		{
			desc: "正常系: UseShellgeiEmojiFontfileが有効な時は組み込みの絵文字パスが設定されて、UseEmojiFont=trueになる",
			config: func() Config {
				c := newDefaultConfig()
				c.UseShellgeiImagedir = true
				c.UseShellgeiEmojiFontfile = true
				return c
			}(),
			args: []string{"hello"},
			ev: EnvVars{
				OutputDir: "sushi",
			},
			want: func() Config {
				c := newDefaultConfig()
				c.Outpath = filepath.Join("sushi", "t.png")
				c.UseShellgeiImagedir = true
				c.ForegroundColor = color.RGBAWhite
				c.BackgroundColor = color.RGBABlack
				c.Texts = []string{"hello"}
				c.FileExtension = ".png"
				c.UseShellgeiEmojiFontfile = true
				c.UseEmojiFont = true
				c.EmojiFontFile = ShellgeiEmojiFontPath
				return c
			}(),
			wantErr: false,
		},
		{
			desc: "正常系: UseShellgeiImagedirが有効でUseAnimationが設定されているときはt.gifになる",
			config: func() Config {
				c := newDefaultConfig()
				c.UseShellgeiImagedir = true
				c.UseAnimation = true
				return c
			}(),
			args: []string{"hello"},
			ev: EnvVars{
				OutputDir: "sushi",
			},
			want: func() Config {
				c := newDefaultConfig()
				c.Outpath = filepath.Join("sushi", "t.gif")
				c.UseShellgeiImagedir = true
				c.UseAnimation = true
				c.ForegroundColor = color.RGBAWhite
				c.BackgroundColor = color.RGBABlack
				c.Texts = []string{"hello"}
				c.FileExtension = ".gif"
				return c
			}(),
			wantErr: false,
		},
		{
			desc: "正常系: ToSlackIconが有効なときはResizeWidthとResizeHeightが設定される",
			config: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.ToSlackIcon = true
				return c
			}(),
			args: []string{"hello"},
			ev:   EnvVars{},
			want: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.ForegroundColor = color.RGBAWhite
				c.BackgroundColor = color.RGBABlack
				c.Texts = []string{"hello"}
				c.FileExtension = ".png"
				c.ToSlackIcon = true
				c.ResizeWidth = 128
				c.ResizeHeight = 128
				return c
			}(),
			wantErr: false,
		},
		{
			desc: "正常系: UseSlideAnimationが有効なときはUseAnimationも有効になる",
			config: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.UseSlideAnimation = true
				return c
			}(),
			args: []string{"hello"},
			ev:   EnvVars{},
			want: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.ForegroundColor = color.RGBAWhite
				c.BackgroundColor = color.RGBABlack
				c.Texts = []string{"hello"}
				c.FileExtension = ".png"
				c.UseSlideAnimation = true
				c.UseAnimation = true
				return c
			}(),
			wantErr: false,
		},
		{
			desc: "正常系: SlideWidthが2以上の時はテキストの処理が変化する",
			config: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.UseSlideAnimation = true
				c.LineCount = 2
				c.SlideWidth = 2
				c.SlideForever = true
				return c
			}(),
			args: []string{"hello", "hello", "hello", "hello"},
			ev:   EnvVars{},
			want: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.ForegroundColor = color.RGBAWhite
				c.BackgroundColor = color.RGBABlack
				c.Texts = []string{"hello", "hello", "hello", "hello"}
				c.FileExtension = ".png"
				c.UseSlideAnimation = true
				c.UseAnimation = true
				c.LineCount = 2
				c.SlideWidth = 2
				c.SlideForever = true
				return c
			}(),
			wantErr: false,
		},
		{
			desc: "正常系: EmojiFontFileに存在しないファイルを指定してもエラーにはならない",
			config: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.EmojiFontFile = "sushi.otf"
				return c
			}(),
			args: []string{"hello"},
			ev:   EnvVars{},
			want: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.ForegroundColor = color.RGBAWhite
				c.BackgroundColor = color.RGBABlack
				c.Texts = []string{"hello"}
				c.FileExtension = ".png"
				c.EmojiFontFile = "sushi.otf"
				return c
			}(),
			wantErr: false,
		},
		{
			desc: "異常系: Foregroundに不正な色指定をした時はエラーを返す",
			config: func() Config {
				c := newDefaultConfig()
				c.Foreground = "sushi"
				return c
			}(),
			args:    []string{"hello"},
			ev:      EnvVars{},
			want:    Config{},
			wantErr: true,
		},
		{
			desc: "異常系: Backgroundに不正な色指定をした時はエラーを返す",
			config: func() Config {
				c := newDefaultConfig()
				c.Background = "sushi"
				return c
			}(),
			args:    []string{"hello"},
			ev:      EnvVars{},
			want:    Config{},
			wantErr: true,
		},
		{
			desc: "異常系: textsが空の時はエラーを返す",
			config: func() Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				return c
			}(),
			args:    []string{},
			ev:      EnvVars{},
			want:    Config{},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)

			err := tt.config.Adjust(tt.args, tt.ev)
			if tt.wantErr {
				assert.Error(err)
				return
			}

			assert.NoError(err)
			assert.Equal(tt.want.Foreground, tt.config.Foreground)
			assert.Equal(tt.want.Background, tt.config.Background)
			assert.Equal(tt.want.Outpath, tt.config.Outpath)
			assert.Equal(tt.want.AddTimeStamp, tt.config.AddTimeStamp)
			assert.Equal(tt.want.SaveNumberedFile, tt.config.SaveNumberedFile)
			assert.Equal(tt.want.FontFile, tt.config.FontFile)
			assert.Equal(tt.want.FontIndex, tt.config.FontIndex)
			assert.Equal(tt.want.EmojiFontFile, tt.config.EmojiFontFile)
			assert.Equal(tt.want.EmojiFontIndex, tt.config.EmojiFontIndex)
			assert.Equal(tt.want.UseEmojiFont, tt.config.UseEmojiFont)
			assert.Equal(tt.want.FontSize, tt.config.FontSize)
			assert.Equal(tt.want.UseAnimation, tt.config.UseAnimation)
			assert.Equal(tt.want.Delay, tt.config.Delay)
			assert.Equal(tt.want.LineCount, tt.config.LineCount)
			assert.Equal(tt.want.UseSlideAnimation, tt.config.UseSlideAnimation)
			assert.Equal(tt.want.SlideWidth, tt.config.SlideWidth)
			assert.Equal(tt.want.SlideForever, tt.config.SlideForever)
			assert.Equal(tt.want.ToSlackIcon, tt.config.ToSlackIcon)
			assert.Equal(tt.want.PrintEnvironments, tt.config.PrintEnvironments)
			assert.Equal(tt.want.UseShellgeiImagedir, tt.config.UseShellgeiImagedir)
			assert.Equal(tt.want.UseShellgeiEmojiFontfile, tt.config.UseShellgeiEmojiFontfile)
			assert.Equal(tt.want.ForegroundColor, tt.config.ForegroundColor)
			assert.Equal(tt.want.BackgroundColor, tt.config.BackgroundColor)
			assert.Equal(tt.want.Texts, tt.config.Texts)
			assert.Equal(tt.want.FileExtension, tt.config.FileExtension)
			// NOTE: ここはテストするのが難しいので無視
			// assert.Equal(tt.want.Writer, tt.config.Writer)
			// assert.Equal(tt.want.FontFace, tt.config.FontFace)
			// assert.Equal(tt.want.EmojiFontFace, tt.config.EmojiFontFace)
			assert.Equal(tt.want.EmojiDir, tt.config.EmojiDir)
		})
	}
}

func TestOptionColorStringToRGBA(t *testing.T) {
	type TestData struct {
		desc   string
		colstr string
		expect color.RGBA
	}
	tds := []TestData{
		{desc: "BLACK", colstr: "BLACK", expect: color.RGBABlack},
		{desc: "black", colstr: "black", expect: color.RGBABlack},
		{desc: "red", colstr: "red", expect: color.RGBARed},
		{desc: "green", colstr: "green", expect: color.RGBAGreen},
		{desc: "yellow", colstr: "yellow", expect: color.RGBAYellow},
		{desc: "blue", colstr: "blue", expect: color.RGBABlue},
		{desc: "magenta", colstr: "magenta", expect: color.RGBAMagenta},
		{desc: "cyan", colstr: "cyan", expect: color.RGBACyan},
		{desc: "white", colstr: "white", expect: color.RGBAWhite},
		{desc: "0,0,0,255", colstr: "0,0,0,255", expect: color.RGBA{R: 0, G: 0, B: 0, A: 255}},
		{desc: "255,255,255,255", colstr: "255,255,255,255", expect: color.RGBA{R: 255, G: 255, B: 255, A: 255}},
		{desc: "0,0,0,0", colstr: "0,0,0,0", expect: color.RGBA{R: 0, G: 0, B: 0, A: 0}},
	}
	for _, v := range tds {
		t.Run(v.desc, func(t *testing.T) {
			got, err := optionColorStringToRGBA(v.colstr)
			assert.Nil(t, err, v.desc)
			assert.Equal(t, v.expect, got, v.desc)
		})
	}

	// 異常系
	tds = []TestData{
		{desc: "不正な色文字列", colstr: "unko"},
		{desc: "RGBAの書式不正(値の数不足)", colstr: "1,2,3"},
		{desc: "RGBAの書式不正(値の数過多)", colstr: "1,2,3,4,5"},
		{desc: "RGBAの書式不正(値がない)", colstr: "1,2,3,"},
		{desc: "RGBAの書式不正(値に文字が混じっている)", colstr: "1,2,3,a"},
		{desc: "RGBAの書式不正(255以上の値)", colstr: "1,2,3,256"},
		{desc: "RGBAの書式不正(負の値)", colstr: "-1,2,3,255"},
		{desc: "RGBAの書式不正(空文字)", colstr: ""},
	}
	for _, v := range tds {
		t.Run(v.desc, func(t *testing.T) {
			_, err := optionColorStringToRGBA(v.colstr)
			assert.NotNil(t, err, v.desc)
		})
	}
}

func TestToSlideStrings(t *testing.T) {
	type TestData struct {
		desc                  string
		src, expect           []string
		lineCount, slideWidth int
		slideForever          bool
	}
	tds := []TestData{
		{
			desc: "2行描画、スライド幅1、無限なし",
			src:  []string{"1", "2", "3", "4", "5"},
			expect: []string{
				"1", "2",
				"2", "3",
				"3", "4",
				"4", "5",
			},
			lineCount:    2,
			slideWidth:   1,
			slideForever: false,
		},
		{
			desc: "2行描画、スライド幅2、無限なし",
			src:  []string{"1", "2", "3", "4", "5"},
			expect: []string{
				"1", "2",
				"3", "4",
				"5", "",
			},
			lineCount:    2,
			slideWidth:   2,
			slideForever: false,
		},
		{
			desc: "3行描画、スライド幅1、無限なし",
			src:  []string{"1", "2", "3", "4", "5"},
			expect: []string{
				"1", "2", "3",
				"2", "3", "4",
				"3", "4", "5",
			},
			lineCount:    3,
			slideWidth:   1,
			slideForever: false,
		},
		{
			desc: "3行描画、スライド幅2、無限なし、不足あり",
			src:  []string{"1", "2", "3", "4", "5", "6"},
			expect: []string{
				"1", "2", "3",
				"3", "4", "5",
				"5", "6", "",
			},
			lineCount:    3,
			slideWidth:   2,
			slideForever: false,
		},
		{
			desc: "3行描画、スライド幅2、無限なし、不足なし",
			src:  []string{"1", "2", "3", "4", "5", "6", "7"},
			expect: []string{
				"1", "2", "3",
				"3", "4", "5",
				"5", "6", "7",
			},
			lineCount:    3,
			slideWidth:   2,
			slideForever: false,
		},
		{
			desc: "3行描画、スライド幅3、無限なし、不足なし",
			src:  []string{"1", "2", "3", "4", "5", "6"},
			expect: []string{
				"1", "2", "3",
				"4", "5", "6",
			},
			lineCount:    3,
			slideWidth:   3,
			slideForever: false,
		},
		{
			desc: "3行描画、スライド幅3、無限なし、不足あり",
			src:  []string{"1", "2", "3", "4", "5", "6", "7"},
			expect: []string{
				"1", "2", "3",
				"4", "5", "6",
				"7", "", "",
			},
			lineCount:    3,
			slideWidth:   3,
			slideForever: false,
		},
		{
			desc: "3行描画、スライド幅3、無限なし、不足あり",
			src:  []string{"1", "2", "3", "4", "5", "6", "7", "8"},
			expect: []string{
				"1", "2", "3",
				"4", "5", "6",
				"7", "8", "",
			},
			lineCount:    3,
			slideWidth:   3,
			slideForever: false,
		},
		{
			desc: "2行描画、スライド幅2、無限あり",
			src:  []string{"1", "2", "3", "4", "5"},
			expect: []string{
				"1", "2",
				"3", "4",
				"5", "1",
			},
			lineCount:    2,
			slideWidth:   2,
			slideForever: true,
		},
		{
			desc: "2行描画、スライド幅2、無限あり",
			src:  []string{"1", "2", "3", "4", "5", "6"},
			expect: []string{
				"1", "2",
				"3", "4",
				"5", "6",
			},
			lineCount:    2,
			slideWidth:   2,
			slideForever: true,
		},
		{
			desc: "3行描画、スライド幅1、無限あり",
			src:  []string{"1", "2", "3", "4", "5"},
			expect: []string{
				"1", "2", "3",
				"2", "3", "4",
				"3", "4", "5",
				"4", "5", "1",
				"5", "1", "2",
			},
			lineCount:    3,
			slideWidth:   1,
			slideForever: true,
		},
		{
			desc: "3行描画、スライド幅1、無限あり",
			src:  []string{"1", "2", "3", "4", "5", "6"},
			expect: []string{
				"1", "2", "3",
				"2", "3", "4",
				"3", "4", "5",
				"4", "5", "6",
				"5", "6", "1",
				"6", "1", "2",
			},
			lineCount:    3,
			slideWidth:   1,
			slideForever: true,
		},
		{
			desc: "3行描画、スライド幅2、無限あり",
			src:  []string{"1", "2", "3", "4", "5"},
			expect: []string{
				"1", "2", "3",
				"3", "4", "5",
				"5", "1", "2",
			},
			lineCount:    3,
			slideWidth:   2,
			slideForever: true,
		},
		{
			desc: "3行描画、スライド幅2、無限あり",
			src:  []string{"1", "2", "3", "4", "5", "6"},
			expect: []string{
				"1", "2", "3",
				"3", "4", "5",
				"5", "6", "1",
			},
			lineCount:    3,
			slideWidth:   2,
			slideForever: true,
		},
		{
			desc: "3行描画、スライド幅3、無限あり",
			src:  []string{"1", "2", "3", "4", "5", "6"},
			expect: []string{
				"1", "2", "3",
				"4", "5", "6",
			},
			lineCount:    3,
			slideWidth:   3,
			slideForever: true,
		},
		{
			desc: "3行描画、スライド幅3、無限あり",
			src:  []string{"1", "2", "3", "4", "5", "6", "7"},
			expect: []string{
				"1", "2", "3",
				"4", "5", "6",
				"7", "1", "2",
			},
			lineCount:    3,
			slideWidth:   3,
			slideForever: true,
		},
	}
	for _, v := range tds {
		t.Run(v.desc, func(t *testing.T) {
			got := toSlideStrings(v.src, v.lineCount, v.slideWidth, v.slideForever)
			assert.Equal(t, v.expect, got, v.desc)
		})
	}
}

func TestRemoveZeroWidthCharacters(t *testing.T) {
	type TestData struct {
		desc   string
		s      string
		expect string
	}
	tds := []TestData{
		{desc: "Zero width space (U+200B)が削除される", s: "A\u200bB", expect: "AB"},
		{desc: "Zero width joiner (U+200C)が削除される", s: "A\u200cB", expect: "AB"},
		{desc: "Zero width joiner (U+200D)が削除される", s: "A\u200dB", expect: "AB"},
		{desc: "U+200B ~ U+200Dが削除される", s: "あ\u200bい\u200cう\u200dえ", expect: "あいうえ"},
	}
	for _, v := range tds {
		t.Run(v.desc, func(t *testing.T) {
			got := removeZeroWidthCharacters(v.s)
			assert.Equal(t, v.expect, got, v.desc)
		})
	}
}

func TestApplicationConfigSetFontFileAndFontIndex(t *testing.T) {
	type TestData struct {
		desc          string
		inFontFile    string
		inFontIndex   int
		inRuntimeOS   string
		wantFontFile  string
		wantFontIndex int
	}
	tests := []TestData{
		{
			desc:          "正常系: FontFileが設定済みの場合は変更なし",
			inFontFile:    "/usr/share/fonts/寿司",
			inFontIndex:   0,
			inRuntimeOS:   "linux",
			wantFontFile:  "/usr/share/fonts/寿司",
			wantFontIndex: 0,
		},
		{
			desc:          "正常系: フォント未設定でwindowsの場合はwindows用のフォントが設定される",
			inRuntimeOS:   "windows",
			wantFontFile:  defaultWindowsFont,
			wantFontIndex: 0,
		},
		{
			desc:          "正常系: フォント未設定でdarwinの場合はdarwin用のフォントが設定される",
			inRuntimeOS:   "darwin",
			wantFontFile:  defaultDarwinFont,
			wantFontIndex: 0,
		},
		{
			desc:          "正常系: フォント未設定でiosの場合はios用のフォントが設定される",
			inRuntimeOS:   "ios",
			wantFontFile:  defaultIOSFont,
			wantFontIndex: 0,
		},
		{
			desc:          "正常系: フォント未設定でandroidの場合はandroid用のフォントが設定される",
			inRuntimeOS:   "android",
			wantFontFile:  defaultAndroidFont,
			wantFontIndex: 5,
		},
		// FIXME: ローカル環境で実行するとエラーになるので一旦無効化
		// {
		// 	desc:          "正常系: フォント未設定でlinuxの場合はlinux用のフォントが設定される。Linux用のフォントは2つ存在するが、1つ目のフォントはalpineコンテナ内にデフォルトでは存在しないため2つ目が設定される",
		// 	inRuntimeOS:   "linux",
		// 	wantFontFile:  defaultLinuxFont2,
		// 	wantFontIndex: 5,
		// },
	}
	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)

			a := Config{
				FontFile:  tt.inFontFile,
				FontIndex: tt.inFontIndex,
			}
			a.SetFontFileAndFontIndex(tt.inRuntimeOS)

			assert.Equal(tt.wantFontFile, a.FontFile)
			assert.Equal(tt.wantFontIndex, a.FontIndex)
		})
	}
}

func TestApplicationConfig_AddTimeStampToOutPath(t *testing.T) {
	type TestData struct {
		desc           string
		inOutpath      string
		inAddTimeStamp bool
		inTime         time.Time
		want           string
	}
	tests := []TestData{
		{
			desc:           "正常系: フラグfalseの場合は変更なし",
			inOutpath:      "t.png",
			inAddTimeStamp: false,
			inTime:         time.Date(2000, 1, 1, 12, 10, 5, 2, time.Local),
			want:           "t.png",
		},
		{
			desc:           "正常系: フラグtrueの場合はタイムスタンプがつく",
			inOutpath:      "t.png",
			inAddTimeStamp: true,
			inTime:         time.Date(2000, 1, 1, 12, 10, 5, 0, time.Local),
			want:           "t_2000-01-01-121005.png",
		},
		{
			desc:           "正常系: フルパスでも同様に動作する",
			inOutpath:      "/images/t.png",
			inAddTimeStamp: true,
			inTime:         time.Date(2000, 1, 1, 12, 10, 5, 0, time.Local),
			want:           "/images/t_2000-01-01-121005.png",
		},
		{
			desc:           "正常系: ファイル拡張子が多重についていても動作する",
			inOutpath:      "/images/t.png.1",
			inAddTimeStamp: true,
			inTime:         time.Date(2000, 1, 1, 12, 10, 5, 0, time.Local),
			want:           "/images/t.png_2000-01-01-121005.1",
		},
		{
			desc:           "正常系: Windowsのパス表現でも動作する",
			inOutpath:      `C:\Users\foobar\Pictures\t.png`,
			inAddTimeStamp: true,
			inTime:         time.Date(2000, 1, 1, 12, 10, 5, 0, time.Local),
			want:           `C:\Users\foobar\Pictures\t_2000-01-01-121005.png`,
		},
	}
	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)

			a := Config{
				Outpath:      tt.inOutpath,
				AddTimeStamp: tt.inAddTimeStamp,
			}
			a.addTimeStampToOutPath(tt.inTime)

			assert.Equal(tt.want, a.Outpath)
		})
	}
}

func TestOutputImageDir(t *testing.T) {
	home, err := os.UserHomeDir()
	assert.NoError(t, err)
	pictDir := filepath.Join(home, "Pictures")

	type TestData struct {
		desc           string
		inEnvDir       string
		inUseAnimation bool
		wantPath       string
		wantErr        bool
	}
	tests := []TestData{
		{
			desc:           "正常系: Env未設定の場合はホームディレクトリ配下のPictures配下が返る",
			inEnvDir:       "",
			inUseAnimation: false,
			wantPath:       filepath.Join(pictDir, "t.png"),
			wantErr:        false,
		},
		{
			desc:           "正常系: animation trueの場合は Basenameが t.gif になる",
			inEnvDir:       "",
			inUseAnimation: true,
			wantPath:       filepath.Join(pictDir, "t.gif"),
			wantErr:        false,
		},
		{
			desc:           "正常系: Envが設定されている場合は設定されている値が優先される",
			inEnvDir:       filepath.Join(".", "sushi"),
			inUseAnimation: false,
			wantPath:       filepath.Join(".", "sushi", "t.png"),
			wantErr:        false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)

			got, err := outputImageDir(tt.inEnvDir, tt.inUseAnimation)
			if tt.wantErr {
				assert.Equal("", got)
				assert.Error(err)
				return
			}

			assert.NoError(err)
			assert.Equal(tt.wantPath, got)
		})
	}
}

func TestApplicationConfig_AddNumberSuffixToOutPath(t *testing.T) {
	testdataDir := filepath.Join("..", "testdata", "in")

	existedFile := filepath.Join(testdataDir, "appconf_add_number_suffix_test_case1.png")
	existedFileWant := filepath.Join(testdataDir, "appconf_add_number_suffix_test_case1_2.png")

	notExistedFile := filepath.Join(testdataDir, "appconf_add_number_suffix_sushi.png")

	type TestData struct {
		desc               string
		inOutpath          string
		inSaveNumberedFile bool
		want               string
	}
	tests := []TestData{
		{
			desc:               "正常系: フラグfalseの場合は変更なし",
			inOutpath:          existedFile,
			inSaveNumberedFile: false,
			want:               existedFile,
		},
		{
			desc:               "正常系: フラグtrueの場合は連番を付与する",
			inOutpath:          existedFile,
			inSaveNumberedFile: true,
			want:               existedFileWant,
		},
		{
			desc:               "正常系: フラグtrueの場合でも、ファイルが存在しなければ何もしない",
			inOutpath:          notExistedFile,
			inSaveNumberedFile: true,
			want:               notExistedFile,
		},
	}
	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)

			a := Config{
				Outpath:          tt.inOutpath,
				SaveNumberedFile: tt.inSaveNumberedFile,
			}
			a.addNumberSuffixToOutPath()

			assert.Equal(tt.want, a.Outpath)
		})
	}
}


================================================
FILE: config/envvar.go
================================================
package config

import (
	"fmt"
	"os"
)

type EnvVars struct {
	EmojiDir      string
	OutputDir     string
	FontFile      string
	EmojiFontFile string
}

const (
	envNameOutputDir     = "TEXTIMG_OUTPUT_DIR"
	envNameFontFile      = "TEXTIMG_FONT_FILE"
	envNameEmojiDir      = "TEXTIMG_EMOJI_DIR"
	envNameEmojiFontFile = "TEXTIMG_EMOJI_FONT_FILE"
)

var (
	envs = map[string]string{
		envNameOutputDir:     os.Getenv(envNameOutputDir),
		envNameFontFile:      os.Getenv(envNameFontFile),
		envNameEmojiDir:      os.Getenv(envNameEmojiDir),
		envNameEmojiFontFile: os.Getenv(envNameEmojiFontFile),
	}
)

func NewEnvVars() EnvVars {
	return EnvVars{
		OutputDir:     envs[envNameOutputDir],
		FontFile:      envs[envNameFontFile],
		EmojiDir:      envs[envNameEmojiDir],
		EmojiFontFile: envs[envNameEmojiFontFile],
	}
}

func PrintEnvs() {
	for k, v := range envs {
		text := fmt.Sprintf("%s=%s", k, v)
		fmt.Println(text)
	}
}


================================================
FILE: config/face.go
================================================
package config

import (
	"os"
	"path/filepath"
	"strings"

	"github.com/jiro4989/textimg/v3/log"
	"golang.org/x/image/font"
	"golang.org/x/image/font/gofont/gomono"
	"golang.org/x/image/font/opentype"
)

// readFace はfontPathのフォントファイルからfaceを返す。
func readFace(fontPath string, fontIndex int, fontSize float64) (font.Face, error) {
	var ft *opentype.Font

	// ファイルが存在しなければビルトインのフォントをデフォルトとして使う
	_, err := os.Stat(fontPath)
	if err == nil {
		fontData, err := os.ReadFile(fontPath)
		if err != nil {
			return nil, err
		}
		switch strings.ToLower(filepath.Ext(fontPath)) {
		case ".otc", ".ttc":
			ftc, err := opentype.ParseCollection(fontData)
			if err != nil {
				return nil, err
			}
			ft, err = ftc.Font(fontIndex)
			if err != nil {
				return nil, err
			}
		default:
			ft, err = opentype.Parse(fontData)
			if err != nil {
				return nil, err
			}
		}
	} else {
		log.Warnf("%s is not found. please set font path with `-f` option", fontPath)
		ft, err = opentype.Parse(gomono.TTF)
		if err != nil {
			return nil, err
		}
	}

	opt := opentype.FaceOptions{
		Size:    fontSize,
		DPI:     72,
		Hinting: 0,
	}
	face, err := opentype.NewFace(ft, &opt)
	if err != nil {
		return nil, err
	}
	return face, nil
}


================================================
FILE: config/face_test.go
================================================
package config

import (
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestReadFace(t *testing.T) {
	testdataDir := filepath.Join("..", "testdata", "in")

	type TestData struct {
		desc        string
		inFontPath  string
		inFontIndex int
		wantErr     bool
	}
	tests := []TestData{
		{
			desc:        "正常系: font.Faceが取得できる",
			inFontPath:  "/tmp/MyricaM.TTC",
			inFontIndex: 0,
			wantErr:     false,
		},
		{
			desc:        "正常系: 存在しないファイルの場合もエラーにはならない",
			inFontPath:  "/tmp/寿司",
			inFontIndex: 0,
			wantErr:     false,
		},
		{
			desc:        "異常系: パスとしては存在するがディレクトリの場合はエラー",
			inFontPath:  "/tmp",
			inFontIndex: 0,
			wantErr:     true,
		},
		{
			desc:        "異常系: ファイルは存在するけれど、フォントファイルじゃない時はエラー (ttc)",
			inFontPath:  filepath.Join(testdataDir, "illegal_font.ttc"),
			inFontIndex: 0,
			wantErr:     true,
		},
		{
			desc:        "異常系: ファイルは存在するけれど、フォントファイルじゃない時はエラー (otc)",
			inFontPath:  filepath.Join(testdataDir, "illegal_font.otc"),
			inFontIndex: 0,
			wantErr:     true,
		},
		{
			desc:        "異常系: ファイルは存在するけれど、フォントファイルじゃない時はエラー (txt)",
			inFontPath:  filepath.Join(testdataDir, "illegal_font.txt"),
			inFontIndex: 0,
			wantErr:     true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)

			got, err := readFace(tt.inFontPath, tt.inFontIndex, 20)
			if tt.wantErr {
				assert.Nil(got)
				assert.Error(err)
				return
			}

			assert.NotNil(got)
			assert.NoError(err)
		})
	}
}


================================================
FILE: config/writer_mock.go
================================================
package config

import (
	"errors"
	"io"
)

type MockWriter struct {
	writeErr bool
	closeErr bool
}

func NewMockWriter(w, c bool) io.WriteCloser {
	return &MockWriter{
		writeErr: w,
		closeErr: c,
	}
}

func (m *MockWriter) Write(p []byte) (n int, err error) {
	if m.writeErr {
		return -1, errors.New("write error")
	}
	return 0, nil
}

func (m *MockWriter) Close() error {
	if m.closeErr {
		return errors.New("close error")
	}
	return nil
}


================================================
FILE: docker-compose.yml
================================================
---

version: '3.7'

services:
  base: &common
    build:
      context: ./
      dockerfile: ./Dockerfile
      target: base
    container_name: textimg_base
    image: jiro4989/textimg-base
    working_dir: /app
    volumes:
      - "$PWD:/app"
      - "gopkg:/go/pkg" # 名前付きボリュームで依存パッケージを永続化
      - "$PWD/images:/images"
    environment:
      TEXTIMG_FONT_FILE: /tmp/MyricaM.TTC
      TEXTIMG_EMOJI_DIR: /usr/local/src/noto-emoji/png/128
      TEXTIMG_EMOJI_FONT_FILE: /tmp/Symbola_hint.ttf

  textimg:
    build:
      context: ./
      dockerfile: ./Dockerfile
    container_name: textimg
    image: jiro4989/textimg
    volumes:
      - "$PWD/images:/images"

volumes:
  gopkg:


================================================
FILE: go.mod
================================================
module github.com/jiro4989/textimg/v3

go 1.25.0

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/kr/pretty v0.2.0 // indirect
	github.com/mattn/go-runewidth v0.0.23
	github.com/spf13/cobra v1.10.2
	github.com/stretchr/testify v1.11.1
	golang.org/x/image v0.39.0
	golang.org/x/sys v0.43.0 // indirect
	golang.org/x/term v0.42.0
	golang.org/x/text v0.36.0 // indirect
	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

require github.com/oliamb/cutter v0.2.2

require (
	github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/spf13/pflag v1.0.9 // indirect
)


================================================
FILE: go.sum
================================================
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k=
github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: image/encode.go
================================================
package image

import (
	"fmt"
	"image"
	"image/color/palette"
	"image/draw"
	"image/gif"
	"image/jpeg"
	"image/png"
	"io"
)

func (i *Image) Encode(w io.Writer, ext string) error {
	img := i.image
	switch ext {
	case ".png":
		return png.Encode(w, img)
	case ".jpg", ".jpeg":
		return jpeg.Encode(w, img, nil)
	case ".gif":
		if i.useAnimation {
			var delays []int
			for x := 0; x < len(i.animationImages); x++ {
				delays = append(delays, i.delay)
			}
			return gif.EncodeAll(w, &gif.GIF{
				Image: toPalettes(i.animationImages),
				Delay: delays,
			})
		}
		return gif.Encode(w, img, nil)
	}
	return fmt.Errorf("%s is not supported extension.", ext)
}

func toPalettes(imgs []image.Image) (ret []*image.Paletted) {
	for _, v := range imgs {
		bounds := v.Bounds()
		p := image.NewPaletted(bounds, palette.Plan9)
		draw.Draw(p, p.Rect, v, bounds.Min, draw.Over)
		ret = append(ret, p)
	}
	return
}


================================================
FILE: image/image.go
================================================
package image

import (
	"image"
	c "image/color"
	"image/draw"
	"os"

	"github.com/jiro4989/textimg/v3/color"
	"github.com/jiro4989/textimg/v3/token"
	"github.com/mattn/go-runewidth"
	"github.com/oliamb/cutter"
	xdraw "golang.org/x/image/draw"
	"golang.org/x/image/font"
	"golang.org/x/image/math/fixed"
)

type (
	Image struct {
		image                     *image.RGBA
		animationImages           []image.Image
		x                         int
		y                         int
		foregroundColor           c.RGBA // 文字色
		backgroundColor           c.RGBA // 背景色
		defaultForegroundColor    c.RGBA // 文字色
		defaultBackgroundColor    c.RGBA // 背景色
		fontSize                  int    // フォントサイズ
		fontFace                  font.Face
		emojiFontFace             font.Face
		charWidth                 int
		charHeight                int
		emojiDir                  string
		useEmoji                  bool
		lineCount                 int
		useAnimation              bool
		animationLineCount        int
		animationImageFlameHeight int
		resizeWidth               int
		resizeHeight              int
		delay                     int
	}
	ImageParam struct {
		BaseWidth          int
		BaseHeight         int
		ForegroundColor    c.RGBA // 文字色
		BackgroundColor    c.RGBA // 背景色
		FontSize           int    // フォントサイズ
		FontFace           font.Face
		EmojiFontFace      font.Face
		EmojiDir           string
		UseEmoji           bool
		UseAnimation       bool
		AnimationLineCount int
		ResizeWidth        int
		ResizeHeight       int
		Delay              int
	}
)

func init() {
	// Unicode Neutral で定義されている絵文字(例: 👁)を幅2として扱う
	runewidth.DefaultCondition.StrictEmojiNeutral = false
}

func NewImage(p *ImageParam) *Image {
	var (
		charWidth   = p.FontSize / 2
		charHeight  = int(float64(p.FontSize) * 1.1)
		imageWidth  = p.BaseWidth * charWidth
		imageHeight = p.BaseHeight * charHeight
	)

	var animationImageFlameHeight int
	if p.UseAnimation {
		animationImageFlameHeight = imageHeight / (p.BaseHeight / p.AnimationLineCount)
	}

	image := newImage(imageWidth, imageHeight)

	return &Image{
		image:                     image,
		foregroundColor:           p.ForegroundColor,
		backgroundColor:           p.BackgroundColor,
		defaultForegroundColor:    p.ForegroundColor,
		defaultBackgroundColor:    p.BackgroundColor,
		fontSize:                  p.FontSize,
		fontFace:                  p.FontFace,
		emojiFontFace:             p.EmojiFontFace,
		charWidth:                 charWidth,
		charHeight:                charHeight,
		emojiDir:                  p.EmojiDir,
		useEmoji:                  p.UseEmoji,
		useAnimation:              p.UseAnimation,
		animationLineCount:        p.AnimationLineCount,
		animationImageFlameHeight: animationImageFlameHeight,
		resizeWidth:               p.ResizeWidth,
		resizeHeight:              p.ResizeHeight,
		delay:                     p.Delay,
	}
}

func newImage(w, h int) *image.RGBA {
	return image.NewRGBA(image.Rect(0, 0, w, h))
}

func (i *Image) Draw(tokens token.Tokens) error {
	i.drawBackgroundAll()

	// 背景のみ描画
	for _, t := range tokens {
		switch t.Kind {
		case token.KindColor:
			i.updateColor(t.ColorType, t.Color)
		case token.KindText:
			i.drawBackground(t.Text)
			for _, r := range t.Text {
				if isLinefeed(r) {
					i.moveDown()
					continue
				}

				i.moveRight(r)
			}
		}
	}
	i.resetColor()
	i.resetPosition()

	// 文字のみ描画
	for _, t := range tokens {
		switch t.Kind {
		case token.KindColor:
			i.updateColor(t.ColorType, t.Color)
		case token.KindText:
			for _, r := range t.Text {
				if isLinefeed(r) {
					i.moveDown()
					continue
				}

				if err := i.draw(r); err != nil {
					return err
				}
				i.moveRight(r)
			}
		}
	}

	if err := i.setAnimationFlames(); err != nil {
		return err
	}
	i.scale()

	return nil
}

// 背景色をデフォルト色で塗りつぶす。
func (i *Image) drawBackgroundAll() {
	var (
		bounds = i.image.Bounds().Max
		width  = bounds.X
		height = bounds.Y
	)
	for x := 0; x < width; x++ {
		for y := 0; y < height; y++ {
			i.image.Set(x, y, c.RGBA(i.defaultBackgroundColor))
		}
	}
}

func (i *Image) updateColor(t token.ColorType, col color.RGBA) {
	switch t {
	case token.ColorTypeReset:
		i.resetColor()
	case token.ColorTypeResetForeground:
		i.foregroundColor = i.defaultForegroundColor
	case token.ColorTypeResetBackground:
		i.backgroundColor = i.defaultBackgroundColor
	case token.ColorTypeReverse:
		i.foregroundColor, i.backgroundColor = i.backgroundColor, i.foregroundColor
	case token.ColorTypeForeground:
		i.foregroundColor = c.RGBA(col)
	case token.ColorTypeBackground:
		i.backgroundColor = c.RGBA(col)
	}
}

func (i *Image) resetColor() {
	i.foregroundColor = i.defaultForegroundColor
	i.backgroundColor = i.defaultBackgroundColor
}

func (i *Image) resetPosition() {
	i.x = 0
	i.y = 0
	i.lineCount = 0
}

func (i *Image) newDrawer(f font.Face) *font.Drawer {
	// 特殊な位置調整。なんでこんな計算式にしたのか覚えていない
	var (
		x = i.x
		y = i.y + i.charHeight - (i.charHeight / 5)
	)
	// FIXME: なんか警告出てる
	point := fixed.Point26_6{fixed.Int26_6(x * 64), fixed.Int26_6(y * 64)}
	d := &font.Drawer{
		Dst:  i.image,
		Src:  image.NewUniform(c.RGBA(i.foregroundColor)),
		Face: f,
		Dot:  point,
	}
	return d
}

func (i *Image) draw(r rune) error {
	if ok, emojiPath := isEmoji(r, i.emojiDir); ok {
		if i.useEmoji {
			i.drawRune(r, i.emojiFontFace)
			return nil
		}
		return i.drawEmoji(r, emojiPath)
	}
	i.drawRune(r, i.fontFace)
	return nil
}

func (i *Image) setAnimationFlames() error {
	if i.useAnimation {
		b := i.image.Bounds().Max
		w, h := b.X, i.animationImageFlameHeight
		max := b.Y / i.animationImageFlameHeight
		for rc := 0; rc < max; rc++ {
			x, y := 0, rc*h
			pt := image.Pt(x, y)
			cimg, err := cutter.Crop(i.image, cutter.Config{
				Width:   w,
				Height:  h,
				Anchor:  pt,
				Mode:    cutter.TopLeft,
				Options: cutter.Copy,
			})
			if err != nil {
				return err
			}
			dist := image.NewRGBA(image.Rectangle{
				image.Pt(0, 0),
				image.Pt(w, h),
			})
			draw.Draw(dist, dist.Bounds(), cimg, pt, draw.Over)
			i.animationImages = append(i.animationImages, dist)
		}
	}
	return nil
}

// rune文字を画像に書き込む。
// 書き込み終えると座標を更新する。
func (i *Image) drawRune(r rune, f font.Face) {
	d := i.newDrawer(f)
	d.DrawString(string(r))
}

func (i *Image) drawEmoji(r rune, path string) error {
	fp, err := os.Open(path)
	if err != nil {
		return err
	}
	defer fp.Close()

	emoji, _, err := image.Decode(fp)
	if err != nil {
		return err
	}

	d := i.newDrawer(i.fontFace)
	// 画像サイズをフォントサイズに合わせる
	// 0.9でさらに微妙に調整
	size := int(float64(d.Face.Metrics().Ascent.Floor()+d.Face.Metrics().Descent.Floor()) * 0.9)
	rect := image.Rect(0, 0, size, size)
	dst := image.NewRGBA(rect)
	xdraw.ApproxBiLinear.Scale(dst, rect, emoji, emoji.Bounds(), draw.Over, nil)

	p := image.Pt(d.Dot.X.Floor(), d.Dot.Y.Floor()-d.Face.Metrics().Ascent.Floor())
	draw.Draw(i.image, rect.Add(p), dst, image.Point{}, draw.Over)
	return nil
}

func (i *Image) drawBackground(s string) {
	var (
		tw     = runewidth.StringWidth(s)
		width  = tw * i.charWidth
		height = i.charHeight
		posX   = i.x
		posY   = i.y
	)
	for x := posX; x < posX+width; x++ {
		for y := posY; y < posY+height; y++ {
			i.image.Set(x, y, c.RGBA(i.backgroundColor))
		}
	}
}

func (i *Image) moveRight(r rune) {
	i.x += runewidth.RuneWidth(r) * i.charWidth
}

func (i *Image) moveDown() {
	i.x = 0
	i.y += i.charHeight
	i.lineCount++
}

func (i *Image) newScaledImage() *image.RGBA {
	if i.resizeWidth == 0 && i.resizeHeight == 0 {
		return i.image
	}

	// 呼び出し側で大きさを調整していること
	dst := scale(i.image, i.resizeWidth, i.resizeHeight)
	return dst
}

func scale(img image.Image, w, h int) *image.RGBA {
	rect := img.Bounds()
	dst := newImage(w, h)
	xdraw.CatmullRom.Scale(dst, dst.Bounds(), img, rect, draw.Over, nil)
	return dst
}

func (i *Image) scale() {
	if i.resizeWidth == 0 && i.resizeHeight == 0 {
		return
	}

	i.image = i.newScaledImage()
	for j, img := range i.animationImages {
		dst := scale(img, i.resizeWidth, i.resizeHeight)
		i.animationImages[j] = dst
	}
}


================================================
FILE: image/util.go
================================================
package image

import (
	"fmt"
	"os"
)

var (
	// 絵文字描画の際に、普通に描画してほしいけれど絵文字としても定義されている
	// 文字のコードポイント
	exRunes = []rune{
		0x0023, // #
		0x002A, // *
		0x0030, // 0
		0x0031, // 1
		0x0032, // 2
		0x0033, // 3
		0x0034, // 4
		0x0035, // 5
		0x0036, // 6
		0x0037, // 7
		0x0038, // 8
		0x0039, // 9
		0x00A9, // ©
		0x00AE, // ®️
	}
)

// コードポイントに対応する画像ファイルかどうかを判定する。
// 画像ファイルだった場合は当該画像ファイルのパスを返却する。
func isEmoji(r rune, emojiDir string) (bool, string) {
	path := fmt.Sprintf("%s/emoji_u%.4x.png", emojiDir, r)
	_, err := os.Stat(path)
	if err == nil && !isExceptionallyCodePoint(r) {
		return true, path
	}
	return false, ""
}

// r が例外的なコードポイントに存在するかを判定する。
// http://unicode.org/Public/emoji/4.0/emoji-data.txt
//
// ここでtrueを返す文字は、絵文字データ的には絵文字ではあるものの、
// シェル芸bot環境ではテキストとして表示したいので例外的に除外するために指定して
// いる。
func isExceptionallyCodePoint(r rune) bool {
	for _, ex := range exRunes {
		if r == ex {
			return true
		}
	}

	return false
}

func isLinefeed(r rune) bool {
	return r == '\n'
}


================================================
FILE: images/.gitkeep
================================================


================================================
FILE: internal/global/env.go
================================================
package global

const (
	EnvNameOutputDir     = "TEXTIMG_OUTPUT_DIR"
	EnvNameFontFile      = "TEXTIMG_FONT_FILE"
	EnvNameEmojiDir      = "TEXTIMG_EMOJI_DIR"
	EnvNameEmojiFontFile = "TEXTIMG_EMOJI_FONT_FILE"
)

var (
	EnvNames = []string{
		EnvNameOutputDir,
		EnvNameFontFile,
		EnvNameEmojiDir,
		EnvNameEmojiFontFile,
	}
)


================================================
FILE: internal/global/version.go
================================================
package global

const (
	AppName = "textimg"
	Version = `3.1.10
Copyright (c) 2019 jiro4989
Released under the MIT License.
https://github.com/jiro4989/textimg`
)


================================================
FILE: log/log.go
================================================
package log

import (
	"fmt"
	"os"
	"runtime"
	"time"

	"github.com/jiro4989/textimg/v3/internal/global"
)

const (
	debugPrefix = "[DEBUG]"
	infoPrefix  = "[INFO]"
	warnPrefix  = "[WARN]"
	errorPrefix = "[ERROR]"
)

func log(lvl string, msg interface{}) {
	_, f, l, ok := runtime.Caller(2)
	if !ok {
		fmt.Fprintln(os.Stderr, "something error occurred.")
		return
	}

	now := time.Now().Format("2006/01/02 03:04:05")
	text := fmt.Sprintf("%s %s %s %s:%d %v", now, global.AppName, lvl, f, l, msg)
	fmt.Fprintln(os.Stderr, text)
}

func Debug(msg interface{}) {
	log(debugPrefix, msg)
}

func Info(msg interface{}) {
	log(infoPrefix, msg)
}

func Warn(msg interface{}) {
	log(warnPrefix, msg)
}

func Warnf(format string, msg interface{}) {
	text := fmt.Sprintf(format, msg)
	log(warnPrefix, text)
}

func Error(msg interface{}) {
	log(errorPrefix, msg)
}


================================================
FILE: log/log_test.go
================================================
package log

import (
	"testing"
)

func TestDebug(t *testing.T) {
	Debug("debug")
	Info("debug")
	Warn("debug")
	Warnf("%v", 1)
	Error("debug")
}


================================================
FILE: main.go
================================================
package main

import (
	"os"
)

func main() {
	os.Exit(Main())
}

func Main() int {
	if err := RootCommand.Execute(); err != nil {
		return -1
	}
	return 0
}


================================================
FILE: main_test.go
================================================
//go:build !docker

package main

import (
	"os"
	"testing"
)

const (
	inDir  = "testdata/in"
	outDir = "testdata/out"
)

func TestMain(m *testing.M) {
	testBefore()
	exitCode := m.Run()
	os.Exit(exitCode)
}

func testBefore() {
	if err := os.RemoveAll(outDir); err != nil {
		panic(err)
	}
	// nolint
	os.Mkdir(outDir, os.ModePerm)
}


================================================
FILE: parser/grammar.peg
================================================
package parser

type Parser Peg {
  ParserFunc
}

root <-
  (colors / ignore / text)*

ignore <-
  prefix number? non_color_suffix
  / escape_sequence

colors <-
  prefix color_suffix { p.pushResetColor() }
  / prefix color (delimiter color)* color_suffix

text <-
  < [^\e] + > { p.pushText(text) }

color <-
  standard_color
  / extended_color
  / text_attributes

standard_color <-
  zero < ([349] / '10') [0-7] > { p.pushStandardColorWithCategory(text) }
  / zero < [39] '9' >           { p.pushResetForegroundColor() }
  / zero < ('4' / '10') '9' >   { p.pushResetBackgroundColor() }

extended_color <-
  extended_color_256 / extended_color_rgb

extended_color_256 <-
  extended_color_prefix
  delimiter
  zero '5'
  delimiter
  < number > { p.setExtendedColor256(text) }

extended_color_rgb <-
  extended_color_prefix
  delimiter
  zero '2'
  delimiter
  < number > { p.setExtendedColorR(text) }
  delimiter
  < number > { p.setExtendedColorG(text) }
  delimiter
  < number > { p.setExtendedColorB(text) }

extended_color_prefix <-
  zero
  < [34] '8' > { p.pushExtendedColor(text) }

text_attributes <-
  (
  '0' { p.pushResetColor() }
  / '7' { p.pushReverseColor() }
  / [1458]
  )+

zero             <- '0' *
number           <- [0-9]+
prefix           <- escape_sequence '['
escape_sequence  <- '\e'
color_suffix     <- 'm'
non_color_suffix <- [A-HfSTJK]
delimiter        <- ';'


================================================
FILE: parser/grammar.peg.go
================================================
package parser

// Code generated by peg parser/grammar.peg DO NOT EDIT.

import (
	"fmt"
	"io"
	"os"
	"sort"
	"strconv"
	"strings"
)

const endSymbol rune = 1114112

/* The rule types inferred from the grammar are below. */
type pegRule uint8

const (
	ruleUnknown pegRule = iota
	ruleroot
	ruleignore
	rulecolors
	ruletext
	rulecolor
	rulestandard_color
	ruleextended_color
	ruleextended_color_256
	ruleextended_color_rgb
	ruleextended_color_prefix
	ruletext_attributes
	rulezero
	rulenumber
	ruleprefix
	ruleescape_sequence
	rulecolor_suffix
	rulenon_color_suffix
	ruledelimiter
	ruleAction0
	rulePegText
	ruleAction1
	ruleAction2
	ruleAction3
	ruleAction4
	ruleAction5
	ruleAction6
	ruleAction7
	ruleAction8
	ruleAction9
	ruleAction10
	ruleAction11
)

var rul3s = [...]string{
	"Unknown",
	"root",
	"ignore",
	"colors",
	"text",
	"color",
	"standard_color",
	"extended_color",
	"extended_color_256",
	"extended_color_rgb",
	"extended_color_prefix",
	"text_attributes",
	"zero",
	"number",
	"prefix",
	"escape_sequence",
	"color_suffix",
	"non_color_suffix",
	"delimiter",
	"Action0",
	"PegText",
	"Action1",
	"Action2",
	"Action3",
	"Action4",
	"Action5",
	"Action6",
	"Action7",
	"Action8",
	"Action9",
	"Action10",
	"Action11",
}

type token32 struct {
	pegRule
	begin, end uint32
}

func (t *token32) String() string {
	return fmt.Sprintf("\x1B[34m%v\x1B[m %v %v", rul3s[t.pegRule], t.begin, t.end)
}

type node32 struct {
	token32
	up, next *node32
}

func (node *node32) print(w io.Writer, pretty bool, buffer string) {
	var print func(node *node32, depth int)
	print = func(node *node32, depth int) {
		for node != nil {
			for c := 0; c < depth; c++ {
				fmt.Fprintf(w, " ")
			}
			rule := rul3s[node.pegRule]
			quote := strconv.Quote(string(([]rune(buffer)[node.begin:node.end])))
			if !pretty {
				fmt.Fprintf(w, "%v %v\n", rule, quote)
			} else {
				fmt.Fprintf(w, "\x1B[36m%v\x1B[m %v\n", rule, quote)
			}
			if node.up != nil {
				print(node.up, depth+1)
			}
			node = node.next
		}
	}
	print(node, 0)
}

func (node *node32) Print(w io.Writer, buffer string) {
	node.print(w, false, buffer)
}

func (node *node32) PrettyPrint(w io.Writer, buffer string) {
	node.print(w, true, buffer)
}

type tokens32 struct {
	tree []token32
}

func (t *tokens32) Trim(length uint32) {
	t.tree = t.tree[:length]
}

func (t *tokens32) Print() {
	for _, token := range t.tree {
		fmt.Println(token.String())
	}
}

func (t *tokens32) AST() *node32 {
	type element struct {
		node *node32
		down *element
	}
	tokens := t.Tokens()
	var stack *element
	for _, token := range tokens {
		if token.begin == token.end {
			continue
		}
		node := &node32{token32: token}
		for stack != nil && stack.node.begin >= token.begin && stack.node.end <= token.end {
			stack.node.next = node.up
			node.up = stack.node
			stack = stack.down
		}
		stack = &element{node: node, down: stack}
	}
	if stack != nil {
		return stack.node
	}
	return nil
}

func (t *tokens32) PrintSyntaxTree(buffer string) {
	t.AST().Print(os.Stdout, buffer)
}

func (t *tokens32) WriteSyntaxTree(w io.Writer, buffer string) {
	t.AST().Print(w, buffer)
}

func (t *tokens32) PrettyPrintSyntaxTree(buffer string) {
	t.AST().PrettyPrint(os.Stdout, buffer)
}

func (t *tokens32) Add(rule pegRule, begin, end, index uint32) {
	tree, i := t.tree, int(index)
	if i >= len(tree) {
		t.tree = append(tree, token32{pegRule: rule, begin: begin, end: end})
		return
	}
	tree[i] = token32{pegRule: rule, begin: begin, end: end}
}

func (t *tokens32) Tokens() []token32 {
	return t.tree
}

type Parser struct {
	ParserFunc

	Buffer string
	buffer []rune
	rules  [32]func() bool
	parse  func(rule ...int) error
	reset  func()
	Pretty bool
	tokens32
}

func (p *Parser) Parse(rule ...int) error {
	return p.parse(rule...)
}

func (p *Parser) Reset() {
	p.reset()
}

type textPosition struct {
	line, symbol int
}

type textPositionMap map[int]textPosition

func translatePositions(buffer []rune, positions []int) textPositionMap {
	length, translations, j, line, symbol := len(positions), make(textPositionMap, len(positions)), 0, 1, 0
	sort.Ints(positions)

search:
	for i, c := range buffer {
		if c == '\n' {
			line, symbol = line+1, 0
		} else {
			symbol++
		}
		if i == positions[j] {
			translations[positions[j]] = textPosition{line, symbol}
			for j++; j < length; j++ {
				if i != positions[j] {
					continue search
				}
			}
			break search
		}
	}

	return translations
}

type parseError struct {
	p   *Parser
	max token32
}

func (e *parseError) Error() string {
	tokens, err := []token32{e.max}, "\n"
	positions, p := make([]int, 2*len(tokens)), 0
	for _, token := range tokens {
		positions[p], p = int(token.begin), p+1
		positions[p], p = int(token.end), p+1
	}
	translations := translatePositions(e.p.buffer, positions)
	format := "parse error near %v (line %v symbol %v - line %v symbol %v):\n%v\n"
	if e.p.Pretty {
		format = "parse error near \x1B[34m%v\x1B[m (line %v symbol %v - line %v symbol %v):\n%v\n"
	}
	for _, token := range tokens {
		begin, end := int(token.begin), int(token.end)
		err += fmt.Sprintf(format,
			rul3s[token.pegRule],
			translations[begin].line, translations[begin].symbol,
			translations[end].line, translations[end].symbol,
			strconv.Quote(string(e.p.buffer[begin:end])))
	}

	return err
}

func (p *Parser) PrintSyntaxTree() {
	if p.Pretty {
		p.tokens32.PrettyPrintSyntaxTree(p.Buffer)
	} else {
		p.tokens32.PrintSyntaxTree(p.Buffer)
	}
}

func (p *Parser) WriteSyntaxTree(w io.Writer) {
	p.tokens32.WriteSyntaxTree(w, p.Buffer)
}

func (p *Parser) SprintSyntaxTree() string {
	var bldr strings.Builder
	p.WriteSyntaxTree(&bldr)
	return bldr.String()
}

func (p *Parser) Execute() {
	buffer, _buffer, text, begin, end := p.Buffer, p.buffer, "", 0, 0
	for _, token := range p.Tokens() {
		switch token.pegRule {

		case rulePegText:
			begin, end = int(token.begin), int(token.end)
			text = string(_buffer[begin:end])

		case ruleAction0:
			p.pushResetColor()
		case ruleAction1:
			p.pushText(text)
		case ruleAction2:
			p.pushStandardColorWithCategory(text)
		case ruleAction3:
			p.pushResetForegroundColor()
		case ruleAction4:
			p.pushResetBackgroundColor()
		case ruleAction5:
			p.setExtendedColor256(text)
		case ruleAction6:
			p.setExtendedColorR(text)
		case ruleAction7:
			p.setExtendedColorG(text)
		case ruleAction8:
			p.setExtendedColorB(text)
		case ruleAction9:
			p.pushExtendedColor(text)
		case ruleAction10:
			p.pushResetColor()
		case ruleAction11:
			p.pushReverseColor()

		}
	}
	_, _, _, _, _ = buffer, _buffer, text, begin, end
}

func Pretty(pretty bool) func(*Parser) error {
	return func(p *Parser) error {
		p.Pretty = pretty
		return nil
	}
}

func Size(size int) func(*Parser) error {
	return func(p *Parser) error {
		p.tokens32 = tokens32{tree: make([]token32, 0, size)}
		return nil
	}
}
func (p *Parser) Init(options ...func(*Parser) error) error {
	var (
		max                  token32
		position, tokenIndex uint32
		buffer               []rune
	)
	for _, option := range options {
		err := option(p)
		if err != nil {
			return err
		}
	}
	p.reset = func() {
		max = token32{}
		position, tokenIndex = 0, 0

		p.buffer = []rune(p.Buffer)
		if len(p.buffer) == 0 || p.buffer[len(p.buffer)-1] != endSymbol {
			p.buffer = append(p.buffer, endSymbol)
		}
		buffer = p.buffer
	}
	p.reset()

	_rules := p.rules
	tree := p.tokens32
	p.parse = func(rule ...int) error {
		r := 1
		if len(rule) > 0 {
			r = rule[0]
		}
		matches := p.rules[r]()
		p.tokens32 = tree
		if matches {
			p.Trim(tokenIndex)
			return nil
		}
		return &parseError{p, max}
	}

	add := func(rule pegRule, begin uint32) {
		tree.Add(rule, begin, position, tokenIndex)
		tokenIndex++
		if begin != position && position > max.end {
			max = token32{rule, begin, position}
		}
	}

	matchDot := func() bool {
		if buffer[position] != endSymbol {
			position++
			return true
		}
		return false
	}

	/*matchChar := func(c byte) bool {
		if buffer[position] == c {
			position++
			return true
		}
		return false
	}*/

	/*matchRange := func(lower byte, upper byte) bool {
		if c := buffer[position]; c >= lower && c <= upper {
			position++
			return true
		}
		return false
	}*/

	_rules = [...]func() bool{
		nil,
		/* 0 root <- <(colors / ignore / text)*> */
		func() bool {
			{
				position1 := position
			l2:
				{
					position3, tokenIndex3 := position, tokenIndex
					{
						position4, tokenIndex4 := position, tokenIndex
						if !_rules[rulecolors]() {
							goto l5
						}
						goto l4
					l5:
						position, tokenIndex = position4, tokenIndex4
						if !_rules[ruleignore]() {
							goto l6
						}
						goto l4
					l6:
						position, tokenIndex = position4, tokenIndex4
						if !_rules[ruletext]() {
							goto l3
						}
					}
				l4:
					goto l2
				l3:
					position, tokenIndex = position3, tokenIndex3
				}
				add(ruleroot, position1)
			}
			return true
		},
		/* 1 ignore <- <((prefix number? non_color_suffix) / escape_sequence)> */
		func() bool {
			position7, tokenIndex7 := position, tokenIndex
			{
				position8 := position
				{
					position9, tokenIndex9 := position, tokenIndex
					if !_rules[ruleprefix]() {
						goto l10
					}
					{
						position11, tokenIndex11 := position, tokenIndex
						if !_rules[rulenumber]() {
							goto l11
						}
						goto l12
					l11:
						position, tokenIndex = position11, tokenIndex11
					}
				l12:
					if !_rules[rulenon_color_suffix]() {
						goto l10
					}
					goto l9
				l10:
					position, tokenIndex = position9, tokenIndex9
					if !_rules[ruleescape_sequence]() {
						goto l7
					}
				}
			l9:
				add(ruleignore, position8)
			}
			return true
		l7:
			position, tokenIndex = position7, tokenIndex7
			return false
		},
		/* 2 colors <- <((prefix color_suffix Action0) / (prefix color (delimiter color)* color_suffix))> */
		func() bool {
			position13, tokenIndex13 := position, tokenIndex
			{
				position14 := position
				{
					position15, tokenIndex15 := position, tokenIndex
					if !_rules[ruleprefix]() {
						goto l16
					}
					if !_rules[rulecolor_suffix]() {
						goto l16
					}
					if !_rules[ruleAction0]() {
						goto l16
					}
					goto l15
				l16:
					position, tokenIndex = position15, tokenIndex15
					if !_rules[ruleprefix]() {
						goto l13
					}
					if !_rules[rulecolor]() {
						goto l13
					}
				l17:
					{
						position18, tokenIndex18 := position, tokenIndex
						if !_rules[ruledelimiter]() {
							goto l18
						}
						if !_rules[rulecolor]() {
							goto l18
						}
						goto l17
					l18:
						position, tokenIndex = position18, tokenIndex18
					}
					if !_rules[rulecolor_suffix]() {
						goto l13
					}
				}
			l15:
				add(rulecolors, position14)
			}
			return true
		l13:
			position, tokenIndex = position13, tokenIndex13
			return false
		},
		/* 3 text <- <(<(!'\x1b' .)+> Action1)> */
		func() bool {
			position19, tokenIndex19 := position, tokenIndex
			{
				position20 := position
				{
					position21 := position
					{
						position24, tokenIndex24 := position, tokenIndex
						if buffer[position] != rune('\x1b') {
							goto l24
						}
						position++
						goto l19
					l24:
						position, tokenIndex = position24, tokenIndex24
					}
					if !matchDot() {
						goto l19
					}
				l22:
					{
						position23, tokenIndex23 := position, tokenIndex
						{
							position25, tokenIndex25 := position, tokenIndex
							if buffer[position] != rune('\x1b') {
								goto l25
							}
							position++
							goto l23
						l25:
							position, tokenIndex = position25, tokenIndex25
						}
						if !matchDot() {
							goto l23
						}
						goto l22
					l23:
						position, tokenIndex = position23, tokenIndex23
					}
					add(rulePegText, position21)
				}
				if !_rules[ruleAction1]() {
					goto l19
				}
				add(ruletext, position20)
			}
			return true
		l19:
			position, tokenIndex = position19, tokenIndex19
			return false
		},
		/* 4 color <- <(standard_color / extended_color / text_attributes)> */
		func() bool {
			position26, tokenIndex26 := position, tokenIndex
			{
				position27 := position
				{
					position28, tokenIndex28 := position, tokenIndex
					if !_rules[rulestandard_color]() {
						goto l29
					}
					goto l28
				l29:
					position, tokenIndex = position28, tokenIndex28
					if !_rules[ruleextended_color]() {
						goto l30
					}
					goto l28
				l30:
					position, tokenIndex = position28, tokenIndex28
					if !_rules[ruletext_attributes]() {
						goto l26
					}
				}
			l28:
				add(rulecolor, position27)
			}
			return true
		l26:
			position, tokenIndex = position26, tokenIndex26
			return false
		},
		/* 5 standard_color <- <((zero <(('3' / '4' / '9' / ('1' '0')) [0-7])> Action2) / (zero <(('3' / '9') '9')> Action3) / (zero <(('4' / ('1' '0')) '9')> Action4))> */
		func() bool {
			position31, tokenIndex31 := position, tokenIndex
			{
				position32 := position
				{
					position33, tokenIndex33 := position, tokenIndex
					if !_rules[rulezero]() {
						goto l34
					}
					{
						position35 := position
						{
							position36, tokenIndex36 := position, tokenIndex
							if buffer[position] != rune('3') {
								goto l37
							}
							position++
							goto l36
						l37:
							position, tokenIndex = position36, tokenIndex36
							if buffer[position] != rune('4') {
								goto l38
							}
							position++
							goto l36
						l38:
							position, tokenIndex = position36, tokenIndex36
							if buffer[position] != rune('9') {
								goto l39
							}
							position++
							goto l36
						l39:
							position, tokenIndex = position36, tokenIndex36
							if buffer[position] != rune('1') {
								goto l34
							}
							position++
							if buffer[position] != rune('0') {
								goto l34
							}
							position++
						}
					l36:
						if c := buffer[position]; c < rune('0') || c > rune('7') {
							goto l34
						}
						position++
						add(rulePegText, position35)
					}
					if !_rules[ruleAction2]() {
						goto l34
					}
					goto l33
				l34:
					position, tokenIndex = position33, tokenIndex33
					if !_rules[rulezero]() {
						goto l40
					}
					{
						position41 := position
						{
							position42, tokenIndex42 := position, tokenIndex
							if buffer[position] != rune('3') {
								goto l43
							}
							position++
							goto l42
						l43:
							position, tokenIndex = position42, tokenIndex42
							if buffer[position] != rune('9') {
								goto l40
							}
							position++
						}
					l42:
						if buffer[position] != rune('9') {
							goto l40
						}
						position++
						add(rulePegText, position41)
					}
					if !_rules[ruleAction3]() {
						goto l40
					}
					goto l33
				l40:
					position, tokenIndex = position33, tokenIndex33
					if !_rules[rulezero]() {
						goto l31
					}
					{
						position44 := position
						{
							position45, tokenIndex45 := position, tokenIndex
							if buffer[position] != rune('4') {
								goto l46
							}
							position++
							goto l45
						l46:
							position, tokenIndex = position45, tokenIndex45
							if buffer[position] != rune('1') {
								goto l31
							}
							position++
							if buffer[position] != rune('0') {
								goto l31
							}
							position++
						}
					l45:
						if buffer[position] != rune('9') {
							goto l31
						}
						position++
						add(rulePegText, position44)
					}
					if !_rules[ruleAction4]() {
						goto l31
					}
				}
			l33:
				add(rulestandard_color, position32)
			}
			return true
		l31:
			position, tokenIndex = position31, tokenIndex31
			return false
		},
		/* 6 extended_color <- <(extended_color_256 / extended_color_rgb)> */
		func() bool {
			position47, tokenIndex47 := position, tokenIndex
			{
				position48 := position
				{
					position49, tokenIndex49 := position, tokenIndex
					if !_rules[ruleextended_color_256]() {
						goto l50
					}
					goto l49
				l50:
					position, tokenIndex = position49, tokenIndex49
					if !_rules[ruleextended_color_rgb]() {
						goto l47
					}
				}
			l49:
				add(ruleextended_color, position48)
			}
			return true
		l47:
			position, tokenIndex = position47, tokenIndex47
			return false
		},
		/* 7 extended_color_256 <- <(extended_color_prefix delimiter zero '5' delimiter <number> Action5)> */
		func() bool {
			position51, tokenIndex51 := position, tokenIndex
			{
				position52 := position
				if !_rules[ruleextended_color_prefix]() {
					goto l51
				}
				if !_rules[ruledelimiter]() {
					goto l51
				}
				if !_rules[rulezero]() {
					goto l51
				}
				if buffer[position] != rune('5') {
					goto l51
				}
				position++
				if !_rules[ruledelimiter]() {
					goto l51
				}
				{
					position53 := position
					if !_rules[rulenumber]() {
						goto l51
					}
					add(rulePegText, position53)
				}
				if !_rules[ruleAction5]() {
					goto l51
				}
				add(ruleextended_color_256, position52)
			}
			return true
		l51:
			position, tokenIndex = position51, tokenIndex51
			return false
		},
		/* 8 extended_color_rgb <- <(extended_color_prefix delimiter zero '2' delimiter <number> Action6 delimiter <number> Action7 delimiter <number> Action8)> */
		func() bool {
			position54, tokenIndex54 := position, tokenIndex
			{
				position55 := position
				if !_rules[ruleextended_color_prefix]() {
					goto l54
				}
				if !_rules[ruledelimiter]() {
					goto l54
				}
				if !_rules[rulezero]() {
					goto l54
				}
				if buffer[position] != rune('2') {
					goto l54
				}
				position++
				if !_rules[ruledelimiter]() {
					goto l54
				}
				{
					position56 := position
					if !_rules[rulenumber]() {
						goto l54
					}
					add(rulePegText, position56)
				}
				if !_rules[ruleAction6]() {
					goto l54
				}
				if !_rules[ruledelimiter]() {
					goto l54
				}
				{
					position57 := position
					if !_rules[rulenumber]() {
						goto l54
					}
					add(rulePegText, position57)
				}
				if !_rules[ruleAction7]() {
					goto l54
				}
				if !_rules[ruledelimiter]() {
					goto l54
				}
				{
					position58 := position
					if !_rules[rulenumber]() {
						goto l54
					}
					add(rulePegText, position58)
				}
				if !_rules[ruleAction8]() {
					goto l54
				}
				add(ruleextended_color_rgb, position55)
			}
			return true
		l54:
			position, tokenIndex = position54, tokenIndex54
			return false
		},
		/* 9 extended_color_prefix <- <(zero <(('3' / '4') '8')> Action9)> */
		func() bool {
			position59, tokenIndex59 := position, tokenIndex
			{
				position60 := position
				if !_rules[rulezero]() {
					goto l59
				}
				{
					position61 := position
					{
						position62, tokenIndex62 := position, tokenIndex
						if buffer[position] != rune('3') {
							goto l63
						}
						position++
						goto l62
					l63:
						position, tokenIndex = position62, tokenIndex62
						if buffer[position] != rune('4') {
							goto l59
						}
						position++
					}
				l62:
					if buffer[position] != rune('8') {
						goto l59
					}
					position++
					add(rulePegText, position61)
				}
				if !_rules[ruleAction9]() {
					goto l59
				}
				add(ruleextended_color_prefix, position60)
			}
			return true
		l59:
			position, tokenIndex = position59, tokenIndex59
			return false
		},
		/* 10 text_attributes <- <(('0' Action10) / ('7' Action11) / ('1' / '4' / '5' / '8'))+> */
		func() bool {
			position64, tokenIndex64 := position, tokenIndex
			{
				position65 := position
				{
					position68, tokenIndex68 := position, tokenIndex
					if buffer[position] != rune('0') {
						goto l69
					}
					position++
					if !_rules[ruleAction10]() {
						goto l69
					}
					goto l68
				l69:
					position, tokenIndex = position68, tokenIndex68
					if buffer[position] != rune('7') {
						goto l70
					}
					position++
					if !_rules[ruleAction11]() {
						goto l70
					}
					goto l68
				l70:
					position, tokenIndex = position68, tokenIndex68
					{
						position71, tokenIndex71 := position, tokenIndex
						if buffer[position] != rune('1') {
							goto l72
						}
						position++
						goto l71
					l72:
						position, tokenIndex = position71, tokenIndex71
						if buffer[position] != rune('4') {
							goto l73
						}
						position++
						goto l71
					l73:
						position, tokenIndex = position71, tokenIndex71
						if buffer[position] != rune('5') {
							goto l74
						}
						position++
						goto l71
					l74:
						position, tokenIndex = position71, tokenIndex71
						if buffer[position] != rune('8') {
							goto l64
						}
						position++
					}
				l71:
				}
			l68:
			l66:
				{
					position67, tokenIndex67 := position, tokenIndex
					{
						position75, tokenIndex75 := position, tokenIndex
						if buffer[position] != rune('0') {
							goto l76
						}
						position++
						if !_rules[ruleAction10]() {
							goto l76
						}
						goto l75
					l76:
						position, tokenIndex = position75, tokenIndex75
						if buffer[position] != rune('7') {
							goto l77
						}
						position++
						if !_rules[ruleAction11]() {
							goto l77
						}
						goto l75
					l77:
						position, tokenIndex = position75, tokenIndex75
						{
							position78, tokenIndex78 := position, tokenIndex
							if buffer[position] != rune('1') {
								goto l79
							}
							position++
							goto l78
						l79:
							position, tokenIndex = position78, tokenIndex78
							if buffer[position] != rune('4') {
								goto l80
							}
							position++
							goto l78
						l80:
							position, tokenIndex = position78, tokenIndex78
							if buffer[position] != rune('5') {
								goto l81
							}
							position++
							goto l78
						l81:
							position, tokenIndex = position78, tokenIndex78
							if buffer[position] != rune('8') {
								goto l67
							}
							position++
						}
					l78:
					}
				l75:
					goto l66
				l67:
					position, tokenIndex = position67, tokenIndex67
				}
				add(ruletext_attributes, position65)
			}
			return true
		l64:
			position, tokenIndex = position64, tokenIndex64
			return false
		},
		/* 11 zero <- <'0'*> */
		func() bool {
			{
				position83 := position
			l84:
				{
					position85, tokenIndex85 := position, tokenIndex
					if buffer[position] != rune('0') {
						goto l85
					}
					position++
					goto l84
				l85:
					position, tokenIndex = position85, tokenIndex85
				}
				add(rulezero, position83)
			}
			return true
		},
		/* 12 number <- <[0-9]+> */
		func() bool {
			position86, tokenIndex86 := position, tokenIndex
			{
				position87 := position
				if c := buffer[position]; c < rune('0') || c > rune('9') {
					goto l86
				}
				position++
			l88:
				{
					position89, tokenIndex89 := position, tokenIndex
					if c := buffer[position]; c < rune('0') || c > rune('9') {
						goto l89
					}
					position++
					goto l88
				l89:
					position, tokenIndex = position89, tokenIndex89
				}
				add(rulenumber, position87)
			}
			return true
		l86:
			position, tokenIndex = position86, tokenIndex86
			return false
		},
		/* 13 prefix <- <(escape_sequence '[')> */
		func() bool {
			position90, tokenIndex90 := position, tokenIndex
			{
				position91 := position
				if !_rules[ruleescape_sequence]() {
					goto l90
				}
				if buffer[position] != rune('[') {
					goto l90
				}
				position++
				add(ruleprefix, position91)
			}
			return true
		l90:
			position, tokenIndex = position90, tokenIndex90
			return false
		},
		/* 14 escape_sequence <- <'\x1b'> */
		func() bool {
			position92, tokenIndex92 := position, tokenIndex
			{
				position93 := position
				if buffer[position] != rune('\x1b') {
					goto l92
				}
				position++
				add(ruleescape_sequence, position93)
			}
			return true
		l92:
			position, tokenIndex = position92, tokenIndex92
			return false
		},
		/* 15 color_suffix <- <'m'> */
		func() bool {
			position94, tokenIndex94 := position, tokenIndex
			{
				position95 := position
				if buffer[position] != rune('m') {
					goto l94
				}
				position++
				add(rulecolor_suffix, position95)
			}
			return true
		l94:
			position, tokenIndex = position94, tokenIndex94
			return false
		},
		/* 16 non_color_suffix <- <([A-H] / 'f' / 'S' / 'T' / 'J' / 'K')> */
		func() bool {
			position96, tokenIndex96 := position, tokenIndex
			{
				position97 := position
				{
					position98, tokenIndex98 := position, tokenIndex
					if c := buffer[position]; c < rune('A') || c > rune('H') {
						goto l99
					}
					position++
					goto l98
				l99:
					position, tokenIndex = position98, tokenIndex98
					if buffer[position] != rune('f') {
						goto l100
					}
					position++
					goto l98
				l100:
					position, tokenIndex = position98, tokenIndex98
					if buffer[position] != rune('S') {
						goto l101
					}
					position++
					goto l98
				l101:
					position, tokenIndex = position98, tokenIndex98
					if buffer[position] != rune('T') {
						goto l102
					}
					position++
					goto l98
				l102:
					position, tokenIndex = position98, tokenIndex98
					if buffer[position] != rune('J') {
						goto l103
					}
					position++
					goto l98
				l103:
					position, tokenIndex = position98, tokenIndex98
					if buffer[position] != rune('K') {
						goto l96
					}
					position++
				}
			l98:
				add(rulenon_color_suffix, position97)
			}
			return true
		l96:
			position, tokenIndex = position96, tokenIndex96
			return false
		},
		/* 17 delimiter <- <';'> */
		func() bool {
			position104, tokenIndex104 := position, tokenIndex
			{
				position105 := position
				if buffer[position] != rune(';') {
					goto l104
				}
				position++
				add(ruledelimiter, position105)
			}
			return true
		l104:
			position, tokenIndex = position104, tokenIndex104
			return false
		},
		/* 19 Action0 <- <{ p.pushResetColor() }> */
		func() bool {
			{
				add(ruleAction0, position)
			}
			return true
		},
		nil,
		/* 21 Action1 <- <{ p.pushText(text) }> */
		func() bool {
			{
				add(ruleAction1, position)
			}
			return true
		},
		/* 22 Action2 <- <{ p.pushStandardColorWithCategory(text) }> */
		func() bool {
			{
				add(ruleAction2, position)
			}
			return true
		},
		/* 23 Action3 <- <{ p.pushResetForegroundColor() }> */
		func() bool {
			{
				add(ruleAction3, position)
			}
			return true
		},
		/* 24 Action4 <- <{ p.pushResetBackgroundColor() }> */
		func() bool {
			{
				add(ruleAction4, position)
			}
			return true
		},
		/* 25 Action5 <- <{ p.setExtendedColor256(text) }> */
		func() bool {
			{
				add(ruleAction5, position)
			}
			return true
		},
		/* 26 Action6 <- <{ p.setExtendedColorR(text) }> */
		func() bool {
			{
				add(ruleAction6, position)
			}
			return true
		},
		/* 27 Action7 <- <{ p.setExtendedColorG(text) }> */
		func() bool {
			{
				add(ruleAction7, position)
			}
			return true
		},
		/* 28 Action8 <- <{ p.setExtendedColorB(text) }> */
		func() bool {
			{
				add(ruleAction8, position)
			}
			return true
		},
		/* 29 Action9 <- <{ p.pushExtendedColor(text) }> */
		func() bool {
			{
				add(ruleAction9, position)
			}
			return true
		},
		/* 30 Action10 <- <{ p.pushResetColor() }> */
		func() bool {
			{
				add(ruleAction10, position)
			}
			return true
		},
		/* 31 Action11 <- <{ p.pushReverseColor() }> */
		func() bool {
			{
				add(ruleAction11, position)
			}
			return true
		},
	}
	p.rules = _rules
	return nil
}


================================================
FILE: parser/parser.go
================================================
package parser

import (
	"strconv"

	"github.com/jiro4989/textimg/v3/color"
	"github.com/jiro4989/textimg/v3/token"
)

type ParserFunc struct {
	// pegが生成するTokensと名前が衝突するので別名にする
	Tk token.Tokens
}

func Parse(s string) (token.Tokens, error) {
	p := &Parser{Buffer: s}
	if err := p.Init(); err != nil {
		return nil, err
	}
	if err := p.Parse(); err != nil {
		return nil, err
	}

	p.Execute()
	return p.Tk, nil
}

func (p *ParserFunc) pushResetColor() {
	p.Tk = append(p.Tk, token.NewResetColor())
}

func (p *ParserFunc) pushResetForegroundColor() {
	p.Tk = append(p.Tk, token.NewResetForegroundColor())
}

func (p *ParserFunc) pushResetBackgroundColor() {
	p.Tk = append(p.Tk, token.NewResetBackgroundColor())
}

func (p *ParserFunc) pushReverseColor() {
	p.Tk = append(p.Tk, token.NewReverseColor())
}

func (p *ParserFunc) pushText(text string) {
	p.Tk = append(p.Tk, token.NewText(text))
}

func (p *ParserFunc) pushStandardColorWithCategory(text string) {
	p.Tk = append(p.Tk, token.NewStandardColorWithCategory(text))
}

func (p *ParserFunc) pushExtendedColor(text string) {
	p.Tk = append(p.Tk, token.NewExtendedColor(text))
}

func (p *ParserFunc) setExtendedColor256(text string) {
	n, _ := strconv.ParseUint(text, 10, 8)
	p.Tk[len(p.Tk)-1].Color = color.Map256[int(n)]
}

func (p *ParserFunc) setExtendedColorR(text string) {
	n, _ := strconv.ParseUint(text, 10, 8)
	p.Tk[len(p.Tk)-1].Color.R = uint8(n)
}

func (p *ParserFunc) setExtendedColorG(text string) {
	n, _ := strconv.ParseUint(text, 10, 8)
	p.Tk[len(p.Tk)-1].Color.G = uint8(n)
}

func (p *ParserFunc) setExtendedColorB(text string) {
	n, _ := strconv.ParseUint(text, 10, 8)
	p.Tk[len(p.Tk)-1].Color.B = uint8(n)
}


================================================
FILE: parser/parser_test.go
================================================
package parser

import (
	"testing"

	"github.com/jiro4989/textimg/v3/color"
	"github.com/jiro4989/textimg/v3/token"
	"github.com/stretchr/testify/assert"
)

func TestParse(t *testing.T) {
	tests := []struct {
		desc    string
		s       string
		want    token.Tokens
		wantErr bool
	}{
		{
			desc: "正常系: 黒",
			s:    "\x1b[30m",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBABlack,
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 90系と100系",
			s:    "\x1b[90;100m",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBADarkGray,
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeBackground,
					Color:     color.RGBADarkGray,
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 黒赤緑",
			s:    "\x1b[30m\x1b[31m\x1b[32m",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBABlack,
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBARed,
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBAGreen,
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 赤とテキストとリセット",
			s:    "\x1b[31m\n hello\tworld \n\x1b[0m",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBARed,
				},
				{
					Kind: token.KindText,
					Text: "\n hello\tworld \n",
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeReset,
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 39はResetForeground, 49はResetBackground",
			s:    "\x1b[39mReset\x1b[49mReset",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeResetForeground,
				},
				{
					Kind: token.KindText,
					Text: "Reset",
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeResetBackground,
				},
				{
					Kind: token.KindText,
					Text: "Reset",
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 前景色と背景色の同時指定 + リセット省略系",
			s:    "\x1b[32;43mhello world\x1b[m",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBAGreen,
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeBackground,
					Color:     color.RGBAYellow,
				},
				{
					Kind: token.KindText,
					Text: "hello world",
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeReset,
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 0埋めありの指定",
			s:    "\x1b[032;00043mhello world",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBAGreen,
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeBackground,
					Color:     color.RGBAYellow,
				},
				{
					Kind: token.KindText,
					Text: "hello world",
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 拡張系 256色",
			s:    "\x1b[38;5;1m",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.Map256[1],
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 拡張系 RGB指定",
			s:    "\x1b[48;2;1;2;3m",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeBackground,
					Color: color.RGBA{
						R: 1,
						G: 2,
						B: 3,
						A: 255,
					},
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 拡張系の混在",
			s:    "\x1b[38;5;2;48;2;1;2;3mこんばんは",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.Map256[2],
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeBackground,
					Color: color.RGBA{
						R: 1,
						G: 2,
						B: 3,
						A: 255,
					},
				},
				{
					Kind: token.KindText,
					Text: "こんばんは",
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 拡張系の混在 + 0埋め",
			s:    "\x1b[038;005;002;048;002;001;002;003mx1bこんば\nんはx1b",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.Map256[2],
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeBackground,
					Color: color.RGBA{
						R: 1,
						G: 2,
						B: 3,
						A: 255,
					},
				},
				{
					Kind: token.KindText,
					Text: "x1bこんば\nんはx1b",
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 関係ないエスケープシーケンス系無視される",
			s:    "\x1b[1A\x1b[A\x1b[K寿司",
			want: token.Tokens{
				{
					Kind: token.KindText,
					Text: "寿司",
				},
			},
			wantErr: false,
		},
		{
			desc:    "正常系: 空文字列の場合は空",
			s:       "",
			want:    nil,
			wantErr: false,
		},
		{
			desc: "正常系: 不完全なANSIエスケープシーケンスは無視",
			s:    "\x1b[31helloworld\x1b[30m",
			want: token.Tokens{
				{
					Kind: token.KindText,
					Text: "[31helloworld",
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBABlack,
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: 範囲外のエスケープシーケンスの場合は無視",
			s:    "\x1b[310mhelloworld",
			want: token.Tokens{
				{
					Kind: token.KindText,
					Text: "[310mhelloworld",
				},
			},
			wantErr: false,
		},
		{
			desc: "正常系: text_attributes",
			s:    "\x1b[01;31mRED",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeReset,
				},
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.RGBARed,
				},
				{
					Kind: token.KindText,
					Text: "RED",
				},
			},
			wantErr: false,
		},
		{
			desc: "異常系: 拡張系 256色で数値がuint8を超えた場合はMap256の最後の値が設定される",
			s:    "\x1b[38;5;256m",
			want: token.Tokens{
				{
					Kind:      token.KindColor,
					ColorType: token.ColorTypeForeground,
					Color:     color.Map256[255],
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)
			got, err := Parse(tt.s)

			if tt.wantErr {
				assert.Error(err)
				assert.Nil(got)
				return
			}
			assert.Equal(tt.want, got)
		})
	}
}


================================================
FILE: root.go
================================================
package main

import (
	"image/color"
	"runtime"
	"strings"

	"github.com/jiro4989/textimg/v3/config"
	"github.com/jiro4989/textimg/v3/image"
	"github.com/jiro4989/textimg/v3/internal/global"
	"github.com/jiro4989/textimg/v3/parser"

	"github.com/spf13/cobra"
)

var (
	conf    config.Config
	envvars config.EnvVars
)

func init() {
	envvars = config.NewEnvVars()
	cobra.OnInitialize()

	RootCommand.Flags().SortFlags = false
	RootCommand.Flags().StringVarP(&conf.Foreground, "foreground", "g", "white", `foreground text color.
available color types are [black|red|green|yellow|blue|magenta|cyan|white]
or (R,G,B,A(0~255))`)
	RootCommand.Flags().StringVarP(&conf.Background, "background", "b", "black", `background text color.
color types are same as "foreground" option`)

	var font string
	envFontFile := envvars.FontFile
	if envFontFile != "" {
		font = envFontFile
	}
	RootCommand.Flags().StringVarP(&conf.FontFile, "fontfile", "f", font, `font file path.
You can change this default value with environment variables TEXTIMG_FONT_FILE`)
	RootCommand.Flags().IntVarP(&conf.FontIndex, "fontindex", "x", 0, "")
	conf.SetFontFileAndFontIndex(runtime.GOOS)

	envEmojiFontFile := envvars.EmojiFontFile
	RootCommand.Flags().StringVarP(&conf.EmojiFontFile, "emoji-fontfile", "e", envEmojiFontFile, "emoji font file")
	RootCommand.Flags().IntVarP(&conf.EmojiFontIndex, "emoji-fontindex", "X", 0, "")

	RootCommand.Flags().BoolVarP(&conf.UseEmojiFont, "use-emoji-font", "i", false, "use emoji font")
	RootCommand.Flags().BoolVarP(&conf.UseShellgeiEmojiFontfile, "shellgei-emoji-fontfile", "z", false, `emoji font file for shellgei-bot (path: "`+config.ShellgeiEmojiFontPath+`")`)

	RootCommand.Flags().IntVarP(&conf.FontSize, "fontsize", "F", 20, "font size")
	RootCommand.Flags().StringVarP(&conf.Outpath, "out", "o", "", `output image file path.
available image formats are [png | jpg | gif]`)
	RootCommand.Flags().BoolVarP(&conf.AddTimeStamp, "timestamp", "t", false, `add time stamp to output image file path.`)
	RootCommand.Flags().BoolVarP(&conf.SaveNumberedFile, "numbered", "n", false, `add number-suffix to filename when the output file was existed.
ex: t_2.png`)
	RootCommand.Flags().BoolVarP(&conf.UseShellgeiImagedir, "shellgei-imagedir", "s", false, `image directory path (path: "$HOME/Pictures/t.png" or "$TEXTIMG_OUTPUT_DIR/t.png")`)

	RootCommand.Flags().BoolVarP(&conf.UseAnimation, "animation", "a", false, "generate animation gif")
	RootCommand.Flags().IntVarP(&conf.Delay, "delay", "d", 20, "animation delay time")
	RootCommand.Flags().IntVarP(&conf.LineCount, "line-count", "l", 1, "animation input line count")
	RootCommand.Flags().BoolVarP(&conf.UseSlideAnimation, "slide", "S", false, "use slide animation")
	RootCommand.Flags().IntVarP(&conf.SlideWidth, "slide-width", "W", 1, "sliding animation width")
	RootCommand.Flags().BoolVarP(&conf.SlideForever, "forever", "E", false, "sliding forever")
	RootCommand.Flags().BoolVarP(&conf.PrintEnvironments, "environments", "", false, "print environment variables")
	RootCommand.Flags().BoolVarP(&conf.ToSlackIcon, "slack", "", false, "resize to slack icon size (128x128 px)")
	RootCommand.Flags().IntVarP(&conf.ResizeWidth, "resize-width", "", 0, "resize width")
	RootCommand.Flags().IntVarP(&conf.ResizeHeight, "resize-height", "", 0, "resize height")
}

var RootCommand = &cobra.Command{
	Use:     global.AppName,
	Short:   global.AppName + " is command to convert from colored text (ANSI or 256) to image.",
	Example: global.AppName + ` $'\x1b[31mRED\x1b[0m' -o out.png`,
	Version: global.Version,
	RunE: func(cmd *cobra.Command, args []string) error {
		return RunRootCommand(conf, args, envvars)
	},
}

func RunRootCommand(c config.Config, args []string, envs config.EnvVars) error {
	if c.PrintEnvironments {
		config.PrintEnvs()
		return nil
	}

	if err := c.Adjust(args, envs); err != nil {
		return err
	}
	defer c.Writer.Close()

	tokens, err := parser.Parse(strings.Join(c.Texts, "\n"))
	if err != nil {
		return err
	}

	bw := tokens.MaxStringWidth()
	bh := len(tokens.StringLines())

	if !c.ToSlackIcon {
		// TODO: コピペコードになってるので共通化する
		var (
			f          = c.FontSize
			charWidth  = f / 2
			charHeight = int(float64(f) * 1.1)
			w          = bw * charWidth
			h          = bh * charHeight
		)
		c.ResizeWidth, c.ResizeHeight = complementWidthHeight(w, h, c.ResizeWidth, c.ResizeHeight)
	}

	param := &image.ImageParam{
		BaseWidth:          bw,
		BaseHeight:         bh,
		ForegroundColor:    color.RGBA(c.ForegroundColor),
		BackgroundColor:    color.RGBA(c.BackgroundColor),
		FontFace:           c.FontFace,
		EmojiFontFace:      c.EmojiFontFace,
		EmojiDir:           c.EmojiDir,
		FontSize:           c.FontSize,
		Delay:              c.Delay,
		UseAnimation:       c.UseAnimation,
		AnimationLineCount: c.LineCount,
		ResizeWidth:        c.ResizeWidth,
		ResizeHeight:       c.ResizeHeight,
		UseEmoji:           c.UseEmojiFont,
	}
	img := image.NewImage(param)
	if err := img.Draw(tokens); err != nil {
		return err
	}
	if err := img.Encode(c.Writer, c.FileExtension); err != nil {
		return err
	}

	return nil
}

// complementWidthHeight は width, height の片方が 0 の時、サイズを調整する。
func complementWidthHeight(x, y, w, h int) (int, int) {
	if w == 0 {
		hh := y
		d := float64(h) / float64(hh)
		w = int(float64(x) * d)
		return w, h
	}
	if h == 0 {
		ww := x
		d := float64(w) / float64(ww)
		h = int(float64(y) * d)
		return w, h
	}
	return w, h
}


================================================
FILE: root_common_test.go
================================================
package main

import "github.com/jiro4989/textimg/v3/config"

func newDefaultConfig() config.Config {
	return config.Config{
		Foreground:               "white",
		Background:               "black",
		Outpath:                  "",
		AddTimeStamp:             false,
		SaveNumberedFile:         false,
		FontFile:                 "",
		FontIndex:                0,
		EmojiFontFile:            "",
		EmojiFontIndex:           0,
		UseEmojiFont:             false,
		FontSize:                 20,
		UseAnimation:             false,
		Delay:                    20,
		LineCount:                1,
		UseSlideAnimation:        false,
		SlideWidth:               1,
		SlideForever:             false,
		ToSlackIcon:              false,
		PrintEnvironments:        false,
		UseShellgeiImagedir:      false,
		UseShellgeiEmojiFontfile: false,
		ResizeWidth:              0,
		ResizeHeight:             0,
		Writer:                   config.NewMockWriter(false, false),
	}
}


================================================
FILE: root_on_docker_test.go
================================================
//go:build docker

//
// 日本語や絵文字が使えるDocker環境上で実行する想定のテスト。
// どうしてもDocker上でしかテストできないもののみこのファイルに記述する。

package main

import (
	"os"
	"testing"

	"github.com/jiro4989/textimg/v3/config"
	"github.com/stretchr/testify/assert"
)

func TestRunRootCommandOnDocker(t *testing.T) {
	var (
		outDockerDir  = "testdata/out_docker"
		fontFile      = "/tmp/MyricaM.TTC"
		emojiDir      = "/usr/local/src/noto-emoji/png/128"
		emojiFontFile = "/tmp/Symbola_hint.ttf"
	)

	// nolint
	os.Mkdir(outDockerDir, os.ModePerm)

	tests := []struct {
		desc       string
		c          config.Config
		args       []string
		envs       config.EnvVars
		wantErr    bool
		existsFile string
	}{
		{
			desc: "正常系: 日本語や絵文字を描画できる",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDockerDir + "/root_on_docker_test_japanese.png"
				c.Writer = nil
				c.FontFile = fontFile
				// c.EmojiDir = emojiDir
				c.EmojiFontFile = emojiFontFile
				return c
			}(),
			args: []string{"\x1b[31mあいうえお\n\x1b[32;43mあ😃a👍!👀ん👄"},
			envs: config.EnvVars{
				EmojiDir: emojiDir,
			},
			wantErr:    false,
			existsFile: outDockerDir + "/root_on_docker_test_japanese.png",
		},
		{
			desc: "正常系: 絵文字を連続して描画しても背景色が絵文字を上書きしない",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDockerDir + "/root_on_docker_test_emoji.png"
				c.Writer = nil
				c.FontFile = fontFile
				// c.EmojiDir = emojiDir
				c.EmojiFontFile = emojiFontFile
				return c
			}(),
			args: []string{"😃👍👀👄"},
			envs: config.EnvVars{
				EmojiDir: emojiDir,
			},
			wantErr:    false,
			existsFile: outDockerDir + "/root_on_docker_test_emoji.png",
		},
		{
			desc: "正常系: 特殊な絵文字を使う",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDockerDir + "/root_on_docker_test_shellgei_emoji.png"
				c.Writer = nil
				c.UseEmojiFont = true
				c.FontFile = fontFile
				// c.EmojiDir = emojiDir
				c.EmojiFontFile = emojiFontFile
				return c
			}(),
			args: []string{"\x1b[31mあいうえお\n\x1b[32;43mあ😃a👍!👀ん👄"},
			envs: config.EnvVars{
				EmojiDir: emojiDir,
			},
			wantErr:    false,
			existsFile: outDockerDir + "/root_on_docker_test_shellgei_emoji.png",
		},
	}

	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)

			err := RunRootCommand(tt.c, tt.args, tt.envs)
			if tt.wantErr {
				assert.Error(err)
				return
			}
			assert.NoError(err)
			if tt.existsFile != "" {
				_, err := os.Stat(tt.existsFile)
				got := os.IsNotExist(err)
				assert.False(got)
			}
		})
	}
}


================================================
FILE: root_test.go
================================================
//go:build !docker

package main

import (
	"os"
	"testing"

	"github.com/jiro4989/textimg/v3/config"
	"github.com/stretchr/testify/assert"
)

func TestRunRootCommand(t *testing.T) {
	b, _ := os.ReadFile(inDir + "/red_grad.txt")
	grad := string(b)
	b, _ = os.ReadFile(inDir + "/255.txt")
	c255 := string(b)

	tests := []struct {
		desc       string
		c          config.Config
		args       []string
		envs       config.EnvVars
		wantErr    bool
		existsFile string
	}{
		{
			desc: "正常系: PrintEnvironmentsが設定されていると環境変数を出力して終了",
			c: config.Config{
				PrintEnvironments: true,
			},
			args:    []string{"hello"},
			envs:    config.EnvVars{},
			wantErr: false,
		},
		{
			desc: "正常系: 正常系がパスする。出力先はモックWriterなのでファイルは生成されない",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				return c
			}(),
			args:    []string{"hello"},
			envs:    config.EnvVars{},
			wantErr: false,
		},
		{
			desc: "異常系: Writerがエラーを返す",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = "t.png"
				c.Writer = config.NewMockWriter(true, false)
				return c
			}(),
			args:    []string{"hello"},
			envs:    config.EnvVars{},
			wantErr: true,
		},
		// 旧 main_test.go を移行してきたもの
		{
			desc: "正常系: 画像ファイルに出力する",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_font_is_red_and_background_is_black.png"
				c.Writer = nil
				return c
			}(),
			args:       []string{"1234\x1b[31mred\x1b[m5678\nabcd\x1b[32mgreen\x1b[0mefgh\nあい\x1b[33mう\x1b[mえお"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_font_is_red_and_background_is_black.png",
		},
		{
			desc: "正常系: Foregroundのみもとにもどす、Backgroundのみもとに戻す",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_foreground_default_background_default.png"
				c.Writer = nil
				return c
			}(),
			args:       []string{"\x1b[31;42mRedGreen\x1b[39mRedGreen\x1b[49mRedGreen"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_foreground_default_background_default.png",
		},
		{
			desc: "正常系: 256色を使う",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_color_256.png"
				c.Writer = nil
				return c
			}(),
			args:       []string{c255},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_color_256.png",
		},
		{
			desc: "正常系: RGB色を使う",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_color_rgb.png"
				c.Writer = nil
				return c
			}(),
			args:       []string{grad},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_color_rgb.png",
		},
		{
			desc: "正常系: JPEGで出力する",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_jpeg.jpeg"
				c.Writer = nil
				return c
			}(),
			args:       []string{"jpeg"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_jpeg.jpeg",
		},
		{
			desc: "正常系: GIFで出力する",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_gif.gif"
				c.Writer = nil
				return c
			}(),
			args:       []string{"gif"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_gif.gif",
		},
		{
			desc: "正常系: 日本語と絵文字を描画する(ただし豆腐になる)。このテストはDockerの方で実施する",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_tofu.png"
				c.Writer = nil
				return c
			}(),
			args:       []string{"あいうえお👍"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_tofu.png",
		},
		{
			desc: "正常系: 前景色と背景色を反転する",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_reverse.png"
				c.Writer = nil
				return c
			}(),
			args:       []string{"\x1b[31;42mRED\x1b[7m\nGREEN\x1b[0m"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_reverse.png",
		},
		{
			desc: "正常系: 文字色と背景色を変更する",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_font_is_green_and_background_is_blue.png"
				c.Writer = nil
				c.Foreground = "green"
				c.Background = "blue"
				return c
			}(),
			args:       []string{"green"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_font_is_green_and_background_is_blue.png",
		},
		{
			desc: "正常系: カンマ区切りで指定",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_font_is_blue_and_background_is_red.png"
				c.Writer = nil
				c.Foreground = "0,0,255,255"
				c.Background = "255,0,0,255"
				return c
			}(),
			args:       []string{"blue"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_font_is_blue_and_background_is_red.png",
		},
		{
			desc: "正常系: Slackアイコンサイズで生成する",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_font_is_blue_and_background_is_red_slack_icon_size.png"
				c.Writer = nil
				c.Foreground = "0,0,255,255"
				c.Background = "255,0,0,255"
				c.ToSlackIcon = true
				return c
			}(),
			args:       []string{"slack"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_font_is_blue_and_background_is_red_slack_icon_size.png",
		},
		{
			desc: "正常系: 明示的に幅を指定できる",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_font_is_blue_and_background_is_red_100x200.png"
				c.Writer = nil
				c.Foreground = "0,0,255,255"
				c.Background = "255,0,0,255"
				c.ResizeWidth = 100
				c.ResizeHeight = 200
				return c
			}(),
			args:       []string{"100x200"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_font_is_blue_and_background_is_red_100x200.png",
		},
		{
			desc: "正常系: Widthのみを指定した場合はHeightが調整される",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_font_is_blue_and_background_is_red_100w.png"
				c.Writer = nil
				c.Foreground = "0,0,255,255"
				c.Background = "255,0,0,255"
				c.ResizeWidth = 100
				return c
			}(),
			args:       []string{"100w"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_font_is_blue_and_background_is_red_100w.png",
		},
		{
			desc: "正常系: Heightのみを指定した場合はWidthが調整される",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_font_is_blue_and_background_is_red_100h.png"
				c.Writer = nil
				c.Foreground = "0,0,255,255"
				c.Background = "255,0,0,255"
				c.ResizeHeight = 100
				return c
			}(),
			args:       []string{"100h"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_font_is_blue_and_background_is_red_100h.png",
		},
		{
			desc: "正常系: 1行のアニメを生成できる",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_animation_1_line.gif"
				c.Writer = nil
				c.UseAnimation = true
				c.LineCount = 1
				return c
			}(),
			args:       []string{"\x1b[31m1\n\x1b[32m2\n\x1b[33m3\n\x1b[34m4"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_animation_1_line.gif",
		},
		{
			desc: "正常系: 2行のアニメを生成できる",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_animation_2_line.gif"
				c.Writer = nil
				c.UseAnimation = true
				c.LineCount = 2
				return c
			}(),
			args:       []string{"\x1b[31m1\n\x1b[32m2\n\x1b[33m3\n\x1b[34m4"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_animation_2_line.gif",
		},
		{
			desc: "正常系: 4行のアニメを生成できる",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_animation_4_line.gif"
				c.Writer = nil
				c.UseAnimation = true
				c.LineCount = 4
				return c
			}(),
			args:       []string{"\x1b[31m1\n\x1b[32m2\n\x1b[33m3\n\x1b[34m4\n\x1b[31m5\n\x1b[32m6\n\x1b[33m7\n\x1b[34m8"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_animation_4_line.gif",
		},
		{
			desc: "正常系: 8行のアニメを生成できる",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_animation_8_line.gif"
				c.Writer = nil
				c.UseAnimation = true
				c.LineCount = 8
				return c
			}(),
			args:       []string{"\x1b[31m1\n\x1b[32m2\n\x1b[33m3\n\x1b[34m4\n\x1b[31m5\n\x1b[32m6\n\x1b[33m7\n\x1b[34m8"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_animation_8_line.gif",
		},
		{
			desc: "正常系: 4行のアニメを2行ずつスライドする",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_animation_4_line_slide_2_forever.gif"
				c.Writer = nil
				c.UseAnimation = true
				c.LineCount = 4
				c.UseSlideAnimation = true
				c.SlideWidth = 2
				c.SlideForever = true
				c.Delay = 100
				return c
			}(),
			args:       []string{"\x1b[31m1\n\x1b[32m2\n\x1b[33m3\n\x1b[34m4\n\x1b[31m5\n\x1b[32m6\n\x1b[33m7\n\x1b[34m8"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_animation_4_line_slide_2_forever.gif",
		},
		{
			desc: "正常系: 4行のアニメを3行ずつスライドする",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_animation_4_line_slide_3_forever.gif"
				c.Writer = nil
				c.UseAnimation = true
				c.LineCount = 4
				c.UseSlideAnimation = true
				c.SlideWidth = 3
				c.SlideForever = true
				c.Delay = 100
				return c
			}(),
			args:       []string{"\x1b[31m1\n\x1b[32m2\n\x1b[33m3\n\x1b[34m4\n\x1b[31m5\n\x1b[32m6\n\x1b[33m7\n\x1b[34m8"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_animation_4_line_slide_3_forever.gif",
		},
		{
			desc: "正常系: SlackアイコンサイズでアニメーションGIFを生成できる",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_slack_icon_size_animation.gif"
				c.Writer = nil
				c.ToSlackIcon = true
				c.UseAnimation = true
				return c
			}(),
			args:       []string{"1\n2\n3\n4"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_slack_icon_size_animation.gif",
		},
		{
			desc: "正常系: すでに同名のファイルが存在する時、別名で保存される",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_numbering.png"
				c.Writer = nil
				c.SaveNumberedFile = true
				return c
			}(),
			args:       []string{"number"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_numbering.png",
		},
		{
			desc: "正常系: すでに同名のファイルが存在する時、別名で保存される_2",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_numbering.png"
				c.Writer = nil
				c.SaveNumberedFile = true
				return c
			}(),
			args:       []string{"number"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_numbering_2.png",
		},
		{
			desc: "正常系: すでに同名のファイルが存在する時、別名で保存される_3",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_numbering.png"
				c.Writer = nil
				c.SaveNumberedFile = true
				return c
			}(),
			args:       []string{"number"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_numbering_3.png",
		},
		{
			desc: "正常系: フォントインデックスを指定できる",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_index.png"
				c.Writer = nil
				c.FontIndex = 0
				c.EmojiFontIndex = 0
				return c
			}(),
			args:       []string{"index"},
			envs:       config.EnvVars{},
			wantErr:    false,
			existsFile: outDir + "/root_test_index.png",
		},
		{
			desc: "異常系: 空文字列は不正",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_empty_string.png"
				c.Writer = nil
				return c
			}(),
			args:    []string{""},
			envs:    config.EnvVars{},
			wantErr: true,
		},
		{
			desc: "異常系: 改行文字のみは不正",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_only_line.png"
				c.Writer = nil
				return c
			}(),
			args:    []string{"\n\n\n"},
			envs:    config.EnvVars{},
			wantErr: true,
		},
		{
			desc: "異常系: 色文字列が不正",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_numbering.png"
				c.Writer = nil
				c.Foreground = "ggg"
				return c
			}(),
			args:    []string{"ggg"},
			envs:    config.EnvVars{},
			wantErr: true,
		},
		{
			desc: "異常系: 背景色が不正",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_numbering.png"
				c.Writer = nil
				c.Background = "ggg"
				return c
			}(),
			args:    []string{"ggg"},
			envs:    config.EnvVars{},
			wantErr: true,
		},
		{
			desc: "異常系: 不正なフォント指定",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_numbering.png"
				c.Writer = nil
				c.FontFile = inDir + "/illegal_font.ttc"
				return c
			}(),
			args:    []string{"ggg"},
			envs:    config.EnvVars{},
			wantErr: true,
		},
		{
			desc: "異常系: 不正な絵文字フォント指定",
			c: func() config.Config {
				c := newDefaultConfig()
				c.Outpath = outDir + "/root_test_numbering.png"
				c.Writer = nil
				c.EmojiFontFile = inDir + "/illegal_font.ttc"
				return c
			}(),
			args:    []string{"ggg"},
			envs:    config.EnvVars{},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)

			err := RunRootCommand(tt.c, tt.args, tt.envs)
			if tt.wantErr {
				assert.Error(err)
				return
			}
			assert.NoError(err)
			if tt.existsFile != "" {
				_, err := os.Stat(tt.existsFile)
				got := os.IsNotExist(err)
				assert.False(got)
			}
		})
	}
}

func TestComplementWidthHeight(t *testing.T) {
	type TestData struct {
		desc       string
		x, y, w, h int
		wantWidth  int
		wantHeight int
	}
	tds := []TestData{
		{
			desc:       "正常系: wが0のときはwidthが調整される",
			x:          200,
			y:          100,
			w:          0,
			h:          200,
			wantWidth:  400,
			wantHeight: 200,
		},
		{
			desc:       "正常系: hが0のときはheightが調整される",
			x:          200,
			y:          100,
			w:          100,
			h:          0,
			wantWidth:  100,
			wantHeight: 50,
		},
		{
			desc:       "正常系: hが0のときはheightが調整される",
			x:          200,
			y:          100,
			w:          100,
			h:          0,
			wantWidth:  100,
			wantHeight: 50,
		},
		{
			desc:       "正常系: wとhが0出ないときはwとhが返る",
			x:          200,
			y:          100,
			w:          400,
			h:          300,
			wantWidth:  400,
			wantHeight: 300,
		},
	}
	for _, v := range tds {
		t.Run(v.desc, func(t *testing.T) {
			a := assert.New(t)
			w, h := complementWidthHeight(v.x, v.y, v.w, v.h)
			a.Equal(v.wantWidth, w)
			a.Equal(v.wantHeight, h)
		})
	}
}


================================================
FILE: scripts/fetch_color.sh
================================================
#!/bin/bash

curl "https://jonasjacek.github.io/colors/" \
  | grep style \
  | sed -r 's@.*<td>([0-9]+)</td>.*<td>(rgb[^<]+)</td>.*@\1:\2@g' \
  | sed -re 's@rgb\(@color.RGBA{@g' -e 's/\)/,255},/g'


================================================
FILE: scripts/width/main.go
================================================
// width は文字のrunewidthが返す文字幅を確認するためのツール。

/*

使い方

cd tools/width
go build .
./width あいうえお■漢字abcde😲

*/

package main

import (
	"fmt"
	"os"

	"github.com/mattn/go-runewidth"
)

func main() {
	args := os.Args
	fmt.Println("Char CodePoint Width")
	runewidth.DefaultCondition.StrictEmojiNeutral = false
	for _, c := range args[1] {
		text := fmt.Sprintf("%v %d %d", string(c), c, runewidth.RuneWidth(c))
		fmt.Println(text)
	}
}


================================================
FILE: testdata/in/255.txt
================================================
000001002003004005006007008009010011012013014015
016017018019020021022023024025026027028029030031
032033034035036037038039040041042043044045046047
048049050051052053054055056057058059060061062063
064065066067068069070071072073074075076077078079
080081082083084085086087088089090091092093094095
096097098099100101102103104105106107108109110111
112113114115116117118119120121122123124125126127
128129130131132133134135136137138139140141142143
144145146147148149150151152153154155156157158159
160161162163164165166167168169170171172173174175
176177178179180181182183184185186187188189190191
192193194195196197198199200201202203204205206207
208209210211212213214215216217218219220221222223
224225226227228229230231232233234235236237238239
240241242243244245246247248249250251252253254255


================================================
FILE: testdata/in/illegal_font.otc
================================================
2


================================================
FILE: testdata/in/illegal_font.ttc
================================================
1


================================================
FILE: testdata/in/illegal_font.txt
================================================
3


================================================
FILE: testdata/in/red_grad.txt
================================================
000001002003004005006007008009010011012013014015
016017018019020021022023024025026027028029030031
032033034035036037038039040041042043044045046047
048049050051052053054055056057058059060061062063
064065066067068069070071072073074075076077078079
080081082083084085086087088089090091092093094095
096097098099100101102103104105106107108109110111
112113114115116117118119120121122123124125126127
128129130131132133134135136137138139140141142143
144145146147148149150151152153154155156157158159
160161162163164165166167168169170171172173174175
176177178179180181182183184185186187188189190191
192193194195196197198199200201202203204205206207
208209210211212213214215216217218219220221222223
224225226227228229230231232233234235236237238239
240241242243244245246247248249250251252253254255


================================================
FILE: token/token.go
================================================
package token

import (
	"strconv"
	"strings"

	"github.com/jiro4989/textimg/v3/color"
	"github.com/mattn/go-runewidth"
)

type (
	Kind      int
	ColorType int
	Token     struct {
		Kind      Kind
		ColorType ColorType
		Color     color.RGBA
		Text      string
	}
	Tokens []Token
)

const (
	KindEmpty Kind = iota
	KindText
	KindColor
	KindNotColor

	ColorTypeReset       ColorType = iota // \x1b[0m 指定をリセット
	ColorTypeBold                         // \x1b[1m 太字
	ColorTypeDim                          // \x1b[2m 薄く表示
	ColorTypeItalic                       // \x1b[3m イタリック
	ColorTypeUnderline                    // \x1b[4m アンダーライン
	ColorTypeBlink                        // \x1b[5m ブリンク
	ColorTypeSpeedyBlink                  // \x1b[6m 高速ブリンク
	ColorTypeReverse                      // \x1b[7m 文字色と背景色の反転
	ColorTypeHide                         // \x1b[8m 表示を隠す
	ColorTypeDelete                       // \x1b[9m 取り消し
	ColorTypeForeground
	ColorTypeBackground
	ColorTypeResetForeground
	ColorTypeResetBackground
)

func init() {
	// Unicode Neutral で定義されている絵文字(例: 👁)を幅2として扱う
	runewidth.DefaultCondition.StrictEmojiNeutral = false
}

func NewResetColor() Token {
	return Token{
		Kind:      KindColor,
		ColorType: ColorTypeReset,
	}
}

func NewResetForegroundColor() Token {
	return Token{
		Kind:      KindColor,
		ColorType: ColorTypeResetForeground,
	}
}

func NewResetBackgroundColor() Token {
	return Token{
		Kind:      KindColor,
		ColorType: ColorTypeResetBackground,
	}
}

func NewReverseColor() Token {
	return Token{
		Kind:      KindColor,
		ColorType: ColorTypeReverse,
	}
}

func NewText(text string) Token {
	return Token{
		Kind: KindText,
		Text: text,
	}
}

func NewStandardColorWithCategory(text string) Token {
	n, _ := strconv.Atoi(text)
	return Token{
		Kind:      KindColor,
		ColorType: colorType(n),
		Color:     color.ANSIMap[n],
	}
}

func NewExtendedColor(text string) Token {
	n, _ := strconv.Atoi(text)
	return Token{
		Kind:      KindColor,
		ColorType: colorType(n),
		Color:     color.RGBA{A: 255},
	}
}

func colorType(n int) ColorType {
	var t ColorType
	switch n / 10 {
	case 3, 9:
		t = ColorTypeForeground
	case 4, 10:
		t = ColorTypeBackground
	}
	return t
}

func (t *Tokens) MaxStringWidth() int {
	var strs []string
	for _, tt := range *t {
		if tt.Kind != KindText {
			continue
		}
		s := tt.Text
		strs = append(strs, s)
	}
	s := strings.Join(strs, "")
	lines := strings.Split(s, "\n")
	var max int
	for _, line := range lines {
		w := runewidth.StringWidth(line)
		if max < w {
			max = w
		}
	}
	return max
}

func (t *Tokens) StringLines() []string {
	var strs []string
	for _, tt := range *t {
		if tt.Kind != KindText {
			continue
		}
		s := tt.Text
		strs = append(strs, s)
	}
	s := strings.Join(strs, "")
	lines := strings.Split(s, "\n")
	return lines
}


================================================
FILE: token/token_test.go
================================================
package token

import (
	"testing"

	"github.com/jiro4989/textimg/v3/color"
	"github.com/stretchr/testify/assert"
)

func TestToken_MaxStringWidth(t *testing.T) {
	tests := []struct {
		desc string
		t    Tokens
		want int
	}{
		{
			desc: "正常系: hello = 5",
			t: Tokens{
				{
					Kind: KindText,
					Text: "hello",
				},
			},
			want: 5,
		},
		{
			desc: "正常系: あいうえお = 10",
			t: Tokens{
				{
					Kind: KindText,
					Text: "あいうえお",
				},
			},
			want: 10,
		},
		{
			desc: "正常系: he\nllo = 3",
			t: Tokens{
				{
					Kind: KindText,
					Text: "he\nllo",
				},
			},
			want: 3,
		},
		{
			desc: "正常系: hel\nlo = 3",
			t: Tokens{
				{
					Kind: KindText,
					Text: "hel\nlo",
				},
			},
			want: 3,
		},
		{
			desc: "正常系: aREDb = 5",
			t: Tokens{
				{
					Kind: KindText,
					Text: "a",
				},
				{
					Kind:      KindColor,
					ColorType: ColorTypeForeground,
					Color:     color.RGBARed,
				},
				{
					Kind: KindText,
					Text: "RED",
				},
				{
					Kind:      KindColor,
					ColorType: ColorTypeReset,
				},
				{
					Kind: KindText,
					Text: "b",
				},
			},
			want: 5,
		},
	}
	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)
			got := tt.t.MaxStringWidth()
			assert.Equal(tt.want, got)
		})
	}
}

func TestToken_StringLines(t *testing.T) {
	tests := []struct {
		desc string
		t    Tokens
		want []string
	}{
		{
			desc: "正常系: hello",
			t: Tokens{
				{
					Kind: KindText,
					Text: "hello",
				},
			},
			want: []string{"hello"},
		},
		{
			desc: "正常系: hel\nlo\nworld",
			t: Tokens{
				{
					Kind: KindText,
					Text: "hel\nlo\nworld",
				},
			},
			want: []string{"hel", "lo", "world"},
		},
		{
			desc: "正常系: aREDb = 5",
			t: Tokens{
				{
					Kind: KindText,
					Text: "a",
				},
				{
					Kind:      KindColor,
					ColorType: ColorTypeForeground,
					Color:     color.RGBARed,
				},
				{
					Kind: KindText,
					Text: "RED",
				},
				{
					Kind:      KindColor,
					ColorType: ColorTypeReset,
				},
				{
					Kind: KindText,
					Text: "b",
				},
			},
			want: []string{"aREDb"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.desc, func(t *testing.T) {
			assert := assert.New(t)
			got := tt.t.StringLines()
			assert.Equal(tt.want, got)
		})
	}
}
Download .txt
gitextract_1f4t7hds/

├── .chglog/
│   ├── CHANGELOG.tpl.md
│   └── config.yml
├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   ├── pr-labeler.yml
│   └── workflows/
│       ├── auto_merge.yml
│       ├── pr-labeler.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── color/
│   └── color.go
├── completions/
│   ├── fish/
│   │   └── textimg.fish
│   └── zsh/
│       └── _textimg
├── config/
│   ├── config.go
│   ├── config_test.go
│   ├── envvar.go
│   ├── face.go
│   ├── face_test.go
│   └── writer_mock.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── image/
│   ├── encode.go
│   ├── image.go
│   └── util.go
├── images/
│   └── .gitkeep
├── internal/
│   └── global/
│       ├── env.go
│       └── version.go
├── log/
│   ├── log.go
│   └── log_test.go
├── main.go
├── main_test.go
├── parser/
│   ├── grammar.peg
│   ├── grammar.peg.go
│   ├── parser.go
│   └── parser_test.go
├── root.go
├── root_common_test.go
├── root_on_docker_test.go
├── root_test.go
├── scripts/
│   ├── fetch_color.sh
│   └── width/
│       └── main.go
├── testdata/
│   └── in/
│       ├── 255.txt
│       ├── illegal_font.otc
│       ├── illegal_font.ttc
│       ├── illegal_font.txt
│       └── red_grad.txt
└── token/
    ├── token.go
    └── token_test.go
Download .txt
SYMBOL INDEX (216 symbols across 26 files)

FILE: color/color.go
  type RGBA (line 7) | type RGBA

FILE: config/config.go
  type Config (line 19) | type Config struct
    method Adjust (line 73) | func (a *Config) Adjust(args []string, ev EnvVars) error {
    method SetFontFileAndFontIndex (line 155) | func (a *Config) SetFontFileAndFontIndex(runtimeOS string) {
    method addTimeStampToOutPath (line 206) | func (a *Config) addTimeStampToOutPath(t time.Time) {
    method addNumberSuffixToOutPath (line 218) | func (a *Config) addNumberSuffixToOutPath() {
    method setWriter (line 242) | func (a *Config) setWriter() error {
  type osDefaultFont (line 54) | type osDefaultFont struct
  constant ShellgeiEmojiFontPath (line 60) | ShellgeiEmojiFontPath = "/usr/share/fonts/truetype/ancient-scripts/Symbo...
  constant defaultWindowsFont (line 63) | defaultWindowsFont = `C:\Windows\Fonts\msgothic.ttc`
  constant defaultDarwinFont (line 64) | defaultDarwinFont  = "/System/Library/Fonts/AppleSDGothicNeo.ttc"
  constant defaultIOSFont (line 65) | defaultIOSFont     = "/System/Library/Fonts/Core/AppleSDGothicNeo.ttc"
  constant defaultAndroidFont (line 66) | defaultAndroidFont = "/system/fonts/NotoSansCJK-Regular.ttc"
  constant defaultLinuxFont1 (line 67) | defaultLinuxFont1  = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular...
  constant defaultLinuxFont2 (line 68) | defaultLinuxFont2  = "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"
  function validateInputText (line 276) | func validateInputText(texts []string) error {
  function validateFileExtension (line 291) | func validateFileExtension(ext string) error {
  function normalizeTexts (line 303) | func normalizeTexts(texts []string) []string {
  function readInputText (line 319) | func readInputText(args []string) []string {
  function outputImageDir (line 332) | func outputImageDir(outDir string, useAnimation bool) (string, error) {
  function optionColorStringToRGBA (line 353) | func optionColorStringToRGBA(colstr string) (color.RGBA, error) {
  function toSlideStrings (line 405) | func toSlideStrings(src []string, lineCount, slideWidth int, slideForeve...
  function removeZeroWidthCharacters (line 447) | func removeZeroWidthCharacters(s string) string {
  function readStdin (line 468) | func readStdin() (ret []string) {

FILE: config/config_test.go
  function newDefaultConfig (line 13) | func newDefaultConfig() Config {
  function TestConfig_Adjust (line 42) | func TestConfig_Adjust(t *testing.T) {
  function TestOptionColorStringToRGBA (line 327) | func TestOptionColorStringToRGBA(t *testing.T) {
  function TestToSlideStrings (line 374) | func TestToSlideStrings(t *testing.T) {
  function TestRemoveZeroWidthCharacters (line 587) | func TestRemoveZeroWidthCharacters(t *testing.T) {
  function TestApplicationConfigSetFontFileAndFontIndex (line 607) | func TestApplicationConfigSetFontFileAndFontIndex(t *testing.T) {
  function TestApplicationConfig_AddTimeStampToOutPath (line 673) | func TestApplicationConfig_AddTimeStampToOutPath(t *testing.T) {
  function TestOutputImageDir (line 733) | func TestOutputImageDir(t *testing.T) {
  function TestApplicationConfig_AddNumberSuffixToOutPath (line 785) | func TestApplicationConfig_AddNumberSuffixToOutPath(t *testing.T) {

FILE: config/envvar.go
  type EnvVars (line 8) | type EnvVars struct
  constant envNameOutputDir (line 16) | envNameOutputDir     = "TEXTIMG_OUTPUT_DIR"
  constant envNameFontFile (line 17) | envNameFontFile      = "TEXTIMG_FONT_FILE"
  constant envNameEmojiDir (line 18) | envNameEmojiDir      = "TEXTIMG_EMOJI_DIR"
  constant envNameEmojiFontFile (line 19) | envNameEmojiFontFile = "TEXTIMG_EMOJI_FONT_FILE"
  function NewEnvVars (line 31) | func NewEnvVars() EnvVars {
  function PrintEnvs (line 40) | func PrintEnvs() {

FILE: config/face.go
  function readFace (line 15) | func readFace(fontPath string, fontIndex int, fontSize float64) (font.Fa...

FILE: config/face_test.go
  function TestReadFace (line 10) | func TestReadFace(t *testing.T) {

FILE: config/writer_mock.go
  type MockWriter (line 8) | type MockWriter struct
    method Write (line 20) | func (m *MockWriter) Write(p []byte) (n int, err error) {
    method Close (line 27) | func (m *MockWriter) Close() error {
  function NewMockWriter (line 13) | func NewMockWriter(w, c bool) io.WriteCloser {

FILE: image/encode.go
  method Encode (line 14) | func (i *Image) Encode(w io.Writer, ext string) error {
  function toPalettes (line 37) | func toPalettes(imgs []image.Image) (ret []*image.Paletted) {

FILE: image/image.go
  type Image (line 19) | type Image struct
    method Draw (line 107) | func (i *Image) Draw(tokens token.Tokens) error {
    method drawBackgroundAll (line 159) | func (i *Image) drawBackgroundAll() {
    method updateColor (line 172) | func (i *Image) updateColor(t token.ColorType, col color.RGBA) {
    method resetColor (line 189) | func (i *Image) resetColor() {
    method resetPosition (line 194) | func (i *Image) resetPosition() {
    method newDrawer (line 200) | func (i *Image) newDrawer(f font.Face) *font.Drawer {
    method draw (line 217) | func (i *Image) draw(r rune) error {
    method setAnimationFlames (line 229) | func (i *Image) setAnimationFlames() error {
    method drawRune (line 260) | func (i *Image) drawRune(r rune, f font.Face) {
    method drawEmoji (line 265) | func (i *Image) drawEmoji(r rune, path string) error {
    method drawBackground (line 290) | func (i *Image) drawBackground(s string) {
    method moveRight (line 305) | func (i *Image) moveRight(r rune) {
    method moveDown (line 309) | func (i *Image) moveDown() {
    method newScaledImage (line 315) | func (i *Image) newScaledImage() *image.RGBA {
    method scale (line 332) | func (i *Image) scale() {
  type ImageParam (line 43) | type ImageParam struct
  function init (line 61) | func init() {
  function NewImage (line 66) | func NewImage(p *ImageParam) *Image {
  function newImage (line 103) | func newImage(w, h int) *image.RGBA {
  function scale (line 325) | func scale(img image.Image, w, h int) *image.RGBA {

FILE: image/util.go
  function isEmoji (line 31) | func isEmoji(r rune, emojiDir string) (bool, string) {
  function isExceptionallyCodePoint (line 46) | func isExceptionallyCodePoint(r rune) bool {
  function isLinefeed (line 56) | func isLinefeed(r rune) bool {

FILE: internal/global/env.go
  constant EnvNameOutputDir (line 4) | EnvNameOutputDir     = "TEXTIMG_OUTPUT_DIR"
  constant EnvNameFontFile (line 5) | EnvNameFontFile      = "TEXTIMG_FONT_FILE"
  constant EnvNameEmojiDir (line 6) | EnvNameEmojiDir      = "TEXTIMG_EMOJI_DIR"
  constant EnvNameEmojiFontFile (line 7) | EnvNameEmojiFontFile = "TEXTIMG_EMOJI_FONT_FILE"

FILE: internal/global/version.go
  constant AppName (line 4) | AppName = "textimg"
  constant Version (line 5) | Version = `3.1.10

FILE: log/log.go
  constant debugPrefix (line 13) | debugPrefix = "[DEBUG]"
  constant infoPrefix (line 14) | infoPrefix  = "[INFO]"
  constant warnPrefix (line 15) | warnPrefix  = "[WARN]"
  constant errorPrefix (line 16) | errorPrefix = "[ERROR]"
  function log (line 19) | func log(lvl string, msg interface{}) {
  function Debug (line 31) | func Debug(msg interface{}) {
  function Info (line 35) | func Info(msg interface{}) {
  function Warn (line 39) | func Warn(msg interface{}) {
  function Warnf (line 43) | func Warnf(format string, msg interface{}) {
  function Error (line 48) | func Error(msg interface{}) {

FILE: log/log_test.go
  function TestDebug (line 7) | func TestDebug(t *testing.T) {

FILE: main.go
  function main (line 7) | func main() {
  function Main (line 11) | func Main() int {

FILE: main_test.go
  constant inDir (line 11) | inDir  = "testdata/in"
  constant outDir (line 12) | outDir = "testdata/out"
  function TestMain (line 15) | func TestMain(m *testing.M) {
  function testBefore (line 21) | func testBefore() {

FILE: parser/grammar.peg.go
  constant endSymbol (line 14) | endSymbol rune = 1114112
  type pegRule (line 17) | type pegRule
  constant ruleUnknown (line 20) | ruleUnknown pegRule = iota
  constant ruleroot (line 21) | ruleroot
  constant ruleignore (line 22) | ruleignore
  constant rulecolors (line 23) | rulecolors
  constant ruletext (line 24) | ruletext
  constant rulecolor (line 25) | rulecolor
  constant rulestandard_color (line 26) | rulestandard_color
  constant ruleextended_color (line 27) | ruleextended_color
  constant ruleextended_color_256 (line 28) | ruleextended_color_256
  constant ruleextended_color_rgb (line 29) | ruleextended_color_rgb
  constant ruleextended_color_prefix (line 30) | ruleextended_color_prefix
  constant ruletext_attributes (line 31) | ruletext_attributes
  constant rulezero (line 32) | rulezero
  constant rulenumber (line 33) | rulenumber
  constant ruleprefix (line 34) | ruleprefix
  constant ruleescape_sequence (line 35) | ruleescape_sequence
  constant rulecolor_suffix (line 36) | rulecolor_suffix
  constant rulenon_color_suffix (line 37) | rulenon_color_suffix
  constant ruledelimiter (line 38) | ruledelimiter
  constant ruleAction0 (line 39) | ruleAction0
  constant rulePegText (line 40) | rulePegText
  constant ruleAction1 (line 41) | ruleAction1
  constant ruleAction2 (line 42) | ruleAction2
  constant ruleAction3 (line 43) | ruleAction3
  constant ruleAction4 (line 44) | ruleAction4
  constant ruleAction5 (line 45) | ruleAction5
  constant ruleAction6 (line 46) | ruleAction6
  constant ruleAction7 (line 47) | ruleAction7
  constant ruleAction8 (line 48) | ruleAction8
  constant ruleAction9 (line 49) | ruleAction9
  constant ruleAction10 (line 50) | ruleAction10
  constant ruleAction11 (line 51) | ruleAction11
  type token32 (line 89) | type token32 struct
    method String (line 94) | func (t *token32) String() string {
  type node32 (line 98) | type node32 struct
    method print (line 103) | func (node *node32) print(w io.Writer, pretty bool, buffer string) {
    method Print (line 126) | func (node *node32) Print(w io.Writer, buffer string) {
    method PrettyPrint (line 130) | func (node *node32) PrettyPrint(w io.Writer, buffer string) {
  type tokens32 (line 134) | type tokens32 struct
    method Trim (line 138) | func (t *tokens32) Trim(length uint32) {
    method Print (line 142) | func (t *tokens32) Print() {
    method AST (line 148) | func (t *tokens32) AST() *node32 {
    method PrintSyntaxTree (line 173) | func (t *tokens32) PrintSyntaxTree(buffer string) {
    method WriteSyntaxTree (line 177) | func (t *tokens32) WriteSyntaxTree(w io.Writer, buffer string) {
    method PrettyPrintSyntaxTree (line 181) | func (t *tokens32) PrettyPrintSyntaxTree(buffer string) {
    method Add (line 185) | func (t *tokens32) Add(rule pegRule, begin, end, index uint32) {
    method Tokens (line 194) | func (t *tokens32) Tokens() []token32 {
  type Parser (line 198) | type Parser struct
    method Parse (line 210) | func (p *Parser) Parse(rule ...int) error {
    method Reset (line 214) | func (p *Parser) Reset() {
    method PrintSyntaxTree (line 278) | func (p *Parser) PrintSyntaxTree() {
    method WriteSyntaxTree (line 286) | func (p *Parser) WriteSyntaxTree(w io.Writer) {
    method SprintSyntaxTree (line 290) | func (p *Parser) SprintSyntaxTree() string {
    method Execute (line 296) | func (p *Parser) Execute() {
    method Init (line 348) | func (p *Parser) Init(options ...func(*Parser) error) error {
  type textPosition (line 218) | type textPosition struct
  type textPositionMap (line 222) | type textPositionMap
  function translatePositions (line 224) | func translatePositions(buffer []rune, positions []int) textPositionMap {
  type parseError (line 249) | type parseError struct
    method Error (line 254) | func (e *parseError) Error() string {
  function Pretty (line 335) | func Pretty(pretty bool) func(*Parser) error {
  function Size (line 342) | func Size(size int) func(*Parser) error {

FILE: parser/parser.go
  type ParserFunc (line 10) | type ParserFunc struct
    method pushResetColor (line 28) | func (p *ParserFunc) pushResetColor() {
    method pushResetForegroundColor (line 32) | func (p *ParserFunc) pushResetForegroundColor() {
    method pushResetBackgroundColor (line 36) | func (p *ParserFunc) pushResetBackgroundColor() {
    method pushReverseColor (line 40) | func (p *ParserFunc) pushReverseColor() {
    method pushText (line 44) | func (p *ParserFunc) pushText(text string) {
    method pushStandardColorWithCategory (line 48) | func (p *ParserFunc) pushStandardColorWithCategory(text string) {
    method pushExtendedColor (line 52) | func (p *ParserFunc) pushExtendedColor(text string) {
    method setExtendedColor256 (line 56) | func (p *ParserFunc) setExtendedColor256(text string) {
    method setExtendedColorR (line 61) | func (p *ParserFunc) setExtendedColorR(text string) {
    method setExtendedColorG (line 66) | func (p *ParserFunc) setExtendedColorG(text string) {
    method setExtendedColorB (line 71) | func (p *ParserFunc) setExtendedColorB(text string) {
  function Parse (line 15) | func Parse(s string) (token.Tokens, error) {

FILE: parser/parser_test.go
  function TestParse (line 11) | func TestParse(t *testing.T) {

FILE: root.go
  function init (line 21) | func init() {
  function RunRootCommand (line 79) | func RunRootCommand(c config.Config, args []string, envs config.EnvVars)...
  function complementWidthHeight (line 138) | func complementWidthHeight(x, y, w, h int) (int, int) {

FILE: root_common_test.go
  function newDefaultConfig (line 5) | func newDefaultConfig() config.Config {

FILE: root_on_docker_test.go
  function TestRunRootCommandOnDocker (line 17) | func TestRunRootCommandOnDocker(t *testing.T) {

FILE: root_test.go
  function TestRunRootCommand (line 13) | func TestRunRootCommand(t *testing.T) {
  function TestComplementWidthHeight (line 526) | func TestComplementWidthHeight(t *testing.T) {

FILE: scripts/width/main.go
  function main (line 22) | func main() {

FILE: token/token.go
  type Kind (line 12) | type Kind
  type ColorType (line 13) | type ColorType
  type Token (line 14) | type Token struct
  type Tokens (line 20) | type Tokens
    method MaxStringWidth (line 114) | func (t *Tokens) MaxStringWidth() int {
    method StringLines (line 135) | func (t *Tokens) StringLines() []string {
  constant KindEmpty (line 24) | KindEmpty Kind = iota
  constant KindText (line 25) | KindText
  constant KindColor (line 26) | KindColor
  constant KindNotColor (line 27) | KindNotColor
  constant ColorTypeReset (line 29) | ColorTypeReset       ColorType = iota
  constant ColorTypeBold (line 30) | ColorTypeBold
  constant ColorTypeDim (line 31) | ColorTypeDim
  constant ColorTypeItalic (line 32) | ColorTypeItalic
  constant ColorTypeUnderline (line 33) | ColorTypeUnderline
  constant ColorTypeBlink (line 34) | ColorTypeBlink
  constant ColorTypeSpeedyBlink (line 35) | ColorTypeSpeedyBlink
  constant ColorTypeReverse (line 36) | ColorTypeReverse
  constant ColorTypeHide (line 37) | ColorTypeHide
  constant ColorTypeDelete (line 38) | ColorTypeDelete
  constant ColorTypeForeground (line 39) | ColorTypeForeground
  constant ColorTypeBackground (line 40) | ColorTypeBackground
  constant ColorTypeResetForeground (line 41) | ColorTypeResetForeground
  constant ColorTypeResetBackground (line 42) | ColorTypeResetBackground
  function init (line 45) | func init() {
  function NewResetColor (line 50) | func NewResetColor() Token {
  function NewResetForegroundColor (line 57) | func NewResetForegroundColor() Token {
  function NewResetBackgroundColor (line 64) | func NewResetBackgroundColor() Token {
  function NewReverseColor (line 71) | func NewReverseColor() Token {
  function NewText (line 78) | func NewText(text string) Token {
  function NewStandardColorWithCategory (line 85) | func NewStandardColorWithCategory(text string) Token {
  function NewExtendedColor (line 94) | func NewExtendedColor(text string) Token {
  function colorType (line 103) | func colorType(n int) ColorType {

FILE: token/token_test.go
  function TestToken_MaxStringWidth (line 10) | func TestToken_MaxStringWidth(t *testing.T) {
  function TestToken_StringLines (line 93) | func TestToken_StringLines(t *testing.T) {
Condensed preview — 55 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (204K chars).
[
  {
    "path": ".chglog/CHANGELOG.tpl.md",
    "chars": 501,
    "preview": "{{ range .Versions }}\n## Changes\n\n{{ range .CommitGroups -}}\n### {{ .Title }}\n\n{{ range .Commits -}}\n* {{ .Subject }}\n{{"
  },
  {
    "path": ".chglog/config.yml",
    "chars": 525,
    "preview": "style: github\ntemplate: CHANGELOG.tpl.md\ninfo:\n  title: CHANGELOG\n  repository_url: https://github.com/YOUR_NAME/REPOSIT"
  },
  {
    "path": ".dockerignore",
    "chars": 15,
    "preview": "bin\nbuild\n.git\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 659,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: jiro4989\n\n---\n\n## Descri"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 605,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature\nassignees: jiro4989\n\n---\n\n##"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 830,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/pr-labeler.yml",
    "chars": 48,
    "preview": "feature: feature/*\nbug: hotfix/*\nchore: chore/*\n"
  },
  {
    "path": ".github/workflows/auto_merge.yml",
    "chars": 850,
    "preview": "---\nname: Dependabot auto-merge\n\"on\": pull_request\n\npermissions:\n  pull-requests: write\n  contents: write\n\njobs:\n  depen"
  },
  {
    "path": ".github/workflows/pr-labeler.yml",
    "chars": 333,
    "preview": "name: labeler\n\non:\n  pull_request:\n    types: [opened]\n\njobs:\n  labeler:\n    runs-on: ubuntu-latest\n    steps:\n      - u"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 7501,
    "preview": "---\nname: release\n\n\"on\":\n  push:\n    tags:\n      - 'v*.*.*'\n\nenv:\n  app: textimg\n  goversion: '1.25'\n  build_opts: '-ldf"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 3110,
    "preview": "---\n\nname: test\n\n\"on\":\n  push:\n    branches:\n      - master\n    paths-ignore:\n      - README*\n      - LICENSE\n  pull_req"
  },
  {
    "path": ".gitignore",
    "chars": 122,
    "preview": "/bin/\n/dist/\n/testdata/out/\n/testdata/out*/\nimages/*\n!images/.gitkeep\nscripts/width/width\n\n!completions/*/textimg\ntextim"
  },
  {
    "path": "Dockerfile",
    "chars": 1471,
    "preview": "FROM golang:1.26-alpine3.22 AS base\n\nRUN go version \\\n    && echo $GOPATH \\\n    && apk update \\\n    && apk add --no-cach"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2019 jiro4989\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "Makefile",
    "chars": 924,
    "preview": "textimg: parser/grammar.peg.go *.go */*.go\n\tgo fmt ./...\n\tgo build\n\nparser/grammar.peg.go: parser/grammar.peg\n\tpeg parse"
  },
  {
    "path": "README.md",
    "chars": 11270,
    "preview": "# textimg\n\n![test](https://github.com/jiro4989/textimg/workflows/test/badge.svg)\n[![codecov](https://codecov.io/gh/jiro4"
  },
  {
    "path": "color/color.go",
    "chars": 8826,
    "preview": "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     "
  },
  {
    "path": "completions/fish/textimg.fish",
    "chars": 1668,
    "preview": "complete -c textimg -x\n\ncomplete -c textimg -s g -l foreground -a 'black red green yellow blue magenta cyan white' -d 'f"
  },
  {
    "path": "completions/zsh/_textimg",
    "chars": 1652,
    "preview": "#compdef textimg\n\n_textimg() {\n  _arguments \\\n    {-g,--foreground}'[foreground text color]: :->color' \\\n    {-b,--backg"
  },
  {
    "path": "config/config.go",
    "chars": 10243,
    "preview": "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/jiro49"
  },
  {
    "path": "config/config_test.go",
    "chars": 21242,
    "preview": "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"
  },
  {
    "path": "config/envvar.go",
    "chars": 925,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\ntype EnvVars struct {\n\tEmojiDir      string\n\tOutputDir     string\n\tFontFile    "
  },
  {
    "path": "config/face.go",
    "chars": 1219,
    "preview": "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/f"
  },
  {
    "path": "config/face_test.go",
    "chars": 1501,
    "preview": "package config\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestReadFace(t *test"
  },
  {
    "path": "config/writer_mock.go",
    "chars": 447,
    "preview": "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"
  },
  {
    "path": "docker-compose.yml",
    "chars": 686,
    "preview": "---\n\nversion: '3.7'\n\nservices:\n  base: &common\n    build:\n      context: ./\n      dockerfile: ./Dockerfile\n      target:"
  },
  {
    "path": "go.mod",
    "chars": 750,
    "preview": "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/k"
  },
  {
    "path": "go.sum",
    "chars": 3326,
    "preview": "github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=\ngithub.com/clipperhouse/uax29/v2"
  },
  {
    "path": "image/encode.go",
    "chars": 907,
    "preview": "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\"i"
  },
  {
    "path": "image/image.go",
    "chars": 8002,
    "preview": "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."
  },
  {
    "path": "image/util.go",
    "chars": 989,
    "preview": "package image\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nvar (\n\t// 絵文字描画の際に、普通に描画してほしいけれど絵文字としても定義されている\n\t// 文字のコードポイント\n\texRunes = []rune{"
  },
  {
    "path": "images/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "internal/global/env.go",
    "chars": 325,
    "preview": "package global\n\nconst (\n\tEnvNameOutputDir     = \"TEXTIMG_OUTPUT_DIR\"\n\tEnvNameFontFile      = \"TEXTIMG_FONT_FILE\"\n\tEnvNam"
  },
  {
    "path": "internal/global/version.go",
    "chars": 163,
    "preview": "package global\n\nconst (\n\tAppName = \"textimg\"\n\tVersion = `3.1.10\nCopyright (c) 2019 jiro4989\nReleased under the MIT Licen"
  },
  {
    "path": "log/log.go",
    "chars": 855,
    "preview": "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\tdeb"
  },
  {
    "path": "log/log_test.go",
    "chars": 147,
    "preview": "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("
  },
  {
    "path": "main.go",
    "chars": 158,
    "preview": "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(); er"
  },
  {
    "path": "main_test.go",
    "chars": 336,
    "preview": "//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"
  },
  {
    "path": "parser/grammar.peg",
    "chars": 1390,
    "preview": "package parser\n\ntype Parser Peg {\n  ParserFunc\n}\n\nroot <-\n  (colors / ignore / text)*\n\nignore <-\n  prefix number? non_co"
  },
  {
    "path": "parser/grammar.peg.go",
    "chars": 27426,
    "preview": "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\""
  },
  {
    "path": "parser/parser.go",
    "chars": 1688,
    "preview": "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\n"
  },
  {
    "path": "parser/parser_test.go",
    "chars": 6362,
    "preview": "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\"g"
  },
  {
    "path": "root.go",
    "chars": 5429,
    "preview": "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/jiro4"
  },
  {
    "path": "root_common_test.go",
    "chars": 964,
    "preview": "package main\n\nimport \"github.com/jiro4989/textimg/v3/config\"\n\nfunc newDefaultConfig() config.Config {\n\treturn config.Con"
  },
  {
    "path": "root_on_docker_test.go",
    "chars": 2524,
    "preview": "//go:build docker\n\n//\n// 日本語や絵文字が使えるDocker環境上で実行する想定のテスト。\n// どうしてもDocker上でしかテストできないもののみこのファイルに記述する。\n\npackage main\n\nimpor"
  },
  {
    "path": "root_test.go",
    "chars": 15028,
    "preview": "//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/stret"
  },
  {
    "path": "scripts/fetch_color.sh",
    "chars": 199,
    "preview": "#!/bin/bash\n\ncurl \"https://jonasjacek.github.io/colors/\" \\\n  | grep style \\\n  | sed -r 's@.*<td>([0-9]+)</td>.*<td>(rgb["
  },
  {
    "path": "scripts/width/main.go",
    "chars": 427,
    "preview": "// width は文字のrunewidthが返す文字幅を確認するためのツール。\n\n/*\n\n使い方\n\ncd tools/width\ngo build .\n./width あいうえお■漢字abcde😲\n\n*/\n\npackage main\n\ni"
  },
  {
    "path": "testdata/in/255.txt",
    "chars": 3490,
    "preview": "\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"
  },
  {
    "path": "testdata/in/illegal_font.otc",
    "chars": 2,
    "preview": "2\n"
  },
  {
    "path": "testdata/in/illegal_font.ttc",
    "chars": 2,
    "preview": "1\n"
  },
  {
    "path": "testdata/in/illegal_font.txt",
    "chars": 2,
    "preview": "3\n"
  },
  {
    "path": "testdata/in/red_grad.txt",
    "chars": 4514,
    "preview": "\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"
  },
  {
    "path": "token/token.go",
    "chars": 2803,
    "preview": "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"
  },
  {
    "path": "token/token_test.go",
    "chars": 2286,
    "preview": "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\nfun"
  }
]

About this extraction

This page contains the full source code of the jiro4989/textimg GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 55 files (164.8 KB), approximately 62.5k tokens, and a symbol index with 216 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!