diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1b88682 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yaml,yml,json,tf,tofu,hcl}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..79c1b70 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +* text=auto eol=lf + +*.sh text eol=lf +*.tf text eol=lf +*.tofu text eol=lf +*.yaml text eol=lf +*.yml text eol=lf + +*.gif binary +*.ico binary +*.jpeg binary +*.jpg binary +*.pdf binary +*.png binary +*.webp binary diff --git a/.gitea/workflows/infra-validate.yml b/.gitea/workflows/infra-validate.yml new file mode 100644 index 0000000..1fafa25 --- /dev/null +++ b/.gitea/workflows/infra-validate.yml @@ -0,0 +1,25 @@ +name: infra-validate + +on: + pull_request: + push: + branches: + - main + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pinned tools + uses: jdx/mise-action@v4.2.3 + with: + install: true + cache: true + + - name: Validate infrastructure source + env: + VALIDATION_PROFILE: full + run: make check diff --git a/.github/CODEOWNERS.example b/.github/CODEOWNERS.example new file mode 100644 index 0000000..0eb0219 --- /dev/null +++ b/.github/CODEOWNERS.example @@ -0,0 +1,7 @@ +# 복제 후 실제 팀/사용자로 바꾸고 파일명을 CODEOWNERS로 변경하세요. +# +# * @your-org/platform-team +# /bootstrap/ @your-org/platform-admins +# /infrastructure/live/**/prod/ @your-org/platform-admins +# /gitops/clusters/prod/ @your-org/platform-admins +# /gitops/apps/ @your-org/application-owners diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..1f78869 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ +## 변경 내용 + + + +## 영향 범위 + + + +## 검증 + +- [ ] `make check` +- [ ] 렌더/plan 결과 검토 +- [ ] 비밀 및 state 파일 미포함 + +## 롤백 + + diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..22aeb9a --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,35 @@ +name: Validate + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: validate-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install kubectl + uses: azure/setup-kubectl@829323503d1be3d00ca8346e5391ca0b07a9ab0d # v5.1.0 + with: + version: v1.36.3 + + # Add a SHA-pinned setup step for the selected IaC/Helm/policy tools + # before adding files that require them. + - name: Run repository checks + run: make check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aab3329 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# Local credentials and environment files +.env +.env.* +!.env.example +vault-init-keys.json +*.key +*.pem +*.p12 +*.pfx +*.jks +credentials.json +id_dsa +id_ecdsa +id_ed25519 +id_rsa + +# Kubernetes local access +kubeconfig +kubeconfig.* +*.kubeconfig + +# Python +.venv/ +venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# Rendered and temporary infrastructure artifacts +.rendered/ +rendered/ +*.tfstate +*.tfstate.* +*.tfplan +plan.out +.terraform.tfstate.lock.info +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json +*.local.tfvars +*.secret.tfvars +**/.terraform/* +**/.terragrunt-cache/* +*.backup +*.snapshot +*.tmp +.decrypted/ +*.decrypted.yaml +*.dec.yaml +.build/ +.cache/ +dist/ +tmp/ +coverage/ +*.log + +# Editor and operating-system files +.idea/ +.vscode/ +.DS_Store +*.swp +*.swo diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..c11203f --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,12 @@ +title = "project-infra secret scanning" + +[extend] +useDefault = true + +[[allowlists]] +description = "Documentation placeholders" +regexes = [ + '''REPLACE_VIA_[A-Z0-9_]+''', + '''<[^>]+>''', + '''\$\{[A-Z0-9_]+\}''', +] diff --git a/.kube-linter.yaml b/.kube-linter.yaml new file mode 100644 index 0000000..80669a9 --- /dev/null +++ b/.kube-linter.yaml @@ -0,0 +1,10 @@ +checks: + addAllBuiltIn: false + exclude: + # These relationships are created by controllers/CRs outside the rendered + # Deployment set, so static object matching cannot resolve them. + - dangling-service + - non-existent-service-account + # Lab intentionally runs single replicas; topology spread is still declared + # on application workloads. staging/prod must add replicas and PDBs. + - no-anti-affinity diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..004de14 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,12 @@ +[tools] +kubectl = "1.36.3" +kustomize = "5.8.1" +kubeconform = "0.8.0" +kube-linter = "0.8.3" +shellcheck = "0.11.0" +shfmt = "3.13.1" +gitleaks = "8.30.1" +helm = "3.19.4" +jq = "1.8.1" +ripgrep = "14.1.1" +"aqua:minio/mc" = "RELEASE.2025-08-13T08-35-41Z" diff --git a/AGENTS.md b/AGENTS.md index ca32449..0033b64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,26 +11,38 @@ Read order: > **WARNING TO AI AGENTS**: You MUST READ the files in `/docs/standards/infra/` and `/docs/examples/infra/` using your file reading tools BEFORE proposing or writing any K8s manifests. DO NOT rely on generic Kubernetes knowledge. The standard documents contain mandatory strict rules (e.g., probe settings, security contexts, non-root constraints, resource limits, namespace strategies) that MUST be hardcoded into your output. Generating plain boilerplate YAML without strict standards compliance is a critical failure. Repo role: -- this repository owns Kubernetes/K3s infrastructure source -- source of truth is Git + Kustomize directories under `k8s/` +- this repository owns infrastructure lifecycle and Kubernetes/K3s desired state +- source of truth is Git + IaC under `infrastructure/` + Kustomize under `gitops/` - scripts are helper tools, not the source of truth - runtime cluster state or server-local manifest files are not authoritative Primary directories: -- `k8s/base/`: environment-neutral Kustomize bases -- `k8s/base/app/`: application-facing workload units -- `k8s/base/managing/`: management/operations units such as bootstrap, migration, backup, restore, and admin jobs -- `k8s/base/plugins/`: platform/plugin-style base resources -- `k8s/overlays//`: environment overlays such as `dev`, `staging`, and `prod` -- `k8s/scripts/`: render, diff, apply, validate, backup, and restore helpers +- `bootstrap/`: foundation and selected GitOps controller bootstrap +- `infrastructure/components/`: reusable IaC primitives +- `infrastructure/stacks/`: optional reusable compositions +- `infrastructure/live/`: the only IaC plan/apply and state entrypoints +- `gitops/apps/`: app-owned workloads, data, and one-shot operations +- `gitops/platform/`: shared services, operators, ingress, mesh, storage, and secret delivery +- `gitops/policies/`: reusable governance and security policy +- `gitops/tenants/`: namespace, RBAC, quota, and tenant boundaries +- `gitops/clusters///namespaces` and `stages/`: current deployable Kubernetes entrypoints +- `gitops/clusters///all/`: render/audit aggregate; never apply it +- `scripts/`: render, diff, apply, validate, backup, and restore helpers - `docs/standards/infra/`: infra standards - `docs/examples/infra/`: approved examples +Current runtime exception: +- `lab` has no Argo CD/Flux controller; `scripts/bin/bootstrap.sh` applies the + cluster `namespaces` entrypoint and ordered `stages/*`; `all/` remains audit-only + Structure principle: -- `k8s/base` is organized by operational ownership and workload role, not by environment. -- `k8s/overlays` is organized by environment first, so large fleets can be rendered, diffed, applied, and audited environment-by-environment. -- This repo intentionally does not use the small-project “each service owns its own base and overlays” pattern as the primary layout. -- Service ownership still exists under `k8s/base/app///**`; environment rollout ownership lives under `k8s/overlays/`. +- catalogs are organized by ownership; every unit may contain an environment-neutral `base` and focused `overlays/` +- catalog directories are never deployment/controller roots +- `infrastructure/live` and `gitops/clusters` are environment-first entrypoint trees +- current `lab` rollout ownership lives under `gitops/clusters/lab/main/namespaces` + and `gitops/clusters/lab/main/stages` +- `_template` is copied to start new units and is never imported by live/cluster roots +- approved manifest examples live only under `docs/examples/infra` Hard bans: - do not treat `/var/lib/rancher/k3s/server/manifests` as source of truth @@ -38,6 +50,7 @@ Hard bans: - do not put production secrets in Git/plain manifests - do not bypass Kustomize with ad-hoc generated YAML as the primary path - do not mix app rollout, DB migration, and control-plane upgrade in one opaque step +- do not apply `gitops/clusters///all`; deploy ordered stage entrypoints - do not expose health, metrics, admin, or management endpoints publicly by default - do not use `hostPath` as an operating default - do not use `start-dev`/dev-mode style configs for production components @@ -84,16 +97,17 @@ Component routing: -> `/docs/standards/infra/flyway.md` Before editing: -- identify target environment: dev / staging / prod -- identify target unit: app / managing / plugin / script / docs -- identify whether the change belongs in `k8s/base`, `k8s/overlays/`, `k8s/scripts`, or docs +- identify target environment: lab / staging / prod +- identify target unit: app / data / platform / operation / script / docs +- identify whether the change belongs in `bootstrap`, `infrastructure`, a `gitops` catalog, + `gitops/clusters`, `scripts`, or docs - identify workload type: Deployment / StatefulSet / Job / CronJob - identify whether storage, secret delivery, ingress, migration, or rollback path changes - identify whether the change touches a K3s-specific rule Default execution flow: - read the owning standard first -- prefer changing Kustomize source under `k8s/` over live cluster state +- prefer changing IaC/Kustomize source under `infrastructure/` or `gitops/` over live state - prefer render -> validate -> diff -> apply thinking - prefer explicit rollback/restore path before risky changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e72b915 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# 기여 가이드 + +## 변경 원칙 + +1. 변경 대상의 소유 디렉터리를 먼저 확인합니다. +2. 재사용 가능한 구현은 catalog 영역에, 환경별 조합과 차이만 live/cluster + 진입점에 둡니다. +3. 하나의 리소스는 하나의 도구와 하나의 디렉터리만 소유합니다. +4. 운영 변경에는 영향 범위, 롤백 방법, 검증 결과를 함께 기록합니다. +5. `make check`를 통과한 변경만 리뷰를 요청합니다. + +## 이름 규칙 + +- 폴더와 리소스: 소문자 `kebab-case` +- 환경: `dev`, `staging`, `prod`처럼 조직에서 합의한 고정 어휘 +- 클러스터: 환경과 위치를 식별할 수 있는 안정적인 이름 +- 임시 이름, 사람 이름, 티켓 번호를 장기 리소스 이름에 사용하지 않음 + +## Pull request 체크리스트 + +- [ ] 변경이 올바른 소유권 경계에 위치한다. +- [ ] 비밀, kubeconfig, state, plan 파일이 포함되지 않았다. +- [ ] 공급자, module, chart, image 버전 변경의 영향을 확인했다. +- [ ] `make check` 결과를 확인했다. +- [ ] 운영 영향이 있으면 rollback/runbook을 갱신했다. +- [ ] 구조적 선택이 바뀌면 ADR을 추가하거나 갱신했다. + +## 배포 + +이 템플릿에는 범용 `apply` 또는 `destroy` 명령이 없습니다. 프로젝트에서 배포 +자동화를 추가할 때는 대상 환경을 필수 입력으로 받고, 운영 환경에는 승인과 +동시 실행 잠금을 적용하며, 검토된 plan만 적용하도록 설계합니다. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7dd310d --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +SHELL := /bin/bash +.DEFAULT_GOAL := help + +.PHONY: help doctor validate validate-structure validate-project check tree + +help: ## 사용 가능한 명령을 표시합니다. + @awk 'BEGIN {FS = ":.*## "; print "Usage: make "} /^[a-zA-Z_-]+:.*## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +doctor: ## 현재 로컬 도구 상태를 확인합니다. + @./scripts/doctor.sh + +validate-structure: ## 템플릿 구조, 민감 파일, Kustomize, Helm과 IaC 형식을 검증합니다. + @./scripts/validate.sh + +validate-project: ## 프로젝트 entrypoint, schema, policy, shell과 문서를 검증합니다. + @bash ./scripts/ci/validate.sh + +validate: validate-structure validate-project ## 구조 및 프로젝트 검증을 모두 실행합니다. + +check: validate ## CI와 동일한 전체 검증을 실행합니다. + +tree: ## 생성물과 Git 메타데이터를 제외한 구조를 표시합니다. + @find . \ + \( \ + -name .build -o \ + -name .cache -o \ + -name .git -o \ + -name .terraform -o \ + -name .terragrunt-cache -o \ + -name dist -o \ + -name rendered -o \ + -name tmp \ + \) -prune -o \ + -print | sort diff --git a/README.md b/README.md index f241a2a..14ab276 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,156 @@ -# Project-Infra +# Project Infra -K3s 기반 백엔드 실행 환경을 Git으로 관리하는 개인 프로젝트입니다. -[Project-Auth-Server](https://github.com/donghyeon-ka/project-auth-server)가 실제로 실행될 때 필요한 인증 게이트, secret 전달, DB 마이그레이션, 네트워크 정책을 Kubernetes 리소스로 구성했습니다. +K3s 기반 인증 플랫폼의 인프라 source-of-truth입니다. 범용 +`k8s-template`의 lifecycle/ownership 스켈레톤을 적용하고, 기존 리소스를 +IaC catalog와 Kubernetes desired-state catalog로 분리했습니다. -이 프로젝트에서 확인하고 싶었던 질문은 네 가지입니다. +현재 실제 배포 환경은 폐기 가능한 단일 클러스터용 `lab`입니다. `dev`, +`staging`, `prod` IaC root는 확장 위치만 예약되어 있으며 아직 실행 가능한 +Terraform/OpenTofu 구성이 아닙니다. -- 로그인 / 세션 처리를 애플리케이션 밖으로 빼면 ingress와 backend의 책임은 어떻게 나뉘는가? -- secret을 Vault에 두면서도 Pod는 Kubernetes-native하게 실행할 수 있는가? -- default-deny NetworkPolicy 환경에서 백엔드 워크로드가 어떤 통신을 명시해야 하는가? -- dev에 집중하되, staging / prod 승격 시 달라질 정책 지점을 overlay 구조로 남길 수 있는가? - -| | | -|---|---| -| **Runtime** | K3s 1.30, Kustomize, Helm, Bash | -| **Ingress / Auth** | Traefik, oauth2-proxy, Keycloak Operator | -| **Secrets** | HashiCorp Vault, Vault Secrets Operator | -| **Data** | PostgreSQL, Flyway, MinIO, Docker Registry | -| **Policy** | NetworkPolicy default-deny, PSS Restricted | -| **Validation** | `kustomize build`, `kubeconform -strict`, `kube-linter` | -| **Scope** | dev overlay 중심. staging / prod는 의도적으로 미구현 | - ---- - -## Architecture - -![System Overview](docs/diagrams/architecture/01-overall.png) - -큰 흐름은 두 가지입니다. - -- 요청 흐름: `Traefik → oauth2-proxy → auth-server → PostgreSQL / MinIO` -- secret 흐름: `Vault → VSO → Kubernetes Secret → Pod envFrom` - -상세 다이어그램: - -- [ForwardAuth cold path](docs/diagrams/sequence/forward-auth-cold.md) / [warm path](docs/diagrams/sequence/forward-auth-warm.md) -- [Secret pipeline bootstrap](docs/diagrams/sequence/secret-pipeline-bootstrap.md) / [runtime reconcile](docs/diagrams/sequence/secret-pipeline-runtime.md) -- [전체 아키텍처 설명](docs/architecture.md) - ---- - -## Engineering Decisions - -### 1. 인증은 Ingress에서 먼저 막고, 백엔드는 JWT를 다시 검증 - -Traefik `ForwardAuth → oauth2-proxy → Keycloak` 조합으로 미인증 요청을 ingress 계층에서 먼저 차단합니다. -그 뒤 auth-server는 Keycloak JWT를 Spring Security Resource Server로 다시 검증합니다. - -이 구조는 로그인 / 세션 처리와 API 권한 검증을 분리하기 위한 선택입니다. 인증 정책 변경은 애플리케이션 코드보다 Kubernetes manifest 변경으로 다룰 수 있습니다. - -![인증·인가 흐름](docs/diagrams/architecture/02-auth-flow.png) - -### 2. Vault는 source of truth, Pod는 Kubernetes Secret만 소비 - -Pod마다 Vault Agent sidecar를 붙이지 않고, Vault Secrets Operator가 Vault KV 값을 Kubernetes Secret으로 동기화합니다. +## Lifecycle ```text -Vault KV-v2 → VSO reconcile → Kubernetes Secret → Pod envFrom +bootstrap/foundation + | + v +infrastructure/live + | + v +bootstrap/gitops + | + v +gitops/clusters ──> platform / policies / tenants / apps ``` -Pod는 Vault endpoint, token, template rendering을 직접 알지 않습니다. 대신 secret이 Kubernetes Secret으로 존재하므로, etcd encryption-at-rest가 다음 검증 항목으로 남습니다. +- `bootstrap/foundation`: remote state와 초기 identity 같은 선행 조건 +- `infrastructure/live`: 네트워크, IAM, DNS, load balancer와 클러스터 생성 root +- `bootstrap/gitops`: 선택한 GitOps controller와 cluster root 연결 +- `gitops/clusters`: Kubernetes desired state의 실제 배포 entrypoint -### 3. Vault role / policy는 도메인별로 분리 +현재는 Argo CD/Flux가 설치되어 있지 않으므로 `bootstrap/gitops`는 확장 +계약만 제공합니다. 실제 `lab` 배포는 readiness 경계를 보존하는 +`scripts/bin/bootstrap.sh`가 ordered stage를 server-side apply합니다. -VSO Operator의 ServiceAccount는 하나지만, Vault 쪽 role과 policy는 `vso-auth-platform` / `vso-storage`로 나눴습니다. -각 `VaultStaticSecret`은 자기 도메인의 `VaultAuth`만 참조합니다. +## Repository layout -auth-platform 권한으로 storage secret 경로에 접근하지 못하게 하려는 결정입니다. +| Path | Responsibility | +| --- | --- | +| `bootstrap/` | foundation 및 GitOps controller bootstrap | +| `infrastructure/components/` | 재사용 가능한 compute/database/networking/storage IaC 단위 | +| `infrastructure/stacks/` | 반복되는 component 조합 | +| `infrastructure/live///` | 실제 IaC plan/apply와 state 경계 | +| `gitops/apps/` | auth-server, PostgreSQL, migration 같은 app 소유 배포 정의 | +| `gitops/platform/` | Traefik, Vault, VSO, Keycloak, MinIO, registry와 operator | +| `gitops/policies/` | 공통 보안·거버넌스 정책 | +| `gitops/tenants/` | namespace, RBAC, quota 같은 tenant 경계 | +| `gitops/clusters/lab/main/namespaces`, `stages/` | 현재 실제 배포 entrypoint | +| `gitops/clusters/lab/main/all/` | 전체 render/schema/policy 감사 전용; apply 금지 | +| `scripts/` | staged bootstrap, teardown와 검증 도구 | +| `tests/kustomize-entrypoints.txt` | 검증할 Kustomize entrypoint inventory | -상세 매핑은 [docs/vault-vso.md](docs/vault-vso.md#vaultauth--vaultstaticsecret-매핑)를 참고합니다. +`_template` 디렉터리는 새 component/root를 만들 때 복제하는 뼈대입니다. +이 저장소는 이미 실제 `lab` 구성이 조립된 프로젝트이므로 루트에 별도 +`examples/`를 유지하지 않습니다. 현재 구현을 참고하고 새 단위는 각 책임 +폴더의 `_template`에서 시작합니다. -### 4. NetworkPolicy는 default-deny에서 시작 - -`mnt` namespace 안에서도 모든 Pod 간 ingress / egress를 기본 차단합니다. -새 워크로드를 추가하려면 필요한 통신을 NetworkPolicy로 명시해야 합니다. - -이 마찰은 의도한 것입니다. wide-open으로 시작하는 실수를 줄이고, 워크로드 간 통신 관계를 코드로 남기기 위함입니다. - -상세 매트릭스는 [docs/networking.md](docs/networking.md)를 참고합니다. - -### 5. K3s packaged Traefik manifest는 직접 수정하지 않음 - -K3s가 관리하는 Traefik manifest를 직접 수정하면 재부팅 / 재적용 시 덮어써질 수 있습니다. -그래서 `HelmChartConfig` overlay와 Middleware / TLSOption 리소스로 운영 정책을 관리합니다. - -![Traefik 파이프라인](docs/diagrams/architecture/07-traefik-pipeline.png) - ---- - -## Repository Layout - -Kustomize `base / components / overlays` 구조입니다. - -| Path | Role | -|---|---| -| `k8s/base/` | 환경 중립 매니페스트 | -| `k8s/components/` | 재사용 component. 현재 ForwardAuth component | -| `k8s/overlays/dev/` | 기본 dev 환경. 현재 ForwardAuth component 포함 | -| `k8s/overlays/{staging,prod}/` | 의도적으로 비워둔 승격 지점 | -| `k8s/scripts/` | bootstrap / validation / reusable tasks | -| `terraform/` | contracts만 존재. 추후 구현 | -| `docs/` | 상세 설계와 운영 문서 | - ---- - -## Validation +예를 들어 새 환경과 app을 시작할 때 다음처럼 복제합니다. ```bash -bash k8s/scripts/ci/validate.sh +mkdir -p infrastructure/live/dev +cp -R infrastructure/live/_template infrastructure/live/dev/cluster + +mkdir -p gitops/clusters/dev +cp -R gitops/clusters/_template gitops/clusters/dev/main + +cp -R gitops/apps/_template gitops/apps/example-api ``` -`validate.sh`는 주요 overlay에 대해 세 단계를 수행합니다. +복제 후 `__REPLACE_ME_*__` 값을 교체하고, 실제 cluster root에서 필요한 +catalog만 참조합니다. -1. `kustomize build` — overlay 조립과 patch 유효성 확인 -2. `kubeconform -strict` — Kubernetes / CRD schema 확인 -3. `kube-linter` — securityContext, resources, image tag 등 정적 점검 - -목표 상태: +## Current architecture ```text -k8s/overlays/dev build=ok schema=ok lint=ok -k8s/overlays/dev/vso build=ok schema=ok lint=ok +Traefik -> oauth2-proxy -> auth-server -> PostgreSQL + \-> Keycloak +docker-registry -> MinIO + +Vault KV-v2 -> Vault Secrets Operator -> Kubernetes Secret -> workload ``` -운영 절차는 [docs/operations.md](docs/operations.md), 실행 매뉴얼은 [guide.md](guide.md)를 참고합니다. +- Namespace `mnt`에는 PSS Restricted와 default-deny NetworkPolicy를 적용합니다. +- K3s packaged Traefik manifest는 수정하지 않고 `HelmChartConfig`만 관리합니다. +- Flyway와 Keycloak realm import는 versioned one-shot operation입니다. +- `lab`의 PostgreSQL, Vault와 MinIO는 운영 HA/backup 설계가 아닙니다. ---- +## Deploy -## Current Status +```bash +export KUBE_CONTEXT_LAB='' +bash scripts/bin/bootstrap.sh lab +``` -| Area | Status | -|---|---| -| dev overlay 매니페스트 | 구성됨 (kustomize / kubeconform / kube-linter 검증 가능) | -| Vault + VSO 부트스트랩 | idempotent script 로 구성 | -| Traefik HelmChartConfig + Middleware | 구성됨 | -| NetworkPolicy default-deny | 구성됨 | -| ForwardAuth | dev overlay에 포함됨 | -| KeycloakRealmImport | overlay 준비됨. 실제 적용 결과 확인 필요 | -| cert-manager + ClusterIssuer | manifest 준비됨. 외부 DNS 필요 | -| staging / prod overlay | 의도적으로 비워둠 | -| Terraform | 디렉토리 contracts만 존재 | +배포 순서는 다음과 같습니다. ---- +```text +namespaces +00-platform +10-vault +20-secrets +30-data +35-registry +40-operations +50-apps +``` -## Limitations +`gitops/clusters/lab/main/all`은 직접 apply하지 않습니다. CRD establishment, +Vault 초기화, Secret materialization과 migration 완료는 한 번의 Kustomize +apply로 표현할 수 없습니다. -운영 완료 상태가 아니라 dev 환경에서 실행 구조를 검증한 프로젝트입니다. +## Validate -- Vault는 file backend 단일 노드입니다. dev에서는 secret 전달 경로와 VSO reconcile을 검증하는 데 충분하다고 보고 선택했습니다. prod에서는 `raft` storage와 KMS auto-unseal로 전환해야 합니다. -- cert-manager / ClusterIssuer manifest는 있지만, 실제 ACME 인증서 발급은 외부 DNS가 Traefik 진입점을 가리켜야 완료됩니다. -- ForwardAuth 로그인 / 로그아웃 / deny-allow E2E 검증은 인증서와 DNS 정리 후 진행할 항목입니다. -- Postgres 백업, Terraform 실제 모듈, staging/prod overlay는 아직 구현하지 않았습니다. +```bash +make doctor +make check ---- +# CI와 같은 pinned toolchain을 사용한 full gate +mise install +VALIDATION_PROFILE=full make check +``` + +`make check`는 템플릿 구조/민감 파일/IaC 계약 검증과 프로젝트별 +Kustomize/schema/policy/shell/docs 검증을 모두 실행합니다. + +## Extension rules + +- 외부 load balancer, VPC와 DNS는 `infrastructure/components`에서 구현하고 + `infrastructure/live` root가 조립합니다. +- in-cluster ingress/LB controller, Istio 같은 service mesh는 + `gitops/platform/`에 두고 cluster stage가 선택합니다. +- sidecar는 workload 기본 계약이면 app `base`, 환경별 선택이면 해당 + `overlays/`에 patch/component로 구성합니다. +- VSO와 secret delivery controller는 `gitops/platform/secret-delivery`, + 실제 비밀값은 Vault에 둡니다. +- 규모가 커지면 `live/////`과 + `clusters///`로 깊이만 확장하고 ownership 경계는 + 유지합니다. + +## Secret incident boundary + +`vault-init-keys.json`은 Git에서 제외되며 로컬 권한은 `0600`이어야 합니다. +과거 Git 이력에 포함된 값은 이미 노출된 것으로 간주해야 하므로 실제 Vault +root token과 unseal/recovery material을 회전한 뒤, 협업자와 조율하여 원격 +이력을 별도로 정리해야 합니다. ## Documentation -- [docs/architecture.md](docs/architecture.md) — 전체 구조, 워크로드 표, 이미지 정책 -- [docs/networking.md](docs/networking.md) — NetworkPolicy 매트릭스 / 작성 규칙 -- [docs/ingress-traefik.md](docs/ingress-traefik.md) — Traefik, ForwardAuth, cert-manager, KeycloakRealmImport -- [docs/vault-vso.md](docs/vault-vso.md) — Vault auth, policy/role, VSO Secret catalog -- [docs/operations.md](docs/operations.md) — bootstrap 단계, validate.sh, 환경별 차등 -- [docs/troubleshooting.md](docs/troubleshooting.md) — 운영 중 만난 함정 7건 (사건 카탈로그) -- [docs/diagrams/architecture/](docs/diagrams/architecture/) — draw.io 아키텍처 그림 -- [docs/diagrams/sequence/](docs/diagrams/sequence/) — Mermaid sequence diagrams -- [guide.md](guide.md) — 실제 실행 매뉴얼 +- [실행 가이드](guide.md) +- [프로젝트 아키텍처](docs/architecture.md) +- [스켈레톤 구조 계약](docs/architecture/repository-structure.md) +- [운영과 검증](docs/operations.md) +- [Vault와 VSO](docs/vault-vso.md) +- [Ingress와 Traefik](docs/ingress-traefik.md) +- [보안 강화](docs/security-hardening.md) +- [장애 기록](docs/troubleshooting.md) +- [템플릿 사용 가이드](docs/guides/getting-started.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2a15961 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,46 @@ +# Security Policy + +## 저장소에 둘 수 없는 항목 + +- 평문 비밀번호, token, access key, private key +- base64로만 인코딩한 Kubernetes `Secret` +- kubeconfig와 클라우드 provider credential +- Terraform/OpenTofu state, plan, 복호화 산출물 +- 실제 비밀이 포함된 `tfvars`, Helm values, `.env` + +`.gitignore`는 실수 완화 장치일 뿐 보안 통제가 아닙니다. 비밀이 한 번이라도 +커밋되었다면 history 삭제 여부와 관계없이 즉시 폐기하고 회전합니다. + +## 허용하는 비밀 관리 방식 + +프로젝트마다 다음 중 하나를 ADR로 선택하고 CI와 운영 절차를 함께 정의합니다. + +- External Secrets 계열 리소스로 외부 secret manager를 참조 +- SOPS와 KMS/age를 사용해 암호화한 파일만 저장 +- 조직에서 승인한 동등한 GitOps 비밀 관리 방식 + +SOPS로 암호화한 Kubernetes Secret은 검증 가능한 규칙을 위해 +`*.sops.yaml`, `*.sops.yml` 또는 `*.sops.json` 이름을 사용합니다. + +암호화 키, 복호화 권한과 secret manager 접근은 workload identity/OIDC와 +최소 권한 원칙으로 부여합니다. 장기 cloud access key를 CI secret으로 +사용하지 않습니다. + +## State와 CI + +- remote state는 암호화, locking, versioning을 활성화합니다. +- 환경과 독립 장애 영역은 별도 state로 분리합니다. +- production apply에는 승인, 직렬화와 감사 로그를 적용합니다. +- fork 또는 신뢰하지 않는 PR 코드에 privileged credential을 제공하지 않습니다. +- CI action, provider, module, chart와 image 버전을 검토 가능한 방식으로 고정합니다. + +## 노출 사고 대응 + +1. 노출된 credential과 파생 token을 폐기하고 회전합니다. +2. 영향받은 시스템의 접근 로그와 변경 이력을 확인합니다. +3. 저장소 history와 artifact/cache에서 민감 데이터를 제거합니다. +4. 원인과 영향 범위, 재발 방지 조치를 incident 문서에 기록합니다. +5. 조직의 보안 연락 채널로 보고합니다. + +이 템플릿을 실제 조직에서 사용하기 전에 비공개 보안 연락처와 대응 SLA를 이 +문서에 추가해야 합니다. diff --git a/bootstrap/README.md b/bootstrap/README.md new file mode 100644 index 0000000..b3fdf92 --- /dev/null +++ b/bootstrap/README.md @@ -0,0 +1,11 @@ +# Bootstrap + +선언형 인프라와 GitOps가 스스로 동작하기 전에 한 번 또는 매우 드물게 수행하는 +최소 초기화만 둡니다. + +- `foundation`: remote state, locking, 최초 identity 같은 선행 조건 +- `gitops`: 선택한 controller 설치와 cluster root 연결 + +일반 네트워크, Kubernetes cluster, addon과 application을 이곳에 두지 않습니다. +부트스트랩 절차는 반복 실행 가능하고 감사 가능해야 하며, 장기 수동 운영 경로가 +되어서는 안 됩니다. diff --git a/bootstrap/foundation/README.md b/bootstrap/foundation/README.md new file mode 100644 index 0000000..4165379 --- /dev/null +++ b/bootstrap/foundation/README.md @@ -0,0 +1,21 @@ +# Foundation Bootstrap + +다른 IaC state가 의존하는 최소 선행 조건을 관리합니다. + +가능한 대상: + +- remote state storage와 locking +- state 암호화 key +- CI의 최초 workload identity/OIDC trust +- 조직 공통 account/project 초기 설정 + +## 계약 + +- 일반 infrastructure state와 분리합니다. +- 변경 권한과 실행 빈도를 최소화합니다. +- local state를 장기간 유지하지 않습니다. +- output과 후속 `infrastructure/live`가 값을 소비하는 방식을 문서화합니다. +- provider credential이나 실제 backend secret을 커밋하지 않습니다. + +조직 공통 foundation이 외부 저장소에 이미 있다면 이 폴더에는 소유 팀, output +contract와 복구 절차만 기록합니다. diff --git a/bootstrap/gitops/README.md b/bootstrap/gitops/README.md new file mode 100644 index 0000000..90c4ccc --- /dev/null +++ b/bootstrap/gitops/README.md @@ -0,0 +1,25 @@ +# GitOps Bootstrap + +Flux, Argo CD 등 **하나의** GitOps controller를 선택해 설치하고 cluster root에 +연결하는 확장 위치입니다. 현재 프로젝트에는 controller가 없으며 `lab`은 +`scripts/bin/bootstrap.sh`가 stage를 순서대로 적용합니다. + +포함 범위: + +- controller 설치 또는 설치 manifest +- source repository/OCI 연결 +- `gitops/clusters/<...>` root reconcile 선언 +- controller용 최소 identity와 secret manager 접근 연결 + +제외 범위: + +- ingress, certificate, DNS, storage, observability addon +- application workload +- controller 설치 후 Git으로 관리할 수 있는 일반 Kubernetes 리소스 + +bootstrap credential과 recovery 절차는 `docs/runbooks`에 문서화합니다. + +현재 `gitops/clusters/lab/main/all`은 readiness 순서를 표현하지 못하는 감사용 +aggregate이므로 controller root로 연결하지 않습니다. Controller를 도입할 때는 +`docs/decisions`에 선택과 ordering/health 모델을 기록한 뒤 각 stage를 순차 +reconcile하도록 구성합니다. diff --git a/docs/architecture.md b/docs/architecture.md index d19487c..bf3389a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,147 +1,93 @@ -# Architecture 상세 +# Architecture -README 의 Architecture / Key Components 를 보충하는 문서. 폴더 구조 전체와 워크로드, 이미지 정책을 모은다. +## Ownership model -## 폴더 구조 +Catalog는 환경이 아니라 workload ownership으로 나눕니다. -``` -Project-Infra/ -├── .gitignore # vault-init-keys.json 제외 -├── .kube-linter.yaml # kube-linter 규칙 (컨텍스트 오탐 4종 제외) -├── README.md # 본 프로젝트 진입 문서 -├── guide.md # 운영 절차서 -├── docs/ # README 보조 분할 문서 -│ -├── k8s/ -│ ├── base/ -│ │ ├── managing/ -│ │ │ ├── namespace/ # mnt namespace + PSS restricted 라벨 -│ │ │ └── migration-flyway/ # Flyway Job (공식 이미지) -│ │ │ -│ │ ├── app/ # 애플리케이션 워크로드 (소유권 = 개발팀) -│ │ │ ├── identity/auth/ -│ │ │ │ ├── stateful/identity-postgres/ -│ │ │ │ └── stateless/auth-server/ -│ │ │ ├── storage/minio/stateful/minio/ -│ │ │ └── test/stateless/test-server-{1,2,3}/ -│ │ │ -│ │ └── plugins/ # 플랫폼 플러그인 (다른 워크로드가 의존) -│ │ ├── vault/ # Vault StatefulSet (공식 이미지) -│ │ ├── docker-registry/ # Registry Deployment (공식 이미지) -│ │ ├── oauth2-proxy/ # ForwardAuth 용 auth proxy -│ │ └── vso/ # VSO CRD 리소스 (VaultConnection / VaultAuth / VaultStaticSecret) -│ │ -│ ├── components/ -│ │ └── forward-auth/ # oauth2-proxy + Traefik ForwardAuth 재사용 component -│ │ -│ ├── overlays/ -│ │ ├── dev/ -│ │ │ ├── kustomization.yaml # dev 전체 집계 (namespace: mnt) -│ │ │ ├── networkpolicy-baseline.yaml # default-deny + DNS egress -│ │ │ ├── platform/ -│ │ │ │ ├── traefik/ # HelmChartConfig + Middleware + TLSOption -│ │ │ │ ├── cert-manager/ # cert-manager v1.20.2 -│ │ │ │ ├── cert-manager-issuers/ # letsencrypt-staging/prod ClusterIssuer -│ │ │ │ └── keycloak-operator/ # Keycloak Operator 26.6.1 -│ │ │ ├── tls/ # cert-manager 적용 후 Certificate -│ │ │ ├── vault/ # Vault overlay + NetworkPolicy + storage patch -│ │ │ ├── registry/ # Registry overlay + NetworkPolicy + storage patch -│ │ │ ├── vso/ # VSO CRDs (Helm 설치 후 별도 apply) -│ │ │ ├── database/ # identity-postgres + VaultStaticSecret -│ │ │ ├── auth/ # auth-server + Ingress(project.com) + flyway -│ │ │ ├── keycloak/ # Keycloak CR + public Ingress -│ │ │ ├── keycloak-realm/ # KeycloakRealmImport (Git-managed realm/client) -│ │ │ ├── storage/ # minio + VaultStaticSecret + certConfig FQDN patch -│ │ │ └── test/ # test-server 1/2/3 -│ │ ├── components/forward-auth/ # dev overlay 에 포함되는 ForwardAuth component -│ │ ├── staging/ # 의도적으로 비어둠 -│ │ └── prod/ # 의도적으로 비어둠 -│ │ -│ └── scripts/ -│ ├── bin/ # 사용자 진입점 (bootstrap.sh / teardown.sh) -│ ├── ci/validate.sh # kustomize + kubeconform + kube-linter -│ ├── lib/ # 공통 라이브러리 (common.sh / vault.sh) -│ └── tasks/ # 재사용 작업 (vault-init / vault-seed-apps / vso-install) -│ -└── terraform/ # contracts 만 존재, 추후 구현 +| Area | Owned resources | +| --- | --- | +| `gitops/apps/auth-server` | auth-server | +| `gitops/apps/identity-postgres` | identity-postgres | +| `gitops/apps/auth-migration` | versioned Flyway Job | +| `gitops/platform` | Vault, Keycloak, MinIO, registry와 operator | +| `gitops/platform/forward-auth` | oauth2-proxy와 Traefik ForwardAuth integration | + +환경별 차이는 각 catalog의 `overlays/`에 두고, rollout ownership은 +`gitops/clusters///stages`가 가집니다. 따라서 경로만 보고 +리소스 소유자와 실제 적용 단계를 구분할 수 있습니다. + +## Deployment graph + +```text +namespaces + | +00-platform ---- cert-manager / Keycloak Operator / Traefik policy + | + +---- Helm: MinIO Operator / Vault Secrets Operator + | +10-vault ---- Vault Running -> init/unseal/policies/roles -> KV seed + | +20-secrets ---- VaultConnection / VaultAuth / pre-data VaultStaticSecret + | +30-data ---- PostgreSQL / MinIO / Keycloak + | + +---- MinIO registry bucket/access-key provisioning -> Vault + | +35-registry ---- registry VaultStaticSecret / docker-registry + | +40-operations ---- auth-server-migrate-0-1-0 / platform-realm-v1 + | +50-apps ---- auth-server / oauth2-proxy / ingress ``` -## 네임스페이스 전략 +`all/`은 이 그래프를 하나로 렌더하지만 실행 순서를 보장하지 않습니다. +따라서 validation 전용입니다. -`mnt` 단일 namespace. 학습 단계의 단순성 우선. 실무에서는 역할별 namespace(`auth`, `storage`, `security`, `registry`) 분리가 원칙이며, base 는 환경 중립이라 overlay 재구성으로 분리 가능하다. +## Runtime boundaries -- Pod Security Standards: `pod-security.kubernetes.io/enforce=restricted` (audit + warn 동시). -- 모든 리소스는 overlay 의 `namespace: mnt` 로 일괄 주입. +| Workload | Kind | Namespace | Base | +| --- | --- | --- | --- | +| Vault | StatefulSet | `mnt` | `gitops/platform/vault` | +| identity-postgres | StatefulSet | `mnt` | `gitops/apps/identity-postgres` | +| MinIO | Tenant CR | `mnt` | `gitops/platform/minio` | +| Keycloak | Keycloak CR | `mnt` | `gitops/platform/keycloak` | +| docker-registry | Deployment | `mnt` | `gitops/platform/registry` | +| auth-server | Deployment | `mnt` | `gitops/apps/auth-server` | +| oauth2-proxy | Deployment | `mnt` | `gitops/platform/forward-auth` | +| DB migration | Job | `mnt` | `gitops/apps/auth-migration` | +| realm import | KeycloakRealmImport | `mnt` | `gitops/apps/keycloak-realm-import` | -## 워크로드 목록 +## One-shot operation contract -| 워크로드 | 종류 | 위치 | 참조 Secret | -|---|---|---|---| -| `identity-postgres` | StatefulSet | `base/app/identity/auth/stateful/` | `identity-postgres-superuser`, `keycloak-db`, `auth-server-db` | -| [`auth-server`](https://github.com/donghyeon-ka/project-auth-server/tree/develop) | Deployment | `base/app/identity/auth/stateless/` | `auth-server-db` | -| `keycloak` | Keycloak CR (Operator 생성 StatefulSet) | `overlays/dev/keycloak/` | `keycloak-db-operator`, `keycloak-bootstrap-admin-operator` | -| `minio` | Tenant CRD | `base/app/storage/minio/stateful/` | `minio-tenant-env` | -| `test-server-1/2/3` | Deployment | `base/app/test/stateless/` | — | -| `migration-flyway` | Job (PreSync / sync-wave=-1) | `base/managing/migration-flyway/` | `auth-server-db` | -| `vault` | StatefulSet | `base/plugins/vault/` | — | -| `docker-registry` | Deployment | `base/plugins/docker-registry/` | `docker-registry-basic-auth`, `docker-registry-pull-credentials` | +Flyway Job 이름은 migration release를 포함합니다: +`auth-server-migrate-0-1-0`. SQL을 변경해 새 migration release를 만들 때는 +Job instance/name도 함께 올립니다. 같은 이름의 완료된 Job을 지웠다가 +묵시적으로 재실행하지 않습니다. -auth-server / keycloak 모두 `jdbc:postgresql://identity-postgres:5432/` 로 short name 접속 (같은 namespace). +KeycloakRealmImport도 `platform-realm-v1`처럼 versioned name을 씁니다. +Operator는 기존 import CR의 spec 변경을 일반 workload rollout처럼 +재실행하지 않으므로, realm 변경은 새 version의 명시적 operation으로 냅니다. -### Flyway 실행 순서 +## Namespace decision -dev overlay 가 migration-flyway Job 에 ArgoCD annotation 을 patch: +현재 lab은 `mnt` 단일 namespace입니다. 이는 운영 권장 구조가 아니라 기존 +runtime을 깨지 않고 먼저 deployment lifecycle을 분리하기 위한 전환 단계입니다. -``` -argocd.argoproj.io/sync-wave: "-1" -argocd.argoproj.io/hook: PreSync -``` +역할별 namespace 분리는 다음을 원자적으로 바꿔야 합니다. -ArgoCD 배포 시 Job 이 앱보다 먼저 돌고 스키마 마이그레이션을 마친 뒤 `auth-server` 가 뜬다. +- Service DNS와 issuer/JWK/DB endpoint +- Vault Kubernetes auth의 bound ServiceAccount/namespace +- VSO destination Secret 위치 +- cross-namespace NetworkPolicy +- Flyway와 DB init credential ownership +- operator watch namespace와 RBAC -### Keycloak hostname patch +따라서 단순 폴더 이동과 함께 수행하지 않습니다. -dev overlay JSON patch 가 Keycloak ConfigMap 에 다음을 주입: +## Environment meaning -- `KC_HOSTNAME=https://keycloak.dev.example.com` -- `KC_HOSTNAME_ADMIN=https://keycloak-admin.dev.example.com` - -staging / prod 는 자체 hostname 을 overlay 에서 주입. - -### MinIO certConfig.dnsNames - -base 는 short name(`minio`, `minio-hl`) 만 둔다. dev overlay 에서 `minio.mnt.svc.cluster.local`, `*.minio-hl.mnt.svc.cluster.local` 을 patch — base 환경 중립성 원칙. - -## 이미지 정책 - -| 구분 | 이미지 | 근거 | -|---|---|---| -| 공식 upstream | `hashicorp/vault:1.17.2` | HashiCorp 공식 | -| | `registry:2.8.3` | Docker library 공식 | -| | `postgres:16.4` | PostgreSQL 공식 | -| | `quay.io/keycloak/keycloak:26.6.1` | Keycloak Operator 26.6.1 관리 | -| | `minio/minio:RELEASE.2025-01-20T14-49-07Z` | MinIO 공식 | -| | `flyway/flyway:10.20.1` | Flyway 공식 | -| 사용자 개발 | `registry.example.com/auth-platform/auth-server:0.1.0` | 조직 개발 서비스 | -| | `registry.example.com/test-platform/test-server-{1,2,3}:0.1.0` | 조직 개발 서비스 | - -prod 승격 시 공식 이미지도 digest pin(`@sha256:…`)으로 전환. - -## Docker Registry - -| 항목 | 값 | -|---|---| -| 이미지 | `registry:2.8.3` | -| 내부 서비스 | `docker-registry.mnt.svc.cluster.local:5000` | -| 외부 Ingress | `registry.project.com` (`/v2` only) | -| 인증 | 내부 Service 무인증, 외부 Ingress + kubelet pull 만 credential 사용 | -| 저장 | MinIO S3 bucket `docker-registry` | - -### 인증 경계 - -Registry 자체 auth 는 켜지 않는다. 인증 경계는 두 곳: - -- 외부 Ingress: Traefik `Middleware/docker-registry-basic-auth` 가 VSO 로 생성된 `docker-registry-basic-auth` Secret 의 htpasswd 를 검증 -- 내부 pull: 앱 ServiceAccount 에 `docker-registry-pull-credentials` imagePullSecret - -따라서 `docker-registry-ingress-traefik` NetworkPolicy + BasicAuth Secret + imagePullSecret 이 함께 있어야 push/pull 양쪽이 안전하다. +- `lab`: 폐기 가능한 K3s 검증 환경. stateful overlay의 local-path, HTTP + ingress, 단일 replica 허용. +- `staging`: production과 같은 보안·TLS·backup path의 승격 검증 환경. +- `prod`: HA, digest pin, backup/restore evidence, TLS, disruption budget가 + 준비되지 않으면 생성하지 않습니다. diff --git a/docs/architecture/repository-structure.md b/docs/architecture/repository-structure.md new file mode 100644 index 0000000..f99cabb --- /dev/null +++ b/docs/architecture/repository-structure.md @@ -0,0 +1,157 @@ +# Repository Structure + +## 설계 목표 + +이 구조는 특정 제품의 파일 배치보다 변경 주기와 소유권을 우선합니다. + +- 한 리소스에는 한 명확한 소유자가 있다. +- 재사용 구현과 실제 배포 진입점을 분리한다. +- 소규모 구성은 선택 영역을 생략할 수 있다. +- 규모가 커져도 기존 경계를 바꾸지 않고 같은 종류의 leaf를 추가한다. +- 사람이 실행하는 명령과 CI 검증이 같은 진입점을 사용한다. + +## 소유권 매트릭스 + +| 대상 | 소유 경로 | 직접 실행 여부 | 변경 주기 | +|---|---|---:|---| +| state backend, 초기 identity | `bootstrap/foundation` | 예 | 매우 낮음 | +| 네트워크, IAM, DNS, 클러스터 | `infrastructure/live` | 예 | 낮음 | +| 재사용 IaC 단위 | `infrastructure/components` | 아니요 | 중간 | +| 재사용 IaC 조합 | `infrastructure/stacks` | 아니요 | 중간 | +| GitOps 컨트롤러와 root 연결 | `bootstrap/gitops` | 예 | 낮음 | +| 클러스터 desired state | `gitops/clusters` | reconcile 진입점 | 지속적 | +| cluster-wide addon | `gitops/platform` | 아니요 | 중간 | +| 애플리케이션 배포 정의 | `gitops/apps` | 아니요 | 높음 | +| 정책과 tenant 정의 | `gitops/policies`, `gitops/tenants` | 아니요 | 중간 | + +Catalog 영역(`components`, `stacks`, `platform`, `policies`, `tenants`, `apps`)은 +직접 배포하지 않습니다. 실제 진입점이 필요한 항목만 조합해서 참조합니다. + +## 의존 방향 + +```text +bootstrap/foundation + │ output + ▼ +infrastructure/components ◀── infrastructure/stacks + ▲ ▲ + └──────── infrastructure/live ─┘ + │ cluster endpoint/identity + ▼ + bootstrap/gitops + │ root reference + ▼ + platform ─┐ + policies ─┼────────▶ gitops/clusters + tenants ─┤ + apps ─┘ +``` + +역방향 의존은 만들지 않습니다. 예를 들어 reusable component가 특정 +`live/prod` 값을 읽거나, app base가 특정 cluster overlay를 참조하면 안 됩니다. + +## Infrastructure 경계 + +### `components` + +네트워크, identity, registry, Kubernetes cluster처럼 작고 응집된 재사용 +단위입니다. Terraform/OpenTofu를 선택했다면 일반적으로 backend가 없는 child +module에 해당합니다. + +### `stacks` + +여러 component를 반복해서 같은 방식으로 조합할 때만 사용합니다. 작은 프로젝트는 +이 계층 없이 `live`가 component를 직접 호출할 수 있습니다. stack이 다른 stack을 +깊게 중첩하기보다는 live root에서 평평하게 조합하는 방식을 권장합니다. + +### `live` + +실제로 plan/apply하는 root입니다. leaf 하나는 다음을 만족해야 합니다. + +- 독립된 state와 locking +- 명시적인 provider와 backend 설정 +- 고정된 component/module/chart 버전 +- 비밀이 아닌 환경 입력만 저장소에 커밋 +- 출력값과 downstream contract 문서화 + +작은 구성은 `live/dev/cluster`로 충분합니다. 계정과 리전이 늘어나면 +`live/////`처럼 경로를 확장합니다. +자동화는 경로의 고정 깊이에 의존하지 말고 실행 가능한 root 파일을 기준으로 +대상을 찾도록 작성합니다. + +서로 다른 환경의 root가 상대 경로로 다른 환경 구현을 import하면 안 됩니다. +공유가 필요하면 versioned component나 명시적인 remote output/data contract를 +사용합니다. + +## GitOps 경계 + +### `clusters` + +클러스터가 reconcile하는 유일한 진입점입니다. 공통 리소스를 복사하지 않고 +platform, policy, tenant, app catalog에서 필요한 항목만 참조합니다. + +현재 프로젝트의 `lab`은 아직 GitOps controller가 없으므로 +`clusters/lab/main/namespaces`와 `stages/*`를 bootstrap script가 순서대로 적용합니다. +`clusters/lab/main/all`은 reconcile root가 아닌 감사용 aggregate입니다. + +작은 구성은 `clusters/dev/main`, 다중 리전 구성은 +`clusters///` 형태를 사용할 수 있습니다. 여기에도 +고정된 경로 깊이를 강제하지 않습니다. + +### `platform` + +cluster-wide controller와 addon을 둡니다. 예시는 다음과 같습니다. + +- ingress/gateway, external DNS, certificate +- storage class/CSI, autoscaling +- metrics, logs, traces, alerting +- secret operator와 delivery controller + +각 component는 `base`와 필요한 `overlays`를 같은 디렉터리 안에 응집시킵니다. +환경 차이는 전체 파일 복사 대신 Kustomize patch 또는 별도 values로 표현합니다. + +### `policies`, `tenants`, `apps` + +- `policies`: cluster-wide admission 규칙, 거버넌스와 예외 +- `tenants`: 구체적인 namespace, RBAC, quota, limit range, NetworkPolicy +- `apps`: application source code가 아닌 배포 정의 + +애플리케이션 팀이 별도 저장소를 소유하면 `apps`에는 그 저장소/OCI artifact를 +참조하는 GitOps 리소스만 둘 수 있습니다. + +Cloud DNS zone/delegation과 cloud IAM role은 `infrastructure`가 소유합니다. +External DNS controller, 동적 record 요청과 Kubernetes ServiceAccount binding은 +`gitops/platform`이 소유합니다. 두 계층 사이에는 zone ID, role ARN 같은 +명시적인 output contract만 전달합니다. + +## Bootstrap 경계 + +Bootstrap은 선언형 관리가 스스로 시작될 수 없는 최소 범위만 담당합니다. + +- `foundation`: state backend, 최초 CI identity와 같은 선행 조건 +- `gitops`: Flux 또는 Argo CD 중 선택한 컨트롤러 설치와 root reference + +현재 `bootstrap/gitops`에는 구현이 없으며 controller 도입 전까지 lab의 staged +bootstrap이 이 역할을 대신합니다. + +ingress, cert-manager, observability 같은 addon은 bootstrap이 아니라 GitOps가 +소유합니다. bootstrap 이후의 변경을 계속 수동 명령으로 누적하지 않습니다. + +## 규모 확장 기준 + +| 단계 | 추가하는 것 | 그대로 유지하는 것 | +|---|---|---| +| 소형 | 단일 live root, 단일 cluster root, 최소 platform | lifecycle/ownership 경계 | +| 중형 | reusable stack, staging/prod, 정책, 관측성 | component와 entrypoint 분리 | +| 대형 | 계정·리전별 state, 다중 cluster, tenants, CODEOWNERS | 한 리소스 한 소유자 | +| 조직 분리 | lifecycle/team별 repository 분리 가능 | 각 repository 내부의 동일한 계약 | + +repository를 분리하는 시점은 폴더 수가 아니라 권한, 배포 주기와 소유 팀이 +실제로 달라졌을 때입니다. + +## 설계 참고 자료 + +- [Kubernetes: Kustomize를 이용한 선언형 객체 관리](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/) +- [Flux: GitOps repository 구조](https://fluxcd.io/flux/guides/repository-structure/) +- [OpenTofu: reusable module](https://opentofu.org/docs/language/modules/) +- [OpenTofu: 평평한 module composition](https://opentofu.org/docs/language/modules/develop/composition/) diff --git a/docs/decisions/0001-lifecycle-and-ownership-boundaries.md b/docs/decisions/0001-lifecycle-and-ownership-boundaries.md new file mode 100644 index 0000000..3a85d56 --- /dev/null +++ b/docs/decisions/0001-lifecycle-and-ownership-boundaries.md @@ -0,0 +1,37 @@ +# 0001. 수명주기와 소유권 경계 + +- 상태: 승인 +- 날짜: 2026-07-26 +- 결정자: repository maintainers + +## 배경 + +인프라 저장소는 규모가 커지면서 cloud provisioning, cluster bootstrap, +platform addon과 application 배포가 뒤섞이기 쉽습니다. 이 경우 동일 리소스를 +여러 도구가 관리하거나, 작은 변경이 불필요하게 넓은 권한과 state를 요구합니다. + +## 결정 + +저장소를 다음 세 수명주기로 분리합니다. + +1. `bootstrap`: 선언형 관리가 시작되기 위한 최소 선행 조건 +2. `infrastructure`: cloud/cluster 리소스 provisioning +3. `gitops`: Kubernetes desired state의 지속적 reconciliation + +재사용 구현은 catalog 영역에 두고, `infrastructure/live`와 +`gitops/clusters`만 실제 환경 진입점으로 사용합니다. 하나의 리소스는 하나의 +수명주기와 하나의 도구만 소유합니다. + +## 결과 + +- state, 권한과 배포 실패 범위를 작게 유지할 수 있습니다. +- 소규모는 선택 디렉터리를 사용하지 않고도 시작할 수 있습니다. +- 계정, 리전과 클러스터가 늘어날 때 같은 leaf를 추가해 확장할 수 있습니다. +- 초기에는 디렉터리가 더 많아 보이지만 각 위치의 책임이 명확해집니다. + +## 대안 + +- 환경별 전체 복사: 시작은 단순하지만 공통 변경의 drift와 중복이 빠르게 증가합니다. +- 도구별 최상위 폴더: 구현 도구는 잘 보이지만 리소스 소유권과 수명주기가 섞입니다. +- 모든 리소스를 단일 state로 관리: 작은 데모에는 가능하지만 권한과 장애 범위가 + 지나치게 커집니다. diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 0000000..7358b27 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,24 @@ +# Architecture Decision Records + +프로젝트의 장기 구조에 영향을 주는 선택은 ADR로 남깁니다. + +파일명은 `NNNN-kebab-case-title.md`를 사용하고 다음 형식을 따릅니다. + +```markdown +# NNNN. 제목 + +- 상태: 제안 | 승인 | 폐기 | 대체 +- 날짜: YYYY-MM-DD +- 결정자: 팀 또는 역할 + +## 배경 + +## 결정 + +## 결과 + +## 대안 +``` + +기존 결정을 바꿀 때 문서를 지우지 말고 새 ADR에서 이전 ADR을 대체했다고 +표시합니다. diff --git a/docs/examples/infra/architecture-environments.md b/docs/examples/infra/architecture-environments.md index b1194a9..d60072c 100644 --- a/docs/examples/infra/architecture-environments.md +++ b/docs/examples/infra/architecture-environments.md @@ -96,41 +96,38 @@ spec: ## 좋은 예시 2: multi-region prod overlay 디렉터리 (kr-main + kr-dr) ```text -k8s/ - base/ - app/ - units/ - identity/ - auth/ +gitops/ + apps/ + identity-auth/ + base/ + kustomization.yaml + deployment.yaml + service.yaml + servicemonitor.yaml + pdb.yaml + hpa.yaml + overlays/ + dev/ + staging/ + prod/ + kr-main/ kustomization.yaml - deployment.yaml - service.yaml - servicemonitor.yaml - pdb.yaml - hpa.yaml - plugins/ - ingress-nginx/ - cert-manager/ - external-secrets/ - managing/ - flyway-migrate-identity/ - overlays/ - dev/ - kustomization.yaml - staging/ - kustomization.yaml - prod/ - kr-main/ - kustomization.yaml - patches/ - auth-replicas.yaml - auth-resources.yaml - auth-topology-spread.yaml - kr-dr/ - kustomization.yaml - patches/ - auth-replicas.yaml - auth-image-pull-mirror.yaml + patches/ + kr-dr/ + kustomization.yaml + patches/ + flyway-migrate-identity/ + base/ + overlays/ + platform/ + ingress-nginx/ + cert-manager/ + secret-delivery/ + clusters/ + dev/main/ + staging/main/ + prod/kr-main/ + prod/kr-dr/ ``` **왜 좋은가:** @@ -375,7 +372,7 @@ spec: image: registry.example.com/auth:1.24.3 ``` -**문제:** `selector.matchLabels`는 Deployment/StatefulSet에서 **immutable**이다. `version`은 배포마다 바뀌고 `environment`는 overlay가 주입한다 → 첫 배포 이후 재apply 시 `field is immutable` 에러로 영구 차단. selector에는 불변 3종(`name`/`instance`/`component`)만. +**문제:** `selector.matchLabels`는 Deployment/StatefulSet에서 **immutable**이다. `version`은 배포마다 바뀌고 `environment`는 overlay가 주입한다 → 첫 배포 이후 재apply 시 `field is immutable` 에러로 영구 차단. selector에는 불변 2종(`name`/`instance`)만. --- diff --git a/docs/examples/infra/config-and-secrets.md b/docs/examples/infra/config-and-secrets.md index af15af1..2fb4316 100644 --- a/docs/examples/infra/config-and-secrets.md +++ b/docs/examples/infra/config-and-secrets.md @@ -600,7 +600,7 @@ resources: - aescbc: keys: - name: fallback-2026-q1 - secret: c2VjcmV0LTMyLWJ5dGUtZmFsbGJhY2sta2V5LTIwMjZxMS1leGFtcGxl + secret: - identity: {} ``` diff --git a/docs/examples/infra/db-and-migration.md b/docs/examples/infra/db-and-migration.md index 8b683db..cb2d127 100644 --- a/docs/examples/infra/db-and-migration.md +++ b/docs/examples/infra/db-and-migration.md @@ -1,6 +1,7 @@ # db / migration 예시 -모든 YAML은 `kubectl apply` 가능하다. 상세 Flyway Job 예시는 `examples/infra/flyway.md` 참조. +모든 YAML은 `kubectl apply` 가능하다. 상세 Flyway Job 예시는 +`docs/examples/infra/flyway.md` 참조. --- diff --git a/docs/examples/infra/keycloak.md b/docs/examples/infra/keycloak.md index e972c1e..62bb861 100644 --- a/docs/examples/infra/keycloak.md +++ b/docs/examples/infra/keycloak.md @@ -105,7 +105,7 @@ metadata: app.kubernetes.io/managed-by: keycloak-operator spec: instances: 3 - image: registry.example.com/platform/keycloak:26.0.7-optimized + image: registry.example.com/platform/keycloak:26.0.7-optimized # gitleaks:allow startOptimized: true db: vendor: postgres diff --git a/docs/examples/infra/kustomize.md b/docs/examples/infra/kustomize.md index 4e46c8a..15e8aa4 100644 --- a/docs/examples/infra/kustomize.md +++ b/docs/examples/infra/kustomize.md @@ -8,35 +8,39 @@ kubectl kustomize | kubectl apply --server-side --field-manager=ci --dry-r --- -## 좋은 예시 1: base / components / overlays 전체 구조 + 실제 base `kustomization.yaml` +## 좋은 예시 1: catalog unit의 base / components / overlays 구조 ```text -k8s/ - base/ - app/units/identity/auth/ - kustomization.yaml - deployment.yaml - service.yaml - servicemonitor.yaml - pdb.yaml - hpa.yaml - components/ - with-topology-spread-zone/ - kustomization.yaml - patch.yaml - with-pdb-tier1/ - kustomization.yaml - patch.yaml - overlays/ +gitops/ + apps/ + identity-auth/ + base/ + kustomization.yaml + deployment.yaml + service.yaml + servicemonitor.yaml + pdb.yaml + hpa.yaml + components/ + with-topology-spread-zone/ + kustomization.yaml + patch.yaml + with-pdb-tier1/ + kustomization.yaml + patch.yaml + overlays/ + prod/kr-main/ + kustomization.yaml + patches/ + auth-resources.yaml + auth-ingress-host.yaml + clusters/ prod/kr-main/ kustomization.yaml - patches/ - auth-resources.yaml - auth-ingress-host.yaml ``` ```yaml -# k8s/base/app/units/identity/auth/kustomization.yaml +# gitops/apps/identity-auth/base/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: @@ -66,7 +70,7 @@ labels: ## 좋은 예시 2: base Deployment (완전 apply-ready) ```yaml -# k8s/base/app/units/identity/auth/deployment.yaml +# gitops/apps/identity-auth/base/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: @@ -206,12 +210,12 @@ spec: ## 좋은 예시 3: overlay prod/kr-main — 환경 차이만 ```yaml -# k8s/overlays/prod/kr-main/kustomization.yaml +# gitops/apps/identity-auth/overlays/prod/kr-main/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: prod-identity-auth resources: - - ../../../base/app/units/identity/auth + - ../../../base components: - ../../../components/with-topology-spread-zone - ../../../components/with-pdb-tier1 @@ -246,7 +250,7 @@ patches: ``` ```yaml -# k8s/overlays/prod/kr-main/patches/auth-resources.yaml +# gitops/apps/identity-auth/overlays/prod/kr-main/patches/auth-resources.yaml apiVersion: apps/v1 kind: Deployment metadata: @@ -302,7 +306,7 @@ spec: ## 좋은 예시 4: Kustomize Component — `with-pdb-tier1` ```yaml -# k8s/components/with-pdb-tier1/kustomization.yaml +# gitops/apps/identity-auth/components/with-pdb-tier1/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component resources: @@ -310,7 +314,7 @@ resources: ``` ```yaml -# k8s/components/with-pdb-tier1/pdb.yaml +# gitops/apps/identity-auth/components/with-pdb-tier1/pdb.yaml apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -343,7 +347,7 @@ spec: ## 좋은 예시 5: ConfigMap generator + hash suffix를 활용한 자동 rollout ```yaml -# k8s/base/app/units/identity/auth/kustomization.yaml (with generator) +# gitops/apps/identity-auth/base/kustomization.yaml (with generator) apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: @@ -373,7 +377,7 @@ generatorOptions: ## 좋은 예시 6: HPA v2 + behavior (base 리소스) ```yaml -# k8s/base/app/units/identity/auth/hpa.yaml +# gitops/apps/identity-auth/base/hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: @@ -433,12 +437,12 @@ spec: ## 나쁜 예시 1: `commonLabels`로 environment 주입 → selector immutable 에러 ```yaml -# k8s/overlays/prod/kustomization.yaml (BAD) +# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD) apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: prod-identity-auth resources: - - ../../base/app/units/identity/auth + - ../../base commonLabels: example.com/environment: prod ``` @@ -450,10 +454,10 @@ commonLabels: ## 나쁜 예시 2: overlay가 base를 거의 재작성 ```text -k8s/base/app/units/identity/auth/deployment.yaml (150 lines) -k8s/overlays/prod/deployment.yaml (140 lines, 95% identical) -k8s/overlays/staging/deployment.yaml (140 lines) -k8s/overlays/dev/deployment.yaml (135 lines) +gitops/apps/identity-auth/base/deployment.yaml (150 lines) +gitops/apps/identity-auth/overlays/prod/deployment.yaml (140 lines, 95% identical) +gitops/apps/identity-auth/overlays/staging/deployment.yaml (140 lines) +gitops/apps/identity-auth/overlays/dev/deployment.yaml (135 lines) ``` **문제:** overlay가 base의 95%를 복붙 + 몇 줄 수정. drift 발생 시점부터 base가 의미 없어진다. 해결: overlay는 `patches:` + `images:` + `replicas:` + `labels:`만 쓰고 전체 리소스는 base에서 가져온다. @@ -463,7 +467,7 @@ k8s/overlays/dev/deployment.yaml (135 lines) ## 나쁜 예시 3: 운영 secret을 `secretGenerator`로 plaintext Git 커밋 ```yaml -# k8s/overlays/prod/kustomization.yaml (BAD) +# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD) secretGenerator: - name: auth-secrets literals: @@ -478,7 +482,7 @@ secretGenerator: ## 나쁜 예시 4: `patchesStrategicMerge` / `patchesJson6902` (deprecated) ```yaml -# k8s/overlays/prod/kustomization.yaml (BAD, v5 deprecated) +# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD, v5 deprecated) patchesStrategicMerge: - patches/auth-resources.yaml patchesJson6902: @@ -497,7 +501,7 @@ patchesJson6902: ## 나쁜 예시 5: base에 환경 host / domain 고정 ```yaml -# k8s/base/app/units/identity/auth/ingress.yaml (BAD) +# gitops/apps/identity-auth/base/ingress.yaml (BAD) apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -523,11 +527,11 @@ spec: ## 나쁜 예시 6: `bases:` 사용 (v2.1에서 `resources:`로 통합됨) ```yaml -# k8s/overlays/prod/kustomization.yaml (BAD) +# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD) apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization bases: - - ../../base/app/units/identity/auth + - ../../base ``` **문제:** `bases:`는 v2.1에서 `resources:`에 흡수됨. 신규 코드에서 사용 금지. 해결: `resources:` 사용. @@ -537,7 +541,7 @@ bases: ## 나쁜 예시 7: HPA가 있는 Deployment에 overlay `replicas:`로 고정값 주입 ```yaml -# k8s/overlays/prod/kustomization.yaml (BAD — conflicts with HPA) +# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD — conflicts with HPA) replicas: - name: auth count: 3 diff --git a/docs/examples/infra/minio.md b/docs/examples/infra/minio.md index b0af153..5e3efc2 100644 --- a/docs/examples/infra/minio.md +++ b/docs/examples/infra/minio.md @@ -581,7 +581,7 @@ metadata: namespace: minio-prod type: Opaque stringData: - token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..." + token: "" --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor @@ -636,42 +636,31 @@ data: #!/bin/sh set -eu - mc alias set minio https://minio.minio-prod.svc.cluster.local "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" --api S3v4 + mc --config-dir /tmp/mc alias import minio < /tmp/render.yaml +kubectl kustomize gitops/clusters/prod/main/stages/50-apps > /tmp/render.yaml # 2) diff -kubectl diff -k k8s/overlays/prod +kubectl diff -k gitops/clusters/prod/main/stages/50-apps # 3) apply -kubectl apply -k k8s/overlays/prod +kubectl apply -k gitops/clusters/prod/main/stages/50-apps # 4) rollout status with timeout kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m @@ -607,10 +607,10 @@ kubectl get pods -A -o wide --field-selector spec.nodeName="${NODE}" git checkout v1.24.0 # 2) diff -kubectl diff -k k8s/overlays/prod +kubectl diff -k gitops/clusters/prod/main/stages/50-apps # 3) apply -kubectl apply -k k8s/overlays/prod +kubectl apply -k gitops/clusters/prod/main/stages/50-apps # 4) rollout status kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m @@ -627,7 +627,7 @@ kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m ## 나쁜 예시 1: diff 없이 apply ```bash -kubectl apply -k k8s/overlays/prod +kubectl apply -k gitops/clusters/prod/main/stages/50-apps ``` **문제:** diff --git a/docs/examples/infra/scripts.md b/docs/examples/infra/scripts.md index f66a4b6..728ca71 100644 --- a/docs/examples/infra/scripts.md +++ b/docs/examples/infra/scripts.md @@ -106,8 +106,8 @@ Environment: CONFIRM=yes non-interactive confirmation (alternative to --yes) Examples: - render-diff-apply --overlay k8s/overlays/prod --context prod-eu - CONFIRM=yes render-diff-apply --overlay k8s/overlays/prod --context prod-eu --timeout 15m + render-diff-apply --overlay gitops/clusters/prod/main/stages/50-apps --context prod-eu + CONFIRM=yes render-diff-apply --overlay gitops/clusters/prod/main/stages/50-apps --context prod-eu --timeout 15m EOF } diff --git a/docs/examples/infra/workload-selection.md b/docs/examples/infra/workload-selection.md index 75e6cbf..941627e 100644 --- a/docs/examples/infra/workload-selection.md +++ b/docs/examples/infra/workload-selection.md @@ -216,7 +216,7 @@ spec: - stateless 장기 실행 → Deployment 정답 - PDB + HPA + topologySpread (zone+hostname) 모두 tier-1에 맞게 동반 - digest pinning, restricted PodSecurity 호환, preStop sleep으로 graceful drain -- selector에는 불변 3종만 (version/environment 없음) +- selector에는 불변 2종만 (`name`/`instance`; version/environment 없음) --- diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md new file mode 100644 index 0000000..c876771 --- /dev/null +++ b/docs/guides/getting-started.md @@ -0,0 +1,146 @@ +# Getting Started + +## 현재 구현과 템플릿 확인하기 + +이 저장소에는 실제 `lab` 구성이 이미 조립되어 있습니다. + +1. `gitops/clusters/lab/main`에서 namespace와 ordered stage 조립 방식을 + 확인합니다. +2. 다음 명령으로 platform과 app이 감사용 cluster root에서 합쳐지는 결과를 + 확인합니다. `all`은 render/schema/policy 감사 전용이며 직접 apply하지 + 않습니다. + + ```bash + kubectl kustomize gitops/clusters/lab/main/all >/dev/null + ``` + +3. 다중 계정·리전·클러스터가 필요하면 account/project, region, cluster 경계를 + ADR로 결정한 뒤 `infrastructure/live//`와 + `gitops/clusters//`로 확장합니다. +4. 새 파일은 현재 구현을 참고하되 각 책임 폴더의 `_template`에서 생성합니다. + +```text +현재 구현 확인 ──▶ 경계 결정 ──▶ _template 복사 ──▶ live/clusters 확장 +``` + +## 1. 프로젝트 선택 기록 + +구현을 추가하기 전에 다음 항목을 결정하고 `docs/decisions`에 ADR을 작성합니다. + +- cloud/on-prem provider와 account/project 구조 +- Terraform, OpenTofu, Pulumi 등 IaC 엔진 +- Kustomize 중심 또는 Helm 사용 범위 +- Flux 또는 Argo CD 등 GitOps 컨트롤러 +- External Secrets 또는 SOPS 등 비밀 관리 방식 +- admission policy와 observability 운영 범위 + +선택하지 않은 도구의 빈 폴더를 모두 만들 필요는 없습니다. + +Terraform/OpenTofu를 선택했다면 IaC 파일을 추가하기 전에 다음 선택 파일을 +만들고 한 값만 활성화합니다. + +```bash +cp infrastructure/.iac-engine.example infrastructure/.iac-engine +``` + +`.iac-engine`에는 주석을 제외하고 `tofu` 또는 `terraform` 한 줄만 남깁니다. +선택한 도구와 version을 CI에도 설치·고정하고 project-specific +`init -backend=false`/`validate` 검사를 추가합니다. + +## 2. 프로젝트 메타데이터 설정 + +1. `README.md`의 제목과 프로젝트 범위를 바꿉니다. +2. `.github/CODEOWNERS.example`을 실제 소유자로 수정한 뒤 `CODEOWNERS`로 + 이름을 바꿉니다. +3. `SECURITY.md`에 조직의 보안 연락처와 SLA를 추가합니다. +4. 선택한 도구 버전을 프로젝트의 버전 관리 방식으로 고정합니다. +5. branch protection과 required check를 설정합니다. + +## 3. Foundation bootstrap + +`bootstrap/foundation` 아래에 remote state, locking, 초기 CI identity 등 +다른 인프라가 의존하는 최소 구성을 작성합니다. + +Foundation은 일반 infrastructure state와 분리하고 변경 권한을 좁게 유지합니다. +이미 조직 공통 foundation이 있다면 이 폴더에는 외부 의존 계약과 초기화 방법만 +문서화해도 됩니다. + +## 4. Infrastructure 작성 + +작은 프로젝트는 component와 live root만으로 시작합니다. + +```bash +cp -R infrastructure/components/_template infrastructure/components/kubernetes-cluster +mkdir -p infrastructure/live/dev +cp -R infrastructure/live/_template infrastructure/live/dev/cluster +``` + +동일한 조합이 여러 환경에서 반복될 때만 stack을 추가합니다. + +```bash +cp -R infrastructure/stacks/_template infrastructure/stacks/cluster +``` + +`live` root마다 backend/state를 분리하고, provider credential은 파일에 저장하지 +않습니다. + +## 5. Desired state 조립 + +필요한 catalog 템플릿을 복사합니다. + +```bash +cp -R gitops/platform/_template gitops/platform/core +cp -R gitops/apps/_template gitops/apps/example-api +mkdir -p gitops/clusters/dev +cp -R gitops/clusters/_template gitops/clusters/dev/main +``` + +component의 `base`에 공통값을 두고, 환경 차이가 있을 때만 overlay를 추가합니다. +마지막으로 cluster `kustomization.yaml`이 사용할 component를 참조하게 합니다. +복사된 README의 `__REPLACE_ME_*__` 값을 모두 실제 메타데이터로 바꿉니다. +controller에 연결하기 전에 실제 cluster root를 로컬에서 렌더해 확인합니다. + +## 6. GitOps bootstrap + +desired-state root가 준비되고 클러스터가 생성되면 `bootstrap/gitops`에서 GitOps +컨트롤러 하나를 선택해 설치합니다. 이 단계에는 다음만 포함합니다. + +- controller 설치 또는 설치 선언 +- repository/OCI source 연결 +- 검증된 `gitops/clusters/<...>` root reconcile 연결 +- controller가 secret manager에 접근하는 최소 identity + +일반 platform addon과 application은 이 단계에 넣지 않습니다. + +## 7. 검증 + +```bash +make doctor +make check +kubectl kustomize gitops/clusters/lab/main/namespaces +kubectl kustomize gitops/clusters/lab/main/all >/dev/null +``` + +프로젝트에서 실제 IaC, Helm, policy 파일을 추가하면 필요한 validator를 +`scripts/validate.sh`에 명시적으로 추가하고 CI에서도 같은 `make check`를 +호출합니다. 도구가 없을 때 조용히 성공하도록 만들지 않습니다. + +첫 환경은 다음 조건을 모두 만족하면 완료된 것으로 봅니다. + +- 실제 `live` root의 대상, state, owner와 실행 절차가 작성되어 있다. +- 실제 cluster root가 필요한 catalog base/overlay를 참조하고 비어 있지 않다. +- cluster root 렌더 결과와 IaC plan이 리뷰 가능하다. +- GitOps bootstrap root가 `_template`이 아닌 실제 cluster root를 가리킨다. +- 비밀 관리, rollback과 담당자 연락 경로가 문서화되어 있다. + +## 8. 운영 준비 + +- production apply 승인 및 concurrency lock +- backup/restore와 disaster recovery runbook +- cluster와 addon upgrade 정책 +- secret rotation과 접근 감사 +- alert routing과 담당자 +- 비용, 용량, SLO 기준 + +운영 준비가 끝나기 전에는 템플릿 placeholder 값을 production에 재사용하지 +않습니다. diff --git a/docs/ingress-traefik.md b/docs/ingress-traefik.md index 997d2f5..c60a7dc 100644 --- a/docs/ingress-traefik.md +++ b/docs/ingress-traefik.md @@ -1,248 +1,63 @@ -# Ingress / Traefik 운영 +# Lab ingress and Traefik -dev 환경은 K3s packaged Traefik 을 그대로 유지한다. 단 **`/var/lib/rancher/k3s/server/manifests/traefik.yaml` 는 수정하지 않는다.** 운영 설정은 `k8s/overlays/dev/platform/traefik/` 의 `HelmChartConfig` 로만 오버라이드한다. +K3s packaged Traefik은 유지하며 +`gitops/platform/traefik/overlays/lab/helmchartconfig.yaml`의 +`HelmChartConfig`로만 설정합니다. -## 현재 구성 +금지: -| 위치 | 역할 | -|---|---| -| `k8s/overlays/dev/platform/traefik/helmchartconfig.yaml` | Traefik replica, 기본 ingressClass, HTTP→HTTPS redirect, metrics, 기본 TLS option 연결 | -| `k8s/overlays/dev/platform/traefik/middleware.yaml` | 공용 `security-headers` Middleware + `modern-tls` TLSOption | -| `k8s/overlays/dev/auth/ingress.yaml` | `project.com` → `auth-server` | -| `k8s/overlays/dev/keycloak/ingress-public.yaml` | `keycloak.dev.example.com` → Keycloak 공개 path (`/realms/`, `/resources/`, `/.well-known/`, `/js/`) | -| `k8s/overlays/dev/platform/cert-manager/` | cert-manager `v1.20.2` CRD/controller 설치 overlay | -| `k8s/overlays/dev/platform/cert-manager-issuers/` | `letsencrypt-staging` / `letsencrypt-prod` ClusterIssuer | -| `k8s/overlays/dev/platform/keycloak-operator/` | Keycloak Operator `26.6.1`. dev 제약상 `mnt` 에 설치해 `mnt` 의 Keycloak CR 을 watch. K8s API egress NetworkPolicy 포함 | -| `k8s/overlays/dev/tls/*.yaml` | cert-manager 설치 후 발급할 `Certificate` 리소스 | -| `k8s/components/forward-auth/` | oauth2-proxy + Traefik ForwardAuth 재사용 component | -| `k8s/overlays/dev/` | 기본 dev overlay. 현재 forward-auth component 를 직접 포함 | -| `k8s/overlays/dev/keycloak-realm/` | `KeycloakRealmImport` 로 realm/client 를 Git 관리 | +- `/var/lib/rancher/k3s/server/manifests/traefik.yaml` 직접 수정 +- health/metrics/admin endpoint 공개 +- 인증 없는 management ingress +- lab HTTP/self-signed 구성을 staging/prod로 승격 -## 설계 원칙 - -- 앱은 `Ingress` 만 선언하고, 공통 보안 정책은 Traefik Middleware / TLSOption 으로 재사용 -- Keycloak 은 외부 전체 공개가 아니라 **최소 공개 path** 만 연다. `/admin`, `/metrics`, `/health` 는 비공개 -- TLS 리소스는 cert-manager + ClusterIssuer 적용 후 `tls/` overlay 에서 발급 -- north-south ingress 는 `kube-system` 의 Traefik Pod 에서만 시작 → app NetworkPolicy 도 그에 맞춰 작성 - -## ForwardAuth variant - -`k8s/components/forward-auth/` 는 oauth2-proxy + ForwardAuth Middleware 를 담은 Kustomize component 다. 현재 `k8s/overlays/dev/` 가 이 component 를 직접 포함한다. - -구성: - -- `oauth2-proxy` Deployment / Service / ConfigMap / VaultStaticSecret -- `project.com/oauth2/*` 경로용 Ingress -- `oauth2-proxy-auth` Traefik Middleware -- `auth-server` Ingress patch — `project.com/` 요청은 oauth2-proxy 를 거친 인증된 사용자만 통과 - -흐름: `Traefik ForwardAuth → oauth2-proxy → Keycloak`. - -### 적용 전제 - -- `k8s/overlays/dev/keycloak-realm/` 또는 동등한 방법으로 `platform` realm + `auth-server-ingress` client 가 준비됨 -- redirect URI: `https://project.com/oauth2/callback` -- Vault path `secret/oauth2-proxy/forward-auth` 에 `client_secret`, `cookie_secret` 저장 -- `project.com`, `keycloak.dev.example.com` 이 실제 Traefik 진입점으로 해석됨 - -### 브라우저 접속 전제 - -curl 검증은 `--resolve project.com:443:` 와 `-k` 로 DNS/TLS 문제를 우회할 수 있다. 브라우저는 이 옵션이 없으므로 dev 환경에서 직접 접속하려면 운영자가 아래를 별도로 맞춰야 한다. +## Request path ```text - project.com - keycloak.dev.example.com +client + -> Traefik Ingress + -> ForwardAuth Middleware + -> oauth2-proxy + -> Keycloak authorization endpoint + -> oauth2-proxy callback/session + -> auth-server ``` -예: Traefik `LoadBalancer` IP 중 하나가 `10.208.141.123` 이면 로컬 `/etc/hosts` 에 두 host 를 추가한다. dev overlay 는 현재 외부 ACME 발급 대신 `dev-selfsigned` ClusterIssuer 를 사용하므로 브라우저에서는 인증서 경고를 허용하거나 해당 인증서를 로컬 trust store 에 등록해야 한다. 공인 DNS 가 Traefik 진입점으로 향하고 ACME 인증서가 Ready 가 되면 이 임시 조치는 제거한다. +auth-server는 ForwardAuth 결과만 신뢰하지 않고 Keycloak JWT를 다시 검증합니다. +Ingress 인증과 API authorization은 서로 다른 경계입니다. -Chrome 에서 계속 실패하면 먼저 boundary 를 나눈다. +## Source paths -| Boundary | 확인 | -|---|---| -| 로컬 DNS | `getent hosts project.com keycloak.dev.example.com` 이 Traefik IP 를 반환해야 한다. | -| 브라우저 DNS cache | `/etc/hosts` 수정 후 Chrome 재시작 또는 `chrome://net-internals/#dns` 에서 cache clear. | -| TLS trust | `ERR_CERT_*` 가 나오면 dev self-signed 인증서를 허용하거나 trust store 에 등록한다. | -| 인증 redirect | `curl -k -D - --resolve project.com:443: https://project.com/swagger-ui.html` 가 `302 Location: https://keycloak...` 를 반환해야 한다. | -| 로그인 후 app route | 인증 후 `404 PRES-005` 는 ForwardAuth 실패가 아니라 auth-server 에 해당 route 가 없다는 뜻이다. | +| Path | Role | +| --- | --- | +| `gitops/platform/traefik/overlays/lab` | K3s HelmChartConfig와 공통 security headers | +| `gitops/platform/forward-auth/component` | oauth2-proxy, Middleware, protected ingress patch | +| `gitops/platform/forward-auth/overlays/lab` | component의 lab composition | +| `gitops/apps/auth-server/overlays/lab` | auth-server ingress와 issuer/JWK setting | +| `gitops/platform/keycloak/overlays/lab` | Keycloak public/admin ingress split | +| `gitops/apps/keycloak-realm-import/overlays/lab` | versioned realm/client import | -### 브라우저 검증 순서 +## Apply boundary -dev ForwardAuth 를 브라우저에서 직접 확인할 때는 아래 순서로 진행한다. 중간 단계를 건너뛰면 "Chrome 이 안 된다" 만 보이고 어느 boundary 가 깨졌는지 알기 어렵다. - -#### 1. Traefik 진입 IP 확인 +개별 ingress 디렉터리를 임의 순서로 적용하지 않습니다. ```bash -kubectl -n kube-system get svc traefik \ - -o jsonpath='{.status.loadBalancer.ingress[*].ip}{"\n"}' +export KUBE_CONTEXT_LAB='' +bash scripts/bin/bootstrap.sh lab ``` -예상 예시: +bootstrap은 operator와 Keycloak 준비, realm import 완료 후 auth-server를 +적용합니다. -```text -10.208.141.123 10.208.141.14 -``` +## Lab DNS/TLS -이 문서의 예시는 `10.208.141.123` 을 사용한다. 실제 클러스터에서 나온 IP 중 하나를 선택한다. +현재 host 값은 lab 검증용이며 공개 서비스 계약이 아닙니다. 브라우저 E2E를 +하려면 운영자가 DNS/hosts와 인증서 trust를 별도로 맞춰야 합니다. -#### 2. curl 로 클러스터 경로 먼저 확인 +staging/prod는 다음이 준비된 뒤 별도 overlay로 만듭니다. -브라우저를 열기 전에 curl 로 Traefik / oauth2-proxy / Keycloak boundary 가 살아있는지 확인한다. - -```bash -curl -k -sS -L \ - -D /tmp/project-infra-login.headers \ - -o /tmp/project-infra-login.body \ - --resolve project.com:443:10.208.141.123 \ - --resolve keycloak.dev.example.com:443:10.208.141.123 \ - https://project.com/swagger-ui.html -``` - -정상 신호: - -```bash -sed -n '1,80p' /tmp/project-infra-login.headers -grep -o '[^<]*' /tmp/project-infra-login.body -``` - -정상이라면 헤더에는 첫 응답 `HTTP/2 302` 와 `location: https://keycloak.dev.example.com/.../auth` 가 보이고, body title 은 아래처럼 나온다. - -```text -<title>Sign in to platform -``` - -이 단계가 실패하면 브라우저를 볼 필요가 없다. 먼저 `docs/troubleshooting.md` 의 `ForwardAuth 로그인 E2E 검증 실패` 사건에서 해당 boundary 를 찾는다. - -#### 3. 빠른 Chrome 임시 프로필로 확인 - -로컬 `/etc/hosts` 와 인증서 trust 를 건드리기 전에, Chrome 실행 옵션으로 DNS/TLS 를 임시 우회해 본다. - -```bash -google-chrome \ - --user-data-dir=/tmp/project-infra-chrome \ - --ignore-certificate-errors \ - --host-resolver-rules="MAP project.com 10.208.141.123, MAP keycloak.dev.example.com 10.208.141.123" \ - https://project.com/swagger-ui.html -``` - -정상 흐름: - -1. `https://project.com/swagger-ui.html` 접속 -2. Traefik ForwardAuth 가 미인증 요청을 감지 -3. `302` 로 Keycloak 로그인 화면 이동 -4. `Sign in to platform` 화면 표시 -5. 로그인 성공 후 `project.com` 으로 callback - -이 방식으로 성공하면 Kubernetes / Traefik / oauth2-proxy / Keycloak 경로는 정상이다. 평소 Chrome 에서 안 되는 원인은 로컬 DNS cache, `/etc/hosts`, 인증서 trust, 기존 쿠키 중 하나다. - -#### 4. 일반 Chrome 으로 볼 수 있게 hosts 등록 - -임시 Chrome 이 성공하면 로컬 OS resolver 를 맞춘다. - -```bash -sudo tee -a /etc/hosts >/dev/null <<'EOF' - -# Project-Infra dev ingress -10.208.141.123 project.com -10.208.141.123 keycloak.dev.example.com -EOF -``` - -확인: - -```bash -getent hosts project.com keycloak.dev.example.com -``` - -두 host 가 선택한 Traefik IP 를 반환해야 한다. - -#### 5. Chrome DNS cache / 기존 세션 정리 - -hosts 를 바꾼 뒤에도 Chrome 이 이전 DNS / 쿠키를 들고 있을 수 있다. - -권장 순서: - -1. `chrome://net-internals/#dns` 에서 DNS cache clear -2. `chrome://net-internals/#sockets` 에서 socket pools flush -3. `project.com`, `keycloak.dev.example.com` 사이트 데이터 삭제 -4. Chrome 완전 종료 후 재시작 - -그래도 헷갈리면 아래처럼 새 임시 프로필을 쓰는 게 가장 빠르다. - -```bash -google-chrome --user-data-dir=/tmp/project-infra-normal https://project.com/swagger-ui.html -``` - -#### 6. 인증서 경고 처리 - -dev overlay 는 현재 `dev-selfsigned` ClusterIssuer 로 TLS Secret 을 만든다. 따라서 일반 Chrome 에서는 인증서 경고가 뜰 수 있다. - -검증 목적이면 고급 옵션에서 예외를 허용한다. 장기적으로 반복 검증할 예정이면 `project-com-tls`, `keycloak-dev-example-com-tls` 인증서를 로컬 trust store 에 등록한다. - -이 경고는 dev self-signed 인증서 때문에 생기는 것으로, ForwardAuth 실패와는 다른 boundary 다. - -#### 7. 로그인 후 결과 해석 - -로그인 후 `swagger-ui.html` 이 열리면 브라우저 검증은 성공이다. - -로그인 후 `/api/me` 를 열어 `404 PRES-005` 가 나오면 이것도 ForwardAuth 실패가 아니다. 인증은 통과했고 auth-server 애플리케이션에 `/api/me` route 가 없다는 뜻이다. - -판단 기준: - -| 결과 | 의미 | -|---|---| -| Keycloak 로그인 화면이 뜸 | 미인증 redirect 정상 | -| 로그인 후 `project.com` 으로 돌아옴 | callback / token exchange / session cookie 정상 | -| `/oauth2/auth` 가 `202` | oauth2-proxy 세션 인증 정상 | -| auth-server 가 `404 PRES-005` 반환 | 인증 통과 후 application route 없음 | -| auth-server 가 `401` 반환 | Authorization header 또는 JWT validation boundary 문제 | - -### 인증 실패 처리 - -Traefik ForwardAuth 는 `/oauth2/auth` 를 호출한다. oauth2-proxy 가 `401` 또는 `403` 을 반환하면 `oauth2-proxy-errors` Middleware 가 `/oauth2/start?rd={url}` 로 넘겨 로그인 흐름을 시작한다. - -중요: Traefik errors middleware 는 기본적으로 원래 status code 를 유지할 수 있다. 그러면 oauth2-proxy 가 `Location` 을 내려도 브라우저는 `401` 응답을 자동 redirect 로 처리하지 않는다. dev 구성은 `statusRewrites` 로 `401`/`403` 을 `302` 로 바꿔 브라우저가 바로 Keycloak 로그인 화면으로 이동하게 한다. - -## cert-manager / ClusterIssuer - -repo 에 `k8s/overlays/dev/platform/cert-manager/` 와 `k8s/overlays/dev/platform/cert-manager-issuers/` 가 추가되어 있다. - -- 설치 overlay: 공식 static install `v1.20.2` -- issuer overlay: ACME HTTP-01 용 `letsencrypt-staging` / `letsencrypt-prod` - -source-of-truth 관점에서 cert-manager 도 이 repo 의 선언형 관리 대상. 단, **실제 인증서 발급은 DNS 가 Traefik 외부 진입점을 가리키고 80/443 도달이 가능해야** 완료된다. - -```bash -kubectl apply -k k8s/overlays/dev/platform/cert-manager -kubectl apply -k k8s/overlays/dev/platform/cert-manager-issuers -kubectl apply -k k8s/overlays/dev/tls -``` - -운영 보정 필요: `admin@project.com` 은 실제 운영 수신 가능한 메일로 교체. - -## Keycloak realm / client Git 관리 - -`k8s/overlays/dev/keycloak-realm/` 는 `KeycloakRealmImport` 로 `platform` realm 과 `auth-server-ingress` client 를 선언한다. `k8s/overlays/dev/keycloak/` 도 수제 `Deployment` 가 아니라 `Keycloak` CR 기반으로 전환되어 있다. - -`KeycloakRealmImport` 는 같은 `mnt` namespace 의 `Keycloak/keycloak` 을 대상으로 동작한다. - -### 적용 순서 - -```bash -kubectl apply -k k8s/overlays/dev/platform/keycloak-operator -# 기존 수제 Deployment/Service/ConfigMap/ServiceAccount keycloak* 정리 -kubectl apply -k k8s/overlays/dev -kubectl apply -k k8s/overlays/dev/keycloak-realm -``` - -## 적용 범위 - -repo 가 커버하는 것: - -- Traefik 운영 정책의 Git 관리 -- app ingress host / path / policy 정의 -- Traefik → app 방향 ingress allow NetworkPolicy -- TLS `Certificate` 선언 준비 -- ForwardAuth variant 와 KeycloakRealmImport 선언 - -> 미완 항목(DNS / ACME 발급 / end-to-end 테스트)은 README 의 [Limitations](../README.md#limitations-honest-scope) 섹션을 참고. +- 실제 DNS ownership +- cert-manager Issuer/Certificate +- TLS redirect와 HSTS +- 신뢰 가능한 oauth2-proxy OIDC issuer +- registry ingress의 인증·TLS diff --git a/docs/networking.md b/docs/networking.md index 82c1e41..01d30f4 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -1,31 +1,31 @@ -# NetworkPolicy 매트릭스 +# Lab networking -단일 namespace(`mnt`) 내부에서도 서비스 간 트래픽을 **최소권한** 으로 제한한다. baseline 은 모든 Pod 의 ingress/egress 를 차단하고, 컴포넌트별로 필요한 경로만 명시적으로 연다. +Namespace `mnt`는 `gitops/policies/baseline`에서 ingress와 egress를 모두 default-deny로 +시작하고 kube-dns UDP/TCP 53만 공통 허용합니다. -## 정책 목록 +| Owner | Allowed traffic | +| --- | --- | +| `gitops/apps/auth-server` | Traefik -> auth-server:8080; auth-server -> PostgreSQL:5432, Keycloak:8080 | +| `gitops/apps/auth-migration` | migration Job -> PostgreSQL:5432 | +| `gitops/apps/identity-postgres` | Keycloak, auth-server, migration Job -> PostgreSQL:5432 | +| `gitops/platform/minio` | MinIO peer; MinIO Operator; approved in-namespace clients | +| `gitops/platform/keycloak` | Traefik/auth-server ingress; PostgreSQL egress; Keycloak peer | +| `gitops/platform/registry` | Traefik/in-namespace ingress; MinIO egress | +| `gitops/platform/vault` | VSO controller ingress; kube-apiserver TokenReview egress | +| `gitops/platform/keycloak-operator` | kube-apiserver egress | -| 정책 파일 | 역할 | -|---|---| -| `overlays/dev/networkpolicy-baseline.yaml` | `default-deny-all` (전 Pod ingress/egress 기본 차단) + `allow-dns-egress` (kube-system/kube-dns 53) | -| `overlays/dev/database/networkpolicy.yaml` | identity-postgres ingress ← keycloak / auth-server / migration-flyway (5432) | -| `overlays/dev/auth/networkpolicy.yaml` | auth-server ingress ← `kube-system/traefik`(8080); egress → postgres(5432) + keycloak(8080); flyway egress → postgres(5432) | -| `overlays/dev/keycloak/networkpolicy.yaml` | keycloak ingress ← `kube-system/traefik`(8080) + auth-server(8080); egress → postgres(5432); Keycloak Pod 간 peer 통신 (Infinispan/JGroups) | -| `overlays/dev/storage/networkpolicy.yaml` | minio ingress ← `part-of=auth-platform`(9000); 자체 peer(9000/9001) | -| `overlays/dev/test/networkpolicy.yaml` | test-server 3 대 내부 상호 통신만 허용 | -| `overlays/dev/vault/networkpolicy.yaml` | vault ingress ← VSO Operator Pod(8200) | -| `overlays/dev/registry/networkpolicy.yaml` | docker-registry ingress ← namespace 내 전 Pod(5000) + `kube-system/traefik`(5000); egress → minio(9000) | +NetworkPolicy CIDR `10.43.0.0/16`과 `10.208.141.0/24`는 lab K3s의 service +CIDR/control-plane subnet 계약입니다. 클러스터 설정이 다르면 overlay에서 +명시적으로 변경해야 합니다. staging/prod로 복사하지 않습니다. -## 작성 규칙 +정책 추가 순서: -- cross-namespace 참조가 필요한 항목(예: `kube-system/traefik`)은 `namespaceSelector` + `podSelector` 를 한 블록에 조합해 **AND 시맨틱** 으로 작성한다. 두 selector 를 별도 블록에 두면 OR 가 되어 정책이 헐거워진다. -- north-south ingress 는 `kube-system` 의 Traefik Pod 에서만 시작되므로, app 측 NetworkPolicy 도 실제 클러스터 기준으로 `kube-system` 을 허용해야 한다 (`ingressClassName=traefik` 만으로는 부족). -- baseline default-deny 가 켜져 있는 한, 새 워크로드를 올릴 때마다 ingress / egress 를 **명시적으로** 추가해야 한다. 이게 의도된 마찰이다 (실수로 wide-open 으로 시작하지 않도록). +1. source/destination workload label과 namespace를 확인합니다. +2. 실제 named port와 protocol을 확인합니다. +3. 가장 좁은 pod/namespace selector로 ingress와 egress 양쪽을 작성합니다. +4. 해당 stage를 렌더하고 정적 검증합니다. +5. server-side dry-run과 diff 후 적용합니다. +6. 허용 요청과 차단되어야 할 요청을 모두 확인합니다. -## Keycloak Operator 추가 고려사항 - -Keycloak Operator 가 Keycloak Pod 를 만들고 watch 하기 때문에 default-deny 환경에서는 다음 두 가지를 NetworkPolicy 로 명시한다: - -- Keycloak Operator 의 Kubernetes API egress (CR reconcile) -- Keycloak Pod 간 Infinispan/JGroups peer 통신 (cluster mode) - -해당 정책은 `overlays/dev/keycloak/networkpolicy.yaml` 에 함께 들어 있다. +`ipBlock`은 Kubernetes API처럼 selector로 표현할 수 없는 lab 경계에서만 +사용합니다. 넓은 RFC1918 CIDR 전체 허용을 기본값으로 만들지 않습니다. diff --git a/docs/operations.md b/docs/operations.md index 6f6bd03..cfe2299 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -1,83 +1,120 @@ -# 운영 / 검증 +# Operations and validation -bootstrap, teardown, validate.sh, 환경별 차등 계획의 **설계 의도** 를 정리한 문서. 단계별 실제 실행 절차는 [guide.md](../guide.md) 에 있다. - -## bootstrap 단계 - -`VaultConnection` / `VaultAuth` / `VaultStaticSecret` 은 VSO Helm 설치로 CRD 가 등록된 뒤에만 apply 할 수 있다. 그래서 `overlays/dev/vso/` 는 dev kustomization 집계에 포함되지 않으며, `bin/bootstrap.sh` 마지막 단계에서 별도로 `kubectl apply -k overlays/dev/vso/` 한다. - -| Phase | 작업 | 의존하는 직전 상태 | 멱등 안전? | -|:---:|---|---|---| -| 0 | MinIO Operator Helm install (`tasks/minio-operator-install.sh`) | helm 가능한 클러스터 | ✅ `helm upgrade --install` | -| 1 | `kubectl apply -k base/managing/namespace/` (PSS restricted 라벨 선행) | — | ✅ `kubectl apply` | -| 2 | VSO-managed Secret 점검 | namespace 존재 | ⚠️ `RESET_STALE_SECRETS=yes` 옵션 시 파괴적 | -| 3 | `kubectl apply -k overlays/dev/` (vault + registry + 앱) | namespace + PSS 라벨 | ✅ `kubectl apply` | -| 4 | `vault-0` Pod Running 대기 | Phase 3 의 Vault StatefulSet | ✅ wait 만 | -| 5 | `tasks/vault-init.sh` (init / unseal / auth / policy×2 / role×2) | `vault-0` Running | ✅ 상태 체크 후 차이만 적용 | -| 6 | `tasks/vso-install.sh` (helm upgrade --install) | Vault auth/role 준비 | ✅ `helm upgrade --install` | -| 7 | `kubectl apply -k overlays/dev/vso/` (VaultConnection / VaultAuth / VaultStaticSecret) | Phase 6 의 VSO CRD 등록 | ✅ `kubectl apply` | - -Phase 5 의 1 회성 셋업 흐름은 [secret-pipeline-bootstrap 시퀀스](diagrams/sequence/secret-pipeline-bootstrap.md), Phase 7 이후의 정상 reconcile 은 [secret-pipeline-runtime 시퀀스](diagrams/sequence/secret-pipeline-runtime.md) 참고. +## Bootstrap ```bash -# dev — 비밀번호를 프롬프트에서 무음 입력 (bash history 에 안 남음) -bash k8s/scripts/bin/bootstrap.sh dev - -# teardown — 대화형 y/N -bash k8s/scripts/bin/teardown.sh dev +export KUBE_CONTEXT_LAB='<expected-context>' +bash scripts/bin/bootstrap.sh lab ``` -## 스크립트 구조 +각 stage는 `render -> server-side dry-run -> diff -> confirm -> apply` 순서로 +실행됩니다. `CONFIRM=yes`는 비대화 환경에서만 사용하고 context 확인을 +우회하지 않습니다. -`k8s/scripts/` 는 `bin / ci / lib / tasks` 4 축: +| Order | Entrypoint / task | Completion boundary | +| ---: | --- | --- | +| 1 | `namespaces` | Namespace Active | +| 2 | `00-platform` | cert-manager와 Keycloak Operator Available | +| 3 | MinIO/VSO Helm task | controller Ready, CRD registered | +| 4 | `10-vault` | `vault-0` Running | +| 5 | Vault init/unseal/policy/seed | KV와 Kubernetes auth 준비 | +| 6 | `20-secrets` | data 선행 destination Secret 생성 | +| 7 | `30-data` | PostgreSQL/MinIO/Keycloak Ready | +| 8 | MinIO registry provision task | bucket/access key 생성 후 Vault 기록 | +| 9 | `35-registry` | registry Secret 생성과 Deployment rollout 완료 | +| 10 | `40-operations` | Flyway Complete, RealmImport Done | +| 11 | `50-apps` | auth-server/oauth2-proxy rollout 완료 | -| 디렉토리 | 역할 | -|---|---| -| `bin/` | 사용자 진입점. `bootstrap.sh` / `teardown.sh` | -| `ci/` | CI / 로컬 검증. `validate.sh` (kustomize + kubeconform + kube-linter) | -| `lib/` | 공통 Bash 라이브러리. `common.sh` (strict mode / trap / log / confirm / retry / mask_secret) + `vault.sh` | -| `tasks/` | 재사용 작업. `vault-init.sh` / `vault-seed-apps.sh` / `vso-install.sh` | +`gitops/clusters/lab/main/all`은 절대 apply하지 않습니다. -모든 쉘 스크립트는 `set -Eeuo pipefail` + `IFS=$'\n\t'` + `trap_cleanup` 으로 공통 에러 처리. root token / registry BasicAuth 같은 민감 값은 **stdin 파이프** 로만 전달하고 stdout 에 찍지 않는다. +## Vault init material -## 검증 (validate.sh) +기본 lab 경로는 repo root의 ignored `vault-init-keys.json`입니다. 스크립트는 +`0600`으로 쓰지만 암호화 파일은 아닙니다. `VAULT_KEYS_FILE`로 repo 밖의 +안전한 위치를 지정하는 방식을 권장하며 prod에서는 필수입니다. + +키, root token, password, MinIO secret key를 argv로 전달하지 않습니다. +unseal/login, JSON 조립, Vault 기록은 stdin 경로를 사용합니다. + +`docker-registry/minio`는 일반 seed 대상이 아닙니다. `30-data`에서 MinIO가 +Ready가 된 뒤 MinIO가 bucket-scoped access key를 생성하고, bootstrap task가 +그 결과를 Vault에 기록합니다. `35-registry`는 그 이후에만 VSO CR과 registry +Deployment를 적용하므로 missing Secret 상태의 Pod를 만들지 않습니다. + +## Secret rotation + +VSO destination은 `overwrite: true`로 선언되어 Vault 변경을 Kubernetes +Secret에 반영합니다. auth-server와 oauth2-proxy는 지원되는 Secret 변경 시 +rollout target을 사용합니다. + +다음 credential은 외부 시스템 상태와 함께 회전해야 하므로 Vault 값만 바꾸면 +안 됩니다. + +- PostgreSQL role password +- Keycloak DB password +- MinIO access key credential +- registry basic-auth/pull credential + +각 소비자와 backend credential을 순서대로 갱신하고 stage health를 확인하는 +별도 rotation runbook이 필요합니다. + +## Teardown + +기본 teardown은 앱과 one-shot operation만 삭제합니다. ```bash -bash k8s/scripts/ci/validate.sh +bash scripts/bin/teardown.sh lab ``` -3 단계: +데이터, Vault, namespace까지 삭제하려면 명시적으로 opt-in합니다. -1. 각 overlay 에 대해 `kustomize build` (환경 중립성 / patch 유효성) -2. 렌더 결과에 `kubeconform -strict -ignore-missing-schemas` (Kubernetes OpenAPI + Datree CRD catalog) -3. 렌더 결과에 `kube-linter lint --config .kube-linter.yaml` (securityContext / resources / PSS / image tag 등) - -`.kube-linter.yaml` 은 **블록 단위 분석으로 생기는 컨텍스트 오탐 4 종**(`dangling-service`, `non-existent-service-account`, `mismatching-selector`, `no-anti-affinity`) 만 제외한다. 나머지는 모두 활성. - -목표 상태: - -``` -k8s/overlays/dev build=ok schema=ok lint=ok -k8s/overlays/dev/vso build=ok schema=ok lint=ok +```bash +DELETE_DATA=yes bash scripts/bin/teardown.sh lab ``` -## 환경별 배포 +공유 operator와 cluster-scoped 리소스까지 삭제하는 것은 전용 lab cluster에서만 +사용합니다. -현재 `dev` overlay 만 완성. `staging` / `prod` 는 의도적으로 비어 있고 추후 확장 예정. validate.sh 는 `kustomization.yaml` 이 없는 환경을 자동 스킵한다 — 빈 overlay 가 CI 를 빨갛게 만들지 않기 위함. +```bash +DELETE_DATA=yes TEARDOWN_PLATFORM=yes \ + bash scripts/bin/teardown.sh lab +``` -### 계획된 환경별 차등 +`FORCE_FINALIZERS=yes`는 정상 삭제가 반복해서 실패한 namespace 복구의 최후 +수단입니다. orphaned volume과 controller state를 만들 수 있습니다. -| 리소스 | dev | staging | prod | -|---|---|---|---| -| Vault replicas / storage | 1 / 1Gi | 1 / 5Gi | 3 (HA Raft) / 20Gi | -| Registry replicas / storage | 1 / 5Gi | 1 / 10Gi | 2 / 50Gi | -| PostgreSQL retention policy | Delete | Retain | Retain | -| 이미지 tag 정책 | semver tag | semver tag | `@sha256:` digest pin | -| TLS | 비활성화 | cert-manager | cert-manager + HSTS | +## Validation -prod 승격 시 필수 작업: +```bash +make check +``` -- Vault storage `file` → `raft` + KMS auto-unseal -- Postgres backup CronJob (Velero / pgBackRest) -- cert-manager ClusterIssuer 로 TLS 전환 -- 이미지 tag → digest pin +로컬 profile은 render를 항상 수행하고 설치되지 않은 부가 도구는 알려준 뒤 +건너뜁니다. full profile은 다음 도구가 모두 없으면 실패합니다. + +- kustomize 또는 kubectl +- kubeconform +- kube-linter +- shellcheck +- shfmt +- gitleaks + +```bash +mise install +VALIDATION_PROFILE=full make check +``` + +Gitea workflow는 full profile을 실행합니다. 검증 entrypoint의 source of truth는 +`tests/kustomize-entrypoints.txt`입니다. + +## External incident actions + +현재 tree에서 민감 파일을 untrack/ignore하는 것만으로 과거 노출은 해결되지 +않습니다. 다음 작업은 live Vault와 모든 협업자에게 영향을 주므로 repository +refactor와 분리합니다. + +1. root token과 unseal/recovery material 회전 +2. 영향 credential 전체 회전 +3. 백업과 감사 로그에서 노출 범위 확인 +4. 협업자에게 force-fetch/reclone 절차 공지 +5. 승인된 maintenance window에서 원격 Git history 정리 diff --git a/docs/runbooks/README.md b/docs/runbooks/README.md new file mode 100644 index 0000000..14b2934 --- /dev/null +++ b/docs/runbooks/README.md @@ -0,0 +1,16 @@ +# Runbooks + +운영자가 긴급 상황에서도 그대로 실행할 수 있는 절차를 둡니다. 프로젝트를 +운영하기 전에 최소한 다음 runbook을 준비합니다. + +- foundation/state 접근 복구 +- 실패한 plan/apply 복구와 state lock 처리 +- GitOps controller 복구와 reconciliation 중지/재개 +- cluster 및 핵심 addon upgrade/rollback +- secret rotation과 credential 노출 대응 +- backup restore와 disaster recovery +- 인증서, DNS, ingress 장애 대응 +- 관측성 또는 alert pipeline 장애 대응 + +각 문서는 `목적`, `사전 조건`, `영향`, `절차`, `검증`, `롤백`, +`에스컬레이션` 섹션을 포함해야 합니다. diff --git a/docs/runbooks/_template.md b/docs/runbooks/_template.md new file mode 100644 index 0000000..3610262 --- /dev/null +++ b/docs/runbooks/_template.md @@ -0,0 +1,15 @@ +# Runbook 제목 + +## 목적 + +## 사전 조건 + +## 영향 + +## 절차 + +## 검증 + +## 롤백 + +## 에스컬레이션 diff --git a/docs/security-hardening.md b/docs/security-hardening.md index 2bcd0ba..f3257b8 100644 --- a/docs/security-hardening.md +++ b/docs/security-hardening.md @@ -127,7 +127,7 @@ root token 은 **비상시 (rekey / generate-root / 전체 복구) 전용**으 REPO_ROOT="$(pwd)" \ VAULT_ADMIN_USERNAME='alice' \ VAULT_ADMIN_PASSWORD='<초기 비밀번호>' \ -bash k8s/scripts/tasks/vault-setup-admin.sh +bash scripts/tasks/vault-setup-admin.sh ``` 이 스크립트가 수행: @@ -185,7 +185,7 @@ vault token revoke <old-root-token> REPO_ROOT="$(pwd)" \ VAULT_ADMIN_USERNAME='bob' \ VAULT_ADMIN_PASSWORD='<임시 pw>' \ -bash k8s/scripts/tasks/vault-setup-admin.sh +bash scripts/tasks/vault-setup-admin.sh # 제거 vault delete auth/userpass/users/bob @@ -227,7 +227,7 @@ vault audit enable socket address=loki-syslog.monitoring.svc:514 socket_type=tcp ### 권장 — 대화형 입력 ```bash -bash k8s/scripts/bin/bootstrap.sh dev +bash scripts/bin/bootstrap.sh lab # 프롬프트에서 무음 입력 (echo 안 됨) ``` @@ -245,7 +245,7 @@ KEYCLOAK_DB_PASSWORD='...' \ AUTH_SERVER_DB_PASSWORD='...' \ KEYCLOAK_ADMIN_PASSWORD='...' \ MINIO_ROOT_PASSWORD='...' \ -bash k8s/scripts/bin/bootstrap.sh dev +bash scripts/bin/bootstrap.sh lab # 또는 세션 전체 history 비활성화 set +o history diff --git a/docs/standards/infra/STYLE.md b/docs/standards/infra/STYLE.md index 970bb8d..ad88d29 100644 --- a/docs/standards/infra/STYLE.md +++ b/docs/standards/infra/STYLE.md @@ -25,7 +25,7 @@ | 키 | 허용 값 | | --- | --- | -| `example.com/environment` | `dev` \| `staging` \| `prod` | +| `example.com/environment` | `lab` \| `dev` \| `staging` \| `prod` | | `example.com/owner-team` | 팀 slug (e.g., `auth-platform`, `sre`) | | `example.com/cost-center` | 회계 코스트 센터 ID | | `example.com/data-classification` | `public` \| `internal` \| `confidential` \| `restricted` | @@ -149,7 +149,7 @@ spec: 1. **prod 환경**: `<registry>/<path>@sha256:<digest>` 형태 digest pin **필수**. 뮤터블 태그(`:1`, `:latest`, `:main`) 금지. 2. **staging**: digest 권장, 최소 semver tag(`:1.24.0`) 허용. 절대 `:latest` 금지. -3. **dev**: semver tag 허용, `:latest` 지양 (로컬 / 노드 cache invalidation 이슈). +3. **lab / dev**: semver tag 허용, `:latest` 지양 (로컬 / 노드 cache invalidation 이슈). `lab` 은 폐기 가능한 단일 클러스터 실험 환경에만 사용한다. 4. `imagePullPolicy`: - digest 사용 시 `IfNotPresent` (이미지 콘텐츠는 immutable) - 뮤터블 태그 사용 시 `Always` @@ -265,7 +265,7 @@ spec: 이 문서의 규약은 CI 에서 기계 검증된다. -- 실행: `k8s/scripts/ci/validate-docs.sh` +- 실행: `scripts/ci/validate-docs.sh` - Lint 설정 위치: `.kube-linter.yaml` (repo root) - 목표 스코어: - syntax 에러: **0** diff --git a/docs/standards/infra/architecture-environments.md b/docs/standards/infra/architecture-environments.md index 2b191a3..eb06968 100644 --- a/docs/standards/infra/architecture-environments.md +++ b/docs/standards/infra/architecture-environments.md @@ -12,7 +12,7 @@ 이 문서의 목표: -- dev / staging / prod 환경 분리를 **label·namespace·selector 레벨에서** 일관되게 만든다 +- lab / dev / staging / prod 환경 분리를 **label·namespace·selector 레벨에서** 일관되게 만든다 - 서비스별 리소스 소유권(팀·도메인·컴포넌트)을 label로 쿼리 가능하게 한다 - K3s packaged component와 사용자 AddOn을 혼동하지 않는다 - 멀티 서버에서 `manifests/` 디렉터리를 source-of-truth로 쓰는 사고를 원천 차단한다 @@ -35,11 +35,13 @@ 기본 환경: +- `lab` — 폐기 가능한 실험 환경 - `dev` - `staging` - `prod` -필요 시 `sandbox` / `canary` / `dr`을 추가할 수 있으나 dev/staging/prod 의미를 흐리지 않는다. +`lab`은 운영 승격 단계가 아니다. 필요 시 `canary` / `dr`을 추가할 수 있으나 +dev/staging/prod 의미를 흐리지 않는다. 각 리소스는 두 곳에 동시에 환경이 드러나야 한다. @@ -95,7 +97,7 @@ well-known label 6종으로 표현되지 않는 축은 다음 키로 고정한다. -- `example.com/environment` — `dev|staging|prod|canary|dr` +- `example.com/environment` — `lab|dev|staging|prod|canary|dr` - `example.com/team` — 소유 팀 (예: `identity-sre`) - `example.com/tier` — `frontend|backend|data|platform` - `example.com/data-classification` — `public|internal|confidential|restricted` @@ -107,7 +109,7 @@ well-known label 6종으로 표현되지 않는 축은 다음 키로 고정한 - `app.kubernetes.io/environment` 사용 (well-known set에 없음) - 도메인 없는 커스텀 키 (`environment: prod` 같은 top-level key) -### 6. selector에 들어가는 label은 **불변 3종만** +### 6. selector에 들어가는 label은 **불변 2종만** Deployment / StatefulSet의 `selector.matchLabels`는 일단 apply 후 수정 불가다. 여기에는 운영 중 **절대 바뀌지 않는** 값만 넣는다. @@ -115,11 +117,11 @@ Deployment / StatefulSet의 `selector.matchLabels`는 일단 apply 후 수정 - `app.kubernetes.io/name` - `app.kubernetes.io/instance` -- `app.kubernetes.io/component` 금지 (selector에 넣지 말 것): - `app.kubernetes.io/version` (배포 때마다 바뀜) +- `app.kubernetes.io/component` (역할 재분류 시 immutable selector 충돌) - `app.kubernetes.io/managed-by` (툴 교체 시 drift) - `example.com/environment` (overlay에서 주입되면 selector immutable 위반) @@ -146,10 +148,11 @@ Deployment / StatefulSet의 `selector.matchLabels`는 일단 apply 후 수정 기본: -- Git repo의 `k8s/` 디렉터리가 SoT +- Git repo의 `gitops/` 디렉터리가 Kubernetes desired state의 SoT - CI/ArgoCD/Flux가 `kubectl apply --server-side`로 push - 서버별 scp / vim 절대 금지 -- 멀티 서버 bootstrap AddOn도 Git 관리(예: `k8s/bootstrap/*`를 첫 서버에만 배치) +- 멀티 서버 bootstrap AddOn도 Git 관리(`bootstrap/`에서 최소 설치 후 + `gitops/clusters/` root로 인계) ### 9. GitOps apply는 Server-Side Apply가 기본 @@ -168,20 +171,25 @@ kubectl diff --server-side -k <overlay> 이후 `kustomize.md`에서 상세히 다룬다. 이 문서에서는 원칙만 박는다. -- `k8s/base/` — 공통 shape, 환경-agnostic -- `k8s/overlays/{dev,staging,prod}/` — patches / images / replicas / resources / labels +- `gitops/{apps,platform,policies,tenants}/<unit>/base` — 공통 shape, 환경-agnostic +- 각 unit의 `overlays/{lab,dev,staging,prod}` — patches / images / replicas / resources / labels +- `gitops/clusters/<env>/<cluster>` — catalog를 선택하는 실제 rollout entrypoint overlay는 base를 재작성하지 않는다. overlay diff가 100줄을 넘으면 base 설계 실패 신호다. -### 11. `app/managing/plugins` 책임 분리 +### 11. catalog와 rollout 책임 분리 -`k8s/base/` 하위는 다음 3축으로 고정한다. +`gitops/` 하위 소유권은 다음 축으로 고정한다. -- `app/units/<domain>/<service>/` — 애플리케이션 유닛 (auth, keycloak, test-server) -- `managing/` — Job/CronJob 운영 작업 (flyway-migrate, backup, restore, bootstrap admin) -- `plugins/` — 플랫폼 (ingress-controller, cert-manager, external-secrets, observability, policy) +- `apps/` — application-facing workload와 그 app이 독점 소유하는 data/operation +- `platform/` — 여러 app이 공유하는 platform service와 operator +- `policies/` — admission, security와 governance policy +- `tenants/` — namespace, RBAC, quota와 tenant boundary +- `clusters/` — 환경/클러스터별 최종 조립과 rollout entrypoint -이 축은 **소유 팀이 다르다**는 가정 위에 있다. 각 축은 독립된 Git owner (CODEOWNERS)를 가진다. +stateful 여부보다 실제 lifecycle owner를 우선합니다. 예를 들어 app 전용 +PostgreSQL과 Flyway는 해당 app catalog가, 공용 MinIO와 Vault는 platform이 +소유합니다. 각 축은 독립된 Git owner(CODEOWNERS)를 가질 수 있습니다. ### 12. 상태 저장 / 외부 공개 범위를 architecture 단계에서 분류 @@ -247,50 +255,41 @@ K3s multi-server에서는 아래가 모든 서버에서 동일해야 한다(불 ## 추천 디렉터리 구조 ```text -k8s/ - base/ - app/ - shared/ - units/ - identity/ - auth/ - kustomization.yaml - keycloak/ - kustomization.yaml - data/ - postgres-identity/ - kustomization.yaml - managing/ - flyway-migrate-identity/ - backup-postgres/ - plugins/ - ingress-nginx/ - cert-manager/ - external-secrets/ - kube-prometheus-stack/ - overlays/ - dev/ - kustomization.yaml - staging/ - kustomization.yaml - prod/ - kustomization.yaml - region-kr-main/ - region-kr-dr/ - bootstrap/ - k3s-addons-disabled/ - scripts/ - render.sh - diff.sh - apply.sh +bootstrap/ + foundation/ + gitops/ +gitops/ + apps/ + auth-server/ + base/ + overlays/{lab,staging,prod}/ + identity-postgres/ + auth-migration/ + platform/ + ingress-nginx/ + cert-manager/ + secret-delivery/ + policies/ + baseline/ + tenants/ + identity/ + clusters/ + lab/main/stages/ + staging/main/stages/ + prod/kr-main/stages/ + prod/kr-dr/stages/ +scripts/ + bin/ + ci/ + tasks/ ``` ## 프로젝트 기준 요약 -- 환경 3종(`dev`/`staging`/`prod`) + namespace prefix 고정 +- 환경 4종(`lab`/`dev`/`staging`/`prod`) + namespace prefix 고정 - well-known `app.kubernetes.io/*` 6개 + 자체 도메인 운영 label 필수 - `app.kubernetes.io/environment` 사용 금지, `example.com/environment`로 대체 -- selector에는 불변 3종만 +- selector에는 불변 2종(`name`/`instance`)만 - K3s packaged component는 초기에 disable 여부 결정, 직접 수정 금지 - `manifests/`는 apply sink, Git이 SoT - `kubectl apply --server-side` GitOps 기본 diff --git a/docs/standards/infra/db-and-migration.md b/docs/standards/infra/db-and-migration.md index 7828939..7fcebb9 100644 --- a/docs/standards/infra/db-and-migration.md +++ b/docs/standards/infra/db-and-migration.md @@ -90,7 +90,8 @@ Flyway migration은 **독립 실행 단계**다. - CI/CD 명시 단계 - 운영자 명시 실행 절차 -장기 실행 Deployment에 넣지 않는다. Flyway 예시는 `examples/infra/flyway.md` 참조. +장기 실행 Deployment에 넣지 않는다. Flyway 예시는 +`docs/examples/infra/flyway.md` 참조. ### 5. migration Job은 배포 흐름 안에서 app보다 먼저 실행 migration을 app보다 **선행**시키는 것은 manifest 메타데이터로 선언한다. @@ -139,7 +140,9 @@ metadata: 4. `flyway info` (결과 확인) 5. app rollout -`validateOnMigrate=true` 기본값이 있더라도, 운영 runbook에서는 validate 단계를 **분리 Job** 또는 **initContainer**로 분리한다. `examples/infra/flyway.md` 참조. +`validateOnMigrate=true` 기본값이 있더라도, 운영 runbook에서는 validate 단계를 +**분리 Job** 또는 **initContainer**로 분리한다. +`docs/examples/infra/flyway.md` 참조. ### 8. migration source는 Git이 source of truth 중요한 것은 아래다. diff --git a/docs/standards/infra/keycloak.md b/docs/standards/infra/keycloak.md index 64b43a2..6c86863 100644 --- a/docs/standards/infra/keycloak.md +++ b/docs/standards/infra/keycloak.md @@ -75,7 +75,7 @@ Pod 내부에서 TLS 재암호화가 필요 없으면 Pod는 HTTP로 수신한 기본: - `KC_DB=postgres` -- `KC_DB_URL=jdbc:postgresql://keycloak-db-rw:5432/keycloak` (CloudNativePG `-rw` RW endpoint 권장) +- `KC_DB_URL=jdbc:postgresql://keycloak-db-rw:5432/keycloak` (CloudNativePG `-rw` RW endpoint 권장) <!-- gitleaks:allow --> - `KC_DB_USERNAME`, `KC_DB_PASSWORD` → Secret `secretKeyRef` - Keycloak schema와 auth-server schema는 **다른 DB 또는 다른 database**로 분리 diff --git a/docs/standards/infra/kustomize.md b/docs/standards/infra/kustomize.md index cdef35c..6bc3b32 100644 --- a/docs/standards/infra/kustomize.md +++ b/docs/standards/infra/kustomize.md @@ -57,10 +57,10 @@ Kustomize는 Kubernetes 리소스를 **template-free**로 조합하고 환경별 overlay 한 디렉터리의 `kustomization.yaml`은 짧아야 한다. diff가 몇 백 줄을 넘으면 base 설계 실패 신호. -권장 구조: +권장 unit overlay 구조: ``` -overlays/prod/ +gitops/apps/auth-server/overlays/prod/ kustomization.yaml patches/ auth-replicas.yaml @@ -70,21 +70,26 @@ overlays/prod/ postgres-storage.yaml ``` -### 4. 디렉터리 구조는 base / components / overlays 3축 +### 4. catalog unit과 cluster entrypoint를 분리 ``` -k8s/ - base/ - app/units/<domain>/<service>/ - managing/<job>/ - plugins/<platform>/ - components/ - <reusable-cross-cutting>/ - overlays/ - <env>/[region/] +gitops/ + apps/<unit>/ + base/ + components/ + overlays/<env>/ + platform/<unit>/ + base/ + components/ + overlays/<env>/ + policies/<unit>/ + tenants/<unit>/ + clusters/<env>/<region-or-cluster>/ ``` -`components/`는 "Kustomize Components"로, 여러 overlay에서 재사용. +`components/`는 소유 unit 내부의 Kustomize Component입니다. 여러 catalog를 +조립하는 최종 경계는 `clusters/`이며 catalog 디렉터리를 controller root로 +직접 사용하지 않습니다. ### 5. `commonLabels` 금지, `labels:` 사용 @@ -108,15 +113,14 @@ labels: 기존 `commonLabels` 사용 코드는 migration plan을 세워 교체. selector에 이미 들어간 label이 있다면 해당 리소스를 **재배포** (delete + recreate) 없이는 변경 불가. -### 6. selector에는 불변 3종만 +### 6. selector에는 불변 2종만 overlay에서 selector를 건드리지 않는다. selector에 허용되는 label은: - `app.kubernetes.io/name` - `app.kubernetes.io/instance` -- `app.kubernetes.io/component` -이 3종은 base에서 고정. overlay가 `labels:`로 추가하는 label은 반드시 `includeSelectors: false`. +이 2종은 base에서 고정. overlay가 `labels:`로 추가하는 label은 반드시 `includeSelectors: false`. ### 7. `patches:` (v5 스타일) 사용, `patchesStrategicMerge` / `patchesJson6902` 금지 @@ -146,7 +150,7 @@ patches: multiple overlay에서 공통으로 끼워야 하는 변경(예: mTLS 활성화, sidecar 주입, monitoring label 추가)은 component로. ``` -components/ +gitops/apps/auth-server/components/ with-istio-sidecar/ kustomization.yaml # kind: Component patches/ @@ -215,11 +219,15 @@ field manager 이름을 환경별로 통일해야 `managedFields` 충돌이 예 CI가 아래를 순서대로 실행: ```bash -kubectl kustomize overlays/prod > /tmp/rendered.yaml +kubectl kustomize gitops/clusters/prod/kr-main/all > /tmp/rendered.yaml kubeconform -strict -summary -schema-location default -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' /tmp/rendered.yaml -kubectl diff --server-side --field-manager=ci -k overlays/prod +kubectl diff --server-side --field-manager=ci -k gitops/clusters/prod/kr-main/stages/50-apps ``` +`all`은 schema/policy 감사용이고 diff/apply는 의존성이 준비된 개별 stage를 +대상으로 합니다. 여러 stage 변경은 승인된 orchestrator/controller가 순서와 +health gate를 보장해야 합니다. + - kubeconform / kubeval: schema validation - kyverno / OPA Gatekeeper: policy validation (post-render) - conftest: opa policy bundle 실행 @@ -254,62 +262,41 @@ v2.1에서 `bases:`가 `resources:`로 통합됨. 신규 파일에서 `bases:` ## 추천 폴더 구조 ```text -k8s/ - base/ - app/ - kustomization.yaml - units/ - identity/ - auth/ - kustomization.yaml - deployment.yaml - service.yaml - servicemonitor.yaml - pdb.yaml - hpa.yaml - keycloak/ - kustomization.yaml - data/ - postgres-identity/ - kustomization.yaml - statefulset.yaml - service-headless.yaml - service.yaml - managing/ - flyway-migrate-identity/ +gitops/ + apps/ + auth/ + base/ kustomization.yaml - job.yaml - backup-postgres/ - kustomization.yaml - cronjob.yaml - plugins/ - ingress-nginx/ - cert-manager/ - external-secrets/ - kube-prometheus-stack/ - fluent-bit/ - components/ - with-service-monitor/ - with-pdb-tier1/ - with-topology-spread-zone/ - with-network-policy-deny-default/ - overlays/ - dev/ - kustomization.yaml - staging/ - kustomization.yaml - prod/ - kr-main/ - kustomization.yaml - patches/ - kr-dr/ - kustomization.yaml - patches/ - scripts/ - render.sh - diff.sh - apply.sh - validate.sh + deployment.yaml + service.yaml + servicemonitor.yaml + pdb.yaml + hpa.yaml + components/ + with-service-monitor/ + with-pdb-tier1/ + with-topology-spread-zone/ + overlays/{lab,staging,prod}/ + postgres-identity/ + base/ + overlays/{lab,staging,prod}/ + flyway-migrate-identity/ + base/ + overlays/{lab,staging,prod}/ + platform/ + ingress-nginx/ + cert-manager/ + secret-delivery/ + policies/ + network-policy-deny-default/ + clusters/ + lab/main/stages/ + staging/main/stages/ + prod/kr-main/stages/ + prod/kr-dr/stages/ +scripts/ + bin/ + ci/ ``` ## 프로젝트 기준 요약 @@ -317,9 +304,10 @@ k8s/ - Kustomize v5 문법 기준, `commonLabels` 금지, `labels:` 사용 - `patches:` 단일 키, `target:` + `path:` 또는 `patch:` inline - `components:`로 cross-cutting 재사용 -- selector에는 불변 3종만 (name / instance / component) +- selector에는 불변 2종만 (name / instance) - generator는 configMap만 기본, secret은 External Secrets - `kubectl apply --server-side --field-manager=<id>` 전제 - render + schema + policy 검증을 CI에서 강제 -- base / components / overlays 3축 디렉터리 +- catalog unit의 base/components/overlays와 cluster entrypoint를 분리 +- `stages/`만 apply하고 `all/`은 render/schema/policy audit에만 사용 - overlay diff는 짧아야 한다 (base 재작성 금지) diff --git a/docs/standards/infra/minio.md b/docs/standards/infra/minio.md index 22ec63c..6affb4f 100644 --- a/docs/standards/infra/minio.md +++ b/docs/standards/infra/minio.md @@ -88,7 +88,10 @@ 기본: - `spec.configuration.name`에 root credential Secret (MINIO_ROOT_USER, MINIO_ROOT_PASSWORD) - Vault KV에 root credential 저장, VSO로 Secret 동기화 -- 앱용 access는 `mc admin user svcacct add` 로 service account 발급 +- 앱용 access는 `mc admin accesskey create`로 서버 생성 credential을 발급하고 + inline policy로 권한을 축소한다 +- 생성 결과의 secret key는 한 번만 수신해 stdin으로 Vault에 기록하며, + `--secret-key` 또는 `mc admin user add`로 secret을 argv에 전달하지 않는다 - service account는 최소 권한 policy 바인딩 ### 7. TLS는 기본 활성화 diff --git a/docs/standards/infra/operations-runbook-upgrade-rollback.md b/docs/standards/infra/operations-runbook-upgrade-rollback.md index 4878948..f8c08ab 100644 --- a/docs/standards/infra/operations-runbook-upgrade-rollback.md +++ b/docs/standards/infra/operations-runbook-upgrade-rollback.md @@ -126,18 +126,17 @@ Flux 플랫폼에서는 `Kustomization.spec.dependsOn` 으로 순서를 명시 apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: - name: auth-server + name: prod-50-apps namespace: flux-system spec: interval: 5m - path: ./k8s/overlays/prod + path: ./gitops/clusters/prod/main/stages/50-apps prune: true sourceRef: kind: GitRepository name: platform dependsOn: - - name: cert-manager - - name: postgres-operator + - name: prod-40-operations ``` ### 11. node 작업 (drain / cordon) 은 PDB 존중 흐름 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d0ca5c2..89806c4 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,19 +1,23 @@ # 운영 중 만난 함정 9건 — 사건 카탈로그 -K3s 기반 로컬 클러스터에서 Project-Infra 를 부트스트랩 / 운영하면서 실제로 만났던 사건들의 narrative 정리. -운영자 절차서 톤은 [`guide.md`](../guide.md) 의 15장 (`15.1` ~ `15.11`) 에 있고, 이 문서는 사건 단위로 "무엇을 보고 / 왜 그랬고 / 어떻게 풀었는지" 를 짧게 쓰기 위한 자료다. +> 이 문서는 과거 `dev` 단일-aggregate 시기의 incident 기록이다. node 이름, +> host, 인증서, 수동 apply 명령은 당시 상태를 설명하며 현재 실행 runbook이 +> 아니다. 현재 배포와 복구는 `docs/operations.md`와 staged bootstrap을 따른다. -| # | 사건 | guide.md cross-ref | -|---|---|---| -| 1 | Registry image pull 실패 (`ImagePullBackOff`) | (별건 — guide.md 15.1 은 ContainerCreating 사건) | -| 2 | `vault-0` 가 `0/1 Running` 에서 멈춤 | [15.10](../guide.md#1510-vault-0-이-01-running-에서-멈춤) | -| 3 | `helm upgrade` 가 `has no deployed releases` 로 실패 | [15.11](../guide.md#1511-helm-upgrade-가-has-no-deployed-releases-로-실패) | -| 4 | VSO 가 기존 K8s Secret 을 덮어쓰지 않음 | [15.8](../guide.md#158-기존-k8s-secret-이-남아있을-때) | -| 5 | ForwardAuth 로그인 E2E 검증 실패 | (현장 검증 사건) | -| 6 | namespace 가 `Terminating` 에 걸림 | [15.9](../guide.md#159-namespace-가-terminating-에-걸림) | -| 7 | PodSecurity 위반 경고 (admission) | [15.7](../guide.md#157-podsecurity-위반-경고) | -| 8 | VSO 가 Vault 로그인 실패 | [15.2](../guide.md#152-vso-가-vault-에-로그인-실패) | -| 9 | Registry 는 살아났지만 auth-server 새 이미지 pull 이 끝나지 않음 | (해결됨 — registries.yaml 제거 + hosts.toml 직접 작성) | +K3s 기반 로컬 클러스터에서 Project-Infra 를 부트스트랩 / 운영하면서 실제로 +만났던 사건을 "무엇을 보고 / 왜 그랬고 / 어떻게 풀었는지" 순서로 기록한다. + +| # | 사건 | +| --- | --- | +| 1 | Registry image pull 실패 (`ImagePullBackOff`) | +| 2 | `vault-0` 가 `0/1 Running` 에서 멈춤 | +| 3 | `helm upgrade` 가 `has no deployed releases` 로 실패 | +| 4 | VSO 가 기존 K8s Secret 을 덮어쓰지 않음 | +| 5 | ForwardAuth 로그인 E2E 검증 실패 | +| 6 | namespace 가 `Terminating` 에 걸림 | +| 7 | PodSecurity 위반 경고 (admission) | +| 8 | VSO 가 Vault 로그인 실패 | +| 9 | Registry 는 살아났지만 auth-server 새 이미지 pull 이 끝나지 않음 | --- @@ -133,8 +137,8 @@ Git/Kustomize 원천 파일을 수정했다. 변경 파일: -- `k8s/base/plugins/docker-registry/configmap.yaml` -- `k8s/overlays/dev/registry/networkpolicy.yaml` +- `gitops/platform/registry/base/configmap.yaml` +- `gitops/platform/registry/overlays/lab/networkpolicy.yaml` 변경 내용: @@ -156,8 +160,8 @@ ports: 적용: ```bash -kubectl diff -k k8s/overlays/dev/registry -kubectl apply -k k8s/overlays/dev/registry +kubectl diff -k gitops/platform/registry/overlays/lab +kubectl apply -k gitops/platform/registry/overlays/lab kubectl -n mnt rollout restart deployment/docker-registry kubectl -n mnt rollout status deployment/docker-registry --timeout=180s ``` @@ -340,8 +344,8 @@ server = "http://registry.project.com" skip_verify = true [host."http://registry.project.com".auth] - username = "testuser" - password = "abcd6845" + username = "<registry-username>" + password = "<registry-password>" EOF # 3) 적용 @@ -360,7 +364,7 @@ sudo systemctl restart k3s # control-plane ```bash # 노드의 registry 연결 직접 검증 sudo /usr/local/bin/k3s ctr -a /run/k3s/containerd/containerd.sock \ - images pull --plain-http --user 'testuser:abcd6845' \ + images pull --plain-http --user '<registry-username>:<registry-password>' \ registry.project.com/auth-platform/auth-server:manual-20260512071751 # Pod 가 새 이미지로 정상 Running 되는지 @@ -425,7 +429,7 @@ vault-0 0/1 Running 0 5m ### 해결 ```bash -REPO_ROOT="$(pwd)" ENV_NAME=dev bash k8s/scripts/tasks/vault-init.sh +REPO_ROOT="$(pwd)" ENV_NAME=lab bash scripts/tasks/vault-init.sh ``` 이 스크립트는 idempotent — 다음을 차례로 처리한다. @@ -526,7 +530,7 @@ Vault KV 에 새 값을 넣었는데 K8s Secret 은 옛날 값을 유지. `Vault bootstrap 스크립트에 명시적 opt-in 환경변수를 둠. ```bash -RESET_STALE_SECRETS=yes bash k8s/scripts/bin/bootstrap.sh dev +RESET_STALE_SECRETS=yes bash scripts/bin/bootstrap.sh lab ``` 이 옵션이 있을 때만 VSO-managed K8s Secret 후보들을 선제 삭제 → VSO 가 새로 생성. @@ -640,7 +644,7 @@ unknown TLS options: kube-system-modern-tls@kubernetescrd Traefik packaged manifest 를 직접 수정하지 않고, Git source-of-truth 인 overlay 를 적용했다. ```bash -kubectl apply -k k8s/overlays/dev/platform/traefik +kubectl apply -k gitops/platform/traefik/overlays/lab ``` 적용된 리소스: @@ -680,17 +684,17 @@ dev 전용 `ClusterIssuer/dev-selfsigned` 를 추가하고, dev TLS `Certificate 변경 파일: -- `k8s/overlays/dev/platform/cert-manager-issuers/dev-selfsigned-clusterissuer.yaml` -- `k8s/overlays/dev/platform/cert-manager-issuers/kustomization.yaml` -- `k8s/overlays/dev/tls/project-com-certificate.yaml` -- `k8s/overlays/dev/tls/keycloak-dev-certificate.yaml` -- `k8s/overlays/dev/tls/registry-project-com-certificate.yaml` +- `gitops/platform/cert-manager/overlays/lab/issuers/dev-selfsigned-clusterissuer.yaml` +- `gitops/platform/cert-manager/overlays/lab/issuers/kustomization.yaml` +- `gitops/platform/cert-manager/overlays/lab/certificates/project-com-certificate.yaml` +- `gitops/platform/cert-manager/overlays/lab/certificates/keycloak-dev-certificate.yaml` +- `gitops/platform/cert-manager/overlays/lab/certificates/registry-project-com-certificate.yaml` 적용: ```bash -kubectl apply -k k8s/overlays/dev/platform/cert-manager-issuers -kubectl apply -k k8s/overlays/dev/tls +kubectl apply -k gitops/platform/cert-manager/overlays/lab/issuers +kubectl apply -k gitops/platform/cert-manager/overlays/lab/certificates kubectl -n mnt wait --for=condition=Ready certificate/project-com --timeout=120s kubectl -n mnt wait --for=condition=Ready certificate/keycloak-dev-example-com --timeout=120s ``` @@ -846,7 +850,7 @@ set_authorization_header = true 변경 파일: -- `k8s/components/forward-auth/oauth2-proxy-config.yaml` +- `gitops/platform/forward-auth/component/oauth2-proxy-config.yaml` 반영 후 oauth2-proxy 를 재시작했다. @@ -1071,15 +1075,15 @@ spec: type: RuntimeDefault ``` -이 패턴은 [`docs/security-hardening.md`](./security-hardening.md) 의 체크리스트에 정리되어 있고, [`k8s/scripts/ci/validate.sh`](../k8s/scripts/ci/validate.sh) 가 `kube-linter` 로 회귀를 막는다. +이 패턴은 [`docs/security-hardening.md`](./security-hardening.md) 의 체크리스트에 정리되어 있고, [`scripts/ci/validate.sh`](../scripts/ci/validate.sh) 가 `kube-linter` 로 회귀를 막는다. ### 검증 ```bash -bash k8s/scripts/ci/validate.sh +bash scripts/ci/validate.sh # build=ok schema=ok lint=ok -kubectl apply -k k8s/overlays/dev +bash scripts/bin/bootstrap.sh lab # Warning 없음 ``` @@ -1123,10 +1127,10 @@ K8s 인증의 의존 그래프는 두 단: ```bash # 1. ClusterRoleBinding 적용 -kubectl apply -k k8s/overlays/<env>/vault/ +kubectl apply -k gitops/platform/vault/overlays/<env>/ # 2. vault auth/kubernetes/config 설정 (idempotent) -REPO_ROOT="$(pwd)" ENV_NAME=<env> bash k8s/scripts/tasks/vault-init.sh +REPO_ROOT="$(pwd)" ENV_NAME=<env> bash scripts/tasks/vault-init.sh ``` `vault-init.sh` 는 다음을 자동으로 한다: diff --git a/docs/validation-report.md b/docs/validation-report.md index b049dfc..9518ff1 100644 --- a/docs/validation-report.md +++ b/docs/validation-report.md @@ -1,5 +1,9 @@ # Docs validation report +> Historical report for the documentation examples at the timestamp below. +> It is not the current repository gate. Use `make check`; current deployable +> entrypoints are listed in `tests/kustomize-entrypoints.txt`. + Generated: 2026-04-20T08:51:44Z Tools: kubeconform v0.6.7, kube-linter v0.7.4, yq v4.44.3 CRD schemas: datreeio/CRDs-catalog (remote fetch) diff --git a/docs/vault-vso.md b/docs/vault-vso.md index f7e242f..f5c7a9b 100644 --- a/docs/vault-vso.md +++ b/docs/vault-vso.md @@ -1,98 +1,71 @@ -# Vault / VSO 상세 +# Vault and Vault Secrets Operator -README 의 Vault·VSO 핵심 섹션을 보충한다. Kubernetes auth 초기화 명령, policy/role 매핑, VaultStaticSecret 카탈로그, dockerconfigjson `.auth` 이슈를 모은다. +## Ownership -## Vault 기본 정보 +- Vault workload: `gitops/platform/vault` +- VSO Helm values: `gitops/platform/secret-delivery/base/helm/values.yaml` +- VaultConnection/VaultAuth: `gitops/platform/secret-delivery/base` +- lab secret declarations: `gitops/platform/secret-delivery/overlays/lab` -| 항목 | 값 | -|---|---| -| 이미지 | `hashicorp/vault:1.17.2` | -| 배포 | StatefulSet (replicas 1, file backend) | -| 실행 | `vault server -config=/vault/config/vault.hcl` | -| 포트 | 8200 (http) / 8201 (cluster) — 내부 ClusterIP, NodePort 없음 | -| 저장 | PVC 5Gi (dev overlay 1Gi patch) | -| UI 접근 | `kubectl -n mnt port-forward svc/vault 8200:8200` (외부 노출 금지) | +Vault KV-v2가 secret source of truth이며, workload는 VSO가 만든 Kubernetes +Secret만 소비합니다. -## 설계 결정 +## Bootstrap order -- **file backend (학습 환경 전용)**: 단일 노드 + 학습 목적으로 `storage "file"`. HA 불가. prod 승격 시 `storage "raft"` + KMS 기반 auto-unseal 로 전환. -- **`disable_mlock = true`**: 컨테이너에 `IPC_LOCK` capability 를 부여하지 않고 PSS Restricted 프로필을 유지하기 위함. 대신 swap 이 꺼진 노드에서 실행해야 한다. -- **`tls_disable = 1`**: 단일 namespace 내부 통신만 발생하고 cert-manager 전에 부트스트랩이 끝나야 해서 현재는 비활성화. 클러스터 밖 노출 시 cert-manager 발급 인증서로 TLS 활성화 필수. -- **`api_addr: http://vault:8200` + `cluster_addr: http://vault:8201`**: 짧은 Service 이름. 모든 소비자가 같은 `mnt` namespace 에 있어 FQDN 불필요. +```text +VSO controller/CRD install +Vault workload Running +Vault init + unseal +Kubernetes auth + policy/role +KV seed +VaultConnection/VaultAuth/pre-data VaultStaticSecret apply +pre-data destination Secret wait +PostgreSQL/MinIO/Keycloak apply +MinIO registry access key generate -> Vault +registry VaultStaticSecret/apply +consumer workloads apply +``` -## RBAC +이 순서 때문에 `all/` aggregate를 직접 apply할 수 없습니다. -ClusterRoleBinding `vault-tokenreview-binding` ← `system:auth-delegator`. Vault 의 Kubernetes auth method 는 클라이언트(VSO 등)가 제출한 ServiceAccount JWT 를 `TokenReview` + `SubjectAccessReview` API 로 검증한다. 이 ClusterRoleBinding 이 없으면 VSO 로그인이 `permission denied` 로 실패한다. +## Access split -Vault Pod 의 ServiceAccount 는 `automountServiceAccountToken: true` (기본). Vault 는 `/var/run/secrets/kubernetes.io/serviceaccount/{token,ca.crt}` 를 읽어 `auth/kubernetes/config` 의 `token_reviewer_jwt` / `kubernetes_ca_cert` 를 채운다. +| VaultAuth | Policy | Read paths | +| --- | --- | --- | +| `vault-auth-auth-platform` | `vso-auth-platform` | `identity-postgres/*`, `auth-server/*`, `keycloak/*`, `oauth2-proxy/*` | +| `vault-auth-storage` | `vso-storage` | `minio/*`, `docker-registry/*` | -## Kubernetes auth 초기 설정 +현재 lab은 두 auth boundary가 같은 namespace와 ServiceAccount를 공유합니다. +실무 namespace 분리 시 workload/domain별 ServiceAccount, VaultAuth, policy, +bound namespace를 함께 분리해야 합니다. -`tasks/vault-init.sh` 가 수행: +## Secret catalog + +| Domain | Kubernetes Secret | +| --- | --- | +| PostgreSQL | `identity-postgres-superuser`, `keycloak-db`, `auth-server-db` | +| Keycloak | `keycloak-db-operator`, `keycloak-bootstrap-admin-operator`, `keycloak-client-auth-server-ingress` | +| oauth2-proxy | `oauth2-proxy-secrets` | +| MinIO | `minio-tenant-env` | +| registry | `docker-registry-minio`, `docker-registry-basic-auth`, `docker-registry-pull-credentials` | + +VSO destinations use `overwrite: true`. auth-server와 oauth2-proxy는 지원되는 +Secret 변경에 rollout target을 선언합니다. DB/MinIO/registry credential은 +backend 상태를 먼저 바꾸는 조정된 rotation이 필요합니다. + +`docker-registry-minio`는 MinIO가 생성한 access key를 Vault에 기록한 뒤 +`35-registry`에서 동기화합니다. 다른 정적 seed와 같은 시점에 미리 만들지 +않습니다. + +## Key material + +`vault-init-keys.json`에는 root token과 unseal material이 들어가므로 Git에 +들어가면 안 됩니다. 기본 lab 파일도 평문이며 단지 ignored/0600일 뿐입니다. ```bash -vault auth enable kubernetes # idempotent 체크 -vault write auth/kubernetes/config \ - kubernetes_host="https://kubernetes.default.svc.cluster.local:443" \ - kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ - token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token - -vault secrets enable -path=secret kv-v2 # idempotent 체크 - -# policy 2개 (역할별 least-privilege) -vault policy write vso-auth-platform - \ - # identity-postgres/* + auth-server/* + keycloak/* read -vault policy write vso-storage - \ - # minio/* read - -# role 2개 (같은 SA, 다른 policy) -vault write auth/kubernetes/role/vso-auth-platform \ - policies=vso-auth-platform bound_sa=vault-secrets-operator/mnt ttl=1h -vault write auth/kubernetes/role/vso-storage \ - policies=vso-storage bound_sa=vault-secrets-operator/mnt ttl=1h +VAULT_KEYS_FILE=/secure/path/lab-vault-init.json \ + bash scripts/bin/bootstrap.sh lab ``` -## VaultAuth / VaultStaticSecret 매핑 - -policy 분리에 따라 VaultAuth CR 도 2 개. 각 VaultStaticSecret 은 자기 도메인의 VaultAuth 를 참조한다: - -| VaultAuth CR | Vault role | 참조 VaultStaticSecret | -|---|---|---| -| `vault-auth-auth-platform` | `vso-auth-platform` | `identity-postgres-superuser`, `keycloak-db-creds`, `auth-server-db-creds`, `keycloak-bootstrap-admin` | -| `vault-auth-storage` | `vso-storage` | `minio-tenant-env` | - -VSO Operator SA(`vault-secrets-operator`) 는 한 개이지만 Vault 쪽에서 role 별 policy 가 분리되어 있다. auth-platform 토큰이 유출돼도 MinIO secret 은 보호된다. - -## VSO 가 관리하는 Secret 카탈로그 - -Registry 는 auth 없이 운영(NetworkPolicy 로 `mnt` 내부 전용 보호)이라 base 에는 VaultStaticSecret 이 없다. dev overlay 에서 BasicAuth / pull credential 두 개를 추가한다. - -| VaultStaticSecret (dev overlay) | Vault 경로 | K8s Secret | 소비 방식 | -|---|---|---|---| -| `identity-postgres-superuser` | `secret/identity-postgres/superuser` | `identity-postgres-superuser` | file mount (`/run/secrets/superuser/`) → `POSTGRES_USER_FILE`, `POSTGRES_PASSWORD_FILE` | -| `keycloak-db-creds` | `secret/keycloak/db` | `keycloak-db` | file mount — postgres initdb + keycloak `KC_DB_PASSWORD_FILE` | -| `auth-server-db-creds` | `secret/auth-server/db` | `auth-server-db` | file mount — `SPRING_CONFIG_IMPORT=configtree:/etc/secrets/` + Flyway sh wrapper | -| `keycloak-bootstrap-admin` | `secret/keycloak/bootstrap-admin` | `keycloak-bootstrap-admin` | file mount — `KC_BOOTSTRAP_ADMIN_{USERNAME,PASSWORD}_FILE` | -| `minio-tenant-env` | `secret/minio/tenant-env` | `minio-tenant-env` | MinIO Operator `spec.configuration.name` (env file) | -| `docker-registry-basic-auth` (dev) | `secret/docker-registry/basic-auth` | `docker-registry-basic-auth` | Traefik Middleware basicAuth | -| `docker-registry-pull-credentials` (dev) | `secret/docker-registry/pull-cred` | `docker-registry-pull-credentials` | imagePullSecret (`.dockerconfigjson`) | - -모든 VaultStaticSecret 은 `destination.overwrite` 기본값(`false`) 사용. 기존 Secret 이 수동으로 존재하면 VSO 가 덮어쓰지 않는다 (소유권 경합 방지). - -> **Vault 값 교체 후 즉시 반영**: `kubectl -n mnt delete secret <name>` 으로 기존 Secret 을 지우면 VSO 가 다음 reconcile 에 새 값으로 재생성한다. - -`refreshAfter: 1h` — Vault 값 변경 시 1 시간 내 K8s Secret 에 자동 반영. - -## dockerconfigjson `.auth` 필드 - -Docker 공식 config 스키마는 `.auth = base64("<username>:<password>")` 형태다. 기존에는 password 만 base64 하던 버그가 있었고 현재는 다음으로 수정되어 있다: - -``` -{{ printf "%s:%s" username password | b64enc }} -``` - -Docker daemon 이 Registry 에 로그인할 때 이 필드를 디코드하므로 정확한 포맷이 필수. - -## VaultConnection address - -base 는 `http://vault:8200` (짧은 이름) 만 둔다. VSO Operator Pod 가 같은 `mnt` namespace 에 있으면 Kubernetes DNS 가 짧은 이름을 해결한다. 다른 namespace 에서 운영할 때는 overlay 에서 FQDN(`http://vault.mnt.svc.cluster.local:8200`) 으로 patch. +과거 Git 이력에 포함된 값은 삭제만으로 복구되지 않습니다. live credential +회전과 원격 history 정리는 별도 incident response로 수행합니다. diff --git a/gitops/AGENTS.md b/gitops/AGENTS.md new file mode 100644 index 0000000..c70fe10 --- /dev/null +++ b/gitops/AGENTS.md @@ -0,0 +1,36 @@ +# gitops AGENTS + +Role: +- own Kubernetes/K3s source-of-truth manifests and Kustomize composition +- keep catalog bases, environment overlays, cluster entrypoints, and Vault Secrets Operator assets separate +- make render / validate / diff / apply possible from Git without relying on live cluster state + +Scope: +- `apps/`: app-owned workload/data/operation catalogs +- `platform/`: shared services and operator catalogs +- `policies/`: security and governance catalogs +- `tenants/`: namespace/RBAC/quota catalogs +- `clusters/<env>/<cluster>/namespaces` and `stages/`: deployable environment composition +- `clusters/<env>/<cluster>/all/`: render/audit aggregate only + +Read first: +- `/docs/standards/infra/architecture-environments.md` +- `/docs/standards/infra/kustomize.md` +- `/docs/standards/infra/k3s-specific.md` +- `/docs/standards/infra/operations-runbook-upgrade-rollback.md` + +Rules: +- prefer `kubectl kustomize`, `kubectl diff -k`, and `kubectl apply -k` +- apply stage entrypoints only; never apply an environment `all/` aggregate +- do not use server-local manifests as source of truth +- do not place environment differences in a catalog `base/` +- do not place base resource definitions directly in overlays unless the resource is environment-only by design +- do not commit production secret values +- keep scripts as helpers; manifests remain declarative source + +Catalog base contract: +- shared labels, selectors, probes, resources, services, storage and policy shape are allowed +- environment hostnames, cluster contexts, production-only sizing, rollout image + references, deletion overrides and secret values are forbidden +- every base must be reusable by lab, staging and prod overlays +- ownership must remain visible in the catalog unit path diff --git a/gitops/CATALOG.md b/gitops/CATALOG.md new file mode 100644 index 0000000..edf361b --- /dev/null +++ b/gitops/CATALOG.md @@ -0,0 +1,20 @@ +# Project catalog ownership + +| Catalog | Ownership | +| --- | --- | +| `apps/auth-server` | API workload와 lab ingress | +| `apps/identity-postgres` | auth platform 전용 PostgreSQL | +| `apps/auth-migration` | versioned Flyway migration Job | +| `apps/keycloak-realm-import` | versioned realm/client import | +| `platform/forward-auth` | oauth2-proxy와 Traefik ForwardAuth component | +| `platform/keycloak` | Keycloak instance와 ingress | +| `platform/minio` | MinIO Tenant | +| `platform/registry` | Docker registry | +| `platform/vault` | Vault workload | +| `platform/secret-delivery` | VSO auth/connection, Helm values와 VaultStaticSecret | +| `platform/*-operator` | cluster-scoped controller source와 environment patch | +| `policies/baseline` | namespace 보안 baseline | +| `tenants/mnt` | `mnt` namespace 경계 | + +Catalog는 직접 controller root로 지정하지 않습니다. 실제 환경은 +`gitops/clusters/<...>` 아래 entrypoint에서 필요한 catalog만 선택합니다. diff --git a/gitops/PROJECT.md b/gitops/PROJECT.md new file mode 100644 index 0000000..4550e94 --- /dev/null +++ b/gitops/PROJECT.md @@ -0,0 +1,44 @@ +# Project GitOps layout + +Project-specific Kubernetes resources are split into reusable catalogs and +cluster entrypoints. + +```text +gitops/ +├── apps/ +│ ├── auth-server/ +│ ├── identity-postgres/ +│ ├── auth-migration/ +│ └── keycloak-realm-import/ +├── platform/ +│ ├── cert-manager/ +│ ├── forward-auth/ +│ ├── keycloak/ +│ ├── keycloak-operator/ +│ ├── minio/ +│ ├── registry/ +│ ├── secret-delivery/ +│ ├── traefik/ +│ └── vault/ +├── policies/baseline/ +├── tenants/mnt/ +└── clusters/lab/main/ + ├── stages/ + └── all/ +``` + +Rules: + +- Catalog `base`는 환경 중립 shape를 소유하고 `overlays/<env>`는 차이만 + 표현합니다. +- `clusters/lab/main/stages/*`만 현재 deployable entrypoint입니다. +- `clusters/lab/main/all`은 전체 렌더와 정적 검사 전용이며 apply하지 않습니다. +- operator readiness, Vault init, Secret 생성과 Job 완료는 stage 사이에서 + `scripts/bin/bootstrap.sh`가 확인합니다. +- 새 deployable entrypoint는 `tests/kustomize-entrypoints.txt`에 추가합니다. + +```bash +kubectl kustomize gitops/clusters/lab/main/all >/tmp/project-infra-lab.yaml +export KUBE_CONTEXT_LAB='<expected-context>' +bash scripts/bin/bootstrap.sh lab +``` diff --git a/gitops/README.md b/gitops/README.md new file mode 100644 index 0000000..9e41d5f --- /dev/null +++ b/gitops/README.md @@ -0,0 +1,33 @@ +# GitOps Desired State + +Kubernetes API 안에서 지속적으로 reconcile할 desired state를 관리합니다. +GitOps controller를 사용하지 않는 초기 단계에도 `kubectl kustomize`로 같은 +entrypoint를 렌더할 수 있습니다. + +```text +platform ─┐ +policies ─┼──▶ clusters/<...> ◀── GitOps controller root +tenants ─┤ +apps ─┘ +``` + +- `clusters`: 클러스터별 최종 조립점 +- `platform`: cluster-wide addon과 controller +- `policies`: cluster-wide admission과 거버넌스 규칙 +- `tenants`: 구체적인 namespace/RBAC/quota/NetworkPolicy +- `apps`: application 배포 정의 + +Catalog 디렉터리를 controller root로 직접 지정하지 않습니다. cluster entrypoint가 +필요한 base/overlay를 선택하고 의존 순서를 명시합니다. + +## 기본 규칙 + +- `base`는 환경을 모르며 재사용 가능한 기본값만 가집니다. +- `overlays`는 차이만 patch하고 전체 manifest를 복사하지 않습니다. +- CRD/controller가 필요한 리소스는 controller 이후에 reconcile합니다. +- resource namespace, ownership label과 버전을 명시합니다. +- raw `Secret` 또는 실제 비밀값을 커밋하지 않습니다. +- 원격 base/chart를 참조할 때 immutable version 또는 digest를 사용합니다. + +Flux/Argo CD 고유 리소스와 sync ordering은 선택한 controller를 기록한 ADR에 +문서화합니다. diff --git a/gitops/apps/AGENTS.md b/gitops/apps/AGENTS.md new file mode 100644 index 0000000..de29652 --- /dev/null +++ b/gitops/apps/AGENTS.md @@ -0,0 +1,6 @@ +# gitops/apps AGENTS + +Application-owned workloads, stateful dependencies, and independently runnable +operations live here. Keep environment-neutral manifests in each unit's `base`. +Ingress hosts, image rollout references, environment labels, storage classes, +retention, and Secret delivery differences belong in `overlays/<env>`. diff --git a/gitops/apps/README.md b/gitops/apps/README.md new file mode 100644 index 0000000..955aa01 --- /dev/null +++ b/gitops/apps/README.md @@ -0,0 +1,18 @@ +# Application Deployment Catalog + +애플리케이션의 source code가 아니라 Kubernetes 배포 정의를 둡니다. app 팀이 +별도 source/deploy 저장소를 소유하면 이곳에는 immutable artifact를 참조하는 +GitOps 리소스만 둘 수 있습니다. + +```text +apps/ +└── example-api/ + ├── base/ + └── overlays/ + ├── dev/ + └── prod/ +``` + +base는 환경을 모르고, overlay에는 replica/resource/config처럼 필요한 차이만 +둡니다. image는 mutable tag 대신 조직 정책에 따른 고정 tag 또는 digest를 +사용합니다. diff --git a/gitops/apps/_template/README.md b/gitops/apps/_template/README.md new file mode 100644 index 0000000..83e889e --- /dev/null +++ b/gitops/apps/_template/README.md @@ -0,0 +1,22 @@ +# __REPLACE_ME_APPLICATION_NAME__ + +## 소유자 + +Team: __REPLACE_ME_OWNER__ + +repository와 on-call 정보를 적습니다. + +## 배포 계약 + +- Namespace: +- Image/artifact source: +- Ports/protocol: +- Dependency: +- SLO/alerts: + +## 구성 + +- `base`: 공통 Kubernetes 배포 정의 +- `overlays`: 환경별 replica, resource, config 차이 + +비밀은 ExternalSecret 같은 참조 또는 승인된 암호화 형식으로만 추가합니다. diff --git a/gitops/apps/_template/base/README.md b/gitops/apps/_template/base/README.md new file mode 100644 index 0000000..8bcecaa --- /dev/null +++ b/gitops/apps/_template/base/README.md @@ -0,0 +1,4 @@ +# Base + +환경을 모르는 application의 공통 manifest를 둡니다. namespace 자체의 소유권이 +tenant catalog에 있다면 이곳에서 중복 생성하지 않습니다. diff --git a/k8s/base/managing/kustomization.yaml b/gitops/apps/_template/base/kustomization.yaml old mode 100755 new mode 100644 similarity index 72% rename from k8s/base/managing/kustomization.yaml rename to gitops/apps/_template/base/kustomization.yaml index 7413ead..e609fb2 --- a/k8s/base/managing/kustomization.yaml +++ b/gitops/apps/_template/base/kustomization.yaml @@ -1,5 +1,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -resources: - - namespace +resources: [] diff --git a/gitops/apps/_template/overlays/README.md b/gitops/apps/_template/overlays/README.md new file mode 100644 index 0000000..6c3d7fa --- /dev/null +++ b/gitops/apps/_template/overlays/README.md @@ -0,0 +1,6 @@ +# Overlays + +필요한 환경에만 overlay를 추가합니다. 각 overlay는 `../../base`를 참조하고 +환경별 patch만 포함합니다. + +비밀값, 임시 debug 설정과 수동 hotfix 결과를 overlay에 커밋하지 않습니다. diff --git a/gitops/apps/auth-migration/AGENTS.md b/gitops/apps/auth-migration/AGENTS.md new file mode 100644 index 0000000..63c1654 --- /dev/null +++ b/gitops/apps/auth-migration/AGENTS.md @@ -0,0 +1,5 @@ +# gitops/apps/auth-migration AGENTS + +Jobs and CronJobs live here. Operations are versioned, independently runnable +units and must not be hidden inside an application base. Define completion, +retry, timeout, rollback, and rerun semantics before changing them. diff --git a/k8s/base/managing/migration-flyway/configmap-sql.yaml b/gitops/apps/auth-migration/base/configmap-sql.yaml similarity index 95% rename from k8s/base/managing/migration-flyway/configmap-sql.yaml rename to gitops/apps/auth-migration/base/configmap-sql.yaml index 4a24e5f..e7ae366 100644 --- a/k8s/base/managing/migration-flyway/configmap-sql.yaml +++ b/gitops/apps/auth-migration/base/configmap-sql.yaml @@ -1,10 +1,10 @@ apiVersion: v1 kind: ConfigMap metadata: - name: migration-flyway-sql + name: auth-server-migrate-sql labels: - app.kubernetes.io/name: migration-flyway - app.kubernetes.io/instance: migration-flyway + app.kubernetes.io/name: auth-server + app.kubernetes.io/instance: auth-server-migrate-0-1-0 app.kubernetes.io/version: "0.1.0" app.kubernetes.io/component: migration app.kubernetes.io/part-of: auth-platform diff --git a/k8s/base/managing/migration-flyway/job.yaml b/gitops/apps/auth-migration/base/job.yaml similarity index 91% rename from k8s/base/managing/migration-flyway/job.yaml rename to gitops/apps/auth-migration/base/job.yaml index 34ba8d6..a4f5910 100644 --- a/k8s/base/managing/migration-flyway/job.yaml +++ b/gitops/apps/auth-migration/base/job.yaml @@ -1,10 +1,10 @@ apiVersion: batch/v1 kind: Job metadata: - name: migration-flyway + name: auth-server-migrate-0-1-0 labels: - app.kubernetes.io/name: migration-flyway - app.kubernetes.io/instance: migration-flyway + app.kubernetes.io/name: auth-server + app.kubernetes.io/instance: auth-server-migrate-0-1-0 app.kubernetes.io/version: "0.1.0" app.kubernetes.io/component: migration app.kubernetes.io/part-of: auth-platform @@ -18,14 +18,14 @@ spec: template: metadata: labels: - app.kubernetes.io/name: migration-flyway - app.kubernetes.io/instance: migration-flyway + app.kubernetes.io/name: auth-server + app.kubernetes.io/instance: auth-server-migrate-0-1-0 app.kubernetes.io/version: "0.1.0" app.kubernetes.io/component: migration app.kubernetes.io/part-of: auth-platform app.kubernetes.io/managed-by: kustomize spec: - serviceAccountName: migration-flyway-sa + serviceAccountName: auth-server-migrate-sa automountServiceAccountToken: false restartPolicy: Never securityContext: @@ -108,7 +108,7 @@ spec: volumes: - name: sql configMap: - name: migration-flyway-sql + name: auth-server-migrate-sql - name: db-secret secret: secretName: auth-server-db diff --git a/k8s/base/managing/migration-flyway/kustomization.yaml b/gitops/apps/auth-migration/base/kustomization.yaml similarity index 79% rename from k8s/base/managing/migration-flyway/kustomization.yaml rename to gitops/apps/auth-migration/base/kustomization.yaml index 2ee969e..b2089a5 100644 --- a/k8s/base/managing/migration-flyway/kustomization.yaml +++ b/gitops/apps/auth-migration/base/kustomization.yaml @@ -6,8 +6,8 @@ resources: - job.yaml labels: - pairs: - app.kubernetes.io/name: migration-flyway - app.kubernetes.io/instance: migration-flyway + app.kubernetes.io/name: auth-server + app.kubernetes.io/instance: auth-server-migrate-0-1-0 app.kubernetes.io/version: "0.1.0" app.kubernetes.io/component: migration app.kubernetes.io/part-of: auth-platform diff --git a/k8s/base/managing/migration-flyway/serviceaccount.yaml b/gitops/apps/auth-migration/base/serviceaccount.yaml similarity index 67% rename from k8s/base/managing/migration-flyway/serviceaccount.yaml rename to gitops/apps/auth-migration/base/serviceaccount.yaml index 015f254..5919f0f 100644 --- a/k8s/base/managing/migration-flyway/serviceaccount.yaml +++ b/gitops/apps/auth-migration/base/serviceaccount.yaml @@ -1,10 +1,10 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: migration-flyway-sa + name: auth-server-migrate-sa labels: - app.kubernetes.io/name: migration-flyway - app.kubernetes.io/instance: migration-flyway + app.kubernetes.io/name: auth-server + app.kubernetes.io/instance: auth-server-migrate-0-1-0 app.kubernetes.io/version: "0.1.0" app.kubernetes.io/component: migration app.kubernetes.io/part-of: auth-platform diff --git a/k8s/overlays/dev/registry/kustomization.yaml b/gitops/apps/auth-migration/overlays/lab/kustomization.yaml similarity index 51% rename from k8s/overlays/dev/registry/kustomization.yaml rename to gitops/apps/auth-migration/overlays/lab/kustomization.yaml index 76ac1f6..72fa894 100644 --- a/k8s/overlays/dev/registry/kustomization.yaml +++ b/gitops/apps/auth-migration/overlays/lab/kustomization.yaml @@ -4,8 +4,5 @@ kind: Kustomization namespace: mnt resources: - - ../../../base/plugins/docker-registry - - vault-secrets.yaml - - middleware.yaml - - ingress-public.yaml + - ../../base - networkpolicy.yaml diff --git a/gitops/apps/auth-migration/overlays/lab/networkpolicy.yaml b/gitops/apps/auth-migration/overlays/lab/networkpolicy.yaml new file mode 100644 index 0000000..fcddad9 --- /dev/null +++ b/gitops/apps/auth-migration/overlays/lab/networkpolicy.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: auth-server-migrate-egress-postgres + labels: + app.kubernetes.io/name: auth-server + app.kubernetes.io/instance: auth-server-migrate-0-1-0 + app.kubernetes.io/component: migration + app.kubernetes.io/part-of: auth-platform + app.kubernetes.io/managed-by: kustomize +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: auth-server + app.kubernetes.io/instance: auth-server-migrate-0-1-0 + policyTypes: + - Egress + egress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: identity-postgres + app.kubernetes.io/instance: identity-postgres + ports: + - protocol: TCP + port: 5432 diff --git a/k8s/base/app/identity/auth/stateless/auth-server/configmap.yaml b/gitops/apps/auth-server/base/configmap.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateless/auth-server/configmap.yaml rename to gitops/apps/auth-server/base/configmap.yaml diff --git a/k8s/base/app/identity/auth/stateless/auth-server/deployment.yaml b/gitops/apps/auth-server/base/deployment.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateless/auth-server/deployment.yaml rename to gitops/apps/auth-server/base/deployment.yaml diff --git a/k8s/base/app/identity/auth/stateless/auth-server/kustomization.yaml b/gitops/apps/auth-server/base/kustomization.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateless/auth-server/kustomization.yaml rename to gitops/apps/auth-server/base/kustomization.yaml diff --git a/k8s/base/app/identity/auth/stateless/auth-server/service.yaml b/gitops/apps/auth-server/base/service.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateless/auth-server/service.yaml rename to gitops/apps/auth-server/base/service.yaml diff --git a/k8s/base/app/identity/auth/stateless/auth-server/serviceaccount.yaml b/gitops/apps/auth-server/base/serviceaccount.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateless/auth-server/serviceaccount.yaml rename to gitops/apps/auth-server/base/serviceaccount.yaml diff --git a/k8s/overlays/dev/auth/ingress.yaml b/gitops/apps/auth-server/overlays/lab/ingress.yaml similarity index 100% rename from k8s/overlays/dev/auth/ingress.yaml rename to gitops/apps/auth-server/overlays/lab/ingress.yaml diff --git a/k8s/overlays/dev/auth/kustomization.yaml b/gitops/apps/auth-server/overlays/lab/kustomization.yaml similarity index 77% rename from k8s/overlays/dev/auth/kustomization.yaml rename to gitops/apps/auth-server/overlays/lab/kustomization.yaml index f82fe9b..d41955b 100644 --- a/k8s/overlays/dev/auth/kustomization.yaml +++ b/gitops/apps/auth-server/overlays/lab/kustomization.yaml @@ -4,8 +4,7 @@ kind: Kustomization namespace: mnt resources: - - ../../../base/app/identity/auth/stateless/auth-server - - ../../../base/managing/migration-flyway + - ../../base - ingress.yaml - networkpolicy.yaml @@ -39,12 +38,3 @@ patches: path: /imagePullSecrets value: - name: docker-registry-pull-credentials - - target: - kind: Job - name: migration-flyway - patch: |- - - op: add - path: /metadata/annotations - value: - argocd.argoproj.io/sync-wave: "-1" - argocd.argoproj.io/hook: PreSync diff --git a/k8s/overlays/dev/auth/networkpolicy.yaml b/gitops/apps/auth-server/overlays/lab/networkpolicy.yaml similarity index 69% rename from k8s/overlays/dev/auth/networkpolicy.yaml rename to gitops/apps/auth-server/overlays/lab/networkpolicy.yaml index 70e89cf..0d73764 100644 --- a/k8s/overlays/dev/auth/networkpolicy.yaml +++ b/gitops/apps/auth-server/overlays/lab/networkpolicy.yaml @@ -61,30 +61,3 @@ spec: ports: - protocol: TCP port: 8080 ---- -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: migration-flyway-egress - labels: - app.kubernetes.io/name: migration-flyway - app.kubernetes.io/instance: migration-flyway - app.kubernetes.io/component: migration - app.kubernetes.io/part-of: auth-platform - app.kubernetes.io/managed-by: kustomize -spec: - podSelector: - matchLabels: - app.kubernetes.io/name: migration-flyway - app.kubernetes.io/instance: migration-flyway - policyTypes: - - Egress - egress: - - to: - - podSelector: - matchLabels: - app.kubernetes.io/name: identity-postgres - app.kubernetes.io/instance: identity-postgres - ports: - - protocol: TCP - port: 5432 diff --git a/gitops/apps/identity-postgres/AGENTS.md b/gitops/apps/identity-postgres/AGENTS.md new file mode 100644 index 0000000..6b213c5 --- /dev/null +++ b/gitops/apps/identity-postgres/AGENTS.md @@ -0,0 +1,5 @@ +# gitops/apps/identity-postgres AGENTS + +Stateful data services live here. Read the storage, backup, security, and +component standard before changing a StatefulSet, Tenant, PVC, or retention +policy. Environment storage classes and deletion behavior belong in overlays. diff --git a/k8s/base/app/identity/auth/stateful/identity-postgres/configmap-initdb.yaml b/gitops/apps/identity-postgres/base/configmap-initdb.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateful/identity-postgres/configmap-initdb.yaml rename to gitops/apps/identity-postgres/base/configmap-initdb.yaml diff --git a/k8s/base/app/identity/auth/stateful/identity-postgres/kustomization.yaml b/gitops/apps/identity-postgres/base/kustomization.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateful/identity-postgres/kustomization.yaml rename to gitops/apps/identity-postgres/base/kustomization.yaml diff --git a/k8s/base/app/identity/auth/stateful/identity-postgres/service-headless.yaml b/gitops/apps/identity-postgres/base/service-headless.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateful/identity-postgres/service-headless.yaml rename to gitops/apps/identity-postgres/base/service-headless.yaml diff --git a/k8s/base/app/identity/auth/stateful/identity-postgres/service.yaml b/gitops/apps/identity-postgres/base/service.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateful/identity-postgres/service.yaml rename to gitops/apps/identity-postgres/base/service.yaml diff --git a/k8s/base/app/identity/auth/stateful/identity-postgres/serviceaccount.yaml b/gitops/apps/identity-postgres/base/serviceaccount.yaml similarity index 100% rename from k8s/base/app/identity/auth/stateful/identity-postgres/serviceaccount.yaml rename to gitops/apps/identity-postgres/base/serviceaccount.yaml diff --git a/k8s/base/app/identity/auth/stateful/identity-postgres/statefulset.yaml b/gitops/apps/identity-postgres/base/statefulset.yaml similarity index 99% rename from k8s/base/app/identity/auth/stateful/identity-postgres/statefulset.yaml rename to gitops/apps/identity-postgres/base/statefulset.yaml index fd7c54f..c2983a3 100644 --- a/k8s/base/app/identity/auth/stateful/identity-postgres/statefulset.yaml +++ b/gitops/apps/identity-postgres/base/statefulset.yaml @@ -167,7 +167,7 @@ spec: spec: accessModes: - ReadWriteOnce - storageClassName: local-path + storageClassName: replace-in-overlay volumeMode: Filesystem resources: requests: diff --git a/k8s/overlays/dev/database/kustomization.yaml b/gitops/apps/identity-postgres/overlays/lab/kustomization.yaml similarity index 77% rename from k8s/overlays/dev/database/kustomization.yaml rename to gitops/apps/identity-postgres/overlays/lab/kustomization.yaml index 9d7eaf6..6594b26 100644 --- a/k8s/overlays/dev/database/kustomization.yaml +++ b/gitops/apps/identity-postgres/overlays/lab/kustomization.yaml @@ -4,8 +4,7 @@ kind: Kustomization namespace: mnt resources: - - ../../../base/app/identity/auth/stateful/identity-postgres - - vault-secrets.yaml + - ../../base - networkpolicy.yaml patches: @@ -19,3 +18,6 @@ patches: - op: replace path: /spec/persistentVolumeClaimRetentionPolicy/whenScaled value: Delete + - op: replace + path: /spec/volumeClaimTemplates/0/spec/storageClassName + value: local-path diff --git a/k8s/overlays/dev/database/networkpolicy.yaml b/gitops/apps/identity-postgres/overlays/lab/networkpolicy.yaml similarity index 88% rename from k8s/overlays/dev/database/networkpolicy.yaml rename to gitops/apps/identity-postgres/overlays/lab/networkpolicy.yaml index 74c693f..5212fc1 100644 --- a/k8s/overlays/dev/database/networkpolicy.yaml +++ b/gitops/apps/identity-postgres/overlays/lab/networkpolicy.yaml @@ -27,8 +27,8 @@ spec: app.kubernetes.io/instance: auth-server - podSelector: matchLabels: - app.kubernetes.io/name: migration-flyway - app.kubernetes.io/instance: migration-flyway + app.kubernetes.io/name: auth-server + app.kubernetes.io/instance: auth-server-migrate-0-1-0 ports: - protocol: TCP port: 5432 diff --git a/k8s/overlays/dev/keycloak-realm/kustomization.yaml b/gitops/apps/keycloak-realm-import/overlays/lab/kustomization.yaml similarity index 100% rename from k8s/overlays/dev/keycloak-realm/kustomization.yaml rename to gitops/apps/keycloak-realm-import/overlays/lab/kustomization.yaml diff --git a/k8s/overlays/dev/keycloak-realm/platform-realm-import.yaml b/gitops/apps/keycloak-realm-import/overlays/lab/platform-realm-import.yaml similarity index 98% rename from k8s/overlays/dev/keycloak-realm/platform-realm-import.yaml rename to gitops/apps/keycloak-realm-import/overlays/lab/platform-realm-import.yaml index de2f5b3..8693ec1 100644 --- a/k8s/overlays/dev/keycloak-realm/platform-realm-import.yaml +++ b/gitops/apps/keycloak-realm-import/overlays/lab/platform-realm-import.yaml @@ -1,7 +1,7 @@ apiVersion: k8s.keycloak.org/v2beta1 kind: KeycloakRealmImport metadata: - name: platform-realm + name: platform-realm-v1 labels: app.kubernetes.io/name: keycloak app.kubernetes.io/instance: keycloak diff --git a/k8s/overlays/AGENTS.md b/gitops/clusters/AGENTS.md similarity index 79% rename from k8s/overlays/AGENTS.md rename to gitops/clusters/AGENTS.md index 0dd5b67..d598954 100644 --- a/k8s/overlays/AGENTS.md +++ b/gitops/clusters/AGENTS.md @@ -1,4 +1,4 @@ -# k8s/overlays AGENTS +# gitops/clusters AGENTS Role: - own environment-specific Kustomize composition @@ -6,9 +6,8 @@ Role: - make environment-level render, diff, apply, audit, and GitOps sync straightforward Scope: -- `dev/` -- `staging/` -- `prod/` +- `<environment>/<region-or-cluster>/stages/` +- `<environment>/<region-or-cluster>/all/` Allowed: - namespace selection @@ -37,8 +36,10 @@ Read first: - `/docs/standards/infra/operations-runbook-upgrade-rollback.md` Rules: -- overlays are environment-first by design +- cluster entrypoints are environment-first by design +- `stages/*` are deployable entrypoints; `all/` is validation-only +- controller readiness and one-shot completion belong between stage applies - keep patches small and named by target and intent - render and diff the target environment before apply - prod overlays must be the most conservative environment -- if an overlay starts re-declaring most of a resource, move common shape back into `k8s/base` +- if an overlay starts re-declaring most of a resource, move common shape back into its catalog `base` diff --git a/gitops/clusters/README.md b/gitops/clusters/README.md new file mode 100644 index 0000000..841b530 --- /dev/null +++ b/gitops/clusters/README.md @@ -0,0 +1,37 @@ +# Cluster Entrypoints + +클러스터별 최종 desired state 진입점입니다. GitOps controller의 root는 이 +디렉터리 아래 **하나의 실제 cluster leaf**만 가리킵니다. + +소규모: + +```text +clusters/ +└── dev/ + └── main/ + └── kustomization.yaml +``` + +다중 환경·리전: + +```text +clusters/ +├── dev/ap-northeast-2/dev-a/ +├── staging/ap-northeast-2/staging-a/ +└── prod/ + ├── ap-northeast-2/prod-a/ + └── ap-southeast-1/prod-b/ +``` + +cluster leaf에는 catalog 구현을 복사하지 않고 선택 목록과 cluster 고유 patch만 +둡니다. 계정 또는 조직 경계가 필요하면 경로 segment를 추가할 수 있지만 자동화가 +고정 depth에 의존하지 않도록 합니다. + +`_template`을 실제 경로로 복사해 시작합니다. + +## Current lab exception + +`lab/main`은 CRD, Vault, Secret과 one-shot operation의 readiness 경계 때문에 +단일 direct leaf 대신 `namespaces`와 ordered `stages/*` leaf를 사용합니다. +현재는 `scripts/bin/bootstrap.sh`가 순서를 보장합니다. `all/`은 감사 +aggregate이므로 controller root로 지정하거나 apply하지 않습니다. diff --git a/gitops/clusters/_template/README.md b/gitops/clusters/_template/README.md new file mode 100644 index 0000000..88b7c59 --- /dev/null +++ b/gitops/clusters/_template/README.md @@ -0,0 +1,31 @@ +# __REPLACE_ME_CLUSTER_NAME__ + +## 대상 + +- Environment: __REPLACE_ME_ENVIRONMENT__ +- Region: __REPLACE_ME_REGION__ +- Cluster: __REPLACE_ME_CLUSTER_NAME__ +- Owner: __REPLACE_ME_OWNER__ + +## 구성 + +`kustomization.yaml`의 `resources`에 필요한 platform, policy, tenant와 app의 +base 또는 overlay를 추가합니다. 예: + +```yaml +resources: + - ../../../platform/core/base + - ../../../policies/baseline/base + - ../../../apps/example-api/overlays/prod +``` + +위 예시는 `clusters/dev/main` 경로를 기준으로 합니다. 실제 상대 경로는 cluster +leaf 깊이에 맞게 조정합니다. + +## 규칙 + +- 이 디렉터리가 해당 클러스터의 유일한 root입니다. +- 공통 manifest를 복사하지 않습니다. +- 클러스터 고유 차이만 local patch로 둡니다. +- dependency/sync 순서와 장애 시 reconcile 중지 절차를 문서화합니다. +- `_template` 자체를 GitOps controller에 연결하지 않습니다. diff --git a/gitops/clusters/_template/kustomization.yaml b/gitops/clusters/_template/kustomization.yaml new file mode 100644 index 0000000..d7587e5 --- /dev/null +++ b/gitops/clusters/_template/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Add reusable platform, policy, tenant and app paths after copying this template. +resources: [] diff --git a/gitops/clusters/lab/main/README.md b/gitops/clusters/lab/main/README.md new file mode 100644 index 0000000..df3e65b --- /dev/null +++ b/gitops/clusters/lab/main/README.md @@ -0,0 +1,22 @@ +# lab / main cluster entrypoints + +현재 `lab`은 readiness 경계를 가진 push-based staged deployment를 사용합니다. + +```text +namespaces +stages/ +├── 00-platform +├── 10-vault +├── 20-secrets +├── 30-data +├── 35-registry +├── 40-operations +└── 50-apps +``` + +- `namespaces`와 각 stage는 독립적으로 렌더 가능한 cluster 배포 entrypoint입니다. +- `scripts/bin/bootstrap.sh lab`만 준비 상태를 확인하며 순차 적용합니다. +- `all/`은 모든 stage를 합친 감사용 Kustomization입니다. apply하거나 GitOps + controller root로 연결하지 않습니다. +- Argo CD를 채택하면 stage별 Application/sync-wave/health gate를, Flux를 + 채택하면 Kustomization `dependsOn`/`healthChecks`를 먼저 설계한 뒤 전환합니다. diff --git a/gitops/clusters/lab/main/all/kustomization.yaml b/gitops/clusters/lab/main/all/kustomization.yaml new file mode 100644 index 0000000..e08642c --- /dev/null +++ b/gitops/clusters/lab/main/all/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Audit/render entrypoint only. Do not apply this aggregate: controllers and +# one-shot operations have readiness boundaries that Kustomize cannot express. +resources: + - ../stages/00-platform + - ../stages/10-vault + - ../stages/20-secrets + - ../stages/30-data + - ../stages/35-registry + - ../stages/40-operations + - ../stages/50-apps diff --git a/gitops/clusters/lab/main/namespaces/kustomization.yaml b/gitops/clusters/lab/main/namespaces/kustomization.yaml new file mode 100644 index 0000000..03b0433 --- /dev/null +++ b/gitops/clusters/lab/main/namespaces/kustomization.yaml @@ -0,0 +1,15 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Namespace creation is a separate cluster entrypoint because server-side +# dry-run cannot create a Namespace and validate namespaced resources against +# it in the same request. +resources: + - ../../../../tenants/mnt/base + +labels: + - pairs: + example.com/environment: lab + example.com/owner-team: platform + includeSelectors: false + includeTemplates: true diff --git a/gitops/clusters/lab/main/stages/00-platform/kustomization.yaml b/gitops/clusters/lab/main/stages/00-platform/kustomization.yaml new file mode 100644 index 0000000..e958e15 --- /dev/null +++ b/gitops/clusters/lab/main/stages/00-platform/kustomization.yaml @@ -0,0 +1,18 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Cluster prerequisites and the deny-by-default policy are applied before any +# workload. This stage is intentionally free of application CRs. +resources: + - ../../../../../tenants/mnt/base + - ../../../../../platform/cert-manager/overlays/lab + - ../../../../../platform/keycloak-operator/overlays/lab + - ../../../../../platform/traefik/overlays/lab + - ../../../../../policies/baseline/overlays/lab + +labels: + - pairs: + example.com/environment: lab + example.com/owner-team: platform + includeSelectors: false + includeTemplates: true diff --git a/gitops/clusters/lab/main/stages/10-vault/kustomization.yaml b/gitops/clusters/lab/main/stages/10-vault/kustomization.yaml new file mode 100644 index 0000000..d97e7db --- /dev/null +++ b/gitops/clusters/lab/main/stages/10-vault/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../../../platform/vault/overlays/lab + +labels: + - pairs: + example.com/environment: lab + example.com/owner-team: platform + includeSelectors: false + includeTemplates: true diff --git a/gitops/clusters/lab/main/stages/20-secrets/kustomization.yaml b/gitops/clusters/lab/main/stages/20-secrets/kustomization.yaml new file mode 100644 index 0000000..e88fe14 --- /dev/null +++ b/gitops/clusters/lab/main/stages/20-secrets/kustomization.yaml @@ -0,0 +1,16 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# The VSO controller is installed by the bootstrap task before this stage. +resources: + - ../../../../../platform/secret-delivery/overlays/lab/operator + - ../../../../../platform/secret-delivery/overlays/lab/identity + - ../../../../../platform/secret-delivery/overlays/lab/storage + - ../../../../../platform/secret-delivery/overlays/lab/apps + +labels: + - pairs: + example.com/environment: lab + example.com/owner-team: platform + includeSelectors: false + includeTemplates: true diff --git a/gitops/clusters/lab/main/stages/30-data/kustomization.yaml b/gitops/clusters/lab/main/stages/30-data/kustomization.yaml new file mode 100644 index 0000000..f0b8238 --- /dev/null +++ b/gitops/clusters/lab/main/stages/30-data/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../../../apps/identity-postgres/overlays/lab + - ../../../../../platform/minio/overlays/lab + - ../../../../../platform/keycloak/overlays/lab + +labels: + - pairs: + example.com/environment: lab + example.com/owner-team: platform + includeSelectors: false + includeTemplates: true diff --git a/gitops/clusters/lab/main/stages/35-registry/kustomization.yaml b/gitops/clusters/lab/main/stages/35-registry/kustomization.yaml new file mode 100644 index 0000000..12a727e --- /dev/null +++ b/gitops/clusters/lab/main/stages/35-registry/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../../../platform/secret-delivery/overlays/lab/storage/registry-minio + - ../../../../../platform/registry/overlays/lab + +labels: + - pairs: + example.com/environment: lab + example.com/owner-team: platform + includeSelectors: false + includeTemplates: true diff --git a/gitops/clusters/lab/main/stages/40-operations/kustomization.yaml b/gitops/clusters/lab/main/stages/40-operations/kustomization.yaml new file mode 100644 index 0000000..6673779 --- /dev/null +++ b/gitops/clusters/lab/main/stages/40-operations/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# One-shot resources. bootstrap.sh waits for their prerequisites before apply. +resources: + - ../../../../../apps/auth-migration/overlays/lab + - ../../../../../apps/keycloak-realm-import/overlays/lab + +labels: + - pairs: + example.com/environment: lab + example.com/owner-team: platform + includeSelectors: false + includeTemplates: true diff --git a/gitops/clusters/lab/main/stages/50-apps/kustomization.yaml b/gitops/clusters/lab/main/stages/50-apps/kustomization.yaml new file mode 100644 index 0000000..05236b9 --- /dev/null +++ b/gitops/clusters/lab/main/stages/50-apps/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../../../apps/auth-server/overlays/lab + - ../../../../../platform/forward-auth/overlays/lab + +labels: + - pairs: + example.com/environment: lab + example.com/owner-team: platform + includeSelectors: false + includeTemplates: true diff --git a/gitops/platform/AGENTS.md b/gitops/platform/AGENTS.md new file mode 100644 index 0000000..6335c28 --- /dev/null +++ b/gitops/platform/AGENTS.md @@ -0,0 +1,6 @@ +# gitops/platform AGENTS + +Shared platform services and environment-neutral operator sources live in +unit-level `base` directories. Cluster-specific CIDRs, namespaces, ingress, +service mesh, load balancer, and K3s packaged-component configuration belong +in focused overlays selected by a cluster stage. diff --git a/gitops/platform/README.md b/gitops/platform/README.md new file mode 100644 index 0000000..f44dd19 --- /dev/null +++ b/gitops/platform/README.md @@ -0,0 +1,23 @@ +# Platform Catalog + +클러스터 전체에서 사용하는 addon과 controller의 재사용 구성을 둡니다. + +예: + +```text +platform/ +├── ingress/ +├── certificates/ +├── external-dns/ +├── external-secrets/ +├── storage/ +├── autoscaling/ +└── observability/ +``` + +실제로 사용하는 항목만 만듭니다. 각 component는 자신의 CRD, controller, +configuration과 values/patch를 응집해서 관리합니다. component 간 숨은 의존을 +만들지 말고 cluster root 또는 선택한 GitOps controller의 ordering 기능으로 +순서를 표현합니다. + +새 component는 `_template`을 복사해 시작합니다. diff --git a/gitops/platform/_template/README.md b/gitops/platform/_template/README.md new file mode 100644 index 0000000..fffdb28 --- /dev/null +++ b/gitops/platform/_template/README.md @@ -0,0 +1,21 @@ +# __REPLACE_ME_PLATFORM_COMPONENT_NAME__ + +## 책임 + +이 component가 소유하는 CRD, controller와 configuration을 적습니다. + +## 의존성 + +선행 component, namespace, identity와 최소 Kubernetes version을 적습니다. + +## 구성 + +- `base`: 모든 대상에서 공유하는 기본값 +- `overlays`: 환경/규모별 차이가 실제로 있을 때만 추가 + +Helm을 사용하면 chart source/version, values와 release 리소스를 이 component +안에 함께 둡니다. 원격 version은 고정합니다. + +## 운영 + +upgrade 순서, health check, rollback과 uninstall 영향을 runbook으로 연결합니다. diff --git a/gitops/platform/_template/base/README.md b/gitops/platform/_template/base/README.md new file mode 100644 index 0000000..961d33d --- /dev/null +++ b/gitops/platform/_template/base/README.md @@ -0,0 +1,7 @@ +# Base + +환경과 클러스터를 모르는 재사용 가능한 기본 manifest만 둡니다. + +- 실제 domain, account ID, credential을 하드코딩하지 않습니다. +- resource request/limit와 security context의 안전한 기본값을 둡니다. +- 환경 차이는 `overlays`에서 patch합니다. diff --git a/gitops/platform/_template/base/kustomization.yaml b/gitops/platform/_template/base/kustomization.yaml new file mode 100644 index 0000000..e609fb2 --- /dev/null +++ b/gitops/platform/_template/base/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: [] diff --git a/gitops/platform/_template/overlays/README.md b/gitops/platform/_template/overlays/README.md new file mode 100644 index 0000000..dce243e --- /dev/null +++ b/gitops/platform/_template/overlays/README.md @@ -0,0 +1,7 @@ +# Overlays + +공통 base와 다른 값이 있을 때만 `<environment-or-profile>/` overlay를 만듭니다. +overlay는 base를 참조하고 patch만 포함해야 합니다. + +`dev`, `prod` 전체 manifest 복사보다 replica, resource, retention처럼 실제로 +달라지는 항목만 표현합니다. diff --git a/k8s/overlays/dev/platform/cert-manager/kustomization.yaml b/gitops/platform/cert-manager/base/kustomization.yaml similarity index 100% rename from k8s/overlays/dev/platform/cert-manager/kustomization.yaml rename to gitops/platform/cert-manager/base/kustomization.yaml diff --git a/gitops/platform/cert-manager/overlays/lab/kustomization.yaml b/gitops/platform/cert-manager/overlays/lab/kustomization.yaml new file mode 100644 index 0000000..c180c8b --- /dev/null +++ b/gitops/platform/cert-manager/overlays/lab/kustomization.yaml @@ -0,0 +1,95 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../base + +labels: + - pairs: + app.kubernetes.io/part-of: certificate-platform + app.kubernetes.io/managed-by: kustomize + includeSelectors: false + includeTemplates: true + +patches: + - target: + kind: Deployment + name: cert-manager + patch: |- + - op: add + path: /spec/revisionHistoryLimit + value: 5 + - op: add + path: /spec/progressDeadlineSeconds + value: 600 + - op: add + path: /spec/template/spec/securityContext/runAsUser + value: 1000 + - op: add + path: /spec/template/spec/securityContext/runAsGroup + value: 1000 + - op: add + path: /spec/template/spec/securityContext/fsGroup + value: 1000 + - op: add + path: /spec/template/spec/containers/0/resources + value: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 256Mi + - target: + kind: Deployment + name: cert-manager-cainjector + patch: |- + - op: add + path: /spec/revisionHistoryLimit + value: 5 + - op: add + path: /spec/progressDeadlineSeconds + value: 600 + - op: add + path: /spec/template/spec/securityContext/runAsUser + value: 1000 + - op: add + path: /spec/template/spec/securityContext/runAsGroup + value: 1000 + - op: add + path: /spec/template/spec/securityContext/fsGroup + value: 1000 + - op: add + path: /spec/template/spec/containers/0/resources + value: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 256Mi + - target: + kind: Deployment + name: cert-manager-webhook + patch: |- + - op: add + path: /spec/revisionHistoryLimit + value: 5 + - op: add + path: /spec/progressDeadlineSeconds + value: 600 + - op: add + path: /spec/template/spec/securityContext/runAsUser + value: 1000 + - op: add + path: /spec/template/spec/securityContext/runAsGroup + value: 1000 + - op: add + path: /spec/template/spec/securityContext/fsGroup + value: 1000 + - op: add + path: /spec/template/spec/containers/0/resources + value: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 128Mi diff --git a/k8s/base/plugins/oauth2-proxy/deployment.yaml b/gitops/platform/forward-auth/base/deployment.yaml similarity index 100% rename from k8s/base/plugins/oauth2-proxy/deployment.yaml rename to gitops/platform/forward-auth/base/deployment.yaml diff --git a/k8s/base/plugins/oauth2-proxy/kustomization.yaml b/gitops/platform/forward-auth/base/kustomization.yaml similarity index 100% rename from k8s/base/plugins/oauth2-proxy/kustomization.yaml rename to gitops/platform/forward-auth/base/kustomization.yaml diff --git a/k8s/base/plugins/oauth2-proxy/service.yaml b/gitops/platform/forward-auth/base/service.yaml similarity index 100% rename from k8s/base/plugins/oauth2-proxy/service.yaml rename to gitops/platform/forward-auth/base/service.yaml diff --git a/k8s/base/plugins/oauth2-proxy/serviceaccount.yaml b/gitops/platform/forward-auth/base/serviceaccount.yaml similarity index 100% rename from k8s/base/plugins/oauth2-proxy/serviceaccount.yaml rename to gitops/platform/forward-auth/base/serviceaccount.yaml diff --git a/k8s/components/forward-auth/kustomization.yaml b/gitops/platform/forward-auth/component/kustomization.yaml similarity index 87% rename from k8s/components/forward-auth/kustomization.yaml rename to gitops/platform/forward-auth/component/kustomization.yaml index 27afc41..81e1683 100644 --- a/k8s/components/forward-auth/kustomization.yaml +++ b/gitops/platform/forward-auth/component/kustomization.yaml @@ -2,9 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1alpha1 kind: Component resources: - - ../../base/plugins/oauth2-proxy + - ../base - oauth2-proxy-config.yaml - - oauth2-proxy-vault-secrets.yaml - oauth2-proxy-ingress.yaml - oauth2-proxy-middleware.yaml - oauth2-proxy-networkpolicy.yaml diff --git a/k8s/components/forward-auth/oauth2-proxy-config.yaml b/gitops/platform/forward-auth/component/oauth2-proxy-config.yaml similarity index 100% rename from k8s/components/forward-auth/oauth2-proxy-config.yaml rename to gitops/platform/forward-auth/component/oauth2-proxy-config.yaml diff --git a/k8s/components/forward-auth/oauth2-proxy-ingress.yaml b/gitops/platform/forward-auth/component/oauth2-proxy-ingress.yaml similarity index 100% rename from k8s/components/forward-auth/oauth2-proxy-ingress.yaml rename to gitops/platform/forward-auth/component/oauth2-proxy-ingress.yaml diff --git a/k8s/components/forward-auth/oauth2-proxy-middleware.yaml b/gitops/platform/forward-auth/component/oauth2-proxy-middleware.yaml similarity index 100% rename from k8s/components/forward-auth/oauth2-proxy-middleware.yaml rename to gitops/platform/forward-auth/component/oauth2-proxy-middleware.yaml diff --git a/k8s/components/forward-auth/oauth2-proxy-networkpolicy.yaml b/gitops/platform/forward-auth/component/oauth2-proxy-networkpolicy.yaml similarity index 100% rename from k8s/components/forward-auth/oauth2-proxy-networkpolicy.yaml rename to gitops/platform/forward-auth/component/oauth2-proxy-networkpolicy.yaml diff --git a/gitops/platform/forward-auth/overlays/lab/kustomization.yaml b/gitops/platform/forward-auth/overlays/lab/kustomization.yaml new file mode 100644 index 0000000..cda52d9 --- /dev/null +++ b/gitops/platform/forward-auth/overlays/lab/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: mnt + +components: + - ../../component diff --git a/gitops/platform/keycloak-operator/base/kustomization.yaml b/gitops/platform/keycloak-operator/base/kustomization.yaml new file mode 100644 index 0000000..c64d2a7 --- /dev/null +++ b/gitops/platform/keycloak-operator/base/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.1/kubernetes/keycloaks.k8s.keycloak.org-v1.yml + - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.1/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml + - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.1/kubernetes/kubernetes.yml + diff --git a/k8s/overlays/dev/platform/keycloak-operator/kustomization.yaml b/gitops/platform/keycloak-operator/overlays/lab/kustomization.yaml similarity index 50% rename from k8s/overlays/dev/platform/keycloak-operator/kustomization.yaml rename to gitops/platform/keycloak-operator/overlays/lab/kustomization.yaml index 40f3471..af33388 100644 --- a/k8s/overlays/dev/platform/keycloak-operator/kustomization.yaml +++ b/gitops/platform/keycloak-operator/overlays/lab/kustomization.yaml @@ -4,11 +4,17 @@ kind: Kustomization namespace: mnt resources: - - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.1/kubernetes/keycloaks.k8s.keycloak.org-v1.yml - - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.1/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml - - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.1/kubernetes/kubernetes.yml + - ../../base - networkpolicy.yaml +labels: + - pairs: + app.kubernetes.io/instance: keycloak-operator + app.kubernetes.io/component: identity-operator + app.kubernetes.io/part-of: auth-platform + includeSelectors: false + includeTemplates: true + patches: - target: kind: ClusterRoleBinding @@ -21,19 +27,41 @@ patches: kind: Deployment name: keycloak-operator patch: |- + - op: add + path: /spec/revisionHistoryLimit + value: 5 + - op: add + path: /spec/progressDeadlineSeconds + value: 600 - op: add path: /spec/template/spec/securityContext value: runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch seccompProfile: type: RuntimeDefault - op: add path: /spec/template/spec/containers/0/securityContext value: allowPrivilegeEscalation: false + readOnlyRootFilesystem: true capabilities: drop: - ALL + - op: add + path: /spec/template/spec/containers/0/volumeMounts + value: + - name: tmp + mountPath: /tmp + - op: add + path: /spec/template/spec/volumes + value: + - name: tmp + emptyDir: + sizeLimit: 128Mi - op: replace path: /spec/template/spec/containers/0/startupProbe/failureThreshold value: 18 diff --git a/k8s/overlays/dev/platform/keycloak-operator/networkpolicy.yaml b/gitops/platform/keycloak-operator/overlays/lab/networkpolicy.yaml similarity index 100% rename from k8s/overlays/dev/platform/keycloak-operator/networkpolicy.yaml rename to gitops/platform/keycloak-operator/overlays/lab/networkpolicy.yaml diff --git a/k8s/base/app/identity/keycloak/stateless/keycloak/keycloak.yaml b/gitops/platform/keycloak/base/keycloak.yaml similarity index 98% rename from k8s/base/app/identity/keycloak/stateless/keycloak/keycloak.yaml rename to gitops/platform/keycloak/base/keycloak.yaml index ad7e405..6653d68 100644 --- a/k8s/base/app/identity/keycloak/stateless/keycloak/keycloak.yaml +++ b/gitops/platform/keycloak/base/keycloak.yaml @@ -43,7 +43,7 @@ spec: app.kubernetes.io/part-of: auth-platform app.kubernetes.io/managed-by: keycloak-operator proxy: - headers: xforwarded # X-Forwarded-For X-Forwarded-Proto X-Forwarded-Host X-Forwarded-Port 해당 헤더들을 사용하겠다는 의미 + headers: xforwarded # X-Forwarded-For X-Forwarded-Proto X-Forwarded-Host X-Forwarded-Port 해당 헤더들을 사용하겠다는 의미 ingress: enabled: false networkPolicy: @@ -78,7 +78,7 @@ spec: matchLabels: app.kubernetes.io/instance: keycloak app.kubernetes.io/managed-by: keycloak-operator - affinity: # 특정 노드에 다른 pod에 관계에 따라 배치 위치를 결정하는 규칙을 정의한다 + affinity: # 특정 노드에 다른 pod에 관계에 따라 배치 위치를 결정하는 규칙을 정의한다 podAntiAffinity: # 특정 조건을 만족하는 포드와는 같은 장소에 있지 않는다. preferredDuringSchedulingIgnoredDuringExecution: # 가급적 지켜줘 - weight: 100 # 가중치가 100임 매우 중요하다고 알림 diff --git a/k8s/base/app/identity/keycloak/stateless/keycloak/kustomization.yaml b/gitops/platform/keycloak/base/kustomization.yaml similarity index 100% rename from k8s/base/app/identity/keycloak/stateless/keycloak/kustomization.yaml rename to gitops/platform/keycloak/base/kustomization.yaml diff --git a/k8s/overlays/dev/keycloak/ingress-admin.yaml b/gitops/platform/keycloak/overlays/lab/ingress-admin.yaml similarity index 100% rename from k8s/overlays/dev/keycloak/ingress-admin.yaml rename to gitops/platform/keycloak/overlays/lab/ingress-admin.yaml diff --git a/k8s/overlays/dev/keycloak/ingress-public.yaml b/gitops/platform/keycloak/overlays/lab/ingress-public.yaml similarity index 100% rename from k8s/overlays/dev/keycloak/ingress-public.yaml rename to gitops/platform/keycloak/overlays/lab/ingress-public.yaml diff --git a/k8s/overlays/dev/keycloak/keycloak-hostname-patch.yaml b/gitops/platform/keycloak/overlays/lab/keycloak-hostname-patch.yaml similarity index 100% rename from k8s/overlays/dev/keycloak/keycloak-hostname-patch.yaml rename to gitops/platform/keycloak/overlays/lab/keycloak-hostname-patch.yaml diff --git a/k8s/overlays/dev/keycloak/kustomization.yaml b/gitops/platform/keycloak/overlays/lab/kustomization.yaml similarity index 77% rename from k8s/overlays/dev/keycloak/kustomization.yaml rename to gitops/platform/keycloak/overlays/lab/kustomization.yaml index 0d6b97a..331f39d 100644 --- a/k8s/overlays/dev/keycloak/kustomization.yaml +++ b/gitops/platform/keycloak/overlays/lab/kustomization.yaml @@ -4,8 +4,7 @@ kind: Kustomization namespace: mnt resources: - - ../../../base/app/identity/keycloak/stateless/keycloak - - vault-secrets.yaml + - ../../base - middleware.yaml - ingress-public.yaml - ingress-admin.yaml diff --git a/k8s/overlays/dev/keycloak/middleware.yaml b/gitops/platform/keycloak/overlays/lab/middleware.yaml similarity index 100% rename from k8s/overlays/dev/keycloak/middleware.yaml rename to gitops/platform/keycloak/overlays/lab/middleware.yaml diff --git a/k8s/overlays/dev/keycloak/networkpolicy.yaml b/gitops/platform/keycloak/overlays/lab/networkpolicy.yaml similarity index 100% rename from k8s/overlays/dev/keycloak/networkpolicy.yaml rename to gitops/platform/keycloak/overlays/lab/networkpolicy.yaml diff --git a/k8s/base/app/storage/minio/stateful/minio/kustomization.yaml b/gitops/platform/minio/base/kustomization.yaml similarity index 100% rename from k8s/base/app/storage/minio/stateful/minio/kustomization.yaml rename to gitops/platform/minio/base/kustomization.yaml diff --git a/k8s/base/app/storage/minio/stateful/minio/tenant.yaml b/gitops/platform/minio/base/tenant.yaml similarity index 96% rename from k8s/base/app/storage/minio/stateful/minio/tenant.yaml rename to gitops/platform/minio/base/tenant.yaml index e4fd2fa..1a7f5b5 100644 --- a/k8s/base/app/storage/minio/stateful/minio/tenant.yaml +++ b/gitops/platform/minio/base/tenant.yaml @@ -26,7 +26,7 @@ spec: pools: - name: pool-0 servers: 4 - volumesPerServer: 1 + volumesPerServer: 4 volumeClaimTemplate: metadata: name: data @@ -36,7 +36,7 @@ spec: resources: requests: storage: 20Gi - storageClassName: local-path + storageClassName: replace-in-overlay resources: requests: cpu: 250m diff --git a/k8s/overlays/dev/storage/kustomization.yaml b/gitops/platform/minio/overlays/lab/kustomization.yaml similarity index 70% rename from k8s/overlays/dev/storage/kustomization.yaml rename to gitops/platform/minio/overlays/lab/kustomization.yaml index 317200e..f6a1bbb 100644 --- a/k8s/overlays/dev/storage/kustomization.yaml +++ b/gitops/platform/minio/overlays/lab/kustomization.yaml @@ -4,8 +4,7 @@ kind: Kustomization namespace: mnt resources: - - ../../../base/app/storage/minio/stateful/minio - - vault-secrets.yaml + - ../../base - networkpolicy.yaml patches: @@ -25,3 +24,9 @@ patches: - op: replace path: /spec/requestAutoCert value: false + - op: replace + path: /spec/pools/0/volumesPerServer + value: 1 + - op: replace + path: /spec/pools/0/volumeClaimTemplate/spec/storageClassName + value: local-path diff --git a/k8s/overlays/dev/storage/networkpolicy.yaml b/gitops/platform/minio/overlays/lab/networkpolicy.yaml similarity index 100% rename from k8s/overlays/dev/storage/networkpolicy.yaml rename to gitops/platform/minio/overlays/lab/networkpolicy.yaml diff --git a/k8s/base/plugins/docker-registry/configmap.yaml b/gitops/platform/registry/base/configmap.yaml similarity index 88% rename from k8s/base/plugins/docker-registry/configmap.yaml rename to gitops/platform/registry/base/configmap.yaml index 31aed24..9a2f9f6 100755 --- a/k8s/base/plugins/docker-registry/configmap.yaml +++ b/gitops/platform/registry/base/configmap.yaml @@ -7,7 +7,7 @@ data: REGISTRY_LOG_FORMATTER: "json" REGISTRY_STORAGE: "s3" REGISTRY_STORAGE_S3_REGION: "us-east-1" - REGISTRY_STORAGE_S3_REGIONENDPOINT: "http://minio" + REGISTRY_STORAGE_S3_REGIONENDPOINT: "https://minio" REGISTRY_STORAGE_S3_BUCKET: "docker-registry" REGISTRY_STORAGE_S3_FORCEPATHSTYLE: "true" REGISTRY_STORAGE_S3_SKIPVERIFY: "true" diff --git a/k8s/base/plugins/docker-registry/deployment.yaml b/gitops/platform/registry/base/deployment.yaml similarity index 100% rename from k8s/base/plugins/docker-registry/deployment.yaml rename to gitops/platform/registry/base/deployment.yaml diff --git a/k8s/base/plugins/docker-registry/kustomization.yaml b/gitops/platform/registry/base/kustomization.yaml similarity index 100% rename from k8s/base/plugins/docker-registry/kustomization.yaml rename to gitops/platform/registry/base/kustomization.yaml diff --git a/k8s/base/plugins/docker-registry/service.yaml b/gitops/platform/registry/base/service.yaml similarity index 100% rename from k8s/base/plugins/docker-registry/service.yaml rename to gitops/platform/registry/base/service.yaml diff --git a/k8s/base/plugins/docker-registry/serviceaccount.yaml b/gitops/platform/registry/base/serviceaccount.yaml similarity index 100% rename from k8s/base/plugins/docker-registry/serviceaccount.yaml rename to gitops/platform/registry/base/serviceaccount.yaml diff --git a/k8s/overlays/dev/registry/ingress-public.yaml b/gitops/platform/registry/overlays/lab/ingress-public.yaml similarity index 100% rename from k8s/overlays/dev/registry/ingress-public.yaml rename to gitops/platform/registry/overlays/lab/ingress-public.yaml diff --git a/gitops/platform/registry/overlays/lab/kustomization.yaml b/gitops/platform/registry/overlays/lab/kustomization.yaml new file mode 100644 index 0000000..27ad9ef --- /dev/null +++ b/gitops/platform/registry/overlays/lab/kustomization.yaml @@ -0,0 +1,22 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: mnt + +resources: + - ../../base + - middleware.yaml + - ingress-public.yaml + - networkpolicy.yaml + +patches: + - target: + kind: ConfigMap + name: docker-registry-config + patch: |- + - op: replace + path: /data/REGISTRY_STORAGE_S3_REGIONENDPOINT + value: http://minio + - op: replace + path: /data/REGISTRY_STORAGE_S3_SKIPVERIFY + value: "false" diff --git a/k8s/overlays/dev/registry/middleware.yaml b/gitops/platform/registry/overlays/lab/middleware.yaml similarity index 100% rename from k8s/overlays/dev/registry/middleware.yaml rename to gitops/platform/registry/overlays/lab/middleware.yaml diff --git a/k8s/overlays/dev/registry/networkpolicy.yaml b/gitops/platform/registry/overlays/lab/networkpolicy.yaml similarity index 100% rename from k8s/overlays/dev/registry/networkpolicy.yaml rename to gitops/platform/registry/overlays/lab/networkpolicy.yaml diff --git a/k8s/base/plugins/vso/helm/values.yaml b/gitops/platform/secret-delivery/base/helm/values.yaml similarity index 81% rename from k8s/base/plugins/vso/helm/values.yaml rename to gitops/platform/secret-delivery/base/helm/values.yaml index 0877e9c..b5394ef 100755 --- a/k8s/base/plugins/vso/helm/values.yaml +++ b/gitops/platform/secret-delivery/base/helm/values.yaml @@ -2,8 +2,7 @@ defaultVaultConnection: enabled: false controller: - # VSO 는 mnt namespace 에 설치되고 mnt 는 PSS restricted enforce 상태이므로 - # chart 의 Operator Pod 도 Restricted 프로필을 만족해야 한다. + # VSO 는 전용 namespace 에 설치하지만 같은 restricted baseline 을 강제한다. podSecurityContext: runAsNonRoot: true seccompProfile: diff --git a/k8s/base/plugins/vso/kustomization.yaml b/gitops/platform/secret-delivery/base/kustomization.yaml similarity index 100% rename from k8s/base/plugins/vso/kustomization.yaml rename to gitops/platform/secret-delivery/base/kustomization.yaml diff --git a/k8s/base/plugins/vso/serviceaccount.yaml b/gitops/platform/secret-delivery/base/serviceaccount.yaml similarity index 100% rename from k8s/base/plugins/vso/serviceaccount.yaml rename to gitops/platform/secret-delivery/base/serviceaccount.yaml diff --git a/k8s/base/plugins/vso/vault-auth.yaml b/gitops/platform/secret-delivery/base/vault-auth.yaml similarity index 100% rename from k8s/base/plugins/vso/vault-auth.yaml rename to gitops/platform/secret-delivery/base/vault-auth.yaml diff --git a/k8s/base/plugins/vso/vault-connection.yaml b/gitops/platform/secret-delivery/base/vault-connection.yaml similarity index 100% rename from k8s/base/plugins/vso/vault-connection.yaml rename to gitops/platform/secret-delivery/base/vault-connection.yaml diff --git a/k8s/overlays/dev/vso/kustomization.yaml b/gitops/platform/secret-delivery/overlays/lab/apps/kustomization.yaml similarity index 75% rename from k8s/overlays/dev/vso/kustomization.yaml rename to gitops/platform/secret-delivery/overlays/lab/apps/kustomization.yaml index bb2f91c..d8a2f85 100644 --- a/k8s/overlays/dev/vso/kustomization.yaml +++ b/gitops/platform/secret-delivery/overlays/lab/apps/kustomization.yaml @@ -4,4 +4,5 @@ kind: Kustomization namespace: mnt resources: - - ../../../base/plugins/vso + - oauth2-proxy.yaml + diff --git a/k8s/components/forward-auth/oauth2-proxy-vault-secrets.yaml b/gitops/platform/secret-delivery/overlays/lab/apps/oauth2-proxy.yaml similarity index 90% rename from k8s/components/forward-auth/oauth2-proxy-vault-secrets.yaml rename to gitops/platform/secret-delivery/overlays/lab/apps/oauth2-proxy.yaml index 159d749..07e9156 100644 --- a/k8s/components/forward-auth/oauth2-proxy-vault-secrets.yaml +++ b/gitops/platform/secret-delivery/overlays/lab/apps/oauth2-proxy.yaml @@ -11,6 +11,7 @@ spec: destination: name: oauth2-proxy-secrets create: true + overwrite: true labels: app.kubernetes.io/name: oauth2-proxy app.kubernetes.io/component: auth-proxy @@ -22,3 +23,6 @@ spec: text: '{{ if index .Secrets "client_secret" }}{{ index .Secrets "client_secret" }}{{ else }}{{ index .Secrets "client-secret" }}{{ end }}' cookie-secret: text: '{{ if index .Secrets "cookie_secret" }}{{ index .Secrets "cookie_secret" }}{{ else }}{{ index .Secrets "cookie-secret" }}{{ end }}' + rolloutRestartTargets: + - kind: Deployment + name: oauth2-proxy diff --git a/k8s/overlays/dev/keycloak/vault-secrets.yaml b/gitops/platform/secret-delivery/overlays/lab/identity/keycloak.yaml similarity index 96% rename from k8s/overlays/dev/keycloak/vault-secrets.yaml rename to gitops/platform/secret-delivery/overlays/lab/identity/keycloak.yaml index daa536f..2ad7fc3 100644 --- a/k8s/overlays/dev/keycloak/vault-secrets.yaml +++ b/gitops/platform/secret-delivery/overlays/lab/identity/keycloak.yaml @@ -11,6 +11,7 @@ spec: destination: name: keycloak-bootstrap-admin create: true + overwrite: true labels: app.kubernetes.io/name: keycloak app.kubernetes.io/component: identity-provider @@ -29,6 +30,7 @@ spec: destination: name: keycloak-bootstrap-admin-operator create: true + overwrite: true labels: app.kubernetes.io/name: keycloak app.kubernetes.io/component: identity-provider @@ -54,6 +56,7 @@ spec: destination: name: keycloak-db-operator create: true + overwrite: true labels: app.kubernetes.io/name: keycloak app.kubernetes.io/component: database @@ -79,6 +82,7 @@ spec: destination: name: keycloak-client-auth-server-ingress create: true + overwrite: true labels: app.kubernetes.io/name: keycloak app.kubernetes.io/component: identity-provider diff --git a/gitops/platform/secret-delivery/overlays/lab/identity/kustomization.yaml b/gitops/platform/secret-delivery/overlays/lab/identity/kustomization.yaml new file mode 100644 index 0000000..a7eeb0e --- /dev/null +++ b/gitops/platform/secret-delivery/overlays/lab/identity/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: mnt + +resources: + - postgres.yaml + - keycloak.yaml + diff --git a/k8s/overlays/dev/database/vault-secrets.yaml b/gitops/platform/secret-delivery/overlays/lab/identity/postgres.yaml similarity index 93% rename from k8s/overlays/dev/database/vault-secrets.yaml rename to gitops/platform/secret-delivery/overlays/lab/identity/postgres.yaml index f8f032d..907346f 100644 --- a/k8s/overlays/dev/database/vault-secrets.yaml +++ b/gitops/platform/secret-delivery/overlays/lab/identity/postgres.yaml @@ -11,6 +11,7 @@ spec: destination: name: identity-postgres-superuser create: true + overwrite: true labels: app.kubernetes.io/name: identity-postgres app.kubernetes.io/component: database @@ -29,6 +30,7 @@ spec: destination: name: keycloak-db create: true + overwrite: true labels: app.kubernetes.io/name: keycloak app.kubernetes.io/component: database @@ -47,6 +49,7 @@ spec: destination: name: auth-server-db create: true + overwrite: true labels: app.kubernetes.io/name: auth-server app.kubernetes.io/component: database @@ -61,3 +64,6 @@ spec: text: '{{ index .Secrets "SPRING_DATASOURCE_USERNAME" }}' APP_DATASOURCE_PASSWORD: text: '{{ index .Secrets "SPRING_DATASOURCE_PASSWORD" }}' + rolloutRestartTargets: + - kind: Deployment + name: auth-server diff --git a/gitops/platform/secret-delivery/overlays/lab/operator/kustomization.yaml b/gitops/platform/secret-delivery/overlays/lab/operator/kustomization.yaml new file mode 100644 index 0000000..5007b8a --- /dev/null +++ b/gitops/platform/secret-delivery/overlays/lab/operator/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: mnt + +resources: + - ../../../base diff --git a/gitops/platform/secret-delivery/overlays/lab/storage/kustomization.yaml b/gitops/platform/secret-delivery/overlays/lab/storage/kustomization.yaml new file mode 100644 index 0000000..471fc4a --- /dev/null +++ b/gitops/platform/secret-delivery/overlays/lab/storage/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: mnt + +resources: + - minio.yaml + - registry-basic-auth.yaml diff --git a/k8s/overlays/dev/storage/vault-secrets.yaml b/gitops/platform/secret-delivery/overlays/lab/storage/minio.yaml similarity index 95% rename from k8s/overlays/dev/storage/vault-secrets.yaml rename to gitops/platform/secret-delivery/overlays/lab/storage/minio.yaml index e41ed63..cd81710 100644 --- a/k8s/overlays/dev/storage/vault-secrets.yaml +++ b/gitops/platform/secret-delivery/overlays/lab/storage/minio.yaml @@ -11,6 +11,7 @@ spec: destination: name: minio-tenant-env create: true + overwrite: true labels: app.kubernetes.io/name: minio app.kubernetes.io/component: object-storage diff --git a/k8s/overlays/dev/registry/vault-secrets.yaml b/gitops/platform/secret-delivery/overlays/lab/storage/registry-basic-auth.yaml similarity index 70% rename from k8s/overlays/dev/registry/vault-secrets.yaml rename to gitops/platform/secret-delivery/overlays/lab/storage/registry-basic-auth.yaml index 52d5ab0..bf4a81f 100644 --- a/k8s/overlays/dev/registry/vault-secrets.yaml +++ b/gitops/platform/secret-delivery/overlays/lab/storage/registry-basic-auth.yaml @@ -1,30 +1,5 @@ apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultStaticSecret -metadata: - name: docker-registry-minio -spec: - vaultAuthRef: vault-auth-storage - mount: secret - type: kv-v2 - path: docker-registry/minio - refreshAfter: 60s - destination: - name: docker-registry-minio - create: true - labels: - app.kubernetes.io/name: docker-registry - app.kubernetes.io/component: registry - app.kubernetes.io/managed-by: vault-secrets-operator - transformation: - excludeRaw: true - templates: - REGISTRY_STORAGE_S3_ACCESSKEY: - text: '{{ .Secrets.access_key }}' - REGISTRY_STORAGE_S3_SECRETKEY: - text: '{{ .Secrets.secret_key }}' ---- -apiVersion: secrets.hashicorp.com/v1beta1 -kind: VaultStaticSecret metadata: name: docker-registry-basic-auth spec: @@ -62,6 +37,7 @@ spec: destination: name: docker-registry-pull-credentials create: true + overwrite: true type: kubernetes.io/dockerconfigjson labels: app.kubernetes.io/name: docker-registry diff --git a/k8s/base/plugins/kustomization.yaml b/gitops/platform/secret-delivery/overlays/lab/storage/registry-minio/kustomization.yaml similarity index 70% rename from k8s/base/plugins/kustomization.yaml rename to gitops/platform/secret-delivery/overlays/lab/storage/registry-minio/kustomization.yaml index 041fdae..fd6ae6f 100644 --- a/k8s/base/plugins/kustomization.yaml +++ b/gitops/platform/secret-delivery/overlays/lab/storage/registry-minio/kustomization.yaml @@ -1,6 +1,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization +namespace: mnt + resources: - - vault - - docker-registry + - secret.yaml diff --git a/gitops/platform/secret-delivery/overlays/lab/storage/registry-minio/secret.yaml b/gitops/platform/secret-delivery/overlays/lab/storage/registry-minio/secret.yaml new file mode 100644 index 0000000..dee892c --- /dev/null +++ b/gitops/platform/secret-delivery/overlays/lab/storage/registry-minio/secret.yaml @@ -0,0 +1,25 @@ +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: docker-registry-minio +spec: + vaultAuthRef: vault-auth-storage + mount: secret + type: kv-v2 + path: docker-registry/minio + refreshAfter: 60s + destination: + name: docker-registry-minio + create: true + overwrite: true + labels: + app.kubernetes.io/name: docker-registry + app.kubernetes.io/component: registry + app.kubernetes.io/managed-by: vault-secrets-operator + transformation: + excludeRaw: true + templates: + REGISTRY_STORAGE_S3_ACCESSKEY: + text: '{{ .Secrets.access_key }}' + REGISTRY_STORAGE_S3_SECRETKEY: + text: '{{ .Secrets.secret_key }}' diff --git a/k8s/overlays/dev/platform/traefik/dashboard-service.yaml b/gitops/platform/traefik/overlays/lab/dashboard-service.yaml similarity index 100% rename from k8s/overlays/dev/platform/traefik/dashboard-service.yaml rename to gitops/platform/traefik/overlays/lab/dashboard-service.yaml diff --git a/k8s/overlays/dev/platform/traefik/helmchartconfig.yaml b/gitops/platform/traefik/overlays/lab/helmchartconfig.yaml similarity index 100% rename from k8s/overlays/dev/platform/traefik/helmchartconfig.yaml rename to gitops/platform/traefik/overlays/lab/helmchartconfig.yaml diff --git a/k8s/overlays/dev/platform/traefik/kustomization.yaml b/gitops/platform/traefik/overlays/lab/kustomization.yaml similarity index 100% rename from k8s/overlays/dev/platform/traefik/kustomization.yaml rename to gitops/platform/traefik/overlays/lab/kustomization.yaml diff --git a/k8s/overlays/dev/platform/traefik/middleware.yaml b/gitops/platform/traefik/overlays/lab/middleware.yaml similarity index 100% rename from k8s/overlays/dev/platform/traefik/middleware.yaml rename to gitops/platform/traefik/overlays/lab/middleware.yaml diff --git a/k8s/base/plugins/vault/clusterrolebinding.yaml b/gitops/platform/vault/base/clusterrolebinding.yaml similarity index 100% rename from k8s/base/plugins/vault/clusterrolebinding.yaml rename to gitops/platform/vault/base/clusterrolebinding.yaml diff --git a/k8s/base/plugins/vault/configmap.yaml b/gitops/platform/vault/base/configmap.yaml similarity index 100% rename from k8s/base/plugins/vault/configmap.yaml rename to gitops/platform/vault/base/configmap.yaml diff --git a/k8s/base/plugins/vault/kustomization.yaml b/gitops/platform/vault/base/kustomization.yaml similarity index 100% rename from k8s/base/plugins/vault/kustomization.yaml rename to gitops/platform/vault/base/kustomization.yaml diff --git a/k8s/base/plugins/vault/service.yaml b/gitops/platform/vault/base/service.yaml similarity index 100% rename from k8s/base/plugins/vault/service.yaml rename to gitops/platform/vault/base/service.yaml diff --git a/k8s/base/plugins/vault/serviceaccount.yaml b/gitops/platform/vault/base/serviceaccount.yaml similarity index 100% rename from k8s/base/plugins/vault/serviceaccount.yaml rename to gitops/platform/vault/base/serviceaccount.yaml diff --git a/k8s/base/plugins/vault/statefulset.yaml b/gitops/platform/vault/base/statefulset.yaml similarity index 98% rename from k8s/base/plugins/vault/statefulset.yaml rename to gitops/platform/vault/base/statefulset.yaml index 727a7af..7f71111 100755 --- a/k8s/base/plugins/vault/statefulset.yaml +++ b/gitops/platform/vault/base/statefulset.yaml @@ -121,7 +121,7 @@ spec: spec: accessModes: - ReadWriteOnce - storageClassName: local-path + storageClassName: replace-in-overlay volumeMode: Filesystem resources: requests: diff --git a/k8s/overlays/dev/vault/kustomization.yaml b/gitops/platform/vault/overlays/lab/kustomization.yaml similarity index 70% rename from k8s/overlays/dev/vault/kustomization.yaml rename to gitops/platform/vault/overlays/lab/kustomization.yaml index c0119e8..e7ce108 100644 --- a/k8s/overlays/dev/vault/kustomization.yaml +++ b/gitops/platform/vault/overlays/lab/kustomization.yaml @@ -4,7 +4,7 @@ kind: Kustomization namespace: mnt resources: - - ../../../base/plugins/vault + - ../../base - networkpolicy.yaml patches: @@ -12,6 +12,9 @@ patches: kind: StatefulSet name: vault patch: |- + - op: replace + path: /spec/volumeClaimTemplates/0/spec/storageClassName + value: local-path - op: replace path: /spec/volumeClaimTemplates/0/spec/resources/requests/storage value: 1Gi diff --git a/k8s/overlays/dev/vault/networkpolicy.yaml b/gitops/platform/vault/overlays/lab/networkpolicy.yaml similarity index 100% rename from k8s/overlays/dev/vault/networkpolicy.yaml rename to gitops/platform/vault/overlays/lab/networkpolicy.yaml diff --git a/gitops/policies/README.md b/gitops/policies/README.md new file mode 100644 index 0000000..6cf111d --- /dev/null +++ b/gitops/policies/README.md @@ -0,0 +1,18 @@ +# Policy Catalog + +cluster-wide admission 규칙과 거버넌스 정책을 둡니다. Kyverno, Gatekeeper 등 +정책 엔진은 하나를 선택하고 ADR로 기록합니다. 구체적인 Namespace, +Role/RoleBinding, ResourceQuota와 NetworkPolicy 인스턴스는 `tenants`가 +소유하며 이곳에서 중복 생성하지 않습니다. + +권장 분류: + +- baseline workload security +- allowed registries와 image 검증 +- resource request/limit +- namespace/RBAC 생성 규칙 검증 +- network isolation 적용 여부 검증 +- 정책 예외와 만료 조건 + +처음에는 audit 모드와 테스트로 영향 범위를 확인한 뒤 enforcement를 적용합니다. +예외에는 owner, 사유와 만료일을 반드시 기록합니다. diff --git a/gitops/policies/_template/README.md b/gitops/policies/_template/README.md new file mode 100644 index 0000000..27f7bd7 --- /dev/null +++ b/gitops/policies/_template/README.md @@ -0,0 +1,17 @@ +# __REPLACE_ME_POLICY_SET_NAME__ + +## 목적과 범위 + +보호하는 대상, 위협과 제외 범위를 적습니다. + +## 적용 단계 + +audit 결과, enforcement 전환 조건과 rollback을 적습니다. + +## 예외 + +예외 schema, 승인자와 만료 정책을 적습니다. + +## 테스트 + +허용/거부 fixture와 선택한 policy engine의 test 명령을 추가합니다. diff --git a/gitops/policies/_template/base/kustomization.yaml b/gitops/policies/_template/base/kustomization.yaml new file mode 100644 index 0000000..e609fb2 --- /dev/null +++ b/gitops/policies/_template/base/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: [] diff --git a/gitops/policies/baseline/overlays/lab/kustomization.yaml b/gitops/policies/baseline/overlays/lab/kustomization.yaml new file mode 100644 index 0000000..6b6c095 --- /dev/null +++ b/gitops/policies/baseline/overlays/lab/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: mnt + +resources: + - networkpolicy.yaml + diff --git a/k8s/overlays/dev/networkpolicy-baseline.yaml b/gitops/policies/baseline/overlays/lab/networkpolicy.yaml similarity index 100% rename from k8s/overlays/dev/networkpolicy-baseline.yaml rename to gitops/policies/baseline/overlays/lab/networkpolicy.yaml diff --git a/gitops/tenants/README.md b/gitops/tenants/README.md new file mode 100644 index 0000000..9e1d4c6 --- /dev/null +++ b/gitops/tenants/README.md @@ -0,0 +1,16 @@ +# Tenant Catalog + +여러 팀 또는 workload 경계를 운영할 때 사용하는 선택 영역입니다. + +가능한 리소스: + +- Namespace와 ownership label +- Role/RoleBinding +- ResourceQuota와 LimitRange +- 기본 NetworkPolicy +- secret manager/service account 연결 + +작은 단일 팀 구성에서는 이 계층을 생략하고 platform 또는 app 소유권에 맞게 +namespace를 관리할 수 있습니다. tenant와 app이 같은 namespace를 중복 생성하지 +않도록 한쪽만 소유합니다. `policies`는 이런 namespace-scoped 리소스 자체가 +아니라 조직 규칙을 검증하는 admission 정책만 소유합니다. diff --git a/gitops/tenants/_template/README.md b/gitops/tenants/_template/README.md new file mode 100644 index 0000000..d204772 --- /dev/null +++ b/gitops/tenants/_template/README.md @@ -0,0 +1,19 @@ +# __REPLACE_ME_TENANT_NAME__ + +## 소유자와 범위 + +Team: __REPLACE_ME_OWNER__ + +namespace, cluster 범위와 연락처를 적습니다. + +## 권한 + +최소 RBAC와 workload identity contract를 적습니다. + +## Guardrail + +quota, limit, network와 승인된 policy exception ID/문서 참조를 적습니다. + +## 온보딩/오프보딩 + +생성, 권한 회수, 데이터 보존과 namespace 삭제 절차를 적습니다. diff --git a/gitops/tenants/_template/base/kustomization.yaml b/gitops/tenants/_template/base/kustomization.yaml new file mode 100644 index 0000000..e609fb2 --- /dev/null +++ b/gitops/tenants/_template/base/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: [] diff --git a/k8s/base/managing/namespace/kustomization.yaml b/gitops/tenants/mnt/base/kustomization.yaml similarity index 100% rename from k8s/base/managing/namespace/kustomization.yaml rename to gitops/tenants/mnt/base/kustomization.yaml diff --git a/k8s/base/managing/namespace/namespace.yaml b/gitops/tenants/mnt/base/namespace.yaml similarity index 100% rename from k8s/base/managing/namespace/namespace.yaml rename to gitops/tenants/mnt/base/namespace.yaml diff --git a/guide.md b/guide.md old mode 100755 new mode 100644 index cd4b4cb..3006239 --- a/guide.md +++ b/guide.md @@ -1,901 +1,128 @@ -# Project-Infra 운영 가이드 +# Project Infra 실행 가이드 -아키텍처 · 폴더 구조 · 설계 결정은 [README.md](README.md) 를 먼저 읽는다. 본 문서는 **실제 배포·운영 절차** 에 집중한다. 보안 정책 / 거버넌스 (etcd 암호화, Vault 토큰 관리, bash history 보호) 는 [docs/security-hardening.md](docs/security-hardening.md) 참고. +## 1. 준비 ---- +필수: -## 목차 - -1. [사전 준비](#1-사전-준비) -2. [최초 부트스트랩 — 자동](#2-최초-부트스트랩--자동) -3. [최초 부트스트랩 — 수동 (단계별)](#3-최초-부트스트랩--수동-단계별) -4. [Traefik / Ingress 운영](#4-traefik--ingress-운영) -5. [TLS / cert-manager 적용](#5-tls--cert-manager-적용) -6. [Keycloak Operator / RealmImport 적용](#6-keycloak-operator--realmimport-적용) -7. [Vault 시크릿 관리](#7-vault-시크릿-관리) -8. [Docker Registry 사용법](#8-docker-registry-사용법) -9. [앱에서 시크릿 사용하기](#9-앱에서-시크릿-사용하기) -10. [마이그레이션 실행 (Flyway)](#10-마이그레이션-실행-flyway) -11. [Vault UI 접근](#11-vault-ui-접근) -12. [환경별 배포](#12-환경별-배포) -13. [검증 / 린트 / 스키마 체크](#13-검증--린트--스키마-체크) -14. [정리 / 롤백 (teardown)](#14-정리--롤백-teardown) -15. [트러블슈팅](#15-트러블슈팅) - ---- - -## 1. 사전 준비 - -> **예상 소요**: 첫 셋업 30 분 (CLI 설치 포함). 두 번째부터는 0. - -### 필요한 CLI - -| 도구 | 용도 | -|---|---| -| `kubectl` | 클러스터 조작 | -| `helm` | VSO Operator 설치 | -| `jq` | JSON 파싱 (scripts 내부) | -| `kustomize` | (선택) 로컬 렌더 | -| `kubeconform` | (선택) 스키마 검증 | -| `kube-linter` | (선택) 안티패턴 린트 | -| `yq` | (선택) YAML 가공 | - -> **참고**: `validate.sh` 는 `~/bin` 에 설치된 도구도 자동으로 PATH 에 추가한다. - -### 필요한 환경 변수 - -| 변수 | 의미 | -|---|---| -| `CONFIRM=yes` | (teardown 전용) 대화형 확인 자동 yes 처리 | -| `RESET_STALE_SECRETS=yes` | (bootstrap 전용) 기존 VSO-managed Secret 삭제 후 재생성 | -| `AUTO_GENERATE=yes` | (vault-seed-apps 전용) 비대화 + env 없음 시 랜덤 비밀번호 생성 | -| `POSTGRES_SUPERUSER_PASSWORD` | (bootstrap Phase 6 비대화) Postgres superuser 비밀번호 | -| `KEYCLOAK_DB_PASSWORD` | (bootstrap Phase 6 비대화) Keycloak DB 비밀번호 | -| `AUTH_SERVER_DB_PASSWORD` | (bootstrap Phase 6 비대화) auth-server DB 비밀번호 | -| `KEYCLOAK_ADMIN_PASSWORD` | (bootstrap Phase 6 비대화) Keycloak 초기 관리자 비밀번호 | -| `MINIO_ROOT_PASSWORD` | (bootstrap Phase 6 비대화) MinIO Tenant 루트 비밀번호 | - -> **주의**: env var 사용 시 [bash history 보호](docs/security-hardening.md#3-bash-history-에-비밀번호-남기지-않기) 참고. `HISTFILE=/dev/null` 접두 권장. - -### 클러스터 전제 - -- Kubernetes 1.25+ (Pod Security Admission 사용) -- `local-path` StorageClass (K3s 기본) 또는 동등한 RWO 프로비저너 -- `kubernetes.io/metadata.name` namespace 라벨이 자동으로 붙는 1.22+ 환경 -- dev 클러스터의 K3s 기본 Traefik 이 `kube-system` namespace 에 존재해야 함 - ---- - -## 2. 최초 부트스트랩 — 자동 - -> **예상 소요**: 10~15 분 (이미지 pull 시간 포함). Phase 6 의 시크릿 입력이 가장 오래 걸림. - -### 대화형 실행 (권장) - -Phase 6 에서 앱 시크릿 5 개 비밀번호를 무음 입력. bash history 에 남지 않는다. +- `kubectl`, `helm`, `jq`, `openssl` +- 대상 K3s cluster의 kubeconfig +- 환경과 정확히 매핑한 context 변수 ```bash -bash k8s/scripts/bin/bootstrap.sh dev -# Phase 6 진행 중: -# Postgres superuser 비밀번호: ******* -# Postgres superuser 비밀번호 한 번 더: ******* -# Keycloak DB 비밀번호: ******* -# ... (5 개 시크릿, 각각 확인 재입력 포함) +export KUBE_CONTEXT_LAB='<expected-context>' +kubectl config current-context ``` -### 비대화 (CI) 실행 +전체 정적 도구는 `mise install`로 설치할 수 있습니다. + +## 2. 사전 렌더 ```bash -HISTFILE=/dev/null \ -POSTGRES_SUPERUSER_PASSWORD='...' \ -KEYCLOAK_DB_PASSWORD='...' \ -AUTH_SERVER_DB_PASSWORD='...' \ -KEYCLOAK_ADMIN_PASSWORD='...' \ -MINIO_ROOT_PASSWORD='...' \ -bash k8s/scripts/bin/bootstrap.sh dev - -# 또는 비대화 + 랜덤 생성 (운영자가 값을 몰라도 됨, Vault 에서 나중에 조회): -AUTO_GENERATE=yes bash k8s/scripts/bin/bootstrap.sh dev +kubectl kustomize gitops/clusters/lab/main/all >/tmp/project-infra-lab.yaml +make check ``` -### 실행 단계 +`all`은 감사용입니다. 렌더 결과를 `kubectl apply -f`로 적용하지 마세요. -`bin/bootstrap.sh` 가 9 단계 (Phase 0~8) 를 순서대로 실행한다: +## 3. Bootstrap -| Phase | 내용 | -|:---:|---| -| 0 | MinIO Operator 설치 (`tasks/minio-operator-install.sh`) — Tenant CRD 선행 등록 (별도 namespace `minio-operator`) | -| 1 | Namespace + PSS 라벨 (`kubectl apply -k k8s/base/managing/namespace`) — 먼저 적용해 의존성 안정화 | -| 2 | 기존 Secret 점검 — VSO-managed K8s Secret 5 개 중 이미 존재하는 것 탐지. `RESET_STALE_SECRETS=yes` 면 삭제 | -| 3 | 인프라 리소스 배포 (`kubectl apply -k k8s/overlays/dev/`) — vault + registry + 앱 워크로드 | -| 4 | vault-0 Running 대기 — Ready 가 아니라 **Running**. Vault readiness probe 는 초기화+unseal 후에만 통과하므로 | -| 5 | Vault 초기화 (`tasks/vault-init.sh`) — init / unseal / KV v2 / k8s auth / policy × 2 / role × 2 | -| 6 | 앱 시크릿 seed (`tasks/vault-seed-apps.sh`) — 5 개 시크릿 대화형 입력 (이미 있으면 skip) | -| 7 | VSO Helm (`tasks/vso-install.sh`) — 기존 dirty 릴리즈 자동 uninstall + `helm upgrade --install --wait` | -| 8 | VSO CRDs (`kubectl apply -k k8s/overlays/dev/vso/`) | +대화형: -> **참고**: 스크립트는 idempotent 다. 이미 진행된 단계는 자동 스킵된다. +```bash +bash scripts/bin/bootstrap.sh lab +``` -> **주의**: 현재 `bootstrap.sh` 는 `mnt` namespace 자원까지만 자동 배포한다. `k8s/overlays/dev/platform/traefik/` 와 `k8s/overlays/dev/tls/` 는 namespace 가 다르거나 optional dependency 가 있어 운영자가 별도로 적용한다 (§4, §5). -> -> `k8s/overlays/dev/platform/cert-manager/`, `k8s/overlays/dev/platform/keycloak-operator/`, `k8s/overlays/dev/keycloak-realm/` 은 CRD / 외부 DNS / 인증 흐름 의존성이 있어 자동 부트스트랩 전에 선행/별도 확인이 필요하다 (§5, §6). +비대화 환경에서는 각 stage 확인을 명시적으로 승인합니다. -### 생성되는 파일 +```bash +CONFIRM=yes AUTO_GENERATE=yes \ + bash scripts/bin/bootstrap.sh lab +``` -- `vault-init-keys.json` — **unseal keys (5 개) + root token**. 권한 0600 으로 저장. +`AUTO_GENERATE=yes`로 생성한 비밀번호는 출력하지 않습니다. Vault에서 권한을 +가진 운영자가 별도로 확인합니다. -> **주의**: 이 파일은 **반드시 오프라인 금고 / 외부 KMS 로 이동** 하고 원본은 삭제한다. `.gitignore` 에 등록되어 있으나 실수로도 커밋하지 말 것. +기본 Vault init 파일은 ignored 상태이지만 평문입니다. 가능하면 repo 밖 경로를 +사용하세요. -### 완료 확인 +```bash +VAULT_KEYS_FILE=/secure/path/lab-vault-init.json \ + bash scripts/bin/bootstrap.sh lab +``` + +## 4. Stage별 확인 ```bash kubectl -n mnt get pods -kubectl -n mnt get secrets | grep -E 'identity-postgres-superuser|keycloak-db|auth-server-db|keycloak-bootstrap-admin|minio-tenant-env' -kubectl -n mnt get vaultstaticsecret +kubectl -n mnt get vaultconnection,vaultauth,vaultstaticsecret +kubectl -n mnt get jobs +kubectl -n mnt get keycloak,keycloakrealmimport ``` -기대 상태: 모든 Pod `Running 1/1`, K8s Secret 5 개 존재, VaultStaticSecret `Status: Synced`. - ---- - -## 3. 최초 부트스트랩 — 수동 (단계별) - -> **예상 소요**: 자동과 동일하나 학습 시 +20~30 분. - -자동 스크립트가 중간에 실패했을 때, 또는 학습 목적으로 단계별 진행이 필요할 때. - -### 3-1. MinIO Operator 설치 +기대 one-shot 결과: ```bash -REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/minio-operator-install.sh +kubectl -n mnt wait --for=condition=Complete \ + job/auth-server-migrate-0-1-0 --timeout=1800s +kubectl -n mnt wait --for=condition=Done \ + keycloakrealmimport/platform-realm-v1 --timeout=600s ``` -### 3-2. 인프라 배포 +## 5. 개별 stage 진단 + +bootstrap이 실패하면 실패한 stage를 먼저 렌더하고 diff합니다. ```bash -kubectl apply -k k8s/base/managing/namespace # namespace 선행 -kubectl apply -k k8s/overlays/dev/ +stage='30-data' +kubectl kustomize "gitops/clusters/lab/main/stages/$stage" >/tmp/stage.yaml +kubectl apply -f /tmp/stage.yaml --dry-run=server --server-side +kubectl diff -f /tmp/stage.yaml --server-side ``` -`mnt` namespace 와 Vault / Registry / 앱 워크로드가 선언된다. +의존 stage가 준비되지 않은 상태에서 뒤 stage를 apply하지 마세요. 특히 +`20-secrets`는 VSO CRD/controller와 Vault auth가, `30-data`는 선행 +destination Secret이, `35-registry`는 MinIO bucket/access key와 Vault 경로가, +`40-operations`는 DB와 Keycloak이 준비되어야 합니다. -> **참고**: Registry 는 MinIO S3 자격증명 Secret 이 주입되기 전까지 대기할 수 있다. Postgres / Keycloak / auth-server 도 Vault secret 이 주입되기 전까지 `ContainerCreating` 으로 대기한다 (정상). +## 6. Migration과 realm 변경 -### 3-3. Vault Pod Running 대기 +Flyway SQL을 추가하거나 수정해 새 release를 만들 때: -> **참고**: Vault 는 초기화 전에는 Ready 가 될 수 없으므로 Running 까지만 기다린다 (§15.10 참고). +1. SQL version을 추가합니다. 이미 적용된 migration을 수정하지 않습니다. +2. Job 이름과 instance label의 release suffix를 올립니다. +3. bootstrap의 completion target도 새 Job 이름으로 맞춥니다. +4. render/schema/diff 후 operation stage를 실행합니다. + +Realm 변경도 기존 `platform-realm-v1` spec을 고쳐 재실행되는 것으로 가정하지 +않습니다. 새 versioned KeycloakRealmImport operation을 만들고 완료 상태를 +확인합니다. + +## 7. Teardown + +앱과 operation만: ```bash -kubectl -n mnt wait --for=jsonpath='{.status.phase}'=Running pod/vault-0 --timeout=120s +bash scripts/bin/teardown.sh lab ``` -### 3-4. Vault 초기화 +데이터와 namespace 포함: ```bash -REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-init.sh +DELETE_DATA=yes bash scripts/bin/teardown.sh lab ``` -이 스크립트가 수행하는 것: -- `vault operator init -key-shares=5 -key-threshold=3` (이미 초기화되었으면 스킵) -- `vault-init-keys.json` 생성 (권한 0600) -- Sealed 상태면 자동 unseal -- root token 으로 로그인 (stdin 파이프 — stdout 에 안 찍힘) -- `secret/` 에 KV v2 활성화 (idempotent) -- `kubernetes` auth method 활성화 + `kubernetes_ca_cert` + `token_reviewer_jwt` 설정 (idempotent) -- `vso-auth-platform` / `vso-storage` policy × 2 작성 (항상 재적용) -- `vso-auth-platform` / `vso-storage` k8s auth role × 2 작성 (항상 재적용) - -### 3-5. VSO Helm 설치 +전용 lab cluster에서 공유 operator까지: ```bash -REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vso-install.sh +DELETE_DATA=yes TEARDOWN_PLATFORM=yes \ + bash scripts/bin/teardown.sh lab ``` -`helm upgrade --install --wait --timeout 5m` 으로 실행. 시작 시 기존 릴리즈가 `failed`/`pending*`/`uninstalling` 상태면 자동 uninstall 후 재설치 (§15.11). `--values` 는 `k8s/base/plugins/vso/helm/values.yaml`. - -### 3-6. VSO CRDs 적용 - -```bash -kubectl apply -k k8s/overlays/dev/vso/ -``` - -`VaultConnection` / `VaultAuth` × 2 가 등록된다. `VaultStaticSecret` 은 dev overlay 각 서브디렉토리 (`database/`, `keycloak/`, `storage/`) 에서 이미 함께 적용됨. VSO Operator 가 Vault KV 를 읽어 K8s Secret 을 합성. - -> **주의**: 해당 Vault KV 경로에 값이 실제로 있어야 성공. 아직 없으면 VSO 가 permission denied 또는 not found 로 남음. §7 의 수동 주입 후 자동 재시도. - ---- - -## 4. Traefik / Ingress 운영 - -> **예상 소요**: 30 초~1 분 (apply 만). - -### 왜 별도 overlay 인가 - -`k8s/overlays/dev/kustomization.yaml` 은 `namespace: mnt` 를 전역으로 주입한다. 반면 K3s 기본 Traefik 은 실제로 `kube-system` 에 존재한다. 그래서 Traefik 운영 리소스는 같은 kustomization 안에 섞지 않고 `k8s/overlays/dev/platform/traefik/` 로 분리했다. - -### 적용 대상 - -| overlay | namespace | 설명 | -|---|---|---| -| `k8s/overlays/dev/platform/traefik` | `kube-system` | `HelmChartConfig` + `Middleware` + `TLSOption` | -| `k8s/overlays/dev/` | `mnt` | `auth-server`, `keycloak` 의 app Ingress 및 app NetworkPolicy | - -### Traefik 운영 overlay 적용 - -```bash -kubectl apply -k k8s/overlays/dev/platform/traefik -``` - -포함되는 것: - -- `HelmChartConfig/traefik` — `replicas=2`, `ingressClass=traefik`, HTTP→HTTPS redirect, metrics 활성화 -- `Middleware/security-headers` — HSTS, `X-Content-Type-Options`, frame deny 등 공용 헤더 -- `TLSOption/modern-tls` — TLS 1.2+, strict SNI, 허용 cipher suite - -### 앱 Ingress 현재 상태 - -| 리소스 | host | 공개 범위 | -|---|---|---| -| `auth-server` | `project.com` | `/` | -| `keycloak-public` | `keycloak.dev.example.com` | `/realms/`, `/resources/`, `/.well-known/`, `/js/` | - -app Pod 는 기본 deny 상태이므로, Traefik 에서 들어오는 8080/TCP 만 NetworkPolicy 로 별도 허용한다. - -### ForwardAuth variant 적용 - -repo 에는 component `k8s/components/forward-auth/` 가 준비되어 있고, 현재 `k8s/overlays/dev/` 가 이 component 를 직접 포함한다. 따라서 dev overlay 를 적용하면 oauth2-proxy 와 `auth-server` 보호 middleware 도 함께 렌더링된다. - -적용 전제: - -1. `k8s/overlays/dev/keycloak-realm/` 또는 동등한 방법으로 `platform` realm + `auth-server-ingress` client 준비 (§6) -2. redirect URI 를 `https://project.com/oauth2/callback` 로 등록 -3. Vault path `secret/oauth2-proxy/forward-auth` 에 아래 key 저장 (§7) - - `client-secret` - - `cookie-secret` -4. `project.com`, `keycloak.dev.example.com` 이 실제 Traefik 진입점으로 해석 - -적용: - -```bash -kubectl apply -k k8s/overlays/dev -``` - -이 overlay 가 추가하는 것: - -- `oauth2-proxy` Deployment / Service -- `project.com/oauth2/` 경로용 Ingress -- `oauth2-proxy-auth` Traefik `Middleware` -- `auth-server` Ingress patch — `project.com/` 요청은 oauth2-proxy ForwardAuth 를 먼저 통과해야 함 - -> **주의**: 현재 dev 용 oauth2-proxy 설정은 `ssl_insecure_skip_verify=false` 이다. 따라서 `keycloak.dev.example.com` 인증서 체인이 정상이어야 로그인 흐름이 끝까지 진행된다. - -> **참고**: dev 운영 완료까지 남은 항목 (DNS / ACME 인증서 / realm 적용 / negative test 등) 은 [README Limitations](README.md#limitations-honest-scope) 참고. - ---- - -## 5. TLS / cert-manager 적용 - -> **예상 소요**: 설치 3~5 분 + 외부 DNS 의존 (실제 발급은 DNS 가 Traefik 진입점을 가리켜야 가능). - -cert-manager 는 repo source of truth 로 편입되어 있다. dev 기준 설치 overlay 는 `k8s/overlays/dev/platform/cert-manager/` 이며, 공식 static install `v1.20.2` 를 적용한다. `ClusterIssuer` 는 CRD 등록 이후 `k8s/overlays/dev/platform/cert-manager-issuers/` 로 별도 적용한다. - -### 적용 - -```bash -kubectl apply -k k8s/overlays/dev/platform/cert-manager -kubectl -n cert-manager rollout status deploy/cert-manager --timeout=180s -kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=180s -kubectl -n cert-manager rollout status deploy/cert-manager-cainjector --timeout=180s -kubectl apply -k k8s/overlays/dev/platform/cert-manager-issuers -``` - -> **주의**: -> - `letsencrypt-prod-clusterissuer.yaml` / `letsencrypt-staging-clusterissuer.yaml` 의 `admin@project.com` 은 실제 수신 가능한 운영 메일로 교체한다. -> - HTTP-01 은 `project.com`, `keycloak.dev.example.com` 이 Traefik 외부 진입점으로 해석되고 80/443 이 도달 가능해야 성공한다. - -### 준비된 Certificate 리소스 - -| 파일 | secretName | host | -|---|---|---| -| `k8s/overlays/dev/tls/project-com-certificate.yaml` | `project-com-tls` | `project.com` | -| `k8s/overlays/dev/tls/keycloak-dev-certificate.yaml` | `keycloak-dev-example-com-tls` | `keycloak.dev.example.com` | - -### Certificate 적용 - -전제: - -- `cert-manager` CRD 설치 완료 -- `ClusterIssuer/letsencrypt-prod` 또는 동등한 issuer 준비 -- DNS 가 실제 Traefik 진입점으로 향함 - -```bash -kubectl apply -k k8s/overlays/dev/tls -``` - -### 완료 확인 - -```bash -kubectl -n mnt get certificate -kubectl -n mnt describe certificate project-com # Status.Conditions.Ready=True -``` - -> **권장**: 인증서 발급 상태를 먼저 확인한 뒤 ForwardAuth E2E 검증을 진행한다 (§4). - ---- - -## 6. Keycloak Operator / RealmImport 적용 - -> **예상 소요**: 5~10 분. - -Keycloak 은 권장 흐름에 맞춰 Operator 기반으로 전환한다. dev 제약상 실제 Keycloak 인스턴스와 realm/client 는 `mnt` 에 두며, Keycloak Operator 도 `mnt` 에 설치해 해당 namespace 를 watch 하게 한다. - -> **참고**: `mnt` 는 default-deny egress namespace 이므로, `k8s/overlays/dev/platform/keycloak-operator/networkpolicy.yaml` 이 Operator Pod 에서 Kubernetes API 로 나가는 443/6443 만 허용한다. 이 정책이 없으면 Operator informer 가 API server 에 연결하지 못해 CrashLoopBackOff 로 떨어진다. - -### 왜 필요한가 - -- `oauth2-proxy` 는 `auth-server-ingress` client 를 전제로 동작한다 -- client / redirect URI 같은 OIDC 계약은 Git 에서 관리되어야 drift 가 줄어든다 -- 표준도 Keycloak realm 을 `KeycloakRealmImport` 로 선언형 관리하라고 권장한다 - -### 적용 순서 - -**1. Keycloak Operator CRD / controller 적용** - -```bash -kubectl apply -k k8s/overlays/dev/platform/keycloak-operator -kubectl -n mnt rollout status deploy/keycloak-operator --timeout=180s -``` - -**2. 기존 수제 Keycloak 리소스 정리** - -기존 `Deployment` 기반 Keycloak 과 Operator 기반 Keycloak 이 같은 `Service/keycloak` 이름을 쓰므로, 전환 시 기존 수제 리소스를 정리한다. - -```bash -kubectl -n mnt delete deployment/keycloak service/keycloak configmap/keycloak-config serviceaccount/keycloak-sa --ignore-not-found -``` - -**3. dev overlay 적용** - -`k8s/overlays/dev/keycloak/` 는 이제 `Keycloak` CR, VSO secret 변환, Ingress, NetworkPolicy 를 포함한다. - -```bash -kubectl apply -k k8s/overlays/dev -kubectl -n mnt get keycloak keycloak -kubectl -n mnt get pods -l app.kubernetes.io/instance=keycloak -``` - -**4. RealmImport 적용** - -```bash -kubectl apply -k k8s/overlays/dev/keycloak-realm -kubectl -n mnt get keycloakrealmimport platform-realm -``` - -### client secret 처리 - -> **주의**: `auth-server-ingress` 같은 confidential client 의 secret 값 자체는 Git 에 넣지 않는다. realm/client shape 는 Git 에 두고, secret 값은 생성 후 Vault path `secret/oauth2-proxy/forward-auth` 로 넣어 oauth2-proxy 가 소비하게 한다 (§7). - ---- - -## 7. Vault 시크릿 관리 - -> **예상 소요**: 회당 1~2 분 (port-forward + put). - -### 애플리케이션 시크릿 저장 — 자동화됨 - -5 개 시크릿은 `bin/bootstrap.sh` Phase 6 에서 자동으로 seed 된다. 또는 단독 실행: - -```bash -REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-seed-apps.sh -``` - -동작: -- **이미 있는 경로는 skip** — 운영자가 회전한 값을 덮어쓰지 않음 -- **대화형 입력** (TTY) — `read -r -s` 로 무음 입력 + 확인 재입력. bash history 에 남지 않음 -- **비대화 + env 지정** — 해당 env var 를 사용 (`HISTFILE=/dev/null` 접두 권장) -- **비대화 + env 없음 + `AUTO_GENERATE=yes`** — `openssl rand` 로 랜덤 24 자 생성 - -> **참고**: Vault CLI 의 `vault kv put <path> -` 모드로 **JSON stdin 전달** 이라 비밀번호가 argv / process table 어디에도 노출되지 않는다. - -### 시크릿 경로 및 키 - -| 경로 | 키 | 용도 | -|---|---|---| -| `secret/identity-postgres/superuser` | `username` (기본 postgres), `password` | Postgres 슈퍼유저 (`POSTGRES_USER_FILE` / `POSTGRES_PASSWORD_FILE`) | -| `secret/keycloak/db` | `password` | Keycloak 의 DB 비밀번호 + initdb 가 생성하는 keycloak DB role | -| `secret/auth-server/db` | `SPRING_DATASOURCE_USERNAME` (기본 auth_server), `SPRING_DATASOURCE_PASSWORD` | Spring Boot configtree + Flyway | -| `secret/keycloak/bootstrap-admin` | `KEYCLOAK_ADMIN` (기본 admin), `KEYCLOAK_ADMIN_PASSWORD` | Keycloak 초기 관리자 계정 | -| `secret/minio/tenant-env` | `config.env` (env-file 포맷 단일 키) | MinIO Operator Tenant 루트 자격증명 | -| `secret/oauth2-proxy/forward-auth` | `client_secret`, `cookie_secret` | dev overlay 의 oauth2-proxy confidential client / session cookie | -| `secret/docker-registry/basic-auth` | `username`, `password`, `users` (htpasswd 한 줄) | Traefik Middleware 가 외부 push 시 검증 | - -### 값 조회 - -```bash -kubectl -n mnt port-forward svc/vault 8200:8200 & -export VAULT_ADDR=http://127.0.0.1:8200 -vault login -method=userpass username=alice # userpass admin 권장 — root token 사용 중단 - -vault kv get secret/keycloak/bootstrap-admin -# 특정 field 만: -vault kv get -field=KEYCLOAK_ADMIN_PASSWORD secret/keycloak/bootstrap-admin -``` - -> **참고**: userpass admin 셋업은 [docs/security-hardening.md §2](docs/security-hardening.md#2-vault-운영자-토큰-관리) 참고. - -### 값 변경 (비밀번호 교체) - -```bash -# 새 값으로 덮어쓰기 (vault kv put) — seed-apps.sh 의 skip 로직 우회 -vault kv put secret/keycloak/db password='<새-pw>' - -# 60s ~ 1h 내 VSO 가 자동으로 K8s Secret 갱신. 즉시 반영 원하면: -kubectl -n mnt delete secret keycloak-db -# VSO 가 Vault KV 를 읽어 재생성 -``` - -> **Tip**: 강제 즉시 반영의 다른 방법 — `kubectl -n mnt annotate vaultstaticsecret <name> refresh=$(date +%s) --overwrite`. - -> **주의 — MinIO 비밀번호에 `"` 금지**: MinIO 의 `config.env` 는 env-file 포맷 (`export KEY="value"`) 이라 값에 `"` 가 들어가면 파싱 깨짐. `vault-seed-apps.sh` 가 프롬프트에서 거부하며 재입력 요구한다. - -### root token 회전 - -운영 정책 / userpass admin 셋업 절차는 [docs/security-hardening.md §2](docs/security-hardening.md#2-vault-운영자-토큰-관리) 참고. - ---- - -## 8. Docker Registry 사용법 - -> **예상 소요**: push/pull 회당 < 1 분 (이미지 크기 의존). - -### Push - -```bash -docker login registry.project.com -# username: <DOCKER_REGISTRY_PUSH_USERNAME, 기본 registry-push> -# password: <Vault 에 저장한 값> - -docker tag my-app:0.1.0 registry.project.com/my-app:0.1.0 -docker push registry.project.com/my-app:0.1.0 -``` - -외부 push 는 `registry.project.com` Ingress 로 들어오며 Traefik BasicAuth 를 통과해야 한다. BasicAuth 의 htpasswd `users` 값은 Vault path `secret/docker-registry/basic-auth` 에 저장되고 VSO 가 `docker-registry-basic-auth` Secret 으로 동기화한다. - -> **주의**: 실제 워크로드 이미지는 `registry.project.com/...` 주소를 사용한다. image pull 은 Pod 내부가 아니라 노드의 kubelet/containerd 가 수행하므로, `docker-registry.mnt.svc.cluster.local` 같은 ClusterIP DNS 를 `image:` 에 쓰는 방식은 피한다. - -### 내부 Service 직접 접근 (debugging) - -내부 Service 는 registry Pod 자체 확인이나 클러스터 내부 HTTP 접근이 필요할 때만 사용한다. - -```bash -docker tag my-app:0.1.0 docker-registry.mnt.svc.cluster.local:5000/my-app:0.1.0 -docker push docker-registry.mnt.svc.cluster.local:5000/my-app:0.1.0 -``` - -### Pull (Pod) - -```yaml -apiVersion: apps/v1 -kind: Deployment -spec: - template: - spec: - serviceAccountName: auth-server-sa - containers: - - name: my-app - image: registry.project.com/my-app:0.1.0 -``` - -`auth-server-sa` / `test-server-*-sa` 는 dev overlay 에서 `docker-registry-pull-credentials` 를 `imagePullSecrets` 로 참조한다. - -### 완료 확인 - -```bash -curl -fsS -u <push-user>:<push-pw> https://registry.project.com/v2/my-app/tags/list -# 정상 응답: {"name":"my-app","tags":["0.1.0"]} - -kubectl -n mnt get secret docker-registry-pull-credentials -o jsonpath='{.type}{"\n"}' -# 정상 응답: kubernetes.io/dockerconfigjson -``` - -> **참고**: dev 환경에서 외부 DNS / TLS 가 아직 준비 전이면 노드의 containerd 에 이미지를 직접 import 하는 임시 우회가 필요할 수 있다. 정상 운영 (DNS + cert-manager 인증서 발급 완료) 에서는 위 push/pull 만으로 충분하다. - ---- - -## 9. 앱에서 시크릿 사용하기 - -### envFrom (권장) - -```yaml -spec: - containers: - - name: app - envFrom: - - secretRef: - name: auth-server-db # VSO 가 dev overlay 에서 합성 -``` - -### 개별 key - -```yaml -env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: identity-postgres-superuser - key: password -``` - -### volume mount - -```yaml -volumes: - - name: db-creds - secret: - secretName: auth-server-db -containers: - - volumeMounts: - - name: db-creds - mountPath: /etc/secrets - readOnly: true -``` - ---- - -## 10. 마이그레이션 실행 (Flyway) - -> **예상 소요**: 30 초~3 분 (마이그레이션 갯수 의존). - -base 에 정의된 `migration-flyway` Job 은 기본적으로는 배포되지 않는다. dev overlay 가 ArgoCD PreSync / sync-wave=-1 annotation 을 patch 하므로, GitOps 로 배포할 때는 ArgoCD 가 앱보다 먼저 Job 을 실행한다. - -> **주의**: `kubectl apply -k` 로 수동 배포 시에는 Job 이 **앱과 동시에** 생성되므로 race 가능. 아래 순서대로 실행한다. - -```bash -# migration 먼저 -kubectl apply -k k8s/overlays/dev/auth/ -l app.kubernetes.io/component=migration - -# 완료 대기 -kubectl -n mnt wait --for=condition=complete job/migration-flyway --timeout=300s - -# 앱 배포 -kubectl apply -k k8s/overlays/dev/ -``` - -ArgoCD 를 쓰면 이 순서가 sync-wave 로 자동화된다. - -### 재실행 - -Flyway Job 은 `backoffLimit: 0` 으로 한 번만 실행된다. 재실행하려면: - -```bash -kubectl -n mnt delete job migration-flyway -kubectl apply -k k8s/overlays/dev/auth/ -``` - ---- - -## 11. Vault UI 접근 - -> **참고**: `service-ui` NodePort 는 보안상 제거되었다. 관리자는 port-forward 로만 접근한다. - -```bash -kubectl -n mnt port-forward svc/vault 8200:8200 -# 브라우저에서 http://127.0.0.1:8200/ui -# Token 입력: $(jq -r .root_token vault-init-keys.json) -``` - -> **권장**: root token 은 최초 설정 / 비상 복구 외에는 사용하지 않는다. 평시 접근은 개인별 userpass / OIDC auth 로 분리한다 — [docs/security-hardening.md §2](docs/security-hardening.md#2-vault-운영자-토큰-관리) 참고. - ---- - -## 12. 환경별 배포 - -현재 `dev` 만 구성되어 있다. - -```bash -bash k8s/scripts/bin/bootstrap.sh dev -``` - -> **참고**: staging / prod overlay 는 비어 있으며, 추후 다음 요소를 추가한다 — Vault storage `file` → `raft` 전환, Postgres backup CronJob, cert-manager ClusterIssuer + Certificate, 환경별 hostname (Keycloak / 공개 Ingress), `persistentVolumeClaimRetentionPolicy` 를 prod 는 `Retain` 유지 (dev 는 overlay 에서 `Delete` 로 patch). - -환경별 차등표는 [docs/operations.md](docs/operations.md#환경별-배포) 참고. - ---- - -## 13. 검증 / 린트 / 스키마 체크 - -> **예상 소요**: 1~3 분 (kubeconform 원격 스키마 조회). - -```bash -bash k8s/scripts/ci/validate.sh -``` - -출력: - -``` -k8s/overlays/dev build=ok schema=ok lint=ok -k8s/overlays/dev/vso build=ok schema=ok lint=ok -모든 overlay 통과 -``` - -- **build**: `kustomize build` (환경 중립성 / patch / labels) -- **schema**: `kubeconform -strict -ignore-missing-schemas` (K8s OpenAPI + Datree CRD catalog 원격 조회) -- **lint**: `kube-linter lint --config .kube-linter.yaml` (securityContext / 리소스 요구사항 / PSS / image tag 등) - -> **참고**: `kustomization.yaml` 이 없는 overlay (`staging`, `prod`) 는 자동 스킵된다. - -CI 파이프라인에서 이 스크립트를 PR 게이트로 사용한다. 실패 시 `build=fail|schema=fail|lint=fail` 로 표기되고 상세 에러가 stderr 에 출력된다. - ---- - -## 14. 정리 / 롤백 (teardown) - -> **예상 소요**: 2~5 분 (PVC 보호 finalizer 정리 + namespace 종료). - -```bash -# 대화형 (y/N 확인) -bash k8s/scripts/bin/teardown.sh dev - -# 비대화 (CI) -CONFIRM=yes bash k8s/scripts/bin/teardown.sh dev - -# MinIO Operator 까지 제거 (기본은 유지) -TEARDOWN_MINIO_OPERATOR=yes bash k8s/scripts/bin/teardown.sh dev -``` - -체계적 7 단계: - -| 단계 | 내용 | -|:---:|---| -| 1 | Precheck — namespace 존재 여부 + phase 확인. 일부 단계는 없으면 skip | -| 2 | VSO CRD 삭제 — `kubectl delete -k overlays/<env>/vso/` 60s timeout. 타임아웃 시 `VaultStaticSecret / VaultAuth / VaultConnection` finalizer 강제 해제 | -| 3 | VSO Helm uninstall — `helm uninstall --wait 5m` (Operator 제거) | -| 4 | 인프라 overlay 삭제 — `kubectl delete -k overlays/<env>/` 120s timeout | -| 5 | namespace 잔존 리소스 finalizer 정리 — PVC 보호 finalizer + VSO CRD + 전체 namespaced 리소스 일괄 finalizer 제거 | -| 6 | namespace 삭제 + Terminating 감지 — `kubectl delete namespace` 60s 대기 → 실패 시 `/finalize` API 호출로 강제 종료 | -| 7 | Cluster-scoped 정리 — `vault-tokenreview-binding` ClusterRoleBinding 제거. `TEARDOWN_MINIO_OPERATOR=yes` 면 MinIO Operator 도 함께 | - -> **참고**: controller 없이 남은 CRD finalizer, PVC 보호 finalizer, 전체 namespaced 리소스 finalizer 를 단계별로 선제 해제해서 namespace 가 Terminating 에 걸리지 않도록 처리. 이미 Terminating 에 걸려 있어도 단계 6 에서 `/finalize` API 직접 호출로 강제 종료. - -> **주의**: teardown 후에도 `vault-init-keys.json` 은 보존된다. 완전 초기화하려면 수동으로 삭제한다. - -### 강제 종료의 부작용 - -> **주의**: 단계 6 의 `/finalize` 는 orphan 리소스 (PV / PVC 바인딩) 를 남길 수 있다. - -```bash -# teardown 후 orphan PV 검사 -kubectl get pv | grep -E 'Released|Failed' - -# 필요시 수동 삭제 -kubectl delete pv <name> -``` - ---- - -## 15. 트러블슈팅 - -> **참고**: 보안 정책 / 거버넌스 (etcd 암호화 / Vault root token / bash history) 는 [docs/security-hardening.md](docs/security-hardening.md) 참고. - -### 15.1 Registry Pod 가 계속 `ContainerCreating` - -Secret `docker-registry-minio` 또는 `docker-registry-basic-auth` 가 아직 생성되지 않은 상태일 수 있다. VSO 가 Vault KV 를 읽어서 만든다. - -```bash -kubectl -n mnt describe vaultstaticsecret docker-registry-minio -kubectl -n mnt describe vaultstaticsecret docker-registry-basic-auth -kubectl -n mnt logs -l app.kubernetes.io/name=vault-secrets-operator --tail=100 -``` - -자주 보는 에러: -- `permission denied` → Vault policy 또는 role 설정 오류. `tasks/vault-init.sh` 재실행. -- `no matching vault path` → Vault KV 에 값이 저장되지 않음. `tasks/vault-seed-apps.sh` 재실행. - -### 15.2 VSO 가 Vault 에 로그인 실패 - -```bash -kubectl -n mnt logs -l app.kubernetes.io/name=vault-secrets-operator --tail=200 | grep -i error -``` - -체크 항목: -- Vault ClusterRoleBinding `vault-tokenreview-binding` 존재? `kubectl get clusterrolebinding vault-tokenreview-binding` -- Vault ServiceAccount 에 token 자동 마운트 되어 있음? (기본값 true) -- `vault auth/kubernetes/config` 에 `kubernetes_ca_cert` + `token_reviewer_jwt` 설정됨? → 없으면 `tasks/vault-init.sh` 재실행 -- VaultConnection address 가 `http://vault:8200` 이고 같은 namespace 에 실제 `vault` Service 존재? - -### 15.3 `vault operator init` 실패 — 이미 초기화됨 - -> **참고**: 정상. `tasks/vault-init.sh` 는 idempotent 하게 이 경우를 스킵하고 unseal 만 다시 수행한다. `kubectl exec vault-0 -- vault status` 로 상태 확인. - -### 15.4 부트스트랩 중단 → 재시작 - -```bash -bash k8s/scripts/bin/bootstrap.sh dev -``` - -각 단계가 idempotent 이므로 그대로 다시 실행해도 된다. 이미 완료된 단계는 스킵된다. - -> **참고**: Phase 6 의 시크릿 입력 시, Vault KV 에 이미 있으면 스킵되므로 비밀번호는 사용되지 않음. - -### 15.5 Vault UI 가 안 열림 - -NodePort 는 제거되었다. port-forward 를 사용한다: - -```bash -kubectl -n mnt port-forward svc/vault 8200:8200 -``` - -`http://127.0.0.1:8200/ui` 로 접근. - -### 15.6 NetworkPolicy 로 트래픽 차단 의심 - -```bash -# 모든 NetworkPolicy 확인 -kubectl -n mnt get networkpolicy - -# 특정 Pod 에 적용된 정책 확인 -kubectl -n mnt describe pod <pod-name> | grep -A3 Labels -kubectl -n mnt get networkpolicy -o yaml | grep -A2 podSelector -``` - -임시 허용 (디버깅): - -```bash -kubectl -n mnt delete networkpolicy default-deny-all -``` - -> **주의**: 진단 완료 후 반드시 복구 — `kubectl apply -k k8s/overlays/dev/`. - -### 15.7 PodSecurity 위반 경고 - -`kubectl apply` 중 `Warning: would violate PodSecurity "restricted:latest": ...` 메시지가 뜨면 **어떤 Pod 의 어떤 필드** 가 위반인지 확인: - -```bash -# 최근 이벤트 -kubectl -n mnt get events --sort-by='.lastTimestamp' \ - | grep -i 'podsecurity\|FailedCreate' - -# 경고 메시지는 apply 시 stderr 로도 나옴 -kubectl apply -k k8s/overlays/dev/ 2>&1 | grep -i warning -``` - -자주 걸리는 항목 체크리스트: - -- `runAsNonRoot: true` 누락 또는 `runAsUser: 0` -- `allowPrivilegeEscalation: false` 누락 -- `capabilities.drop: [ALL]` 누락 -- `seccompProfile.type: RuntimeDefault` 누락 -- `readOnlyRootFilesystem: true` 누락 (선택이지만 권장) -- hostPath / hostNetwork / hostPID / hostIPC 사용 -- hostPorts 사용 - -> **참고**: base 의 모든 워크로드는 이미 Restricted 통과. 경고가 뜨는 건 보통 다음 두 가지 — VSO Operator Helm chart, MinIO Operator Helm chart. 둘 다 자기 namespace 에서 돌아가므로 `mnt` 의 PSS 와 무관. `mnt` 안의 Pod 에서 경고가 나면 매니페스트를 수정해야 함. - -### 15.8 기존 K8s Secret 이 남아있을 때 - -VSO 는 `destination.overwrite: false` 기본값이라 **이미 존재하는 Secret 을 덮어쓰지 않는다**. Vault KV 에 새 값을 넣어도 K8s Secret 은 옛날 값을 유지. - -확인: - -```bash -kubectl -n mnt get secret -l 'kubernetes.io/managed-by!=Helm' \ - -o custom-columns=NAME:.metadata.name,AGE:.metadata.creationTimestamp -``` - -해결 1 — 개별 삭제 후 VSO 재생성: - -```bash -kubectl -n mnt delete secret docker-registry-minio docker-registry-basic-auth -# VSO 가 1-2 분 내 Vault KV 에서 읽어 재생성 -kubectl -n mnt get vaultstaticsecret -``` - -해결 2 — bootstrap 재실행 시 자동 정리: - -```bash -RESET_STALE_SECRETS=yes bash k8s/scripts/bin/bootstrap.sh dev -# Phase 2 에서 VSO-managed Secret 7 개 전부 삭제 → Phase 7/8 에서 VSO 재생성 -``` - -> **주의**: 이 옵션은 **destructive**. 운영자가 명시적으로 지정했을 때만 동작. - -### 15.9 namespace 가 Terminating 에 걸림 - -`mnt` namespace 가 `Terminating` 에서 오래 멈추면 보통 다음 셋 중 하나다. - -- controller 가 이미 사라졌는데 CRD finalizer 가 남아 있음 -- PVC protection finalizer 가 남아 있음 -- namespaced 리소스 일부가 finalizer 때문에 삭제 완료를 못 함 - -> **참고**: 현재 `teardown.sh` 는 이 상황을 고려해 단계적으로 정리한다 — `VaultStaticSecret / VaultAuth / VaultConnection` finalizer 제거 → PVC 보호 finalizer 제거 → 남은 namespaced 리소스 finalizer 일괄 제거 → 마지막에 namespace `/finalize` 호출. - -수동 확인: - -```bash -kubectl get namespace mnt -o yaml -kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl -n mnt get --ignore-not-found -``` - -이미 teardown 을 사용 중이라면 대부분은 스크립트가 자동 처리한다. 수동 개입은 정말 스크립트가 실패했을 때만 한다. - -### 15.10 vault-0 이 `0/1 Running` 에서 멈춤 - -**현상**: - -``` -NAME READY STATUS RESTARTS AGE -vault-0 0/1 Running 0 2m -``` - -계속 `0/1 Running`. `kubectl wait --for=condition=Ready` 가 timeout 으로 실패. - -**원인 — 의도된 동작**: - -Vault 의 readiness probe 는 `/v1/sys/health?sealedcode=503&uninitcode=503` 를 사용한다. 즉: -- **uninitialized** → HTTP 503 → readiness fail -- **sealed** → HTTP 503 → readiness fail -- **initialized + unsealed** → HTTP 200 → Ready - -이건 sealed Vault 가 Service Endpoints 에서 제외되어 트래픽이 흘러가지 않도록 하는 **보안 설계**. 초기화 전에는 구조상 Ready 가 될 수 없다. - -**해결 — `vault-init.sh` 실행**: - -```bash -# Pod 이 Running 이면 exec 가능 → 초기화 실행 가능 -REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-init.sh -``` - -수행되는 것: -1. `vault operator init` — unseal keys + root token 생성 -2. unseal 5 shares 중 3 개로 자동 unseal -3. root login → KV v2 + k8s auth + policy × 2 + role × 2 - -`vault-init.sh` 가 끝나고 몇 초 뒤 Pod 이 자동으로 Ready 로 전환: - -```bash -kubectl -n mnt get pod vault-0 -# vault-0 1/1 Running -``` - -**bootstrap.sh 가 Phase 4 에서 Ready 대기로 실패했을 때 — 재개**: - -```bash -# Phase 4 까지는 apply + Pod Running 완료 상태 -# 남은 Phase 5~8 만 수동 실행 -REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-init.sh # Phase 5 -REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-seed-apps.sh # Phase 6 -REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vso-install.sh # Phase 7 -kubectl apply -k k8s/overlays/dev/vso/ # Phase 8 -``` - -또는 bootstrap.sh 를 그냥 다시 실행해도 된다 (idempotent). - -> **참고 — 다른 Pod 들이 `ContainerCreating` 상태**: `auth-server`, `keycloak`, `identity-postgres`, `docker-registry`, `migration-flyway` 가 `ContainerCreating` 에 머무는 건 **VSO 가 만드는 K8s Secret 이 아직 없어서** volume mount 가 대기 중인 것. Vault 초기화 + 앱 secret 주입 (§7) + VSO sync 가 끝나면 차례로 Running 으로 전환된다. 정상 동작. - -> **참고 — `test-server-*` 가 `ImagePullBackOff`**: `registry.example.com/test-platform/test-server-*:0.1.0` 은 **예시 이미지** 로, 실제 레지스트리에 존재하지 않는다. 사용자가 실제 이미지를 빌드해서 내부 Registry 에 푸시해야 한다. 무시해도 된다. - -### 15.11 `helm upgrade` 가 `has no deployed releases` 로 실패 - -**현상** — bootstrap Phase 7 (VSO Helm) 에서: - -``` -Error: UPGRADE FAILED: "vault-secrets-operator" has no deployed releases -``` - -**원인**: - -이전 `helm upgrade --install` 시도가 `--atomic` 때문에 rollback 되며 릴리즈가 `failed` 또는 `uninstalled` 상태로 남음. Helm 이 metadata 는 보존하는데 실제 배포물은 없는 상태. 이 상태에선 `upgrade --install` 이 **upgrade 로 분기하려다 "deployed release 없음" 으로 실패**. - -**해결**: - -> **참고**: `tasks/vso-install.sh` 는 이제 실행 시 릴리즈 상태를 먼저 검사해서 `failed`/`pending*`/`uninstalling`/`uninstalled` 면 자동으로 `helm uninstall` 을 먼저 수행한다. 또한 `--atomic` 플래그를 제거했다 (실패 시 재실행으로 복구가 더 안전). - -구버전 스크립트로 이미 이 상태에 빠졌다면 수동 정리: - -```bash -helm -n mnt uninstall vault-secrets-operator -# (Error: uninstall: Release not loaded: ... 이 떠도 무시) - -bash k8s/scripts/bin/bootstrap.sh dev -# Phase 7 부터 깔끔하게 재개됨 -``` +## 8. 금지 사항 + +- `gitops/clusters/lab/main/all` 직접 apply +- K3s packaged manifest 직접 수정 +- `vault-init-keys.json` commit +- Secret 값을 shell argv나 문서 예시에 기록 +- 완료된 Flyway Job을 이유 없이 삭제해 같은 release 재실행 +- 데이터 삭제 flag 없이 finalizer 강제 제거 diff --git a/infrastructure/.iac-engine.example b/infrastructure/.iac-engine.example new file mode 100644 index 0000000..e5295e7 --- /dev/null +++ b/infrastructure/.iac-engine.example @@ -0,0 +1,4 @@ +# Copy this file to .iac-engine after choosing one engine. +# Leave exactly one uncommented value in the copied file: +# tofu +# terraform diff --git a/infrastructure/README.md b/infrastructure/README.md new file mode 100644 index 0000000..6ec7860 --- /dev/null +++ b/infrastructure/README.md @@ -0,0 +1,33 @@ +# Infrastructure + +클라우드 또는 온프레미스의 네트워크, identity, DNS, registry와 Kubernetes +cluster 같은 기반 리소스를 코드로 관리합니다. + +```text +components reusable primitive + │ + ├──────────────┐ + ▼ ▼ + stacks live + │ ▲ + └──────────────┘ +``` + +- `components`: 작고 재사용 가능한 구현 단위 +- `stacks`: 반복되는 component 조합(선택) +- `live`: 실제 environment root와 state 경계 +- `tests`: component와 contract 검증 + +IaC 엔진은 프로젝트에서 하나를 선택합니다. Terraform/OpenTofu 호환이 필요하면 +같은 `.tf` 구성을 공유하고 실행 명령만 프로젝트 표준으로 통일합니다. 서로 다른 +엔진용으로 동일한 인프라 트리를 복제하지 않습니다. + +IaC 파일을 추가할 때 `.iac-engine.example`을 `.iac-engine`으로 복사하고 +`tofu` 또는 `terraform` 중 하나만 기록합니다. 이 선택 파일은 커밋합니다. +선택한 실행 파일과 version을 로컬/CI에 고정하고, 프로젝트별 `init -backend=false` +및 semantic validate 단계도 검증 스크립트에 추가합니다. 기본 스켈레톤의 자동 +검사는 provider를 선택하지 않았기 때문에 IaC format까지만 수행합니다. + +Kubernetes API 안의 platform/app 리소스는 원칙적으로 `gitops`가 소유합니다. +클러스터 생성 시 반드시 필요한 최소 bootstrap 출력만 명시적인 contract로 +전달합니다. diff --git a/infrastructure/components/README.md b/infrastructure/components/README.md new file mode 100644 index 0000000..ae66219 --- /dev/null +++ b/infrastructure/components/README.md @@ -0,0 +1,24 @@ +# Infrastructure Components + +작고 응집된 재사용 단위를 둡니다. + +예: + +```text +components/ +├── aws/ +│ ├── network/ +│ ├── identity/ +│ └── eks/ +├── gcp/ +│ ├── network/ +│ └── gke/ +└── shared/ + └── naming/ +``` + +실제로 사용하는 provider 경로만 만듭니다. component에는 environment backend, +실제 credential과 환경 고유 값을 두지 않습니다. 입력, 출력, version constraint, +권한 요구 사항과 사용 예를 component README에 기록합니다. + +새 component는 `_template`을 복사해 시작합니다. diff --git a/infrastructure/components/_template/README.md b/infrastructure/components/_template/README.md new file mode 100644 index 0000000..f32f28b --- /dev/null +++ b/infrastructure/components/_template/README.md @@ -0,0 +1,29 @@ +# __REPLACE_ME_COMPONENT_NAME__ + +## 책임 + +이 component가 생성하고 소유하는 리소스를 적습니다. + +## 입력 + +필수/선택 입력과 민감 정보 여부를 적습니다. + +## 출력 + +다른 component 또는 live root에 제공하는 안정적인 contract를 적습니다. + +## 요구 권한 + +plan/apply에 필요한 최소 provider 권한을 적습니다. + +## 사용 예 + +실제 credential, account ID와 운영 값을 포함하지 않는 호출 예를 적습니다. + +## 구현 체크리스트 + +- [ ] backend를 선언하지 않는다. +- [ ] provider/version constraint를 명시한다. +- [ ] 입력 validation과 민감 output 표시를 추가한다. +- [ ] 환경 이름을 내부에 하드코딩하지 않는다. +- [ ] README와 테스트를 함께 갱신한다. diff --git a/terraform/modules/compute/README.md b/infrastructure/components/compute/README.md similarity index 51% rename from terraform/modules/compute/README.md rename to infrastructure/components/compute/README.md index a087166..55ede95 100644 --- a/terraform/modules/compute/README.md +++ b/infrastructure/components/compute/README.md @@ -1,7 +1,8 @@ -# compute Terraform Module +# compute IaC Component Contract: - Owns reusable compute resources only. - Must expose inputs and outputs before an environment depends on it. - Must not contain environment-specific values. -- Replace this contract with Terraform files when the module is implemented. +- This is a non-runnable placeholder. Replace it with files for the selected + Terraform/OpenTofu-compatible engine when implemented. diff --git a/terraform/modules/database/README.md b/infrastructure/components/database/README.md similarity index 51% rename from terraform/modules/database/README.md rename to infrastructure/components/database/README.md index 2b18cd1..088c04b 100644 --- a/terraform/modules/database/README.md +++ b/infrastructure/components/database/README.md @@ -1,7 +1,8 @@ -# database Terraform Module +# database IaC Component Contract: - Owns reusable database resources only. - Must expose inputs and outputs before an environment depends on it. - Must not contain environment-specific values. -- Replace this contract with Terraform files when the module is implemented. +- This is a non-runnable placeholder. Replace it with files for the selected + Terraform/OpenTofu-compatible engine when implemented. diff --git a/terraform/modules/networking/README.md b/infrastructure/components/networking/README.md similarity index 51% rename from terraform/modules/networking/README.md rename to infrastructure/components/networking/README.md index af06819..85952c7 100644 --- a/terraform/modules/networking/README.md +++ b/infrastructure/components/networking/README.md @@ -1,7 +1,8 @@ -# networking Terraform Module +# networking IaC Component Contract: - Owns reusable networking resources only. - Must expose inputs and outputs before an environment depends on it. - Must not contain environment-specific values. -- Replace this contract with Terraform files when the module is implemented. +- This is a non-runnable placeholder. Replace it with files for the selected + Terraform/OpenTofu-compatible engine when implemented. diff --git a/terraform/modules/storage/README.md b/infrastructure/components/storage/README.md similarity index 51% rename from terraform/modules/storage/README.md rename to infrastructure/components/storage/README.md index ab65035..14401cb 100644 --- a/terraform/modules/storage/README.md +++ b/infrastructure/components/storage/README.md @@ -1,7 +1,8 @@ -# storage Terraform Module +# storage IaC Component Contract: - Owns reusable storage resources only. - Must expose inputs and outputs before an environment depends on it. - Must not contain environment-specific values. -- Replace this contract with Terraform files when the module is implemented. +- This is a non-runnable placeholder. Replace it with files for the selected + Terraform/OpenTofu-compatible engine when implemented. diff --git a/infrastructure/live/README.md b/infrastructure/live/README.md new file mode 100644 index 0000000..25bd3f6 --- /dev/null +++ b/infrastructure/live/README.md @@ -0,0 +1,38 @@ +# Live Infrastructure + +실제로 plan/apply하는 root를 둡니다. leaf 디렉터리 하나가 독립적인 state, +locking, 권한과 실패 범위입니다. + +소규모 예: + +```text +live/ +└── dev/ + ├── network/ + └── cluster/ +``` + +확장 예: + +```text +live/ +└── aws/ + └── platform-prod/ + └── ap-northeast-2/ + └── prod/ + ├── network/ + ├── shared-services/ + └── cluster-a/ +``` + +경로 깊이보다 leaf의 실행 계약이 중요합니다. 자동화는 특정 depth를 가정하지 +말고 IaC root marker를 기준으로 대상을 찾습니다. + +## 규칙 + +- 각 root는 remote backend와 locking을 사용합니다. +- production과 non-production state/credential을 분리합니다. +- 민감하지 않은 입력만 커밋하며 secret 입력은 runtime에 주입합니다. +- 다른 환경 경로를 상대 import하지 않습니다. +- provider, module과 component version을 고정합니다. +- output consumer와 파괴 영향 범위를 README에 기록합니다. diff --git a/infrastructure/live/_template/README.md b/infrastructure/live/_template/README.md new file mode 100644 index 0000000..66d0424 --- /dev/null +++ b/infrastructure/live/_template/README.md @@ -0,0 +1,34 @@ +# __REPLACE_ME_LIVE_ROOT_NAME__ + +## 대상 + +- Provider/account/project: __REPLACE_ME_SCOPE__ +- Region: __REPLACE_ME_REGION_OR_GLOBAL__ +- Environment: __REPLACE_ME_ENVIRONMENT__ +- Stack: __REPLACE_ME_STACK__ +- Owner: __REPLACE_ME_OWNER__ + +## State + +- Backend: __REPLACE_ME_BACKEND__ +- Locking: +- Encryption: +- Recovery runbook: + +## 의존성 + +선행 state/output과 사용하는 component/stack version을 적습니다. + +## Output Contract + +downstream bootstrap 또는 system에 전달하는 값을 적습니다. 민감 output은 +명시적으로 표시하고 로그에 출력하지 않습니다. + +## 실행 + +프로젝트가 선택한 IaC 엔진의 init/plan/apply 절차를 적습니다. production은 +검토된 plan, 승인과 concurrency lock 없이 적용하지 않습니다. + +## 롤백/복구 + +되돌릴 수 있는 변경과 state 복구 절차 링크를 적습니다. diff --git a/infrastructure/live/dev/cluster/README.md b/infrastructure/live/dev/cluster/README.md new file mode 100644 index 0000000..ed92570 --- /dev/null +++ b/infrastructure/live/dev/cluster/README.md @@ -0,0 +1,8 @@ +# dev IaC Live Root + +Contract: +- Reserves the dev-specific IaC root and state boundary. +- Reuses components or stacks from `../../../components` and `../../../stacks`. +- Must not define reusable infrastructure patterns that belong in components or stacks. +- This is a non-runnable placeholder. Add entry files only after selecting and + recording the Terraform/OpenTofu-compatible engine. diff --git a/infrastructure/live/prod/cluster/README.md b/infrastructure/live/prod/cluster/README.md new file mode 100644 index 0000000..b299c53 --- /dev/null +++ b/infrastructure/live/prod/cluster/README.md @@ -0,0 +1,8 @@ +# prod IaC Live Root + +Contract: +- Reserves the prod-specific IaC root and state boundary. +- Reuses components or stacks from `../../../components` and `../../../stacks`. +- Must not define reusable infrastructure patterns that belong in components or stacks. +- This is a non-runnable placeholder. Add entry files only after selecting and + recording the Terraform/OpenTofu-compatible engine. diff --git a/infrastructure/live/staging/cluster/README.md b/infrastructure/live/staging/cluster/README.md new file mode 100644 index 0000000..ded77a9 --- /dev/null +++ b/infrastructure/live/staging/cluster/README.md @@ -0,0 +1,8 @@ +# staging IaC Live Root + +Contract: +- Reserves the staging-specific IaC root and state boundary. +- Reuses components or stacks from `../../../components` and `../../../stacks`. +- Must not define reusable infrastructure patterns that belong in components or stacks. +- This is a non-runnable placeholder. Add entry files only after selecting and + recording the Terraform/OpenTofu-compatible engine. diff --git a/infrastructure/stacks/README.md b/infrastructure/stacks/README.md new file mode 100644 index 0000000..5edda95 --- /dev/null +++ b/infrastructure/stacks/README.md @@ -0,0 +1,13 @@ +# Infrastructure Stacks + +여러 component 조합이 두 개 이상의 live root에서 반복될 때 사용하는 선택 계층입니다. + +예: + +- `regional-foundation`: network + shared identity + DNS +- `cluster`: Kubernetes cluster + node pools + workload identity +- `edge`: CDN + load balancer + certificate + +작은 프로젝트에서는 `live`가 component를 직접 호출하고 이 계층을 생략합니다. +stack은 실행 가능한 environment root가 아니므로 backend와 실제 credential을 +두지 않습니다. stack 중첩을 깊게 만들기보다 live root에서 평평하게 조합합니다. diff --git a/infrastructure/stacks/_template/README.md b/infrastructure/stacks/_template/README.md new file mode 100644 index 0000000..ea17743 --- /dev/null +++ b/infrastructure/stacks/_template/README.md @@ -0,0 +1,19 @@ +# __REPLACE_ME_STACK_NAME__ + +## 목적 + +반복해서 함께 배포하는 component 조합을 설명합니다. + +## 포함 Component + +각 component의 version과 책임을 적습니다. + +## 입력과 출력 + +live root에 노출하는 최소 interface를 적습니다. + +## 제약 + +- backend와 environment credential을 선언하지 않습니다. +- 특정 live 경로를 역참조하지 않습니다. +- component가 한 번만 필요하면 stack 계층을 만들지 않습니다. diff --git a/infrastructure/tests/README.md b/infrastructure/tests/README.md new file mode 100644 index 0000000..7084935 --- /dev/null +++ b/infrastructure/tests/README.md @@ -0,0 +1,13 @@ +# Infrastructure Tests + +IaC를 선택한 뒤 다음 검증을 필요에 따라 추가합니다. + +- formatter와 syntax/validate +- component input/output contract test +- policy/static analysis +- ephemeral account/project integration test +- upgrade와 state migration test + +실제 cloud 통합 테스트는 일반 PR 검증과 분리하고, 짧은 수명의 identity와 +격리된 account/project를 사용합니다. 테스트가 production state를 읽거나 +변경해서는 안 됩니다. diff --git a/k8s/AGENTS.md b/k8s/AGENTS.md deleted file mode 100644 index 6e1ffdd..0000000 --- a/k8s/AGENTS.md +++ /dev/null @@ -1,26 +0,0 @@ -# k8s AGENTS - -Role: -- own Kubernetes/K3s source-of-truth manifests and Kustomize composition -- keep base resources, environment overlays, helper scripts, and Vault Secrets Operator assets separate -- make render / validate / diff / apply possible from Git without relying on live cluster state - -Scope: -- `base/`: environment-neutral bases -- `overlays/<env>/`: environment-specific composition -- `scripts/`: helper automation -- `vso/`: Vault Secrets Operator assets - -Read first: -- `/docs/standards/infra/architecture-environments.md` -- `/docs/standards/infra/kustomize.md` -- `/docs/standards/infra/k3s-specific.md` -- `/docs/standards/infra/operations-runbook-upgrade-rollback.md` - -Rules: -- prefer `kubectl kustomize`, `kubectl diff -k`, and `kubectl apply -k` -- do not use server-local manifests as source of truth -- do not place environment differences in `base/` -- do not place base resource definitions directly in overlays unless the resource is environment-only by design -- do not commit production secret values -- keep scripts as helpers; manifests remain declarative source diff --git a/k8s/README.md b/k8s/README.md deleted file mode 100644 index d6bb049..0000000 --- a/k8s/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Kubernetes Packaging - -This tree is organized for many independently owned workloads. - -Entry points: -- `overlays/<env>/` renders the whole environment skeleton. -- `overlays/<env>/<domain>/` renders one namespace/domain boundary. -- `overlays/<env>/managing/vault-secrets-operator/` is applied after the VSO CRDs are installed. -- `scripts/env/` contains environment bootstrap and teardown orchestration. -- `scripts/` contains imperative bootstrap and break-glass operations only. -- `vso/` contains Vault Secrets Operator installation lifecycle files. - -Scale rules: -- Do not place hundreds of workloads directly under one package. -- Add workloads under `base/app/units/<unit>/<domain>/<workload-kind>/<workload>/`. -- Add matching environment overrides under `overlays/<env>/app/units/<unit>/<domain>/<workload-kind>/<workload>/` only when that workload has environment-specific differences. -- A workload package owns its Kubernetes object files together, such as `deployment.yaml`, `service.yaml`, `configmap.yaml`, `cronjob.yaml`, `statefulset.yaml`, or `pvc.yaml`. -- Do not create kind-based package roots such as `deployments/`, `services/`, or `configmaps/`. - -Example for a 1000-workload organization: - -```text -base/app/units/ -├── commerce/ -│ ├── checkout/ -│ │ ├── services/order-api/ -│ │ ├── services/payment-api/ -│ │ ├── workers/payment-settlement-worker/ -│ │ ├── schedulers/cart-expiry-scheduler/ -│ │ └── stateful/orders-postgres/ -│ └── catalog/ -│ ├── services/catalog-api/ -│ ├── workers/search-index-worker/ -│ └── jobs/catalog-backfill-job/ -├── identity/ -│ ├── auth/ -│ │ ├── services/auth-api/ -│ │ ├── services/token-api/ -│ │ └── schedulers/token-cleanup-scheduler/ -│ └── profile/ -│ ├── services/profile-api/ -│ └── workers/profile-event-worker/ -├── media/ -│ ├── playback/ -│ │ ├── services/playback-api/ -│ │ └── workers/session-event-worker/ -│ └── recommendation/ -│ ├── services/recommendation-api/ -│ ├── workers/model-feature-worker/ -│ └── jobs/model-refresh-job/ -└── data/ - ├── ingestion/ - │ ├── workers/event-ingest-worker/ - │ └── stateful/ingest-kafka/ - └── analytics/ - ├── schedulers/daily-report-scheduler/ - └── jobs/monthly-rollup-job/ -``` diff --git a/k8s/base/AGENTS.md b/k8s/base/AGENTS.md deleted file mode 100644 index 6bbff16..0000000 --- a/k8s/base/AGENTS.md +++ /dev/null @@ -1,38 +0,0 @@ -# k8s/base AGENTS - -Role: -- own environment-neutral Kustomize base resources -- define reusable workload, namespace, service, storage, policy, and platform/plugin shapes -- keep environment-specific values out of base - -Scope: -- `app/`: application-facing base units -- `managing/`: management and operational base units -- `plugins/`: platform/plugin base resources - -Allowed: -- shared labels/selectors -- common workload shape -- common probe/resource shape -- common service/storage/policy shape -- unit composition through nested `kustomization.yaml` - -Forbidden: -- environment-specific hostnames -- production-only replicas/resources -- environment-specific secret values -- direct references to a specific cluster context -- overlay-only patches masquerading as base manifests - -Read first: -- `/docs/standards/infra/kustomize.md` -- `/docs/standards/infra/architecture-environments.md` -- `/docs/standards/infra/workload-selection.md` -- `/docs/standards/infra/config-and-secrets.md` -- `/docs/standards/infra/security-hardening.md` - -Rules: -- base must be reusable by dev, staging, and prod overlays -- base may define default shape, but overlays own environment differences -- unit ownership should be visible in path names -- large fleets should stay navigable by role first, then domain/unit diff --git a/k8s/base/README.md b/k8s/base/README.md deleted file mode 100644 index 5b2a3c3..0000000 --- a/k8s/base/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Base Packages - -Base packages contain shared Kubernetes definitions. - -Boundaries: -- `managing/` owns infrastructure management resources in the `mnt` namespace. -- `app/` owns application namespace resources and large-scale workload package contracts. -- `plugins/` owns plugin namespace resources. - -Rules: -- Base packages must not contain environment-specific values. -- A base package can be rendered with `kubectl kustomize` through its own `kustomization.yaml`. -- Parent packages compose child packages; child packages own their internal Kubernetes object files. diff --git a/k8s/base/app/AGENTS.md b/k8s/base/app/AGENTS.md deleted file mode 100644 index 9cc9956..0000000 --- a/k8s/base/app/AGENTS.md +++ /dev/null @@ -1,69 +0,0 @@ -# k8s/base/app AGENTS - -Role: -- own application-facing infrastructure base units -- define environment-neutral app workload, service, config reference, secret reference, storage, network policy, probe, and resource shapes -- organize many services by domain/service path directly under `k8s/base/app/<domain>/` - -Current structure: -- domains sit directly under `k8s/base/app/` (`identity/`, `storage/`, `test/`) -- inside each domain, separate by workload role (`stateful/`, `stateless/`) -- example: `identity/auth/stateful/identity-postgres/`, `identity/auth/stateless/auth-server/` - -Allowed: -- Kustomize base edits for application units -- Deployment / StatefulSet / Job / CronJob base resources -- Service / NetworkPolicy / PDB / HPA base resources -- ConfigMap / Secret reference wiring without secret values -- probe / resource / PVC / StorageClass reference shape -- component-specific manifests aligned with standards - -Forbidden: -- environment-specific values that belong in `k8s/overlays/<env>` -- editing packaged K3s component manifests directly -- treating server-local manifests as source of truth -- embedding production secret values in Git -- generating opaque YAML via script as the primary ownership path -- merging unrelated app rollout + migration + cluster upgrade into one hidden change - -Read first: -- `/docs/standards/infra/architecture-environments.md` -- `/docs/standards/infra/kustomize.md` -- `/docs/standards/infra/config-and-secrets.md` -- `/docs/standards/infra/workload-selection.md` -- `/docs/standards/infra/storage-pvc.md` -- `/docs/standards/infra/network-ingress-tls.md` -- `/docs/standards/infra/resources-probes-availability.md` -- `/docs/standards/infra/security-hardening.md` -- `/docs/standards/infra/observability-health.md` -- `/docs/standards/infra/db-and-migration.md` - -Component-specific routing: -- paths containing `keycloak` -> `/docs/standards/infra/keycloak.md` -- paths containing `vault` -> `/docs/standards/infra/vault.md` -- paths containing `minio` -> `/docs/standards/infra/minio.md` -- paths containing `flyway` or migration jobs -> `/docs/standards/infra/flyway.md` - -Examples: -- `/docs/examples/infra/kustomize.md` -- `/docs/examples/infra/config-and-secrets.md` -- `/docs/examples/infra/workload-selection.md` -- `/docs/examples/infra/storage-pvc.md` -- `/docs/examples/infra/network-ingress-tls.md` -- `/docs/examples/infra/resources-probes-availability.md` -- `/docs/examples/infra/security-hardening.md` -- `/docs/examples/infra/observability-health.md` -- `/docs/examples/infra/db-and-migration.md` -- `/docs/examples/infra/keycloak.md` -- `/docs/examples/infra/vault.md` -- `/docs/examples/infra/minio.md` -- `/docs/examples/infra/flyway.md` - -Rules: -- base must stay environment-neutral -- app services default to `ClusterIP` -- public exposure must be explicit and added through overlays/ingress policy -- health/metrics/admin endpoints stay non-public by default -- DB migration must stay separate from app startup -- stateful workloads must have explicit storage and restore reasoning -- domain/unit nesting should make ownership clear for large service counts diff --git a/k8s/base/app/README.md b/k8s/base/app/README.md deleted file mode 100644 index 6e62236..0000000 --- a/k8s/base/app/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Application Base - -This package owns the `app` namespace and the contract for application workloads. - -For large scale, workloads are grouped by unit, domain, workload kind, and workload: - -```text -base/app/units/<unit>/<domain>/<workload-kind>/<workload>/ -``` - -Examples: -- `base/app/units/commerce/checkout/services/order-api/` -- `base/app/units/commerce/checkout/workers/payment-settlement-worker/` -- `base/app/units/identity/auth/schedulers/token-cleanup-scheduler/` -- `base/app/units/data/storage/stateful/orders-postgres/` - -Rules: -- The unit folder is the broad ownership boundary for a business unit, platform area, or organization. -- The domain folder is the bounded context inside a unit. -- The workload-kind folder groups similar operating models inside one domain. -- The workload folder is the smallest independently deployable package. -- Kubernetes object files stay together inside the workload package. -- Shared namespace-level objects go in `shared/`. diff --git a/k8s/base/managing/AGENTS.md b/k8s/base/managing/AGENTS.md deleted file mode 100644 index ca48bda..0000000 --- a/k8s/base/managing/AGENTS.md +++ /dev/null @@ -1,46 +0,0 @@ -# k8s/base/managing AGENTS - -Role: -- own management and operational Kubernetes base units -- model bootstrap, migration, backup, restore, maintenance, and admin workloads as declarative Kustomize bases -- keep operational workloads separate from long-running application serving workloads - -Allowed: -- Job / CronJob base resources for operational tasks -- maintenance ServiceAccount / RBAC / ConfigMap / Secret reference wiring -- backup / restore / migration helper workload shapes -- admin-only service shapes when explicitly justified - -Forbidden: -- long-running product application workloads -- environment-specific values that belong in `k8s/overlays/<env>` -- scripts becoming the primary source of YAML truth -- large heredoc-generated manifests as the default path -- embedding production secret values -- hiding environment differences in shell conditionals instead of overlays -- giant all-in-one jobs that mix unrelated concerns - -Read first: -- `/docs/standards/infra/workload-selection.md` -- `/docs/standards/infra/db-and-migration.md` -- `/docs/standards/infra/flyway.md` -- `/docs/standards/infra/backup-restore.md` -- `/docs/standards/infra/operations-runbook-upgrade-rollback.md` -- `/docs/standards/infra/config-and-secrets.md` -- `/docs/standards/infra/security-hardening.md` -- `/docs/standards/infra/kustomize.md` - -Examples: -- `/docs/examples/infra/flyway.md` -- `/docs/examples/infra/backup-restore.md` -- `/docs/examples/infra/operations-runbook-upgrade-rollback.md` -- `/docs/examples/infra/db-and-migration.md` -- `/docs/examples/infra/kustomize.md` -- `/docs/examples/infra/scripts.md` - -Rules: -- management jobs are explicit operational units, not hidden app startup hooks -- migrations must stay separate from app startup -- backup and restore paths must be documented before risky stateful changes -- destructive operations require explicit opt-in and runbook backing -- operational workloads must still follow security, resource, secret, and namespace standards diff --git a/k8s/overlays/README.md b/k8s/overlays/README.md deleted file mode 100644 index b54e740..0000000 --- a/k8s/overlays/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Environment Overlays - -Overlays contain environment-specific differences. - -Required shape: - -```text -overlays/<env>/ -├── kustomization.yaml -├── app/ -├── managing/ -└── plugins/ -``` - -Rules: -- Overlay packages reuse base packages. -- Overlay packages contain patches, generated config, or environment-specific resources only. -- Do not duplicate base package composition in overlays. -- A workload override belongs at `overlays/<env>/app/units/<unit>/<domain>/<workload-kind>/<workload>/`. diff --git a/k8s/overlays/dev/kustomization.yaml b/k8s/overlays/dev/kustomization.yaml deleted file mode 100644 index d5d9192..0000000 --- a/k8s/overlays/dev/kustomization.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: mnt - -resources: - - ../../base/managing - - networkpolicy-baseline.yaml - - vault - - registry - - database - - auth - - keycloak - - storage - - keycloak-realm - -components: - - ../../components/forward-auth diff --git a/k8s/scripts/README.md b/k8s/scripts/README.md deleted file mode 100644 index 8952393..0000000 --- a/k8s/scripts/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Kubernetes Scripts - -Imperative bootstrap, teardown, and break-glass operations for the `mnt` infra -stack. 선언적 관리(Service/Job/DB rollout 등)는 Kustomize 패키지의 몫이고, -여기 스크립트는 **phase orchestration** 과 **one-shot imperative setup** -(Vault init / unseal, Helm install) 만 담당한다. - -## Layout - -``` -scripts/ -├── bin/ # 사용자가 직접 실행하는 엔트리 포인트 (main "$@") -│ ├── bootstrap.sh # 전체 스택 부트스트랩 -│ └── teardown.sh # 전체 스택 제거 -├── tasks/ # bin/ 이 호출하는 단일 책임 태스크 -│ ├── minio-operator-install.sh -│ ├── vault-init.sh # Vault init + unseal + k8s auth + policy/role -│ ├── vault-seed-apps.sh # 앱 시크릿 5 개 Vault KV 에 seed -│ ├── vault-setup-admin.sh -│ └── vso-install.sh # VSO Helm upgrade --install -├── lib/ # 공유 쉘 라이브러리 -│ ├── common.sh # log/die, retry, confirm, kube-context 가드, finalizer 헬퍼 -│ └── vault.sh # vault_exec 래퍼, 로그인/unseal 유틸 -└── ci/ - └── validate.sh # kustomize build + kubeconform + kube-linter + shellcheck + shfmt -``` - -## Entry points - -```bash -# 부트스트랩 (대화형, bash history 안전) -bash k8s/scripts/bin/bootstrap.sh <dev|staging|prod> - -# 제거 (정상 경로) -bash k8s/scripts/bin/teardown.sh <dev|staging|prod> -CONFIRM=yes bash k8s/scripts/bin/teardown.sh <dev|staging|prod> - -# 로컬/CI 품질 게이트 -bash k8s/scripts/ci/validate.sh -``` - -## 필수 환경 변수 (안전 가드) - -쉘 rc 에 한 번 선언: - -```bash -export KUBE_CONTEXT_DEV="homelab" -export KUBE_CONTEXT_STAGING="staging-cluster" -export KUBE_CONTEXT_PROD="prod-cluster" -``` - -- `bin/bootstrap.sh` 와 `bin/teardown.sh` 는 `ENV_NAME` 에 대응하는 - `KUBE_CONTEXT_<ENV>` 와 `kubectl config current-context` 가 일치하는지 - **실행 전 검증**한다. 매핑이 없으면 대화형으로 현재 context 이름 재입력을 - 요구한다. -- `env=prod` destructive 작업은 `ALLOW_PROD_DESTRUCTIVE=yes` 와 namespace - 이름 재입력 TTY 확인이 추가로 필요하다. - -## 파괴적 플래그 - -| 플래그 | 범위 | 효과 | -| --- | --- | --- | -| `CONFIRM=yes` | 두 엔트리 | 대화형 확인 프롬프트 자동 승인 (CI) | -| `SKIP_DIFF=yes` | bootstrap | Phase 3 의 `kubectl diff` preview 생략 (비권장) | -| `RESET_STALE_SECRETS=yes` | bootstrap | VSO-managed K8s Secret 을 선제 삭제 (Vault 값 재주입 유도) | -| `VAULT_KEYS_FILE=<path>` | bootstrap | Vault init key 저장 위치. **env=prod 는 repo 바깥 경로 필수** | -| `FORCE_FINALIZERS=yes` | teardown | Phase 6 활성 — finalizer 강제 제거 + `/finalize` API. Terminating 복구 외 금지 | -| `ALLOW_PROD_DESTRUCTIVE=yes` | teardown | env=prod teardown 허용 (namespace 재입력 추가 확인 필요) | -| `TEARDOWN_VSO_OPERATOR=yes` | teardown | **클러스터 공용** VSO Helm 릴리즈 + 전용 namespace + `vault-tokenreview-binding` 제거. 다른 env 의 Vault/VSO 가 함께 멈춤 | -| `TEARDOWN_VSO_CRDS=yes` | teardown | **클러스터 공용** VSO CRD + ClusterRole/CRB/Webhook 제거. 모든 env 의 VaultStaticSecret 인스턴스가 삭제됨 | -| `TEARDOWN_MINIO_OPERATOR=yes` | teardown | MinIO Operator Helm 릴리즈까지 제거 | - -## Rules - -- 서비스 단위 스크립트를 새로 만들지 않는다. -- Service / Job / Scheduler / DB rollout 은 Kustomize 패키지에서 선언. -- 스크립트는 phase orchestration 이지 Kubernetes 리소스 스펙의 출처가 아니다. -- 비트리비얼 스크립트는 `functions + main "$@"` 구조를 따른다. -- 모든 `.sh` 는 `ci/validate.sh` 의 `shellcheck -S style` + `shfmt -i 2 -bn -ci` - 를 통과해야 한다. diff --git a/k8s/scripts/bin/bootstrap.sh b/k8s/scripts/bin/bootstrap.sh deleted file mode 100755 index 99dc65e..0000000 --- a/k8s/scripts/bin/bootstrap.sh +++ /dev/null @@ -1,346 +0,0 @@ -#!/usr/bin/env bash -# Entry point: bootstrap the full infra stack for a given environment. -# -# Phases: -# 0. MinIO Operator Helm install (tasks/minio-operator-install.sh) -# 1. Apply namespace + PSS labels (kubectl apply -k base/managing/namespace) -# 2. (optional) Reset stale K8s Secrets (RESET_STALE_SECRETS=yes 일 때만) -# 2.5 VSO CRD 선행 설치 (VaultStaticSecret CR 가 overlay 에 포함돼 있어) -# 2.6 cert-manager 선행 설치 (ClusterIssuer / Certificate CR 가 overlay 에 포함돼 있어) -# 2.7 Keycloak Operator 선행 설치 (Keycloak / KeycloakRealmImport CR 가 overlay 에 포함돼 있어) -# 2.8 Traefik HelmChartConfig + Middleware/TLSOption (kube-system 에 배치 — root overlay 의 `namespace: mnt` 와 충돌하므로 별도 apply) -# 3. Render + diff + confirm + apply overlay (--server-side --field-manager=project-infra-bootstrap) -# 4. Wait for vault-0 Running -# 5. Vault init + unseal + KV + k8s auth + policy / role (tasks/vault-init.sh) -# 6. Seed application secrets (tasks/vault-seed-apps.sh) -# 7. Helm install VSO (tasks/vso-install.sh) -# 8. Apply VSO CRs (kubectl apply -k overlays/<env>/vso/) -# 9. MinIO docker-registry bucket/user/policy 프로비저닝 (tasks/minio-provision-registry.sh) -# -# 대화형 실행 (bash history 에 비밀번호 남지 않음): -# bash k8s/scripts/bin/bootstrap.sh dev -# -# Required context (안전 가드): -# KUBE_CONTEXT_DEV / KUBE_CONTEXT_STAGING / KUBE_CONTEXT_PROD -# 또는 KUBE_CONTEXT= 로 명시. 쉘 rc 에 선언해 두면 현재 kubectl context -# 와 일치 여부를 자동 검증한다. 매핑이 없으면 대화형으로 context 이름 -# 재입력을 요구. -# -# Optional env: -# RESET_STALE_SECRETS=yes 이미 있는 VSO-managed K8s Secret 을 삭제 후 재생성 -# VAULT_KEYS_FILE=<path> env=prod 필수 — repo 바깥 경로 -# SKIP_DIFF=yes Phase 3 의 kubectl diff preview 생략 -# CONFIRM=yes 대화형 프롬프트 자동 승인 (CI) - -set -Eeuo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=../lib/common.sh -. "$SCRIPT_DIR/../lib/common.sh" - -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -K8S_ROOT="$REPO_ROOT/k8s" -TASKS_DIR="$SCRIPT_DIR/../tasks" -export REPO_ROOT - -NAMESPACE="mnt" -FIELD_MANAGER="project-infra-bootstrap" - -# VSO 가 관리하는 K8s Secret 이름 목록 (기존 찌꺼기 감지용) -VSO_MANAGED_SECRETS=( - identity-postgres-superuser - keycloak-db - keycloak-db-operator - auth-server-db - keycloak-bootstrap-admin - keycloak-bootstrap-admin-operator - keycloak-client-auth-server-ingress - minio-tenant-env - docker-registry-minio -) - -usage() { - cat >&2 <<EOF -Usage: bin/bootstrap.sh <dev|staging|prod> - -Required env (권장 — 쉘 rc 에 선언): - KUBE_CONTEXT_DEV / KUBE_CONTEXT_STAGING / KUBE_CONTEXT_PROD env → kubectl context 매핑 - (또는 KUBE_CONTEXT=<name> 로 한 번만 overrides) - -Options: - RESET_STALE_SECRETS=yes 이미 존재하는 VSO-managed Secret 을 삭제 후 재생성 - VAULT_KEYS_FILE=<path> env=prod 필수 — repo 바깥 경로 - SKIP_DIFF=yes Phase 3 diff preview 생략 (비권장) - CONFIRM=yes 대화형 확인 자동 승인 (CI) -EOF - exit 1 -} - -precheck_namespace() { - if ! ns_exists "$NAMESPACE"; then - log "Precheck: namespace '$NAMESPACE' 없음 — 신규 생성 흐름" - return 0 - fi - local phase - phase="$(ns_phase "$NAMESPACE")" - if [[ "$phase" == "Terminating" ]]; then - err "namespace '$NAMESPACE' 가 Terminating 상태입니다." - err "이전 teardown 이 완료되지 않아 새 리소스 생성이 불가능합니다." - err "" - err "복구 절차:" - err " 1) 이전 teardown 을 마저 끝내기:" - err " CONFIRM=yes bash k8s/scripts/bin/teardown.sh $ENV_NAME" - err " 2) namespace 가 완전히 사라진 걸 확인한 뒤 bootstrap 재실행:" - err " kubectl get namespace $NAMESPACE" - err " bash k8s/scripts/bin/bootstrap.sh $ENV_NAME" - die "bootstrap 중단" - fi - log "Precheck: namespace '$NAMESPACE' phase=$phase — 계속 진행" -} - -phase0_minio_operator() { - log "[0/9] MinIO Operator 설치 (Tenant CRD 선행)" - bash "$TASKS_DIR/minio-operator-install.sh" -} - -phase1_namespace() { - log "[1/9] Namespace + PSS 라벨 선행 apply" - kubectl apply -k "$K8S_ROOT/base/managing/namespace" \ - --server-side --field-manager="$FIELD_MANAGER" - retry 5 1 kubectl get namespace "$NAMESPACE" >/dev/null -} - -phase2_reset_stale_secrets() { - log "[2/9] 기존 VSO-managed K8s Secret 점검" - local stale_found=0 s - for s in "${VSO_MANAGED_SECRETS[@]}"; do - if kubectl -n "$NAMESPACE" get secret "$s" >/dev/null 2>&1; then - stale_found=$((stale_found + 1)) - if [[ "${RESET_STALE_SECRETS:-}" == "yes" ]]; then - log " 삭제: $s (RESET_STALE_SECRETS=yes)" - kubectl -n "$NAMESPACE" delete secret "$s" --ignore-not-found - else - warn " $s 이미 존재 (VSO 가 덮어쓰지 않음). Vault 값 반영이 필요하면 RESET_STALE_SECRETS=yes 로 재실행하거나 수동 삭제하세요." - fi - fi - done - (( stale_found == 0 )) && log " 기존 Secret 없음" -} - -phase2_5_vso_crds() { - # overlays/<env>/{database,keycloak,storage}/vault-secrets.yaml 에 VaultStaticSecret - # CR 들이 포함돼 있어, Phase 3 overlay apply 시점에 CRD 가 없으면 "resource mapping - # not found" 로 실패. Helm 차트 install 은 Phase 7 이므로 CRD 만 선행 적용. - log "[2.5/9] VSO CRD 선행 설치" - local version="${VSO_VERSION:-0.9.0}" - helm repo add hashicorp https://helm.releases.hashicorp.com >/dev/null 2>&1 || true - helm repo update hashicorp >/dev/null - helm show crds hashicorp/vault-secrets-operator --version "$version" \ - | kubectl apply -f - --server-side --field-manager="$FIELD_MANAGER" -} - -# cert-manager 전체 (CRD + namespace + controller + webhook) 을 overlay 밖에서 -# 선행 설치. 이후 phase 3 에서 overlay 가 참조하는 ClusterIssuer / Certificate CR -# 이 등록될 수 있다. CRD established 대기를 반드시 건다 — deploy/webhook 가 -# Ready 되기 전에 Certificate CR apply 시 admission webhook 이 거부함. -phase2_6_cert_manager() { - log "[2.6/9] cert-manager 선행 설치" - kubectl apply -k "$K8S_ROOT/overlays/$ENV_NAME/platform/cert-manager" \ - --server-side --field-manager="$FIELD_MANAGER" - - log " cert-manager CRD established 대기" - retry 30 2 kubectl wait --for=condition=Established --timeout=10s \ - crd/clusterissuers.cert-manager.io \ - crd/certificates.cert-manager.io \ - crd/certificaterequests.cert-manager.io \ - crd/orders.acme.cert-manager.io \ - crd/challenges.acme.cert-manager.io - - log " cert-manager Deployment Available 대기" - retry 30 5 kubectl -n cert-manager wait --for=condition=Available --timeout=10s \ - deploy/cert-manager \ - deploy/cert-manager-webhook \ - deploy/cert-manager-cainjector -} - -# Keycloak Operator (CRDs + Operator Deployment) 선행 설치. Keycloak / -# KeycloakRealmImport CR 이 phase 3 에 포함되므로 CRD 등록이 먼저 되어야 한다. -phase2_7_keycloak_operator() { - log "[2.7/9] Keycloak Operator 선행 설치" - kubectl apply -k "$K8S_ROOT/overlays/$ENV_NAME/platform/keycloak-operator" \ - --server-side --field-manager="$FIELD_MANAGER" - - log " Keycloak Operator CRD established 대기" - retry 30 2 kubectl wait --for=condition=Established --timeout=10s \ - crd/keycloaks.k8s.keycloak.org \ - crd/keycloakrealmimports.k8s.keycloak.org - - log " Keycloak Operator Deployment Available 대기" - retry 60 5 kubectl -n "$NAMESPACE" wait --for=condition=Available --timeout=10s \ - deploy/keycloak-operator -} - -# kube-system Traefik 커스터마이징 — HelmChartConfig (K3s Helm-controller 가 -# 재수렴), Middleware (https-redirect / security-headers), TLSOption (modern-tls). -# root overlay 가 `namespace: mnt` 로 전역 주입하므로 kube-system 타깃 리소스는 -# 이 overlay 빌드에 포함시키지 않고 별도 apply 한다. -phase2_8_traefik() { - log "[2.8/9] Traefik HelmChartConfig + Middleware 적용 (kube-system)" - kubectl apply -k "$K8S_ROOT/overlays/$ENV_NAME/platform/traefik" \ - --server-side --field-manager="$FIELD_MANAGER" -} - -# render → server-side dry-run → diff → confirm → apply -# -# kubectl diff exit codes (GNU man page): -# 0 — 변경 없음 -# 1 — 변경 있음 (정상) -# >1 — 실행 오류 (RBAC / API 연결 / invalid manifest 등) -# 이전 구현은 `|| true` 로 모든 비-0 을 흡수해서 에러가 apply 까지 흘러갔다. -phase3_overlay_apply() { - log "[3/9] 인프라 overlay 배포 ($OVERLAY_DIR)" - - local tmpdir - tmpdir="$(mktemp -d -t project-infra-bootstrap.XXXXXX)" - trap_cleanup_path "$tmpdir" - - local rendered="$tmpdir/rendered.yaml" - kustomize build "$OVERLAY_DIR" > "$rendered" - log " render 완료: $(grep -c '^kind:' "$rendered") resources" - - # --- server-side dry-run: admission / RBAC / schema 를 API server 로 검증 --- - log " server-side dry-run 검증" - if ! kubectl apply -f "$rendered" --dry-run=server \ - --server-side --field-manager="$FIELD_MANAGER" \ - >"$tmpdir/dryrun.out" 2>&1; then - err " server-side dry-run 실패 — apply 중단" - sed 's/^/ /' "$tmpdir/dryrun.out" >&2 - die "dry-run 검증 실패" - fi - - # --- diff preview — 변경 있음(rc=1)은 정상, 실행 오류(rc>=2)는 die --- - if [[ "${SKIP_DIFF:-}" == "yes" ]]; then - warn " SKIP_DIFF=yes → diff preview 생략" - else - log " server-side diff preview (대용량이면 스크롤)" - local rc=0 - kubectl diff -f "$rendered" --server-side --field-manager="$FIELD_MANAGER" \ - >"$tmpdir/diff.out" 2>&1 || rc=$? - case "$rc" in - 0) log " (변경 없음)" ;; - 1) - if [[ -s "$tmpdir/diff.out" ]]; then - sed 's/^/ /' "$tmpdir/diff.out" >&2 - else - log " (diff 출력 없음)" - fi - ;; - *) - err " kubectl diff 실행 실패 (exit=$rc) — apply 중단" - sed 's/^/ /' "$tmpdir/diff.out" >&2 - die "diff 실패" - ;; - esac - fi - - confirm "위 변경사항을 env=$ENV_NAME context='$(kubectl config current-context)' 에 apply 하시겠습니까?" \ - || die "사용자 취소" - - kubectl apply -f "$rendered" --server-side --field-manager="$FIELD_MANAGER" - log " vault/registry/앱 워크로드 apply 완료" -} - -phase4_wait_vault_running() { - # Vault readiness probe 는 initialized+unsealed 일 때만 통과하므로 초기화 *전* - # 에는 Ready 가 될 수 없음 → Running 단계까지만 기다림. - log "[4/9] vault-0 Running 대기 (120s × 재시도 3회)" - retry 3 10 kubectl -n "$NAMESPACE" wait \ - --for=jsonpath='{.status.phase}'=Running pod/vault-0 --timeout=120s -} - -phase5_vault_init() { - log "[5/9] Vault 초기화 / unseal / auth / policy / role" - ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/vault-init.sh" -} - -phase6_seed_apps() { - log "[6/9] 앱 시크릿 5 개 Vault KV 에 seed" - ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/vault-seed-apps.sh" -} - -phase7_vso_install() { - log "[7/9] VSO Helm upgrade --install" - ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/vso-install.sh" -} - -phase8_vso_crs() { - log "[8/9] VSO CR 적용 ($OVERLAY_DIR/vso/)" - kubectl apply -k "$OVERLAY_DIR/vso/" \ - --server-side --field-manager="$FIELD_MANAGER" -} - -# docker-registry 가 MinIO 를 S3 backend 로 쓰도록 bucket + 서비스 user + -# bucket-scoped policy 를 설정. MinIO Tenant 는 phase 8 에서 minio-tenant-env -# Secret 이 생긴 뒤 기동되므로 이 phase 는 반드시 phase 8 이후에 실행. -phase9_minio_provision_registry() { - log "[9/9] MinIO docker-registry bucket/user/policy 프로비저닝" - ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/minio-provision-registry.sh" -} - -summary() { - log "============================================" - log " $ENV_NAME 부트스트랩 완료" - log "============================================" - log "unseal keys + root token: ${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json} (0600)" - log " → 오프라인 / 외부 KMS 로 즉시 이동하세요 (Git 반입 금지)" - log "" - log "앱 시크릿 5 개는 Phase 6 에서 입력/seed 됨." - log "조회: vault kv get -field=password secret/<path>" - log "" - log "다음 확인:" - log " kubectl -n $NAMESPACE get pods" - log " kubectl -n $NAMESPACE get secrets" - log " kubectl -n $NAMESPACE get vaultstaticsecret" -} - -main() { - ENV_NAME="${1:-}" - case "$ENV_NAME" in - dev|staging|prod) ;; - *) usage ;; - esac - - OVERLAY_DIR="$K8S_ROOT/overlays/$ENV_NAME" - [[ -d "$OVERLAY_DIR" ]] || die "overlay 디렉토리 없음: $OVERLAY_DIR" - - require_cmd kubectl helm jq kustomize - - # 안전 가드 — 실수 클러스터 apply 방지 - require_kube_context "$ENV_NAME" - - log "============================================" - log " Project-Infra 부트스트랩 ($ENV_NAME)" - log " context : $(kubectl config current-context)" - log "============================================" - - precheck_namespace - phase0_minio_operator - phase1_namespace - phase2_reset_stale_secrets - phase2_5_vso_crds - phase2_6_cert_manager - phase2_7_keycloak_operator - phase2_8_traefik - phase3_overlay_apply - phase4_wait_vault_running - phase5_vault_init - phase6_seed_apps - phase7_vso_install - phase8_vso_crs - phase9_minio_provision_registry - summary -} - -# Google shell style — 직접 실행일 때만 main 호출 (source 된 경우 함수만 노출). -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" -fi diff --git a/k8s/scripts/bin/teardown.sh b/k8s/scripts/bin/teardown.sh deleted file mode 100755 index 9a1232f..0000000 --- a/k8s/scripts/bin/teardown.sh +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env bash -# Entry point: tear down the full infra stack for a given environment. -# -# 체계적 정리 — 기본 경로는 "정상 API 삭제" 만 사용한다. finalizer 강제 제거와 -# /finalize API 호출 같은 파괴적 복구 경로는 별도 플래그 아래로 격리했다. -# -# Phases: -# 1. Precheck (namespace 존재 여부) -# 2. VSO CR 삭제 (controller 살아있을 때 정상 경로만) -# 3. VSO Helm uninstall -# 4. 인프라 overlay 삭제 (Vault / Registry / 앱) -# 5. namespace 삭제 -# 6. (opt) FORCE_FINALIZERS=yes 일 때만 — finalizer 강제 제거 + /finalize -# 7. Cluster-scoped 리소스 정리 (ClusterRoleBinding, VSO ClusterRole/Webhook/CRD) -# -# 대화형: -# bash k8s/scripts/bin/teardown.sh dev -# -# CI (비대화): -# CONFIRM=yes bash k8s/scripts/bin/teardown.sh dev -# -# Required context (bootstrap 과 동일한 안전 가드): -# KUBE_CONTEXT_<ENV_UPPER> 또는 KUBE_CONTEXT -# -# Destructive 플래그: -# FORCE_FINALIZERS=yes -# Phase 6 실행 — PVC / CRD / 모든 namespaced 리소스 finalizer 강제 제거 및 -# namespace /finalize 호출. PV 가 orphaned 되고 컨트롤러 정리 누락이 발생할 -# 수 있으므로 Terminating 복구 외 목적으로는 쓰지 말 것. -# -# ALLOW_PROD_DESTRUCTIVE=yes -# env=prod teardown 을 허용. 추가로 namespace 이름 재입력 TTY 확인이 필요. -# -# TEARDOWN_VSO_OPERATOR=yes -# Phase 3 에서 VSO Helm 릴리즈와 vault-secrets-operator-system namespace 까지 -# 제거. VSO operator 는 환경별이 아니라 **클러스터 공용** 이다. 다른 env 가 -# 같은 클러스터에 있으면 그쪽 VSO reconciliation 이 같이 멈춘다. -# Phase 7 의 vault-tokenreview-binding (cluster-scoped 이름 고정) 도 이 -# 플래그로 함께 삭제한다. VSO 를 완전히 내릴 때만 켜라. 기본은 skip. -# -# TEARDOWN_VSO_CRDS=yes -# Phase 7 에서 cluster-scoped VSO 리소스 (ClusterRole/CRB/Webhook/CRD) 를 -# 제거. CRD 는 클러스터 전체 공유라 같은 클러스터에 다른 env 가 있으면 -# 그쪽 인스턴스까지 날아간다. 단일-env-per-cluster 환경 또는 VSO 를 완전히 -# 버리려는 의도일 때만 켜라. 기본은 skip. -# -# TEARDOWN_MINIO_OPERATOR=yes -# MinIO Operator Helm 릴리즈도 제거 (기본 유지). - -set -Eeuo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=../lib/common.sh -. "$SCRIPT_DIR/../lib/common.sh" - -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -K8S_ROOT="$REPO_ROOT/k8s" - -NAMESPACE="mnt" -VSO_NAMESPACE="${VSO_NAMESPACE:-vault-secrets-operator-system}" -VSO_RELEASE="${VSO_RELEASE:-vault-secrets-operator}" -VSO_CRD_KINDS=(vaultstaticsecret vaultauth vaultconnection) - -usage() { - cat >&2 <<EOF -Usage: bin/teardown.sh <dev|staging|prod> - -Required env: - KUBE_CONTEXT_DEV / KUBE_CONTEXT_STAGING / KUBE_CONTEXT_PROD - (또는 KUBE_CONTEXT=<name>) - -Options: - CONFIRM=yes 대화형 확인 자동 승인 - FORCE_FINALIZERS=yes Phase 6 (finalizer 강제 + /finalize) 활성화 - ALLOW_PROD_DESTRUCTIVE=yes env=prod teardown 허용 + namespace 재입력 필요 - TEARDOWN_VSO_OPERATOR=yes VSO Helm 릴리즈 + 전용 namespace + vault-tokenreview-binding - 모두 제거. VSO 는 클러스터 공용 플랫폼 리소스이므로 - 다른 env 의 reconciliation 이 함께 멈춤. - TEARDOWN_VSO_CRDS=yes VSO cluster-scoped 리소스 (ClusterRole/CRB/Webhook/CRD) - 제거. 클러스터 전체에 영향. - TEARDOWN_MINIO_OPERATOR=yes MinIO Operator 도 함께 제거 -EOF - exit 1 -} - -phase1_precheck() { - log "[1/7] Precheck" - if ns_exists "$NAMESPACE"; then - log " namespace $NAMESPACE phase=$(ns_phase "$NAMESPACE")" - else - log " namespace $NAMESPACE 없음 → 일부 phase 는 skip" - fi -} - -phase2_vso_cr_delete() { - log "[2/7] VSO CR 삭제" - if kubectl get -k "$OVERLAY_DIR/vso/" >/dev/null 2>&1; then - # 정상 API 삭제만 시도. 타임아웃 나도 Phase 6 (FORCE_FINALIZERS) 에 위임. - kubectl delete -k "$OVERLAY_DIR/vso/" --ignore-not-found \ - --wait=true --timeout=60s 2>&1 \ - || warn " VSO CR 삭제 타임아웃 — FORCE_FINALIZERS=yes 로 재실행하면 강제 해제" - else - log " VSO overlay 리소스 없음 → skip" - fi -} - -uninstall_vso_in_ns() { - local ns="$1" - kubectl get namespace "$ns" >/dev/null 2>&1 || return 0 - - # pre-delete hook Job 이 stuck 이면 uninstall 이 hang → 먼저 정리 - kubectl -n "$ns" get job -l app.kubernetes.io/instance="$VSO_RELEASE" -o name 2>/dev/null \ - | xargs -r kubectl -n "$ns" delete --force --grace-period=0 --ignore-not-found 2>/dev/null || true - kubectl -n "$ns" delete job pdcc-vault-secrets-operator \ - --force --grace-period=0 --ignore-not-found 2>/dev/null || true - - if helm -n "$ns" status "$VSO_RELEASE" >/dev/null 2>&1; then - log " [$ns] helm uninstall $VSO_RELEASE" - helm -n "$ns" uninstall "$VSO_RELEASE" --wait --timeout 5m 2>&1 \ - || warn " [$ns] helm uninstall 실패 — 릴리즈 Secret 잔존물 직접 제거" - else - log " [$ns] VSO 릴리즈 없음" - fi - - # uninstall 실패 / rollback 잔존 상태에서 남는 sh.helm.release.v1.* Secret 정리 - kubectl -n "$ns" get secret -o name 2>/dev/null \ - | grep "sh.helm.release.*${VSO_RELEASE}" \ - | xargs -r kubectl -n "$ns" delete --ignore-not-found 2>/dev/null || true -} - -# $NAMESPACE (mnt) 내 잔존 uninstall 은 **env-specific 레거시 cleanup** 이라 -# 항상 실행한다 — 과거 VSO 가 mnt 에 설치됐던 적이 있으면 릴리즈 메타데이터가 -# 남아 다음 install 을 막으므로. $VSO_NAMESPACE (공용 operator 공간) 에 대한 -# uninstall 은 클러스터 공유이므로 명시적 opt-in 요구. -phase3_vso_helm_uninstall() { - log "[3/7] VSO Helm uninstall" - uninstall_vso_in_ns "$NAMESPACE" - - if [[ "${TEARDOWN_VSO_OPERATOR:-}" == "yes" ]]; then - warn " TEARDOWN_VSO_OPERATOR=yes — 공용 VSO operator ($VSO_NAMESPACE) 제거" - warn " 경고: 같은 클러스터에 다른 env 의 VaultStaticSecret 이 있으면 reconciliation 이 멈춥니다." - uninstall_vso_in_ns "$VSO_NAMESPACE" - else - log " VSO operator ($VSO_NAMESPACE) 보존 (TEARDOWN_VSO_OPERATOR=yes 로 제거 가능)" - fi -} - -phase4_overlay_delete() { - log "[4/7] 인프라 리소스 삭제 (kustomize overlay)" - if ns_exists "$NAMESPACE"; then - kubectl delete -k "$OVERLAY_DIR" --ignore-not-found \ - --wait=true --timeout=120s 2>&1 \ - || warn " overlay 삭제 타임아웃 — 필요 시 FORCE_FINALIZERS=yes 로 Phase 6 사용" - else - log " namespace 없음 → overlay 삭제 skip" - fi -} - -phase5_namespace_delete() { - log "[5/7] namespace 정상 삭제" - if ns_exists "$NAMESPACE"; then - kubectl delete namespace "$NAMESPACE" --ignore-not-found --wait=false 2>/dev/null || true - log " namespace 제거 대기 (최대 60s)" - if wait_namespace_gone "$NAMESPACE" 60; then - log " namespace $NAMESPACE 제거 완료" - else - warn " namespace $NAMESPACE 가 Terminating 60s 초과." - warn " 정상 삭제로 끝나지 않은 경우:" - warn " 1) 'kubectl get all -n $NAMESPACE' 로 남은 리소스 원인 확인" - warn " 2) controller/operator 재기동 으로 finalizer 처리 시도" - warn " 3) 그래도 막히면 FORCE_FINALIZERS=yes 로 재실행 — 단 파괴적" - fi - else - log " namespace 이미 없음" - fi -} - -# FORCE_FINALIZERS=yes 일 때만 — PV orphaned / 데이터 정합성 리스크 있음. -phase6_force_finalizers() { - if [[ "${FORCE_FINALIZERS:-}" != "yes" ]]; then - log "[6/7] FORCE_FINALIZERS!=yes → skip (파괴적 경로 비활성)" - return 0 - fi - - log "[6/7] FORCE_FINALIZERS=yes — finalizer 강제 제거 + /finalize" - warn " 경고: PV orphan / controller 정리 누락이 발생할 수 있습니다." - warn " Terminating stuck 복구 외 목적으로 사용하지 마세요." - - if ! ns_exists "$NAMESPACE"; then - log " namespace 이미 없음 → skip" - return 0 - fi - - strip_finalizers_in_ns "$NAMESPACE" persistentvolumeclaim - strip_finalizers_in_ns "$NAMESPACE" "${VSO_CRD_KINDS[@]}" 2>/dev/null || true - log " namespace $NAMESPACE 전체 리소스 finalizer 일괄 제거 (최후 수단)" - strip_finalizers_all_ns_resources "$NAMESPACE" - - kubectl delete namespace "$NAMESPACE" --ignore-not-found --wait=false 2>/dev/null || true - if wait_namespace_gone "$NAMESPACE" 30; then - log " namespace $NAMESPACE 제거 완료 (FORCE_FINALIZERS)" - return 0 - fi - - warn " namespace 여전히 Terminating → /finalize API 호출" - force_finalize_namespace "$NAMESPACE" \ - || warn " /finalize 호출 실패 — 수동 확인 필요" - if wait_namespace_gone "$NAMESPACE" 30; then - log " namespace $NAMESPACE 제거 완료 (/finalize)" - else - err " namespace $NAMESPACE 여전히 존재. 'kubectl get namespace $NAMESPACE -o yaml' 로 확인 필요." - fi -} - -phase7_cluster_scoped() { - log "[7/7] Cluster-scoped 리소스 정리" - - # vault-tokenreview-binding 은 cluster-scoped 이름 고정이라 env 별 분리가 - # 불가능하다 (kustomize base 를 env-scoped 이름으로 재설계하기 전까지). - # 기본 경로에서 삭제하면 다른 env 의 Vault k8s auth TokenReview 가 부서지므로 - # TEARDOWN_VSO_OPERATOR=yes 와 함께 게이트 (플랫폼 auth 평면 전체 정리). - if [[ "${TEARDOWN_VSO_OPERATOR:-}" == "yes" ]]; then - log " vault-tokenreview-binding 제거 (TEARDOWN_VSO_OPERATOR=yes)" - kubectl delete clusterrolebinding vault-tokenreview-binding --ignore-not-found 2>&1 \ - | sed 's/^/ /' || true - if ns_exists "$VSO_NAMESPACE"; then - log " VSO namespace $VSO_NAMESPACE 삭제" - kubectl delete namespace "$VSO_NAMESPACE" --ignore-not-found --wait=true --timeout=60s 2>/dev/null \ - || warn " $VSO_NAMESPACE 삭제 타임아웃 — 수동 확인 필요" - fi - else - log " vault-tokenreview-binding 보존 (TEARDOWN_VSO_OPERATOR=yes 로 제거 가능)" - fi - - # VSO 의 ClusterRole / CRB / Webhook / CRD 는 클러스터 전체 공유다. - # 운영자가 의도적으로 VSO 전체를 버릴 때만 TEARDOWN_VSO_CRDS=yes 로 활성화. - if [[ "${TEARDOWN_VSO_CRDS:-}" == "yes" ]]; then - warn " TEARDOWN_VSO_CRDS=yes — cluster-scoped VSO 리소스 제거 (ClusterRole/CRB/Webhook/CRD)" - warn " 경고: 같은 클러스터의 다른 env 에서 VSO 를 쓰고 있으면 모두 영향받습니다." - local kind - for kind in clusterrole clusterrolebinding validatingwebhookconfiguration mutatingwebhookconfiguration; do - kubectl get "$kind" -o name 2>/dev/null \ - | grep -E 'vault-secrets-operator' \ - | xargs -r kubectl delete --ignore-not-found 2>/dev/null || true - done - kubectl get crd -o name 2>/dev/null \ - | grep 'secrets.hashicorp.com' \ - | xargs -r kubectl delete --ignore-not-found 2>/dev/null || true - else - log " cluster-scoped VSO 리소스 보존 (TEARDOWN_VSO_CRDS=yes 로 제거 가능)" - fi - - if [[ "${TEARDOWN_MINIO_OPERATOR:-}" == "yes" ]]; then - log " TEARDOWN_MINIO_OPERATOR=yes → MinIO Operator 제거" - helm -n minio-operator uninstall minio-operator --wait --timeout 5m 2>/dev/null \ - || warn " MinIO Operator Helm uninstall 실패" - kubectl delete namespace minio-operator --ignore-not-found --wait=true --timeout=60s \ - || warn " minio-operator namespace 삭제 실패" - else - log " MinIO Operator 유지 (TEARDOWN_MINIO_OPERATOR=yes 로 제거 가능)" - fi -} - -summary() { - log "============================================" - log " $ENV_NAME 삭제 완료" - log "============================================" - log "Vault unseal key 파일은 그대로 남아있습니다 (${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json})." - log "완전 초기화하려면 수동 삭제하세요." -} - -main() { - ENV_NAME="${1:-}" - case "$ENV_NAME" in - dev|staging|prod) ;; - *) usage ;; - esac - - OVERLAY_DIR="$K8S_ROOT/overlays/$ENV_NAME" - [[ -d "$OVERLAY_DIR" ]] || die "overlay 디렉토리 없음: $OVERLAY_DIR" - - require_cmd kubectl helm jq - - # 안전 가드 1 — kubectl context 가 env 와 맞는지 - require_kube_context "$ENV_NAME" - - # 안전 가드 2 — prod 는 추가 게이트 (namespace 재입력 포함) - require_production_gate "$ENV_NAME" "$NAMESPACE" - - log "============================================" - log " Project-Infra 삭제 ($ENV_NAME)" - log " context : $(kubectl config current-context)" - log "============================================" - - confirm "namespace='$NAMESPACE' 의 모든 Vault / Registry / VSO / 앱 리소스를 삭제합니다. 계속?" \ - || die "사용자 취소" - - phase1_precheck - phase2_vso_cr_delete - phase3_vso_helm_uninstall - phase4_overlay_delete - phase5_namespace_delete - phase6_force_finalizers - phase7_cluster_scoped - summary -} - -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" -fi diff --git a/k8s/scripts/ci/validate.sh b/k8s/scripts/ci/validate.sh deleted file mode 100755 index c959f22..0000000 --- a/k8s/scripts/ci/validate.sh +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env bash -# Validate all kustomize overlays against schema + lint rules, -# 그리고 scripts/ 전체에 대해 shell 품질 게이트 (shellcheck + shfmt) 를 실행한다. -# -# Runs, for each environment (dev, staging, prod): -# 1. `kustomize build overlays/<env>` — structural validity -# 2. `kustomize build overlays/<env>/vso` — VSO CRD overlay (built separately -# because it requires the VSO Helm -# release to be installed first) -# 3. `kubeconform -strict -ignore-missing-schemas` — OpenAPI schema validation -# with CRD schemas fetched from the Datree catalog. -# 4. `kube-linter lint` — anti-pattern lint against -# root .kube-linter.yaml configuration. -# -# Plus (not per-overlay): -# 5. `shellcheck -S style` over k8s/scripts/**/*.sh — shell correctness + style -# 6. `shfmt -i 2 -bn -ci -d` over k8s/scripts/ — formatting diff (fail on drift) -# -# Exit codes: -# 0 — everything passed -# 1 — at least one step failed (details printed) -# -# Tools expected on $PATH: -# kustomize kubeconform kube-linter shellcheck shfmt - -set -Eeuo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=../lib/common.sh -. "$SCRIPT_DIR/../lib/common.sh" - -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -K8S_ROOT="$REPO_ROOT/k8s" -KUBE_LINTER_CFG="$REPO_ROOT/.kube-linter.yaml" - -# Local-user tooling fallback: when tools aren't installed system-wide, -# allow users to drop them under ~/bin (CI typically installs to a PATH dir). -if [[ -d "$HOME/bin" ]]; then - export PATH="$HOME/bin:$PATH" -fi - -require_cmd kustomize kubeconform kube-linter shellcheck shfmt - -ENVS=(dev staging prod) -OVERLAYS_TO_BUILD=() -for env in "${ENVS[@]}"; do - root_overlay="$K8S_ROOT/overlays/$env" - vso_overlay="$K8S_ROOT/overlays/$env/vso" - if [[ -f "$root_overlay/kustomization.yaml" ]]; then - OVERLAYS_TO_BUILD+=("$root_overlay") - else - log "skip (kustomization.yaml 없음): overlays/$env" - fi - if [[ -f "$vso_overlay/kustomization.yaml" ]]; then - OVERLAYS_TO_BUILD+=("$vso_overlay") - fi -done - -# Datree CRD catalog — kubeconform fetches per-CRD JSON schema on demand. -CRD_CATALOG='https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' - -WORK_DIR="$(mktemp -d -t project-infra-validate.XXXXXX)" -trap_cleanup_path "$WORK_DIR" - -declare -A BUILD_STATUS SCHEMA_STATUS LINT_STATUS - -for overlay in "${OVERLAYS_TO_BUILD[@]}"; do - rel="${overlay#"$REPO_ROOT/"}" - rendered="$WORK_DIR/$(echo "$rel" | tr '/' '_').yaml" - - # -- 1. kustomize build ---------------------------------------------------- - if kustomize build "$overlay" > "$rendered" 2>"$rendered.err"; then - BUILD_STATUS[$rel]="ok" - log "kustomize build $rel — OK ($(grep -c '^kind:' "$rendered") resources)" - else - BUILD_STATUS[$rel]="fail" - err "kustomize build $rel — FAIL" - cat "$rendered.err" >&2 - continue - fi - - # -- 2. kubeconform -------------------------------------------------------- - if kubeconform -strict -ignore-missing-schemas \ - -schema-location default \ - -schema-location "$CRD_CATALOG" \ - -summary \ - "$rendered" >"$rendered.kconf" 2>&1; then - SCHEMA_STATUS[$rel]="ok" - log " kubeconform — OK ($(tail -1 "$rendered.kconf"))" - else - SCHEMA_STATUS[$rel]="fail" - err " kubeconform — FAIL" - cat "$rendered.kconf" >&2 - fi - - # -- 3. kube-linter -------------------------------------------------------- - if kube-linter lint --config "$KUBE_LINTER_CFG" "$rendered" \ - >"$rendered.klint" 2>&1; then - LINT_STATUS[$rel]="ok" - log " kube-linter — OK" - else - LINT_STATUS[$rel]="fail" - err " kube-linter — findings:" - sed 's/^/ /' "$rendered.klint" >&2 - fi -done - -# ----------------------------------------------------------------------------- -# 5. shellcheck — scripts/**/*.sh 전체 -# ----------------------------------------------------------------------------- -SHELL_STATUS="skip" -SCRIPTS_ROOT="$K8S_ROOT/scripts" -mapfile -t SHELL_FILES < <(find "$SCRIPTS_ROOT" -type f -name '*.sh' -print | sort) - -if (( ${#SHELL_FILES[@]} > 0 )); then - log "shellcheck — ${#SHELL_FILES[@]} files" - if shellcheck -S style -x "${SHELL_FILES[@]}" >"$WORK_DIR/shellcheck.out" 2>&1; then - SHELL_STATUS="ok" - log " shellcheck — OK" - else - SHELL_STATUS="fail" - err " shellcheck — findings:" - sed 's/^/ /' "$WORK_DIR/shellcheck.out" >&2 - fi -fi - -# ----------------------------------------------------------------------------- -# 6. shfmt — 포매팅 drift 검증 (수정 없이 diff 만 출력) -# ----------------------------------------------------------------------------- -FMT_STATUS="skip" -if (( ${#SHELL_FILES[@]} > 0 )); then - log "shfmt -i 2 -bn -ci -d" - if shfmt -i 2 -bn -ci -d "${SHELL_FILES[@]}" >"$WORK_DIR/shfmt.out" 2>&1; then - FMT_STATUS="ok" - log " shfmt — OK" - else - FMT_STATUS="fail" - err " shfmt — drift 감지 (로컬에서 'shfmt -i 2 -bn -ci -w k8s/scripts' 로 정렬하세요):" - sed 's/^/ /' "$WORK_DIR/shfmt.out" >&2 - fi -fi - -# ----------------------------------------------------------------------------- -# Summary -# ----------------------------------------------------------------------------- -echo >&2 -log "============================================" -log " Validation summary" -log "============================================" - -failed=0 -for overlay in "${OVERLAYS_TO_BUILD[@]}"; do - rel="${overlay#"$REPO_ROOT/"}" - b="${BUILD_STATUS[$rel]:-skip}" - s="${SCHEMA_STATUS[$rel]:-skip}" - l="${LINT_STATUS[$rel]:-skip}" - printf ' %-40s build=%-4s schema=%-4s lint=%s\n' "$rel" "$b" "$s" "$l" >&2 - [[ "$b" == "fail" || "$s" == "fail" || "$l" == "fail" ]] && failed=$((failed + 1)) -done -printf ' %-40s shellcheck=%-4s shfmt=%s\n' "scripts/" "$SHELL_STATUS" "$FMT_STATUS" >&2 -[[ "$SHELL_STATUS" == "fail" || "$FMT_STATUS" == "fail" ]] && failed=$((failed + 1)) - -if (( failed > 0 )); then - err "$failed check(s) 실패" - exit 1 -fi - -log "모든 check 통과" diff --git a/k8s/scripts/tasks/minio-provision-registry.sh b/k8s/scripts/tasks/minio-provision-registry.sh deleted file mode 100755 index f557e1e..0000000 --- a/k8s/scripts/tasks/minio-provision-registry.sh +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env bash -# MinIO Tenant 기동 후 docker-registry 용 bucket / 서비스 user / policy 를 -# 프로비저닝. -# -# 전제: -# - MinIO Tenant Pod 가 Ready (VSO 가 minio-tenant-env Secret 을 이미 동기화) -# - Vault 가 init + unseal 완료 -# - vault-seed 가 다음 경로를 seed 완료: -# secret/minio/tenant-env (root 계정, config.env 포맷) -# secret/docker-registry/minio (access_key / secret_key) -# -# 수행 순서: -# 1. Vault 에서 MinIO root 자격증명 + registry 전용 AK/SK 읽기 -# 2. kubectl port-forward 로 127.0.0.1 → MinIO Service 터널 생성 -# 3. mc alias 등록 → bucket 생성 → policy 작성 → user 생성 → policy 부착 -# -# idempotent: 이미 있는 bucket/user/policy 는 건너뜀 (회전은 별도 절차). -# -# Required env: -# REPO_ROOT -# -# Optional env: -# DOCKER_REGISTRY_BUCKET 기본 docker-registry -# DOCKER_REGISTRY_POLICY 기본 docker-registry-rw -# PF_LOCAL_PORT 기본 9900 (local port-forward) -# MINIO_SERVICE_PORT 기본 443 (Service 쪽 포트 — autoCert HTTPS) - -set -Eeuo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=../lib/common.sh -. "$SCRIPT_DIR/../lib/common.sh" -# shellcheck source=../lib/vault.sh -. "$SCRIPT_DIR/../lib/vault.sh" - -: "${DOCKER_REGISTRY_BUCKET:=docker-registry}" -: "${DOCKER_REGISTRY_POLICY:=docker-registry-rw}" -: "${PF_LOCAL_PORT:=9900}" -: "${MINIO_SERVICE_PORT:=443}" -: "${MINIO_NAMESPACE:=mnt}" -: "${MINIO_SERVICE:=minio}" - -MC_ALIAS="minio-prov" -PF_PID="" - -cleanup_port_forward() { - if [[ -n "${PF_PID:-}" ]] && kill -0 "$PF_PID" 2>/dev/null; then - kill "$PF_PID" 2>/dev/null || true - wait "$PF_PID" 2>/dev/null || true - fi -} - -wait_minio_ready() { - log "MinIO Tenant Pod Ready 대기" - retry 30 5 kubectl -n "$MINIO_NAMESPACE" wait --for=condition=Ready --timeout=10s \ - pod -l "v1.min.io/tenant=$MINIO_SERVICE" -} - -start_port_forward() { - log "port-forward 터널 생성 (127.0.0.1:${PF_LOCAL_PORT} → svc/${MINIO_SERVICE}:${MINIO_SERVICE_PORT})" - kubectl -n "$MINIO_NAMESPACE" port-forward \ - "svc/${MINIO_SERVICE}" "${PF_LOCAL_PORT}:${MINIO_SERVICE_PORT}" \ - >/dev/null 2>&1 & - PF_PID=$! - trap_cleanup_fn cleanup_port_forward - - retry 15 1 curl -ks --max-time 2 \ - "https://127.0.0.1:${PF_LOCAL_PORT}/minio/health/live" -o /dev/null - log " 터널 응답 OK" -} - -read_root_credentials() { - # secret/minio/tenant-env 는 "config.env" 필드 하나에 쉘 export 문 2 줄이 - # 들어 있다. 파싱해서 ROOT_USER / ROOT_PASSWORD 전역에 적재. - local env_blob - env_blob="$(vault_exec kv get -format=json secret/minio/tenant-env \ - | jq -r '.data.data["config.env"]')" - ROOT_USER="$(printf '%s\n' "$env_blob" \ - | sed -n 's/^export MINIO_ROOT_USER="\(.*\)"$/\1/p')" - ROOT_PASSWORD="$(printf '%s\n' "$env_blob" \ - | sed -n 's/^export MINIO_ROOT_PASSWORD="\(.*\)"$/\1/p')" - [[ -n "$ROOT_USER" && -n "$ROOT_PASSWORD" ]] \ - || die "MinIO root 자격증명 파싱 실패 — secret/minio/tenant-env 확인" -} - -read_registry_credentials() { - local json - json="$(vault_exec kv get -format=json secret/docker-registry/minio)" - REG_AK="$(printf '%s' "$json" | jq -r '.data.data.access_key')" - REG_SK="$(printf '%s' "$json" | jq -r '.data.data.secret_key')" - [[ -n "$REG_AK" && -n "$REG_SK" && "$REG_AK" != "null" && "$REG_SK" != "null" ]] \ - || die "docker-registry 자격증명 읽기 실패 — secret/docker-registry/minio 확인" -} - -configure_mc_alias() { - log "mc alias 등록 (alias=${MC_ALIAS})" - mc --insecure alias set "$MC_ALIAS" \ - "https://127.0.0.1:${PF_LOCAL_PORT}" \ - "$ROOT_USER" "$ROOT_PASSWORD" >/dev/null -} - -ensure_bucket() { - log "bucket 보장 (${DOCKER_REGISTRY_BUCKET})" - mc --insecure mb --ignore-existing "${MC_ALIAS}/${DOCKER_REGISTRY_BUCKET}" >/dev/null -} - -ensure_policy() { - log "policy 작성 (${DOCKER_REGISTRY_POLICY}) — bucket-scoped read/write" - local policy_file policy_json - policy_file="$(mktemp)" - trap_cleanup_path "$policy_file" - policy_json="$(jq -n --arg b "$DOCKER_REGISTRY_BUCKET" '{ - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Action: [ - "s3:ListBucket", - "s3:ListBucketMultipartUploads", - "s3:GetBucketLocation" - ], - Resource: ["arn:aws:s3:::\($b)"] - }, - { - Effect: "Allow", - Action: [ - "s3:PutObject", - "s3:GetObject", - "s3:DeleteObject", - "s3:ListMultipartUploadParts", - "s3:AbortMultipartUpload" - ], - Resource: ["arn:aws:s3:::\($b)/*"] - } - ] - }')" - printf '%s' "$policy_json" > "$policy_file" - chmod 600 "$policy_file" - # `policy create` 는 idempotent: 이미 있으면 덮어쓴다. - mc --insecure admin policy create "$MC_ALIAS" \ - "$DOCKER_REGISTRY_POLICY" "$policy_file" >/dev/null -} - -ensure_user() { - if mc --insecure admin user info "$MC_ALIAS" "$REG_AK" >/dev/null 2>&1; then - log "user '${REG_AK}' 이미 존재 — 재생성 skip (secret key 회전은 별도 절차)" - return 0 - fi - log "user '${REG_AK}' 생성" - mc --insecure admin user add "$MC_ALIAS" "$REG_AK" "$REG_SK" >/dev/null -} - -attach_policy() { - log "policy '${DOCKER_REGISTRY_POLICY}' → user '${REG_AK}' 부착" - # 이미 부착돼 있으면 mc 가 exit 1 을 반환. 우리는 idempotent 를 의도하므로 - # stderr 를 소비해서 noise 만 판별한 뒤 무시한다. - local out rc=0 - out="$(mc --insecure admin policy attach "$MC_ALIAS" \ - "$DOCKER_REGISTRY_POLICY" --user "$REG_AK" 2>&1)" || rc=$? - if (( rc != 0 )) && ! grep -qi "already" <<<"$out"; then - err " policy attach 실패: $out" - return "$rc" - fi -} - -main() { - require_cmd kubectl jq mc curl - require_env REPO_ROOT - - local keys_file="${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json}" - vault_login_root_from_keyfile "$keys_file" - - wait_minio_ready - start_port_forward - - read_root_credentials - read_registry_credentials - - configure_mc_alias - ensure_bucket - ensure_policy - ensure_user - attach_policy - - log "minio-provision-registry 태스크 완료" -} - -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" -fi diff --git a/presentation/.venv/bin/Activate.ps1 b/presentation/.venv/bin/Activate.ps1 deleted file mode 100644 index b49d77b..0000000 --- a/presentation/.venv/bin/Activate.ps1 +++ /dev/null @@ -1,247 +0,0 @@ -<# -.Synopsis -Activate a Python virtual environment for the current PowerShell session. - -.Description -Pushes the python executable for a virtual environment to the front of the -$Env:PATH environment variable and sets the prompt to signify that you are -in a Python virtual environment. Makes use of the command line switches as -well as the `pyvenv.cfg` file values present in the virtual environment. - -.Parameter VenvDir -Path to the directory that contains the virtual environment to activate. The -default value for this is the parent of the directory that the Activate.ps1 -script is located within. - -.Parameter Prompt -The prompt prefix to display when this virtual environment is activated. By -default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parentheses and followed by a single space (ie. '(.venv) '). - -.Example -Activate.ps1 -Activates the Python virtual environment that contains the Activate.ps1 script. - -.Example -Activate.ps1 -Verbose -Activates the Python virtual environment that contains the Activate.ps1 script, -and shows extra information about the activation as it executes. - -.Example -Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv -Activates the Python virtual environment located in the specified location. - -.Example -Activate.ps1 -Prompt "MyPython" -Activates the Python virtual environment that contains the Activate.ps1 script, -and prefixes the current prompt with the specified string (surrounded in -parentheses) while the virtual environment is active. - -.Notes -On Windows, it may be required to enable this Activate.ps1 script by setting the -execution policy for the user. You can do this by issuing the following PowerShell -command: - -PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -For more information on Execution Policies: -https://go.microsoft.com/fwlink/?LinkID=135170 - -#> -Param( - [Parameter(Mandatory = $false)] - [String] - $VenvDir, - [Parameter(Mandatory = $false)] - [String] - $Prompt -) - -<# Function declarations --------------------------------------------------- #> - -<# -.Synopsis -Remove all shell session elements added by the Activate script, including the -addition of the virtual environment's Python executable from the beginning of -the PATH variable. - -.Parameter NonDestructive -If present, do not remove this function from the global namespace for the -session. - -#> -function global:deactivate ([switch]$NonDestructive) { - # Revert to original values - - # The prior prompt: - if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { - Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt - Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT - } - - # The prior PYTHONHOME: - if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { - Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME - Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME - } - - # The prior PATH: - if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { - Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH - Remove-Item -Path Env:_OLD_VIRTUAL_PATH - } - - # Just remove the VIRTUAL_ENV altogether: - if (Test-Path -Path Env:VIRTUAL_ENV) { - Remove-Item -Path env:VIRTUAL_ENV - } - - # Just remove VIRTUAL_ENV_PROMPT altogether. - if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { - Remove-Item -Path env:VIRTUAL_ENV_PROMPT - } - - # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: - if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { - Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force - } - - # Leave deactivate function in the global namespace if requested: - if (-not $NonDestructive) { - Remove-Item -Path function:deactivate - } -} - -<# -.Description -Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the -given folder, and returns them in a map. - -For each line in the pyvenv.cfg file, if that line can be parsed into exactly -two strings separated by `=` (with any amount of whitespace surrounding the =) -then it is considered a `key = value` line. The left hand string is the key, -the right hand is the value. - -If the value starts with a `'` or a `"` then the first and last character is -stripped from the value before being captured. - -.Parameter ConfigDir -Path to the directory that contains the `pyvenv.cfg` file. -#> -function Get-PyVenvConfig( - [String] - $ConfigDir -) { - Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" - - # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue - - # An empty map will be returned if no config file is found. - $pyvenvConfig = @{ } - - if ($pyvenvConfigPath) { - - Write-Verbose "File exists, parse `key = value` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - - $pyvenvConfigContent | ForEach-Object { - $keyval = $PSItem -split "\s*=\s*", 2 - if ($keyval[0] -and $keyval[1]) { - $val = $keyval[1] - - # Remove extraneous quotations around a string value. - if ("'""".Contains($val.Substring(0, 1))) { - $val = $val.Substring(1, $val.Length - 2) - } - - $pyvenvConfig[$keyval[0]] = $val - Write-Verbose "Adding Key: '$($keyval[0])'='$val'" - } - } - } - return $pyvenvConfig -} - - -<# Begin Activate script --------------------------------------------------- #> - -# Determine the containing directory of this script -$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath - -Write-Verbose "Activation script is located in path: '$VenvExecPath'" -Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" -Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" - -# Set values required in priority: CmdLine, ConfigFile, Default -# First, get the location of the virtual environment, it might not be -# VenvExecDir if specified on the command line. -if ($VenvDir) { - Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" -} -else { - Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." - $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") - Write-Verbose "VenvDir=$VenvDir" -} - -# Next, read the `pyvenv.cfg` file to determine any required value such -# as `prompt`. -$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir - -# Next, set the prompt from the command line, or the config file, or -# just use the name of the virtual environment folder. -if ($Prompt) { - Write-Verbose "Prompt specified as argument, using '$Prompt'" -} -else { - Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" - if ($pyvenvCfg -and $pyvenvCfg['prompt']) { - Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" - $Prompt = $pyvenvCfg['prompt']; - } - else { - Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" - Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" - $Prompt = Split-Path -Path $venvDir -Leaf - } -} - -Write-Verbose "Prompt = '$Prompt'" -Write-Verbose "VenvDir='$VenvDir'" - -# Deactivate any currently active virtual environment, but leave the -# deactivate function in place. -deactivate -nondestructive - -# Now set the environment variable VIRTUAL_ENV, used by many tools to determine -# that there is an activated venv. -$env:VIRTUAL_ENV = $VenvDir - -if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { - - Write-Verbose "Setting prompt to '$Prompt'" - - # Set the prompt to include the env name - # Make sure _OLD_VIRTUAL_PROMPT is global - function global:_OLD_VIRTUAL_PROMPT { "" } - Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT - New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt - - function global:prompt { - Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " - _OLD_VIRTUAL_PROMPT - } - $env:VIRTUAL_ENV_PROMPT = $Prompt -} - -# Clear PYTHONHOME -if (Test-Path -Path Env:PYTHONHOME) { - Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME - Remove-Item -Path Env:PYTHONHOME -} - -# Add the venv to the PATH -Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH -$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/presentation/.venv/bin/__pycache__/vba_extract.cpython-312.pyc b/presentation/.venv/bin/__pycache__/vba_extract.cpython-312.pyc deleted file mode 100644 index 14b1f3a..0000000 Binary files a/presentation/.venv/bin/__pycache__/vba_extract.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/bin/activate b/presentation/.venv/bin/activate deleted file mode 100644 index b75ce97..0000000 --- a/presentation/.venv/bin/activate +++ /dev/null @@ -1,70 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# You cannot run it directly - -deactivate () { - # reset old environment variables - if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then - PATH="${_OLD_VIRTUAL_PATH:-}" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then - PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - # Call hash to forget past commands. Without forgetting - # past commands the $PATH changes we made may not be respected - hash -r 2> /dev/null - - if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then - PS1="${_OLD_VIRTUAL_PS1:-}" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - unset VIRTUAL_ENV_PROMPT - if [ ! "${1:-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -# on Windows, a path can contain colons and backslashes and has to be converted: -if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then - # transform D:\path\to\venv to /d/path/to/venv on MSYS - # and to /cygdrive/d/path/to/venv on Cygwin - export VIRTUAL_ENV=$(cygpath /home/donghyeon/dev/Project-Infra/presentation/.venv) -else - # use the path as-is - export VIRTUAL_ENV=/home/donghyeon/dev/Project-Infra/presentation/.venv -fi - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/"bin":$PATH" -export PATH - -# unset PYTHONHOME if set -# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) -# could use `if (set -u; : $PYTHONHOME) ;` in bash -if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" - unset PYTHONHOME -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1:-}" - PS1='(.venv) '"${PS1:-}" - export PS1 - VIRTUAL_ENV_PROMPT='(.venv) ' - export VIRTUAL_ENV_PROMPT -fi - -# Call hash to forget past commands. Without forgetting -# past commands the $PATH changes we made may not be respected -hash -r 2> /dev/null diff --git a/presentation/.venv/bin/activate.csh b/presentation/.venv/bin/activate.csh deleted file mode 100644 index 5109e73..0000000 --- a/presentation/.venv/bin/activate.csh +++ /dev/null @@ -1,27 +0,0 @@ -# This file must be used with "source bin/activate.csh" *from csh*. -# You cannot run it directly. - -# Created by Davide Di Blasi <davidedb@gmail.com>. -# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com> - -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' - -# Unset irrelevant variables. -deactivate nondestructive - -setenv VIRTUAL_ENV /home/donghyeon/dev/Project-Infra/presentation/.venv - -set _OLD_VIRTUAL_PATH="$PATH" -setenv PATH "$VIRTUAL_ENV/"bin":$PATH" - - -set _OLD_VIRTUAL_PROMPT="$prompt" - -if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then - set prompt = '(.venv) '"$prompt" - setenv VIRTUAL_ENV_PROMPT '(.venv) ' -endif - -alias pydoc python -m pydoc - -rehash diff --git a/presentation/.venv/bin/activate.fish b/presentation/.venv/bin/activate.fish deleted file mode 100644 index adf5580..0000000 --- a/presentation/.venv/bin/activate.fish +++ /dev/null @@ -1,69 +0,0 @@ -# This file must be used with "source <venv>/bin/activate.fish" *from fish* -# (https://fishshell.com/). You cannot run it directly. - -function deactivate -d "Exit virtual environment and return to normal shell environment" - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$_OLD_FISH_PROMPT_OVERRIDE" - set -e _OLD_FISH_PROMPT_OVERRIDE - # prevents error when using nested fish instances (Issue #93858) - if functions -q _old_fish_prompt - functions -e fish_prompt - functions -c _old_fish_prompt fish_prompt - functions -e _old_fish_prompt - end - end - - set -e VIRTUAL_ENV - set -e VIRTUAL_ENV_PROMPT - if test "$argv[1]" != "nondestructive" - # Self-destruct! - functions -e deactivate - end -end - -# Unset irrelevant variables. -deactivate nondestructive - -set -gx VIRTUAL_ENV /home/donghyeon/dev/Project-Infra/presentation/.venv - -set -gx _OLD_VIRTUAL_PATH $PATH -set -gx PATH "$VIRTUAL_ENV/"bin $PATH - -# Unset PYTHONHOME if set. -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME - set -e PYTHONHOME -end - -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - # fish uses a function instead of an env var to generate the prompt. - - # Save the current fish_prompt function as the function _old_fish_prompt. - functions -c fish_prompt _old_fish_prompt - - # With the original prompt function renamed, we can override with our own. - function fish_prompt - # Save the return status of the last command. - set -l old_status $status - - # Output the venv prompt; color taken from the blue of the Python logo. - printf "%s%s%s" (set_color 4B8BBE) '(.venv) ' (set_color normal) - - # Restore the return status of the previous command. - echo "exit $old_status" | . - # Output the original/"old" prompt. - _old_fish_prompt - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" - set -gx VIRTUAL_ENV_PROMPT '(.venv) ' -end diff --git a/presentation/.venv/bin/pip b/presentation/.venv/bin/pip deleted file mode 100755 index 13b6b66..0000000 --- a/presentation/.venv/bin/pip +++ /dev/null @@ -1,8 +0,0 @@ -#!/home/donghyeon/dev/Project-Infra/presentation/.venv/bin/python3 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/presentation/.venv/bin/pip3 b/presentation/.venv/bin/pip3 deleted file mode 100755 index 13b6b66..0000000 --- a/presentation/.venv/bin/pip3 +++ /dev/null @@ -1,8 +0,0 @@ -#!/home/donghyeon/dev/Project-Infra/presentation/.venv/bin/python3 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/presentation/.venv/bin/pip3.12 b/presentation/.venv/bin/pip3.12 deleted file mode 100755 index 13b6b66..0000000 --- a/presentation/.venv/bin/pip3.12 +++ /dev/null @@ -1,8 +0,0 @@ -#!/home/donghyeon/dev/Project-Infra/presentation/.venv/bin/python3 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/presentation/.venv/bin/python b/presentation/.venv/bin/python deleted file mode 120000 index b8a0adb..0000000 --- a/presentation/.venv/bin/python +++ /dev/null @@ -1 +0,0 @@ -python3 \ No newline at end of file diff --git a/presentation/.venv/bin/python3 b/presentation/.venv/bin/python3 deleted file mode 120000 index ae65fda..0000000 --- a/presentation/.venv/bin/python3 +++ /dev/null @@ -1 +0,0 @@ -/usr/bin/python3 \ No newline at end of file diff --git a/presentation/.venv/bin/python3.12 b/presentation/.venv/bin/python3.12 deleted file mode 120000 index b8a0adb..0000000 --- a/presentation/.venv/bin/python3.12 +++ /dev/null @@ -1 +0,0 @@ -python3 \ No newline at end of file diff --git a/presentation/.venv/bin/vba_extract.py b/presentation/.venv/bin/vba_extract.py deleted file mode 100755 index 44c53d4..0000000 --- a/presentation/.venv/bin/vba_extract.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/home/donghyeon/dev/Project-Infra/presentation/.venv/bin/python3 - -############################################################################## -# -# vba_extract - A simple utility to extract a vbaProject.bin binary from an -# Excel 2007+ xlsm file for insertion into an XlsxWriter file. -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org -# - -import sys -from zipfile import BadZipFile, ZipFile - - -def extract_file(xlsm_zip, filename): - # Extract a single file from an Excel xlsm macro file. - data = xlsm_zip.read("xl/" + filename) - - # Write the data to a local file. - file = open(filename, "wb") - file.write(data) - file.close() - - -# The VBA project file and project signature file we want to extract. -vba_filename = "vbaProject.bin" -vba_signature_filename = "vbaProjectSignature.bin" - -# Get the xlsm file name from the commandline. -if len(sys.argv) > 1: - xlsm_file = sys.argv[1] -else: - print( - "\nUtility to extract a vbaProject.bin binary from an Excel 2007+ " - "xlsm macro file for insertion into an XlsxWriter file.\n" - "If the macros are digitally signed, extracts also a vbaProjectSignature.bin " - "file.\n" - "\n" - "See: https://xlsxwriter.readthedocs.io/working_with_macros.html\n" - "\n" - "Usage: vba_extract file.xlsm\n" - ) - sys.exit() - -try: - # Open the Excel xlsm file as a zip file. - xlsm_zip = ZipFile(xlsm_file, "r") - - # Read the xl/vbaProject.bin file. - extract_file(xlsm_zip, vba_filename) - print(f"Extracted: {vba_filename}") - - if "xl/" + vba_signature_filename in xlsm_zip.namelist(): - extract_file(xlsm_zip, vba_signature_filename) - print(f"Extracted: {vba_signature_filename}") - - -except IOError as e: - print(f"File error: {str(e)}") - sys.exit() - -except KeyError as e: - # Usually when there isn't a xl/vbaProject.bin member in the file. - print(f"File error: {str(e)}") - print(f"File may not be an Excel xlsm macro file: '{xlsm_file}'") - sys.exit() - -except BadZipFile as e: - # Usually if the file is an xls file and not an xlsm file. - print(f"File error: {str(e)}: '{xlsm_file}'") - print("File may not be an Excel xlsm macro file.") - sys.exit() - -except Exception as e: - # Catch any other exceptions. - print(f"File error: {str(e)}") - sys.exit() diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/AvifImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/AvifImagePlugin.py deleted file mode 100644 index 43c39a9..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/AvifImagePlugin.py +++ /dev/null @@ -1,293 +0,0 @@ -from __future__ import annotations - -import os -from io import BytesIO -from typing import IO - -from . import ExifTags, Image, ImageFile - -try: - from . import _avif - - SUPPORTED = True -except ImportError: - SUPPORTED = False - -# Decoder options as module globals, until there is a way to pass parameters -# to Image.open (see https://github.com/python-pillow/Pillow/issues/569) -DECODE_CODEC_CHOICE = "auto" -DEFAULT_MAX_THREADS = 0 - - -def get_codec_version(codec_name: str) -> str | None: - versions = _avif.codec_versions() - for version in versions.split(", "): - if version.split(" [")[0] == codec_name: - return version.split(":")[-1].split(" ")[0] - return None - - -def _accept(prefix: bytes) -> bool | str: - if prefix[4:8] != b"ftyp": - return False - major_brand = prefix[8:12] - if major_brand in ( - # coding brands - b"avif", - b"avis", - # We accept files with AVIF container brands; we can't yet know if - # the ftyp box has the correct compatible brands, but if it doesn't - # then the plugin will raise a SyntaxError which Pillow will catch - # before moving on to the next plugin that accepts the file. - # - # Also, because this file might not actually be an AVIF file, we - # don't raise an error if AVIF support isn't properly compiled. - b"mif1", - b"msf1", - ): - if not SUPPORTED: - return ( - "image file could not be identified because AVIF support not installed" - ) - return True - return False - - -def _get_default_max_threads() -> int: - if DEFAULT_MAX_THREADS: - return DEFAULT_MAX_THREADS - if hasattr(os, "sched_getaffinity"): - return len(os.sched_getaffinity(0)) - else: - return os.cpu_count() or 1 - - -class AvifImageFile(ImageFile.ImageFile): - format = "AVIF" - format_description = "AVIF image" - __frame = -1 - - def _open(self) -> None: - if not SUPPORTED: - msg = "image file could not be opened because AVIF support not installed" - raise SyntaxError(msg) - - if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available( - DECODE_CODEC_CHOICE - ): - msg = "Invalid opening codec" - raise ValueError(msg) - - assert self.fp is not None - self._decoder = _avif.AvifDecoder( - self.fp.read(), - DECODE_CODEC_CHOICE, - _get_default_max_threads(), - ) - - # Get info from decoder - self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = ( - self._decoder.get_info() - ) - self.is_animated = self.n_frames > 1 - - if icc: - self.info["icc_profile"] = icc - if xmp: - self.info["xmp"] = xmp - - if exif_orientation != 1 or exif: - exif_data = Image.Exif() - if exif: - exif_data.load(exif) - original_orientation = exif_data.get(ExifTags.Base.Orientation, 1) - else: - original_orientation = 1 - if exif_orientation != original_orientation: - exif_data[ExifTags.Base.Orientation] = exif_orientation - exif = exif_data.tobytes() - if exif: - self.info["exif"] = exif - self.seek(0) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - - # Set tile - self.__frame = frame - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] - - def load(self) -> Image.core.PixelAccess | None: - if self.tile: - # We need to load the image data for this frame - data, timescale, pts_in_timescales, duration_in_timescales = ( - self._decoder.get_frame(self.__frame) - ) - self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale)) - self.info["duration"] = round(1000 * (duration_in_timescales / timescale)) - - if self.fp and self._exclusive_fp: - self.fp.close() - self.fp = BytesIO(data) - - return super().load() - - def load_seek(self, pos: int) -> None: - pass - - def tell(self) -> int: - return self.__frame - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -def _save( - im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False -) -> None: - info = im.encoderinfo.copy() - if save_all: - append_images = list(info.get("append_images", [])) - else: - append_images = [] - - total = 0 - for ims in [im] + append_images: - total += getattr(ims, "n_frames", 1) - - quality = info.get("quality", 75) - if not isinstance(quality, int) or quality < 0 or quality > 100: - msg = "Invalid quality setting" - raise ValueError(msg) - - duration = info.get("duration", 0) - subsampling = info.get("subsampling", "4:2:0") - speed = info.get("speed", 6) - max_threads = info.get("max_threads", _get_default_max_threads()) - codec = info.get("codec", "auto") - if codec != "auto" and not _avif.encoder_codec_available(codec): - msg = "Invalid saving codec" - raise ValueError(msg) - range_ = info.get("range", "full") - tile_rows_log2 = info.get("tile_rows", 0) - tile_cols_log2 = info.get("tile_cols", 0) - alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) - autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) - - icc_profile = info.get("icc_profile", im.info.get("icc_profile")) - exif_orientation = 1 - if exif := info.get("exif"): - if isinstance(exif, Image.Exif): - exif_data = exif - else: - exif_data = Image.Exif() - exif_data.load(exif) - if ExifTags.Base.Orientation in exif_data: - exif_orientation = exif_data.pop(ExifTags.Base.Orientation) - exif = exif_data.tobytes() if exif_data else b"" - elif isinstance(exif, Image.Exif): - exif = exif_data.tobytes() - - xmp = info.get("xmp") - - if isinstance(xmp, str): - xmp = xmp.encode("utf-8") - - advanced = info.get("advanced") - if advanced is not None: - if isinstance(advanced, dict): - advanced = advanced.items() - try: - advanced = tuple(advanced) - except TypeError: - invalid = True - else: - invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced) - if invalid: - msg = ( - "advanced codec options must be a dict of key-value string " - "pairs or a series of key-value two-tuples" - ) - raise ValueError(msg) - - # Setup the AVIF encoder - enc = _avif.AvifEncoder( - im.size, - subsampling, - quality, - speed, - max_threads, - codec, - range_, - tile_rows_log2, - tile_cols_log2, - alpha_premultiplied, - autotiling, - icc_profile or b"", - exif or b"", - exif_orientation, - xmp or b"", - advanced, - ) - - # Add each frame - frame_idx = 0 - frame_duration = 0 - cur_idx = im.tell() - is_single_frame = total == 1 - try: - for ims in [im] + append_images: - # Get number of frames in this image - nfr = getattr(ims, "n_frames", 1) - - for idx in range(nfr): - ims.seek(idx) - - # Make sure image mode is supported - frame = ims - rawmode = ims.mode - if ims.mode not in {"RGB", "RGBA"}: - rawmode = "RGBA" if ims.has_transparency_data else "RGB" - frame = ims.convert(rawmode) - - # Update frame duration - if isinstance(duration, (list, tuple)): - frame_duration = duration[frame_idx] - else: - frame_duration = duration - - # Append the frame to the animation encoder - enc.add( - frame.tobytes("raw", rawmode), - frame_duration, - frame.size, - rawmode, - is_single_frame, - ) - - # Update frame index - frame_idx += 1 - - if not save_all: - break - - finally: - im.seek(cur_idx) - - # Get the final output from the encoder - data = enc.finish() - if data is None: - msg = "cannot write file as AVIF (encoder returned None)" - raise OSError(msg) - - fp.write(data) - - -Image.register_open(AvifImageFile.format, AvifImageFile, _accept) -if SUPPORTED: - Image.register_save(AvifImageFile.format, _save) - Image.register_save_all(AvifImageFile.format, _save_all) - Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) - Image.register_mime(AvifImageFile.format, "image/avif") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/BdfFontFile.py b/presentation/.venv/lib/python3.12/site-packages/PIL/BdfFontFile.py deleted file mode 100644 index 1c8c28f..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/BdfFontFile.py +++ /dev/null @@ -1,123 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# bitmap distribution font (bdf) file parser -# -# history: -# 1996-05-16 fl created (as bdf2pil) -# 1997-08-25 fl converted to FontFile driver -# 2001-05-25 fl removed bogus __init__ call -# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev) -# 2003-04-22 fl more robustification (from Graham Dumpleton) -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1997-2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -""" -Parse X Bitmap Distribution Format (BDF) -""" - -from __future__ import annotations - -from typing import BinaryIO - -from . import FontFile, Image - - -def bdf_char( - f: BinaryIO, -) -> ( - tuple[ - str, - int, - tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]], - Image.Image, - ] - | None -): - # skip to STARTCHAR - while True: - s = f.readline() - if not s: - return None - if s.startswith(b"STARTCHAR"): - break - id = s[9:].strip().decode("ascii") - - # load symbol properties - props = {} - while True: - s = f.readline() - if not s or s.startswith(b"BITMAP"): - break - i = s.find(b" ") - props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") - - # load bitmap - bitmap = bytearray() - while True: - s = f.readline() - if not s or s.startswith(b"ENDCHAR"): - break - bitmap += s[:-1] - - # The word BBX - # followed by the width in x (BBw), height in y (BBh), - # and x and y displacement (BBxoff0, BByoff0) - # of the lower left corner from the origin of the character. - width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split()) - - # The word DWIDTH - # followed by the width in x and y of the character in device pixels. - dwx, dwy = (int(p) for p in props["DWIDTH"].split()) - - bbox = ( - (dwx, dwy), - (x_disp, -y_disp - height, width + x_disp, -y_disp), - (0, 0, width, height), - ) - - try: - im = Image.frombytes("1", (width, height), bitmap, "hex", "1") - except ValueError: - # deal with zero-width characters - im = Image.new("1", (width, height)) - - return id, int(props["ENCODING"]), bbox, im - - -class BdfFontFile(FontFile.FontFile): - """Font file plugin for the X11 BDF format.""" - - def __init__(self, fp: BinaryIO) -> None: - super().__init__() - - s = fp.readline() - if not s.startswith(b"STARTFONT 2.1"): - msg = "not a valid BDF file" - raise SyntaxError(msg) - - props = {} - comments = [] - - while True: - s = fp.readline() - if not s or s.startswith(b"ENDPROPERTIES"): - break - i = s.find(b" ") - props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") - if s[:i] in [b"COMMENT", b"COPYRIGHT"]: - if s.find(b"LogicalFontDescription") < 0: - comments.append(s[i + 1 : -1].decode("ascii")) - - while True: - c = bdf_char(fp) - if not c: - break - id, ch, (xy, dst, src), im = c - if 0 <= ch < len(self.glyph): - self.glyph[ch] = xy, dst, src, im diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/BlpImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/BlpImagePlugin.py deleted file mode 100644 index 6bb92ed..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/BlpImagePlugin.py +++ /dev/null @@ -1,498 +0,0 @@ -""" -Blizzard Mipmap Format (.blp) -Jerome Leclanche <jerome@leclan.ch> - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: - https://creativecommons.org/publicdomain/zero/1.0/ - -BLP1 files, used mostly in Warcraft III, are not fully supported. -All types of BLP2 files used in World of Warcraft are supported. - -The BLP file structure consists of a header, up to 16 mipmaps of the -texture - -Texture sizes must be powers of two, though the two dimensions do -not have to be equal; 512x256 is valid, but 512x200 is not. -The first mipmap (mipmap #0) is the full size image; each subsequent -mipmap halves both dimensions. The final mipmap should be 1x1. - -BLP files come in many different flavours: -* JPEG-compressed (type == 0) - only supported for BLP1. -* RAW images (type == 1, encoding == 1). Each mipmap is stored as an - array of 8-bit values, one per pixel, left to right, top to bottom. - Each value is an index to the palette. -* DXT-compressed (type == 1, encoding == 2): -- DXT1 compression is used if alpha_encoding == 0. - - An additional alpha bit is used if alpha_depth == 1. - - DXT3 compression is used if alpha_encoding == 1. - - DXT5 compression is used if alpha_encoding == 7. -""" - -from __future__ import annotations - -import abc -import os -import struct -from enum import IntEnum -from io import BytesIO -from typing import IO - -from . import Image, ImageFile - - -class Format(IntEnum): - JPEG = 0 - - -class Encoding(IntEnum): - UNCOMPRESSED = 1 - DXT = 2 - UNCOMPRESSED_RAW_BGRA = 3 - - -class AlphaEncoding(IntEnum): - DXT1 = 0 - DXT3 = 1 - DXT5 = 7 - - -def unpack_565(i: int) -> tuple[int, int, int]: - return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 - - -def decode_dxt1( - data: bytes, alpha: bool = False -) -> tuple[bytearray, bytearray, bytearray, bytearray]: - """ - input: one "row" of data (i.e. will produce 4*width pixels) - """ - - blocks = len(data) // 8 # number of blocks in row - ret = (bytearray(), bytearray(), bytearray(), bytearray()) - - for block_index in range(blocks): - # Decode next 8-byte block. - idx = block_index * 8 - color0, color1, bits = struct.unpack_from("<HHI", data, idx) - - r0, g0, b0 = unpack_565(color0) - r1, g1, b1 = unpack_565(color1) - - # Decode this block into 4x4 pixels - # Accumulate the results onto our 4 row accumulators - for j in range(4): - for i in range(4): - # get next control op and generate a pixel - - control = bits & 3 - bits = bits >> 2 - - a = 0xFF - if control == 0: - r, g, b = r0, g0, b0 - elif control == 1: - r, g, b = r1, g1, b1 - elif control == 2: - if color0 > color1: - r = (2 * r0 + r1) // 3 - g = (2 * g0 + g1) // 3 - b = (2 * b0 + b1) // 3 - else: - r = (r0 + r1) // 2 - g = (g0 + g1) // 2 - b = (b0 + b1) // 2 - elif control == 3: - if color0 > color1: - r = (2 * r1 + r0) // 3 - g = (2 * g1 + g0) // 3 - b = (2 * b1 + b0) // 3 - else: - r, g, b, a = 0, 0, 0, 0 - - if alpha: - ret[j].extend([r, g, b, a]) - else: - ret[j].extend([r, g, b]) - - return ret - - -def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: - """ - input: one "row" of data (i.e. will produce 4*width pixels) - """ - - blocks = len(data) // 16 # number of blocks in row - ret = (bytearray(), bytearray(), bytearray(), bytearray()) - - for block_index in range(blocks): - idx = block_index * 16 - block = data[idx : idx + 16] - # Decode next 16-byte block. - bits = struct.unpack_from("<8B", block) - color0, color1 = struct.unpack_from("<HH", block, 8) - - (code,) = struct.unpack_from("<I", block, 12) - - r0, g0, b0 = unpack_565(color0) - r1, g1, b1 = unpack_565(color1) - - for j in range(4): - high = False # Do we want the higher bits? - for i in range(4): - alphacode_index = (4 * j + i) // 2 - a = bits[alphacode_index] - if high: - high = False - a >>= 4 - else: - high = True - a &= 0xF - a *= 17 # We get a value between 0 and 15 - - color_code = (code >> 2 * (4 * j + i)) & 0x03 - - if color_code == 0: - r, g, b = r0, g0, b0 - elif color_code == 1: - r, g, b = r1, g1, b1 - elif color_code == 2: - r = (2 * r0 + r1) // 3 - g = (2 * g0 + g1) // 3 - b = (2 * b0 + b1) // 3 - elif color_code == 3: - r = (2 * r1 + r0) // 3 - g = (2 * g1 + g0) // 3 - b = (2 * b1 + b0) // 3 - - ret[j].extend([r, g, b, a]) - - return ret - - -def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: - """ - input: one "row" of data (i.e. will produce 4 * width pixels) - """ - - blocks = len(data) // 16 # number of blocks in row - ret = (bytearray(), bytearray(), bytearray(), bytearray()) - - for block_index in range(blocks): - idx = block_index * 16 - block = data[idx : idx + 16] - # Decode next 16-byte block. - a0, a1 = struct.unpack_from("<BB", block) - - bits = struct.unpack_from("<6B", block, 2) - alphacode1 = bits[2] | (bits[3] << 8) | (bits[4] << 16) | (bits[5] << 24) - alphacode2 = bits[0] | (bits[1] << 8) - - color0, color1 = struct.unpack_from("<HH", block, 8) - - (code,) = struct.unpack_from("<I", block, 12) - - r0, g0, b0 = unpack_565(color0) - r1, g1, b1 = unpack_565(color1) - - for j in range(4): - for i in range(4): - # get next control op and generate a pixel - alphacode_index = 3 * (4 * j + i) - - if alphacode_index <= 12: - alphacode = (alphacode2 >> alphacode_index) & 0x07 - elif alphacode_index == 15: - alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06) - else: # alphacode_index >= 18 and alphacode_index <= 45 - alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07 - - if alphacode == 0: - a = a0 - elif alphacode == 1: - a = a1 - elif a0 > a1: - a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7 - elif alphacode == 6: - a = 0 - elif alphacode == 7: - a = 255 - else: - a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5 - - color_code = (code >> 2 * (4 * j + i)) & 0x03 - - if color_code == 0: - r, g, b = r0, g0, b0 - elif color_code == 1: - r, g, b = r1, g1, b1 - elif color_code == 2: - r = (2 * r0 + r1) // 3 - g = (2 * g0 + g1) // 3 - b = (2 * b0 + b1) // 3 - elif color_code == 3: - r = (2 * r1 + r0) // 3 - g = (2 * g1 + g0) // 3 - b = (2 * b1 + b0) // 3 - - ret[j].extend([r, g, b, a]) - - return ret - - -class BLPFormatError(NotImplementedError): - pass - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"BLP1", b"BLP2")) - - -class BlpImageFile(ImageFile.ImageFile): - """ - Blizzard Mipmap Format - """ - - format = "BLP" - format_description = "Blizzard Mipmap Format" - - def _open(self) -> None: - assert self.fp is not None - self.magic = self.fp.read(4) - if not _accept(self.magic): - msg = f"Bad BLP magic {repr(self.magic)}" - raise BLPFormatError(msg) - - compression = struct.unpack("<i", self.fp.read(4))[0] - if self.magic == b"BLP1": - alpha = struct.unpack("<I", self.fp.read(4))[0] != 0 - else: - encoding = struct.unpack("<b", self.fp.read(1))[0] - alpha = struct.unpack("<b", self.fp.read(1))[0] != 0 - alpha_encoding = struct.unpack("<b", self.fp.read(1))[0] - self.fp.seek(1, os.SEEK_CUR) # mips - - self._size = struct.unpack("<II", self.fp.read(8)) - - args: tuple[int, int, bool] | tuple[int, int, bool, int] - if self.magic == b"BLP1": - encoding = struct.unpack("<i", self.fp.read(4))[0] - self.fp.seek(4, os.SEEK_CUR) # subtype - - args = (compression, encoding, alpha) - offset = 28 - else: - args = (compression, encoding, alpha, alpha_encoding) - offset = 20 - - decoder = self.magic.decode() - - self._mode = "RGBA" if alpha else "RGB" - self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)] - - -class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - try: - self._read_header() - self._load() - except struct.error as e: - msg = "Truncated BLP file" - raise OSError(msg) from e - return -1, 0 - - @abc.abstractmethod - def _load(self) -> None: - pass - - def _read_header(self) -> None: - self._offsets = struct.unpack("<16I", self._safe_read(16 * 4)) - self._lengths = struct.unpack("<16I", self._safe_read(16 * 4)) - - def _safe_read(self, length: int) -> bytes: - assert self.fd is not None - return ImageFile._safe_read(self.fd, length) - - def _read_palette(self) -> list[tuple[int, int, int, int]]: - ret = [] - for i in range(256): - try: - b, g, r, a = struct.unpack("<4B", self._safe_read(4)) - except struct.error: - break - ret.append((b, g, r, a)) - return ret - - def _read_bgra( - self, palette: list[tuple[int, int, int, int]], alpha: bool - ) -> bytearray: - data = bytearray() - _data = BytesIO(self._safe_read(self._lengths[0])) - while True: - try: - (offset,) = struct.unpack("<B", _data.read(1)) - except struct.error: - break - b, g, r, a = palette[offset] - d: tuple[int, ...] = (r, g, b) - if alpha: - d += (a,) - data.extend(d) - return data - - -class BLP1Decoder(_BLPBaseDecoder): - def _load(self) -> None: - self._compression, self._encoding, alpha = self.args - - if self._compression == Format.JPEG: - self._decode_jpeg_stream() - - elif self._compression == 1: - if self._encoding in (4, 5): - palette = self._read_palette() - data = self._read_bgra(palette, alpha) - self.set_as_raw(data) - else: - msg = f"Unsupported BLP encoding {repr(self._encoding)}" - raise BLPFormatError(msg) - else: - msg = f"Unsupported BLP compression {repr(self._encoding)}" - raise BLPFormatError(msg) - - def _decode_jpeg_stream(self) -> None: - from .JpegImagePlugin import JpegImageFile - - (jpeg_header_size,) = struct.unpack("<I", self._safe_read(4)) - jpeg_header = self._safe_read(jpeg_header_size) - assert self.fd is not None - self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this? - data = self._safe_read(self._lengths[0]) - data = jpeg_header + data - image = JpegImageFile(BytesIO(data)) - Image._decompression_bomb_check(image.size) - if image.mode == "CMYK": - args = image.tile[0].args - assert isinstance(args, tuple) - image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))] - self.set_as_raw(image.convert("RGB").tobytes(), "BGR") - - -class BLP2Decoder(_BLPBaseDecoder): - def _load(self) -> None: - self._compression, self._encoding, alpha, self._alpha_encoding = self.args - - palette = self._read_palette() - - assert self.fd is not None - self.fd.seek(self._offsets[0]) - - if self._compression == 1: - # Uncompressed or DirectX compression - - if self._encoding == Encoding.UNCOMPRESSED: - data = self._read_bgra(palette, alpha) - - elif self._encoding == Encoding.DXT: - data = bytearray() - if self._alpha_encoding == AlphaEncoding.DXT1: - linesize = (self.state.xsize + 3) // 4 * 8 - for yb in range((self.state.ysize + 3) // 4): - for d in decode_dxt1(self._safe_read(linesize), alpha): - data += d - - elif self._alpha_encoding == AlphaEncoding.DXT3: - linesize = (self.state.xsize + 3) // 4 * 16 - for yb in range((self.state.ysize + 3) // 4): - for d in decode_dxt3(self._safe_read(linesize)): - data += d - - elif self._alpha_encoding == AlphaEncoding.DXT5: - linesize = (self.state.xsize + 3) // 4 * 16 - for yb in range((self.state.ysize + 3) // 4): - for d in decode_dxt5(self._safe_read(linesize)): - data += d - else: - msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}" - raise BLPFormatError(msg) - else: - msg = f"Unknown BLP encoding {repr(self._encoding)}" - raise BLPFormatError(msg) - - else: - msg = f"Unknown BLP compression {repr(self._compression)}" - raise BLPFormatError(msg) - - self.set_as_raw(data) - - -class BLPEncoder(ImageFile.PyEncoder): - _pushes_fd = True - - def _write_palette(self) -> bytes: - data = b"" - assert self.im is not None - palette = self.im.getpalette("RGBA", "RGBA") - for i in range(len(palette) // 4): - r, g, b, a = palette[i * 4 : (i + 1) * 4] - data += struct.pack("<4B", b, g, r, a) - while len(data) < 256 * 4: - data += b"\x00" * 4 - return data - - def encode(self, bufsize: int) -> tuple[int, int, bytes]: - palette_data = self._write_palette() - - offset = 20 + 16 * 4 * 2 + len(palette_data) - data = struct.pack("<16I", offset, *((0,) * 15)) - - assert self.im is not None - w, h = self.im.size - data += struct.pack("<16I", w * h, *((0,) * 15)) - - data += palette_data - - for y in range(h): - for x in range(w): - data += struct.pack("<B", self.im.getpixel((x, y))) - - return len(data), 0, data - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode != "P": - msg = "Unsupported BLP image mode" - raise ValueError(msg) - - magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" - fp.write(magic) - - assert im.palette is not None - fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression - - alpha_depth = 1 if im.palette.mode == "RGBA" else 0 - if magic == b"BLP1": - fp.write(struct.pack("<L", alpha_depth)) - else: - fp.write(struct.pack("<b", Encoding.UNCOMPRESSED)) - fp.write(struct.pack("<b", alpha_depth)) - fp.write(struct.pack("<b", 0)) # alpha encoding - fp.write(struct.pack("<b", 0)) # mips - fp.write(struct.pack("<II", *im.size)) - if magic == b"BLP1": - fp.write(struct.pack("<i", 5)) - fp.write(struct.pack("<i", 0)) - - ImageFile._save(im, fp, [ImageFile._Tile("BLP", (0, 0) + im.size, 0, im.mode)]) - - -Image.register_open(BlpImageFile.format, BlpImageFile, _accept) -Image.register_extension(BlpImageFile.format, ".blp") -Image.register_decoder("BLP1", BLP1Decoder) -Image.register_decoder("BLP2", BLP2Decoder) - -Image.register_save(BlpImageFile.format, _save) -Image.register_encoder("BLP", BLPEncoder) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/BmpImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/BmpImagePlugin.py deleted file mode 100644 index a6724ca..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/BmpImagePlugin.py +++ /dev/null @@ -1,514 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# BMP file handler -# -# Windows (and OS/2) native bitmap storage format. -# -# history: -# 1995-09-01 fl Created -# 1996-04-30 fl Added save -# 1997-08-27 fl Fixed save of 1-bit images -# 1998-03-06 fl Load P images as L where possible -# 1998-07-03 fl Load P images as 1 where possible -# 1998-12-29 fl Handle small palettes -# 2002-12-30 fl Fixed load of 1-bit palette images -# 2003-04-21 fl Fixed load of 1-bit monochrome images -# 2003-04-23 fl Added limited support for BI_BITFIELDS compression -# -# Copyright (c) 1997-2003 by Secret Labs AB -# Copyright (c) 1995-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import IO, Any - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import i32le as i32 -from ._binary import o8 -from ._binary import o16le as o16 -from ._binary import o32le as o32 - -# -# -------------------------------------------------------------------- -# Read BMP file - -BIT2MODE = { - # bits => mode, rawmode - 1: ("P", "P;1"), - 4: ("P", "P;4"), - 8: ("P", "P"), - 16: ("RGB", "BGR;15"), - 24: ("RGB", "BGR"), - 32: ("RGB", "BGRX"), -} - -USE_RAW_ALPHA = False - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"BM") - - -def _dib_accept(prefix: bytes) -> bool: - return i32(prefix) in [12, 40, 52, 56, 64, 108, 124] - - -# ============================================================================= -# Image plugin for the Windows BMP format. -# ============================================================================= -class BmpImageFile(ImageFile.ImageFile): - """Image plugin for the Windows Bitmap format (BMP)""" - - # ------------------------------------------------------------- Description - format_description = "Windows Bitmap" - format = "BMP" - - # -------------------------------------------------- BMP Compression values - COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5} - for k, v in COMPRESSIONS.items(): - vars()[k] = v - - def _bitmap(self, header: int = 0, offset: int = 0) -> None: - """Read relevant info about the BMP""" - assert self.fp is not None - read, seek = self.fp.read, self.fp.seek - if header: - seek(header) - # read bmp header size @offset 14 (this is part of the header size) - file_info: dict[str, bool | int | tuple[int, ...]] = { - "header_size": i32(read(4)), - "direction": -1, - } - - # -------------------- If requested, read header at a specific position - # read the rest of the bmp header, without its size - assert isinstance(file_info["header_size"], int) - header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) - - # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1 - # ----- This format has different offsets because of width/height types - # 12: BITMAPCOREHEADER/OS21XBITMAPHEADER - if file_info["header_size"] == 12: - file_info["width"] = i16(header_data, 0) - file_info["height"] = i16(header_data, 2) - file_info["planes"] = i16(header_data, 4) - file_info["bits"] = i16(header_data, 6) - file_info["compression"] = self.COMPRESSIONS["RAW"] - file_info["palette_padding"] = 3 - - # --------------------------------------------- Windows Bitmap v3 to v5 - # 40: BITMAPINFOHEADER - # 52: BITMAPV2HEADER - # 56: BITMAPV3HEADER - # 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER - # 108: BITMAPV4HEADER - # 124: BITMAPV5HEADER - elif file_info["header_size"] in (40, 52, 56, 64, 108, 124): - file_info["y_flip"] = header_data[7] == 0xFF - file_info["direction"] = 1 if file_info["y_flip"] else -1 - file_info["width"] = i32(header_data, 0) - file_info["height"] = ( - i32(header_data, 4) - if not file_info["y_flip"] - else 2**32 - i32(header_data, 4) - ) - file_info["planes"] = i16(header_data, 8) - file_info["bits"] = i16(header_data, 10) - file_info["compression"] = i32(header_data, 12) - # byte size of pixel data - file_info["data_size"] = i32(header_data, 16) - file_info["pixels_per_meter"] = ( - i32(header_data, 20), - i32(header_data, 24), - ) - file_info["colors"] = i32(header_data, 28) - file_info["palette_padding"] = 4 - assert isinstance(file_info["pixels_per_meter"], tuple) - self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) - if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: - masks = ["r_mask", "g_mask", "b_mask"] - if len(header_data) >= 48: - if len(header_data) >= 52: - masks.append("a_mask") - else: - file_info["a_mask"] = 0x0 - for idx, mask in enumerate(masks): - file_info[mask] = i32(header_data, 36 + idx * 4) - else: - # 40 byte headers only have the three components in the - # bitfields masks, ref: - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx - # See also - # https://github.com/python-pillow/Pillow/issues/1293 - # There is a 4th component in the RGBQuad, in the alpha - # location, but it is listed as a reserved component, - # and it is not generally an alpha channel - file_info["a_mask"] = 0x0 - for mask in masks: - file_info[mask] = i32(read(4)) - assert isinstance(file_info["r_mask"], int) - assert isinstance(file_info["g_mask"], int) - assert isinstance(file_info["b_mask"], int) - assert isinstance(file_info["a_mask"], int) - file_info["rgb_mask"] = ( - file_info["r_mask"], - file_info["g_mask"], - file_info["b_mask"], - ) - file_info["rgba_mask"] = ( - file_info["r_mask"], - file_info["g_mask"], - file_info["b_mask"], - file_info["a_mask"], - ) - else: - msg = f"Unsupported BMP header type ({file_info['header_size']})" - raise OSError(msg) - - # ------------------ Special case : header is reported 40, which - # ---------------------- is shorter than real size for bpp >= 16 - assert isinstance(file_info["width"], int) - assert isinstance(file_info["height"], int) - self._size = file_info["width"], file_info["height"] - - # ------- If color count was not found in the header, compute from bits - assert isinstance(file_info["bits"], int) - if not file_info.get("colors", 0): - file_info["colors"] = 1 << file_info["bits"] - assert isinstance(file_info["palette_padding"], int) - assert isinstance(file_info["colors"], int) - if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: - offset += file_info["palette_padding"] * file_info["colors"] - - # ---------------------- Check bit depth for unusual unsupported values - self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", "")) - if not self.mode: - msg = f"Unsupported BMP pixel depth ({file_info['bits']})" - raise OSError(msg) - - # ---------------- Process BMP with Bitfields compression (not palette) - decoder_name = "raw" - if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: - SUPPORTED: dict[int, list[tuple[int, ...]]] = { - 32: [ - (0xFF0000, 0xFF00, 0xFF, 0x0), - (0xFF000000, 0xFF0000, 0xFF00, 0x0), - (0xFF000000, 0xFF00, 0xFF, 0x0), - (0xFF000000, 0xFF0000, 0xFF00, 0xFF), - (0xFF, 0xFF00, 0xFF0000, 0xFF000000), - (0xFF0000, 0xFF00, 0xFF, 0xFF000000), - (0xFF000000, 0xFF00, 0xFF, 0xFF0000), - (0x0, 0x0, 0x0, 0x0), - ], - 24: [(0xFF0000, 0xFF00, 0xFF)], - 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)], - } - MASK_MODES = { - (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", - (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", - (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR", - (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", - (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", - (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", - (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR", - (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", - (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", - (16, (0xF800, 0x7E0, 0x1F)): "BGR;16", - (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15", - } - if file_info["bits"] in SUPPORTED: - if ( - file_info["bits"] == 32 - and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] - ): - assert isinstance(file_info["rgba_mask"], tuple) - raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] - self._mode = "RGBA" if "A" in raw_mode else self.mode - elif ( - file_info["bits"] in (24, 16) - and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] - ): - assert isinstance(file_info["rgb_mask"], tuple) - raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] - else: - msg = "Unsupported BMP bitfields layout" - raise OSError(msg) - else: - msg = "Unsupported BMP bitfields layout" - raise OSError(msg) - elif file_info["compression"] == self.COMPRESSIONS["RAW"]: - if file_info["bits"] == 32 and ( - header == 22 or USE_RAW_ALPHA # 32-bit .cur offset - ): - raw_mode, self._mode = "BGRA", "RGBA" - elif file_info["compression"] in ( - self.COMPRESSIONS["RLE8"], - self.COMPRESSIONS["RLE4"], - ): - decoder_name = "bmp_rle" - else: - msg = f"Unsupported BMP compression ({file_info['compression']})" - raise OSError(msg) - - # --------------- Once the header is processed, process the palette/LUT - if self.mode == "P": # Paletted for 1, 4 and 8 bit images - # ---------------------------------------------------- 1-bit images - if not (0 < file_info["colors"] <= 65536): - msg = f"Unsupported BMP Palette size ({file_info['colors']})" - raise OSError(msg) - else: - padding = file_info["palette_padding"] - palette = read(padding * file_info["colors"]) - grayscale = True - indices = ( - (0, 255) - if file_info["colors"] == 2 - else list(range(file_info["colors"])) - ) - - # ----------------- Check if grayscale and ignore palette if so - for ind, val in enumerate(indices): - rgb = palette[ind * padding : ind * padding + 3] - if rgb != o8(val) * 3: - grayscale = False - - # ------- If all colors are gray, white or black, ditch palette - if grayscale: - self._mode = "1" if file_info["colors"] == 2 else "L" - raw_mode = self.mode - else: - self._mode = "P" - self.palette = ImagePalette.raw( - "BGRX" if padding == 4 else "BGR", palette - ) - - # ---------------------------- Finally set the tile data for the plugin - self.info["compression"] = file_info["compression"] - args: list[Any] = [raw_mode] - if decoder_name == "bmp_rle": - args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"]) - else: - assert isinstance(file_info["width"], int) - args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) - args.append(file_info["direction"]) - self.tile = [ - ImageFile._Tile( - decoder_name, - (0, 0, file_info["width"], file_info["height"]), - offset or self.fp.tell(), - tuple(args), - ) - ] - - def _open(self) -> None: - """Open file, check magic number and read header""" - # read 14 bytes: magic number, filesize, reserved, header final offset - assert self.fp is not None - head_data = self.fp.read(14) - # choke if the file does not have the required magic bytes - if not _accept(head_data): - msg = "Not a BMP file" - raise SyntaxError(msg) - # read the start position of the BMP image data (u32) - offset = i32(head_data, 10) - # load bitmap information (offset=raster info) - self._bitmap(offset=offset) - - -class BmpRleDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - rle4 = self.args[1] - data = bytearray() - x = 0 - dest_length = self.state.xsize * self.state.ysize - while len(data) < dest_length: - pixels = self.fd.read(1) - byte = self.fd.read(1) - if not pixels or not byte: - break - num_pixels = pixels[0] - if num_pixels: - # encoded mode - if x + num_pixels > self.state.xsize: - # Too much data for row - num_pixels = max(0, self.state.xsize - x) - if rle4: - first_pixel = o8(byte[0] >> 4) - second_pixel = o8(byte[0] & 0x0F) - for index in range(num_pixels): - if index % 2 == 0: - data += first_pixel - else: - data += second_pixel - else: - data += byte * num_pixels - x += num_pixels - else: - if byte[0] == 0: - # end of line - while len(data) % self.state.xsize != 0: - data += b"\x00" - x = 0 - elif byte[0] == 1: - # end of bitmap - break - elif byte[0] == 2: - # delta - bytes_read = self.fd.read(2) - if len(bytes_read) < 2: - break - right, up = bytes_read - data += b"\x00" * (right + up * self.state.xsize) - x = len(data) % self.state.xsize - else: - # absolute mode - if rle4: - # 2 pixels per byte - byte_count = byte[0] // 2 - bytes_read = self.fd.read(byte_count) - for byte_read in bytes_read: - data += o8(byte_read >> 4) - data += o8(byte_read & 0x0F) - else: - byte_count = byte[0] - bytes_read = self.fd.read(byte_count) - data += bytes_read - if len(bytes_read) < byte_count: - break - x += byte[0] - - # align to 16-bit word boundary - if self.fd.tell() % 2 != 0: - self.fd.seek(1, os.SEEK_CUR) - rawmode = "L" if self.mode == "L" else "P" - self.set_as_raw(bytes(data), rawmode, (0, self.args[-1])) - return -1, 0 - - -# ============================================================================= -# Image plugin for the DIB format (BMP alias) -# ============================================================================= -class DibImageFile(BmpImageFile): - format = "DIB" - format_description = "Windows Bitmap" - - def _open(self) -> None: - self._bitmap() - - -# -# -------------------------------------------------------------------- -# Write BMP file - - -SAVE = { - "1": ("1", 1, 2), - "L": ("L", 8, 256), - "P": ("P", 8, 256), - "RGB": ("BGR", 24, 0), - "RGBA": ("BGRA", 32, 0), -} - - -def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, False) - - -def _save( - im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True -) -> None: - try: - rawmode, bits, colors = SAVE[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as BMP" - raise OSError(msg) from e - - info = im.encoderinfo - - dpi = info.get("dpi", (96, 96)) - - # 1 meter == 39.3701 inches - ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi) - - stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) - header = 40 # or 64 for OS/2 version 2 - image = stride * im.size[1] - - if im.mode == "1": - palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255)) - elif im.mode == "L": - palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256)) - elif im.mode == "P": - palette = im.im.getpalette("RGB", "BGRX") - colors = len(palette) // 4 - else: - palette = None - - # bitmap header - if bitmap_header: - offset = 14 + header + colors * 4 - file_size = offset + image - if file_size > 2**32 - 1: - msg = "File size is too large for the BMP format" - raise ValueError(msg) - fp.write( - b"BM" # file type (magic) - + o32(file_size) # file size - + o32(0) # reserved - + o32(offset) # image data offset - ) - - # bitmap info header - fp.write( - o32(header) # info header size - + o32(im.size[0]) # width - + o32(im.size[1]) # height - + o16(1) # planes - + o16(bits) # depth - + o32(0) # compression (0=uncompressed) - + o32(image) # size of bitmap - + o32(ppm[0]) # resolution - + o32(ppm[1]) # resolution - + o32(colors) # colors used - + o32(colors) # colors important - ) - - fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) - - if palette: - fp.write(palette) - - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))] - ) - - -# -# -------------------------------------------------------------------- -# Registry - - -Image.register_open(BmpImageFile.format, BmpImageFile, _accept) -Image.register_save(BmpImageFile.format, _save) - -Image.register_extension(BmpImageFile.format, ".bmp") - -Image.register_mime(BmpImageFile.format, "image/bmp") - -Image.register_decoder("bmp_rle", BmpRleDecoder) - -Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) -Image.register_save(DibImageFile.format, _dib_save) - -Image.register_extension(DibImageFile.format, ".dib") - -Image.register_mime(DibImageFile.format, "image/bmp") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/BufrStubImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/BufrStubImagePlugin.py deleted file mode 100644 index d82c4c7..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/BufrStubImagePlugin.py +++ /dev/null @@ -1,72 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# BUFR stub adapter -# -# Copyright (c) 1996-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import IO - -from . import Image, ImageFile - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific BUFR image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -# -------------------------------------------------------------------- -# Image adapter - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"BUFR", b"ZCZC")) - - -class BufrStubImageFile(ImageFile.StubImageFile): - format = "BUFR" - format_description = "BUFR" - - def _open(self) -> None: - assert self.fp is not None - if not _accept(self.fp.read(4)): - msg = "Not a BUFR file" - raise SyntaxError(msg) - - self.fp.seek(-4, os.SEEK_CUR) - - # make something up - self._mode = "F" - self._size = 1, 1 - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "BUFR save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept) -Image.register_save(BufrStubImageFile.format, _save) - -Image.register_extension(BufrStubImageFile.format, ".bufr") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ContainerIO.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ContainerIO.py deleted file mode 100644 index ec9e66c..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ContainerIO.py +++ /dev/null @@ -1,173 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a class to read from a container file -# -# History: -# 1995-06-18 fl Created -# 1995-09-07 fl Added readline(), readlines() -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1995 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -from collections.abc import Iterable -from typing import IO, AnyStr, NoReturn - - -class ContainerIO(IO[AnyStr]): - """ - A file object that provides read access to a part of an existing - file (for example a TAR file). - """ - - def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None: - """ - Create file object. - - :param file: Existing file. - :param offset: Start of region, in bytes. - :param length: Size of region, in bytes. - """ - self.fh: IO[AnyStr] = file - self.pos = 0 - self.offset = offset - self.length = length - self.fh.seek(offset) - - ## - # Always false. - - def isatty(self) -> bool: - return False - - def seekable(self) -> bool: - return True - - def seek(self, offset: int, mode: int = io.SEEK_SET) -> int: - """ - Move file pointer. - - :param offset: Offset in bytes. - :param mode: Starting position. Use 0 for beginning of region, 1 - for current offset, and 2 for end of region. You cannot move - the pointer outside the defined region. - :returns: Offset from start of region, in bytes. - """ - if mode == 1: - self.pos = self.pos + offset - elif mode == 2: - self.pos = self.length + offset - else: - self.pos = offset - # clamp - self.pos = max(0, min(self.pos, self.length)) - self.fh.seek(self.offset + self.pos) - return self.pos - - def tell(self) -> int: - """ - Get current file pointer. - - :returns: Offset from start of region, in bytes. - """ - return self.pos - - def readable(self) -> bool: - return True - - def read(self, n: int = -1) -> AnyStr: - """ - Read data. - - :param n: Number of bytes to read. If omitted, zero or negative, - read until end of region. - :returns: An 8-bit string. - """ - if n > 0: - n = min(n, self.length - self.pos) - else: - n = self.length - self.pos - if n <= 0: # EOF - return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] - self.pos = self.pos + n - return self.fh.read(n) - - def readline(self, n: int = -1) -> AnyStr: - """ - Read a line of text. - - :param n: Number of bytes to read. If omitted, zero or negative, - read until end of line. - :returns: An 8-bit string. - """ - s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] - newline_character = b"\n" if "b" in self.fh.mode else "\n" - while True: - c = self.read(1) - if not c: - break - s = s + c - if c == newline_character or len(s) == n: - break - return s - - def readlines(self, n: int | None = -1) -> list[AnyStr]: - """ - Read multiple lines of text. - - :param n: Number of lines to read. If omitted, zero, negative or None, - read until end of region. - :returns: A list of 8-bit strings. - """ - lines = [] - while True: - s = self.readline() - if not s: - break - lines.append(s) - if len(lines) == n: - break - return lines - - def writable(self) -> bool: - return False - - def write(self, b: AnyStr) -> NoReturn: - raise NotImplementedError() - - def writelines(self, lines: Iterable[AnyStr]) -> NoReturn: - raise NotImplementedError() - - def truncate(self, size: int | None = None) -> int: - raise NotImplementedError() - - def __enter__(self) -> ContainerIO[AnyStr]: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def __iter__(self) -> ContainerIO[AnyStr]: - return self - - def __next__(self) -> AnyStr: - line = self.readline() - if not line: - msg = "end of region" - raise StopIteration(msg) - return line - - def fileno(self) -> int: - return self.fh.fileno() - - def flush(self) -> None: - self.fh.flush() - - def close(self) -> None: - self.fh.close() diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/CurImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/CurImagePlugin.py deleted file mode 100644 index 9c188e0..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/CurImagePlugin.py +++ /dev/null @@ -1,75 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Windows Cursor support for PIL -# -# notes: -# uses BmpImagePlugin.py to read the bitmap data. -# -# history: -# 96-05-27 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import BmpImagePlugin, Image -from ._binary import i16le as i16 -from ._binary import i32le as i32 - -# -# -------------------------------------------------------------------- - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\0\0\2\0") - - -## -# Image plugin for Windows Cursor files. - - -class CurImageFile(BmpImagePlugin.BmpImageFile): - format = "CUR" - format_description = "Windows Cursor" - - def _open(self) -> None: - assert self.fp is not None - offset = self.fp.tell() - - # check magic - s = self.fp.read(6) - if not _accept(s): - msg = "not a CUR file" - raise SyntaxError(msg) - - # pick the largest cursor in the file - m = b"" - for i in range(i16(s, 4)): - s = self.fp.read(16) - if not m: - m = s - elif s[0] > m[0] and s[1] > m[1]: - m = s - if not m: - msg = "No cursors were found" - raise TypeError(msg) - - # load as bitmap - self._bitmap(i32(m, 12) + offset) - - # patch up the bitmap height - self._size = self.size[0], self.size[1] // 2 - self.tile = [self.tile[0]._replace(extents=(0, 0) + self.size)] - - -# -# -------------------------------------------------------------------- - -Image.register_open(CurImageFile.format, CurImageFile, _accept) - -Image.register_extension(CurImageFile.format, ".cur") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/DcxImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/DcxImagePlugin.py deleted file mode 100644 index d3f456d..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/DcxImagePlugin.py +++ /dev/null @@ -1,84 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# DCX file handling -# -# DCX is a container file format defined by Intel, commonly used -# for fax applications. Each DCX file consists of a directory -# (a list of file offsets) followed by a set of (usually 1-bit) -# PCX files. -# -# History: -# 1995-09-09 fl Created -# 1996-03-20 fl Properly derived from PcxImageFile. -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 2002-07-30 fl Fixed file handling -# -# Copyright (c) 1997-98 by Secret Labs AB. -# Copyright (c) 1995-96 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image -from ._binary import i32le as i32 -from ._util import DeferredError -from .PcxImagePlugin import PcxImageFile - -MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 4 and i32(prefix) == MAGIC - - -## -# Image plugin for the Intel DCX format. - - -class DcxImageFile(PcxImageFile): - format = "DCX" - format_description = "Intel DCX" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # Header - assert self.fp is not None - s = self.fp.read(4) - if not _accept(s): - msg = "not a DCX file" - raise SyntaxError(msg) - - # Component directory - self._offset = [] - for i in range(1024): - offset = i32(self.fp.read(4)) - if not offset: - break - self._offset.append(offset) - - self._fp = self.fp - self.frame = -1 - self.n_frames = len(self._offset) - self.is_animated = self.n_frames > 1 - self.seek(0) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self.frame = frame - self.fp = self._fp - self.fp.seek(self._offset[frame]) - PcxImageFile._open(self) - - def tell(self) -> int: - return self.frame - - -Image.register_open(DcxImageFile.format, DcxImageFile, _accept) - -Image.register_extension(DcxImageFile.format, ".dcx") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/DdsImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/DdsImagePlugin.py deleted file mode 100644 index 312f602..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/DdsImagePlugin.py +++ /dev/null @@ -1,625 +0,0 @@ -""" -A Pillow plugin for .dds files (S3TC-compressed aka DXTC) -Jerome Leclanche <jerome@leclan.ch> - -Documentation: -https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: -https://creativecommons.org/publicdomain/zero/1.0/ -""" - -from __future__ import annotations - -import struct -import sys -from enum import IntEnum, IntFlag -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i32le as i32 -from ._binary import o8 -from ._binary import o32le as o32 - -# Magic ("DDS ") -DDS_MAGIC = 0x20534444 - - -# DDS flags -class DDSD(IntFlag): - CAPS = 0x1 - HEIGHT = 0x2 - WIDTH = 0x4 - PITCH = 0x8 - PIXELFORMAT = 0x1000 - MIPMAPCOUNT = 0x20000 - LINEARSIZE = 0x80000 - DEPTH = 0x800000 - - -# DDS caps -class DDSCAPS(IntFlag): - COMPLEX = 0x8 - TEXTURE = 0x1000 - MIPMAP = 0x400000 - - -class DDSCAPS2(IntFlag): - CUBEMAP = 0x200 - CUBEMAP_POSITIVEX = 0x400 - CUBEMAP_NEGATIVEX = 0x800 - CUBEMAP_POSITIVEY = 0x1000 - CUBEMAP_NEGATIVEY = 0x2000 - CUBEMAP_POSITIVEZ = 0x4000 - CUBEMAP_NEGATIVEZ = 0x8000 - VOLUME = 0x200000 - - -# Pixel Format -class DDPF(IntFlag): - ALPHAPIXELS = 0x1 - ALPHA = 0x2 - FOURCC = 0x4 - PALETTEINDEXED8 = 0x20 - RGB = 0x40 - LUMINANCE = 0x20000 - - -# dxgiformat.h -class DXGI_FORMAT(IntEnum): - UNKNOWN = 0 - R32G32B32A32_TYPELESS = 1 - R32G32B32A32_FLOAT = 2 - R32G32B32A32_UINT = 3 - R32G32B32A32_SINT = 4 - R32G32B32_TYPELESS = 5 - R32G32B32_FLOAT = 6 - R32G32B32_UINT = 7 - R32G32B32_SINT = 8 - R16G16B16A16_TYPELESS = 9 - R16G16B16A16_FLOAT = 10 - R16G16B16A16_UNORM = 11 - R16G16B16A16_UINT = 12 - R16G16B16A16_SNORM = 13 - R16G16B16A16_SINT = 14 - R32G32_TYPELESS = 15 - R32G32_FLOAT = 16 - R32G32_UINT = 17 - R32G32_SINT = 18 - R32G8X24_TYPELESS = 19 - D32_FLOAT_S8X24_UINT = 20 - R32_FLOAT_X8X24_TYPELESS = 21 - X32_TYPELESS_G8X24_UINT = 22 - R10G10B10A2_TYPELESS = 23 - R10G10B10A2_UNORM = 24 - R10G10B10A2_UINT = 25 - R11G11B10_FLOAT = 26 - R8G8B8A8_TYPELESS = 27 - R8G8B8A8_UNORM = 28 - R8G8B8A8_UNORM_SRGB = 29 - R8G8B8A8_UINT = 30 - R8G8B8A8_SNORM = 31 - R8G8B8A8_SINT = 32 - R16G16_TYPELESS = 33 - R16G16_FLOAT = 34 - R16G16_UNORM = 35 - R16G16_UINT = 36 - R16G16_SNORM = 37 - R16G16_SINT = 38 - R32_TYPELESS = 39 - D32_FLOAT = 40 - R32_FLOAT = 41 - R32_UINT = 42 - R32_SINT = 43 - R24G8_TYPELESS = 44 - D24_UNORM_S8_UINT = 45 - R24_UNORM_X8_TYPELESS = 46 - X24_TYPELESS_G8_UINT = 47 - R8G8_TYPELESS = 48 - R8G8_UNORM = 49 - R8G8_UINT = 50 - R8G8_SNORM = 51 - R8G8_SINT = 52 - R16_TYPELESS = 53 - R16_FLOAT = 54 - D16_UNORM = 55 - R16_UNORM = 56 - R16_UINT = 57 - R16_SNORM = 58 - R16_SINT = 59 - R8_TYPELESS = 60 - R8_UNORM = 61 - R8_UINT = 62 - R8_SNORM = 63 - R8_SINT = 64 - A8_UNORM = 65 - R1_UNORM = 66 - R9G9B9E5_SHAREDEXP = 67 - R8G8_B8G8_UNORM = 68 - G8R8_G8B8_UNORM = 69 - BC1_TYPELESS = 70 - BC1_UNORM = 71 - BC1_UNORM_SRGB = 72 - BC2_TYPELESS = 73 - BC2_UNORM = 74 - BC2_UNORM_SRGB = 75 - BC3_TYPELESS = 76 - BC3_UNORM = 77 - BC3_UNORM_SRGB = 78 - BC4_TYPELESS = 79 - BC4_UNORM = 80 - BC4_SNORM = 81 - BC5_TYPELESS = 82 - BC5_UNORM = 83 - BC5_SNORM = 84 - B5G6R5_UNORM = 85 - B5G5R5A1_UNORM = 86 - B8G8R8A8_UNORM = 87 - B8G8R8X8_UNORM = 88 - R10G10B10_XR_BIAS_A2_UNORM = 89 - B8G8R8A8_TYPELESS = 90 - B8G8R8A8_UNORM_SRGB = 91 - B8G8R8X8_TYPELESS = 92 - B8G8R8X8_UNORM_SRGB = 93 - BC6H_TYPELESS = 94 - BC6H_UF16 = 95 - BC6H_SF16 = 96 - BC7_TYPELESS = 97 - BC7_UNORM = 98 - BC7_UNORM_SRGB = 99 - AYUV = 100 - Y410 = 101 - Y416 = 102 - NV12 = 103 - P010 = 104 - P016 = 105 - OPAQUE_420 = 106 - YUY2 = 107 - Y210 = 108 - Y216 = 109 - NV11 = 110 - AI44 = 111 - IA44 = 112 - P8 = 113 - A8P8 = 114 - B4G4R4A4_UNORM = 115 - P208 = 130 - V208 = 131 - V408 = 132 - SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189 - SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190 - - -class D3DFMT(IntEnum): - UNKNOWN = 0 - R8G8B8 = 20 - A8R8G8B8 = 21 - X8R8G8B8 = 22 - R5G6B5 = 23 - X1R5G5B5 = 24 - A1R5G5B5 = 25 - A4R4G4B4 = 26 - R3G3B2 = 27 - A8 = 28 - A8R3G3B2 = 29 - X4R4G4B4 = 30 - A2B10G10R10 = 31 - A8B8G8R8 = 32 - X8B8G8R8 = 33 - G16R16 = 34 - A2R10G10B10 = 35 - A16B16G16R16 = 36 - A8P8 = 40 - P8 = 41 - L8 = 50 - A8L8 = 51 - A4L4 = 52 - V8U8 = 60 - L6V5U5 = 61 - X8L8V8U8 = 62 - Q8W8V8U8 = 63 - V16U16 = 64 - A2W10V10U10 = 67 - D16_LOCKABLE = 70 - D32 = 71 - D15S1 = 73 - D24S8 = 75 - D24X8 = 77 - D24X4S4 = 79 - D16 = 80 - D32F_LOCKABLE = 82 - D24FS8 = 83 - D32_LOCKABLE = 84 - S8_LOCKABLE = 85 - L16 = 81 - VERTEXDATA = 100 - INDEX16 = 101 - INDEX32 = 102 - Q16W16V16U16 = 110 - R16F = 111 - G16R16F = 112 - A16B16G16R16F = 113 - R32F = 114 - G32R32F = 115 - A32B32G32R32F = 116 - CxV8U8 = 117 - A1 = 118 - A2B10G10R10_XR_BIAS = 119 - BINARYBUFFER = 199 - - UYVY = i32(b"UYVY") - R8G8_B8G8 = i32(b"RGBG") - YUY2 = i32(b"YUY2") - G8R8_G8B8 = i32(b"GRGB") - DXT1 = i32(b"DXT1") - DXT2 = i32(b"DXT2") - DXT3 = i32(b"DXT3") - DXT4 = i32(b"DXT4") - DXT5 = i32(b"DXT5") - DX10 = i32(b"DX10") - BC4S = i32(b"BC4S") - BC4U = i32(b"BC4U") - BC5S = i32(b"BC5S") - BC5U = i32(b"BC5U") - ATI1 = i32(b"ATI1") - ATI2 = i32(b"ATI2") - MULTI2_ARGB8 = i32(b"MET1") - - -# Backward compatibility layer -module = sys.modules[__name__] -for item in DDSD: - assert item.name is not None - setattr(module, f"DDSD_{item.name}", item.value) -for item1 in DDSCAPS: - assert item1.name is not None - setattr(module, f"DDSCAPS_{item1.name}", item1.value) -for item2 in DDSCAPS2: - assert item2.name is not None - setattr(module, f"DDSCAPS2_{item2.name}", item2.value) -for item3 in DDPF: - assert item3.name is not None - setattr(module, f"DDPF_{item3.name}", item3.value) - -DDS_FOURCC = DDPF.FOURCC -DDS_RGB = DDPF.RGB -DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS -DDS_LUMINANCE = DDPF.LUMINANCE -DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS -DDS_ALPHA = DDPF.ALPHA -DDS_PAL8 = DDPF.PALETTEINDEXED8 - -DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT -DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT -DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH -DDS_HEADER_FLAGS_PITCH = DDSD.PITCH -DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE - -DDS_HEIGHT = DDSD.HEIGHT -DDS_WIDTH = DDSD.WIDTH - -DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE -DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP -DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX - -DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX -DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX -DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY -DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY -DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ -DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ - -DXT1_FOURCC = D3DFMT.DXT1 -DXT3_FOURCC = D3DFMT.DXT3 -DXT5_FOURCC = D3DFMT.DXT5 - -DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS -DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM -DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB -DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS -DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM -DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM -DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16 -DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16 -DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS -DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM -DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB - - -class DdsImageFile(ImageFile.ImageFile): - format = "DDS" - format_description = "DirectDraw Surface" - - def _open(self) -> None: - assert self.fp is not None - if not _accept(self.fp.read(4)): - msg = "not a DDS file" - raise SyntaxError(msg) - (header_size,) = struct.unpack("<I", self.fp.read(4)) - if header_size != 124: - msg = f"Unsupported header size {repr(header_size)}" - raise OSError(msg) - header = self.fp.read(header_size - 4) - if len(header) != 120: - msg = f"Incomplete header: {len(header)} bytes" - raise OSError(msg) - - flags, height, width = struct.unpack("<3I", header[:12]) - self._size = (width, height) - extents = (0, 0) + self.size - - pitch, depth, mipmaps = struct.unpack("<3I", header[12:24]) - struct.unpack("<11I", header[24:68]) # reserved - - # pixel format - pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header[68:84]) - n = 0 - rawmode = None - if pfflags & DDPF.RGB: - # Texture contains uncompressed RGB data - if pfflags & DDPF.ALPHAPIXELS: - self._mode = "RGBA" - mask_count = 4 - else: - self._mode = "RGB" - mask_count = 3 - - masks = struct.unpack(f"<{mask_count}I", header[84 : 84 + mask_count * 4]) - self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))] - return - elif pfflags & DDPF.LUMINANCE: - if bitcount == 8: - self._mode = "L" - elif bitcount == 16 and pfflags & DDPF.ALPHAPIXELS: - self._mode = "LA" - else: - msg = f"Unsupported bitcount {bitcount} for {pfflags}" - raise OSError(msg) - elif pfflags & DDPF.PALETTEINDEXED8: - self._mode = "P" - self.palette = ImagePalette.raw("RGBA", self.fp.read(1024)) - self.palette.mode = "RGBA" - elif pfflags & DDPF.FOURCC: - offset = header_size + 4 - if fourcc == D3DFMT.DXT1: - self._mode = "RGBA" - self.pixel_format = "DXT1" - n = 1 - elif fourcc == D3DFMT.DXT3: - self._mode = "RGBA" - self.pixel_format = "DXT3" - n = 2 - elif fourcc == D3DFMT.DXT5: - self._mode = "RGBA" - self.pixel_format = "DXT5" - n = 3 - elif fourcc in (D3DFMT.BC4U, D3DFMT.ATI1): - self._mode = "L" - self.pixel_format = "BC4" - n = 4 - elif fourcc == D3DFMT.BC5S: - self._mode = "RGB" - self.pixel_format = "BC5S" - n = 5 - elif fourcc in (D3DFMT.BC5U, D3DFMT.ATI2): - self._mode = "RGB" - self.pixel_format = "BC5" - n = 5 - elif fourcc == D3DFMT.DX10: - offset += 20 - # ignoring flags which pertain to volume textures and cubemaps - (dxgi_format,) = struct.unpack("<I", self.fp.read(4)) - self.fp.read(16) - if dxgi_format in ( - DXGI_FORMAT.BC1_UNORM, - DXGI_FORMAT.BC1_TYPELESS, - ): - self._mode = "RGBA" - self.pixel_format = "BC1" - n = 1 - elif dxgi_format in (DXGI_FORMAT.BC2_TYPELESS, DXGI_FORMAT.BC2_UNORM): - self._mode = "RGBA" - self.pixel_format = "BC2" - n = 2 - elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM): - self._mode = "RGBA" - self.pixel_format = "BC3" - n = 3 - elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM): - self._mode = "L" - self.pixel_format = "BC4" - n = 4 - elif dxgi_format in (DXGI_FORMAT.BC5_TYPELESS, DXGI_FORMAT.BC5_UNORM): - self._mode = "RGB" - self.pixel_format = "BC5" - n = 5 - elif dxgi_format == DXGI_FORMAT.BC5_SNORM: - self._mode = "RGB" - self.pixel_format = "BC5S" - n = 5 - elif dxgi_format == DXGI_FORMAT.BC6H_UF16: - self._mode = "RGB" - self.pixel_format = "BC6H" - n = 6 - elif dxgi_format == DXGI_FORMAT.BC6H_SF16: - self._mode = "RGB" - self.pixel_format = "BC6HS" - n = 6 - elif dxgi_format in ( - DXGI_FORMAT.BC7_TYPELESS, - DXGI_FORMAT.BC7_UNORM, - DXGI_FORMAT.BC7_UNORM_SRGB, - ): - self._mode = "RGBA" - self.pixel_format = "BC7" - n = 7 - if dxgi_format == DXGI_FORMAT.BC7_UNORM_SRGB: - self.info["gamma"] = 1 / 2.2 - elif dxgi_format in ( - DXGI_FORMAT.R8G8B8A8_TYPELESS, - DXGI_FORMAT.R8G8B8A8_UNORM, - DXGI_FORMAT.R8G8B8A8_UNORM_SRGB, - ): - self._mode = "RGBA" - if dxgi_format == DXGI_FORMAT.R8G8B8A8_UNORM_SRGB: - self.info["gamma"] = 1 / 2.2 - else: - msg = f"Unimplemented DXGI format {dxgi_format}" - raise NotImplementedError(msg) - else: - msg = f"Unimplemented pixel format {repr(fourcc)}" - raise NotImplementedError(msg) - else: - msg = f"Unknown pixel format flags {pfflags}" - raise NotImplementedError(msg) - - if n: - self.tile = [ - ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format)) - ] - else: - self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)] - - def load_seek(self, pos: int) -> None: - pass - - -class DdsRgbDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - bitcount, masks = self.args - - # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 - # Calculate how many zeros each mask is padded with - mask_offsets = [] - # And the maximum value of each channel without the padding - mask_totals = [] - for mask in masks: - offset = 0 - if mask != 0: - while mask >> (offset + 1) << (offset + 1) == mask: - offset += 1 - mask_offsets.append(offset) - mask_totals.append(mask >> offset) - - data = bytearray() - bytecount = bitcount // 8 - dest_length = self.state.xsize * self.state.ysize * len(masks) - while len(data) < dest_length: - value = int.from_bytes(self.fd.read(bytecount), "little") - for i, mask in enumerate(masks): - masked_value = value & mask - # Remove the zero padding, and scale it to 8 bits - data += o8( - int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) - if mask_totals[i] - else 0 - ) - self.set_as_raw(data) - return -1, 0 - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode not in ("RGB", "RGBA", "L", "LA"): - msg = f"cannot write mode {im.mode} as DDS" - raise OSError(msg) - - flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT - bitcount = len(im.getbands()) * 8 - pixel_format = im.encoderinfo.get("pixel_format") - args: tuple[int] | str - if pixel_format: - codec_name = "bcn" - flags |= DDSD.LINEARSIZE - pitch = (im.width + 3) * 4 - rgba_mask = [0, 0, 0, 0] - pixel_flags = DDPF.FOURCC - if pixel_format == "DXT1": - fourcc = D3DFMT.DXT1 - args = (1,) - elif pixel_format == "DXT3": - fourcc = D3DFMT.DXT3 - args = (2,) - elif pixel_format == "DXT5": - fourcc = D3DFMT.DXT5 - args = (3,) - else: - fourcc = D3DFMT.DX10 - if pixel_format == "BC2": - args = (2,) - dxgi_format = DXGI_FORMAT.BC2_TYPELESS - elif pixel_format == "BC3": - args = (3,) - dxgi_format = DXGI_FORMAT.BC3_TYPELESS - elif pixel_format == "BC5": - args = (5,) - dxgi_format = DXGI_FORMAT.BC5_TYPELESS - if im.mode != "RGB": - msg = "only RGB mode can be written as BC5" - raise OSError(msg) - else: - msg = f"cannot write pixel format {pixel_format}" - raise OSError(msg) - else: - codec_name = "raw" - flags |= DDSD.PITCH - pitch = (im.width * bitcount + 7) // 8 - - alpha = im.mode[-1] == "A" - if im.mode[0] == "L": - pixel_flags = DDPF.LUMINANCE - args = im.mode - if alpha: - rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] - else: - rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] - else: - pixel_flags = DDPF.RGB - args = im.mode[::-1] - rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] - - if alpha: - r, g, b, a = im.split() - im = Image.merge("RGBA", (a, r, g, b)) - if alpha: - pixel_flags |= DDPF.ALPHAPIXELS - rgba_mask.append(0xFF000000 if alpha else 0) - - fourcc = D3DFMT.UNKNOWN - fp.write( - o32(DDS_MAGIC) - + struct.pack( - "<7I", - 124, # header size - flags, # flags - im.height, - im.width, - pitch, - 0, # depth - 0, # mipmaps - ) - + struct.pack("11I", *((0,) * 11)) # reserved - # pfsize, pfflags, fourcc, bitcount - + struct.pack("<4I", 32, pixel_flags, fourcc, bitcount) - + struct.pack("<4I", *rgba_mask) # dwRGBABitMask - + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) - ) - if fourcc == D3DFMT.DX10: - fp.write( - # dxgi_format, 2D resource, misc, array size, straight alpha - struct.pack("<5I", dxgi_format, 3, 0, 0, 1) - ) - ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)]) - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"DDS ") - - -Image.register_open(DdsImageFile.format, DdsImageFile, _accept) -Image.register_decoder("dds_rgb", DdsRgbDecoder) -Image.register_save(DdsImageFile.format, _save) -Image.register_extension(DdsImageFile.format, ".dds") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/EpsImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/EpsImagePlugin.py deleted file mode 100644 index aeb7b0c..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/EpsImagePlugin.py +++ /dev/null @@ -1,481 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# EPS file handling -# -# History: -# 1995-09-01 fl Created (0.1) -# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2) -# 1996-08-22 fl Don't choke on floating point BoundingBox values -# 1996-08-23 fl Handle files from Macintosh (0.3) -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) -# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5) -# 2014-05-07 e Handling of EPS with binary preview and fixed resolution -# resizing -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import re -import subprocess -import sys -import tempfile -from typing import IO - -from . import Image, ImageFile -from ._binary import i32le as i32 - -# -------------------------------------------------------------------- - - -split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") -field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") - -gs_binary: str | bool | None = None -gs_windows_binary = None - - -def has_ghostscript() -> bool: - global gs_binary, gs_windows_binary - if gs_binary is None: - if sys.platform.startswith("win"): - if gs_windows_binary is None: - import shutil - - for binary in ("gswin32c", "gswin64c", "gs"): - if shutil.which(binary) is not None: - gs_windows_binary = binary - break - else: - gs_windows_binary = False - gs_binary = gs_windows_binary - else: - try: - subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) - gs_binary = "gs" - except OSError: - gs_binary = False - return gs_binary is not False - - -def Ghostscript( - tile: list[ImageFile._Tile], - size: tuple[int, int], - fp: IO[bytes], - scale: int = 1, - transparency: bool = False, -) -> Image.core.ImagingCore: - """Render an image using Ghostscript""" - global gs_binary - if not has_ghostscript(): - msg = "Unable to locate Ghostscript on paths" - raise OSError(msg) - assert isinstance(gs_binary, str) - - # Unpack decoder tile - args = tile[0].args - assert isinstance(args, tuple) - length, bbox = args - - # Hack to support hi-res rendering - scale = int(scale) or 1 - width = size[0] * scale - height = size[1] * scale - # resolution is dependent on bbox and size - res_x = 72.0 * width / (bbox[2] - bbox[0]) - res_y = 72.0 * height / (bbox[3] - bbox[1]) - - out_fd, outfile = tempfile.mkstemp() - os.close(out_fd) - - infile_temp = None - if hasattr(fp, "name") and os.path.exists(fp.name): - infile = fp.name - else: - in_fd, infile_temp = tempfile.mkstemp() - os.close(in_fd) - infile = infile_temp - - # Ignore length and offset! - # Ghostscript can read it - # Copy whole file to read in Ghostscript - with open(infile_temp, "wb") as f: - # fetch length of fp - fp.seek(0, io.SEEK_END) - fsize = fp.tell() - # ensure start position - # go back - fp.seek(0) - lengthfile = fsize - while lengthfile > 0: - s = fp.read(min(lengthfile, 100 * 1024)) - if not s: - break - lengthfile -= len(s) - f.write(s) - - if transparency: - # "RGBA" - device = "pngalpha" - else: - # "pnmraw" automatically chooses between - # PBM ("1"), PGM ("L"), and PPM ("RGB"). - device = "pnmraw" - - # Build Ghostscript command - command = [ - gs_binary, - "-q", # quiet mode - f"-g{width:d}x{height:d}", # set output geometry (pixels) - f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch) - "-dBATCH", # exit after processing - "-dNOPAUSE", # don't pause between pages - "-dSAFER", # safe mode - f"-sDEVICE={device}", - f"-sOutputFile={outfile}", # output file - # adjust for image origin - "-c", - f"{-bbox[0]} {-bbox[1]} translate", - "-f", - infile, # input file - # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) - "-c", - "showpage", - ] - - # push data through Ghostscript - try: - startupinfo = None - if sys.platform.startswith("win"): - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - subprocess.check_call(command, startupinfo=startupinfo) - with Image.open(outfile) as out_im: - out_im.load() - return out_im.im.copy() - finally: - try: - os.unlink(outfile) - if infile_temp: - os.unlink(infile_temp) - except OSError: - pass - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"%!PS") or ( - len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5 - ) - - -## -# Image plugin for Encapsulated PostScript. This plugin supports only -# a few variants of this format. - - -class EpsImageFile(ImageFile.ImageFile): - """EPS File Parser for the Python Imaging Library""" - - format = "EPS" - format_description = "Encapsulated Postscript" - - mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} - - def _open(self) -> None: - assert self.fp is not None - length, offset = self._find_offset(self.fp) - - # go to offset - start of "%!PS" - self.fp.seek(offset) - - self._mode = "RGB" - - # When reading header comments, the first comment is used. - # When reading trailer comments, the last comment is used. - bounding_box: list[int] | None = None - imagedata_size: tuple[int, int] | None = None - - byte_arr = bytearray(255) - bytes_mv = memoryview(byte_arr) - bytes_read = 0 - reading_header_comments = True - reading_trailer_comments = False - trailer_reached = False - - def check_required_header_comments() -> None: - """ - The EPS specification requires that some headers exist. - This should be checked when the header comments formally end, - when image data starts, or when the file ends, whichever comes first. - """ - if "PS-Adobe" not in self.info: - msg = 'EPS header missing "%!PS-Adobe" comment' - raise SyntaxError(msg) - if "BoundingBox" not in self.info: - msg = 'EPS header missing "%%BoundingBox" comment' - raise SyntaxError(msg) - - def read_comment(s: str) -> bool: - nonlocal bounding_box, reading_trailer_comments - try: - m = split.match(s) - except re.error as e: - msg = "not an EPS file" - raise SyntaxError(msg) from e - - if not m: - return False - - k, v = m.group(1, 2) - self.info[k] = v - if k == "BoundingBox": - if v == "(atend)": - reading_trailer_comments = True - elif not bounding_box or (trailer_reached and reading_trailer_comments): - try: - # Note: The DSC spec says that BoundingBox - # fields should be integers, but some drivers - # put floating point values there anyway. - bounding_box = [int(float(i)) for i in v.split()] - except Exception: - pass - return True - - while True: - byte = self.fp.read(1) - if byte == b"": - # if we didn't read a byte we must be at the end of the file - if bytes_read == 0: - if reading_header_comments: - check_required_header_comments() - break - elif byte in b"\r\n": - # if we read a line ending character, ignore it and parse what - # we have already read. if we haven't read any other characters, - # continue reading - if bytes_read == 0: - continue - else: - # ASCII/hexadecimal lines in an EPS file must not exceed - # 255 characters, not including line ending characters - if bytes_read >= 255: - # only enforce this for lines starting with a "%", - # otherwise assume it's binary data - if byte_arr[0] == ord("%"): - msg = "not an EPS file" - raise SyntaxError(msg) - else: - if reading_header_comments: - check_required_header_comments() - reading_header_comments = False - # reset bytes_read so we can keep reading - # data until the end of the line - bytes_read = 0 - byte_arr[bytes_read] = byte[0] - bytes_read += 1 - continue - - if reading_header_comments: - # Load EPS header - - # if this line doesn't start with a "%", - # or does start with "%%EndComments", - # then we've reached the end of the header/comments - if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": - check_required_header_comments() - reading_header_comments = False - continue - - s = str(bytes_mv[:bytes_read], "latin-1") - if not read_comment(s): - m = field.match(s) - if m: - k = m.group(1) - if k.startswith("PS-Adobe"): - self.info["PS-Adobe"] = k[9:] - else: - self.info[k] = "" - elif s[0] == "%": - # handle non-DSC PostScript comments that some - # tools mistakenly put in the Comments section - pass - else: - msg = "bad EPS header" - raise OSError(msg) - elif bytes_mv[:11] == b"%ImageData:": - # Check for an "ImageData" descriptor - # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 - - # If we've already read an "ImageData" descriptor, - # don't read another one. - if imagedata_size: - bytes_read = 0 - continue - - # Values: - # columns - # rows - # bit depth (1 or 8) - # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) - # number of padding channels - # block size (number of bytes per row per channel) - # binary/ascii (1: binary, 2: ascii) - # data start identifier (the image data follows after a single line - # consisting only of this quoted value) - image_data_values = byte_arr[11:bytes_read].split(None, 7) - columns, rows, bit_depth, mode_id = ( - int(value) for value in image_data_values[:4] - ) - - if bit_depth == 1: - self._mode = "1" - elif bit_depth == 8: - try: - self._mode = self.mode_map[mode_id] - except ValueError: - break - else: - break - - # Parse the columns and rows after checking the bit depth and mode - # in case the bit depth and/or mode are invalid. - imagedata_size = columns, rows - elif bytes_mv[:5] == b"%%EOF": - break - elif trailer_reached and reading_trailer_comments: - # Load EPS trailer - s = str(bytes_mv[:bytes_read], "latin-1") - read_comment(s) - elif bytes_mv[:9] == b"%%Trailer": - trailer_reached = True - elif bytes_mv[:14] == b"%%BeginBinary:": - bytecount = int(byte_arr[14:bytes_read]) - self.fp.seek(bytecount, os.SEEK_CUR) - bytes_read = 0 - - # A "BoundingBox" is always required, - # even if an "ImageData" descriptor size exists. - if not bounding_box: - msg = "cannot determine EPS bounding box" - raise OSError(msg) - - # An "ImageData" size takes precedence over the "BoundingBox". - self._size = imagedata_size or ( - bounding_box[2] - bounding_box[0], - bounding_box[3] - bounding_box[1], - ) - - self.tile = [ - ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box)) - ] - - def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: - s = fp.read(4) - - if s == b"%!PS": - # for HEAD without binary preview - fp.seek(0, io.SEEK_END) - length = fp.tell() - offset = 0 - elif i32(s) == 0xC6D3D0C5: - # FIX for: Some EPS file not handled correctly / issue #302 - # EPS can contain binary data - # or start directly with latin coding - # more info see: - # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - s = fp.read(8) - offset = i32(s) - length = i32(s, 4) - else: - msg = "not an EPS file" - raise SyntaxError(msg) - - return length, offset - - def load( - self, scale: int = 1, transparency: bool = False - ) -> Image.core.PixelAccess | None: - # Load EPS via Ghostscript - if self.tile: - assert self.fp is not None - self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) - self._mode = self.im.mode - self._size = self.im.size - self.tile = [] - return Image.Image.load(self) - - def load_seek(self, pos: int) -> None: - # we can't incrementally load, so force ImageFile.parser to - # use our custom load method by defining this method. - pass - - -# -------------------------------------------------------------------- - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None: - """EPS Writer for the Python Imaging Library.""" - - # make sure image data is available - im.load() - - # determine PostScript image mode - if im.mode == "L": - operator = (8, 1, b"image") - elif im.mode == "RGB": - operator = (8, 3, b"false 3 colorimage") - elif im.mode == "CMYK": - operator = (8, 4, b"false 4 colorimage") - else: - msg = "image mode is not supported" - raise ValueError(msg) - - if eps: - # write EPS header - fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") - fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") - # fp.write("%%CreationDate: %s"...) - fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size) - fp.write(b"%%Pages: 1\n") - fp.write(b"%%EndComments\n") - fp.write(b"%%Page: 1 1\n") - fp.write(b"%%ImageData: %d %d " % im.size) - fp.write(b'%d %d 0 1 1 "%s"\n' % operator) - - # image header - fp.write(b"gsave\n") - fp.write(b"10 dict begin\n") - fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1])) - fp.write(b"%d %d scale\n" % im.size) - fp.write(b"%d %d 8\n" % im.size) # <= bits - fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) - fp.write(b"{ currentfile buf readhexstring pop } bind\n") - fp.write(operator[2] + b"\n") - if hasattr(fp, "flush"): - fp.flush() - - ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) - - fp.write(b"\n%%%%EndBinary\n") - fp.write(b"grestore end\n") - if hasattr(fp, "flush"): - fp.flush() - - -# -------------------------------------------------------------------- - - -Image.register_open(EpsImageFile.format, EpsImageFile, _accept) - -Image.register_save(EpsImageFile.format, _save) - -Image.register_extensions(EpsImageFile.format, [".ps", ".eps"]) - -Image.register_mime(EpsImageFile.format, "application/postscript") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ExifTags.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ExifTags.py deleted file mode 100644 index a9522e7..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ExifTags.py +++ /dev/null @@ -1,384 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# EXIF tags -# -# Copyright (c) 2003 by Secret Labs AB -# -# See the README file for information on usage and redistribution. -# - -""" -This module provides constants and clear-text names for various -well-known EXIF tags. -""" - -from __future__ import annotations - -from enum import IntEnum - - -class Base(IntEnum): - # possibly incomplete - InteropIndex = 0x0001 - ProcessingSoftware = 0x000B - NewSubfileType = 0x00FE - SubfileType = 0x00FF - ImageWidth = 0x0100 - ImageLength = 0x0101 - BitsPerSample = 0x0102 - Compression = 0x0103 - PhotometricInterpretation = 0x0106 - Thresholding = 0x0107 - CellWidth = 0x0108 - CellLength = 0x0109 - FillOrder = 0x010A - DocumentName = 0x010D - ImageDescription = 0x010E - Make = 0x010F - Model = 0x0110 - StripOffsets = 0x0111 - Orientation = 0x0112 - SamplesPerPixel = 0x0115 - RowsPerStrip = 0x0116 - StripByteCounts = 0x0117 - MinSampleValue = 0x0118 - MaxSampleValue = 0x0119 - XResolution = 0x011A - YResolution = 0x011B - PlanarConfiguration = 0x011C - PageName = 0x011D - FreeOffsets = 0x0120 - FreeByteCounts = 0x0121 - GrayResponseUnit = 0x0122 - GrayResponseCurve = 0x0123 - T4Options = 0x0124 - T6Options = 0x0125 - ResolutionUnit = 0x0128 - PageNumber = 0x0129 - TransferFunction = 0x012D - Software = 0x0131 - DateTime = 0x0132 - Artist = 0x013B - HostComputer = 0x013C - Predictor = 0x013D - WhitePoint = 0x013E - PrimaryChromaticities = 0x013F - ColorMap = 0x0140 - HalftoneHints = 0x0141 - TileWidth = 0x0142 - TileLength = 0x0143 - TileOffsets = 0x0144 - TileByteCounts = 0x0145 - SubIFDs = 0x014A - InkSet = 0x014C - InkNames = 0x014D - NumberOfInks = 0x014E - DotRange = 0x0150 - TargetPrinter = 0x0151 - ExtraSamples = 0x0152 - SampleFormat = 0x0153 - SMinSampleValue = 0x0154 - SMaxSampleValue = 0x0155 - TransferRange = 0x0156 - ClipPath = 0x0157 - XClipPathUnits = 0x0158 - YClipPathUnits = 0x0159 - Indexed = 0x015A - JPEGTables = 0x015B - OPIProxy = 0x015F - JPEGProc = 0x0200 - JpegIFOffset = 0x0201 - JpegIFByteCount = 0x0202 - JpegRestartInterval = 0x0203 - JpegLosslessPredictors = 0x0205 - JpegPointTransforms = 0x0206 - JpegQTables = 0x0207 - JpegDCTables = 0x0208 - JpegACTables = 0x0209 - YCbCrCoefficients = 0x0211 - YCbCrSubSampling = 0x0212 - YCbCrPositioning = 0x0213 - ReferenceBlackWhite = 0x0214 - XMLPacket = 0x02BC - RelatedImageFileFormat = 0x1000 - RelatedImageWidth = 0x1001 - RelatedImageLength = 0x1002 - Rating = 0x4746 - RatingPercent = 0x4749 - ImageID = 0x800D - CFARepeatPatternDim = 0x828D - BatteryLevel = 0x828F - Copyright = 0x8298 - ExposureTime = 0x829A - FNumber = 0x829D - IPTCNAA = 0x83BB - ImageResources = 0x8649 - ExifOffset = 0x8769 - InterColorProfile = 0x8773 - ExposureProgram = 0x8822 - SpectralSensitivity = 0x8824 - GPSInfo = 0x8825 - ISOSpeedRatings = 0x8827 - OECF = 0x8828 - Interlace = 0x8829 - TimeZoneOffset = 0x882A - SelfTimerMode = 0x882B - SensitivityType = 0x8830 - StandardOutputSensitivity = 0x8831 - RecommendedExposureIndex = 0x8832 - ISOSpeed = 0x8833 - ISOSpeedLatitudeyyy = 0x8834 - ISOSpeedLatitudezzz = 0x8835 - ExifVersion = 0x9000 - DateTimeOriginal = 0x9003 - DateTimeDigitized = 0x9004 - OffsetTime = 0x9010 - OffsetTimeOriginal = 0x9011 - OffsetTimeDigitized = 0x9012 - ComponentsConfiguration = 0x9101 - CompressedBitsPerPixel = 0x9102 - ShutterSpeedValue = 0x9201 - ApertureValue = 0x9202 - BrightnessValue = 0x9203 - ExposureBiasValue = 0x9204 - MaxApertureValue = 0x9205 - SubjectDistance = 0x9206 - MeteringMode = 0x9207 - LightSource = 0x9208 - Flash = 0x9209 - FocalLength = 0x920A - Noise = 0x920D - ImageNumber = 0x9211 - SecurityClassification = 0x9212 - ImageHistory = 0x9213 - TIFFEPStandardID = 0x9216 - MakerNote = 0x927C - UserComment = 0x9286 - SubsecTime = 0x9290 - SubsecTimeOriginal = 0x9291 - SubsecTimeDigitized = 0x9292 - AmbientTemperature = 0x9400 - Humidity = 0x9401 - Pressure = 0x9402 - WaterDepth = 0x9403 - Acceleration = 0x9404 - CameraElevationAngle = 0x9405 - XPTitle = 0x9C9B - XPComment = 0x9C9C - XPAuthor = 0x9C9D - XPKeywords = 0x9C9E - XPSubject = 0x9C9F - FlashPixVersion = 0xA000 - ColorSpace = 0xA001 - ExifImageWidth = 0xA002 - ExifImageHeight = 0xA003 - RelatedSoundFile = 0xA004 - ExifInteroperabilityOffset = 0xA005 - FlashEnergy = 0xA20B - SpatialFrequencyResponse = 0xA20C - FocalPlaneXResolution = 0xA20E - FocalPlaneYResolution = 0xA20F - FocalPlaneResolutionUnit = 0xA210 - SubjectLocation = 0xA214 - ExposureIndex = 0xA215 - SensingMethod = 0xA217 - FileSource = 0xA300 - SceneType = 0xA301 - CFAPattern = 0xA302 - CustomRendered = 0xA401 - ExposureMode = 0xA402 - WhiteBalance = 0xA403 - DigitalZoomRatio = 0xA404 - FocalLengthIn35mmFilm = 0xA405 - SceneCaptureType = 0xA406 - GainControl = 0xA407 - Contrast = 0xA408 - Saturation = 0xA409 - Sharpness = 0xA40A - DeviceSettingDescription = 0xA40B - SubjectDistanceRange = 0xA40C - ImageUniqueID = 0xA420 - CameraOwnerName = 0xA430 - BodySerialNumber = 0xA431 - LensSpecification = 0xA432 - LensMake = 0xA433 - LensModel = 0xA434 - LensSerialNumber = 0xA435 - CompositeImage = 0xA460 - CompositeImageCount = 0xA461 - CompositeImageExposureTimes = 0xA462 - Gamma = 0xA500 - PrintImageMatching = 0xC4A5 - DNGVersion = 0xC612 - DNGBackwardVersion = 0xC613 - UniqueCameraModel = 0xC614 - LocalizedCameraModel = 0xC615 - CFAPlaneColor = 0xC616 - CFALayout = 0xC617 - LinearizationTable = 0xC618 - BlackLevelRepeatDim = 0xC619 - BlackLevel = 0xC61A - BlackLevelDeltaH = 0xC61B - BlackLevelDeltaV = 0xC61C - WhiteLevel = 0xC61D - DefaultScale = 0xC61E - DefaultCropOrigin = 0xC61F - DefaultCropSize = 0xC620 - ColorMatrix1 = 0xC621 - ColorMatrix2 = 0xC622 - CameraCalibration1 = 0xC623 - CameraCalibration2 = 0xC624 - ReductionMatrix1 = 0xC625 - ReductionMatrix2 = 0xC626 - AnalogBalance = 0xC627 - AsShotNeutral = 0xC628 - AsShotWhiteXY = 0xC629 - BaselineExposure = 0xC62A - BaselineNoise = 0xC62B - BaselineSharpness = 0xC62C - BayerGreenSplit = 0xC62D - LinearResponseLimit = 0xC62E - CameraSerialNumber = 0xC62F - LensInfo = 0xC630 - ChromaBlurRadius = 0xC631 - AntiAliasStrength = 0xC632 - ShadowScale = 0xC633 - DNGPrivateData = 0xC634 - MakerNoteSafety = 0xC635 - CalibrationIlluminant1 = 0xC65A - CalibrationIlluminant2 = 0xC65B - BestQualityScale = 0xC65C - RawDataUniqueID = 0xC65D - OriginalRawFileName = 0xC68B - OriginalRawFileData = 0xC68C - ActiveArea = 0xC68D - MaskedAreas = 0xC68E - AsShotICCProfile = 0xC68F - AsShotPreProfileMatrix = 0xC690 - CurrentICCProfile = 0xC691 - CurrentPreProfileMatrix = 0xC692 - ColorimetricReference = 0xC6BF - CameraCalibrationSignature = 0xC6F3 - ProfileCalibrationSignature = 0xC6F4 - AsShotProfileName = 0xC6F6 - NoiseReductionApplied = 0xC6F7 - ProfileName = 0xC6F8 - ProfileHueSatMapDims = 0xC6F9 - ProfileHueSatMapData1 = 0xC6FA - ProfileHueSatMapData2 = 0xC6FB - ProfileToneCurve = 0xC6FC - ProfileEmbedPolicy = 0xC6FD - ProfileCopyright = 0xC6FE - ForwardMatrix1 = 0xC714 - ForwardMatrix2 = 0xC715 - PreviewApplicationName = 0xC716 - PreviewApplicationVersion = 0xC717 - PreviewSettingsName = 0xC718 - PreviewSettingsDigest = 0xC719 - PreviewColorSpace = 0xC71A - PreviewDateTime = 0xC71B - RawImageDigest = 0xC71C - OriginalRawFileDigest = 0xC71D - SubTileBlockSize = 0xC71E - RowInterleaveFactor = 0xC71F - ProfileLookTableDims = 0xC725 - ProfileLookTableData = 0xC726 - OpcodeList1 = 0xC740 - OpcodeList2 = 0xC741 - OpcodeList3 = 0xC74E - NoiseProfile = 0xC761 - FrameRate = 0xC764 - - -"""Maps EXIF tags to tag names.""" -TAGS = { - **{i.value: i.name for i in Base}, - 0x920C: "SpatialFrequencyResponse", - 0x9214: "SubjectLocation", - 0x9215: "ExposureIndex", - 0x828E: "CFAPattern", - 0x920B: "FlashEnergy", - 0x9216: "TIFF/EPStandardID", -} - - -class GPS(IntEnum): - GPSVersionID = 0x00 - GPSLatitudeRef = 0x01 - GPSLatitude = 0x02 - GPSLongitudeRef = 0x03 - GPSLongitude = 0x04 - GPSAltitudeRef = 0x05 - GPSAltitude = 0x06 - GPSTimeStamp = 0x07 - GPSSatellites = 0x08 - GPSStatus = 0x09 - GPSMeasureMode = 0x0A - GPSDOP = 0x0B - GPSSpeedRef = 0x0C - GPSSpeed = 0x0D - GPSTrackRef = 0x0E - GPSTrack = 0x0F - GPSImgDirectionRef = 0x10 - GPSImgDirection = 0x11 - GPSMapDatum = 0x12 - GPSDestLatitudeRef = 0x13 - GPSDestLatitude = 0x14 - GPSDestLongitudeRef = 0x15 - GPSDestLongitude = 0x16 - GPSDestBearingRef = 0x17 - GPSDestBearing = 0x18 - GPSDestDistanceRef = 0x19 - GPSDestDistance = 0x1A - GPSProcessingMethod = 0x1B - GPSAreaInformation = 0x1C - GPSDateStamp = 0x1D - GPSDifferential = 0x1E - GPSHPositioningError = 0x1F - - -"""Maps EXIF GPS tags to tag names.""" -GPSTAGS = {i.value: i.name for i in GPS} - - -class Interop(IntEnum): - InteropIndex = 0x0001 - InteropVersion = 0x0002 - RelatedImageFileFormat = 0x1000 - RelatedImageWidth = 0x1001 - RelatedImageHeight = 0x1002 - - -class IFD(IntEnum): - Exif = 0x8769 - GPSInfo = 0x8825 - MakerNote = 0x927C - Makernote = 0x927C # Deprecated - Interop = 0xA005 - IFD1 = -1 - - -class LightSource(IntEnum): - Unknown = 0x00 - Daylight = 0x01 - Fluorescent = 0x02 - Tungsten = 0x03 - Flash = 0x04 - Fine = 0x09 - Cloudy = 0x0A - Shade = 0x0B - DaylightFluorescent = 0x0C - DayWhiteFluorescent = 0x0D - CoolWhiteFluorescent = 0x0E - WhiteFluorescent = 0x0F - StandardLightA = 0x11 - StandardLightB = 0x12 - StandardLightC = 0x13 - D55 = 0x14 - D65 = 0x15 - D75 = 0x16 - D50 = 0x17 - ISO = 0x18 - Other = 0xFF diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/FitsImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/FitsImagePlugin.py deleted file mode 100644 index e918407..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/FitsImagePlugin.py +++ /dev/null @@ -1,153 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# FITS file handling -# -# Copyright (c) 1998-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import gzip -import math - -from . import Image, ImageFile - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"SIMPLE") - - -class FitsImageFile(ImageFile.ImageFile): - format = "FITS" - format_description = "FITS" - - def _open(self) -> None: - assert self.fp is not None - - headers: dict[bytes, bytes] = {} - header_in_progress = False - decoder_name = "" - while True: - header = self.fp.read(80) - if not header: - msg = "Truncated FITS file" - raise OSError(msg) - keyword = header[:8].strip() - if keyword in (b"SIMPLE", b"XTENSION"): - header_in_progress = True - elif headers and not header_in_progress: - # This is now a data unit - break - elif keyword == b"END": - # Seek to the end of the header unit - self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880) - if not decoder_name: - decoder_name, offset, args = self._parse_headers(headers) - - header_in_progress = False - continue - - if decoder_name: - # Keep going to read past the headers - continue - - value = header[8:].split(b"/")[0].strip() - if value.startswith(b"="): - value = value[1:].strip() - if not headers and (not _accept(keyword) or value != b"T"): - msg = "Not a FITS file" - raise SyntaxError(msg) - headers[keyword] = value - - if not decoder_name: - msg = "No image data" - raise ValueError(msg) - - offset += self.fp.tell() - 80 - self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)] - - def _get_size( - self, headers: dict[bytes, bytes], prefix: bytes - ) -> tuple[int, int] | None: - naxis = int(headers[prefix + b"NAXIS"]) - if naxis == 0: - return None - - if naxis == 1: - return 1, int(headers[prefix + b"NAXIS1"]) - else: - return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"]) - - def _parse_headers( - self, headers: dict[bytes, bytes] - ) -> tuple[str, int, tuple[str | int, ...]]: - prefix = b"" - decoder_name = "raw" - offset = 0 - if ( - headers.get(b"XTENSION") == b"'BINTABLE'" - and headers.get(b"ZIMAGE") == b"T" - and headers[b"ZCMPTYPE"] == b"'GZIP_1 '" - ): - no_prefix_size = self._get_size(headers, prefix) or (0, 0) - number_of_bits = int(headers[b"BITPIX"]) - offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8) - - prefix = b"Z" - decoder_name = "fits_gzip" - - size = self._get_size(headers, prefix) - if not size: - return "", 0, () - - self._size = size - - number_of_bits = int(headers[prefix + b"BITPIX"]) - if number_of_bits == 8: - self._mode = "L" - elif number_of_bits == 16: - self._mode = "I;16" - elif number_of_bits == 32: - self._mode = "I" - elif number_of_bits in (-32, -64): - self._mode = "F" - - args: tuple[str | int, ...] - if decoder_name == "raw": - args = (self.mode, 0, -1) - else: - args = (number_of_bits,) - return decoder_name, offset, args - - -class FitsGzipDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - with gzip.open(self.fd) as fp: - value = fp.read(self.state.xsize * self.state.ysize * 4) - - rows = [] - offset = 0 - number_of_bits = min(self.args[0] // 8, 4) - for y in range(self.state.ysize): - row = bytearray() - for x in range(self.state.xsize): - row += value[offset + (4 - number_of_bits) : offset + 4] - offset += 4 - rows.append(row) - self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row])) - return -1, 0 - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(FitsImageFile.format, FitsImageFile, _accept) -Image.register_decoder("fits_gzip", FitsGzipDecoder) - -Image.register_extensions(FitsImageFile.format, [".fit", ".fits"]) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/FliImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/FliImagePlugin.py deleted file mode 100644 index da1e8e9..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/FliImagePlugin.py +++ /dev/null @@ -1,184 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# FLI/FLC file handling. -# -# History: -# 95-09-01 fl Created -# 97-01-03 fl Fixed parser, setup decoder tile -# 98-07-15 fl Renamed offset attribute to avoid name clash -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1995-97. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import i32le as i32 -from ._binary import o8 -from ._util import DeferredError - -# -# decoder - - -def _accept(prefix: bytes) -> bool: - return ( - len(prefix) >= 16 - and i16(prefix, 4) in [0xAF11, 0xAF12] - and i16(prefix, 14) in [0, 3] # flags - ) - - -## -# Image plugin for the FLI/FLC animation format. Use the <b>seek</b> -# method to load individual frames. - - -class FliImageFile(ImageFile.ImageFile): - format = "FLI" - format_description = "Autodesk FLI/FLC Animation" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # HEAD - assert self.fp is not None - s = self.fp.read(128) - if not ( - _accept(s) - and s[20:22] == b"\x00" * 2 - and s[42:80] == b"\x00" * 38 - and s[88:] == b"\x00" * 40 - ): - msg = "not an FLI/FLC file" - raise SyntaxError(msg) - - # frames - self.n_frames = i16(s, 6) - self.is_animated = self.n_frames > 1 - - # image characteristics - self._mode = "P" - self._size = i16(s, 8), i16(s, 10) - - # animation speed - duration = i32(s, 16) - magic = i16(s, 4) - if magic == 0xAF11: - duration = (duration * 1000) // 70 - self.info["duration"] = duration - - # look for palette - palette = [(a, a, a) for a in range(256)] - - s = self.fp.read(16) - - self.__offset = 128 - - if i16(s, 4) == 0xF100: - # prefix chunk; ignore it - self.fp.seek(self.__offset + i32(s)) - s = self.fp.read(16) - - if i16(s, 4) == 0xF1FA: - # look for palette chunk - number_of_subchunks = i16(s, 6) - chunk_size: int | None = None - for _ in range(number_of_subchunks): - if chunk_size is not None: - self.fp.seek(chunk_size - 6, os.SEEK_CUR) - s = self.fp.read(6) - chunk_type = i16(s, 4) - if chunk_type in (4, 11): - self._palette(palette, 2 if chunk_type == 11 else 0) - break - chunk_size = i32(s) - if not chunk_size: - break - - self.palette = ImagePalette.raw( - "RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette) - ) - - # set things up to decode first frame - self.__frame = -1 - self._fp = self.fp - self.__rewind = self.fp.tell() - self.seek(0) - - def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None: - # load palette - - i = 0 - assert self.fp is not None - for e in range(i16(self.fp.read(2))): - s = self.fp.read(2) - i = i + s[0] - n = s[1] - if n == 0: - n = 256 - s = self.fp.read(n * 3) - for n in range(0, len(s), 3): - r = s[n] << shift - g = s[n + 1] << shift - b = s[n + 2] << shift - palette[i] = (r, g, b) - i += 1 - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if frame < self.__frame: - self._seek(0) - - for f in range(self.__frame + 1, frame + 1): - self._seek(f) - - def _seek(self, frame: int) -> None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - if frame == 0: - self.__frame = -1 - self._fp.seek(self.__rewind) - self.__offset = 128 - else: - # ensure that the previous frame was loaded - self.load() - - if frame != self.__frame + 1: - msg = f"cannot seek to frame {frame}" - raise ValueError(msg) - self.__frame = frame - - # move to next frame - self.fp = self._fp - self.fp.seek(self.__offset) - - s = self.fp.read(4) - if not s: - msg = "missing frame size" - raise EOFError(msg) - - framesize = i32(s) - - self.decodermaxblock = framesize - self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)] - - self.__offset += framesize - - def tell(self) -> int: - return self.__frame - - -# -# registry - -Image.register_open(FliImageFile.format, FliImageFile, _accept) - -Image.register_extensions(FliImageFile.format, [".fli", ".flc"]) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/FontFile.py b/presentation/.venv/lib/python3.12/site-packages/PIL/FontFile.py deleted file mode 100644 index 341431d..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/FontFile.py +++ /dev/null @@ -1,159 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# base class for raster font file parsers -# -# history: -# 1997-06-05 fl created -# 1997-08-19 fl restrict image width -# -# Copyright (c) 1997-1998 by Secret Labs AB -# Copyright (c) 1997-1998 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import BinaryIO - -from . import Image, ImageFont, _binary - -WIDTH = 800 - - -def puti16( - fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int] -) -> None: - """Write network order (big-endian) 16-bit sequence""" - for v in values: - if v < 0: - v += 65536 - fp.write(_binary.o16be(v)) - - -class FontFile: - """Base class for raster font file handlers.""" - - bitmap: Image.Image | None = None - - def __init__(self) -> None: - self.info: dict[bytes, bytes | int] = {} - self.glyph: list[ - tuple[ - tuple[int, int], - tuple[int, int, int, int], - tuple[int, int, int, int], - Image.Image, - ] - | None - ] = [None] * 256 - - def __getitem__(self, ix: int) -> ( - tuple[ - tuple[int, int], - tuple[int, int, int, int], - tuple[int, int, int, int], - Image.Image, - ] - | None - ): - return self.glyph[ix] - - def compile(self) -> None: - """Create metrics and bitmap""" - - if self.bitmap: - return - - # create bitmap large enough to hold all data - h = w = maxwidth = 0 - lines = 1 - for glyph in self.glyph: - if glyph: - d, dst, src, im = glyph - h = max(h, src[3] - src[1]) - w = w + (src[2] - src[0]) - if w > WIDTH: - lines += 1 - w = src[2] - src[0] - maxwidth = max(maxwidth, w) - - xsize = maxwidth - ysize = lines * h - - if xsize == 0 and ysize == 0: - return - - self.ysize = h - - # paste glyphs into bitmap - self.bitmap = Image.new("1", (xsize, ysize)) - self.metrics: list[ - tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]] - | None - ] = [None] * 256 - x = y = 0 - for i in range(256): - glyph = self[i] - if glyph: - d, dst, src, im = glyph - xx = src[2] - src[0] - x0, y0 = x, y - x = x + xx - if x > WIDTH: - x, y = 0, y + h - x0, y0 = x, y - x = xx - s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0 - self.bitmap.paste(im.crop(src), s) - self.metrics[i] = d, dst, s - - def _encode_metrics(self) -> bytes: - values: list[int] = [] - for i in range(256): - m = self.metrics[i] - if m: - values.extend(m[0] + m[1] + m[2]) - else: - values.extend((0,) * 10) - - data = bytearray() - for v in values: - if v < 0: - v += 65536 - data += _binary.o16be(v) - return bytes(data) - - def save(self, filename: str) -> None: - """Save font""" - - self.compile() - - # font data - if not self.bitmap: - msg = "No bitmap created" - raise ValueError(msg) - self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") - - # font metrics - with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: - fp.write(b"PILfont\n") - fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! - fp.write(b"DATA\n") - fp.write(self._encode_metrics()) - - def to_imagefont(self) -> ImageFont.ImageFont: - """Convert to ImageFont""" - - self.compile() - - # font data - if not self.bitmap: - msg = "No bitmap created" - raise ValueError(msg) - - imagefont = ImageFont.ImageFont() - imagefont._load(self.bitmap, self._encode_metrics()) - return imagefont diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/FpxImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/FpxImagePlugin.py deleted file mode 100644 index 0b06aac..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/FpxImagePlugin.py +++ /dev/null @@ -1,258 +0,0 @@ -# -# THIS IS WORK IN PROGRESS -# -# The Python Imaging Library. -# $Id$ -# -# FlashPix support for PIL -# -# History: -# 97-01-25 fl Created (reads uncompressed RGB images only) -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import olefile - -from . import Image, ImageFile -from ._binary import i32le as i32 - -# we map from colour field tuples to (mode, rawmode) descriptors -MODES = { - # opacity - (0x00007FFE,): ("A", "L"), - # monochrome - (0x00010000,): ("L", "L"), - (0x00018000, 0x00017FFE): ("RGBA", "LA"), - # photo YCC - (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"), - (0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"), - # standard RGB (NIFRGB) - (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"), - (0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"), -} - - -# -# -------------------------------------------------------------------- - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(olefile.MAGIC) - - -## -# Image plugin for the FlashPix images. - - -class FpxImageFile(ImageFile.ImageFile): - format = "FPX" - format_description = "FlashPix" - - def _open(self) -> None: - # - # read the OLE directory and see if this is a likely - # to be a FlashPix file - - assert self.fp is not None - try: - self.ole = olefile.OleFileIO(self.fp) - except OSError as e: - msg = "not an FPX file; invalid OLE file" - raise SyntaxError(msg) from e - - root = self.ole.root - if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": - msg = "not an FPX file; bad root CLSID" - raise SyntaxError(msg) - - self._open_index(1) - - def _open_index(self, index: int = 1) -> None: - # - # get the Image Contents Property Set - - prop = self.ole.getproperties( - [f"Data Object Store {index:06d}", "\005Image Contents"] - ) - - # size (highest resolution) - - assert isinstance(prop[0x1000002], int) - assert isinstance(prop[0x1000003], int) - self._size = prop[0x1000002], prop[0x1000003] - - size = max(self.size) - i = 1 - while size > 64: - size = size // 2 - i += 1 - self.maxid = i - 1 - - # mode. instead of using a single field for this, flashpix - # requires you to specify the mode for each channel in each - # resolution subimage, and leaves it to the decoder to make - # sure that they all match. for now, we'll cheat and assume - # that this is always the case. - - id = self.maxid << 16 - - s = prop[0x2000002 | id] - - if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4: - msg = "Invalid number of bands" - raise OSError(msg) - - # note: for now, we ignore the "uncalibrated" flag - colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands)) - - self._mode, self.rawmode = MODES[colors] - - # load JPEG tables, if any - self.jpeg = {} - for i in range(256): - id = 0x3000001 | (i << 16) - if id in prop: - self.jpeg[i] = prop[id] - - self._open_subimage(1, self.maxid) - - def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: - # - # setup tile descriptors for a given subimage - - stream = [ - f"Data Object Store {index:06d}", - f"Resolution {subimage:04d}", - "Subimage 0000 Header", - ] - - fp = self.ole.openstream(stream) - - # skip prefix - fp.read(28) - - # header stream - s = fp.read(36) - - size = i32(s, 4), i32(s, 8) - # tilecount = i32(s, 12) - xtile, ytile = i32(s, 16), i32(s, 20) - # channels = i32(s, 24) - offset = i32(s, 28) - length = i32(s, 32) - - if size != self.size: - msg = "subimage mismatch" - raise OSError(msg) - - # get tile descriptors - fp.seek(28 + offset) - s = fp.read(i32(s, 12) * length) - - x = y = 0 - xsize, ysize = size - self.tile = [] - - for i in range(0, len(s), length): - x1 = min(xsize, x + xtile) - y1 = min(ysize, y + ytile) - - compression = i32(s, i + 8) - - if compression == 0: - self.tile.append( - ImageFile._Tile( - "raw", - (x, y, x1, y1), - i32(s, i) + 28, - self.rawmode, - ) - ) - - elif compression == 1: - # FIXME: the fill decoder is not implemented - self.tile.append( - ImageFile._Tile( - "fill", - (x, y, x1, y1), - i32(s, i) + 28, - (self.rawmode, s[12:16]), - ) - ) - - elif compression == 2: - internal_color_conversion = s[14] - jpeg_tables = s[15] - rawmode = self.rawmode - - if internal_color_conversion: - # The image is stored as usual (usually YCbCr). - if rawmode == "RGBA": - # For "RGBA", data is stored as YCbCrA based on - # negative RGB. The following trick works around - # this problem : - jpegmode, rawmode = "YCbCrK", "CMYK" - else: - jpegmode = None # let the decoder decide - - else: - # The image is stored as defined by rawmode - jpegmode = rawmode - - self.tile.append( - ImageFile._Tile( - "jpeg", - (x, y, x1, y1), - i32(s, i) + 28, - (rawmode, jpegmode), - ) - ) - - # FIXME: jpeg tables are tile dependent; the prefix - # data must be placed in the tile descriptor itself! - - if jpeg_tables: - self.tile_prefix = self.jpeg[jpeg_tables] - - else: - msg = "unknown/invalid compression" - raise OSError(msg) - - x += xtile - if x >= xsize: - x, y = 0, y + ytile - if y >= ysize: - break # isn't really required - - assert self.fp is not None - self.stream = stream - self._fp = self.fp - self.fp = None - - def load(self) -> Image.core.PixelAccess | None: - if not self.fp: - self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) - - return ImageFile.ImageFile.load(self) - - def close(self) -> None: - self.ole.close() - super().close() - - def __exit__(self, *args: object) -> None: - self.ole.close() - super().__exit__() - - -# -# -------------------------------------------------------------------- - - -Image.register_open(FpxImageFile.format, FpxImageFile, _accept) - -Image.register_extension(FpxImageFile.format, ".fpx") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/FtexImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/FtexImagePlugin.py deleted file mode 100644 index e4d836c..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/FtexImagePlugin.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -A Pillow loader for .ftc and .ftu files (FTEX) -Jerome Leclanche <jerome@leclan.ch> - -The contents of this file are hereby released in the public domain (CC0) -Full text of the CC0 license: - https://creativecommons.org/publicdomain/zero/1.0/ - -Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001 - -The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a -packed custom format called FTEX. This file format uses file extensions FTC -and FTU. -* FTC files are compressed textures (using standard texture compression). -* FTU files are not compressed. -Texture File Format -The FTC and FTU texture files both use the same format. This -has the following structure: -{header} -{format_directory} -{data} -Where: -{header} = { - u32:magic, - u32:version, - u32:width, - u32:height, - u32:mipmap_count, - u32:format_count -} - -* The "magic" number is "FTEX". -* "width" and "height" are the dimensions of the texture. -* "mipmap_count" is the number of mipmaps in the texture. -* "format_count" is the number of texture formats (different versions of the -same texture) in this file. - -{format_directory} = format_count * { u32:format, u32:where } - -The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB -uncompressed textures. -The texture data for a format starts at the position "where" in the file. - -Each set of texture data in the file has the following structure: -{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } } -* "mipmap_size" is the number of bytes in that mip level. For compressed -textures this is the size of the texture data compressed with DXT1. For 24 bit -uncompressed textures, this is 3 * width * height. Following this are the image -bytes for that mipmap level. - -Note: All data is stored in little-Endian (Intel) byte order. -""" - -from __future__ import annotations - -import struct -from enum import IntEnum -from io import BytesIO - -from . import Image, ImageFile - -MAGIC = b"FTEX" - - -class Format(IntEnum): - DXT1 = 0 - UNCOMPRESSED = 1 - - -class FtexImageFile(ImageFile.ImageFile): - format = "FTEX" - format_description = "Texture File Format (IW2:EOC)" - - def _open(self) -> None: - assert self.fp is not None - if not _accept(self.fp.read(4)): - msg = "not an FTEX file" - raise SyntaxError(msg) - struct.unpack("<i", self.fp.read(4)) # version - self._size = struct.unpack("<2i", self.fp.read(8)) - mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8)) - - # Only support single-format files. - # I don't know of any multi-format file. - assert format_count == 1 - - format, where = struct.unpack("<2i", self.fp.read(8)) - self.fp.seek(where) - (mipmap_size,) = struct.unpack("<i", self.fp.read(4)) - - data = self.fp.read(mipmap_size) - - if format == Format.DXT1: - self._mode = "RGBA" - self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] - elif format == Format.UNCOMPRESSED: - self._mode = "RGB" - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")] - else: - msg = f"Invalid texture compression format: {repr(format)}" - raise ValueError(msg) - - self.fp.close() - self.fp = BytesIO(data) - - def load_seek(self, pos: int) -> None: - pass - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(MAGIC) - - -Image.register_open(FtexImageFile.format, FtexImageFile, _accept) -Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"]) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/GbrImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/GbrImagePlugin.py deleted file mode 100644 index ec666c8..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/GbrImagePlugin.py +++ /dev/null @@ -1,103 +0,0 @@ -# -# The Python Imaging Library -# -# load a GIMP brush file -# -# History: -# 96-03-14 fl Created -# 16-01-08 es Version 2 -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# Copyright (c) Eric Soroos 2016. -# -# See the README file for information on usage and redistribution. -# -# -# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for -# format documentation. -# -# This code Interprets version 1 and 2 .gbr files. -# Version 1 files are obsolete, and should not be used for new -# brushes. -# Version 2 files are saved by GIMP v2.8 (at least) -# Version 3 files have a format specifier of 18 for 16bit floats in -# the color depth field. This is currently unsupported by Pillow. -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i32be as i32 - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2) - - -## -# Image plugin for the GIMP brush format. - - -class GbrImageFile(ImageFile.ImageFile): - format = "GBR" - format_description = "GIMP brush file" - - def _open(self) -> None: - assert self.fp is not None - header_size = i32(self.fp.read(4)) - if header_size < 20: - msg = "not a GIMP brush" - raise SyntaxError(msg) - version = i32(self.fp.read(4)) - if version not in (1, 2): - msg = f"Unsupported GIMP brush version: {version}" - raise SyntaxError(msg) - - width = i32(self.fp.read(4)) - height = i32(self.fp.read(4)) - color_depth = i32(self.fp.read(4)) - if width == 0 or height == 0: - msg = "not a GIMP brush" - raise SyntaxError(msg) - if color_depth not in (1, 4): - msg = f"Unsupported GIMP brush color depth: {color_depth}" - raise SyntaxError(msg) - - if version == 1: - comment_length = header_size - 20 - else: - comment_length = header_size - 28 - magic_number = self.fp.read(4) - if magic_number != b"GIMP": - msg = "not a GIMP brush, bad magic number" - raise SyntaxError(msg) - self.info["spacing"] = i32(self.fp.read(4)) - - self.info["comment"] = self.fp.read(comment_length)[:-1] - - if color_depth == 1: - self._mode = "L" - else: - self._mode = "RGBA" - - self._size = width, height - - # Image might not be small - Image._decompression_bomb_check(self.size) - - # Data is an uncompressed block of w * h * bytes/pixel - self._data_size = width * height * color_depth - - def load(self) -> Image.core.PixelAccess | None: - if self._im is None: - assert self.fp is not None - self.im = Image.core.new(self.mode, self.size) - self.frombytes(self.fp.read(self._data_size)) - return Image.Image.load(self) - - -# -# registry - - -Image.register_open(GbrImageFile.format, GbrImageFile, _accept) -Image.register_extension(GbrImageFile.format, ".gbr") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/GdImageFile.py b/presentation/.venv/lib/python3.12/site-packages/PIL/GdImageFile.py deleted file mode 100644 index d73bc19..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/GdImageFile.py +++ /dev/null @@ -1,103 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# GD file handling -# -# History: -# 1996-04-12 fl Created -# -# Copyright (c) 1997 by Secret Labs AB. -# Copyright (c) 1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - - -""" -.. note:: - This format cannot be automatically recognized, so the - class is not registered for use with :py:func:`PIL.Image.open()`. To open a - gd file, use the :py:func:`PIL.GdImageFile.open()` function instead. - -.. warning:: - THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This - implementation is provided for convenience and demonstrational - purposes only. -""" - -from __future__ import annotations - -from typing import IO - -from . import ImageFile, ImagePalette, UnidentifiedImageError -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._typing import StrOrBytesPath - - -class GdImageFile(ImageFile.ImageFile): - """ - Image plugin for the GD uncompressed format. Note that this format - is not supported by the standard :py:func:`PIL.Image.open()` function. To use - this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and - use the :py:func:`PIL.GdImageFile.open()` function. - """ - - format = "GD" - format_description = "GD uncompressed images" - - def _open(self) -> None: - # Header - assert self.fp is not None - - s = self.fp.read(1037) - - if i16(s) not in [65534, 65535]: - msg = "Not a valid GD 2.x .gd file" - raise SyntaxError(msg) - - self._mode = "P" - self._size = i16(s, 2), i16(s, 4) - - true_color = s[6] - true_color_offset = 2 if true_color else 0 - - # transparency index - tindex = i32(s, 7 + true_color_offset) - if tindex < 256: - self.info["transparency"] = tindex - - self.palette = ImagePalette.raw( - "RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4] - ) - - self.tile = [ - ImageFile._Tile( - "raw", - (0, 0) + self.size, - 7 + true_color_offset + 6 + 256 * 4, - "L", - ) - ] - - -def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: - """ - Load texture from a GD image file. - - :param fp: GD file name, or an opened file handle. - :param mode: Optional mode. In this version, if the mode argument - is given, it must be "r". - :returns: An image instance. - :raises OSError: If the image could not be read. - """ - if mode != "r": - msg = "bad mode" - raise ValueError(msg) - - try: - return GdImageFile(fp) - except SyntaxError as e: - msg = "cannot identify this image file" - raise UnidentifiedImageError(msg) from e diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/GifImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/GifImagePlugin.py deleted file mode 100644 index b8db5d8..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/GifImagePlugin.py +++ /dev/null @@ -1,1223 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# GIF file handling -# -# History: -# 1995-09-01 fl Created -# 1996-12-14 fl Added interlace support -# 1996-12-30 fl Added animation support -# 1997-01-05 fl Added write support, fixed local colour map bug -# 1997-02-23 fl Make sure to load raster data in getdata() -# 1997-07-05 fl Support external decoder (0.4) -# 1998-07-09 fl Handle all modes when saving (0.5) -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6) -# 2001-04-17 fl Added palette optimization (0.7) -# 2002-06-06 fl Added transparency support for save (0.8) -# 2004-02-24 fl Disable interlacing for small images -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1995-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import itertools -import math -import os -import subprocess -from enum import IntEnum -from functools import cached_property -from typing import Any, NamedTuple, cast - -from . import ( - Image, - ImageChops, - ImageFile, - ImageMath, - ImageOps, - ImagePalette, - ImageSequence, -) -from ._binary import i16le as i16 -from ._binary import o8 -from ._binary import o16le as o16 -from ._util import DeferredError - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import IO, Literal - - from . import _imaging - from ._typing import Buffer - - -class LoadingStrategy(IntEnum): - """.. versionadded:: 9.1.0""" - - RGB_AFTER_FIRST = 0 - RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 - RGB_ALWAYS = 2 - - -#: .. versionadded:: 9.1.0 -LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST - -# -------------------------------------------------------------------- -# Identify/read GIF files - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"GIF87a", b"GIF89a")) - - -## -# Image plugin for GIF images. This plugin supports both GIF87 and -# GIF89 images. - - -class GifImageFile(ImageFile.ImageFile): - format = "GIF" - format_description = "Compuserve GIF" - _close_exclusive_fp_after_loading = False - - global_palette = None - - def data(self) -> bytes | None: - assert self.fp is not None - s = self.fp.read(1) - if s and s[0]: - return self.fp.read(s[0]) - return None - - def _is_palette_needed(self, p: bytes) -> bool: - for i in range(0, len(p), 3): - if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): - return True - return False - - def _open(self) -> None: - # Screen - assert self.fp is not None - s = self.fp.read(13) - if not _accept(s): - msg = "not a GIF file" - raise SyntaxError(msg) - - self.info["version"] = s[:6] - self._size = i16(s, 6), i16(s, 8) - flags = s[10] - bits = (flags & 7) + 1 - - if flags & 128: - # get global palette - self.info["background"] = s[11] - # check if palette contains colour indices - p = self.fp.read(3 << bits) - if self._is_palette_needed(p): - palette = ImagePalette.raw("RGB", p) - self.global_palette = self.palette = palette - - self._fp = self.fp # FIXME: hack - self.__rewind = self.fp.tell() - self._n_frames: int | None = None - self._seek(0) # get ready to read first frame - - @property - def n_frames(self) -> int: - if self._n_frames is None: - current = self.tell() - try: - while True: - self._seek(self.tell() + 1, False) - except EOFError: - self._n_frames = self.tell() + 1 - self.seek(current) - return self._n_frames - - @cached_property - def is_animated(self) -> bool: - if self._n_frames is not None: - return self._n_frames != 1 - - current = self.tell() - if current: - return True - - try: - self._seek(1, False) - is_animated = True - except EOFError: - is_animated = False - - self.seek(current) - return is_animated - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if frame < self.__frame: - self._im = None - self._seek(0) - - last_frame = self.__frame - try: - for f in range(self.__frame + 1, frame + 1): - self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in GIF file" - raise EOFError(msg) from e - - def _seek(self, frame: int, update_image: bool = True) -> None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - if frame == 0: - # rewind - self.__offset = 0 - self.dispose: _imaging.ImagingCore | None = None - self.__frame = -1 - self._fp.seek(self.__rewind) - self.disposal_method = 0 - if "comment" in self.info: - del self.info["comment"] - else: - # ensure that the previous frame was loaded - if self.tile and update_image: - self.load() - - if frame != self.__frame + 1: - msg = f"cannot seek to frame {frame}" - raise ValueError(msg) - - self.fp = self._fp - if self.__offset: - # backup to last frame - self.fp.seek(self.__offset) - while self.data(): - pass - self.__offset = 0 - - s = self.fp.read(1) - if not s or s == b";": - msg = "no more images in GIF file" - raise EOFError(msg) - - palette: ImagePalette.ImagePalette | Literal[False] | None = None - - info: dict[str, Any] = {} - frame_transparency = None - interlace = None - frame_dispose_extent = None - while True: - if not s: - s = self.fp.read(1) - if not s or s == b";": - break - - elif s == b"!": - # - # extensions - # - s = self.fp.read(1) - block = self.data() - if s[0] == 249 and block is not None: - # - # graphic control extension - # - flags = block[0] - if flags & 1: - frame_transparency = block[3] - info["duration"] = i16(block, 1) * 10 - - # disposal method - find the value of bits 4 - 6 - dispose_bits = 0b00011100 & flags - dispose_bits = dispose_bits >> 2 - if dispose_bits: - # only set the dispose if it is not - # unspecified. I'm not sure if this is - # correct, but it seems to prevent the last - # frame from looking odd for some animations - self.disposal_method = dispose_bits - elif s[0] == 254: - # - # comment extension - # - comment = b"" - - # Read this comment block - while block: - comment += block - block = self.data() - - if "comment" in info: - # If multiple comment blocks in frame, separate with \n - info["comment"] += b"\n" + comment - else: - info["comment"] = comment - s = b"" - continue - elif s[0] == 255 and frame == 0 and block is not None: - # - # application extension - # - info["extension"] = block, self.fp.tell() - if block.startswith(b"NETSCAPE2.0"): - block = self.data() - if block and len(block) >= 3 and block[0] == 1: - self.info["loop"] = i16(block, 1) - while self.data(): - pass - - elif s == b",": - # - # local image - # - s = self.fp.read(9) - - # extent - x0, y0 = i16(s, 0), i16(s, 2) - x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) - if (x1 > self.size[0] or y1 > self.size[1]) and update_image: - self._size = max(x1, self.size[0]), max(y1, self.size[1]) - Image._decompression_bomb_check(self._size) - frame_dispose_extent = x0, y0, x1, y1 - flags = s[8] - - interlace = (flags & 64) != 0 - - if flags & 128: - bits = (flags & 7) + 1 - p = self.fp.read(3 << bits) - if self._is_palette_needed(p): - palette = ImagePalette.raw("RGB", p) - else: - palette = False - - # image data - bits = self.fp.read(1)[0] - self.__offset = self.fp.tell() - break - s = b"" - - if interlace is None: - msg = "image not found in GIF frame" - raise EOFError(msg) - - self.__frame = frame - if not update_image: - return - - self.tile = [] - - if self.dispose: - self.im.paste(self.dispose, self.dispose_extent) - - self._frame_palette = palette if palette is not None else self.global_palette - self._frame_transparency = frame_transparency - if frame == 0: - if self._frame_palette: - if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: - self._mode = "RGBA" if frame_transparency is not None else "RGB" - else: - self._mode = "P" - else: - self._mode = "L" - - if palette: - self.palette = palette - elif self.global_palette: - from copy import copy - - self.palette = copy(self.global_palette) - else: - self.palette = None - else: - if self.mode == "P": - if ( - LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY - or palette - ): - if "transparency" in self.info: - self.im.putpalettealpha(self.info["transparency"], 0) - self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) - self._mode = "RGBA" - del self.info["transparency"] - else: - self._mode = "RGB" - self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) - - def _rgb(color: int) -> tuple[int, int, int]: - if self._frame_palette: - if color * 3 + 3 > len(self._frame_palette.palette): - color = 0 - return cast( - tuple[int, int, int], - tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), - ) - else: - return (color, color, color) - - self.dispose = None - self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent - if self.dispose_extent and self.disposal_method >= 2: - try: - if self.disposal_method == 2: - # replace with background colour - - # only dispose the extent in this frame - x0, y0, x1, y1 = self.dispose_extent - dispose_size = (x1 - x0, y1 - y0) - - Image._decompression_bomb_check(dispose_size) - - # by convention, attempt to use transparency first - dispose_mode = "P" - color = self.info.get("transparency", frame_transparency) - if color is not None: - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGBA" - color = _rgb(color) + (0,) - else: - color = self.info.get("background", 0) - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGB" - color = _rgb(color) - self.dispose = Image.core.fill(dispose_mode, dispose_size, color) - else: - # replace with previous contents - if self._im is not None: - # only dispose the extent in this frame - self.dispose = self._crop(self.im, self.dispose_extent) - elif frame_transparency is not None: - x0, y0, x1, y1 = self.dispose_extent - dispose_size = (x1 - x0, y1 - y0) - - Image._decompression_bomb_check(dispose_size) - dispose_mode = "P" - color = frame_transparency - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGBA" - color = _rgb(frame_transparency) + (0,) - self.dispose = Image.core.fill( - dispose_mode, dispose_size, color - ) - except AttributeError: - pass - - if interlace is not None: - transparency = -1 - if frame_transparency is not None: - if frame == 0: - if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: - self.info["transparency"] = frame_transparency - elif self.mode not in ("RGB", "RGBA"): - transparency = frame_transparency - self.tile = [ - ImageFile._Tile( - "gif", - (x0, y0, x1, y1), - self.__offset, - (bits, interlace, transparency), - ) - ] - - if info.get("comment"): - self.info["comment"] = info["comment"] - for k in ["duration", "extension"]: - if k in info: - self.info[k] = info[k] - elif k in self.info: - del self.info[k] - - def load_prepare(self) -> None: - temp_mode = "P" if self._frame_palette else "L" - self._prev_im = None - if self.__frame == 0: - if self._frame_transparency is not None: - self.im = Image.core.fill( - temp_mode, self.size, self._frame_transparency - ) - elif self.mode in ("RGB", "RGBA"): - self._prev_im = self.im - if self._frame_palette: - self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) - self.im.putpalette("RGB", *self._frame_palette.getdata()) - else: - self._im = None - if not self._prev_im and self._im is not None and self.size != self.im.size: - expanded_im = Image.core.fill(self.im.mode, self.size) - if self._frame_palette: - expanded_im.putpalette("RGB", *self._frame_palette.getdata()) - expanded_im.paste(self.im, (0, 0) + self.im.size) - - self.im = expanded_im - self._mode = temp_mode - self._frame_palette = None - - super().load_prepare() - - def load_end(self) -> None: - if self.__frame == 0: - if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: - if self._frame_transparency is not None: - self.im.putpalettealpha(self._frame_transparency, 0) - self._mode = "RGBA" - else: - self._mode = "RGB" - self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) - return - if not self._prev_im: - return - if self.size != self._prev_im.size: - if self._frame_transparency is not None: - expanded_im = Image.core.fill("RGBA", self.size) - else: - expanded_im = Image.core.fill("P", self.size) - expanded_im.putpalette("RGB", "RGB", self.im.getpalette()) - expanded_im = expanded_im.convert("RGB") - expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size) - - self._prev_im = expanded_im - assert self._prev_im is not None - if self._frame_transparency is not None: - if self.mode == "L": - frame_im = self.im.convert_transparent("LA", self._frame_transparency) - else: - self.im.putpalettealpha(self._frame_transparency, 0) - frame_im = self.im.convert("RGBA") - else: - frame_im = self.im.convert("RGB") - - assert self.dispose_extent is not None - frame_im = self._crop(frame_im, self.dispose_extent) - - self.im = self._prev_im - self._mode = self.im.mode - if frame_im.mode in ("LA", "RGBA"): - self.im.paste(frame_im, self.dispose_extent, frame_im) - else: - self.im.paste(frame_im, self.dispose_extent) - - def tell(self) -> int: - return self.__frame - - -# -------------------------------------------------------------------- -# Write GIF files - - -RAWMODE = {"1": "L", "L": "L", "P": "P"} - - -def _normalize_mode(im: Image.Image) -> Image.Image: - """ - Takes an image (or frame), returns an image in a mode that is appropriate - for saving in a Gif. - - It may return the original image, or it may return an image converted to - palette or 'L' mode. - - :param im: Image object - :returns: Image object - """ - if im.mode in RAWMODE: - im.load() - return im - if Image.getmodebase(im.mode) == "RGB": - im = im.convert("P", palette=Image.Palette.ADAPTIVE) - assert im.palette is not None - if im.palette.mode == "RGBA": - for rgba in im.palette.colors: - if rgba[3] == 0: - im.info["transparency"] = im.palette.colors[rgba] - break - return im - return im.convert("L") - - -_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette - - -def _normalize_palette( - im: Image.Image, palette: _Palette | None, info: dict[str, Any] -) -> Image.Image: - """ - Normalizes the palette for image. - - Sets the palette to the incoming palette, if provided. - - Ensures that there's a palette for L mode images - - Optimizes the palette if necessary/desired. - - :param im: Image object - :param palette: bytes object containing the source palette, or .... - :param info: encoderinfo - :returns: Image object - """ - source_palette = None - if palette: - # a bytes palette - if isinstance(palette, (bytes, bytearray, list)): - source_palette = bytearray(palette[:768]) - if isinstance(palette, ImagePalette.ImagePalette): - source_palette = bytearray(palette.palette) - - if im.mode == "P": - if not source_palette: - im_palette = im.getpalette(None) - assert im_palette is not None - source_palette = bytearray(im_palette) - else: # L-mode - if not source_palette: - source_palette = bytearray(i // 3 for i in range(768)) - im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) - assert source_palette is not None - - if palette: - used_palette_colors: list[int | None] = [] - assert im.palette is not None - for i in range(0, len(source_palette), 3): - source_color = tuple(source_palette[i : i + 3]) - index = im.palette.colors.get(source_color) - if index in used_palette_colors: - index = None - used_palette_colors.append(index) - for i, index in enumerate(used_palette_colors): - if index is None: - for j in range(len(used_palette_colors)): - if j not in used_palette_colors: - used_palette_colors[i] = j - break - dest_map: list[int] = [] - for index in used_palette_colors: - assert index is not None - dest_map.append(index) - im = im.remap_palette(dest_map) - else: - optimized_palette_colors = _get_optimize(im, info) - if optimized_palette_colors is not None: - im = im.remap_palette(optimized_palette_colors, source_palette) - if "transparency" in info: - try: - info["transparency"] = optimized_palette_colors.index( - info["transparency"] - ) - except ValueError: - del info["transparency"] - return im - - assert im.palette is not None - im.palette.palette = source_palette - return im - - -def _write_single_frame( - im: Image.Image, - fp: IO[bytes], - palette: _Palette | None, -) -> None: - im_out = _normalize_mode(im) - for k, v in im_out.info.items(): - if isinstance(k, str): - im.encoderinfo.setdefault(k, v) - im_out = _normalize_palette(im_out, palette, im.encoderinfo) - - for s in _get_global_header(im_out, im.encoderinfo): - fp.write(s) - - # local image header - flags = 0 - if get_interlace(im): - flags = flags | 64 - _write_local_header(fp, im, (0, 0), flags) - - im_out.encoderconfig = (8, get_interlace(im)) - ImageFile._save( - im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])] - ) - - fp.write(b"\0") # end of image data - - -def _getbbox( - base_im: Image.Image, im_frame: Image.Image -) -> tuple[Image.Image, tuple[int, int, int, int] | None]: - palette_bytes = [ - bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame) - ] - if palette_bytes[0] != palette_bytes[1]: - im_frame = im_frame.convert("RGBA") - base_im = base_im.convert("RGBA") - delta = ImageChops.subtract_modulo(im_frame, base_im) - return delta, delta.getbbox(alpha_only=False) - - -class _Frame(NamedTuple): - im: Image.Image - bbox: tuple[int, int, int, int] | None - encoderinfo: dict[str, Any] - - -def _write_multiple_frames( - im: Image.Image, fp: IO[bytes], palette: _Palette | None -) -> bool: - duration = im.encoderinfo.get("duration") - disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) - - im_frames: list[_Frame] = [] - previous_im: Image.Image | None = None - frame_count = 0 - background_im = None - for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): - for im_frame in ImageSequence.Iterator(imSequence): - # a copy is required here since seek can still mutate the image - im_frame = _normalize_mode(im_frame.copy()) - if frame_count == 0: - for k, v in im_frame.info.items(): - if k == "transparency": - continue - if isinstance(k, str): - im.encoderinfo.setdefault(k, v) - - encoderinfo = im.encoderinfo.copy() - if "transparency" in im_frame.info: - encoderinfo.setdefault("transparency", im_frame.info["transparency"]) - im_frame = _normalize_palette(im_frame, palette, encoderinfo) - if isinstance(duration, (list, tuple)): - encoderinfo["duration"] = duration[frame_count] - elif duration is None and "duration" in im_frame.info: - encoderinfo["duration"] = im_frame.info["duration"] - if isinstance(disposal, (list, tuple)): - encoderinfo["disposal"] = disposal[frame_count] - frame_count += 1 - - diff_frame = None - if im_frames and previous_im: - # delta frame - delta, bbox = _getbbox(previous_im, im_frame) - if not bbox: - # This frame is identical to the previous frame - if encoderinfo.get("duration"): - im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] - continue - if im_frames[-1].encoderinfo.get("disposal") == 2: - # To appear correctly in viewers using a convention, - # only consider transparency, and not background color - color = im.encoderinfo.get( - "transparency", im.info.get("transparency") - ) - if color is not None: - if background_im is None: - background = _get_background(im_frame, color) - background_im = Image.new("P", im_frame.size, background) - first_palette = im_frames[0].im.palette - assert first_palette is not None - background_im.putpalette(first_palette, first_palette.mode) - bbox = _getbbox(background_im, im_frame)[1] - else: - bbox = (0, 0) + im_frame.size - elif encoderinfo.get("optimize") and im_frame.mode != "1": - if "transparency" not in encoderinfo: - assert im_frame.palette is not None - try: - encoderinfo["transparency"] = ( - im_frame.palette._new_color_index(im_frame) - ) - except ValueError: - pass - if "transparency" in encoderinfo: - # When the delta is zero, fill the image with transparency - diff_frame = im_frame.copy() - fill = Image.new("P", delta.size, encoderinfo["transparency"]) - if delta.mode == "RGBA": - r, g, b, a = delta.split() - mask = ImageMath.lambda_eval( - lambda args: args["convert"]( - args["max"]( - args["max"]( - args["max"](args["r"], args["g"]), args["b"] - ), - args["a"], - ) - * 255, - "1", - ), - r=r, - g=g, - b=b, - a=a, - ) - else: - if delta.mode == "P": - # Convert to L without considering palette - delta_l = Image.new("L", delta.size) - delta_l.putdata(delta.get_flattened_data()) - delta = delta_l - mask = ImageMath.lambda_eval( - lambda args: args["convert"](args["im"] * 255, "1"), - im=delta, - ) - diff_frame.paste(fill, mask=ImageOps.invert(mask)) - else: - bbox = None - previous_im = im_frame - im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo)) - - if len(im_frames) == 1: - if "duration" in im.encoderinfo: - # Since multiple frames will not be written, use the combined duration - im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"] - return False - - for frame_data in im_frames: - im_frame = frame_data.im - if not frame_data.bbox: - # global header - for s in _get_global_header(im_frame, frame_data.encoderinfo): - fp.write(s) - offset = (0, 0) - else: - # compress difference - if not palette: - frame_data.encoderinfo["include_color_table"] = True - - if frame_data.bbox != (0, 0) + im_frame.size: - im_frame = im_frame.crop(frame_data.bbox) - offset = frame_data.bbox[:2] - _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo) - return True - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -def _save( - im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False -) -> None: - # header - if "palette" in im.encoderinfo or "palette" in im.info: - palette = im.encoderinfo.get("palette", im.info.get("palette")) - else: - palette = None - im.encoderinfo.setdefault("optimize", True) - - if not save_all or not _write_multiple_frames(im, fp, palette): - _write_single_frame(im, fp, palette) - - fp.write(b";") # end of file - - if hasattr(fp, "flush"): - fp.flush() - - -def get_interlace(im: Image.Image) -> int: - interlace = im.encoderinfo.get("interlace", 1) - - # workaround for @PIL153 - if min(im.size) < 16: - interlace = 0 - - return interlace - - -def _write_local_header( - fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int -) -> None: - try: - transparency = im.encoderinfo["transparency"] - except KeyError: - transparency = None - - if "duration" in im.encoderinfo: - duration = int(im.encoderinfo["duration"] / 10) - else: - duration = 0 - - disposal = int(im.encoderinfo.get("disposal", 0)) - - if transparency is not None or duration != 0 or disposal: - packed_flag = 1 if transparency is not None else 0 - packed_flag |= disposal << 2 - - fp.write( - b"!" - + o8(249) # extension intro - + o8(4) # length - + o8(packed_flag) # packed fields - + o16(duration) # duration - + o8(transparency or 0) # transparency index - + o8(0) - ) - - include_color_table = im.encoderinfo.get("include_color_table") - if include_color_table: - palette_bytes = _get_palette_bytes(im) - color_table_size = _get_color_table_size(palette_bytes) - if color_table_size: - flags = flags | 128 # local color table flag - flags = flags | color_table_size - - fp.write( - b"," - + o16(offset[0]) # offset - + o16(offset[1]) - + o16(im.size[0]) # size - + o16(im.size[1]) - + o8(flags) # flags - ) - if include_color_table and color_table_size: - fp.write(_get_header_palette(palette_bytes)) - fp.write(o8(8)) # bits - - -def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # Unused by default. - # To use, uncomment the register_save call at the end of the file. - # - # If you need real GIF compression and/or RGB quantization, you - # can use the external NETPBM/PBMPLUS utilities. See comments - # below for information on how to enable this. - tempfile = im._dump() - - try: - with open(filename, "wb") as f: - if im.mode != "RGB": - subprocess.check_call( - ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL - ) - else: - # Pipe ppmquant output into ppmtogif - # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) - quant_cmd = ["ppmquant", "256", tempfile] - togif_cmd = ["ppmtogif"] - quant_proc = subprocess.Popen( - quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) - togif_proc = subprocess.Popen( - togif_cmd, - stdin=quant_proc.stdout, - stdout=f, - stderr=subprocess.DEVNULL, - ) - - # Allow ppmquant to receive SIGPIPE if ppmtogif exits - assert quant_proc.stdout is not None - quant_proc.stdout.close() - - retcode = quant_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, quant_cmd) - - retcode = togif_proc.wait() - if retcode: - raise subprocess.CalledProcessError(retcode, togif_cmd) - finally: - try: - os.unlink(tempfile) - except OSError: - pass - - -# Force optimization so that we can test performance against -# cases where it took lots of memory and time previously. -_FORCE_OPTIMIZE = False - - -def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: - """ - Palette optimization is a potentially expensive operation. - - This function determines if the palette should be optimized using - some heuristics, then returns the list of palette entries in use. - - :param im: Image object - :param info: encoderinfo - :returns: list of indexes of palette entries in use, or None - """ - if ( - im.mode in ("P", "L") - and info - and info.get("optimize") - and im.width != 0 - and im.height != 0 - ): - # Potentially expensive operation. - - # The palette saves 3 bytes per color not used, but palette - # lengths are restricted to 3*(2**N) bytes. Max saving would - # be 768 -> 6 bytes if we went all the way down to 2 colors. - # * If we're over 128 colors, we can't save any space. - # * If there aren't any holes, it's not worth collapsing. - # * If we have a 'large' image, the palette is in the noise. - - # create the new palette if not every color is used - optimise = _FORCE_OPTIMIZE or im.mode == "L" - if optimise or im.width * im.height < 512 * 512: - # check which colors are used - used_palette_colors = [] - for i, count in enumerate(im.histogram()): - if count: - used_palette_colors.append(i) - - if optimise or max(used_palette_colors) >= len(used_palette_colors): - return used_palette_colors - - assert im.palette is not None - num_palette_colors = len(im.palette.palette) // Image.getmodebands( - im.palette.mode - ) - current_palette_size = 1 << (num_palette_colors - 1).bit_length() - if ( - # check that the palette would become smaller when saved - len(used_palette_colors) <= current_palette_size // 2 - # check that the palette is not already the smallest possible size - and current_palette_size > 2 - ): - return used_palette_colors - return None - - -def _get_color_table_size(palette_bytes: bytes) -> int: - # calculate the palette size for the header - if not palette_bytes: - return 0 - elif len(palette_bytes) < 9: - return 1 - else: - return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 - - -def _get_header_palette(palette_bytes: bytes) -> bytes: - """ - Returns the palette, null padded to the next power of 2 (*3) bytes - suitable for direct inclusion in the GIF header - - :param palette_bytes: Unpadded palette bytes, in RGBRGB form - :returns: Null padded palette - """ - color_table_size = _get_color_table_size(palette_bytes) - - # add the missing amount of bytes - # the palette has to be 2<<n in size - actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3 - if actual_target_size_diff > 0: - palette_bytes += o8(0) * 3 * actual_target_size_diff - return palette_bytes - - -def _get_palette_bytes(im: Image.Image) -> bytes: - """ - Gets the palette for inclusion in the gif header - - :param im: Image object - :returns: Bytes, len<=768 suitable for inclusion in gif header - """ - if not im.palette: - return b"" - - palette = bytes(im.palette.palette) - if im.palette.mode == "RGBA": - palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3)) - return palette - - -def _get_background( - im: Image.Image, - info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None, -) -> int: - background = 0 - if info_background: - if isinstance(info_background, tuple): - # WebPImagePlugin stores an RGBA value in info["background"] - # So it must be converted to the same format as GifImagePlugin's - # info["background"] - a global color table index - assert im.palette is not None - try: - background = im.palette.getcolor(info_background, im) - except ValueError as e: - if str(e) not in ( - # If all 256 colors are in use, - # then there is no need for the background color - "cannot allocate more than 256 colors", - # Ignore non-opaque WebP background - "cannot add non-opaque RGBA color to RGB palette", - ): - raise - else: - background = info_background - return background - - -def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]: - """Return a list of strings representing a GIF header""" - - # Header Block - # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp - - version = b"87a" - if im.info.get("version") == b"89a" or ( - info - and ( - "transparency" in info - or info.get("loop") is not None - or info.get("duration") - or info.get("comment") - ) - ): - version = b"89a" - - background = _get_background(im, info.get("background")) - - palette_bytes = _get_palette_bytes(im) - color_table_size = _get_color_table_size(palette_bytes) - - header = [ - b"GIF" # signature - + version # version - + o16(im.size[0]) # canvas width - + o16(im.size[1]), # canvas height - # Logical Screen Descriptor - # size of global color table + global color table flag - o8(color_table_size + 128), # packed fields - # background + reserved/aspect - o8(background) + o8(0), - # Global Color Table - _get_header_palette(palette_bytes), - ] - if info.get("loop") is not None: - header.append( - b"!" - + o8(255) # extension intro - + o8(11) - + b"NETSCAPE2.0" - + o8(3) - + o8(1) - + o16(info["loop"]) # number of loops - + o8(0) - ) - if info.get("comment"): - comment_block = b"!" + o8(254) # extension intro - - comment = info["comment"] - if isinstance(comment, str): - comment = comment.encode() - for i in range(0, len(comment), 255): - subblock = comment[i : i + 255] - comment_block += o8(len(subblock)) + subblock - - comment_block += o8(0) - header.append(comment_block) - return header - - -def _write_frame_data( - fp: IO[bytes], - im_frame: Image.Image, - offset: tuple[int, int], - params: dict[str, Any], -) -> None: - try: - im_frame.encoderinfo = params - - # local image header - _write_local_header(fp, im_frame, offset, 0) - - ImageFile._save( - im_frame, - fp, - [ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])], - ) - - fp.write(b"\0") # end of image data - finally: - del im_frame.encoderinfo - - -# -------------------------------------------------------------------- -# Legacy GIF utilities - - -def getheader( - im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None -) -> tuple[list[bytes], list[int] | None]: - """ - Legacy Method to get Gif data from image. - - Warning:: May modify image data. - - :param im: Image object - :param palette: bytes object containing the source palette, or .... - :param info: encoderinfo - :returns: tuple of(list of header items, optimized palette) - - """ - if info is None: - info = {} - - used_palette_colors = _get_optimize(im, info) - - if "background" not in info and "background" in im.info: - info["background"] = im.info["background"] - - im_mod = _normalize_palette(im, palette, info) - im.palette = im_mod.palette - im.im = im_mod.im - header = _get_global_header(im, info) - - return header, used_palette_colors - - -def getdata( - im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any -) -> list[bytes]: - """ - Legacy Method - - Return a list of strings representing this image. - The first string is a local image header, the rest contains - encoded image data. - - To specify duration, add the time in milliseconds, - e.g. ``getdata(im_frame, duration=1000)`` - - :param im: Image object - :param offset: Tuple of (x, y) pixels. Defaults to (0, 0) - :param \\**params: e.g. duration or other encoder info parameters - :returns: List of bytes containing GIF encoded frame data - - """ - from io import BytesIO - - class Collector(BytesIO): - data = [] - - def write(self, data: Buffer) -> int: - self.data.append(data) - return len(data) - - im.load() # make sure raster data is available - - fp = Collector() - - _write_frame_data(fp, im, offset, params) - - return fp.data - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(GifImageFile.format, GifImageFile, _accept) -Image.register_save(GifImageFile.format, _save) -Image.register_save_all(GifImageFile.format, _save_all) -Image.register_extension(GifImageFile.format, ".gif") -Image.register_mime(GifImageFile.format, "image/gif") - -# -# Uncomment the following line if you wish to use NETPBM/PBMPLUS -# instead of the built-in "uncompressed" GIF encoder - -# Image.register_save(GifImageFile.format, _save_netpbm) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/GimpGradientFile.py b/presentation/.venv/lib/python3.12/site-packages/PIL/GimpGradientFile.py deleted file mode 100644 index fb95872..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/GimpGradientFile.py +++ /dev/null @@ -1,154 +0,0 @@ -# -# Python Imaging Library -# $Id$ -# -# stuff to read (and render) GIMP gradient files -# -# History: -# 97-08-23 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# - -""" -Stuff to translate curve segments to palette values (derived from -the corresponding code in GIMP, written by Federico Mena Quintero. -See the GIMP distribution for more information.) -""" - -from __future__ import annotations - -from math import log, pi, sin, sqrt - -from ._binary import o8 - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable - from typing import IO - -EPSILON = 1e-10 -"""""" # Enable auto-doc for data member - - -def linear(middle: float, pos: float) -> float: - if pos <= middle: - if middle < EPSILON: - return 0.0 - else: - return 0.5 * pos / middle - else: - pos = pos - middle - middle = 1.0 - middle - if middle < EPSILON: - return 1.0 - else: - return 0.5 + 0.5 * pos / middle - - -def curved(middle: float, pos: float) -> float: - return pos ** (log(0.5) / log(max(middle, EPSILON))) - - -def sine(middle: float, pos: float) -> float: - return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 - - -def sphere_increasing(middle: float, pos: float) -> float: - return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) - - -def sphere_decreasing(middle: float, pos: float) -> float: - return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) - - -SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] -"""""" # Enable auto-doc for data member - - -class GradientFile: - gradient: ( - list[ - tuple[ - float, - float, - float, - list[float], - list[float], - Callable[[float, float], float], - ] - ] - | None - ) = None - - def getpalette(self, entries: int = 256) -> tuple[bytes, str]: - assert self.gradient is not None - palette = [] - - ix = 0 - x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] - - for i in range(entries): - x = i / (entries - 1) - - while x1 < x: - ix += 1 - x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] - - w = x1 - x0 - - if w < EPSILON: - scale = segment(0.5, 0.5) - else: - scale = segment((xm - x0) / w, (x - x0) / w) - - # expand to RGBA - r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5)) - g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5)) - b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5)) - a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5)) - - # add to palette - palette.append(r + g + b + a) - - return b"".join(palette), "RGBA" - - -class GimpGradientFile(GradientFile): - """File handler for GIMP's gradient format.""" - - def __init__(self, fp: IO[bytes]) -> None: - if not fp.readline().startswith(b"GIMP Gradient"): - msg = "not a GIMP gradient file" - raise SyntaxError(msg) - - line = fp.readline() - - # GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do - if line.startswith(b"Name: "): - line = fp.readline().strip() - - count = int(line) - - self.gradient = [] - - for i in range(count): - s = fp.readline().split() - w = [float(x) for x in s[:11]] - - x0, x1 = w[0], w[2] - xm = w[1] - rgb0 = w[3:7] - rgb1 = w[7:11] - - segment = SEGMENTS[int(s[11])] - cspace = int(s[12]) - - if cspace != 0: - msg = "cannot handle HSV colour space" - raise OSError(msg) - - self.gradient.append((x0, x1, xm, rgb0, rgb1, segment)) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/GimpPaletteFile.py b/presentation/.venv/lib/python3.12/site-packages/PIL/GimpPaletteFile.py deleted file mode 100644 index 016257d..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/GimpPaletteFile.py +++ /dev/null @@ -1,75 +0,0 @@ -# -# Python Imaging Library -# $Id$ -# -# stuff to read GIMP palette files -# -# History: -# 1997-08-23 fl Created -# 2004-09-07 fl Support GIMP 2.0 palette files. -# -# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. -# Copyright (c) Fredrik Lundh 1997-2004. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re -from io import BytesIO - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import IO - - -class GimpPaletteFile: - """File handler for GIMP's palette format.""" - - rawmode = "RGB" - - def _read(self, fp: IO[bytes], limit: bool = True) -> None: - if not fp.readline().startswith(b"GIMP Palette"): - msg = "not a GIMP palette file" - raise SyntaxError(msg) - - palette: list[int] = [] - i = 0 - while True: - if limit and i == 256 + 3: - break - - i += 1 - s = fp.readline() - if not s: - break - - # skip fields and comment lines - if re.match(rb"\w+:|#", s): - continue - if limit and len(s) > 100: - msg = "bad palette file" - raise SyntaxError(msg) - - v = s.split(maxsplit=3) - if len(v) < 3: - msg = "bad palette entry" - raise ValueError(msg) - - palette += (int(v[i]) for i in range(3)) - if limit and len(palette) == 768: - break - - self.palette = bytes(palette) - - def __init__(self, fp: IO[bytes]) -> None: - self._read(fp) - - @classmethod - def frombytes(cls, data: bytes) -> GimpPaletteFile: - self = cls.__new__(cls) - self._read(BytesIO(data), False) - return self - - def getpalette(self) -> tuple[bytes, str]: - return self.palette, self.rawmode diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/GribStubImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/GribStubImagePlugin.py deleted file mode 100644 index 3784ef2..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/GribStubImagePlugin.py +++ /dev/null @@ -1,72 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# GRIB stub adapter -# -# Copyright (c) 1996-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import IO - -from . import Image, ImageFile - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific GRIB image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -# -------------------------------------------------------------------- -# Image adapter - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1 - - -class GribStubImageFile(ImageFile.StubImageFile): - format = "GRIB" - format_description = "GRIB" - - def _open(self) -> None: - assert self.fp is not None - if not _accept(self.fp.read(8)): - msg = "Not a GRIB file" - raise SyntaxError(msg) - - self.fp.seek(-8, os.SEEK_CUR) - - # make something up - self._mode = "F" - self._size = 1, 1 - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "GRIB save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept) -Image.register_save(GribStubImageFile.format, _save) - -Image.register_extension(GribStubImageFile.format, ".grib") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/Hdf5StubImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/Hdf5StubImagePlugin.py deleted file mode 100644 index 1a56660..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/Hdf5StubImagePlugin.py +++ /dev/null @@ -1,72 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# HDF5 stub adapter -# -# Copyright (c) 2000-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import IO - -from . import Image, ImageFile - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific HDF5 image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -# -------------------------------------------------------------------- -# Image adapter - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\x89HDF\r\n\x1a\n") - - -class HDF5StubImageFile(ImageFile.StubImageFile): - format = "HDF5" - format_description = "HDF5" - - def _open(self) -> None: - assert self.fp is not None - if not _accept(self.fp.read(8)): - msg = "Not an HDF file" - raise SyntaxError(msg) - - self.fp.seek(-8, os.SEEK_CUR) - - # make something up - self._mode = "F" - self._size = 1, 1 - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "HDF5 save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept) -Image.register_save(HDF5StubImageFile.format, _save) - -Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"]) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/IcnsImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/IcnsImagePlugin.py deleted file mode 100644 index cb7a74c..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/IcnsImagePlugin.py +++ /dev/null @@ -1,401 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# macOS icns file decoder, based on icns.py by Bob Ippolito. -# -# history: -# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies. -# 2020-04-04 Allow saving on all operating systems. -# -# Copyright (c) 2004 by Bob Ippolito. -# Copyright (c) 2004 by Secret Labs. -# Copyright (c) 2004 by Fredrik Lundh. -# Copyright (c) 2014 by Alastair Houghton. -# Copyright (c) 2020 by Pan Jing. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import struct -import sys -from typing import IO - -from . import Image, ImageFile, PngImagePlugin, features - -enable_jpeg2k = features.check_codec("jpg_2000") -if enable_jpeg2k: - from . import Jpeg2KImagePlugin - -MAGIC = b"icns" -HEADERSIZE = 8 - - -def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]: - return struct.unpack(">4sI", fobj.read(HEADERSIZE)) - - -def read_32t( - fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] -) -> dict[str, Image.Image]: - # The 128x128 icon seems to have an extra header for some reason. - start, length = start_length - fobj.seek(start) - sig = fobj.read(4) - if sig != b"\x00\x00\x00\x00": - msg = "Unknown signature, expecting 0x00000000" - raise SyntaxError(msg) - return read_32(fobj, (start + 4, length - 4), size) - - -def read_32( - fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] -) -> dict[str, Image.Image]: - """ - Read a 32bit RGB icon resource. Seems to be either uncompressed or - an RLE packbits-like scheme. - """ - start, length = start_length - fobj.seek(start) - pixel_size = (size[0] * size[2], size[1] * size[2]) - sizesq = pixel_size[0] * pixel_size[1] - if length == sizesq * 3: - # uncompressed ("RGBRGBGB") - indata = fobj.read(length) - im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1) - else: - # decode image - im = Image.new("RGB", pixel_size, None) - for band_ix in range(3): - data = [] - bytesleft = sizesq - while bytesleft > 0: - byte = fobj.read(1) - if not byte: - break - byte_int = byte[0] - if byte_int & 0x80: - blocksize = byte_int - 125 - byte = fobj.read(1) - data.extend([byte] * blocksize) - else: - blocksize = byte_int + 1 - data.append(fobj.read(blocksize)) - bytesleft -= blocksize - if bytesleft <= 0: - break - if bytesleft != 0: - msg = f"Error reading channel [{repr(bytesleft)} left]" - raise SyntaxError(msg) - band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) - im.im.putband(band.im, band_ix) - return {"RGB": im} - - -def read_mk( - fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] -) -> dict[str, Image.Image]: - # Alpha masks seem to be uncompressed - start = start_length[0] - fobj.seek(start) - pixel_size = (size[0] * size[2], size[1] * size[2]) - sizesq = pixel_size[0] * pixel_size[1] - band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1) - return {"A": band} - - -def read_png_or_jpeg2000( - fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] -) -> dict[str, Image.Image]: - start, length = start_length - fobj.seek(start) - sig = fobj.read(12) - - im: Image.Image - if sig.startswith(b"\x89PNG\x0d\x0a\x1a\x0a"): - fobj.seek(start) - im = PngImagePlugin.PngImageFile(fobj) - Image._decompression_bomb_check(im.size) - return {"RGBA": im} - elif ( - sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a")) - or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" - ): - if not enable_jpeg2k: - msg = ( - "Unsupported icon subimage format (rebuild PIL " - "with JPEG 2000 support to fix this)" - ) - raise ValueError(msg) - # j2k, jpc or j2c - fobj.seek(start) - jp2kstream = fobj.read(length) - f = io.BytesIO(jp2kstream) - im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) - Image._decompression_bomb_check(im.size) - if im.mode != "RGBA": - im = im.convert("RGBA") - return {"RGBA": im} - else: - msg = "Unsupported icon subimage format" - raise ValueError(msg) - - -class IcnsFile: - SIZES = { - (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], - (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], - (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)], - (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)], - (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)], - (128, 128, 1): [ - (b"ic07", read_png_or_jpeg2000), - (b"it32", read_32t), - (b"t8mk", read_mk), - ], - (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)], - (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)], - (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)], - (32, 32, 1): [ - (b"icp5", read_png_or_jpeg2000), - (b"il32", read_32), - (b"l8mk", read_mk), - ], - (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)], - (16, 16, 1): [ - (b"icp4", read_png_or_jpeg2000), - (b"is32", read_32), - (b"s8mk", read_mk), - ], - } - - def __init__(self, fobj: IO[bytes]) -> None: - """ - fobj is a file-like object as an icns resource - """ - # signature : (start, length) - self.dct = {} - self.fobj = fobj - sig, filesize = nextheader(fobj) - if not _accept(sig): - msg = "not an icns file" - raise SyntaxError(msg) - i = HEADERSIZE - while i < filesize: - sig, blocksize = nextheader(fobj) - if blocksize <= 0: - msg = "invalid block header" - raise SyntaxError(msg) - i += HEADERSIZE - blocksize -= HEADERSIZE - self.dct[sig] = (i, blocksize) - fobj.seek(blocksize, io.SEEK_CUR) - i += blocksize - - def itersizes(self) -> list[tuple[int, int, int]]: - sizes = [] - for size, fmts in self.SIZES.items(): - for fmt, reader in fmts: - if fmt in self.dct: - sizes.append(size) - break - return sizes - - def bestsize(self) -> tuple[int, int, int]: - sizes = self.itersizes() - if not sizes: - msg = "No 32bit icon resources found" - raise SyntaxError(msg) - return max(sizes) - - def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]: - """ - Get an icon resource as {channel: array}. Note that - the arrays are bottom-up like windows bitmaps and will likely - need to be flipped or transposed in some way. - """ - dct = {} - for code, reader in self.SIZES[size]: - desc = self.dct.get(code) - if desc is not None: - dct.update(reader(self.fobj, desc, size)) - return dct - - def getimage( - self, size: tuple[int, int] | tuple[int, int, int] | None = None - ) -> Image.Image: - if size is None: - size = self.bestsize() - elif len(size) == 2: - size = (size[0], size[1], 1) - channels = self.dataforsize(size) - - im = channels.get("RGBA") - if im: - return im - - im = channels["RGB"].copy() - try: - im.putalpha(channels["A"]) - except KeyError: - pass - return im - - -## -# Image plugin for Mac OS icons. - - -class IcnsImageFile(ImageFile.ImageFile): - """ - PIL image support for Mac OS .icns files. - Chooses the best resolution, but will possibly load - a different size image if you mutate the size attribute - before calling 'load'. - - The info dictionary has a key 'sizes' that is a list - of sizes that the icns file has. - """ - - format = "ICNS" - format_description = "Mac OS icns resource" - - def _open(self) -> None: - assert self.fp is not None - self.icns = IcnsFile(self.fp) - self._mode = "RGBA" - self.info["sizes"] = self.icns.itersizes() - self.best_size = self.icns.bestsize() - self.size = ( - self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2], - ) - - @property - def size(self) -> tuple[int, int]: - return self._size - - @size.setter - def size(self, value: tuple[int, int]) -> None: - # Check that a matching size exists, - # or that there is a scale that would create a size that matches - for size in self.info["sizes"]: - simple_size = size[0] * size[2], size[1] * size[2] - scale = simple_size[0] // value[0] - if simple_size[1] / value[1] == scale: - self._size = value - return - msg = "This is not one of the allowed sizes of this image" - raise ValueError(msg) - - def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: - if scale is not None: - width, height = self.size[:2] - self.size = width * scale, height * scale - self.best_size = width, height, scale - - px = Image.Image.load(self) - if self._im is not None and self.im.size == self.size: - # Already loaded - return px - self.load_prepare() - # This is likely NOT the best way to do it, but whatever. - im = self.icns.getimage(self.best_size) - - # If this is a PNG or JPEG 2000, it won't be loaded yet - px = im.load() - - self.im = im.im - self._mode = im.mode - self.size = im.size - - return px - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - """ - Saves the image as a series of PNG files, - that are then combined into a .icns file. - """ - if hasattr(fp, "flush"): - fp.flush() - - sizes = { - b"ic07": 128, - b"ic08": 256, - b"ic09": 512, - b"ic10": 1024, - b"ic11": 32, - b"ic12": 64, - b"ic13": 256, - b"ic14": 512, - } - provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])} - size_streams = {} - for size in set(sizes.values()): - image = ( - provided_images[size] - if size in provided_images - else im.resize((size, size)) - ) - - temp = io.BytesIO() - image.save(temp, "png") - size_streams[size] = temp.getvalue() - - entries = [] - for type, size in sizes.items(): - stream = size_streams[size] - entries.append((type, HEADERSIZE + len(stream), stream)) - - # Header - fp.write(MAGIC) - file_length = HEADERSIZE # Header - file_length += HEADERSIZE + 8 * len(entries) # TOC - file_length += sum(entry[1] for entry in entries) - fp.write(struct.pack(">i", file_length)) - - # TOC - fp.write(b"TOC ") - fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) - for entry in entries: - fp.write(entry[0]) - fp.write(struct.pack(">i", entry[1])) - - # Data - for entry in entries: - fp.write(entry[0]) - fp.write(struct.pack(">i", entry[1])) - fp.write(entry[2]) - - if hasattr(fp, "flush"): - fp.flush() - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(MAGIC) - - -Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) -Image.register_extension(IcnsImageFile.format, ".icns") - -Image.register_save(IcnsImageFile.format, _save) -Image.register_mime(IcnsImageFile.format, "image/icns") - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python3 IcnsImagePlugin.py [file]") - sys.exit() - - with open(sys.argv[1], "rb") as fp: - imf = IcnsImageFile(fp) - for size in imf.info["sizes"]: - width, height, scale = imf.size = size - imf.save(f"out-{width}-{height}-{scale}.png") - with Image.open(sys.argv[1]) as im: - im.save("out.png") - if sys.platform == "windows": - os.startfile("out.png") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/IcoImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/IcoImagePlugin.py deleted file mode 100644 index 8dd57ff..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/IcoImagePlugin.py +++ /dev/null @@ -1,396 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Windows Icon support for PIL -# -# History: -# 96-05-27 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# - -# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis -# <casadebender@gmail.com>. -# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki -# -# Copyright 2008 Bryan Davis -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Icon format references: -# * https://en.wikipedia.org/wiki/ICO_(file_format) -# * https://msdn.microsoft.com/en-us/library/ms997538.aspx -from __future__ import annotations - -import warnings -from io import BytesIO -from math import ceil, log -from typing import IO, NamedTuple - -from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin -from ._binary import i16le as i16 -from ._binary import i32le as i32 -from ._binary import o8 -from ._binary import o16le as o16 -from ._binary import o32le as o32 - -# -# -------------------------------------------------------------------- - -_MAGIC = b"\0\0\1\0" - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - fp.write(_MAGIC) # (2+2) - bmp = im.encoderinfo.get("bitmap_format") == "bmp" - sizes = im.encoderinfo.get( - "sizes", - [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], - ) - frames = [] - provided_ims = [im] + im.encoderinfo.get("append_images", []) - width, height = im.size - for size in sorted(set(sizes)): - if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256: - continue - - for provided_im in provided_ims: - if provided_im.size != size: - continue - frames.append(provided_im) - if bmp: - bits = BmpImagePlugin.SAVE[provided_im.mode][1] - bits_used = [bits] - for other_im in provided_ims: - if other_im.size != size: - continue - bits = BmpImagePlugin.SAVE[other_im.mode][1] - if bits not in bits_used: - # Another image has been supplied for this size - # with a different bit depth - frames.append(other_im) - bits_used.append(bits) - break - else: - # TODO: invent a more convenient method for proportional scalings - frame = provided_im.copy() - frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) - frames.append(frame) - fp.write(o16(len(frames))) # idCount(2) - offset = fp.tell() + len(frames) * 16 - for frame in frames: - width, height = frame.size - # 0 means 256 - fp.write(o8(width if width < 256 else 0)) # bWidth(1) - fp.write(o8(height if height < 256 else 0)) # bHeight(1) - - bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0) - fp.write(o8(colors)) # bColorCount(1) - fp.write(b"\0") # bReserved(1) - fp.write(b"\0\0") # wPlanes(2) - fp.write(o16(bits)) # wBitCount(2) - - image_io = BytesIO() - if bmp: - frame.save(image_io, "dib") - - if bits != 32: - and_mask = Image.new("1", size) - ImageFile._save( - and_mask, - image_io, - [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], - ) - else: - frame.save(image_io, "png") - image_io.seek(0) - image_bytes = image_io.read() - if bmp: - image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:] - bytes_len = len(image_bytes) - fp.write(o32(bytes_len)) # dwBytesInRes(4) - fp.write(o32(offset)) # dwImageOffset(4) - current = fp.tell() - fp.seek(offset) - fp.write(image_bytes) - offset = offset + bytes_len - fp.seek(current) - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(_MAGIC) - - -class IconHeader(NamedTuple): - width: int - height: int - nb_color: int - reserved: int - planes: int - bpp: int - size: int - offset: int - dim: tuple[int, int] - square: int - color_depth: int - - -class IcoFile: - def __init__(self, buf: IO[bytes]) -> None: - """ - Parse image from file-like object containing ico file data - """ - - # check magic - s = buf.read(6) - if not _accept(s): - msg = "not an ICO file" - raise SyntaxError(msg) - - self.buf = buf - self.entry = [] - - # Number of items in file - self.nb_items = i16(s, 4) - - # Get headers for each item - for i in range(self.nb_items): - s = buf.read(16) - - # See Wikipedia - width = s[0] or 256 - height = s[1] or 256 - - # No. of colors in image (0 if >=8bpp) - nb_color = s[2] - bpp = i16(s, 6) - icon_header = IconHeader( - width=width, - height=height, - nb_color=nb_color, - reserved=s[3], - planes=i16(s, 4), - bpp=i16(s, 6), - size=i32(s, 8), - offset=i32(s, 12), - dim=(width, height), - square=width * height, - # See Wikipedia notes about color depth. - # We need this just to differ images with equal sizes - color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256, - ) - - self.entry.append(icon_header) - - self.entry = sorted(self.entry, key=lambda x: x.color_depth) - # ICO images are usually squares - self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True) - - def sizes(self) -> set[tuple[int, int]]: - """ - Get a set of all available icon sizes and color depths. - """ - return {(h.width, h.height) for h in self.entry} - - def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int: - for i, h in enumerate(self.entry): - if size == h.dim and (bpp is False or bpp == h.color_depth): - return i - return 0 - - def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image: - """ - Get an image from the icon - """ - return self.frame(self.getentryindex(size, bpp)) - - def frame(self, idx: int) -> Image.Image: - """ - Get an image from frame idx - """ - - header = self.entry[idx] - - self.buf.seek(header.offset) - data = self.buf.read(8) - self.buf.seek(header.offset) - - im: Image.Image - if data[:8] == PngImagePlugin._MAGIC: - # png frame - im = PngImagePlugin.PngImageFile(self.buf) - Image._decompression_bomb_check(im.size) - else: - # XOR + AND mask bmp frame - im = BmpImagePlugin.DibImageFile(self.buf) - Image._decompression_bomb_check(im.size) - - # change tile dimension to only encompass XOR image - im._size = (im.size[0], int(im.size[1] / 2)) - d, e, o, a = im.tile[0] - im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a) - - # figure out where AND mask image starts - if header.bpp == 32: - # 32-bit color depth icon image allows semitransparent areas - # PIL's DIB format ignores transparency bits, recover them. - # The DIB is packed in BGRX byte order where X is the alpha - # channel. - - # Back up to start of bmp data - self.buf.seek(o) - # extract every 4th byte (eg. 3,7,11,15,...) - alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] - - # convert to an 8bpp grayscale image - try: - mask = Image.frombuffer( - "L", # 8bpp - im.size, # (w, h) - alpha_bytes, # source chars - "raw", # raw decoder - ("L", 0, -1), # 8bpp inverted, unpadded, reversed - ) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - mask = None - else: - raise - else: - # get AND image from end of bitmap - w = im.size[0] - if (w % 32) > 0: - # bitmap row data is aligned to word boundaries - w += 32 - (im.size[0] % 32) - - # the total mask data is - # padded row size * height / bits per char - - total_bytes = int((w * im.size[1]) / 8) - and_mask_offset = header.offset + header.size - total_bytes - - self.buf.seek(and_mask_offset) - mask_data = self.buf.read(total_bytes) - - # convert raw data to image - try: - mask = Image.frombuffer( - "1", # 1 bpp - im.size, # (w, h) - mask_data, # source chars - "raw", # raw decoder - ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed - ) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - mask = None - else: - raise - - # now we have two images, im is XOR image and mask is AND image - - # apply mask image as alpha channel - if mask: - im = im.convert("RGBA") - im.putalpha(mask) - - return im - - -## -# Image plugin for Windows Icon files. - - -class IcoImageFile(ImageFile.ImageFile): - """ - PIL read-only image support for Microsoft Windows .ico files. - - By default the largest resolution image in the file will be loaded. This - can be changed by altering the 'size' attribute before calling 'load'. - - The info dictionary has a key 'sizes' that is a list of the sizes available - in the icon file. - - Handles classic, XP and Vista icon formats. - - When saving, PNG compression is used. Support for this was only added in - Windows Vista. If you are unable to view the icon in Windows, convert the - image to "RGBA" mode before saving. - - This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis - <casadebender@gmail.com>. - https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki - """ - - format = "ICO" - format_description = "Windows Icon" - - def _open(self) -> None: - assert self.fp is not None - self.ico = IcoFile(self.fp) - self.info["sizes"] = self.ico.sizes() - self.size = self.ico.entry[0].dim - self.load() - - @property - def size(self) -> tuple[int, int]: - return self._size - - @size.setter - def size(self, value: tuple[int, int]) -> None: - if value not in self.info["sizes"]: - msg = "This is not one of the allowed sizes of this image" - raise ValueError(msg) - self._size = value - - def load(self) -> Image.core.PixelAccess | None: - if self._im is not None and self.im.size == self.size: - # Already loaded - return Image.Image.load(self) - im = self.ico.getimage(self.size) - # if tile is PNG, it won't really be loaded yet - im.load() - self.im = im.im - self._mode = im.mode - if im.palette: - self.palette = im.palette - if im.size != self.size: - warnings.warn("Image was not the expected size") - - index = self.ico.getentryindex(self.size) - sizes = list(self.info["sizes"]) - sizes[index] = im.size - self.info["sizes"] = set(sizes) - - self.size = im.size - return Image.Image.load(self) - - def load_seek(self, pos: int) -> None: - # Flag the ImageFile.Parser so that it - # just does all the decode at the end. - pass - - -# -# -------------------------------------------------------------------- - - -Image.register_open(IcoImageFile.format, IcoImageFile, _accept) -Image.register_save(IcoImageFile.format, _save) -Image.register_extension(IcoImageFile.format, ".ico") - -Image.register_mime(IcoImageFile.format, "image/x-icon") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImImagePlugin.py deleted file mode 100644 index ef54f16..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImImagePlugin.py +++ /dev/null @@ -1,390 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# IFUNC IM file handling for PIL -# -# history: -# 1995-09-01 fl Created. -# 1997-01-03 fl Save palette images -# 1997-01-08 fl Added sequence support -# 1997-01-23 fl Added P and RGB save support -# 1997-05-31 fl Read floating point images -# 1997-06-22 fl Save floating point images -# 1997-08-27 fl Read and save 1-bit images -# 1998-06-25 fl Added support for RGB+LUT images -# 1998-07-02 fl Added support for YCC images -# 1998-07-15 fl Renamed offset attribute to avoid name clash -# 1998-12-29 fl Added I;16 support -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) -# 2003-09-26 fl Added LA/PA support -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2001 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -import re -from typing import IO, Any - -from . import Image, ImageFile, ImagePalette -from ._util import DeferredError - -# -------------------------------------------------------------------- -# Standard tags - -COMMENT = "Comment" -DATE = "Date" -EQUIPMENT = "Digitalization equipment" -FRAMES = "File size (no of images)" -LUT = "Lut" -NAME = "Name" -SCALE = "Scale (x,y)" -SIZE = "Image size (x*y)" -MODE = "Image type" - -TAGS = { - COMMENT: 0, - DATE: 0, - EQUIPMENT: 0, - FRAMES: 0, - LUT: 0, - NAME: 0, - SCALE: 0, - SIZE: 0, - MODE: 0, -} - -OPEN = { - # ifunc93/p3cfunc formats - "0 1 image": ("1", "1"), - "L 1 image": ("1", "1"), - "Greyscale image": ("L", "L"), - "Grayscale image": ("L", "L"), - "RGB image": ("RGB", "RGB;L"), - "RLB image": ("RGB", "RLB"), - "RYB image": ("RGB", "RLB"), - "B1 image": ("1", "1"), - "B2 image": ("P", "P;2"), - "B4 image": ("P", "P;4"), - "X 24 image": ("RGB", "RGB"), - "L 32 S image": ("I", "I;32"), - "L 32 F image": ("F", "F;32"), - # old p3cfunc formats - "RGB3 image": ("RGB", "RGB;T"), - "RYB3 image": ("RGB", "RYB;T"), - # extensions - "LA image": ("LA", "LA;L"), - "PA image": ("LA", "PA;L"), - "RGBA image": ("RGBA", "RGBA;L"), - "RGBX image": ("RGB", "RGBX;L"), - "CMYK image": ("CMYK", "CMYK;L"), - "YCC image": ("YCbCr", "YCbCr;L"), -} - -# ifunc95 extensions -for i in ["8", "8S", "16", "16S", "32", "32F"]: - OPEN[f"L {i} image"] = ("F", f"F;{i}") - OPEN[f"L*{i} image"] = ("F", f"F;{i}") -for i in ["16", "16L", "16B"]: - OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}") - OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}") -for i in ["32S"]: - OPEN[f"L {i} image"] = ("I", f"I;{i}") - OPEN[f"L*{i} image"] = ("I", f"I;{i}") -for j in range(2, 33): - OPEN[f"L*{j} image"] = ("F", f"F;{j}") - - -# -------------------------------------------------------------------- -# Read IM directory - -split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") - - -def number(s: Any) -> float: - try: - return int(s) - except ValueError: - return float(s) - - -## -# Image plugin for the IFUNC IM file format. - - -class ImImageFile(ImageFile.ImageFile): - format = "IM" - format_description = "IFUNC Image Memory" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # Quick rejection: if there's not an LF among the first - # 100 bytes, this is (probably) not a text header. - - assert self.fp is not None - if b"\n" not in self.fp.read(100): - msg = "not an IM file" - raise SyntaxError(msg) - self.fp.seek(0) - - n = 0 - - # Default values - self.info[MODE] = "L" - self.info[SIZE] = (512, 512) - self.info[FRAMES] = 1 - - self.rawmode = "L" - - while True: - s = self.fp.read(1) - - # Some versions of IFUNC uses \n\r instead of \r\n... - if s == b"\r": - continue - - if not s or s == b"\0" or s == b"\x1a": - break - - # FIXME: this may read whole file if not a text file - s = s + self.fp.readline() - - if len(s) > 100: - msg = "not an IM file" - raise SyntaxError(msg) - - if s.endswith(b"\r\n"): - s = s[:-2] - elif s.endswith(b"\n"): - s = s[:-1] - - try: - m = split.match(s) - except re.error as e: - msg = "not an IM file" - raise SyntaxError(msg) from e - - if m: - k, v = m.group(1, 2) - - # Don't know if this is the correct encoding, - # but a decent guess (I guess) - k = k.decode("latin-1", "replace") - v = v.decode("latin-1", "replace") - - # Convert value as appropriate - if k in [FRAMES, SCALE, SIZE]: - v = v.replace("*", ",") - v = tuple(map(number, v.split(","))) - if len(v) == 1: - v = v[0] - elif k == MODE and v in OPEN: - v, self.rawmode = OPEN[v] - - # Add to dictionary. Note that COMMENT tags are - # combined into a list of strings. - if k == COMMENT: - if k in self.info: - self.info[k].append(v) - else: - self.info[k] = [v] - else: - self.info[k] = v - - if k in TAGS: - n += 1 - - else: - msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}" - raise SyntaxError(msg) - - if not n: - msg = "Not an IM file" - raise SyntaxError(msg) - - # Basic attributes - self._size = self.info[SIZE] - self._mode = self.info[MODE] - - # Skip forward to start of image data - while s and not s.startswith(b"\x1a"): - s = self.fp.read(1) - if not s: - msg = "File truncated" - raise SyntaxError(msg) - - if LUT in self.info: - # convert lookup table to palette or lut attribute - palette = self.fp.read(768) - greyscale = 1 # greyscale palette - linear = 1 # linear greyscale palette - for i in range(256): - if palette[i] == palette[i + 256] == palette[i + 512]: - if palette[i] != i: - linear = 0 - else: - greyscale = 0 - if self.mode in ["L", "LA", "P", "PA"]: - if greyscale: - if not linear: - self.lut = list(palette[:256]) - else: - if self.mode in ["L", "P"]: - self._mode = self.rawmode = "P" - elif self.mode in ["LA", "PA"]: - self._mode = "PA" - self.rawmode = "PA;L" - self.palette = ImagePalette.raw("RGB;L", palette) - elif self.mode == "RGB": - if not greyscale or not linear: - self.lut = list(palette) - - self.frame = 0 - - self.__offset = offs = self.fp.tell() - - self._fp = self.fp # FIXME: hack - - if self.rawmode.startswith("F;"): - # ifunc95 formats - try: - # use bit decoder (if necessary) - bits = int(self.rawmode[2:]) - if bits not in [8, 16, 32]: - self.tile = [ - ImageFile._Tile( - "bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1) - ) - ] - return - except ValueError: - pass - - if self.rawmode in ["RGB;T", "RYB;T"]: - # Old LabEye/3PC files. Would be very surprised if anyone - # ever stumbled upon such a file ;-) - size = self.size[0] * self.size[1] - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)), - ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), - ImageFile._Tile( - "raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1) - ), - ] - else: - # LabEye/IFUNC files - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) - ] - - @property - def n_frames(self) -> int: - return self.info[FRAMES] - - @property - def is_animated(self) -> bool: - return self.info[FRAMES] > 1 - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - - self.frame = frame - - if self.mode == "1": - bits = 1 - else: - bits = 8 * len(self.mode) - - size = ((self.size[0] * bits + 7) // 8) * self.size[1] - offs = self.__offset + frame * size - - self.fp = self._fp - - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) - ] - - def tell(self) -> int: - return self.frame - - -# -# -------------------------------------------------------------------- -# Save IM files - - -SAVE = { - # mode: (im type, raw mode) - "1": ("0 1", "1"), - "L": ("Greyscale", "L"), - "LA": ("LA", "LA;L"), - "P": ("Greyscale", "P"), - "PA": ("LA", "PA;L"), - "I": ("L 32S", "I;32S"), - "I;16": ("L 16", "I;16"), - "I;16L": ("L 16L", "I;16L"), - "I;16B": ("L 16B", "I;16B"), - "F": ("L 32F", "F;32F"), - "RGB": ("RGB", "RGB;L"), - "RGBA": ("RGBA", "RGBA;L"), - "RGBX": ("RGBX", "RGBX;L"), - "CMYK": ("CMYK", "CMYK;L"), - "YCbCr": ("YCC", "YCbCr;L"), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - image_type, rawmode = SAVE[im.mode] - except KeyError as e: - msg = f"Cannot save {im.mode} images as IM" - raise ValueError(msg) from e - - frames = im.encoderinfo.get("frames", 1) - - fp.write(f"Image type: {image_type} image\r\n".encode("ascii")) - if filename: - # Each line must be 100 characters or less, - # or: SyntaxError("not an IM file") - # 8 characters are used for "Name: " and "\r\n" - # Keep just the filename, ditch the potentially overlong path - if isinstance(filename, bytes): - filename = filename.decode("ascii") - name, ext = os.path.splitext(os.path.basename(filename)) - name = "".join([name[: 92 - len(ext)], ext]) - - fp.write(f"Name: {name}\r\n".encode("ascii")) - fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii")) - fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) - if im.mode in ["P", "PA"]: - fp.write(b"Lut: 1\r\n") - fp.write(b"\000" * (511 - fp.tell()) + b"\032") - if im.mode in ["P", "PA"]: - im_palette = im.im.getpalette("RGB", "RGB;L") - colors = len(im_palette) // 3 - palette = b"" - for i in range(3): - palette += im_palette[colors * i : colors * (i + 1)] - palette += b"\x00" * (256 - colors) - fp.write(palette) # 768 bytes - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))] - ) - - -# -# -------------------------------------------------------------------- -# Registry - - -Image.register_open(ImImageFile.format, ImImageFile) -Image.register_save(ImImageFile.format, _save) - -Image.register_extension(ImImageFile.format, ".im") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/Image.py b/presentation/.venv/lib/python3.12/site-packages/PIL/Image.py deleted file mode 100644 index 5749807..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/Image.py +++ /dev/null @@ -1,4381 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# the Image class wrapper -# -# partial release history: -# 1995-09-09 fl Created -# 1996-03-11 fl PIL release 0.0 (proof of concept) -# 1996-04-30 fl PIL release 0.1b1 -# 1999-07-28 fl PIL release 1.0 final -# 2000-06-07 fl PIL release 1.1 -# 2000-10-20 fl PIL release 1.1.1 -# 2001-05-07 fl PIL release 1.1.2 -# 2002-03-15 fl PIL release 1.1.3 -# 2003-05-10 fl PIL release 1.1.4 -# 2005-03-28 fl PIL release 1.1.5 -# 2006-12-02 fl PIL release 1.1.6 -# 2009-11-15 fl PIL release 1.1.7 -# -# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2009 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -from __future__ import annotations - -import abc -import atexit -import builtins -import io -import logging -import math -import os -import re -import struct -import sys -import tempfile -import warnings -from collections.abc import MutableMapping -from enum import IntEnum -from typing import IO, Protocol, cast - -# VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION was removed in Pillow 9.0.0. -# Use __version__ instead. -from . import ( - ExifTags, - ImageMode, - TiffTags, - UnidentifiedImageError, - __version__, - _plugins, -) -from ._binary import i32le, o32be, o32le -from ._deprecate import deprecate -from ._util import DeferredError, is_path - -ElementTree: ModuleType | None -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable, Iterator, Sequence - from types import ModuleType - from typing import Any, Literal - -logger = logging.getLogger(__name__) - - -class DecompressionBombWarning(RuntimeWarning): - pass - - -class DecompressionBombError(Exception): - pass - - -WARN_POSSIBLE_FORMATS: bool = False - -# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image -MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3) - - -try: - # If the _imaging C module is not present, Pillow will not load. - # Note that other modules should not refer to _imaging directly; - # import Image and use the Image.core variable instead. - # Also note that Image.core is not a publicly documented interface, - # and should be considered private and subject to change. - from . import _imaging as core - - if __version__ != getattr(core, "PILLOW_VERSION", None): - msg = ( - "The _imaging extension was built for another version of Pillow or PIL:\n" - f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" - f"Pillow version: {__version__}" - ) - raise ImportError(msg) - -except ImportError as v: - # Explanations for ways that we know we might have an import error - if str(v).startswith("Module use of python"): - # The _imaging C module is present, but not compiled for - # the right version (windows only). Print a warning, if - # possible. - warnings.warn( - "The _imaging extension was built for another version of Python.", - RuntimeWarning, - ) - elif str(v).startswith("The _imaging extension"): - warnings.warn(str(v), RuntimeWarning) - # Fail here anyway. Don't let people run with a mostly broken Pillow. - # see docs/porting.rst - raise - - -# -# Constants - - -# transpose -class Transpose(IntEnum): - FLIP_LEFT_RIGHT = 0 - FLIP_TOP_BOTTOM = 1 - ROTATE_90 = 2 - ROTATE_180 = 3 - ROTATE_270 = 4 - TRANSPOSE = 5 - TRANSVERSE = 6 - - -# transforms (also defined in Imaging.h) -class Transform(IntEnum): - AFFINE = 0 - EXTENT = 1 - PERSPECTIVE = 2 - QUAD = 3 - MESH = 4 - - -# resampling filters (also defined in Imaging.h) -class Resampling(IntEnum): - NEAREST = 0 - BOX = 4 - BILINEAR = 2 - HAMMING = 5 - BICUBIC = 3 - LANCZOS = 1 - - -_filters_support = { - Resampling.BOX: 0.5, - Resampling.BILINEAR: 1.0, - Resampling.HAMMING: 1.0, - Resampling.BICUBIC: 2.0, - Resampling.LANCZOS: 3.0, -} - - -# dithers -class Dither(IntEnum): - NONE = 0 - ORDERED = 1 # Not yet implemented - RASTERIZE = 2 # Not yet implemented - FLOYDSTEINBERG = 3 # default - - -# palettes/quantizers -class Palette(IntEnum): - WEB = 0 - ADAPTIVE = 1 - - -class Quantize(IntEnum): - MEDIANCUT = 0 - MAXCOVERAGE = 1 - FASTOCTREE = 2 - LIBIMAGEQUANT = 3 - - -module = sys.modules[__name__] -for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): - for item in enum: - setattr(module, item.name, item.value) - - -if hasattr(core, "DEFAULT_STRATEGY"): - DEFAULT_STRATEGY = core.DEFAULT_STRATEGY - FILTERED = core.FILTERED - HUFFMAN_ONLY = core.HUFFMAN_ONLY - RLE = core.RLE - FIXED = core.FIXED - - -# -------------------------------------------------------------------- -# Registries - -TYPE_CHECKING = False -if TYPE_CHECKING: - import mmap - from xml.etree.ElementTree import Element - - from IPython.lib.pretty import PrettyPrinter - - from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin - from ._typing import CapsuleType, NumpyArray, StrOrBytesPath -ID: list[str] = [] -OPEN: dict[ - str, - tuple[ - Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], - Callable[[bytes], bool | str] | None, - ], -] = {} -MIME: dict[str, str] = {} -SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} -SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} -EXTENSION: dict[str, str] = {} -DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} -ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} - -# -------------------------------------------------------------------- -# Modes - -_ENDIAN = "<" if sys.byteorder == "little" else ">" - - -def _conv_type_shape(im: Image) -> tuple[tuple[int, ...], str]: - m = ImageMode.getmode(im.mode) - shape: tuple[int, ...] = (im.height, im.width) - extra = len(m.bands) - if extra != 1: - shape += (extra,) - return shape, m.typestr - - -MODES = [ - "1", - "CMYK", - "F", - "HSV", - "I", - "I;16", - "I;16B", - "I;16L", - "I;16N", - "L", - "LA", - "La", - "LAB", - "P", - "PA", - "RGB", - "RGBA", - "RGBa", - "RGBX", - "YCbCr", -] - -# raw modes that may be memory mapped. NOTE: if you change this, you -# may have to modify the stride calculation in map.c too! -_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") - - -def getmodebase(mode: str) -> str: - """ - Gets the "base" mode for given mode. This function returns "L" for - images that contain grayscale data, and "RGB" for images that - contain color data. - - :param mode: Input mode. - :returns: "L" or "RGB". - :exception KeyError: If the input mode was not a standard mode. - """ - return ImageMode.getmode(mode).basemode - - -def getmodetype(mode: str) -> str: - """ - Gets the storage type mode. Given a mode, this function returns a - single-layer mode suitable for storing individual bands. - - :param mode: Input mode. - :returns: "L", "I", or "F". - :exception KeyError: If the input mode was not a standard mode. - """ - return ImageMode.getmode(mode).basetype - - -def getmodebandnames(mode: str) -> tuple[str, ...]: - """ - Gets a list of individual band names. Given a mode, this function returns - a tuple containing the names of individual bands (use - :py:method:`~PIL.Image.getmodetype` to get the mode used to store each - individual band. - - :param mode: Input mode. - :returns: A tuple containing band names. The length of the tuple - gives the number of bands in an image of the given mode. - :exception KeyError: If the input mode was not a standard mode. - """ - return ImageMode.getmode(mode).bands - - -def getmodebands(mode: str) -> int: - """ - Gets the number of individual bands for this mode. - - :param mode: Input mode. - :returns: The number of bands in this mode. - :exception KeyError: If the input mode was not a standard mode. - """ - return len(ImageMode.getmode(mode).bands) - - -# -------------------------------------------------------------------- -# Helpers - -_initialized = 0 - -# Mapping from file extension to plugin module name for lazy importing -_EXTENSION_PLUGIN: dict[str, str] = { - # Common formats (preinit) - ".bmp": "BmpImagePlugin", - ".dib": "BmpImagePlugin", - ".gif": "GifImagePlugin", - ".jfif": "JpegImagePlugin", - ".jpe": "JpegImagePlugin", - ".jpg": "JpegImagePlugin", - ".jpeg": "JpegImagePlugin", - ".pbm": "PpmImagePlugin", - ".pgm": "PpmImagePlugin", - ".pnm": "PpmImagePlugin", - ".ppm": "PpmImagePlugin", - ".pfm": "PpmImagePlugin", - ".png": "PngImagePlugin", - ".apng": "PngImagePlugin", - # Less common formats (init) - ".avif": "AvifImagePlugin", - ".avifs": "AvifImagePlugin", - ".blp": "BlpImagePlugin", - ".bufr": "BufrStubImagePlugin", - ".cur": "CurImagePlugin", - ".dcx": "DcxImagePlugin", - ".dds": "DdsImagePlugin", - ".ps": "EpsImagePlugin", - ".eps": "EpsImagePlugin", - ".fit": "FitsImagePlugin", - ".fits": "FitsImagePlugin", - ".fli": "FliImagePlugin", - ".flc": "FliImagePlugin", - ".fpx": "FpxImagePlugin", - ".ftc": "FtexImagePlugin", - ".ftu": "FtexImagePlugin", - ".gbr": "GbrImagePlugin", - ".grib": "GribStubImagePlugin", - ".h5": "Hdf5StubImagePlugin", - ".hdf": "Hdf5StubImagePlugin", - ".icns": "IcnsImagePlugin", - ".ico": "IcoImagePlugin", - ".im": "ImImagePlugin", - ".iim": "IptcImagePlugin", - ".jp2": "Jpeg2KImagePlugin", - ".j2k": "Jpeg2KImagePlugin", - ".jpc": "Jpeg2KImagePlugin", - ".jpf": "Jpeg2KImagePlugin", - ".jpx": "Jpeg2KImagePlugin", - ".j2c": "Jpeg2KImagePlugin", - ".mic": "MicImagePlugin", - ".mpg": "MpegImagePlugin", - ".mpeg": "MpegImagePlugin", - ".mpo": "MpoImagePlugin", - ".msp": "MspImagePlugin", - ".palm": "PalmImagePlugin", - ".pcd": "PcdImagePlugin", - ".pcx": "PcxImagePlugin", - ".pdf": "PdfImagePlugin", - ".pxr": "PixarImagePlugin", - ".psd": "PsdImagePlugin", - ".qoi": "QoiImagePlugin", - ".bw": "SgiImagePlugin", - ".rgb": "SgiImagePlugin", - ".rgba": "SgiImagePlugin", - ".sgi": "SgiImagePlugin", - ".ras": "SunImagePlugin", - ".tga": "TgaImagePlugin", - ".icb": "TgaImagePlugin", - ".vda": "TgaImagePlugin", - ".vst": "TgaImagePlugin", - ".tif": "TiffImagePlugin", - ".tiff": "TiffImagePlugin", - ".webp": "WebPImagePlugin", - ".wmf": "WmfImagePlugin", - ".emf": "WmfImagePlugin", - ".xbm": "XbmImagePlugin", - ".xpm": "XpmImagePlugin", -} - - -def _import_plugin_for_extension(ext: str | bytes) -> bool: - """Import only the plugin needed for a specific file extension.""" - if not ext: - return False - - if isinstance(ext, bytes): - ext = ext.decode() - ext = ext.lower() - if ext in EXTENSION: - return True - - plugin = _EXTENSION_PLUGIN.get(ext) - if plugin is None: - return False - - try: - logger.debug("Importing %s", plugin) - __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) - return True - except ImportError as e: - logger.debug("Image: failed to import %s: %s", plugin, e) - return False - - -def preinit() -> None: - """ - Explicitly loads BMP, GIF, JPEG, PPM and PNG file format drivers. - - It is called when opening or saving images. - """ - - global _initialized - if _initialized >= 1: - return - - try: - from . import BmpImagePlugin - - assert BmpImagePlugin - except ImportError: - pass - try: - from . import GifImagePlugin - - assert GifImagePlugin - except ImportError: - pass - try: - from . import JpegImagePlugin - - assert JpegImagePlugin - except ImportError: - pass - try: - from . import PpmImagePlugin - - assert PpmImagePlugin - except ImportError: - pass - try: - from . import PngImagePlugin - - assert PngImagePlugin - except ImportError: - pass - - _initialized = 1 - - -def init() -> bool: - """ - Explicitly initializes the Python Imaging Library. This function - loads all available file format drivers. - - It is called when opening or saving images if :py:meth:`~preinit()` is - insufficient, and by :py:meth:`~PIL.features.pilinfo`. - """ - - global _initialized - if _initialized >= 2: - return False - - for plugin in _plugins: - try: - logger.debug("Importing %s", plugin) - __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) - except ImportError as e: # noqa: PERF203 - logger.debug("Image: failed to import %s: %s", plugin, e) - - if OPEN or SAVE: - _initialized = 2 - return True - return False - - -# -------------------------------------------------------------------- -# Codec factories (used by tobytes/frombytes and ImageFile.load) - - -def _getdecoder( - mode: str, decoder_name: str, args: Any, extra: tuple[Any, ...] = () -) -> core.ImagingDecoder | ImageFile.PyDecoder: - # tweak arguments - if args is None: - args = () - elif not isinstance(args, tuple): - args = (args,) - - try: - decoder = DECODERS[decoder_name] - except KeyError: - pass - else: - return decoder(mode, *args + extra) - - try: - # get decoder - decoder = getattr(core, f"{decoder_name}_decoder") - except AttributeError as e: - msg = f"decoder {decoder_name} not available" - raise OSError(msg) from e - return decoder(mode, *args + extra) - - -def _getencoder( - mode: str, encoder_name: str, args: Any, extra: tuple[Any, ...] = () -) -> core.ImagingEncoder | ImageFile.PyEncoder: - # tweak arguments - if args is None: - args = () - elif not isinstance(args, tuple): - args = (args,) - - try: - encoder = ENCODERS[encoder_name] - except KeyError: - pass - else: - return encoder(mode, *args + extra) - - try: - # get encoder - encoder = getattr(core, f"{encoder_name}_encoder") - except AttributeError as e: - msg = f"encoder {encoder_name} not available" - raise OSError(msg) from e - return encoder(mode, *args + extra) - - -# -------------------------------------------------------------------- -# Simple expression analyzer - - -class ImagePointTransform: - """ - Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than - 8 bits, this represents an affine transformation, where the value is multiplied by - ``scale`` and ``offset`` is added. - """ - - def __init__(self, scale: float, offset: float) -> None: - self.scale = scale - self.offset = offset - - def __neg__(self) -> ImagePointTransform: - return ImagePointTransform(-self.scale, -self.offset) - - def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform: - if isinstance(other, ImagePointTransform): - return ImagePointTransform( - self.scale + other.scale, self.offset + other.offset - ) - return ImagePointTransform(self.scale, self.offset + other) - - __radd__ = __add__ - - def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform: - return self + -other - - def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform: - return other + -self - - def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform: - if isinstance(other, ImagePointTransform): - return NotImplemented - return ImagePointTransform(self.scale * other, self.offset * other) - - __rmul__ = __mul__ - - def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform: - if isinstance(other, ImagePointTransform): - return NotImplemented - return ImagePointTransform(self.scale / other, self.offset / other) - - -def _getscaleoffset( - expr: Callable[[ImagePointTransform], ImagePointTransform | float], -) -> tuple[float, float]: - a = expr(ImagePointTransform(1, 0)) - return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) - - -# -------------------------------------------------------------------- -# Implementation wrapper - - -class SupportsGetData(Protocol): - def getdata( - self, - ) -> tuple[Transform, Sequence[int]]: ... - - -class Image: - """ - This class represents an image object. To create - :py:class:`~PIL.Image.Image` objects, use the appropriate factory - functions. There's hardly ever any reason to call the Image constructor - directly. - - * :py:func:`~PIL.Image.open` - * :py:func:`~PIL.Image.new` - * :py:func:`~PIL.Image.frombytes` - """ - - format: str | None = None - format_description: str | None = None - _close_exclusive_fp_after_loading = True - - def __init__(self) -> None: - # FIXME: take "new" parameters / other image? - self._im: core.ImagingCore | DeferredError | None = None - self._mode = "" - self._size = (0, 0) - self.palette: ImagePalette.ImagePalette | None = None - self.info: dict[str | tuple[int, int], Any] = {} - self.readonly = 0 - self._exif: Exif | None = None - - @property - def im(self) -> core.ImagingCore: - if isinstance(self._im, DeferredError): - raise self._im.ex - assert self._im is not None - return self._im - - @im.setter - def im(self, im: core.ImagingCore) -> None: - self._im = im - - @property - def width(self) -> int: - return self.size[0] - - @property - def height(self) -> int: - return self.size[1] - - @property - def size(self) -> tuple[int, int]: - return self._size - - @property - def mode(self) -> str: - return self._mode - - @property - def readonly(self) -> int: - return (self._im and self._im.readonly) or self._readonly - - @readonly.setter - def readonly(self, readonly: int) -> None: - self._readonly = readonly - - def _new(self, im: core.ImagingCore) -> Image: - new = Image() - new.im = im - new._mode = im.mode - new._size = im.size - if im.mode in ("P", "PA"): - if self.palette: - new.palette = self.palette.copy() - else: - from . import ImagePalette - - new.palette = ImagePalette.ImagePalette() - new.info = self.info.copy() - return new - - # Context manager support - def __enter__(self) -> Image: - return self - - def __exit__(self, *args: object) -> None: - pass - - def close(self) -> None: - """ - This operation will destroy the image core and release its memory. - The image data will be unusable afterward. - - This function is required to close images that have multiple frames or - have not had their file read and closed by the - :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for - more information. - """ - if getattr(self, "map", None): - if sys.platform == "win32" and hasattr(sys, "pypy_version_info"): - self.map.close() - self.map: mmap.mmap | None = None - - # Instead of simply setting to None, we're setting up a - # deferred error that will better explain that the core image - # object is gone. - self._im = DeferredError(ValueError("Operation on closed image")) - - def _copy(self) -> None: - self.load() - self.im = self.im.copy() - self.readonly = 0 - - def _ensure_mutable(self) -> None: - if self.readonly: - self._copy() - else: - self.load() - - def _dump( - self, file: str | None = None, format: str | None = None, **options: Any - ) -> str: - suffix = "" - if format: - suffix = f".{format}" - - if not file: - f, filename = tempfile.mkstemp(suffix) - os.close(f) - else: - filename = file - if not filename.endswith(suffix): - filename = filename + suffix - - self.load() - - if not format or format == "PPM": - self.im.save_ppm(filename) - else: - self.save(filename, format, **options) - - return filename - - def __eq__(self, other: object) -> bool: - if self.__class__ is not other.__class__: - return False - assert isinstance(other, Image) - return ( - self.mode == other.mode - and self.size == other.size - and self.info == other.info - and self.getpalette() == other.getpalette() - and self.tobytes() == other.tobytes() - ) - - def __repr__(self) -> str: - return ( - f"<{self.__class__.__module__}.{self.__class__.__name__} " - f"image mode={self.mode} size={self.size[0]}x{self.size[1]} " - f"at 0x{id(self):X}>" - ) - - def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: - """IPython plain text display support""" - - # Same as __repr__ but without unpredictable id(self), - # to keep Jupyter notebook `text/plain` output stable. - p.text( - f"<{self.__class__.__module__}.{self.__class__.__name__} " - f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>" - ) - - def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: - """Helper function for iPython display hook. - - :param image_format: Image format. - :returns: image as bytes, saved into the given format. - """ - b = io.BytesIO() - try: - self.save(b, image_format, **kwargs) - except Exception: - return None - return b.getvalue() - - def _repr_png_(self) -> bytes | None: - """iPython display hook support for PNG format. - - :returns: PNG version of the image as bytes - """ - return self._repr_image("PNG", compress_level=1) - - def _repr_jpeg_(self) -> bytes | None: - """iPython display hook support for JPEG format. - - :returns: JPEG version of the image as bytes - """ - return self._repr_image("JPEG") - - @property - def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: - # numpy array interface support - new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} - if self.mode == "1": - # Binary images need to be extended from bits to bytes - # See: https://github.com/python-pillow/Pillow/issues/350 - new["data"] = self.tobytes("raw", "L") - else: - new["data"] = self.tobytes() - new["shape"], new["typestr"] = _conv_type_shape(self) - return new - - def __arrow_c_schema__(self) -> object: - self.load() - return self.im.__arrow_c_schema__() - - def __arrow_c_array__( - self, requested_schema: object | None = None - ) -> tuple[object, object]: - self.load() - return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__()) - - def __getstate__(self) -> list[Any]: - im_data = self.tobytes() # load image first - return [self.info, self.mode, self.size, self.getpalette(), im_data] - - def __setstate__(self, state: list[Any]) -> None: - Image.__init__(self) - info, mode, size, palette, data = state[:5] - self.info = info - self._mode = mode - self._size = size - self.im = core.new(mode, size) - if mode in ("L", "LA", "P", "PA") and palette: - self.putpalette(palette) - self.frombytes(data) - - def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: - """ - Return image as a bytes object. - - .. warning:: - - This method returns raw image data derived from Pillow's internal - storage. For compressed image data (e.g. PNG, JPEG) use - :meth:`~.save`, with a BytesIO parameter for in-memory data. - - :param encoder_name: What encoder to use. - - The default is to use the standard "raw" encoder. - To see how this packs pixel data into the returned - bytes, see :file:`libImaging/Pack.c`. - - A list of C encoders can be seen under codecs - section of the function array in - :file:`_imaging.c`. Python encoders are registered - within the relevant plugins. - :param args: Extra arguments to the encoder. - :returns: A :py:class:`bytes` object. - """ - - encoder_args: Any = args - if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple): - # may pass tuple instead of argument list - encoder_args = encoder_args[0] - - if encoder_name == "raw" and encoder_args == (): - encoder_args = self.mode - - self.load() - - if self.width == 0 or self.height == 0: - return b"" - - # unpack data - e = _getencoder(self.mode, encoder_name, encoder_args) - e.setimage(self.im, (0, 0) + self.size) - - from . import ImageFile - - bufsize = max(ImageFile.MAXBLOCK, self.size[0] * 4) # see RawEncode.c - - output = [] - while True: - bytes_consumed, errcode, data = e.encode(bufsize) - output.append(data) - if errcode: - break - if errcode < 0: - msg = f"encoder error {errcode} in tobytes" - raise RuntimeError(msg) - - return b"".join(output) - - def tobitmap(self, name: str = "image") -> bytes: - """ - Returns the image converted to an X11 bitmap. - - .. note:: This method only works for mode "1" images. - - :param name: The name prefix to use for the bitmap variables. - :returns: A string containing an X11 bitmap. - :raises ValueError: If the mode is not "1" - """ - - self.load() - if self.mode != "1": - msg = "not a bitmap" - raise ValueError(msg) - data = self.tobytes("xbm") - return b"".join( - [ - f"#define {name}_width {self.size[0]}\n".encode("ascii"), - f"#define {name}_height {self.size[1]}\n".encode("ascii"), - f"static char {name}_bits[] = {{\n".encode("ascii"), - data, - b"};", - ] - ) - - def frombytes( - self, - data: bytes | bytearray | SupportsArrayInterface, - decoder_name: str = "raw", - *args: Any, - ) -> None: - """ - Loads this image with pixel data from a bytes object. - - This method is similar to the :py:func:`~PIL.Image.frombytes` function, - but loads data into this image instead of creating a new image object. - """ - - if self.width == 0 or self.height == 0: - return - - decoder_args: Any = args - if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): - # may pass tuple instead of argument list - decoder_args = decoder_args[0] - - # default format - if decoder_name == "raw" and decoder_args == (): - decoder_args = self.mode - - # unpack data - d = _getdecoder(self.mode, decoder_name, decoder_args) - d.setimage(self.im, (0, 0) + self.size) - s = d.decode(data) - - if s[0] >= 0: - msg = "not enough image data" - raise ValueError(msg) - if s[1] != 0: - msg = "cannot decode image data" - raise ValueError(msg) - - def load(self) -> core.PixelAccess | None: - """ - Allocates storage for the image and loads the pixel data. In - normal cases, you don't need to call this method, since the - Image class automatically loads an opened image when it is - accessed for the first time. - - If the file associated with the image was opened by Pillow, then this - method will close it. The exception to this is if the image has - multiple frames, in which case the file will be left open for seek - operations. See :ref:`file-handling` for more information. - - :returns: An image access object. - :rtype: :py:class:`.PixelAccess` - """ - if self._im is not None and self.palette and self.palette.dirty: - # realize palette - mode, arr = self.palette.getdata() - self.im.putpalette(self.palette.mode, mode, arr) - self.palette.dirty = 0 - self.palette.rawmode = None - if "transparency" in self.info and mode in ("LA", "PA"): - if isinstance(self.info["transparency"], int): - self.im.putpalettealpha(self.info["transparency"], 0) - else: - self.im.putpalettealphas(self.info["transparency"]) - self.palette.mode = "RGBA" - elif self.palette.mode != mode: - # If the palette rawmode is different to the mode, - # then update the Python palette data - self.palette.palette = self.im.getpalette( - self.palette.mode, self.palette.mode - ) - - if self._im is not None: - return self.im.pixel_access(self.readonly) - return None - - def verify(self) -> None: - """ - Verifies the contents of a file. For data read from a file, this - method attempts to determine if the file is broken, without - actually decoding the image data. If this method finds any - problems, it raises suitable exceptions. If you need to load - the image after using this method, you must reopen the image - file. - """ - pass - - def convert( - self, - mode: str | None = None, - matrix: tuple[float, ...] | None = None, - dither: Dither | None = None, - palette: Palette = Palette.WEB, - colors: int = 256, - ) -> Image: - """ - Returns a converted copy of this image. For the "P" mode, this - method translates pixels through the palette. If mode is - omitted, a mode is chosen so that all information in the image - and the palette can be represented without a palette. - - This supports all possible conversions between "L", "RGB" and "CMYK". The - ``matrix`` argument only supports "L" and "RGB". - - When translating a color image to grayscale (mode "L"), - the library uses the ITU-R 601-2 luma transform:: - - L = R * 299/1000 + G * 587/1000 + B * 114/1000 - - The default method of converting a grayscale ("L") or "RGB" - image into a bilevel (mode "1") image uses Floyd-Steinberg - dither to approximate the original image luminosity levels. If - dither is ``None``, all values larger than 127 are set to 255 (white), - all other values to 0 (black). To use other thresholds, use the - :py:meth:`~PIL.Image.Image.point` method. - - When converting from "RGBA" to "P" without a ``matrix`` argument, - this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, - and ``dither`` and ``palette`` are ignored. - - When converting from "PA", if an "RGBA" palette is present, the alpha - channel from the image will be used instead of the values from the palette. - - :param mode: The requested mode. See: :ref:`concept-modes`. - :param matrix: An optional conversion matrix. If given, this - should be 4- or 12-tuple containing floating point values. - :param dither: Dithering method, used when converting from - mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` - (default). Note that this is not used when ``matrix`` is supplied. - :param palette: Palette to use when converting from mode "RGB" - to "P". Available palettes are :data:`Palette.WEB` or - :data:`Palette.ADAPTIVE`. - :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE` - palette. Defaults to 256. - :rtype: :py:class:`~PIL.Image.Image` - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - self.load() - - has_transparency = "transparency" in self.info - if not mode and self.mode == "P": - # determine default mode - if self.palette: - mode = self.palette.mode - else: - mode = "RGB" - if mode == "RGB" and has_transparency: - mode = "RGBA" - if not mode or (mode == self.mode and not matrix): - return self.copy() - - if matrix: - # matrix conversion - if mode not in ("L", "RGB"): - msg = "illegal conversion" - raise ValueError(msg) - im = self.im.convert_matrix(mode, matrix) - new_im = self._new(im) - if has_transparency and self.im.bands == 3: - transparency = new_im.info["transparency"] - - def convert_transparency( - m: tuple[float, ...], v: tuple[int, int, int] - ) -> int: - value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 - return max(0, min(255, int(value))) - - if mode == "L": - transparency = convert_transparency(matrix, transparency) - elif len(mode) == 3: - transparency = tuple( - convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) - for i in range(len(transparency)) - ) - new_im.info["transparency"] = transparency - return new_im - - if self.mode == "RGBA": - if mode == "P": - return self.quantize(colors) - elif mode == "PA": - r, g, b, a = self.split() - rgb = merge("RGB", (r, g, b)) - p = rgb.quantize(colors) - return merge("PA", (p, a)) - - trns = None - delete_trns = False - # transparency handling - if has_transparency: - if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or ( - self.mode == "RGB" and mode in ("La", "LA", "RGBa", "RGBA") - ): - # Use transparent conversion to promote from transparent - # color to an alpha channel. - new_im = self._new( - self.im.convert_transparent(mode, self.info["transparency"]) - ) - del new_im.info["transparency"] - return new_im - elif self.mode in ("L", "RGB", "P") and mode in ("L", "RGB", "P"): - t = self.info["transparency"] - if isinstance(t, bytes): - # Dragons. This can't be represented by a single color - warnings.warn( - "Palette images with Transparency expressed in bytes should be " - "converted to RGBA images" - ) - delete_trns = True - else: - # get the new transparency color. - # use existing conversions - trns_im = new(self.mode, (1, 1)) - if self.mode == "P": - assert self.palette is not None - trns_im.putpalette(self.palette, self.palette.mode) - if isinstance(t, tuple): - err = "Couldn't allocate a palette color for transparency" - assert trns_im.palette is not None - try: - t = trns_im.palette.getcolor(t, self) - except ValueError as e: - if str(e) == "cannot allocate more than 256 colors": - # If all 256 colors are in use, - # then there is no need for transparency - t = None - else: - raise ValueError(err) from e - if t is None: - trns = None - else: - trns_im.putpixel((0, 0), t) - - if mode in ("L", "RGB"): - trns_im = trns_im.convert(mode) - else: - # can't just retrieve the palette number, got to do it - # after quantization. - trns_im = trns_im.convert("RGB") - trns = trns_im.getpixel((0, 0)) - - elif self.mode == "P" and mode in ("LA", "PA", "RGBA"): - t = self.info["transparency"] - delete_trns = True - - if isinstance(t, bytes): - self.im.putpalettealphas(t) - elif isinstance(t, int): - self.im.putpalettealpha(t, 0) - else: - msg = "Transparency for P mode should be bytes or int" - raise ValueError(msg) - - if mode == "P" and palette == Palette.ADAPTIVE: - im = self.im.quantize(colors) - new_im = self._new(im) - from . import ImagePalette - - new_im.palette = ImagePalette.ImagePalette( - "RGB", new_im.im.getpalette("RGB") - ) - if delete_trns: - # This could possibly happen if we requantize to fewer colors. - # The transparency would be totally off in that case. - del new_im.info["transparency"] - if trns is not None: - try: - new_im.info["transparency"] = new_im.palette.getcolor( - cast(tuple[int, ...], trns), # trns was converted to RGB - new_im, - ) - except Exception: - # if we can't make a transparent color, don't leave the old - # transparency hanging around to mess us up. - del new_im.info["transparency"] - warnings.warn("Couldn't allocate palette entry for transparency") - return new_im - - if "LAB" in (self.mode, mode): - im = self - if mode == "LAB": - if im.mode not in ("RGB", "RGBA", "RGBX"): - im = im.convert("RGBA") - other_mode = im.mode - else: - other_mode = mode - if other_mode in ("RGB", "RGBA", "RGBX"): - from . import ImageCms - - srgb = ImageCms.createProfile("sRGB") - lab = ImageCms.createProfile("LAB") - profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab] - transform = ImageCms.buildTransform( - profiles[0], profiles[1], im.mode, mode - ) - return transform.apply(im) - - # colorspace conversion - if dither is None: - dither = Dither.FLOYDSTEINBERG - - try: - im = self.im.convert(mode, dither) - except ValueError: - try: - # normalize source image and try again - modebase = getmodebase(self.mode) - if modebase == self.mode: - raise - im = self.im.convert(modebase) - im = im.convert(mode, dither) - except KeyError as e: - msg = "illegal conversion" - raise ValueError(msg) from e - - new_im = self._new(im) - if mode in ("P", "PA") and palette != Palette.ADAPTIVE: - from . import ImagePalette - - new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB")) - if delete_trns: - # crash fail if we leave a bytes transparency in an rgb/l mode. - del new_im.info["transparency"] - if trns is not None: - if new_im.mode == "P" and new_im.palette: - try: - new_im.info["transparency"] = new_im.palette.getcolor( - cast(tuple[int, ...], trns), new_im # trns was converted to RGB - ) - except ValueError as e: - del new_im.info["transparency"] - if str(e) != "cannot allocate more than 256 colors": - # If all 256 colors are in use, - # then there is no need for transparency - warnings.warn( - "Couldn't allocate palette entry for transparency" - ) - else: - new_im.info["transparency"] = trns - return new_im - - def quantize( - self, - colors: int = 256, - method: int | None = None, - kmeans: int = 0, - palette: Image | None = None, - dither: Dither = Dither.FLOYDSTEINBERG, - ) -> Image: - """ - Convert the image to 'P' mode with the specified number - of colors. - - :param colors: The desired number of colors, <= 256 - :param method: :data:`Quantize.MEDIANCUT` (median cut), - :data:`Quantize.MAXCOVERAGE` (maximum coverage), - :data:`Quantize.FASTOCTREE` (fast octree), - :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support - using :py:func:`PIL.features.check_feature` with - ``feature="libimagequant"``). - - By default, :data:`Quantize.MEDIANCUT` will be used. - - The exception to this is RGBA images. :data:`Quantize.MEDIANCUT` - and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so - :data:`Quantize.FASTOCTREE` is used by default instead. - :param kmeans: Integer greater than or equal to zero. - :param palette: Quantize to the palette of given - :py:class:`PIL.Image.Image`. - :param dither: Dithering method, used when converting from - mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` - (default). - :returns: A new image - """ - - self.load() - - if method is None: - # defaults: - method = Quantize.MEDIANCUT - if self.mode == "RGBA": - method = Quantize.FASTOCTREE - - if self.mode == "RGBA" and method not in ( - Quantize.FASTOCTREE, - Quantize.LIBIMAGEQUANT, - ): - # Caller specified an invalid mode. - msg = ( - "Fast Octree (method == 2) and libimagequant (method == 3) " - "are the only valid methods for quantizing RGBA images" - ) - raise ValueError(msg) - - if palette: - # use palette from reference image - palette.load() - if palette.mode != "P": - msg = "bad mode for palette image" - raise ValueError(msg) - if self.mode not in {"RGB", "L"}: - msg = "only RGB or L mode images can be quantized to a palette" - raise ValueError(msg) - im = self.im.convert("P", dither, palette.im) - new_im = self._new(im) - assert palette.palette is not None - new_im.palette = palette.palette.copy() - return new_im - - if kmeans < 0: - msg = "kmeans must not be negative" - raise ValueError(msg) - - im = self._new(self.im.quantize(colors, method, kmeans)) - - from . import ImagePalette - - mode = im.im.getpalettemode() - palette_data = im.im.getpalette(mode, mode)[: colors * len(mode)] - im.palette = ImagePalette.ImagePalette(mode, palette_data) - - return im - - def copy(self) -> Image: - """ - Copies this image. Use this method if you wish to paste things - into an image, but still retain the original. - - :rtype: :py:class:`~PIL.Image.Image` - :returns: An :py:class:`~PIL.Image.Image` object. - """ - self.load() - return self._new(self.im.copy()) - - __copy__ = copy - - def crop(self, box: tuple[float, float, float, float] | None = None) -> Image: - """ - Returns a rectangular region from this image. The box is a - 4-tuple defining the left, upper, right, and lower pixel - coordinate. See :ref:`coordinate-system`. - - Note: Prior to Pillow 3.4.0, this was a lazy operation. - - :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. - :rtype: :py:class:`~PIL.Image.Image` - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if box is None: - return self.copy() - - if box[2] < box[0]: - msg = "Coordinate 'right' is less than 'left'" - raise ValueError(msg) - elif box[3] < box[1]: - msg = "Coordinate 'lower' is less than 'upper'" - raise ValueError(msg) - - self.load() - return self._new(self._crop(self.im, box)) - - def _crop( - self, im: core.ImagingCore, box: tuple[float, float, float, float] - ) -> core.ImagingCore: - """ - Returns a rectangular region from the core image object im. - - This is equivalent to calling im.crop((x0, y0, x1, y1)), but - includes additional sanity checks. - - :param im: a core image object - :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. - :returns: A core image object. - """ - - x0, y0, x1, y1 = map(int, map(round, box)) - - absolute_values = (abs(x1 - x0), abs(y1 - y0)) - - _decompression_bomb_check(absolute_values) - - return im.crop((x0, y0, x1, y1)) - - def draft( - self, mode: str | None, size: tuple[int, int] | None - ) -> tuple[str, tuple[int, int, float, float]] | None: - """ - Configures the image file loader so it returns a version of the - image that as closely as possible matches the given mode and - size. For example, you can use this method to convert a color - JPEG to grayscale while loading it. - - If any changes are made, returns a tuple with the chosen ``mode`` and - ``box`` with coordinates of the original image within the altered one. - - Note that this method modifies the :py:class:`~PIL.Image.Image` object - in place. If the image has already been loaded, this method has no - effect. - - Note: This method is not implemented for most images. It is - currently implemented only for JPEG and MPO images. - - :param mode: The requested mode. - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - """ - pass - - def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: - """ - Filters this image using the given filter. For a list of - available filters, see the :py:mod:`~PIL.ImageFilter` module. - - :param filter: Filter kernel. - :returns: An :py:class:`~PIL.Image.Image` object.""" - - from . import ImageFilter - - self.load() - - if callable(filter): - filter = filter() - if not hasattr(filter, "filter"): - msg = "filter argument should be ImageFilter.Filter instance or class" - raise TypeError(msg) - - multiband = isinstance(filter, ImageFilter.MultibandFilter) - if self.im.bands == 1 or multiband: - return self._new(filter.filter(self.im)) - - ims = [ - self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands) - ] - return merge(self.mode, ims) - - def getbands(self) -> tuple[str, ...]: - """ - Returns a tuple containing the name of each band in this image. - For example, ``getbands`` on an RGB image returns ("R", "G", "B"). - - :returns: A tuple containing band names. - :rtype: tuple - """ - return ImageMode.getmode(self.mode).bands - - def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None: - """ - Calculates the bounding box of the non-zero regions in the - image. - - :param alpha_only: Optional flag, defaulting to ``True``. - If ``True`` and the image has an alpha channel, trim transparent pixels. - Otherwise, trim pixels when all channels are zero. - Keyword-only argument. - :returns: The bounding box is returned as a 4-tuple defining the - left, upper, right, and lower pixel coordinate. See - :ref:`coordinate-system`. If the image is completely empty, this - method returns None. - - """ - - self.load() - return self.im.getbbox(alpha_only) - - def getcolors( - self, maxcolors: int = 256 - ) -> list[tuple[int, tuple[int, ...]]] | list[tuple[int, float]] | None: - """ - Returns a list of colors used in this image. - - The colors will be in the image's mode. For example, an RGB image will - return a tuple of (red, green, blue) color values, and a P image will - return the index of the color in the palette. - - :param maxcolors: Maximum number of colors. If this number is - exceeded, this method returns None. The default limit is - 256 colors. - :returns: An unsorted list of (count, pixel) values. - """ - - self.load() - if self.mode in ("1", "L", "P"): - h = self.im.histogram() - out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]] - if len(out) > maxcolors: - return None - return out - return self.im.getcolors(maxcolors) - - def getdata(self, band: int | None = None) -> core.ImagingCore: - """ - Returns the contents of this image as a sequence object - containing pixel values. The sequence object is flattened, so - that values for line one follow directly after the values of - line zero, and so on. - - Note that the sequence object returned by this method is an - internal PIL data type, which only supports certain sequence - operations. To convert it to an ordinary sequence (e.g. for - printing), use ``list(im.getdata())``. - - :param band: What band to return. The default is to return - all bands. To return a single band, pass in the index - value (e.g. 0 to get the "R" band from an "RGB" image). - :returns: A sequence-like object. - """ - deprecate("Image.Image.getdata", 14, "get_flattened_data") - - self.load() - if band is not None: - return self.im.getband(band) - return self.im # could be abused - - def get_flattened_data( - self, band: int | None = None - ) -> tuple[tuple[int, ...], ...] | tuple[float, ...]: - """ - Returns the contents of this image as a tuple containing pixel values. - The sequence object is flattened, so that values for line one follow - directly after the values of line zero, and so on. - - :param band: What band to return. The default is to return - all bands. To return a single band, pass in the index - value (e.g. 0 to get the "R" band from an "RGB" image). - :returns: A tuple containing pixel values. - """ - self.load() - if band is not None: - return tuple(self.im.getband(band)) - return tuple(self.im) - - def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: - """ - Gets the minimum and maximum pixel values for each band in - the image. - - :returns: For a single-band image, a 2-tuple containing the - minimum and maximum pixel value. For a multi-band image, - a tuple containing one 2-tuple for each band. - """ - - self.load() - if self.im.bands > 1: - return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) - return self.im.getextrema() - - def getxmp(self) -> dict[str, Any]: - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - - :returns: XMP tags in a dictionary. - """ - - def get_name(tag: str) -> str: - return re.sub("^{[^}]+}", "", tag) - - def get_value(element: Element) -> str | dict[str, Any] | None: - value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()} - children = list(element) - if children: - for child in children: - name = get_name(child.tag) - child_value = get_value(child) - if name in value: - if not isinstance(value[name], list): - value[name] = [value[name]] - value[name].append(child_value) - else: - value[name] = child_value - elif value: - if element.text: - value["text"] = element.text - else: - return element.text - return value - - if ElementTree is None: - warnings.warn("XMP data cannot be read without defusedxml dependency") - return {} - if "xmp" not in self.info: - return {} - root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 ")) - return {get_name(root.tag): get_value(root)} - - def getexif(self) -> Exif: - """ - Gets EXIF data from the image. - - :returns: an :py:class:`~PIL.Image.Exif` object. - """ - if self._exif is None: - self._exif = Exif() - elif self._exif._loaded: - return self._exif - self._exif._loaded = True - - exif_info = self.info.get("exif") - if exif_info is None: - if "Raw profile type exif" in self.info: - exif_info = bytes.fromhex( - "".join(self.info["Raw profile type exif"].split("\n")[3:]) - ) - elif hasattr(self, "tag_v2"): - from . import TiffImagePlugin - - assert isinstance(self, TiffImagePlugin.TiffImageFile) - self._exif.bigtiff = self.tag_v2._bigtiff - self._exif.endian = self.tag_v2._endian - - assert self.fp is not None - self._exif.load_from_fp(self.fp, self.tag_v2._offset) - if exif_info is not None: - self._exif.load(exif_info) - - # XMP tags - if ExifTags.Base.Orientation not in self._exif: - xmp_tags = self.info.get("XML:com.adobe.xmp") - pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' - if not xmp_tags and (xmp_tags := self.info.get("xmp")): - pattern = rb'tiff:Orientation(="|>)([0-9])' - if xmp_tags: - match = re.search(pattern, xmp_tags) - if match: - self._exif[ExifTags.Base.Orientation] = int(match[2]) - - return self._exif - - def _reload_exif(self) -> None: - if self._exif is None or not self._exif._loaded: - return - self._exif._loaded = False - self.getexif() - - def get_child_images(self) -> list[ImageFile.ImageFile]: - from . import ImageFile - - deprecate("Image.Image.get_child_images", 13) - return ImageFile.ImageFile.get_child_images(self) # type: ignore[arg-type] - - def getim(self) -> CapsuleType: - """ - Returns a capsule that points to the internal image memory. - - :returns: A capsule object. - """ - - self.load() - return self.im.ptr - - def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: - """ - Returns the image palette as a list. - - :param rawmode: The mode in which to return the palette. ``None`` will - return the palette in its current mode. - - .. versionadded:: 9.1.0 - - :returns: A list of color values [r, g, b, ...], or None if the - image has no palette. - """ - - self.load() - try: - mode = self.im.getpalettemode() - except ValueError: - return None # no palette - if rawmode is None: - rawmode = mode - return list(self.im.getpalette(mode, rawmode)) - - @property - def has_transparency_data(self) -> bool: - """ - Determine if an image has transparency data, whether in the form of an - alpha channel, a palette with an alpha channel, or a "transparency" key - in the info dictionary. - - Note the image might still appear solid, if all of the values shown - within are opaque. - - :returns: A boolean. - """ - if ( - self.mode in ("LA", "La", "PA", "RGBA", "RGBa") - or "transparency" in self.info - ): - return True - if self.mode == "P": - assert self.palette is not None - return self.palette.mode.endswith("A") - return False - - def apply_transparency(self) -> None: - """ - If a P mode image has a "transparency" key in the info dictionary, - remove the key and instead apply the transparency to the palette. - Otherwise, the image is unchanged. - """ - if self.mode != "P" or "transparency" not in self.info: - return - - from . import ImagePalette - - palette = self.getpalette("RGBA") - assert palette is not None - transparency = self.info["transparency"] - if isinstance(transparency, bytes): - for i, alpha in enumerate(transparency): - palette[i * 4 + 3] = alpha - else: - palette[transparency * 4 + 3] = 0 - self.palette = ImagePalette.ImagePalette("RGBA", bytes(palette)) - self.palette.dirty = 1 - - del self.info["transparency"] - - def getpixel( - self, xy: tuple[int, int] | list[int] - ) -> float | tuple[int, ...] | None: - """ - Returns the pixel value at a given position. - - :param xy: The coordinate, given as (x, y). See - :ref:`coordinate-system`. - :returns: The pixel value. If the image is a multi-layer image, - this method returns a tuple. - """ - - self.load() - return self.im.getpixel(tuple(xy)) - - def getprojection(self) -> tuple[list[int], list[int]]: - """ - Get projection to x and y axes - - :returns: Two sequences, indicating where there are non-zero - pixels along the X-axis and the Y-axis, respectively. - """ - - self.load() - x, y = self.im.getprojection() - return list(x), list(y) - - def histogram( - self, mask: Image | None = None, extrema: tuple[float, float] | None = None - ) -> list[int]: - """ - Returns a histogram for the image. The histogram is returned as a - list of pixel counts, one for each pixel value in the source - image. Counts are grouped into 256 bins for each band, even if - the image has more than 8 bits per band. If the image has more - than one band, the histograms for all bands are concatenated (for - example, the histogram for an "RGB" image contains 768 values). - - A bilevel image (mode "1") is treated as a grayscale ("L") image - by this method. - - If a mask is provided, the method returns a histogram for those - parts of the image where the mask image is non-zero. The mask - image must have the same size as the image, and be either a - bi-level image (mode "1") or a grayscale image ("L"). - - :param mask: An optional mask. - :param extrema: An optional tuple of manually-specified extrema. - :returns: A list containing pixel counts. - """ - self.load() - if mask: - mask.load() - return self.im.histogram((0, 0), mask.im) - if self.mode in ("I", "F"): - return self.im.histogram( - extrema if extrema is not None else self.getextrema() - ) - return self.im.histogram() - - def entropy( - self, mask: Image | None = None, extrema: tuple[float, float] | None = None - ) -> float: - """ - Calculates and returns the entropy for the image. - - A bilevel image (mode "1") is treated as a grayscale ("L") - image by this method. - - If a mask is provided, the method employs the histogram for - those parts of the image where the mask image is non-zero. - The mask image must have the same size as the image, and be - either a bi-level image (mode "1") or a grayscale image ("L"). - - :param mask: An optional mask. - :param extrema: An optional tuple of manually-specified extrema. - :returns: A float value representing the image entropy - """ - self.load() - if mask: - mask.load() - return self.im.entropy((0, 0), mask.im) - if self.mode in ("I", "F"): - return self.im.entropy( - extrema if extrema is not None else self.getextrema() - ) - return self.im.entropy() - - def paste( - self, - im: Image | str | float | tuple[float, ...], - box: Image | tuple[int, int, int, int] | tuple[int, int] | None = None, - mask: Image | None = None, - ) -> None: - """ - Pastes another image into this image. The box argument is either - a 2-tuple giving the upper left corner, a 4-tuple defining the - left, upper, right, and lower pixel coordinate, or None (same as - (0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size - of the pasted image must match the size of the region. - - If the modes don't match, the pasted image is converted to the mode of - this image (see the :py:meth:`~PIL.Image.Image.convert` method for - details). - - Instead of an image, the source can be a integer or tuple - containing pixel values. The method then fills the region - with the given color. When creating RGB images, you can - also use color strings as supported by the ImageColor module. See - :ref:`colors` for more information. - - If a mask is given, this method updates only the regions - indicated by the mask. You can use either "1", "L", "LA", "RGBA" - or "RGBa" images (if present, the alpha band is used as mask). - Where the mask is 255, the given image is copied as is. Where - the mask is 0, the current value is preserved. Intermediate - values will mix the two images together, including their alpha - channels if they have them. - - See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to - combine images with respect to their alpha channels. - - :param im: Source image or pixel value (integer, float or tuple). - :param box: An optional 4-tuple giving the region to paste into. - If a 2-tuple is used instead, it's treated as the upper left - corner. If omitted or None, the source is pasted into the - upper left corner. - - If an image is given as the second argument and there is no - third, the box defaults to (0, 0), and the second argument - is interpreted as a mask image. - :param mask: An optional mask image. - """ - - if isinstance(box, Image): - if mask is not None: - msg = "If using second argument as mask, third argument must be None" - raise ValueError(msg) - # abbreviated paste(im, mask) syntax - mask = box - box = None - - if box is None: - box = (0, 0) - - if len(box) == 2: - # upper left corner given; get size from image or mask - if isinstance(im, Image): - size = im.size - elif isinstance(mask, Image): - size = mask.size - else: - # FIXME: use self.size here? - msg = "cannot determine region size; use 4-item box" - raise ValueError(msg) - box += (box[0] + size[0], box[1] + size[1]) - - source: core.ImagingCore | str | float | tuple[float, ...] - if isinstance(im, str): - from . import ImageColor - - source = ImageColor.getcolor(im, self.mode) - elif isinstance(im, Image): - im.load() - if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): - # should use an adapter for this! - im = im.convert(self.mode) - source = im.im - else: - source = im - - self._ensure_mutable() - - if mask: - mask.load() - self.im.paste(source, box, mask.im) - else: - self.im.paste(source, box) - - def alpha_composite( - self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0) - ) -> None: - """'In-place' analog of Image.alpha_composite. Composites an image - onto this image. - - :param im: image to composite over this one - :param dest: Optional 2 tuple (left, top) specifying the upper - left corner in this (destination) image. - :param source: Optional 2 (left, top) tuple for the upper left - corner in the overlay source image, or 4 tuple (left, top, right, - bottom) for the bounds of the source rectangle - - Performance Note: Not currently implemented in-place in the core layer. - """ - - if not isinstance(source, (list, tuple)): - msg = "Source must be a list or tuple" - raise ValueError(msg) - if not isinstance(dest, (list, tuple)): - msg = "Destination must be a list or tuple" - raise ValueError(msg) - - if len(source) == 4: - overlay_crop_box = tuple(source) - elif len(source) == 2: - overlay_crop_box = tuple(source) + im.size - else: - msg = "Source must be a sequence of length 2 or 4" - raise ValueError(msg) - - if not len(dest) == 2: - msg = "Destination must be a sequence of length 2" - raise ValueError(msg) - if min(source) < 0: - msg = "Source must be non-negative" - raise ValueError(msg) - - # over image, crop if it's not the whole image. - if overlay_crop_box == (0, 0) + im.size: - overlay = im - else: - overlay = im.crop(overlay_crop_box) - - # target for the paste - box = tuple(dest) + (dest[0] + overlay.width, dest[1] + overlay.height) - - # destination image. don't copy if we're using the whole image. - if box == (0, 0) + self.size: - background = self - else: - background = self.crop(box) - - result = alpha_composite(background, overlay) - self.paste(result, box) - - def point( - self, - lut: ( - Sequence[float] - | NumpyArray - | Callable[[int], float] - | Callable[[ImagePointTransform], ImagePointTransform | float] - | ImagePointHandler - ), - mode: str | None = None, - ) -> Image: - """ - Maps this image through a lookup table or function. - - :param lut: A lookup table, containing 256 (or 65536 if - self.mode=="I" and mode == "L") values per band in the - image. A function can be used instead, it should take a - single argument. The function is called once for each - possible pixel value, and the resulting table is applied to - all bands of the image. - - It may also be an :py:class:`~PIL.Image.ImagePointHandler` - object:: - - class Example(Image.ImagePointHandler): - def point(self, im: Image) -> Image: - # Return result - :param mode: Output mode (default is same as input). This can only be used if - the source image has mode "L" or "P", and the output has mode "1" or the - source image mode is "I" and the output mode is "L". - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - self.load() - - if isinstance(lut, ImagePointHandler): - return lut.point(self) - - if callable(lut): - # if it isn't a list, it should be a function - if self.mode in ("I", "I;16", "F"): - # check if the function can be used with point_transform - # UNDONE wiredfool -- I think this prevents us from ever doing - # a gamma function point transform on > 8bit images. - scale, offset = _getscaleoffset(lut) # type: ignore[arg-type] - return self._new(self.im.point_transform(scale, offset)) - # for other modes, convert the function to a table - flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type] - else: - flatLut = lut - - if self.mode == "F": - # FIXME: _imaging returns a confusing error message for this case - msg = "point operation not supported for this mode" - raise ValueError(msg) - - if mode != "F": - flatLut = [round(i) for i in flatLut] - return self._new(self.im.point(flatLut, mode)) - - def putalpha(self, alpha: Image | int) -> None: - """ - Adds or replaces the alpha layer in this image. If the image - does not have an alpha layer, it's converted to "LA" or "RGBA". - The new layer must be either "L" or "1". - - :param alpha: The new alpha layer. This can either be an "L" or "1" - image having the same size as this image, or an integer. - """ - - self._ensure_mutable() - - if self.mode not in ("LA", "PA", "RGBA"): - # attempt to promote self to a matching alpha mode - try: - mode = getmodebase(self.mode) + "A" - try: - self.im.setmode(mode) - except (AttributeError, ValueError) as e: - # do things the hard way - im = self.im.convert(mode) - if im.mode not in ("LA", "PA", "RGBA"): - msg = "alpha channel could not be added" - raise ValueError(msg) from e # sanity check - self.im = im - self._mode = self.im.mode - except KeyError as e: - msg = "illegal image mode" - raise ValueError(msg) from e - - if self.mode in ("LA", "PA"): - band = 1 - else: - band = 3 - - if isinstance(alpha, Image): - # alpha layer - if alpha.mode not in ("1", "L"): - msg = "illegal image mode" - raise ValueError(msg) - alpha.load() - if alpha.mode == "1": - alpha = alpha.convert("L") - else: - # constant alpha - try: - self.im.fillband(band, alpha) - except (AttributeError, ValueError): - # do things the hard way - alpha = new("L", self.size, alpha) - else: - return - - self.im.putband(alpha.im, band) - - def putdata( - self, - data: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray, - scale: float = 1.0, - offset: float = 0.0, - ) -> None: - """ - Copies pixel data from a flattened sequence object into the image. The - values should start at the upper left corner (0, 0), continue to the - end of the line, followed directly by the first value of the second - line, and so on. Data will be read until either the image or the - sequence ends. The scale and offset values are used to adjust the - sequence values: **pixel = value*scale + offset**. - - :param data: A flattened sequence object. See :ref:`colors` for more - information about values. - :param scale: An optional scale value. The default is 1.0. - :param offset: An optional offset value. The default is 0.0. - """ - - self._ensure_mutable() - - self.im.putdata(data, scale, offset) - - def putpalette( - self, - data: ImagePalette.ImagePalette | bytes | Sequence[int], - rawmode: str = "RGB", - ) -> None: - """ - Attaches a palette to this image. The image must be a "P", "PA", "L" - or "LA" image. - - The palette sequence must contain at most 256 colors, made up of one - integer value for each channel in the raw mode. - For example, if the raw mode is "RGB", then it can contain at most 768 - values, made up of red, green and blue values for the corresponding pixel - index in the 256 colors. - If the raw mode is "RGBA", then it can contain at most 1024 values, - containing red, green, blue and alpha values. - - Alternatively, an 8-bit string may be used instead of an integer sequence. - - :param data: A palette sequence (either a list or a string). - :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", "CMYK", or a - mode that can be transformed to one of those modes (e.g. "R", "RGBA;L"). - """ - from . import ImagePalette - - if self.mode not in ("L", "LA", "P", "PA"): - msg = "illegal image mode" - raise ValueError(msg) - if isinstance(data, ImagePalette.ImagePalette): - if data.rawmode is not None: - palette = ImagePalette.raw(data.rawmode, data.palette) - else: - palette = ImagePalette.ImagePalette(palette=data.palette) - palette.dirty = 1 - else: - if not isinstance(data, bytes): - data = bytes(data) - palette = ImagePalette.raw(rawmode, data) - self._mode = "PA" if "A" in self.mode else "P" - self.palette = palette - if rawmode.startswith("CMYK"): - self.palette.mode = "CMYK" - elif "A" in rawmode: - self.palette.mode = "RGBA" - else: - self.palette.mode = "RGB" - self.load() # install new palette - - def putpixel( - self, xy: tuple[int, int], value: float | tuple[int, ...] | list[int] - ) -> None: - """ - Modifies the pixel at the given position. The color is given as - a single numerical value for single-band images, and a tuple for - multi-band images. In addition to this, RGB and RGBA tuples are - accepted for P and PA images. See :ref:`colors` for more information. - - Note that this method is relatively slow. For more extensive changes, - use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` - module instead. - - See: - - * :py:meth:`~PIL.Image.Image.paste` - * :py:meth:`~PIL.Image.Image.putdata` - * :py:mod:`~PIL.ImageDraw` - - :param xy: The pixel coordinate, given as (x, y). See - :ref:`coordinate-system`. - :param value: The pixel value. - """ - - self._ensure_mutable() - - if ( - self.mode in ("P", "PA") - and isinstance(value, (list, tuple)) - and len(value) in [3, 4] - ): - # RGB or RGBA value for a P or PA image - if self.mode == "PA": - alpha = value[3] if len(value) == 4 else 255 - value = value[:3] - assert self.palette is not None - palette_index = self.palette.getcolor(tuple(value), self) - value = (palette_index, alpha) if self.mode == "PA" else palette_index - return self.im.putpixel(xy, value) - - def remap_palette( - self, dest_map: list[int], source_palette: bytes | bytearray | None = None - ) -> Image: - """ - Rewrites the image to reorder the palette. - - :param dest_map: A list of indexes into the original palette. - e.g. ``[1,0]`` would swap a two item palette, and ``list(range(256))`` - is the identity transform. - :param source_palette: Bytes or None. - :returns: An :py:class:`~PIL.Image.Image` object. - - """ - from . import ImagePalette - - if self.mode not in ("L", "P"): - msg = "illegal image mode" - raise ValueError(msg) - - bands = 3 - palette_mode = "RGB" - if source_palette is None: - if self.mode == "P": - self.load() - palette_mode = self.im.getpalettemode() - if palette_mode == "RGBA": - bands = 4 - source_palette = self.im.getpalette(palette_mode, palette_mode) - else: # L-mode - source_palette = bytearray(i // 3 for i in range(768)) - elif len(source_palette) > 768: - bands = 4 - palette_mode = "RGBA" - - palette_bytes = b"" - new_positions = [0] * 256 - - # pick only the used colors from the palette - for i, oldPosition in enumerate(dest_map): - palette_bytes += source_palette[ - oldPosition * bands : oldPosition * bands + bands - ] - new_positions[oldPosition] = i - - # replace the palette color id of all pixel with the new id - - # Palette images are [0..255], mapped through a 1 or 3 - # byte/color map. We need to remap the whole image - # from palette 1 to palette 2. New_positions is - # an array of indexes into palette 1. Palette 2 is - # palette 1 with any holes removed. - - # We're going to leverage the convert mechanism to use the - # C code to remap the image from palette 1 to palette 2, - # by forcing the source image into 'L' mode and adding a - # mapping 'L' mode palette, then converting back to 'L' - # sans palette thus converting the image bytes, then - # assigning the optimized RGB palette. - - # perf reference, 9500x4000 gif, w/~135 colors - # 14 sec prepatch, 1 sec postpatch with optimization forced. - - mapping_palette = bytearray(new_positions) - - m_im = self.copy() - m_im._mode = "P" - - m_im.palette = ImagePalette.ImagePalette( - palette_mode, palette=mapping_palette * bands - ) - # possibly set palette dirty, then - # m_im.putpalette(mapping_palette, 'L') # converts to 'P' - # or just force it. - # UNDONE -- this is part of the general issue with palettes - m_im.im.putpalette(palette_mode, palette_mode + ";L", m_im.palette.tobytes()) - - m_im = m_im.convert("L") - - m_im.putpalette(palette_bytes, palette_mode) - m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) - - if "transparency" in self.info: - try: - m_im.info["transparency"] = dest_map.index(self.info["transparency"]) - except ValueError: - if "transparency" in m_im.info: - del m_im.info["transparency"] - - return m_im - - def _get_safe_box( - self, - size: tuple[int, int], - resample: Resampling, - box: tuple[float, float, float, float], - ) -> tuple[int, int, int, int]: - """Expands the box so it includes adjacent pixels - that may be used by resampling with the given resampling filter. - """ - filter_support = _filters_support[resample] - 0.5 - scale_x = (box[2] - box[0]) / size[0] - scale_y = (box[3] - box[1]) / size[1] - support_x = filter_support * scale_x - support_y = filter_support * scale_y - - return ( - max(0, int(box[0] - support_x)), - max(0, int(box[1] - support_y)), - min(self.size[0], math.ceil(box[2] + support_x)), - min(self.size[1], math.ceil(box[3] + support_y)), - ) - - def resize( - self, - size: tuple[int, int] | list[int] | NumpyArray, - resample: int | None = None, - box: tuple[float, float, float, float] | None = None, - reducing_gap: float | None = None, - ) -> Image: - """ - Returns a resized copy of this image. - - :param size: The requested size in pixels, as a tuple or array: - (width, height). - :param resample: An optional resampling filter. This can be - one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, - :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, - :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. - If the image has mode "1" or "P", it is always set to - :py:data:`Resampling.NEAREST`. Otherwise, the default filter is - :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. - :param box: An optional 4-tuple of floats providing - the source image region to be scaled. - The values must be within (0, 0, width, height) rectangle. - If omitted or None, the entire source is used. - :param reducing_gap: Apply optimization by resizing the image - in two steps. First, reducing the image by integer times - using :py:meth:`~PIL.Image.Image.reduce`. - Second, resizing using regular resampling. The last step - changes size no less than by ``reducing_gap`` times. - ``reducing_gap`` may be None (no first step is performed) - or should be greater than 1.0. The bigger ``reducing_gap``, - the closer the result to the fair resampling. - The smaller ``reducing_gap``, the faster resizing. - With ``reducing_gap`` greater or equal to 3.0, the result is - indistinguishable from fair resampling in most cases. - The default value is None (no optimization). - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if resample is None: - resample = Resampling.BICUBIC - elif resample not in ( - Resampling.NEAREST, - Resampling.BILINEAR, - Resampling.BICUBIC, - Resampling.LANCZOS, - Resampling.BOX, - Resampling.HAMMING, - ): - msg = f"Unknown resampling filter ({resample})." - - filters = [ - f"{filter[1]} ({filter[0]})" - for filter in ( - (Resampling.NEAREST, "Image.Resampling.NEAREST"), - (Resampling.LANCZOS, "Image.Resampling.LANCZOS"), - (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), - (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), - (Resampling.BOX, "Image.Resampling.BOX"), - (Resampling.HAMMING, "Image.Resampling.HAMMING"), - ) - ] - msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" - raise ValueError(msg) - - if reducing_gap is not None and reducing_gap < 1.0: - msg = "reducing_gap must be 1.0 or greater" - raise ValueError(msg) - - if box is None: - box = (0, 0) + self.size - - size = tuple(size) - if self.size == size and box == (0, 0) + self.size: - return self.copy() - - if self.mode in ("1", "P"): - resample = Resampling.NEAREST - - if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST: - im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) - im = im.resize(size, resample, box) - return im.convert(self.mode) - - self.load() - - if reducing_gap is not None and resample != Resampling.NEAREST: - factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 - factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 - if factor_x > 1 or factor_y > 1: - reduce_box = self._get_safe_box(size, cast(Resampling, resample), box) - factor = (factor_x, factor_y) - self = ( - self.reduce(factor, box=reduce_box) - if callable(self.reduce) - else Image.reduce(self, factor, box=reduce_box) - ) - box = ( - (box[0] - reduce_box[0]) / factor_x, - (box[1] - reduce_box[1]) / factor_y, - (box[2] - reduce_box[0]) / factor_x, - (box[3] - reduce_box[1]) / factor_y, - ) - - if self.size[1] > self.size[0] * 100 and size[1] < self.size[1]: - im = self.im.resize( - (self.size[0], size[1]), resample, (0, box[1], self.size[0], box[3]) - ) - im = im.resize(size, resample, (box[0], 0, box[2], size[1])) - else: - im = self.im.resize(size, resample, box) - return self._new(im) - - def reduce( - self, - factor: int | tuple[int, int], - box: tuple[int, int, int, int] | None = None, - ) -> Image: - """ - Returns a copy of the image reduced ``factor`` times. - If the size of the image is not dividable by ``factor``, - the resulting size will be rounded up. - - :param factor: A greater than 0 integer or tuple of two integers - for width and height separately. - :param box: An optional 4-tuple of ints providing - the source image region to be reduced. - The values must be within ``(0, 0, width, height)`` rectangle. - If omitted or ``None``, the entire source is used. - """ - if not isinstance(factor, (list, tuple)): - factor = (factor, factor) - - if box is None: - box = (0, 0) + self.size - - if factor == (1, 1) and box == (0, 0) + self.size: - return self.copy() - - if self.mode in ["LA", "RGBA"]: - im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) - im = im.reduce(factor, box) - return im.convert(self.mode) - - self.load() - - return self._new(self.im.reduce(factor, box)) - - def rotate( - self, - angle: float, - resample: Resampling = Resampling.NEAREST, - expand: int | bool = False, - center: tuple[float, float] | None = None, - translate: tuple[int, int] | None = None, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: - """ - Returns a rotated copy of this image. This method returns a - copy of this image, rotated the given number of degrees counter - clockwise around its centre. - - :param angle: In degrees counter clockwise. - :param resample: An optional resampling filter. This can be - one of :py:data:`Resampling.NEAREST` (use nearest neighbour), - :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`Resampling.BICUBIC` (cubic spline - interpolation in a 4x4 environment). If omitted, or if the image has - mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. - See :ref:`concept-filters`. - :param expand: Optional expansion flag. If true, expands the output - image to make it large enough to hold the entire rotated image. - If false or omitted, make the output image the same size as the - input image. Note that the expand flag assumes rotation around - the center and no translation. - :param center: Optional center of rotation (a 2-tuple). Origin is - the upper left corner. Default is the center of the image. - :param translate: An optional post-rotate translation (a 2-tuple). - :param fillcolor: An optional color for area outside the rotated image. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - angle = angle % 360.0 - - # Fast paths regardless of filter, as long as we're not - # translating or changing the center. - if not (center or translate): - if angle == 0: - return self.copy() - if angle == 180: - return self.transpose(Transpose.ROTATE_180) - if angle in (90, 270) and (expand or self.width == self.height): - return self.transpose( - Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270 - ) - - # Calculate the affine matrix. Note that this is the reverse - # transformation (from destination image to source) because we - # want to interpolate the (discrete) destination pixel from - # the local area around the (floating) source pixel. - - # The matrix we actually want (note that it operates from the right): - # (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx) - # (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy) - # (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1) - - # The reverse matrix is thus: - # (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx) - # (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty) - # (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1) - - # In any case, the final translation may be updated at the end to - # compensate for the expand flag. - - w, h = self.size - - if translate is None: - post_trans = (0, 0) - else: - post_trans = translate - if center is None: - center = (w / 2, h / 2) - - angle = -math.radians(angle) - matrix = [ - round(math.cos(angle), 15), - round(math.sin(angle), 15), - 0.0, - round(-math.sin(angle), 15), - round(math.cos(angle), 15), - 0.0, - ] - - def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]: - a, b, c, d, e, f = matrix - return a * x + b * y + c, d * x + e * y + f - - matrix[2], matrix[5] = transform( - -center[0] - post_trans[0], -center[1] - post_trans[1], matrix - ) - matrix[2] += center[0] - matrix[5] += center[1] - - if expand: - # calculate output size - xx = [] - yy = [] - for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - transformed_x, transformed_y = transform(x, y, matrix) - xx.append(transformed_x) - yy.append(transformed_y) - nw = math.ceil(max(xx)) - math.floor(min(xx)) - nh = math.ceil(max(yy)) - math.floor(min(yy)) - - # We multiply a translation matrix from the right. Because of its - # special form, this is the same as taking the image of the - # translation vector as new translation vector. - matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) - w, h = nw, nh - - return self.transform( - (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor - ) - - def save( - self, fp: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any - ) -> None: - """ - Saves this image under the given filename. If no format is - specified, the format to use is determined from the filename - extension, if possible. - - Keyword options can be used to provide additional instructions - to the writer. If a writer doesn't recognise an option, it is - silently ignored. The available options are described in the - :doc:`image format documentation - <../handbook/image-file-formats>` for each writer. - - You can use a file object instead of a filename. In this case, - you must always specify the format. The file object must - implement the ``seek``, ``tell``, and ``write`` - methods, and be opened in binary mode. - - :param fp: A filename (string), os.PathLike object or file object. - :param format: Optional format override. If omitted, the - format to use is determined from the filename extension. - If a file object was used instead of a filename, this - parameter should always be used. - :param params: Extra parameters to the image writer. These can also be - set on the image itself through ``encoderinfo``. This is useful when - saving multiple images:: - - # Saving XMP data to a single image - from PIL import Image - red = Image.new("RGB", (1, 1), "#f00") - red.save("out.mpo", xmp=b"test") - - # Saving XMP data to the second frame of an image - from PIL import Image - black = Image.new("RGB", (1, 1)) - red = Image.new("RGB", (1, 1), "#f00") - red.encoderinfo = {"xmp": b"test"} - black.save("out.mpo", save_all=True, append_images=[red]) - :returns: None - :exception ValueError: If the output format could not be determined - from the file name. Use the format option to solve this. - :exception OSError: If the file could not be written. The file - may have been created, and may contain partial data. - """ - - filename: str | bytes = "" - open_fp = False - if is_path(fp): - filename = os.fspath(fp) - open_fp = True - elif fp == sys.stdout: - try: - fp = sys.stdout.buffer - except AttributeError: - pass - if not filename and hasattr(fp, "name") and is_path(fp.name): - # only set the name for metadata purposes - filename = os.fspath(fp.name) - - if format: - preinit() - else: - filename_ext = os.path.splitext(filename)[1].lower() - ext = ( - filename_ext.decode() - if isinstance(filename_ext, bytes) - else filename_ext - ) - - # Try importing only the plugin for this extension first - if not _import_plugin_for_extension(ext): - preinit() - - if ext not in EXTENSION: - init() - try: - format = EXTENSION[ext] - except KeyError as e: - msg = f"unknown file extension: {ext}" - raise ValueError(msg) from e - - from . import ImageFile - - # may mutate self! - if isinstance(self, ImageFile.ImageFile) and os.path.abspath( - filename - ) == os.path.abspath(self.filename): - self._ensure_mutable() - else: - self.load() - - save_all = params.pop("save_all", None) - self._default_encoderinfo = params - encoderinfo = getattr(self, "encoderinfo", {}) - self._attach_default_encoderinfo(self) - self.encoderconfig: tuple[Any, ...] = () - - if format.upper() not in SAVE: - init() - if save_all or ( - save_all is None - and params.get("append_images") - and format.upper() in SAVE_ALL - ): - save_handler = SAVE_ALL[format.upper()] - else: - save_handler = SAVE[format.upper()] - - created = False - if open_fp: - created = not os.path.exists(filename) - if params.get("append", False): - # Open also for reading ("+"), because TIFF save_all - # writer needs to go back and edit the written data. - fp = builtins.open(filename, "r+b") - else: - fp = builtins.open(filename, "w+b") - else: - fp = cast(IO[bytes], fp) - - try: - save_handler(self, fp, filename) - except Exception: - if open_fp: - fp.close() - if created: - try: - os.remove(filename) - except PermissionError: - pass - raise - finally: - self.encoderinfo = encoderinfo - if open_fp: - fp.close() - - def _attach_default_encoderinfo(self, im: Image) -> dict[str, Any]: - encoderinfo = getattr(self, "encoderinfo", {}) - self.encoderinfo = {**im._default_encoderinfo, **encoderinfo} - return encoderinfo - - def seek(self, frame: int) -> None: - """ - Seeks to the given frame in this sequence file. If you seek - beyond the end of the sequence, the method raises an - ``EOFError`` exception. When a sequence file is opened, the - library automatically seeks to frame 0. - - See :py:meth:`~PIL.Image.Image.tell`. - - If defined, :attr:`~PIL.Image.Image.n_frames` refers to the - number of available frames. - - :param frame: Frame number, starting at 0. - :exception EOFError: If the call attempts to seek beyond the end - of the sequence. - """ - - # overridden by file handlers - if frame != 0: - msg = "no more images in file" - raise EOFError(msg) - - def show(self, title: str | None = None) -> None: - """ - Displays this image. This method is mainly intended for debugging purposes. - - This method calls :py:func:`PIL.ImageShow.show` internally. You can use - :py:func:`PIL.ImageShow.register` to override its default behaviour. - - The image is first saved to a temporary file. By default, it will be in - PNG format. - - On Unix, the image is then opened using the **xdg-open**, **display**, - **gm**, **eog** or **xv** utility, depending on which one can be found. - - On macOS, the image is opened with the native Preview application. - - On Windows, the image is opened with the standard PNG display utility. - - :param title: Optional title to use for the image window, where possible. - """ - - from . import ImageShow - - ImageShow.show(self, title) - - def split(self) -> tuple[Image, ...]: - """ - Split this image into individual bands. This method returns a - tuple of individual image bands from an image. For example, - splitting an "RGB" image creates three new images each - containing a copy of one of the original bands (red, green, - blue). - - If you need only one band, :py:meth:`~PIL.Image.Image.getchannel` - method can be more convenient and faster. - - :returns: A tuple containing bands. - """ - - self.load() - if self.im.bands == 1: - return (self.copy(),) - return tuple(map(self._new, self.im.split())) - - def getchannel(self, channel: int | str) -> Image: - """ - Returns an image containing a single channel of the source image. - - :param channel: What channel to return. Could be index - (0 for "R" channel of "RGB") or channel name - ("A" for alpha channel of "RGBA"). - :returns: An image in "L" mode. - - .. versionadded:: 4.3.0 - """ - self.load() - - if isinstance(channel, str): - try: - channel = self.getbands().index(channel) - except ValueError as e: - msg = f'The image has no channel "{channel}"' - raise ValueError(msg) from e - - return self._new(self.im.getband(channel)) - - def tell(self) -> int: - """ - Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. - - If defined, :attr:`~PIL.Image.Image.n_frames` refers to the - number of available frames. - - :returns: Frame number, starting with 0. - """ - return 0 - - def thumbnail( - self, - size: tuple[float, float], - resample: Resampling = Resampling.BICUBIC, - reducing_gap: float | None = 2.0, - ) -> None: - """ - Make this image into a thumbnail. This method modifies the - image to contain a thumbnail version of itself, no larger than - the given size. This method calculates an appropriate thumbnail - size to preserve the aspect of the image, calls the - :py:meth:`~PIL.Image.Image.draft` method to configure the file reader - (where applicable), and finally resizes the image. - - Note that this function modifies the :py:class:`~PIL.Image.Image` - object in place. If you need to use the full resolution image as well, - apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original - image. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param resample: Optional resampling filter. This can be one - of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, - :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, - :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. - If omitted, it defaults to :py:data:`Resampling.BICUBIC`. - (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). - See: :ref:`concept-filters`. - :param reducing_gap: Apply optimization by resizing the image - in two steps. First, reducing the image by integer times - using :py:meth:`~PIL.Image.Image.reduce` or - :py:meth:`~PIL.Image.Image.draft` for JPEG images. - Second, resizing using regular resampling. The last step - changes size no less than by ``reducing_gap`` times. - ``reducing_gap`` may be None (no first step is performed) - or should be greater than 1.0. The bigger ``reducing_gap``, - the closer the result to the fair resampling. - The smaller ``reducing_gap``, the faster resizing. - With ``reducing_gap`` greater or equal to 3.0, the result is - indistinguishable from fair resampling in most cases. - The default value is 2.0 (very close to fair resampling - while still being faster in many cases). - :returns: None - """ - - provided_size = tuple(map(math.floor, size)) - - def preserve_aspect_ratio() -> tuple[int, int] | None: - def round_aspect(number: float, key: Callable[[int], float]) -> int: - return max(min(math.floor(number), math.ceil(number), key=key), 1) - - x, y = provided_size - if x >= self.width and y >= self.height: - return None - - aspect = self.width / self.height - if x / y >= aspect: - x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) - else: - y = round_aspect( - x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) - ) - return x, y - - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size - - box = None - if reducing_gap is not None: - res = self.draft( - None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) - ) - if res is not None: - box = res[1] - - if self.size != final_size: - im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) - - self.im = im.im - self._size = final_size - self._mode = self.im.mode - - self.readonly = 0 - - # FIXME: the different transform methods need further explanation - # instead of bloating the method docs, add a separate chapter. - def transform( - self, - size: tuple[int, int], - method: Transform | ImageTransformHandler | SupportsGetData, - data: Sequence[Any] | None = None, - resample: int = Resampling.NEAREST, - fill: int = 1, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: - """ - Transforms this image. This method creates a new image with the - given size, and the same mode as the original, and copies data - to the new image using the given transform. - - :param size: The output size in pixels, as a 2-tuple: - (width, height). - :param method: The transformation method. This is one of - :py:data:`Transform.EXTENT` (cut out a rectangular subregion), - :py:data:`Transform.AFFINE` (affine transform), - :py:data:`Transform.PERSPECTIVE` (perspective transform), - :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or - :py:data:`Transform.MESH` (map a number of source quadrilaterals - in one operation). - - It may also be an :py:class:`~PIL.Image.ImageTransformHandler` - object:: - - class Example(Image.ImageTransformHandler): - def transform(self, size, data, resample, fill=1): - # Return result - - Implementations of :py:class:`~PIL.Image.ImageTransformHandler` - for some of the :py:class:`Transform` methods are provided - in :py:mod:`~PIL.ImageTransform`. - - It may also be an object with a ``method.getdata`` method - that returns a tuple supplying new ``method`` and ``data`` values:: - - class Example: - def getdata(self): - method = Image.Transform.EXTENT - data = (0, 0, 100, 100) - return method, data - :param data: Extra data to the transformation method. - :param resample: Optional resampling filter. It can be one of - :py:data:`Resampling.NEAREST` (use nearest neighbour), - :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`Resampling.BICUBIC` (cubic spline - interpolation in a 4x4 environment). If omitted, or if the image - has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. - See: :ref:`concept-filters`. - :param fill: If ``method`` is an - :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of - the arguments passed to it. Otherwise, it is unused. - :param fillcolor: Optional fill color for the area outside the - transform in the output image. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST: - return ( - self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) - .transform(size, method, data, resample, fill, fillcolor) - .convert(self.mode) - ) - - if isinstance(method, ImageTransformHandler): - return method.transform(size, self, resample=resample, fill=fill) - - if hasattr(method, "getdata"): - # compatibility w. old-style transform objects - method, data = method.getdata() - - if data is None: - msg = "missing method data" - raise ValueError(msg) - - im = new(self.mode, size, fillcolor) - if self.mode == "P" and self.palette: - im.palette = self.palette.copy() - im.info = self.info.copy() - if method == Transform.MESH: - # list of quads - for box, quad in data: - im.__transformer( - box, self, Transform.QUAD, quad, resample, fillcolor is None - ) - else: - im.__transformer( - (0, 0) + size, self, method, data, resample, fillcolor is None - ) - - return im - - def __transformer( - self, - box: tuple[int, int, int, int], - image: Image, - method: Transform, - data: Sequence[float], - resample: int = Resampling.NEAREST, - fill: bool = True, - ) -> None: - w = box[2] - box[0] - h = box[3] - box[1] - - if method == Transform.AFFINE: - data = data[:6] - - elif method == Transform.EXTENT: - # convert extent to an affine transform - x0, y0, x1, y1 = data - xs = (x1 - x0) / w - ys = (y1 - y0) / h - method = Transform.AFFINE - data = (xs, 0, x0, 0, ys, y0) - - elif method == Transform.PERSPECTIVE: - data = data[:8] - - elif method == Transform.QUAD: - # quadrilateral warp. data specifies the four corners - # given as NW, SW, SE, and NE. - nw = data[:2] - sw = data[2:4] - se = data[4:6] - ne = data[6:8] - x0, y0 = nw - As = 1.0 / w - At = 1.0 / h - data = ( - x0, - (ne[0] - x0) * As, - (sw[0] - x0) * At, - (se[0] - sw[0] - ne[0] + x0) * As * At, - y0, - (ne[1] - y0) * As, - (sw[1] - y0) * At, - (se[1] - sw[1] - ne[1] + y0) * As * At, - ) - - else: - msg = "unknown transformation method" - raise ValueError(msg) - - if resample not in ( - Resampling.NEAREST, - Resampling.BILINEAR, - Resampling.BICUBIC, - ): - if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): - unusable: dict[int, str] = { - Resampling.BOX: "Image.Resampling.BOX", - Resampling.HAMMING: "Image.Resampling.HAMMING", - Resampling.LANCZOS: "Image.Resampling.LANCZOS", - } - msg = unusable[resample] + f" ({resample}) cannot be used." - else: - msg = f"Unknown resampling filter ({resample})." - - filters = [ - f"{filter[1]} ({filter[0]})" - for filter in ( - (Resampling.NEAREST, "Image.Resampling.NEAREST"), - (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), - (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), - ) - ] - msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" - raise ValueError(msg) - - image.load() - - self.load() - - if image.mode in ("1", "P"): - resample = Resampling.NEAREST - - self.im.transform(box, image.im, method, data, resample, fill) - - def transpose(self, method: Transpose) -> Image: - """ - Transpose image (flip or rotate in 90 degree steps) - - :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, - :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, - :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, - :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. - :returns: Returns a flipped or rotated copy of this image. - """ - - self.load() - return self._new(self.im.transpose(method)) - - def effect_spread(self, distance: int) -> Image: - """ - Randomly spread pixels in an image. - - :param distance: Distance to spread pixels. - """ - self.load() - return self._new(self.im.effect_spread(distance)) - - def toqimage(self) -> ImageQt.ImageQt: - """Returns a QImage copy of this image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.toqimage(self) - - def toqpixmap(self) -> ImageQt.QPixmap: - """Returns a QPixmap copy of this image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.toqpixmap(self) - - -# -------------------------------------------------------------------- -# Abstract handlers. - - -class ImagePointHandler(abc.ABC): - """ - Used as a mixin by point transforms - (for use with :py:meth:`~PIL.Image.Image.point`) - """ - - @abc.abstractmethod - def point(self, im: Image) -> Image: - pass - - -class ImageTransformHandler(abc.ABC): - """ - Used as a mixin by geometry transforms - (for use with :py:meth:`~PIL.Image.Image.transform`) - """ - - @abc.abstractmethod - def transform( - self, - size: tuple[int, int], - image: Image, - **options: Any, - ) -> Image: - pass - - -# -------------------------------------------------------------------- -# Factories - - -def _check_size(size: Any) -> None: - """ - Common check to enforce type and sanity check on size tuples - - :param size: Should be a 2 tuple of (width, height) - :returns: None, or raises a ValueError - """ - - if not isinstance(size, (list, tuple)): - msg = "Size must be a list or tuple" - raise ValueError(msg) - if len(size) != 2: - msg = "Size must be a sequence of length 2" - raise ValueError(msg) - if size[0] < 0 or size[1] < 0: - msg = "Width and height must be >= 0" - raise ValueError(msg) - - -def new( - mode: str, - size: tuple[int, int] | list[int], - color: float | tuple[float, ...] | str | None = 0, -) -> Image: - """ - Creates a new image with the given mode and size. - - :param mode: The mode to use for the new image. See: - :ref:`concept-modes`. - :param size: A 2-tuple, containing (width, height) in pixels. - :param color: What color to use for the image. Default is black. If given, - this should be a single integer or floating point value for single-band - modes, and a tuple for multi-band modes (one value per band). When - creating RGB or HSV images, you can also use color strings as supported - by the ImageColor module. See :ref:`colors` for more information. If the - color is None, the image is not initialised. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - _check_size(size) - - if color is None: - # don't initialize - return Image()._new(core.new(mode, size)) - - if isinstance(color, str): - # css3-style specifier - - from . import ImageColor - - color = ImageColor.getcolor(color, mode) - - im = Image() - if ( - mode == "P" - and isinstance(color, (list, tuple)) - and all(isinstance(i, int) for i in color) - ): - color_ints: tuple[int, ...] = cast(tuple[int, ...], tuple(color)) - if len(color_ints) == 3 or len(color_ints) == 4: - # RGB or RGBA value for a P image - from . import ImagePalette - - im.palette = ImagePalette.ImagePalette() - color = im.palette.getcolor(color_ints) - return im._new(core.fill(mode, size, color)) - - -def frombytes( - mode: str, - size: tuple[int, int], - data: bytes | bytearray | SupportsArrayInterface, - decoder_name: str = "raw", - *args: Any, -) -> Image: - """ - Creates a copy of an image memory from pixel data in a buffer. - - In its simplest form, this function takes three arguments - (mode, size, and unpacked pixel data). - - You can also use any pixel decoder supported by PIL. For more - information on available decoders, see the section - :ref:`Writing Your Own File Codec <file-codecs>`. - - Note that this function decodes pixel data only, not entire images. - If you have an entire image in a string, wrap it in a - :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load - it. - - :param mode: The image mode. See: :ref:`concept-modes`. - :param size: The image size. - :param data: A byte buffer containing raw data for the given mode. - :param decoder_name: What decoder to use. - :param args: Additional parameters for the given decoder. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - _check_size(size) - - im = new(mode, size) - if im.width != 0 and im.height != 0: - decoder_args: Any = args - if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): - # may pass tuple instead of argument list - decoder_args = decoder_args[0] - - if decoder_name == "raw" and decoder_args == (): - decoder_args = mode - - im.frombytes(data, decoder_name, decoder_args) - return im - - -def frombuffer( - mode: str, - size: tuple[int, int], - data: bytes | SupportsArrayInterface, - decoder_name: str = "raw", - *args: Any, -) -> Image: - """ - Creates an image memory referencing pixel data in a byte buffer. - - This function is similar to :py:func:`~PIL.Image.frombytes`, but uses data - in the byte buffer, where possible. This means that changes to the - original buffer object are reflected in this image). Not all modes can - share memory; supported modes include "L", "RGBX", "RGBA", and "CMYK". - - Note that this function decodes pixel data only, not entire images. - If you have an entire image file in a string, wrap it in a - :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. - - The default parameters used for the "raw" decoder differs from that used for - :py:func:`~PIL.Image.frombytes`. This is a bug, and will probably be fixed in a - future release. The current release issues a warning if you do this; to disable - the warning, you should provide the full set of parameters. See below for details. - - :param mode: The image mode. See: :ref:`concept-modes`. - :param size: The image size. - :param data: A bytes or other buffer object containing raw - data for the given mode. - :param decoder_name: What decoder to use. - :param args: Additional parameters for the given decoder. For the - default encoder ("raw"), it's recommended that you provide the - full set of parameters:: - - frombuffer(mode, size, data, "raw", mode, 0, 1) - - :returns: An :py:class:`~PIL.Image.Image` object. - - .. versionadded:: 1.1.4 - """ - - _check_size(size) - - # may pass tuple instead of argument list - if len(args) == 1 and isinstance(args[0], tuple): - args = args[0] - - if decoder_name == "raw": - if args == (): - args = mode, 0, 1 - if args[0] in _MAPMODES: - im = new(mode, (0, 0)) - im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) - if mode == "P": - from . import ImagePalette - - im.palette = ImagePalette.ImagePalette("RGB", im.im.getpalette("RGB")) - im.readonly = 1 - return im - - return frombytes(mode, size, data, decoder_name, args) - - -class SupportsArrayInterface(Protocol): - """ - An object that has an ``__array_interface__`` dictionary. - """ - - @property - def __array_interface__(self) -> dict[str, Any]: - raise NotImplementedError() - - -class SupportsArrowArrayInterface(Protocol): - """ - An object that has an ``__arrow_c_array__`` method corresponding to the arrow c - data interface. - """ - - def __arrow_c_array__( - self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037 - ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037 - raise NotImplementedError() - - -def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: - """ - Creates an image memory from an object exporting the array interface - (using the buffer protocol):: - - from PIL import Image - import numpy as np - a = np.zeros((5, 5)) - im = Image.fromarray(a) - - If ``obj`` is not contiguous, then the ``tobytes`` method is called - and :py:func:`~PIL.Image.frombuffer` is used. - - In the case of NumPy, be aware that Pillow modes do not always correspond - to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, - 32-bit signed integer pixels, and 32-bit floating point pixels. - - Pillow images can also be converted to arrays:: - - from PIL import Image - import numpy as np - im = Image.open("hopper.jpg") - a = np.asarray(im) - - When converting Pillow images to arrays however, only pixel values are - transferred. This means that P and PA mode images will lose their palette. - - :param obj: Object with array interface - :param mode: Optional mode to use when reading ``obj``. Since pixel values do not - contain information about palettes or color spaces, this can be used to place - grayscale L mode data within a P mode image, or read RGB data as YCbCr for - example. - - See: :ref:`concept-modes` for general information about modes. - :returns: An image object. - - .. versionadded:: 1.1.6 - """ - arr = obj.__array_interface__ - shape = arr["shape"] - ndim = len(shape) - strides = arr.get("strides", None) - try: - typekey = (1, 1) + shape[2:], arr["typestr"] - except KeyError as e: - if mode is not None: - typekey = None - color_modes: list[str] = [] - else: - msg = "Cannot handle this data type" - raise TypeError(msg) from e - if typekey is not None: - try: - typemode, rawmode, color_modes = _fromarray_typemap[typekey] - except KeyError as e: - typekey_shape, typestr = typekey - msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" - raise TypeError(msg) from e - if mode is not None: - if mode != typemode and mode not in color_modes: - deprecate("'mode' parameter for changing data types", 13) - rawmode = mode - else: - mode = typemode - if mode in ["1", "L", "I", "P", "F"]: - ndmax = 2 - elif mode == "RGB": - ndmax = 3 - else: - ndmax = 4 - if ndim > ndmax: - msg = f"Too many dimensions: {ndim} > {ndmax}." - raise ValueError(msg) - - size = 1 if ndim == 1 else shape[1], shape[0] - if strides is not None: - if hasattr(obj, "tobytes"): - obj = obj.tobytes() - elif hasattr(obj, "tostring"): - obj = obj.tostring() - else: - msg = "'strides' requires either tobytes() or tostring()" - raise ValueError(msg) - - return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) - - -def fromarrow( - obj: SupportsArrowArrayInterface, mode: str, size: tuple[int, int] -) -> Image: - """Creates an image with zero-copy shared memory from an object exporting - the arrow_c_array interface protocol:: - - from PIL import Image - import pyarrow as pa - arr = pa.array([0]*(5*5*4), type=pa.uint8()) - im = Image.fromarrow(arr, 'RGBA', (5, 5)) - - If the data representation of the ``obj`` is not compatible with - Pillow internal storage, a ValueError is raised. - - Pillow images can also be converted to Arrow objects:: - - from PIL import Image - import pyarrow as pa - im = Image.open('hopper.jpg') - arr = pa.array(im) - - As with array support, when converting Pillow images to arrays, - only pixel values are transferred. This means that P and PA mode - images will lose their palette. - - :param obj: Object with an arrow_c_array interface - :param mode: Image mode. - :param size: Image size. This must match the storage of the arrow object. - :returns: An Image object - - Note that according to the Arrow spec, both the producer and the - consumer should consider the exported array to be immutable, as - unsynchronized updates will potentially cause inconsistent data. - - See: :ref:`arrow-support` for more detailed information - - .. versionadded:: 11.2.1 - - """ - if not hasattr(obj, "__arrow_c_array__"): - msg = "arrow_c_array interface not found" - raise ValueError(msg) - - schema_capsule, array_capsule = obj.__arrow_c_array__() - _im = core.new_arrow(mode, size, schema_capsule, array_capsule) - if _im: - return Image()._new(_im) - - msg = "new_arrow returned None without an exception" - raise ValueError(msg) - - -def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: - """Creates an image instance from a QImage image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.fromqimage(im) - - -def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: - """Creates an image instance from a QPixmap image""" - from . import ImageQt - - if not ImageQt.qt_is_installed: - msg = "Qt bindings are not installed" - raise ImportError(msg) - return ImageQt.fromqpixmap(im) - - -_fromarray_typemap = { - # (shape, typestr) => mode, rawmode, color modes - # first two members of shape are set to one - ((1, 1), "|b1"): ("1", "1;8", []), - ((1, 1), "|u1"): ("L", "L", ["P"]), - ((1, 1), "|i1"): ("I", "I;8", []), - ((1, 1), "<u2"): ("I", "I;16", []), - ((1, 1), ">u2"): ("I", "I;16B", []), - ((1, 1), "<i2"): ("I", "I;16S", []), - ((1, 1), ">i2"): ("I", "I;16BS", []), - ((1, 1), "<u4"): ("I", "I;32", []), - ((1, 1), ">u4"): ("I", "I;32B", []), - ((1, 1), "<i4"): ("I", "I;32S", []), - ((1, 1), ">i4"): ("I", "I;32BS", []), - ((1, 1), "<f4"): ("F", "F;32F", []), - ((1, 1), ">f4"): ("F", "F;32BF", []), - ((1, 1), "<f8"): ("F", "F;64F", []), - ((1, 1), ">f8"): ("F", "F;64BF", []), - ((1, 1, 2), "|u1"): ("LA", "LA", ["La", "PA"]), - ((1, 1, 3), "|u1"): ("RGB", "RGB", ["YCbCr", "LAB", "HSV"]), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa", "RGBX", "CMYK"]), - # shortcuts: - ((1, 1), f"{_ENDIAN}i4"): ("I", "I", []), - ((1, 1), f"{_ENDIAN}f4"): ("F", "F", []), -} - - -def _decompression_bomb_check(size: tuple[int, int]) -> None: - if MAX_IMAGE_PIXELS is None: - return - - pixels = max(1, size[0]) * max(1, size[1]) - - if pixels > 2 * MAX_IMAGE_PIXELS: - msg = ( - f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " - "pixels, could be decompression bomb DOS attack." - ) - raise DecompressionBombError(msg) - - if pixels > MAX_IMAGE_PIXELS: - warnings.warn( - f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, " - "could be decompression bomb DOS attack.", - DecompressionBombWarning, - ) - - -def open( - fp: StrOrBytesPath | IO[bytes], - mode: Literal["r"] = "r", - formats: list[str] | tuple[str, ...] | None = None, -) -> ImageFile.ImageFile: - """ - Opens and identifies the given image file. - - This is a lazy operation; this function identifies the file, but - the file remains open and the actual image data is not read from - the file until you try to process the data (or call the - :py:meth:`~PIL.Image.Image.load` method). See - :py:func:`~PIL.Image.new`. See :ref:`file-handling`. - - :param fp: A filename (string), os.PathLike object or a file object. - The file object must implement ``file.read``, - ``file.seek``, and ``file.tell`` methods, - and be opened in binary mode. The file object will also seek to zero - before reading. - :param mode: The mode. If given, this argument must be "r". - :param formats: A list or tuple of formats to attempt to load the file in. - This can be used to restrict the set of formats checked. - Pass ``None`` to try all supported formats. You can print the set of - available formats by running ``python3 -m PIL`` or using - the :py:func:`PIL.features.pilinfo` function. - :returns: An :py:class:`~PIL.Image.Image` object. - :exception FileNotFoundError: If the file cannot be found. - :exception PIL.UnidentifiedImageError: If the image cannot be opened and - identified. - :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO`` - instance is used for ``fp``. - :exception TypeError: If ``formats`` is not ``None``, a list or a tuple. - """ - - if mode != "r": - msg = f"bad mode {repr(mode)}" # type: ignore[unreachable] - raise ValueError(msg) - elif isinstance(fp, io.StringIO): - msg = ( # type: ignore[unreachable] - "StringIO cannot be used to open an image. " - "Binary data must be used instead." - ) - raise ValueError(msg) - - if formats is None: - formats = ID - elif not isinstance(formats, (list, tuple)): - msg = "formats must be a list or tuple" # type: ignore[unreachable] - raise TypeError(msg) - - exclusive_fp = False - filename: str | bytes = "" - if is_path(fp): - filename = os.fspath(fp) - fp = builtins.open(filename, "rb") - exclusive_fp = True - else: - fp = cast(IO[bytes], fp) - - try: - fp.seek(0) - except (AttributeError, io.UnsupportedOperation): - fp = io.BytesIO(fp.read()) - exclusive_fp = True - - prefix = fp.read(16) - - # Try to import just the plugin needed for this file extension - # before falling back to preinit() which imports common plugins - ext = os.path.splitext(filename)[1] if filename else "" - if not _import_plugin_for_extension(ext): - preinit() - - warning_messages: list[str] = [] - - def _open_core( - fp: IO[bytes], - filename: str | bytes, - prefix: bytes, - formats: list[str] | tuple[str, ...], - ) -> ImageFile.ImageFile | None: - for i in formats: - i = i.upper() - if i not in OPEN: - init() - try: - factory, accept = OPEN[i] - result = not accept or accept(prefix) - if isinstance(result, str): - warning_messages.append(result) - elif result: - fp.seek(0) - im = factory(fp, filename) - _decompression_bomb_check(im.size) - return im - except (SyntaxError, IndexError, TypeError, struct.error) as e: - if WARN_POSSIBLE_FORMATS: - warning_messages.append(i + " opening failed. " + str(e)) - except BaseException: - if exclusive_fp: - fp.close() - raise - return None - - im = _open_core(fp, filename, prefix, formats) - - if im is None and formats is ID: - # Try preinit (few common plugins) then init (all plugins) - for loader in (preinit, init): - checked_formats = ID.copy() - loader() - if formats != checked_formats: - im = _open_core( - fp, - filename, - prefix, - tuple(f for f in formats if f not in checked_formats), - ) - if im is not None: - break - - if im: - im._exclusive_fp = exclusive_fp - return im - - if exclusive_fp: - fp.close() - for message in warning_messages: - warnings.warn(message) - msg = "cannot identify image file %r" % (filename if filename else fp) - raise UnidentifiedImageError(msg) - - -# -# Image processing. - - -def alpha_composite(im1: Image, im2: Image) -> Image: - """ - Alpha composite im2 over im1. - - :param im1: The first image. Must have mode RGBA or LA. - :param im2: The second image. Must have the same mode and size as the first image. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - im1.load() - im2.load() - return im1._new(core.alpha_composite(im1.im, im2.im)) - - -def blend(im1: Image, im2: Image, alpha: float) -> Image: - """ - Creates a new image by interpolating between two input images, using - a constant alpha:: - - out = image1 * (1.0 - alpha) + image2 * alpha - - :param im1: The first image. - :param im2: The second image. Must have the same mode and size as - the first image. - :param alpha: The interpolation alpha factor. If alpha is 0.0, a - copy of the first image is returned. If alpha is 1.0, a copy of - the second image is returned. There are no restrictions on the - alpha value. If necessary, the result is clipped to fit into - the allowed output range. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - im1.load() - im2.load() - return im1._new(core.blend(im1.im, im2.im, alpha)) - - -def composite(image1: Image, image2: Image, mask: Image) -> Image: - """ - Create composite image by blending images using a transparency mask. - - :param image1: The first image. - :param image2: The second image. Must have the same mode and - size as the first image. - :param mask: A mask image. This image can have mode - "1", "L", or "RGBA", and must have the same size as the - other two images. - """ - - image = image2.copy() - image.paste(image1, None, mask) - return image - - -def eval(image: Image, *args: Callable[[int], float]) -> Image: - """ - Applies the function (which should take one argument) to each pixel - in the given image. If the image has more than one band, the same - function is applied to each band. Note that the function is - evaluated once for each possible pixel value, so you cannot use - random components or other generators. - - :param image: The input image. - :param function: A function object, taking one integer argument. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - return image.point(args[0]) - - -def merge(mode: str, bands: Sequence[Image]) -> Image: - """ - Merge a set of single band images into a new multiband image. - - :param mode: The mode to use for the output image. See: - :ref:`concept-modes`. - :param bands: A sequence containing one single-band image for - each band in the output image. All bands must have the - same size. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - - if getmodebands(mode) != len(bands) or "*" in mode: - msg = "wrong number of bands" - raise ValueError(msg) - for band in bands[1:]: - if band.mode != getmodetype(mode): - msg = "mode mismatch" - raise ValueError(msg) - if band.size != bands[0].size: - msg = "size mismatch" - raise ValueError(msg) - for band in bands: - band.load() - return bands[0]._new(core.merge(mode, *[b.im for b in bands])) - - -# -------------------------------------------------------------------- -# Plugin registry - - -def register_open( - id: str, - factory: ( - Callable[[IO[bytes], str | bytes], ImageFile.ImageFile] - | type[ImageFile.ImageFile] - ), - accept: Callable[[bytes], bool | str] | None = None, -) -> None: - """ - Register an image file plugin. This function should not be used - in application code. - - :param id: An image format identifier. - :param factory: An image file factory method. - :param accept: An optional function that can be used to quickly - reject images having another format. - """ - id = id.upper() - if id not in ID: - ID.append(id) - OPEN[id] = factory, accept - - -def register_mime(id: str, mimetype: str) -> None: - """ - Registers an image MIME type by populating ``Image.MIME``. This function - should not be used in application code. - - ``Image.MIME`` provides a mapping from image format identifiers to mime - formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can - provide a different result for specific images. - - :param id: An image format identifier. - :param mimetype: The image MIME type for this format. - """ - MIME[id.upper()] = mimetype - - -def register_save( - id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] -) -> None: - """ - Registers an image save function. This function should not be - used in application code. - - :param id: An image format identifier. - :param driver: A function to save images in this format. - """ - SAVE[id.upper()] = driver - - -def register_save_all( - id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] -) -> None: - """ - Registers an image function to save all the frames - of a multiframe format. This function should not be - used in application code. - - :param id: An image format identifier. - :param driver: A function to save images in this format. - """ - SAVE_ALL[id.upper()] = driver - - -def register_extension(id: str, extension: str) -> None: - """ - Registers an image extension. This function should not be - used in application code. - - :param id: An image format identifier. - :param extension: An extension used for this format. - """ - EXTENSION[extension.lower()] = id.upper() - - -def register_extensions(id: str, extensions: list[str]) -> None: - """ - Registers image extensions. This function should not be - used in application code. - - :param id: An image format identifier. - :param extensions: A list of extensions used for this format. - """ - for extension in extensions: - register_extension(id, extension) - - -def registered_extensions() -> dict[str, str]: - """ - Returns a dictionary containing all file extensions belonging - to registered plugins - """ - init() - return EXTENSION - - -def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: - """ - Registers an image decoder. This function should not be - used in application code. - - :param name: The name of the decoder - :param decoder: An ImageFile.PyDecoder object - - .. versionadded:: 4.1.0 - """ - DECODERS[name] = decoder - - -def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: - """ - Registers an image encoder. This function should not be - used in application code. - - :param name: The name of the encoder - :param encoder: An ImageFile.PyEncoder object - - .. versionadded:: 4.1.0 - """ - ENCODERS[name] = encoder - - -# -------------------------------------------------------------------- -# Simple display support. - - -def _show(image: Image, **options: Any) -> None: - from . import ImageShow - - deprecate("Image._show", 13, "ImageShow.show") - ImageShow.show(image, **options) - - -# -------------------------------------------------------------------- -# Effects - - -def effect_mandelbrot( - size: tuple[int, int], extent: tuple[float, float, float, float], quality: int -) -> Image: - """ - Generate a Mandelbrot set covering the given extent. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param extent: The extent to cover, as a 4-tuple: - (x0, y0, x1, y1). - :param quality: Quality. - """ - return Image()._new(core.effect_mandelbrot(size, extent, quality)) - - -def effect_noise(size: tuple[int, int], sigma: float) -> Image: - """ - Generate Gaussian noise centered around 128. - - :param size: The requested size in pixels, as a 2-tuple: - (width, height). - :param sigma: Standard deviation of noise. - """ - return Image()._new(core.effect_noise(size, sigma)) - - -def linear_gradient(mode: str) -> Image: - """ - Generate 256x256 linear gradient from black to white, top to bottom. - - :param mode: Input mode. - """ - return Image()._new(core.linear_gradient(mode)) - - -def radial_gradient(mode: str) -> Image: - """ - Generate 256x256 radial gradient from black to white, centre to edge. - - :param mode: Input mode. - """ - return Image()._new(core.radial_gradient(mode)) - - -# -------------------------------------------------------------------- -# Resources - - -def _apply_env_variables(env: dict[str, str] | None = None) -> None: - env_dict = env if env is not None else os.environ - - for var_name, setter in [ - ("PILLOW_ALIGNMENT", core.set_alignment), - ("PILLOW_BLOCK_SIZE", core.set_block_size), - ("PILLOW_BLOCKS_MAX", core.set_blocks_max), - ]: - if var_name not in env_dict: - continue - - var = env_dict[var_name].lower() - - units = 1 - for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: - if var.endswith(postfix): - units = mul - var = var[: -len(postfix)] - - try: - var_int = int(var) * units - except ValueError: - warnings.warn(f"{var_name} is not int") - continue - - try: - setter(var_int) - except ValueError as e: - warnings.warn(f"{var_name}: {e}") - - -_apply_env_variables() -atexit.register(core.clear_cache) - - -if TYPE_CHECKING: - _ExifBase = MutableMapping[int, Any] -else: - _ExifBase = MutableMapping - - -class Exif(_ExifBase): - """ - This class provides read and write access to EXIF image data:: - - from PIL import Image - im = Image.open("exif.png") - exif = im.getexif() # Returns an instance of this class - - Information can be read and written, iterated over or deleted:: - - print(exif[274]) # 1 - exif[274] = 2 - for k, v in exif.items(): - print("Tag", k, "Value", v) # Tag 274 Value 2 - del exif[274] - - To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` - returns a dictionary:: - - from PIL import ExifTags - im = Image.open("exif_gps.jpg") - exif = im.getexif() - gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) - print(gps_ifd) - - Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``, - ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. - - :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: - - print(exif[ExifTags.Base.Software]) # PIL - print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 - """ - - endian: str | None = None - bigtiff = False - _loaded = False - - def __init__(self) -> None: - self._data: dict[int, Any] = {} - self._hidden_data: dict[int, Any] = {} - self._ifds: dict[int, dict[int, Any]] = {} - self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None - self._loaded_exif: bytes | None = None - - def _fixup(self, value: Any) -> Any: - try: - if len(value) == 1 and isinstance(value, tuple): - return value[0] - except Exception: - pass - return value - - def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]: - # Helper function - # returns a dict with any single item tuples/lists as individual values - return {k: self._fixup(v) for k, v in src_dict.items()} - - def _get_ifd_dict( - self, offset: int, group: int | None = None - ) -> dict[int, Any] | None: - try: - # an offset pointer to the location of the nested embedded IFD. - # It should be a long, but may be corrupted. - self.fp.seek(offset) - except (KeyError, TypeError): - return None - else: - from . import TiffImagePlugin - - info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) - info.load(self.fp) - return self._fixup_dict(dict(info)) - - def _get_head(self) -> bytes: - version = b"\x2b" if self.bigtiff else b"\x2a" - if self.endian == "<": - head = b"II" + version + b"\x00" + o32le(8) - else: - head = b"MM\x00" + version + o32be(8) - if self.bigtiff: - head += o32le(8) if self.endian == "<" else o32be(8) - head += b"\x00\x00\x00\x00" - return head - - def load(self, data: bytes) -> None: - # Extract EXIF information. This is highly experimental, - # and is likely to be replaced with something better in a future - # version. - - # The EXIF record consists of a TIFF file embedded in a JPEG - # application marker (!). - if data == self._loaded_exif: - return - self._loaded_exif = data - self._data.clear() - self._hidden_data.clear() - self._ifds.clear() - while data and data.startswith(b"Exif\x00\x00"): - data = data[6:] - if not data: - self._info = None - return - - self.fp: IO[bytes] = io.BytesIO(data) - self.head = self.fp.read(8) - # process dictionary - from . import TiffImagePlugin - - self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) - self.endian = self._info._endian - self.fp.seek(self._info.next) - self._info.load(self.fp) - - def load_from_fp(self, fp: IO[bytes], offset: int | None = None) -> None: - self._loaded_exif = None - self._data.clear() - self._hidden_data.clear() - self._ifds.clear() - - # process dictionary - from . import TiffImagePlugin - - self.fp = fp - if offset is not None: - self.head = self._get_head() - else: - self.head = self.fp.read(8) - self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) - if self.endian is None: - self.endian = self._info._endian - if offset is None: - offset = self._info.next - self.fp.tell() - self.fp.seek(offset) - self._info.load(self.fp) - - def _get_merged_dict(self) -> dict[int, Any]: - merged_dict = dict(self) - - # get EXIF extension - if ExifTags.IFD.Exif in self: - ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif], ExifTags.IFD.Exif) - if ifd: - merged_dict.update(ifd) - - # GPS - if ExifTags.IFD.GPSInfo in self: - merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( - self[ExifTags.IFD.GPSInfo], ExifTags.IFD.GPSInfo - ) - - return merged_dict - - def tobytes(self, offset: int = 8) -> bytes: - from . import TiffImagePlugin - - head = self._get_head() - ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) - for tag, ifd_dict in self._ifds.items(): - if tag not in self: - ifd[tag] = ifd_dict - for tag, value in self.items(): - if tag in [ - ExifTags.IFD.Exif, - ExifTags.IFD.GPSInfo, - ] and not isinstance(value, dict): - value = self.get_ifd(tag) - if ( - tag == ExifTags.IFD.Exif - and ExifTags.IFD.Interop in value - and not isinstance(value[ExifTags.IFD.Interop], dict) - ): - value = value.copy() - value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) - ifd[tag] = value - return b"Exif\x00\x00" + head + ifd.tobytes(offset) - - def get_ifd(self, tag: int) -> dict[int, Any]: - if tag not in self._ifds: - if tag == ExifTags.IFD.IFD1: - if self._info is not None and self._info.next != 0: - ifd = self._get_ifd_dict(self._info.next) - if ifd is not None: - self._ifds[tag] = ifd - elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: - offset = self._hidden_data.get(tag, self.get(tag)) - if offset is not None: - ifd = self._get_ifd_dict(offset, tag) - if ifd is not None: - self._ifds[tag] = ifd - elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]: - if ExifTags.IFD.Exif not in self._ifds: - self.get_ifd(ExifTags.IFD.Exif) - tag_data = self._ifds[ExifTags.IFD.Exif][tag] - if tag == ExifTags.IFD.MakerNote: - from .TiffImagePlugin import ImageFileDirectory_v2 - - try: - if tag_data.startswith(b"FUJIFILM"): - ifd_offset = i32le(tag_data, 8) - ifd_data = tag_data[ifd_offset:] - - makernote = {} - for i in range(struct.unpack("<H", ifd_data[:2])[0]): - ifd_tag, typ, count, data = struct.unpack( - "<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2] - ) - try: - ( - unit_size, - handler, - ) = ImageFileDirectory_v2._load_dispatch[typ] - except KeyError: - continue - size = count * unit_size - if size > 4: - (offset,) = struct.unpack("<L", data) - data = ifd_data[offset - 12 : offset + size - 12] - else: - data = data[:size] - - if len(data) != size: - warnings.warn( - "Possibly corrupt EXIF MakerNote data. " - f"Expecting to read {size} bytes but only got " - f"{len(data)}. Skipping tag {ifd_tag}" - ) - continue - - if not data: - continue - - makernote[ifd_tag] = handler( - ImageFileDirectory_v2(), data, False - ) - self._ifds[tag] = dict(self._fixup_dict(makernote)) - elif self.get(0x010F) == "Nintendo": - makernote = {} - for i in range(struct.unpack(">H", tag_data[:2])[0]): - ifd_tag, typ, count, data = struct.unpack( - ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] - ) - if ifd_tag == 0x1101: - # CameraInfo - (offset,) = struct.unpack(">L", data) - self.fp.seek(offset) - - camerainfo: dict[str, int | bytes] = { - "ModelID": self.fp.read(4) - } - - self.fp.read(4) - # Seconds since 2000 - camerainfo["TimeStamp"] = i32le(self.fp.read(12)) - - self.fp.read(4) - camerainfo["InternalSerialNumber"] = self.fp.read(4) - - self.fp.read(12) - parallax = self.fp.read(4) - handler = ImageFileDirectory_v2._load_dispatch[ - TiffTags.FLOAT - ][1] - camerainfo["Parallax"] = handler( - ImageFileDirectory_v2(), parallax, False - )[0] - - self.fp.read(4) - camerainfo["Category"] = self.fp.read(2) - - makernote = {0x1101: camerainfo} - self._ifds[tag] = makernote - except struct.error: - pass - else: - # Interop - ifd = self._get_ifd_dict(tag_data, tag) - if ifd is not None: - self._ifds[tag] = ifd - ifd = self._ifds.setdefault(tag, {}) - if tag == ExifTags.IFD.Exif and self._hidden_data: - ifd = { - k: v - for (k, v) in ifd.items() - if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) - } - return ifd - - def hide_offsets(self) -> None: - for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): - if tag in self: - self._hidden_data[tag] = self[tag] - del self[tag] - - def __str__(self) -> str: - if self._info is not None: - # Load all keys into self._data - for tag in self._info: - self[tag] - - return str(self._data) - - def __len__(self) -> int: - keys = set(self._data) - if self._info is not None: - keys.update(self._info) - return len(keys) - - def __getitem__(self, tag: int) -> Any: - if self._info is not None and tag not in self._data and tag in self._info: - self._data[tag] = self._fixup(self._info[tag]) - del self._info[tag] - return self._data[tag] - - def __contains__(self, tag: object) -> bool: - return tag in self._data or (self._info is not None and tag in self._info) - - def __setitem__(self, tag: int, value: Any) -> None: - if self._info is not None and tag in self._info: - del self._info[tag] - self._data[tag] = value - - def __delitem__(self, tag: int) -> None: - if self._info is not None and tag in self._info: - del self._info[tag] - else: - del self._data[tag] - if tag in self._ifds: - del self._ifds[tag] - - def __iter__(self) -> Iterator[int]: - keys = set(self._data) - if self._info is not None: - keys.update(self._info) - return iter(keys) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageChops.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageChops.py deleted file mode 100644 index 29a5c99..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageChops.py +++ /dev/null @@ -1,311 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard channel operations -# -# History: -# 1996-03-24 fl Created -# 1996-08-13 fl Added logical operations (for "1" images) -# 2000-10-12 fl Added offset method (from Image.py) -# -# Copyright (c) 1997-2000 by Secret Labs AB -# Copyright (c) 1996-2000 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from __future__ import annotations - -from . import Image - - -def constant(image: Image.Image, value: int) -> Image.Image: - """Fill a channel with a given gray level. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return Image.new("L", image.size, value) - - -def duplicate(image: Image.Image) -> Image.Image: - """Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return image.copy() - - -def invert(image: Image.Image) -> Image.Image: - """ - Invert an image (channel). :: - - out = MAX - image - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image.load() - return image._new(image.im.chop_invert()) - - -def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. :: - - out = max(image1, image2) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_lighter(image2.im)) - - -def darker(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Compares the two images, pixel by pixel, and returns a new image containing - the darker values. :: - - out = min(image1, image2) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_darker(image2.im)) - - -def difference(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Returns the absolute value of the pixel-by-pixel difference between the two - images. :: - - out = abs(image1 - image2) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_difference(image2.im)) - - -def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other. - - If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. :: - - out = image1 * image2 / MAX - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_multiply(image2.im)) - - -def screen(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two inverted images on top of each other. :: - - out = MAX - ((MAX - image1) * (MAX - image2) / MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_screen(image2.im)) - - -def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other using the Soft Light algorithm - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_soft_light(image2.im)) - - -def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other using the Hard Light algorithm - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_hard_light(image2.im)) - - -def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image: - """ - Superimposes two images on top of each other using the Overlay algorithm - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_overlay(image2.im)) - - -def add( - image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 -) -> Image.Image: - """ - Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: - - out = ((image1 + image2) / scale + offset) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_add(image2.im, scale, offset)) - - -def subtract( - image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0 -) -> Image.Image: - """ - Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. :: - - out = ((image1 - image2) / scale + offset) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_subtract(image2.im, scale, offset)) - - -def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Add two images, without clipping the result. :: - - out = ((image1 + image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_add_modulo(image2.im)) - - -def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Subtract two images, without clipping the result. :: - - out = ((image1 - image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_subtract_modulo(image2.im)) - - -def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Logical AND between two images. - - Both of the images must have mode "1". If you would like to perform a - logical AND on an image with a mode other than "1", try - :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. :: - - out = ((image1 and image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_and(image2.im)) - - -def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Logical OR between two images. - - Both of the images must have mode "1". :: - - out = ((image1 or image2) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_or(image2.im)) - - -def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image: - """Logical XOR between two images. - - Both of the images must have mode "1". :: - - out = ((bool(image1) != bool(image2)) % MAX) - - :rtype: :py:class:`~PIL.Image.Image` - """ - - image1.load() - image2.load() - return image1._new(image1.im.chop_xor(image2.im)) - - -def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image: - """Blend images using constant transparency weight. Alias for - :py:func:`PIL.Image.blend`. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return Image.blend(image1, image2, alpha) - - -def composite( - image1: Image.Image, image2: Image.Image, mask: Image.Image -) -> Image.Image: - """Create composite using transparency mask. Alias for - :py:func:`PIL.Image.composite`. - - :rtype: :py:class:`~PIL.Image.Image` - """ - - return Image.composite(image1, image2, mask) - - -def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image: - """Returns a copy of the image where data has been offset by the given - distances. Data wraps around the edges. If ``yoffset`` is omitted, it - is assumed to be equal to ``xoffset``. - - :param image: Input image. - :param xoffset: The horizontal distance. - :param yoffset: The vertical distance. If omitted, both - distances are set to the same value. - :rtype: :py:class:`~PIL.Image.Image` - """ - - if yoffset is None: - yoffset = xoffset - image.load() - return image._new(image.im.offset(xoffset, yoffset)) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageCms.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageCms.py deleted file mode 100644 index 513e28a..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageCms.py +++ /dev/null @@ -1,1076 +0,0 @@ -# The Python Imaging Library. -# $Id$ - -# Optional color management support, based on Kevin Cazabon's PyCMS -# library. - -# Originally released under LGPL. Graciously donated to PIL in -# March 2009, for distribution under the standard PIL license - -# History: - -# 2009-03-08 fl Added to PIL. - -# Copyright (C) 2002-2003 Kevin Cazabon -# Copyright (c) 2009 by Fredrik Lundh -# Copyright (c) 2013 by Eric Soroos - -# See the README file for information on usage and redistribution. See -# below for the original description. -from __future__ import annotations - -import operator -import sys -from enum import IntEnum, IntFlag -from functools import reduce -from typing import Any, Literal, SupportsFloat, SupportsInt, Union - -from . import Image -from ._deprecate import deprecate -from ._typing import SupportsRead - -try: - from . import _imagingcms as core - - _CmsProfileCompatible = Union[ - str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile" - ] -except ImportError as ex: - # Allow error import for doc purposes, but error out when accessing - # anything in core. - from ._util import DeferredError - - core = DeferredError.new(ex) - -_DESCRIPTION = """ -pyCMS - - a Python / PIL interface to the littleCMS ICC Color Management System - Copyright (C) 2002-2003 Kevin Cazabon - kevin@cazabon.com - https://www.cazabon.com - - pyCMS home page: https://www.cazabon.com/pyCMS - littleCMS home page: https://www.littlecms.com - (littleCMS is Copyright (C) 1998-2001 Marti Maria) - - Originally released under LGPL. Graciously donated to PIL in - March 2009, for distribution under the standard PIL license - - The pyCMS.py module provides a "clean" interface between Python/PIL and - pyCMSdll, taking care of some of the more complex handling of the direct - pyCMSdll functions, as well as error-checking and making sure that all - relevant data is kept together. - - While it is possible to call pyCMSdll functions directly, it's not highly - recommended. - - Version History: - - 1.0.0 pil Oct 2013 Port to LCMS 2. - - 0.1.0 pil mod March 10, 2009 - - Renamed display profile to proof profile. The proof - profile is the profile of the device that is being - simulated, not the profile of the device which is - actually used to display/print the final simulation - (that'd be the output profile) - also see LCMSAPI.txt - input colorspace -> using 'renderingIntent' -> proof - colorspace -> using 'proofRenderingIntent' -> output - colorspace - - Added LCMS FLAGS support. - Added FLAGS["SOFTPROOFING"] as default flag for - buildProofTransform (otherwise the proof profile/intent - would be ignored). - - 0.1.0 pil March 2009 - added to PIL, as PIL.ImageCms - - 0.0.2 alpha Jan 6, 2002 - - Added try/except statements around type() checks of - potential CObjects... Python won't let you use type() - on them, and raises a TypeError (stupid, if you ask - me!) - - Added buildProofTransformFromOpenProfiles() function. - Additional fixes in DLL, see DLL code for details. - - 0.0.1 alpha first public release, Dec. 26, 2002 - - Known to-do list with current version (of Python interface, not pyCMSdll): - - none - -""" - -_VERSION = "1.0.0 pil" - - -# --------------------------------------------------------------------. - - -# -# intent/direction values - - -class Intent(IntEnum): - PERCEPTUAL = 0 - RELATIVE_COLORIMETRIC = 1 - SATURATION = 2 - ABSOLUTE_COLORIMETRIC = 3 - - -class Direction(IntEnum): - INPUT = 0 - OUTPUT = 1 - PROOF = 2 - - -# -# flags - - -class Flags(IntFlag): - """Flags and documentation are taken from ``lcms2.h``.""" - - NONE = 0 - NOCACHE = 0x0040 - """Inhibit 1-pixel cache""" - NOOPTIMIZE = 0x0100 - """Inhibit optimizations""" - NULLTRANSFORM = 0x0200 - """Don't transform anyway""" - GAMUTCHECK = 0x1000 - """Out of Gamut alarm""" - SOFTPROOFING = 0x4000 - """Do softproofing""" - BLACKPOINTCOMPENSATION = 0x2000 - NOWHITEONWHITEFIXUP = 0x0004 - """Don't fix scum dot""" - HIGHRESPRECALC = 0x0400 - """Use more memory to give better accuracy""" - LOWRESPRECALC = 0x0800 - """Use less memory to minimize resources""" - # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: - USE_8BITS_DEVICELINK = 0x0008 - """Create 8 bits devicelinks""" - GUESSDEVICECLASS = 0x0020 - """Guess device class (for ``transform2devicelink``)""" - KEEP_SEQUENCE = 0x0080 - """Keep profile sequence for devicelink creation""" - FORCE_CLUT = 0x0002 - """Force CLUT optimization""" - CLUT_POST_LINEARIZATION = 0x0001 - """create postlinearization tables if possible""" - CLUT_PRE_LINEARIZATION = 0x0010 - """create prelinearization tables if possible""" - NONEGATIVES = 0x8000 - """Prevent negative numbers in floating point transforms""" - COPY_ALPHA = 0x04000000 - """Alpha channels are copied on ``cmsDoTransform()``""" - NODEFAULTRESOURCEDEF = 0x01000000 - - _GRIDPOINTS_1 = 1 << 16 - _GRIDPOINTS_2 = 2 << 16 - _GRIDPOINTS_4 = 4 << 16 - _GRIDPOINTS_8 = 8 << 16 - _GRIDPOINTS_16 = 16 << 16 - _GRIDPOINTS_32 = 32 << 16 - _GRIDPOINTS_64 = 64 << 16 - _GRIDPOINTS_128 = 128 << 16 - - @staticmethod - def GRIDPOINTS(n: int) -> Flags: - """ - Fine-tune control over number of gridpoints - - :param n: :py:class:`int` in range ``0 <= n <= 255`` - """ - return Flags.NONE | ((n & 0xFF) << 16) - - -_MAX_FLAG = reduce(operator.or_, Flags) - - -_FLAGS = { - "MATRIXINPUT": 1, - "MATRIXOUTPUT": 2, - "MATRIXONLY": (1 | 2), - "NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot - # Don't create prelinearization tables on precalculated transforms - # (internal use): - "NOPRELINEARIZATION": 16, - "GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink) - "NOTCACHE": 64, # Inhibit 1-pixel cache - "NOTPRECALC": 256, - "NULLTRANSFORM": 512, # Don't transform anyway - "HIGHRESPRECALC": 1024, # Use more memory to give better accuracy - "LOWRESPRECALC": 2048, # Use less memory to minimize resources - "WHITEBLACKCOMPENSATION": 8192, - "BLACKPOINTCOMPENSATION": 8192, - "GAMUTCHECK": 4096, # Out of Gamut alarm - "SOFTPROOFING": 16384, # Do softproofing - "PRESERVEBLACK": 32768, # Black preservation - "NODEFAULTRESOURCEDEF": 16777216, # CRD special - "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints -} - - -# --------------------------------------------------------------------. -# Experimental PIL-level API -# --------------------------------------------------------------------. - -## -# Profile. - - -class ImageCmsProfile: - def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None: - """ - :param profile: Either a string representing a filename, - a file like object containing a profile or a - low-level profile object - - """ - self.filename: str | None = None - - if isinstance(profile, str): - if sys.platform == "win32": - profile_bytes_path = profile.encode() - try: - profile_bytes_path.decode("ascii") - except UnicodeDecodeError: - with open(profile, "rb") as f: - self.profile = core.profile_frombytes(f.read()) - return - self.filename = profile - self.profile = core.profile_open(profile) - elif hasattr(profile, "read"): - self.profile = core.profile_frombytes(profile.read()) - elif isinstance(profile, core.CmsProfile): - self.profile = profile - else: - msg = "Invalid type for Profile" # type: ignore[unreachable] - raise TypeError(msg) - - def __getattr__(self, name: str) -> Any: - if name in ("product_name", "product_info"): - deprecate(f"ImageCms.ImageCmsProfile.{name}", 13) - return None - msg = f"'{self.__class__.__name__}' object has no attribute '{name}'" - raise AttributeError(msg) - - def tobytes(self) -> bytes: - """ - Returns the profile in a format suitable for embedding in - saved images. - - :returns: a bytes object containing the ICC profile. - """ - - return core.profile_tobytes(self.profile) - - -class ImageCmsTransform(Image.ImagePointHandler): - """ - Transform. This can be used with the procedural API, or with the standard - :py:func:`~PIL.Image.Image.point` method. - - Will return the output profile in the ``output.info['icc_profile']``. - """ - - def __init__( - self, - input: ImageCmsProfile, - output: ImageCmsProfile, - input_mode: str, - output_mode: str, - intent: Intent = Intent.PERCEPTUAL, - proof: ImageCmsProfile | None = None, - proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, - flags: Flags = Flags.NONE, - ): - if proof is None: - self.transform = core.buildTransform( - input.profile, output.profile, input_mode, output_mode, intent, flags - ) - else: - self.transform = core.buildProofTransform( - input.profile, - output.profile, - proof.profile, - input_mode, - output_mode, - intent, - proof_intent, - flags, - ) - # Note: inputMode and outputMode are for pyCMS compatibility only - self.input_mode = self.inputMode = input_mode - self.output_mode = self.outputMode = output_mode - - self.output_profile = output - - def point(self, im: Image.Image) -> Image.Image: - return self.apply(im) - - def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: - if imOut is None: - imOut = Image.new(self.output_mode, im.size, None) - self.transform.apply(im.getim(), imOut.getim()) - imOut.info["icc_profile"] = self.output_profile.tobytes() - return imOut - - def apply_in_place(self, im: Image.Image) -> Image.Image: - if im.mode != self.output_mode: - msg = "mode mismatch" - raise ValueError(msg) # wrong output mode - self.transform.apply(im.getim(), im.getim()) - im.info["icc_profile"] = self.output_profile.tobytes() - return im - - -def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | None: - """ - (experimental) Fetches the profile for the current display device. - - :returns: ``None`` if the profile is not known. - """ - - if sys.platform != "win32": - return None - - from . import ImageWin # type: ignore[unused-ignore, unreachable] - - if isinstance(handle, ImageWin.HDC): - profile = core.get_display_profile_win32(int(handle), 1) - else: - profile = core.get_display_profile_win32(int(handle or 0)) - if profile is None: - return None - return ImageCmsProfile(profile) - - -# --------------------------------------------------------------------. -# pyCMS compatible layer -# --------------------------------------------------------------------. - - -class PyCMSError(Exception): - """(pyCMS) Exception class. - This is used for all errors in the pyCMS API.""" - - pass - - -def profileToProfile( - im: Image.Image, - inputProfile: _CmsProfileCompatible, - outputProfile: _CmsProfileCompatible, - renderingIntent: Intent = Intent.PERCEPTUAL, - outputMode: str | None = None, - inPlace: bool = False, - flags: Flags = Flags.NONE, -) -> Image.Image | None: - """ - (pyCMS) Applies an ICC transformation to a given image, mapping from - ``inputProfile`` to ``outputProfile``. - - If the input or output profiles specified are not valid filenames, a - :exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and - ``outputMode != im.mode``, a :exc:`PyCMSError` will be raised. - If an error occurs during application of the profiles, - a :exc:`PyCMSError` will be raised. - If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS), - a :exc:`PyCMSError` will be raised. - - This function applies an ICC transformation to im from ``inputProfile``'s - color space to ``outputProfile``'s color space using the specified rendering - intent to decide how to handle out-of-gamut colors. - - ``outputMode`` can be used to specify that a color mode conversion is to - be done using these profiles, but the specified profiles must be able - to handle that mode. I.e., if converting im from RGB to CMYK using - profiles, the input profile must handle RGB data, and the output - profile must handle CMYK data. - - :param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...) - or Image.open(...), etc.) - :param inputProfile: String, as a valid filename path to the ICC input - profile you wish to use for this image, or a profile object - :param outputProfile: String, as a valid filename path to the ICC output - profile you wish to use for this image, or a profile object - :param renderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for the transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param outputMode: A valid PIL mode for the output image (i.e. "RGB", - "CMYK", etc.). Note: if rendering the image "inPlace", outputMode - MUST be the same mode as the input, or omitted completely. If - omitted, the outputMode will be the same as the mode of the input - image (im.mode) - :param inPlace: Boolean. If ``True``, the original image is modified in-place, - and ``None`` is returned. If ``False`` (default), a new - :py:class:`~PIL.Image.Image` object is returned with the transform applied. - :param flags: Integer (0-...) specifying additional flags - :returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on - the value of ``inPlace`` - :exception PyCMSError: - """ - - if outputMode is None: - outputMode = im.mode - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - msg = "renderingIntent must be an integer between 0 and 3" - raise PyCMSError(msg) - - if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = f"flags must be an integer between 0 and {_MAX_FLAG}" - raise PyCMSError(msg) - - try: - if not isinstance(inputProfile, ImageCmsProfile): - inputProfile = ImageCmsProfile(inputProfile) - if not isinstance(outputProfile, ImageCmsProfile): - outputProfile = ImageCmsProfile(outputProfile) - transform = ImageCmsTransform( - inputProfile, - outputProfile, - im.mode, - outputMode, - renderingIntent, - flags=flags, - ) - if inPlace: - transform.apply_in_place(im) - imOut = None - else: - imOut = transform.apply(im) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - return imOut - - -def getOpenProfile( - profileFilename: str | SupportsRead[bytes] | core.CmsProfile, -) -> ImageCmsProfile: - """ - (pyCMS) Opens an ICC profile file. - - The PyCMSProfile object can be passed back into pyCMS for use in creating - transforms and such (as in ImageCms.buildTransformFromOpenProfiles()). - - If ``profileFilename`` is not a valid filename for an ICC profile, - a :exc:`PyCMSError` will be raised. - - :param profileFilename: String, as a valid filename path to the ICC profile - you wish to open, or a file-like object. - :returns: A CmsProfile class object. - :exception PyCMSError: - """ - - try: - return ImageCmsProfile(profileFilename) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def buildTransform( - inputProfile: _CmsProfileCompatible, - outputProfile: _CmsProfileCompatible, - inMode: str, - outMode: str, - renderingIntent: Intent = Intent.PERCEPTUAL, - flags: Flags = Flags.NONE, -) -> ImageCmsTransform: - """ - (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the - ``outputProfile``. Use applyTransform to apply the transform to a given - image. - - If the input or output profiles specified are not valid filenames, a - :exc:`PyCMSError` will be raised. If an error occurs during creation - of the transform, a :exc:`PyCMSError` will be raised. - - If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` - (or by pyCMS), a :exc:`PyCMSError` will be raised. - - This function builds and returns an ICC transform from the ``inputProfile`` - to the ``outputProfile`` using the ``renderingIntent`` to determine what to do - with out-of-gamut colors. It will ONLY work for converting images that - are in ``inMode`` to images that are in ``outMode`` color format (PIL mode, - i.e. "RGB", "RGBA", "CMYK", etc.). - - Building the transform is a fair part of the overhead in - ImageCms.profileToProfile(), so if you're planning on converting multiple - images using the same input/output settings, this can save you time. - Once you have a transform object, it can be used with - ImageCms.applyProfile() to convert images without the need to re-compute - the lookup table for the transform. - - The reason pyCMS returns a class object rather than a handle directly - to the transform is that it needs to keep track of the PIL input/output - modes that the transform is meant for. These attributes are stored in - the ``inMode`` and ``outMode`` attributes of the object (which can be - manually overridden if you really want to, but I don't know of any - time that would be of use, or would even work). - - :param inputProfile: String, as a valid filename path to the ICC input - profile you wish to use for this transform, or a profile object - :param outputProfile: String, as a valid filename path to the ICC output - profile you wish to use for this transform, or a profile object - :param inMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param outMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param renderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for the transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param flags: Integer (0-...) specifying additional flags - :returns: A CmsTransform class object. - :exception PyCMSError: - """ - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - msg = "renderingIntent must be an integer between 0 and 3" - raise PyCMSError(msg) - - if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = f"flags must be an integer between 0 and {_MAX_FLAG}" - raise PyCMSError(msg) - - try: - if not isinstance(inputProfile, ImageCmsProfile): - inputProfile = ImageCmsProfile(inputProfile) - if not isinstance(outputProfile, ImageCmsProfile): - outputProfile = ImageCmsProfile(outputProfile) - return ImageCmsTransform( - inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags - ) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def buildProofTransform( - inputProfile: _CmsProfileCompatible, - outputProfile: _CmsProfileCompatible, - proofProfile: _CmsProfileCompatible, - inMode: str, - outMode: str, - renderingIntent: Intent = Intent.PERCEPTUAL, - proofRenderingIntent: Intent = Intent.ABSOLUTE_COLORIMETRIC, - flags: Flags = Flags.SOFTPROOFING, -) -> ImageCmsTransform: - """ - (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the - ``outputProfile``, but tries to simulate the result that would be - obtained on the ``proofProfile`` device. - - If the input, output, or proof profiles specified are not valid - filenames, a :exc:`PyCMSError` will be raised. - - If an error occurs during creation of the transform, - a :exc:`PyCMSError` will be raised. - - If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` - (or by pyCMS), a :exc:`PyCMSError` will be raised. - - This function builds and returns an ICC transform from the ``inputProfile`` - to the ``outputProfile``, but tries to simulate the result that would be - obtained on the ``proofProfile`` device using ``renderingIntent`` and - ``proofRenderingIntent`` to determine what to do with out-of-gamut - colors. This is known as "soft-proofing". It will ONLY work for - converting images that are in ``inMode`` to images that are in outMode - color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.). - - Usage of the resulting transform object is exactly the same as with - ImageCms.buildTransform(). - - Proof profiling is generally used when using an output device to get a - good idea of what the final printed/displayed image would look like on - the ``proofProfile`` device when it's quicker and easier to use the - output device for judging color. Generally, this means that the - output device is a monitor, or a dye-sub printer (etc.), and the simulated - device is something more expensive, complicated, or time consuming - (making it difficult to make a real print for color judgement purposes). - - Soft-proofing basically functions by adjusting the colors on the - output device to match the colors of the device being simulated. However, - when the simulated device has a much wider gamut than the output - device, you may obtain marginal results. - - :param inputProfile: String, as a valid filename path to the ICC input - profile you wish to use for this transform, or a profile object - :param outputProfile: String, as a valid filename path to the ICC output - (monitor, usually) profile you wish to use for this transform, or a - profile object - :param proofProfile: String, as a valid filename path to the ICC proof - profile you wish to use for this transform, or a profile object - :param inMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param outMode: String, as a valid PIL mode that the appropriate profile - also supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param renderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for the input->proof (simulated) transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param proofRenderingIntent: Integer (0-3) specifying the rendering intent - you wish to use for proof->output transform - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param flags: Integer (0-...) specifying additional flags - :returns: A CmsTransform class object. - :exception PyCMSError: - """ - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - msg = "renderingIntent must be an integer between 0 and 3" - raise PyCMSError(msg) - - if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = f"flags must be an integer between 0 and {_MAX_FLAG}" - raise PyCMSError(msg) - - try: - if not isinstance(inputProfile, ImageCmsProfile): - inputProfile = ImageCmsProfile(inputProfile) - if not isinstance(outputProfile, ImageCmsProfile): - outputProfile = ImageCmsProfile(outputProfile) - if not isinstance(proofProfile, ImageCmsProfile): - proofProfile = ImageCmsProfile(proofProfile) - return ImageCmsTransform( - inputProfile, - outputProfile, - inMode, - outMode, - renderingIntent, - proofProfile, - proofRenderingIntent, - flags, - ) - except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -buildTransformFromOpenProfiles = buildTransform -buildProofTransformFromOpenProfiles = buildProofTransform - - -def applyTransform( - im: Image.Image, transform: ImageCmsTransform, inPlace: bool = False -) -> Image.Image | None: - """ - (pyCMS) Applies a transform to a given image. - - If ``im.mode != transform.input_mode``, a :exc:`PyCMSError` is raised. - - If ``inPlace`` is ``True`` and ``transform.input_mode != transform.output_mode``, a - :exc:`PyCMSError` is raised. - - If ``im.mode``, ``transform.input_mode`` or ``transform.output_mode`` is not - supported by pyCMSdll or the profiles you used for the transform, a - :exc:`PyCMSError` is raised. - - If an error occurs while the transform is being applied, - a :exc:`PyCMSError` is raised. - - This function applies a pre-calculated transform (from - ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles()) - to an image. The transform can be used for multiple images, saving - considerable calculation time if doing the same conversion multiple times. - - If you want to modify im in-place instead of receiving a new image as - the return value, set ``inPlace`` to ``True``. This can only be done if - ``transform.input_mode`` and ``transform.output_mode`` are the same, because we - can't change the mode in-place (the buffer sizes for some modes are - different). The default behavior is to return a new :py:class:`~PIL.Image.Image` - object of the same dimensions in mode ``transform.output_mode``. - - :param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same - as the ``input_mode`` supported by the transform. - :param transform: A valid CmsTransform class object - :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is - returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the - transform applied is returned (and ``im`` is not changed). The default is - ``False``. - :returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object, - depending on the value of ``inPlace``. The profile will be returned in - the image's ``info['icc_profile']``. - :exception PyCMSError: - """ - - try: - if inPlace: - transform.apply_in_place(im) - imOut = None - else: - imOut = transform.apply(im) - except (TypeError, ValueError) as v: - raise PyCMSError(v) from v - - return imOut - - -def createProfile( - colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0 -) -> core.CmsProfile: - """ - (pyCMS) Creates a profile. - - If colorSpace not in ``["LAB", "XYZ", "sRGB"]``, - a :exc:`PyCMSError` is raised. - - If using LAB and ``colorTemp`` is not a positive integer, - a :exc:`PyCMSError` is raised. - - If an error occurs while creating the profile, - a :exc:`PyCMSError` is raised. - - Use this function to create common profiles on-the-fly instead of - having to supply a profile on disk and knowing the path to it. It - returns a normal CmsProfile object that can be passed to - ImageCms.buildTransformFromOpenProfiles() to create a transform to apply - to images. - - :param colorSpace: String, the color space of the profile you wish to - create. - Currently only "LAB", "XYZ", and "sRGB" are supported. - :param colorTemp: Positive number for the white point for the profile, in - degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50 - illuminant if omitted (5000k). colorTemp is ONLY applied to LAB - profiles, and is ignored for XYZ and sRGB. - :returns: A CmsProfile class object - :exception PyCMSError: - """ - - if colorSpace not in ["LAB", "XYZ", "sRGB"]: - msg = ( - f"Color space not supported for on-the-fly profile creation ({colorSpace})" - ) - raise PyCMSError(msg) - - if colorSpace == "LAB": - try: - colorTemp = float(colorTemp) - except (TypeError, ValueError) as e: - msg = f'Color temperature must be numeric, "{colorTemp}" not valid' - raise PyCMSError(msg) from e - - try: - return core.createProfile(colorSpace, colorTemp) - except (TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileName(profile: _CmsProfileCompatible) -> str: - """ - - (pyCMS) Gets the internal product name for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, - a :exc:`PyCMSError` is raised If an error occurs while trying - to obtain the name tag, a :exc:`PyCMSError` is raised. - - Use this function to obtain the INTERNAL name of the profile (stored - in an ICC tag in the profile itself), usually the one used when the - profile was originally created. Sometimes this tag also contains - additional information supplied by the creator. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal name of the profile as stored - in an ICC tag. - :exception PyCMSError: - """ - - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - # do it in python, not c. - # // name was "%s - %s" (model, manufacturer) || Description , - # // but if the Model and Manufacturer were the same or the model - # // was long, Just the model, in 1.x - model = profile.profile.model - manufacturer = profile.profile.manufacturer - - if not (model or manufacturer): - return (profile.profile.profile_description or "") + "\n" - if not manufacturer or (model and len(model) > 30): - return f"{model}\n" - return f"{model} - {manufacturer}\n" - - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileInfo(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the internal product information for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, - a :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the info tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - info tag. This often contains details about the profile, and how it - was created, as supplied by the creator. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - - try: - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - # add an extra newline to preserve pyCMS compatibility - # Python, not C. the white point bits weren't working well, - # so skipping. - # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint - description = profile.profile.profile_description - cpright = profile.profile.copyright - elements = [element for element in (description, cpright) if element] - return "\r\n\r\n".join(elements) + "\r\n\r\n" - - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileCopyright(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the copyright for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the copyright tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - copyright tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.copyright or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileManufacturer(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the manufacturer for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the manufacturer tag, a - :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - manufacturer tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.manufacturer or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileModel(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the model for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the model tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - model tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in - an ICC tag. - :exception PyCMSError: - """ - - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.model or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getProfileDescription(profile: _CmsProfileCompatible) -> str: - """ - (pyCMS) Gets the description for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the description tag, - a :exc:`PyCMSError` is raised. - - Use this function to obtain the information stored in the profile's - description tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: A string containing the internal profile information stored in an - ICC tag. - :exception PyCMSError: - """ - - try: - # add an extra newline to preserve pyCMS compatibility - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return (profile.profile.profile_description or "") + "\n" - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def getDefaultIntent(profile: _CmsProfileCompatible) -> int: - """ - (pyCMS) Gets the default intent name for the given profile. - - If ``profile`` isn't a valid CmsProfile object or filename to a profile, a - :exc:`PyCMSError` is raised. - - If an error occurs while trying to obtain the default intent, a - :exc:`PyCMSError` is raised. - - Use this function to determine the default (and usually best optimized) - rendering intent for this profile. Most profiles support multiple - rendering intents, but are intended mostly for one type of conversion. - If you wish to use a different intent than returned, use - ImageCms.isIntentSupported() to verify it will work first. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :returns: Integer 0-3 specifying the default rendering intent for this - profile. - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :exception PyCMSError: - """ - - try: - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - return profile.profile.rendering_intent - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v - - -def isIntentSupported( - profile: _CmsProfileCompatible, intent: Intent, direction: Direction -) -> Literal[-1, 1]: - """ - (pyCMS) Checks if a given intent is supported. - - Use this function to verify that you can use your desired - ``intent`` with ``profile``, and that ``profile`` can be used for the - input/output/proof profile as you desire. - - Some profiles are created specifically for one "direction", can cannot - be used for others. Some profiles can only be used for certain - rendering intents, so it's best to either verify this before trying - to create a transform with them (using this function), or catch the - potential :exc:`PyCMSError` that will occur if they don't - support the modes you select. - - :param profile: EITHER a valid CmsProfile object, OR a string of the - filename of an ICC profile. - :param intent: Integer (0-3) specifying the rendering intent you wish to - use with this profile - - ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) - ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 - ImageCms.Intent.SATURATION = 2 - ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 - - see the pyCMS documentation for details on rendering intents and what - they do. - :param direction: Integer specifying if the profile is to be used for - input, output, or proof - - INPUT = 0 (or use ImageCms.Direction.INPUT) - OUTPUT = 1 (or use ImageCms.Direction.OUTPUT) - PROOF = 2 (or use ImageCms.Direction.PROOF) - - :returns: 1 if the intent/direction are supported, -1 if they are not. - :exception PyCMSError: - """ - - try: - if not isinstance(profile, ImageCmsProfile): - profile = ImageCmsProfile(profile) - # FIXME: I get different results for the same data w. different - # compilers. Bug in LittleCMS or in the binding? - if profile.profile.is_intent_supported(intent, direction): - return 1 - else: - return -1 - except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) from v diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageColor.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageColor.py deleted file mode 100644 index 9a15a8e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageColor.py +++ /dev/null @@ -1,320 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# map CSS3-style colour description strings to RGB -# -# History: -# 2002-10-24 fl Added support for CSS-style color strings -# 2002-12-15 fl Added RGBA support -# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2 -# 2004-07-19 fl Fixed gray/grey spelling issues -# 2009-03-05 fl Fixed rounding error in grayscale calculation -# -# Copyright (c) 2002-2004 by Secret Labs AB -# Copyright (c) 2002-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re -from functools import lru_cache - -from . import Image - - -@lru_cache -def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]: - """ - Convert a color string to an RGB or RGBA tuple. If the string cannot be - parsed, this function raises a :py:exc:`ValueError` exception. - - .. versionadded:: 1.1.4 - - :param color: A color string - :return: ``(red, green, blue[, alpha])`` - """ - if len(color) > 100: - msg = "color specifier is too long" - raise ValueError(msg) - color = color.lower() - - rgb = colormap.get(color, None) - if rgb: - if isinstance(rgb, tuple): - return rgb - rgb_tuple = getrgb(rgb) - assert len(rgb_tuple) == 3 - colormap[color] = rgb_tuple - return rgb_tuple - - # check for known string formats - if re.match("#[a-f0-9]{3}$", color): - return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16) - - if re.match("#[a-f0-9]{4}$", color): - return ( - int(color[1] * 2, 16), - int(color[2] * 2, 16), - int(color[3] * 2, 16), - int(color[4] * 2, 16), - ) - - if re.match("#[a-f0-9]{6}$", color): - return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16) - - if re.match("#[a-f0-9]{8}$", color): - return ( - int(color[1:3], 16), - int(color[3:5], 16), - int(color[5:7], 16), - int(color[7:9], 16), - ) - - m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) - if m: - return int(m.group(1)), int(m.group(2)), int(m.group(3)) - - m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) - if m: - return ( - int((int(m.group(1)) * 255) / 100.0 + 0.5), - int((int(m.group(2)) * 255) / 100.0 + 0.5), - int((int(m.group(3)) * 255) / 100.0 + 0.5), - ) - - m = re.match( - r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color - ) - if m: - from colorsys import hls_to_rgb - - rgb_floats = hls_to_rgb( - float(m.group(1)) / 360.0, - float(m.group(3)) / 100.0, - float(m.group(2)) / 100.0, - ) - return ( - int(rgb_floats[0] * 255 + 0.5), - int(rgb_floats[1] * 255 + 0.5), - int(rgb_floats[2] * 255 + 0.5), - ) - - m = re.match( - r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color - ) - if m: - from colorsys import hsv_to_rgb - - rgb_floats = hsv_to_rgb( - float(m.group(1)) / 360.0, - float(m.group(2)) / 100.0, - float(m.group(3)) / 100.0, - ) - return ( - int(rgb_floats[0] * 255 + 0.5), - int(rgb_floats[1] * 255 + 0.5), - int(rgb_floats[2] * 255 + 0.5), - ) - - m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) - if m: - return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)) - msg = f"unknown color specifier: {repr(color)}" - raise ValueError(msg) - - -@lru_cache -def getcolor(color: str, mode: str) -> int | tuple[int, ...]: - """ - Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if - ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is - not color or a palette image, converts the RGB value to a grayscale value. - If the string cannot be parsed, this function raises a :py:exc:`ValueError` - exception. - - .. versionadded:: 1.1.4 - - :param color: A color string - :param mode: Convert result to this mode - :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])`` - """ - # same as getrgb, but converts the result to the given mode - rgb, alpha = getrgb(color), 255 - if len(rgb) == 4: - alpha = rgb[3] - rgb = rgb[:3] - - if mode == "HSV": - from colorsys import rgb_to_hsv - - r, g, b = rgb - h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) - return int(h * 255), int(s * 255), int(v * 255) - elif Image.getmodebase(mode) == "L": - r, g, b = rgb - # ITU-R Recommendation 601-2 for nonlinear RGB - # scaled to 24 bits to match the convert's implementation. - graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 - if mode[-1] == "A": - return graylevel, alpha - return graylevel - elif mode[-1] == "A": - return rgb + (alpha,) - return rgb - - -colormap: dict[str, str | tuple[int, int, int]] = { - # X11 colour table from https://drafts.csswg.org/css-color-4/, with - # gray/grey spelling issues fixed. This is a superset of HTML 4.0 - # colour names used in CSS 1. - "aliceblue": "#f0f8ff", - "antiquewhite": "#faebd7", - "aqua": "#00ffff", - "aquamarine": "#7fffd4", - "azure": "#f0ffff", - "beige": "#f5f5dc", - "bisque": "#ffe4c4", - "black": "#000000", - "blanchedalmond": "#ffebcd", - "blue": "#0000ff", - "blueviolet": "#8a2be2", - "brown": "#a52a2a", - "burlywood": "#deb887", - "cadetblue": "#5f9ea0", - "chartreuse": "#7fff00", - "chocolate": "#d2691e", - "coral": "#ff7f50", - "cornflowerblue": "#6495ed", - "cornsilk": "#fff8dc", - "crimson": "#dc143c", - "cyan": "#00ffff", - "darkblue": "#00008b", - "darkcyan": "#008b8b", - "darkgoldenrod": "#b8860b", - "darkgray": "#a9a9a9", - "darkgrey": "#a9a9a9", - "darkgreen": "#006400", - "darkkhaki": "#bdb76b", - "darkmagenta": "#8b008b", - "darkolivegreen": "#556b2f", - "darkorange": "#ff8c00", - "darkorchid": "#9932cc", - "darkred": "#8b0000", - "darksalmon": "#e9967a", - "darkseagreen": "#8fbc8f", - "darkslateblue": "#483d8b", - "darkslategray": "#2f4f4f", - "darkslategrey": "#2f4f4f", - "darkturquoise": "#00ced1", - "darkviolet": "#9400d3", - "deeppink": "#ff1493", - "deepskyblue": "#00bfff", - "dimgray": "#696969", - "dimgrey": "#696969", - "dodgerblue": "#1e90ff", - "firebrick": "#b22222", - "floralwhite": "#fffaf0", - "forestgreen": "#228b22", - "fuchsia": "#ff00ff", - "gainsboro": "#dcdcdc", - "ghostwhite": "#f8f8ff", - "gold": "#ffd700", - "goldenrod": "#daa520", - "gray": "#808080", - "grey": "#808080", - "green": "#008000", - "greenyellow": "#adff2f", - "honeydew": "#f0fff0", - "hotpink": "#ff69b4", - "indianred": "#cd5c5c", - "indigo": "#4b0082", - "ivory": "#fffff0", - "khaki": "#f0e68c", - "lavender": "#e6e6fa", - "lavenderblush": "#fff0f5", - "lawngreen": "#7cfc00", - "lemonchiffon": "#fffacd", - "lightblue": "#add8e6", - "lightcoral": "#f08080", - "lightcyan": "#e0ffff", - "lightgoldenrodyellow": "#fafad2", - "lightgreen": "#90ee90", - "lightgray": "#d3d3d3", - "lightgrey": "#d3d3d3", - "lightpink": "#ffb6c1", - "lightsalmon": "#ffa07a", - "lightseagreen": "#20b2aa", - "lightskyblue": "#87cefa", - "lightslategray": "#778899", - "lightslategrey": "#778899", - "lightsteelblue": "#b0c4de", - "lightyellow": "#ffffe0", - "lime": "#00ff00", - "limegreen": "#32cd32", - "linen": "#faf0e6", - "magenta": "#ff00ff", - "maroon": "#800000", - "mediumaquamarine": "#66cdaa", - "mediumblue": "#0000cd", - "mediumorchid": "#ba55d3", - "mediumpurple": "#9370db", - "mediumseagreen": "#3cb371", - "mediumslateblue": "#7b68ee", - "mediumspringgreen": "#00fa9a", - "mediumturquoise": "#48d1cc", - "mediumvioletred": "#c71585", - "midnightblue": "#191970", - "mintcream": "#f5fffa", - "mistyrose": "#ffe4e1", - "moccasin": "#ffe4b5", - "navajowhite": "#ffdead", - "navy": "#000080", - "oldlace": "#fdf5e6", - "olive": "#808000", - "olivedrab": "#6b8e23", - "orange": "#ffa500", - "orangered": "#ff4500", - "orchid": "#da70d6", - "palegoldenrod": "#eee8aa", - "palegreen": "#98fb98", - "paleturquoise": "#afeeee", - "palevioletred": "#db7093", - "papayawhip": "#ffefd5", - "peachpuff": "#ffdab9", - "peru": "#cd853f", - "pink": "#ffc0cb", - "plum": "#dda0dd", - "powderblue": "#b0e0e6", - "purple": "#800080", - "rebeccapurple": "#663399", - "red": "#ff0000", - "rosybrown": "#bc8f8f", - "royalblue": "#4169e1", - "saddlebrown": "#8b4513", - "salmon": "#fa8072", - "sandybrown": "#f4a460", - "seagreen": "#2e8b57", - "seashell": "#fff5ee", - "sienna": "#a0522d", - "silver": "#c0c0c0", - "skyblue": "#87ceeb", - "slateblue": "#6a5acd", - "slategray": "#708090", - "slategrey": "#708090", - "snow": "#fffafa", - "springgreen": "#00ff7f", - "steelblue": "#4682b4", - "tan": "#d2b48c", - "teal": "#008080", - "thistle": "#d8bfd8", - "tomato": "#ff6347", - "turquoise": "#40e0d0", - "violet": "#ee82ee", - "wheat": "#f5deb3", - "white": "#ffffff", - "whitesmoke": "#f5f5f5", - "yellow": "#ffff00", - "yellowgreen": "#9acd32", -} diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageDraw.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageDraw.py deleted file mode 100644 index 9b0864d..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageDraw.py +++ /dev/null @@ -1,1035 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# drawing interface operations -# -# History: -# 1996-04-13 fl Created (experimental) -# 1996-08-07 fl Filled polygons, ellipses. -# 1996-08-13 fl Added text support -# 1998-06-28 fl Handle I and F images -# 1998-12-29 fl Added arc; use arc primitive to draw ellipses -# 1999-01-10 fl Added shape stuff (experimental) -# 1999-02-06 fl Added bitmap support -# 1999-02-11 fl Changed all primitives to take options -# 1999-02-20 fl Fixed backwards compatibility -# 2000-10-12 fl Copy on write, when necessary -# 2001-02-18 fl Use default ink for bitmap/text also in fill mode -# 2002-10-24 fl Added support for CSS-style color strings -# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing -# 2002-12-11 fl Refactored low-level drawing API (work in progress) -# 2004-08-26 fl Made Draw() a factory function, added getdraw() support -# 2004-09-04 fl Added width support to line primitive -# 2004-09-10 fl Added font mode handling -# 2006-06-19 fl Added font bearing support (getmask2) -# -# Copyright (c) 1997-2006 by Secret Labs AB -# Copyright (c) 1996-2006 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import math -import struct -from collections.abc import Sequence -from typing import cast - -from . import Image, ImageColor, ImageText - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable - from types import ModuleType - from typing import Any, AnyStr - - from . import ImageDraw2, ImageFont - from ._typing import Coords, _Ink - -# experimental access to the outline API -Outline: Callable[[], Image.core._Outline] = Image.core.outline - -""" -A simple 2D drawing interface for PIL images. -<p> -Application code should use the <b>Draw</b> factory, instead of -directly. -""" - - -class ImageDraw: - font: ( - ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None - ) = None - - def __init__(self, im: Image.Image, mode: str | None = None) -> None: - """ - Create a drawing instance. - - :param im: The image to draw in. - :param mode: Optional mode to use for color values. For RGB - images, this argument can be RGB or RGBA (to blend the - drawing into the image). For all other modes, this argument - must be the same as the image mode. If omitted, the mode - defaults to the mode of the image. - """ - im._ensure_mutable() - blend = 0 - if mode is None: - mode = im.mode - if mode != im.mode: - if mode == "RGBA" and im.mode == "RGB": - blend = 1 - else: - msg = "mode mismatch" - raise ValueError(msg) - if mode == "P": - self.palette = im.palette - else: - self.palette = None - self._image = im - self.im = im.im - self.draw = Image.core.draw(self.im, blend) - self.mode = mode - if mode in ("I", "F"): - self.ink = self.draw.draw_ink(1) - else: - self.ink = self.draw.draw_ink(-1) - if mode in ("1", "P", "I", "F"): - # FIXME: fix Fill2 to properly support matte for I+F images - self.fontmode = "1" - else: - self.fontmode = "L" # aliasing is okay for other modes - self.fill = False - - def getfont( - self, - ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: - """ - Get the current default font. - - To set the default font for this ImageDraw instance:: - - from PIL import ImageDraw, ImageFont - draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - To set the default font for all future ImageDraw instances:: - - from PIL import ImageDraw, ImageFont - ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") - - If the current default font is ``None``, - it is initialized with ``ImageFont.load_default()``. - - :returns: An image font.""" - if not self.font: - # FIXME: should add a font repository - from . import ImageFont - - self.font = ImageFont.load_default() - return self.font - - def _getfont( - self, font_size: float | None - ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: - if font_size is not None: - from . import ImageFont - - return ImageFont.load_default(font_size) - else: - return self.getfont() - - def _getink( - self, ink: _Ink | None, fill: _Ink | None = None - ) -> tuple[int | None, int | None]: - result_ink = None - result_fill = None - if ink is None and fill is None: - if self.fill: - result_fill = self.ink - else: - result_ink = self.ink - else: - if ink is not None: - if isinstance(ink, str): - ink = ImageColor.getcolor(ink, self.mode) - if self.palette and isinstance(ink, tuple): - ink = self.palette.getcolor(ink, self._image) - result_ink = self.draw.draw_ink(ink) - if fill is not None: - if isinstance(fill, str): - fill = ImageColor.getcolor(fill, self.mode) - if self.palette and isinstance(fill, tuple): - fill = self.palette.getcolor(fill, self._image) - result_fill = self.draw.draw_ink(fill) - return result_ink, result_fill - - def arc( - self, - xy: Coords, - start: float, - end: float, - fill: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw an arc.""" - ink, fill = self._getink(fill) - if ink is not None: - self.draw.draw_arc(xy, start, end, ink, width) - - def bitmap( - self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None - ) -> None: - """Draw a bitmap.""" - bitmap.load() - ink, fill = self._getink(fill) - if ink is None: - ink = fill - if ink is not None: - self.draw.draw_bitmap(xy, bitmap.im, ink) - - def chord( - self, - xy: Coords, - start: float, - end: float, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a chord.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_chord(xy, start, end, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - self.draw.draw_chord(xy, start, end, ink, 0, width) - - def ellipse( - self, - xy: Coords, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw an ellipse.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_ellipse(xy, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - self.draw.draw_ellipse(xy, ink, 0, width) - - def circle( - self, - xy: Sequence[float], - radius: float, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a circle given center coordinates and a radius.""" - ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) - self.ellipse(ellipse_xy, fill, outline, width) - - def line( - self, - xy: Coords, - fill: _Ink | None = None, - width: int = 0, - joint: str | None = None, - ) -> None: - """Draw a line, or a connected sequence of line segments.""" - ink = self._getink(fill)[0] - if ink is not None: - self.draw.draw_lines(xy, ink, width) - if joint == "curve" and width > 4: - points: Sequence[Sequence[float]] - if isinstance(xy[0], (list, tuple)): - points = cast(Sequence[Sequence[float]], xy) - else: - points = [ - cast(Sequence[float], tuple(xy[i : i + 2])) - for i in range(0, len(xy), 2) - ] - for i in range(1, len(points) - 1): - point = points[i] - angles = [ - math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) - % 360 - for start, end in ( - (points[i - 1], point), - (point, points[i + 1]), - ) - ] - if angles[0] == angles[1]: - # This is a straight line, so no joint is required - continue - - def coord_at_angle( - coord: Sequence[float], angle: float - ) -> tuple[float, ...]: - x, y = coord - angle -= 90 - distance = width / 2 - 1 - return tuple( - p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) - for p, p_d in ( - (x, distance * math.cos(math.radians(angle))), - (y, distance * math.sin(math.radians(angle))), - ) - ) - - flipped = ( - angles[1] > angles[0] and angles[1] - 180 > angles[0] - ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0]) - coords = [ - (point[0] - width / 2 + 1, point[1] - width / 2 + 1), - (point[0] + width / 2 - 1, point[1] + width / 2 - 1), - ] - if flipped: - start, end = (angles[1] + 90, angles[0] + 90) - else: - start, end = (angles[0] - 90, angles[1] - 90) - self.pieslice(coords, start - 90, end - 90, fill) - - if width > 8: - # Cover potential gaps between the line and the joint - if flipped: - gap_coords = [ - coord_at_angle(point, angles[0] + 90), - point, - coord_at_angle(point, angles[1] + 90), - ] - else: - gap_coords = [ - coord_at_angle(point, angles[0] - 90), - point, - coord_at_angle(point, angles[1] - 90), - ] - self.line(gap_coords, fill, width=3) - - def shape( - self, - shape: Image.core._Outline, - fill: _Ink | None = None, - outline: _Ink | None = None, - ) -> None: - """(Experimental) Draw a shape.""" - shape.close() - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_outline(shape, fill_ink, 1) - if ink is not None and ink != fill_ink: - self.draw.draw_outline(shape, ink, 0) - - def pieslice( - self, - xy: Coords, - start: float, - end: float, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a pieslice.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_pieslice(xy, start, end, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - self.draw.draw_pieslice(xy, start, end, ink, 0, width) - - def point(self, xy: Coords, fill: _Ink | None = None) -> None: - """Draw one or more individual pixels.""" - ink, fill = self._getink(fill) - if ink is not None: - self.draw.draw_points(xy, ink) - - def polygon( - self, - xy: Coords, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a polygon.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_polygon(xy, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - if width == 1: - self.draw.draw_polygon(xy, ink, 0, width) - elif self.im is not None: - # To avoid expanding the polygon outwards, - # use the fill as a mask - mask = Image.new("1", self.im.size) - mask_ink = self._getink(1)[0] - draw = Draw(mask) - draw.draw.draw_polygon(xy, mask_ink, 1) - - self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im) - - def regular_polygon( - self, - bounding_circle: Sequence[Sequence[float] | float], - n_sides: int, - rotation: float = 0, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a regular polygon.""" - xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - self.polygon(xy, fill, outline, width) - - def rectangle( - self, - xy: Coords, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - ) -> None: - """Draw a rectangle.""" - ink, fill_ink = self._getink(outline, fill) - if fill_ink is not None: - self.draw.draw_rectangle(xy, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - self.draw.draw_rectangle(xy, ink, 0, width) - - def rounded_rectangle( - self, - xy: Coords, - radius: float = 0, - fill: _Ink | None = None, - outline: _Ink | None = None, - width: int = 1, - *, - corners: tuple[bool, bool, bool, bool] | None = None, - ) -> None: - """Draw a rounded rectangle.""" - if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) - else: - x0, y0, x1, y1 = cast(Sequence[float], xy) - if x1 < x0: - msg = "x1 must be greater than or equal to x0" - raise ValueError(msg) - if y1 < y0: - msg = "y1 must be greater than or equal to y0" - raise ValueError(msg) - if corners is None: - corners = (True, True, True, True) - - d = radius * 2 - - x0 = round(x0) - y0 = round(y0) - x1 = round(x1) - y1 = round(y1) - full_x, full_y = False, False - if all(corners): - full_x = d >= x1 - x0 - 1 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - 1 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) - - if d == 0 or not any(corners): - # If the corners have no curve, - # or there are no corners, - # that is a rectangle - return self.rectangle(xy, fill, outline, width) - - r = int(d // 2) - ink, fill_ink = self._getink(outline, fill) - - def draw_corners(pieslice: bool) -> None: - parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] - if full_x: - # Draw top and bottom halves - parts = ( - ((x0, y0, x0 + d, y0 + d), 180, 360), - ((x0, y1 - d, x0 + d, y1), 0, 180), - ) - elif full_y: - # Draw left and right halves - parts = ( - ((x0, y0, x0 + d, y0 + d), 90, 270), - ((x1 - d, y0, x1, y0 + d), 270, 90), - ) - else: - # Draw four separate corners - parts = tuple( - part - for i, part in enumerate( - ( - ((x0, y0, x0 + d, y0 + d), 180, 270), - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ) - ) - if corners[i] - ) - for part in parts: - if pieslice: - self.draw.draw_pieslice(*(part + (fill_ink, 1))) - else: - self.draw.draw_arc(*(part + (ink, width))) - - if fill_ink is not None: - draw_corners(True) - - if full_x: - self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) - elif x1 - r - 1 >= x0 + r + 1: - self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) - if not full_x and not full_y: - left = [x0, y0, x0 + r, y1] - if corners[0]: - left[1] += r + 1 - if corners[3]: - left[3] -= r + 1 - self.draw.draw_rectangle(left, fill_ink, 1) - - right = [x1 - r, y0, x1, y1] - if corners[1]: - right[1] += r + 1 - if corners[2]: - right[3] -= r + 1 - self.draw.draw_rectangle(right, fill_ink, 1) - if ink is not None and ink != fill_ink and width != 0: - draw_corners(False) - - if not full_x: - top = [x0, y0, x1, y0 + width - 1] - if corners[0]: - top[0] += r + 1 - if corners[1]: - top[2] -= r + 1 - self.draw.draw_rectangle(top, ink, 1) - - bottom = [x0, y1 - width + 1, x1, y1] - if corners[3]: - bottom[0] += r + 1 - if corners[2]: - bottom[2] -= r + 1 - self.draw.draw_rectangle(bottom, ink, 1) - if not full_y: - left = [x0, y0, x0 + width - 1, y1] - if corners[0]: - left[1] += r + 1 - if corners[3]: - left[3] -= r + 1 - self.draw.draw_rectangle(left, ink, 1) - - right = [x1 - width + 1, y0, x1, y1] - if corners[1]: - right[1] += r + 1 - if corners[2]: - right[3] -= r + 1 - self.draw.draw_rectangle(right, ink, 1) - - def text( - self, - xy: tuple[float, float], - text: AnyStr | ImageText.Text[AnyStr], - fill: _Ink | None = None, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor: str | None = None, - spacing: float = 4, - align: str = "left", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - stroke_fill: _Ink | None = None, - embedded_color: bool = False, - *args: Any, - **kwargs: Any, - ) -> None: - """Draw text.""" - if isinstance(text, ImageText.Text): - image_text = text - else: - if font is None: - font = self._getfont(kwargs.get("font_size")) - image_text = ImageText.Text( - text, font, self.mode, spacing, direction, features, language - ) - if embedded_color: - image_text.embed_color() - if stroke_width: - image_text.stroke(stroke_width, stroke_fill) - - def getink(fill: _Ink | None) -> int: - ink, fill_ink = self._getink(fill) - if ink is None: - assert fill_ink is not None - return fill_ink - return ink - - ink = getink(fill) - if ink is None: - return - - stroke_ink = None - if image_text.stroke_width: - stroke_ink = ( - getink(image_text.stroke_fill) - if image_text.stroke_fill is not None - else ink - ) - - for line in image_text._split(xy, anchor, align): - - def draw_text(ink: int, stroke_width: float = 0) -> None: - mode = self.fontmode - if stroke_width == 0 and embedded_color: - mode = "RGBA" - x = int(line.x) - y = int(line.y) - start = (math.modf(line.x)[0], math.modf(line.y)[0]) - try: - mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc] - line.text, - mode, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - stroke_filled=True, - anchor=line.anchor, - ink=ink, - start=start, - *args, - **kwargs, - ) - x += offset[0] - y += offset[1] - except AttributeError: - try: - mask = image_text.font.getmask( # type: ignore[misc] - line.text, - mode, - direction, - features, - language, - stroke_width, - line.anchor, - ink, - start=start, - *args, - **kwargs, - ) - except TypeError: - mask = image_text.font.getmask(line.text) - if mode == "RGBA": - # image_text.font.getmask2(mode="RGBA") - # returns color in RGB bands and mask in A - # extract mask and set text alpha - color, mask = mask, mask.getband(3) - ink_alpha = struct.pack("i", ink)[3] - color.fillband(3, ink_alpha) - if self.im is not None: - self.im.paste( - color, (x, y, x + mask.size[0], y + mask.size[1]), mask - ) - else: - self.draw.draw_bitmap((x, y), mask, ink) - - if stroke_ink is not None: - # Draw stroked text - draw_text(stroke_ink, image_text.stroke_width) - - # Draw normal text - if ink != stroke_ink: - draw_text(ink) - else: - # Only draw normal text - draw_text(ink) - - def multiline_text( - self, - xy: tuple[float, float], - text: AnyStr, - fill: _Ink | None = None, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor: str | None = None, - spacing: float = 4, - align: str = "left", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - stroke_fill: _Ink | None = None, - embedded_color: bool = False, - *, - font_size: float | None = None, - ) -> None: - return self.text( - xy, - text, - fill, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - stroke_fill, - embedded_color, - font_size=font_size, - ) - - def textlength( - self, - text: AnyStr, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - embedded_color: bool = False, - *, - font_size: float | None = None, - ) -> float: - """Get the length of a given string, in pixels with 1/64 precision.""" - if font is None: - font = self._getfont(font_size) - image_text = ImageText.Text( - text, - font, - self.mode, - direction=direction, - features=features, - language=language, - ) - if embedded_color: - image_text.embed_color() - return image_text.get_length() - - def textbbox( - self, - xy: tuple[float, float], - text: AnyStr, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor: str | None = None, - spacing: float = 4, - align: str = "left", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - embedded_color: bool = False, - *, - font_size: float | None = None, - ) -> tuple[float, float, float, float]: - """Get the bounding box of a given string, in pixels.""" - if font is None: - font = self._getfont(font_size) - image_text = ImageText.Text( - text, font, self.mode, spacing, direction, features, language - ) - if embedded_color: - image_text.embed_color() - if stroke_width: - image_text.stroke(stroke_width) - return image_text.get_bbox(xy, anchor, align) - - def multiline_textbbox( - self, - xy: tuple[float, float], - text: AnyStr, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, - anchor: str | None = None, - spacing: float = 4, - align: str = "left", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - embedded_color: bool = False, - *, - font_size: float | None = None, - ) -> tuple[float, float, float, float]: - return self.textbbox( - xy, - text, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - embedded_color, - font_size=font_size, - ) - - -def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: - """ - A simple 2D drawing interface for PIL images. - - :param im: The image to draw in. - :param mode: Optional mode to use for color values. For RGB - images, this argument can be RGB or RGBA (to blend the - drawing into the image). For all other modes, this argument - must be the same as the image mode. If omitted, the mode - defaults to the mode of the image. - """ - try: - return getattr(im, "getdraw")(mode) - except AttributeError: - return ImageDraw(im, mode) - - -def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]: - """ - :param im: The image to draw in. - :returns: A (drawing context, drawing resource factory) tuple. - """ - from . import ImageDraw2 - - draw = ImageDraw2.Draw(im) if im is not None else None - return draw, ImageDraw2 - - -def floodfill( - image: Image.Image, - xy: tuple[int, int], - value: float | tuple[int, ...], - border: float | tuple[int, ...] | None = None, - thresh: float = 0, -) -> None: - """ - .. warning:: This method is experimental. - - Fills a bounded region with a given color. - - :param image: Target image. - :param xy: Seed position (a 2-item coordinate tuple). See - :ref:`coordinate-system`. - :param value: Fill color. - :param border: Optional border value. If given, the region consists of - pixels with a color different from the border color. If not given, - the region consists of pixels having the same color as the seed - pixel. - :param thresh: Optional threshold value which specifies a maximum - tolerable difference of a pixel value from the 'background' in - order for it to be replaced. Useful for filling regions of - non-homogeneous, but similar, colors. - """ - # based on an implementation by Eric S. Raymond - # amended by yo1995 @20180806 - pixel = image.load() - assert pixel is not None - x, y = xy - try: - background = pixel[x, y] - if _color_diff(value, background) <= thresh: - return # seed point already has fill color - pixel[x, y] = value - except (ValueError, IndexError): - return # seed point outside image - edge = {(x, y)} - # use a set to keep record of current and previous edge pixels - # to reduce memory consumption - full_edge = set() - while edge: - new_edge = set() - for x, y in edge: # 4 adjacent method - for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): - # If already processed, or if a coordinate is negative, skip - if (s, t) in full_edge or s < 0 or t < 0: - continue - try: - p = pixel[s, t] - except (ValueError, IndexError): - pass - else: - full_edge.add((s, t)) - if border is None: - fill = _color_diff(p, background) <= thresh - else: - fill = p not in (value, border) - if fill: - pixel[s, t] = value - new_edge.add((s, t)) - full_edge = edge # discard pixels processed - edge = new_edge - - -def _compute_regular_polygon_vertices( - bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float -) -> list[tuple[float, float]]: - """ - Generate a list of vertices for a 2D regular polygon. - - :param bounding_circle: The bounding circle is a sequence defined - by a point and radius. The polygon is inscribed in this circle. - (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) - :param n_sides: Number of sides - (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) - :param rotation: Apply an arbitrary rotation to the polygon - (e.g. ``rotation=90``, applies a 90 degree rotation) - :return: List of regular polygon vertices - (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) - - How are the vertices computed? - 1. Compute the following variables - - theta: Angle between the apothem & the nearest polygon vertex - - side_length: Length of each polygon edge - - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) - - polygon_radius: Polygon radius (last element of bounding_circle) - - angles: Location of each polygon vertex in polar grid - (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) - - 2. For each angle in angles, get the polygon vertex at that angle - The vertex is computed using the equation below. - X= xcos(φ) + ysin(φ) - Y= −xsin(φ) + ycos(φ) - - Note: - φ = angle in degrees - x = 0 - y = polygon_radius - - The formula above assumes rotation around the origin. - In our case, we are rotating around the centroid. - To account for this, we use the formula below - X = xcos(φ) + ysin(φ) + centroid_x - Y = −xsin(φ) + ycos(φ) + centroid_y - """ - # 1. Error Handling - # 1.1 Check `n_sides` has an appropriate value - if not isinstance(n_sides, int): - msg = "n_sides should be an int" # type: ignore[unreachable] - raise TypeError(msg) - if n_sides < 3: - msg = "n_sides should be an int > 2" - raise ValueError(msg) - - # 1.2 Check `bounding_circle` has an appropriate value - if not isinstance(bounding_circle, (list, tuple)): - msg = "bounding_circle should be a sequence" - raise TypeError(msg) - - if len(bounding_circle) == 3: - if not all(isinstance(i, (int, float)) for i in bounding_circle): - msg = "bounding_circle should only contain numeric data" - raise ValueError(msg) - - *centroid, polygon_radius = cast(list[float], list(bounding_circle)) - elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)): - if not all( - isinstance(i, (int, float)) for i in bounding_circle[0] - ) or not isinstance(bounding_circle[1], (int, float)): - msg = "bounding_circle should only contain numeric data" - raise ValueError(msg) - - if len(bounding_circle[0]) != 2: - msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" - raise ValueError(msg) - - centroid = cast(list[float], list(bounding_circle[0])) - polygon_radius = cast(float, bounding_circle[1]) - else: - msg = ( - "bounding_circle should contain 2D coordinates " - "and a radius (e.g. (x, y, r) or ((x, y), r) )" - ) - raise ValueError(msg) - - if polygon_radius <= 0: - msg = "bounding_circle radius should be > 0" - raise ValueError(msg) - - # 1.3 Check `rotation` has an appropriate value - if not isinstance(rotation, (int, float)): - msg = "rotation should be an int or float" # type: ignore[unreachable] - raise ValueError(msg) - - # 2. Define Helper Functions - def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]: - return ( - round( - point[0] * math.cos(math.radians(360 - degrees)) - - point[1] * math.sin(math.radians(360 - degrees)) - + centroid[0], - 2, - ), - round( - point[1] * math.cos(math.radians(360 - degrees)) - + point[0] * math.sin(math.radians(360 - degrees)) - + centroid[1], - 2, - ), - ) - - def _compute_polygon_vertex(angle: float) -> tuple[float, float]: - start_point = [polygon_radius, 0] - return _apply_rotation(start_point, angle) - - def _get_angles(n_sides: int, rotation: float) -> list[float]: - angles = [] - degrees = 360 / n_sides - # Start with the bottom left polygon vertex - current_angle = (270 - 0.5 * degrees) + rotation - for _ in range(n_sides): - angles.append(current_angle) - current_angle += degrees - if current_angle > 360: - current_angle -= 360 - return angles - - # 3. Variable Declarations - angles = _get_angles(n_sides, rotation) - - # 4. Compute Vertices - return [_compute_polygon_vertex(angle) for angle in angles] - - -def _color_diff( - color1: float | tuple[int, ...], color2: float | tuple[int, ...] -) -> float: - """ - Uses 1-norm distance to calculate difference between two values. - """ - first = color1 if isinstance(color1, tuple) else (color1,) - second = color2 if isinstance(color2, tuple) else (color2,) - - return sum(abs(first[i] - second[i]) for i in range(len(second))) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageDraw2.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageDraw2.py deleted file mode 100644 index 2c9e39b..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageDraw2.py +++ /dev/null @@ -1,244 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# WCK-style drawing interface operations -# -# History: -# 2003-12-07 fl created -# 2005-05-15 fl updated; added to PIL as ImageDraw2 -# 2005-05-15 fl added text support -# 2005-05-20 fl added arc/chord/pieslice support -# -# Copyright (c) 2003-2005 by Secret Labs AB -# Copyright (c) 2003-2005 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - - -""" -(Experimental) WCK-style drawing interface operations - -.. seealso:: :py:mod:`PIL.ImageDraw` -""" - -from __future__ import annotations - -from typing import Any, AnyStr, BinaryIO - -from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath -from ._typing import Coords, StrOrBytesPath - - -class Pen: - """Stores an outline color and width.""" - - def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None: - self.color = ImageColor.getrgb(color) - self.width = width - - -class Brush: - """Stores a fill color""" - - def __init__(self, color: str, opacity: int = 255) -> None: - self.color = ImageColor.getrgb(color) - - -class Font: - """Stores a TrueType font and color""" - - def __init__( - self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12 - ) -> None: - # FIXME: add support for bitmap fonts - self.color = ImageColor.getrgb(color) - self.font = ImageFont.truetype(file, size) - - -class Draw: - """ - (Experimental) WCK-style drawing interface - """ - - def __init__( - self, - image: Image.Image | str, - size: tuple[int, int] | list[int] | None = None, - color: float | tuple[float, ...] | str | None = None, - ) -> None: - if isinstance(image, str): - if size is None: - msg = "If image argument is mode string, size must be a list or tuple" - raise ValueError(msg) - image = Image.new(image, size, color) - self.draw = ImageDraw.Draw(image) - self.image = image - self.transform: tuple[float, float, float, float, float, float] | None = None - - def flush(self) -> Image.Image: - return self.image - - def render( - self, - op: str, - xy: Coords, - pen: Pen | Brush | None, - brush: Brush | Pen | None = None, - **kwargs: Any, - ) -> None: - # handle color arguments - outline = fill = None - width = 1 - if isinstance(pen, Pen): - outline = pen.color - width = pen.width - elif isinstance(brush, Pen): - outline = brush.color - width = brush.width - if isinstance(brush, Brush): - fill = brush.color - elif isinstance(pen, Brush): - fill = pen.color - # handle transformation - if self.transform: - path = ImagePath.Path(xy) - path.transform(self.transform) - xy = path - # render the item - if op in ("arc", "line"): - kwargs.setdefault("fill", outline) - else: - kwargs.setdefault("fill", fill) - kwargs.setdefault("outline", outline) - if op == "line": - kwargs.setdefault("width", width) - getattr(self.draw, op)(xy, **kwargs) - - def settransform(self, offset: tuple[float, float]) -> None: - """Sets a transformation offset.""" - xoffset, yoffset = offset - self.transform = (1, 0, xoffset, 0, 1, yoffset) - - def arc( - self, - xy: Coords, - pen: Pen | Brush | None, - start: float, - end: float, - *options: Any, - ) -> None: - """ - Draws an arc (a portion of a circle outline) between the start and end - angles, inside the given bounding box. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` - """ - self.render("arc", xy, pen, *options, start=start, end=end) - - def chord( - self, - xy: Coords, - pen: Pen | Brush | None, - start: float, - end: float, - *options: Any, - ) -> None: - """ - Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points - with a straight line. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` - """ - self.render("chord", xy, pen, *options, start=start, end=end) - - def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: - """ - Draws an ellipse inside the given bounding box. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` - """ - self.render("ellipse", xy, pen, *options) - - def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: - """ - Draws a line between the coordinates in the ``xy`` list. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` - """ - self.render("line", xy, pen, *options) - - def pieslice( - self, - xy: Coords, - pen: Pen | Brush | None, - start: float, - end: float, - *options: Any, - ) -> None: - """ - Same as arc, but also draws straight lines between the end points and the - center of the bounding box. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` - """ - self.render("pieslice", xy, pen, *options, start=start, end=end) - - def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: - """ - Draws a polygon. - - The polygon outline consists of straight lines between the given - coordinates, plus a straight line between the last and the first - coordinate. - - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` - """ - self.render("polygon", xy, pen, *options) - - def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: - """ - Draws a rectangle. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` - """ - self.render("rectangle", xy, pen, *options) - - def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None: - """ - Draws the string at the given position. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` - """ - if self.transform: - path = ImagePath.Path(xy) - path.transform(self.transform) - xy = path - self.draw.text(xy, text, font=font.font, fill=font.color) - - def textbbox( - self, xy: tuple[float, float], text: AnyStr, font: Font - ) -> tuple[float, float, float, float]: - """ - Returns bounding box (in pixels) of given text. - - :return: ``(left, top, right, bottom)`` bounding box - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` - """ - if self.transform: - path = ImagePath.Path(xy) - path.transform(self.transform) - xy = path - return self.draw.textbbox(xy, text, font=font.font) - - def textlength(self, text: AnyStr, font: Font) -> float: - """ - Returns length (in pixels) of given text. - This is the amount by which following text should be offset. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength` - """ - return self.draw.textlength(text, font=font.font) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageEnhance.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageEnhance.py deleted file mode 100644 index 0e7e6dd..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageEnhance.py +++ /dev/null @@ -1,113 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# image enhancement classes -# -# For a background, see "Image Processing By Interpolation and -# Extrapolation", Paul Haeberli and Douglas Voorhies. Available -# at http://www.graficaobscura.com/interp/index.html -# -# History: -# 1996-03-23 fl Created -# 2009-06-16 fl Fixed mean calculation -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFilter, ImageStat - - -class _Enhance: - image: Image.Image - degenerate: Image.Image - - def enhance(self, factor: float) -> Image.Image: - """ - Returns an enhanced image. - - :param factor: A floating point value controlling the enhancement. - Factor 1.0 always returns a copy of the original image, - lower factors mean less color (brightness, contrast, - etc), and higher values more. There are no restrictions - on this value. - :rtype: :py:class:`~PIL.Image.Image` - """ - return Image.blend(self.degenerate, self.image, factor) - - -class Color(_Enhance): - """Adjust image color balance. - - This class can be used to adjust the colour balance of an image, in - a manner similar to the controls on a colour TV set. An enhancement - factor of 0.0 gives a black and white image. A factor of 1.0 gives - the original image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - self.intermediate_mode = "L" - if "A" in image.getbands(): - self.intermediate_mode = "LA" - - if self.intermediate_mode != image.mode: - image = image.convert(self.intermediate_mode).convert(image.mode) - self.degenerate = image - - -class Contrast(_Enhance): - """Adjust image contrast. - - This class can be used to control the contrast of an image, similar - to the contrast control on a TV set. An enhancement factor of 0.0 - gives a solid gray image. A factor of 1.0 gives the original image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - if image.mode != "L": - image = image.convert("L") - mean = int(ImageStat.Stat(image).mean[0] + 0.5) - self.degenerate = Image.new("L", image.size, mean) - if self.degenerate.mode != self.image.mode: - self.degenerate = self.degenerate.convert(self.image.mode) - - if "A" in self.image.getbands(): - self.degenerate.putalpha(self.image.getchannel("A")) - - -class Brightness(_Enhance): - """Adjust image brightness. - - This class can be used to control the brightness of an image. An - enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the - original image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - self.degenerate = Image.new(image.mode, image.size, 0) - - if "A" in image.getbands(): - self.degenerate.putalpha(image.getchannel("A")) - - -class Sharpness(_Enhance): - """Adjust image sharpness. - - This class can be used to adjust the sharpness of an image. An - enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the - original image, and a factor of 2.0 gives a sharpened image. - """ - - def __init__(self, image: Image.Image) -> None: - self.image = image - self.degenerate = image.filter(ImageFilter.SMOOTH) - - if "A" in image.getbands(): - self.degenerate.putalpha(image.getchannel("A")) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageFile.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageFile.py deleted file mode 100644 index c70d93f..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageFile.py +++ /dev/null @@ -1,935 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# base class for image file handlers -# -# history: -# 1995-09-09 fl Created -# 1996-03-11 fl Fixed load mechanism. -# 1996-04-15 fl Added pcx/xbm decoders. -# 1996-04-30 fl Added encoders. -# 1996-12-14 fl Added load helpers -# 1997-01-11 fl Use encode_to_file where possible -# 1997-08-27 fl Flush output in _save -# 1998-03-05 fl Use memory mapping for some modes -# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B" -# 1999-05-31 fl Added image parser -# 2000-10-12 fl Set readonly flag on memory-mapped images -# 2002-03-20 fl Use better messages for common decoder errors -# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available -# 2003-10-30 fl Added StubImageFile class -# 2004-02-25 fl Made incremental parser more robust -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1995-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import abc -import io -import itertools -import logging -import os -import struct -from typing import IO, Any, NamedTuple, cast - -from . import ExifTags, Image -from ._util import DeferredError, is_path - -TYPE_CHECKING = False -if TYPE_CHECKING: - from ._typing import StrOrBytesPath - -logger = logging.getLogger(__name__) - -MAXBLOCK = 65536 -""" -By default, Pillow processes image data in blocks. This helps to prevent excessive use -of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``. - -When reading an image, this is the number of bytes to read at once. - -When writing an image, this is the number of bytes to write at once. -If the image width times 4 is greater, then that will be used instead. -Plugins may also set a greater number. - -User code may set this to another number. -""" - -SAFEBLOCK = 1024 * 1024 - -LOAD_TRUNCATED_IMAGES = False -"""Whether or not to load truncated image files. User code may change this.""" - -ERRORS = { - -1: "image buffer overrun error", - -2: "decoding error", - -3: "unknown error", - -8: "bad configuration", - -9: "out of memory error", -} -""" -Dict of known error codes returned from :meth:`.PyDecoder.decode`, -:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and -:meth:`.PyEncoder.encode_to_file`. -""" - - -# -# -------------------------------------------------------------------- -# Helpers - - -def _get_oserror(error: int, *, encoder: bool) -> OSError: - try: - msg = Image.core.getcodecstatus(error) - except AttributeError: - msg = ERRORS.get(error) - if not msg: - msg = f"{'encoder' if encoder else 'decoder'} error {error}" - msg += f" when {'writing' if encoder else 'reading'} image file" - return OSError(msg) - - -def _tilesort(t: _Tile) -> int: - # sort on offset - return t[2] - - -class _Tile(NamedTuple): - codec_name: str - extents: tuple[int, int, int, int] | None - offset: int = 0 - args: tuple[Any, ...] | str | None = None - - -# -# -------------------------------------------------------------------- -# ImageFile base class - - -class ImageFile(Image.Image): - """Base class for image file format handlers.""" - - def __init__( - self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None - ) -> None: - super().__init__() - - self._min_frame = 0 - - self.custom_mimetype: str | None = None - - self.tile: list[_Tile] = [] - """ A list of tile descriptors """ - - self.readonly = 1 # until we know better - - self.decoderconfig: tuple[Any, ...] = () - self.decodermaxblock = MAXBLOCK - - self.fp: IO[bytes] | None - self._fp: IO[bytes] | DeferredError - if is_path(fp): - # filename - self.fp = open(fp, "rb") - self.filename = os.fspath(fp) - self._exclusive_fp = True - else: - # stream - self.fp = cast(IO[bytes], fp) - self.filename = filename if filename is not None else "" - # can be overridden - self._exclusive_fp = False - - try: - try: - self._open() - - if isinstance(self, StubImageFile): - if loader := self._load(): - loader.open(self) - except ( - IndexError, # end of data - TypeError, # end of data (ord) - KeyError, # unsupported mode - EOFError, # got header but not the first frame - struct.error, - ) as v: - raise SyntaxError(v) from v - - if not self.mode or self.size[0] <= 0 or self.size[1] <= 0: - msg = "not identified by this driver" - raise SyntaxError(msg) - except BaseException: - # close the file only if we have opened it this constructor - if self._exclusive_fp: - self.fp.close() - raise - - def _open(self) -> None: - pass - - # Context manager support - def __enter__(self) -> ImageFile: - return self - - def _close_fp(self) -> None: - if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() - - def __exit__(self, *args: object) -> None: - if getattr(self, "_exclusive_fp", False): - self._close_fp() - self.fp = None - - def close(self) -> None: - """ - Closes the file pointer, if possible. - - This operation will destroy the image core and release its memory. - The image data will be unusable afterward. - - This function is required to close images that have multiple frames or - have not had their file read and closed by the - :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for - more information. - """ - try: - self._close_fp() - self.fp = None - except Exception as msg: - logger.debug("Error closing: %s", msg) - - super().close() - - def get_child_images(self) -> list[ImageFile]: - child_images = [] - exif = self.getexif() - ifds = [] - if ExifTags.Base.SubIFDs in exif: - subifd_offsets = exif[ExifTags.Base.SubIFDs] - if subifd_offsets: - if not isinstance(subifd_offsets, tuple): - subifd_offsets = (subifd_offsets,) - ifds = [ - (exif._get_ifd_dict(subifd_offset), subifd_offset) - for subifd_offset in subifd_offsets - ] - ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) - if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): - assert exif._info is not None - ifds.append((ifd1, exif._info.next)) - - offset = None - for ifd, ifd_offset in ifds: - assert self.fp is not None - current_offset = self.fp.tell() - if offset is None: - offset = current_offset - - fp = self.fp - if ifd is not None: - thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) - if thumbnail_offset is not None: - thumbnail_offset += getattr(self, "_exif_offset", 0) - self.fp.seek(thumbnail_offset) - - length = ifd.get(ExifTags.Base.JpegIFByteCount) - assert isinstance(length, int) - data = self.fp.read(length) - fp = io.BytesIO(data) - - with Image.open(fp) as im: - from . import TiffImagePlugin - - if thumbnail_offset is None and isinstance( - im, TiffImagePlugin.TiffImageFile - ): - im._frame_pos = [ifd_offset] - im._seek(0) - im.load() - child_images.append(im) - - if offset is not None: - assert self.fp is not None - self.fp.seek(offset) - return child_images - - def get_format_mimetype(self) -> str | None: - if self.custom_mimetype: - return self.custom_mimetype - if self.format is not None: - return Image.MIME.get(self.format.upper()) - return None - - def __getstate__(self) -> list[Any]: - return super().__getstate__() + [self.filename] - - def __setstate__(self, state: list[Any]) -> None: - self.tile = [] - if len(state) > 5: - self.filename = state[5] - super().__setstate__(state) - - def verify(self) -> None: - """Check file integrity""" - - # raise exception if something's wrong. must be called - # directly after open, and closes file when finished. - if self._exclusive_fp and self.fp: - self.fp.close() - self.fp = None - - def load(self) -> Image.core.PixelAccess | None: - """Load image data based on tile list""" - - if not self.tile and self._im is None: - msg = "cannot load this image" - raise OSError(msg) - - pixel = Image.Image.load(self) - if not self.tile: - return pixel - - self.map: mmap.mmap | None = None - use_mmap = self.filename and len(self.tile) == 1 - - assert self.fp is not None - readonly = 0 - - # look for read/seek overrides - if hasattr(self, "load_read"): - read = self.load_read - # don't use mmap if there are custom read/seek functions - use_mmap = False - else: - read = self.fp.read - - if hasattr(self, "load_seek"): - seek = self.load_seek - use_mmap = False - else: - seek = self.fp.seek - - if use_mmap: - # try memory mapping - decoder_name, extents, offset, args = self.tile[0] - if isinstance(args, str): - args = (args, 0, 1) - if ( - decoder_name == "raw" - and isinstance(args, tuple) - and len(args) >= 3 - and args[0] == self.mode - and args[0] in Image._MAPMODES - ): - if offset < 0: - msg = "Tile offset cannot be negative" - raise ValueError(msg) - try: - # use mmap, if possible - import mmap - - with open(self.filename) as fp: - self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) - if offset + self.size[1] * args[1] > self.map.size(): - msg = "buffer is not large enough" - raise OSError(msg) - self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, offset, args - ) - readonly = 1 - # After trashing self.im, - # we might need to reload the palette data. - if self.palette: - self.palette.dirty = 1 - except (AttributeError, OSError, ImportError): - self.map = None - - self.load_prepare() - err_code = -3 # initialize to unknown error - if not self.map: - # sort tiles in file order - self.tile.sort(key=_tilesort) - - # FIXME: This is a hack to handle TIFF's JpegTables tag. - prefix = getattr(self, "tile_prefix", b"") - - # Remove consecutive duplicates that only differ by their offset - self.tile = [ - list(tiles)[-1] - for _, tiles in itertools.groupby( - self.tile, lambda tile: (tile[0], tile[1], tile[3]) - ) - ] - for i, (decoder_name, extents, offset, args) in enumerate(self.tile): - seek(offset) - decoder = Image._getdecoder( - self.mode, decoder_name, args, self.decoderconfig - ) - try: - decoder.setimage(self.im, extents) - if decoder.pulls_fd: - decoder.setfd(self.fp) - err_code = decoder.decode(b"")[1] - else: - b = prefix - while True: - read_bytes = self.decodermaxblock - if i + 1 < len(self.tile): - next_offset = self.tile[i + 1].offset - if next_offset > offset: - read_bytes = next_offset - offset - try: - s = read(read_bytes) - except (IndexError, struct.error) as e: - # truncated png/gif - if LOAD_TRUNCATED_IMAGES: - break - else: - msg = "image file is truncated" - raise OSError(msg) from e - - if not s: # truncated jpeg - if LOAD_TRUNCATED_IMAGES: - break - else: - msg = ( - "image file is truncated " - f"({len(b)} bytes not processed)" - ) - raise OSError(msg) - - b = b + s - n, err_code = decoder.decode(b) - if n < 0: - break - b = b[n:] - finally: - # Need to cleanup here to prevent leaks - decoder.cleanup() - - self.tile = [] - self.readonly = readonly - - self.load_end() - - if self._exclusive_fp and self._close_exclusive_fp_after_loading: - self.fp.close() - self.fp = None - - if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0: - # still raised if decoder fails to return anything - raise _get_oserror(err_code, encoder=False) - - return Image.Image.load(self) - - def load_prepare(self) -> None: - # create image memory if necessary - if self._im is None: - self.im = Image.core.new(self.mode, self.size) - # create palette (optional) - if self.mode == "P": - Image.Image.load(self) - - def load_end(self) -> None: - # may be overridden - pass - - # may be defined for contained formats - # def load_seek(self, pos: int) -> None: - # pass - - # may be defined for blocked formats (e.g. PNG) - # def load_read(self, read_bytes: int) -> bytes: - # pass - - def _seek_check(self, frame: int) -> bool: - if ( - frame < self._min_frame - # Only check upper limit on frames if additional seek operations - # are not required to do so - or ( - not (hasattr(self, "_n_frames") and self._n_frames is None) - and frame >= getattr(self, "n_frames") + self._min_frame - ) - ): - msg = "attempt to seek outside sequence" - raise EOFError(msg) - - return self.tell() != frame - - -class StubHandler(abc.ABC): - def open(self, im: StubImageFile) -> None: - pass - - @abc.abstractmethod - def load(self, im: StubImageFile) -> Image.Image: - pass - - -class StubImageFile(ImageFile, metaclass=abc.ABCMeta): - """ - Base class for stub image loaders. - - A stub loader is an image loader that can identify files of a - certain format, but relies on external code to load the file. - """ - - @abc.abstractmethod - def _open(self) -> None: - pass - - def load(self) -> Image.core.PixelAccess | None: - loader = self._load() - if loader is None: - msg = f"cannot find loader for this {self.format} file" - raise OSError(msg) - image = loader.load(self) - assert image is not None - # become the other object (!) - self.__class__ = image.__class__ # type: ignore[assignment] - self.__dict__ = image.__dict__ - return image.load() - - @abc.abstractmethod - def _load(self) -> StubHandler | None: - """(Hook) Find actual image loader.""" - pass - - -class Parser: - """ - Incremental image parser. This class implements the standard - feed/close consumer interface. - """ - - incremental = None - image: Image.Image | None = None - data: bytes | None = None - decoder: Image.core.ImagingDecoder | PyDecoder | None = None - offset = 0 - finished = 0 - - def reset(self) -> None: - """ - (Consumer) Reset the parser. Note that you can only call this - method immediately after you've created a parser; parser - instances cannot be reused. - """ - assert self.data is None, "cannot reuse parsers" - - def feed(self, data: bytes) -> None: - """ - (Consumer) Feed data to the parser. - - :param data: A string buffer. - :exception OSError: If the parser failed to parse the image file. - """ - # collect data - - if self.finished: - return - - if self.data is None: - self.data = data - else: - self.data = self.data + data - - # parse what we have - if self.decoder: - if self.offset > 0: - # skip header - skip = min(len(self.data), self.offset) - self.data = self.data[skip:] - self.offset = self.offset - skip - if self.offset > 0 or not self.data: - return - - n, e = self.decoder.decode(self.data) - - if n < 0: - # end of stream - self.data = None - self.finished = 1 - if e < 0: - # decoding error - self.image = None - raise _get_oserror(e, encoder=False) - else: - # end of image - return - self.data = self.data[n:] - - elif self.image: - # if we end up here with no decoder, this file cannot - # be incrementally parsed. wait until we've gotten all - # available data - pass - - else: - # attempt to open this file - try: - with io.BytesIO(self.data) as fp: - im = Image.open(fp) - except OSError: - pass # not enough data - else: - flag = hasattr(im, "load_seek") or hasattr(im, "load_read") - if not flag and len(im.tile) == 1: - # initialize decoder - im.load_prepare() - d, e, o, a = im.tile[0] - im.tile = [] - self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig) - self.decoder.setimage(im.im, e) - - # calculate decoder offset - self.offset = o - if self.offset <= len(self.data): - self.data = self.data[self.offset :] - self.offset = 0 - - self.image = im - - def __enter__(self) -> Parser: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self) -> Image.Image: - """ - (Consumer) Close the stream. - - :returns: An image object. - :exception OSError: If the parser failed to parse the image file either - because it cannot be identified or cannot be - decoded. - """ - # finish decoding - if self.decoder: - # get rid of what's left in the buffers - self.feed(b"") - self.data = self.decoder = None - if not self.finished: - msg = "image was incomplete" - raise OSError(msg) - if not self.image: - msg = "cannot parse this image" - raise OSError(msg) - if self.data: - # incremental parsing not possible; reopen the file - # not that we have all data - with io.BytesIO(self.data) as fp: - try: - self.image = Image.open(fp) - finally: - self.image.load() - return self.image - - -# -------------------------------------------------------------------- - - -def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None: - """Helper to save image based on tile list - - :param im: Image object. - :param fp: File object. - :param tile: Tile list. - :param bufsize: Optional buffer size - """ - - im.load() - if not hasattr(im, "encoderconfig"): - im.encoderconfig = () - tile.sort(key=_tilesort) - # FIXME: make MAXBLOCK a configuration parameter - # It would be great if we could have the encoder specify what it needs - # But, it would need at least the image size in most cases. RawEncode is - # a tricky case. - bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c - try: - fh = fp.fileno() - fp.flush() - _encode_tile(im, fp, tile, bufsize, fh) - except (AttributeError, io.UnsupportedOperation) as exc: - _encode_tile(im, fp, tile, bufsize, None, exc) - if hasattr(fp, "flush"): - fp.flush() - - -def _encode_tile( - im: Image.Image, - fp: IO[bytes], - tile: list[_Tile], - bufsize: int, - fh: int | None, - exc: BaseException | None = None, -) -> None: - for encoder_name, extents, offset, args in tile: - if offset > 0: - fp.seek(offset) - encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig) - try: - encoder.setimage(im.im, extents) - if encoder.pushes_fd: - encoder.setfd(fp) - errcode = encoder.encode_to_pyfd()[1] - else: - if exc: - # compress to Python file-compatible object - while True: - errcode, data = encoder.encode(bufsize)[1:] - fp.write(data) - if errcode: - break - else: - # slight speedup: compress to real file object - assert fh is not None - errcode = encoder.encode_to_file(fh, bufsize) - if errcode < 0: - raise _get_oserror(errcode, encoder=True) from exc - finally: - encoder.cleanup() - - -def _safe_read(fp: IO[bytes], size: int) -> bytes: - """ - Reads large blocks in a safe way. Unlike fp.read(n), this function - doesn't trust the user. If the requested size is larger than - SAFEBLOCK, the file is read block by block. - - :param fp: File handle. Must implement a <b>read</b> method. - :param size: Number of bytes to read. - :returns: A string containing <i>size</i> bytes of data. - - Raises an OSError if the file is truncated and the read cannot be completed - - """ - if size <= 0: - return b"" - if size <= SAFEBLOCK: - data = fp.read(size) - if len(data) < size: - msg = "Truncated File Read" - raise OSError(msg) - return data - blocks: list[bytes] = [] - remaining_size = size - while remaining_size > 0: - block = fp.read(min(remaining_size, SAFEBLOCK)) - if not block: - break - blocks.append(block) - remaining_size -= len(block) - if sum(len(block) for block in blocks) < size: - msg = "Truncated File Read" - raise OSError(msg) - return b"".join(blocks) - - -class PyCodecState: - def __init__(self) -> None: - self.xsize = 0 - self.ysize = 0 - self.xoff = 0 - self.yoff = 0 - - def extents(self) -> tuple[int, int, int, int]: - return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize - - -class PyCodec: - fd: IO[bytes] | None - - def __init__(self, mode: str, *args: Any) -> None: - self.im: Image.core.ImagingCore | None = None - self.state = PyCodecState() - self.fd = None - self.mode = mode - self.init(args) - - def init(self, args: tuple[Any, ...]) -> None: - """ - Override to perform codec specific initialization - - :param args: Tuple of arg items from the tile entry - :returns: None - """ - self.args = args - - def cleanup(self) -> None: - """ - Override to perform codec specific cleanup - - :returns: None - """ - pass - - def setfd(self, fd: IO[bytes]) -> None: - """ - Called from ImageFile to set the Python file-like object - - :param fd: A Python file-like object - :returns: None - """ - self.fd = fd - - def setimage( - self, - im: Image.core.ImagingCore, - extents: tuple[int, int, int, int] | None = None, - ) -> None: - """ - Called from ImageFile to set the core output image for the codec - - :param im: A core image object - :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle - for this tile - :returns: None - """ - - # following c code - self.im = im - - if extents: - x0, y0, x1, y1 = extents - - if x0 < 0 or y0 < 0 or x1 > self.im.size[0] or y1 > self.im.size[1]: - msg = "Tile cannot extend outside image" - raise ValueError(msg) - - self.state.xoff = x0 - self.state.yoff = y0 - self.state.xsize = x1 - x0 - self.state.ysize = y1 - y0 - else: - self.state.xsize, self.state.ysize = self.im.size - - if self.state.xsize <= 0 or self.state.ysize <= 0: - msg = "Size must be positive" - raise ValueError(msg) - - -class PyDecoder(PyCodec): - """ - Python implementation of a format decoder. Override this class and - add the decoding logic in the :meth:`decode` method. - - See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` - """ - - _pulls_fd = False - - @property - def pulls_fd(self) -> bool: - return self._pulls_fd - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - """ - Override to perform the decoding process. - - :param buffer: A bytes object with the data to be decoded. - :returns: A tuple of ``(bytes consumed, errcode)``. - If finished with decoding return -1 for the bytes consumed. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - msg = "unavailable in base decoder" - raise NotImplementedError(msg) - - def set_as_raw( - self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = () - ) -> None: - """ - Convenience method to set the internal image from a stream of raw data - - :param data: Bytes to be set - :param rawmode: The rawmode to be used for the decoder. - If not specified, it will default to the mode of the image - :param extra: Extra arguments for the decoder. - :returns: None - """ - - if not rawmode: - rawmode = self.mode - d = Image._getdecoder(self.mode, "raw", rawmode, extra) - assert self.im is not None - d.setimage(self.im, self.state.extents()) - s = d.decode(data) - - if s[0] >= 0: - msg = "not enough image data" - raise ValueError(msg) - if s[1] != 0: - msg = "cannot decode image data" - raise ValueError(msg) - - -class PyEncoder(PyCodec): - """ - Python implementation of a format encoder. Override this class and - add the decoding logic in the :meth:`encode` method. - - See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` - """ - - _pushes_fd = False - - @property - def pushes_fd(self) -> bool: - return self._pushes_fd - - def encode(self, bufsize: int) -> tuple[int, int, bytes]: - """ - Override to perform the encoding process. - - :param bufsize: Buffer size. - :returns: A tuple of ``(bytes encoded, errcode, bytes)``. - If finished with encoding return 1 for the error code. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - msg = "unavailable in base encoder" - raise NotImplementedError(msg) - - def encode_to_pyfd(self) -> tuple[int, int]: - """ - If ``pushes_fd`` is ``True``, then this method will be used, - and ``encode()`` will only be called once. - - :returns: A tuple of ``(bytes consumed, errcode)``. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - if not self.pushes_fd: - return 0, -8 # bad configuration - bytes_consumed, errcode, data = self.encode(0) - if data: - assert self.fd is not None - self.fd.write(data) - return bytes_consumed, errcode - - def encode_to_file(self, fh: int, bufsize: int) -> int: - """ - :param fh: File handle. - :param bufsize: Buffer size. - - :returns: If finished successfully, return 0. - Otherwise, return an error code. Err codes are from - :data:`.ImageFile.ERRORS`. - """ - errcode = 0 - while errcode == 0: - status, errcode, buf = self.encode(bufsize) - if status > 0: - os.write(fh, buf[status:]) - return errcode diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageFilter.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageFilter.py deleted file mode 100644 index 9326eee..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageFilter.py +++ /dev/null @@ -1,607 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard filters -# -# History: -# 1995-11-27 fl Created -# 2002-06-08 fl Added rank and mode filters -# 2003-09-15 fl Fixed rank calculation in rank filter; added expand call -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2002 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import abc -import functools -from collections.abc import Sequence -from typing import cast - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable - from types import ModuleType - from typing import Any - - from . import _imaging - from ._typing import NumpyArray - - -class Filter(abc.ABC): - @abc.abstractmethod - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - pass - - -class MultibandFilter(Filter): - pass - - -class BuiltinFilter(MultibandFilter): - filterargs: tuple[Any, ...] - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - if image.mode == "P": - msg = "cannot filter palette images" - raise ValueError(msg) - return image.filter(*self.filterargs) - - -class Kernel(BuiltinFilter): - """ - Create a convolution kernel. This only supports 3x3 and 5x5 integer and floating - point kernels. - - Kernels can only be applied to "L" and "RGB" images. - - :param size: Kernel size, given as (width, height). This must be (3,3) or (5,5). - :param kernel: A sequence containing kernel weights. The kernel will be flipped - vertically before being applied to the image. - :param scale: Scale factor. If given, the result for each pixel is divided by this - value. The default is the sum of the kernel weights. - :param offset: Offset. If given, this value is added to the result, after it has - been divided by the scale factor. - """ - - name = "Kernel" - - def __init__( - self, - size: tuple[int, int], - kernel: Sequence[float], - scale: float | None = None, - offset: float = 0, - ) -> None: - if scale is None: - # default scale is sum of kernel - scale = functools.reduce(lambda a, b: a + b, kernel) - if size[0] * size[1] != len(kernel): - msg = "not enough coefficients in kernel" - raise ValueError(msg) - self.filterargs = size, scale, offset, kernel - - -class RankFilter(Filter): - """ - Create a rank filter. The rank filter sorts all pixels in - a window of the given size, and returns the ``rank``'th value. - - :param size: The kernel size, in pixels. - :param rank: What pixel value to pick. Use 0 for a min filter, - ``size * size / 2`` for a median filter, ``size * size - 1`` - for a max filter, etc. - """ - - name = "Rank" - - def __init__(self, size: int, rank: int) -> None: - self.size = size - self.rank = rank - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - if image.mode == "P": - msg = "cannot filter palette images" - raise ValueError(msg) - image = image.expand(self.size // 2, self.size // 2) - return image.rankfilter(self.size, self.rank) - - -class MedianFilter(RankFilter): - """ - Create a median filter. Picks the median pixel value in a window with the - given size. - - :param size: The kernel size, in pixels. - """ - - name = "Median" - - def __init__(self, size: int = 3) -> None: - self.size = size - self.rank = size * size // 2 - - -class MinFilter(RankFilter): - """ - Create a min filter. Picks the lowest pixel value in a window with the - given size. - - :param size: The kernel size, in pixels. - """ - - name = "Min" - - def __init__(self, size: int = 3) -> None: - self.size = size - self.rank = 0 - - -class MaxFilter(RankFilter): - """ - Create a max filter. Picks the largest pixel value in a window with the - given size. - - :param size: The kernel size, in pixels. - """ - - name = "Max" - - def __init__(self, size: int = 3) -> None: - self.size = size - self.rank = size * size - 1 - - -class ModeFilter(Filter): - """ - Create a mode filter. Picks the most frequent pixel value in a box with the - given size. Pixel values that occur only once or twice are ignored; if no - pixel value occurs more than twice, the original pixel value is preserved. - - :param size: The kernel size, in pixels. - """ - - name = "Mode" - - def __init__(self, size: int = 3) -> None: - self.size = size - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - return image.modefilter(self.size) - - -class GaussianBlur(MultibandFilter): - """Blurs the image with a sequence of extended box filters, which - approximates a Gaussian kernel. For details on accuracy see - <https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf> - - :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two - numbers for x and y, or a single number for both. - """ - - name = "GaussianBlur" - - def __init__(self, radius: float | Sequence[float] = 2) -> None: - self.radius = radius - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - xy = self.radius - if isinstance(xy, (int, float)): - xy = (xy, xy) - if xy == (0, 0): - return image.copy() - return image.gaussian_blur(xy) - - -class BoxBlur(MultibandFilter): - """Blurs the image by setting each pixel to the average value of the pixels - in a square box extending radius pixels in each direction. - Supports float radius of arbitrary size. Uses an optimized implementation - which runs in linear time relative to the size of the image - for any radius value. - - :param radius: Size of the box in a direction. Either a sequence of two numbers for - x and y, or a single number for both. - - Radius 0 does not blur, returns an identical image. - Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. - """ - - name = "BoxBlur" - - def __init__(self, radius: float | Sequence[float]) -> None: - xy = radius if isinstance(radius, (tuple, list)) else (radius, radius) - if xy[0] < 0 or xy[1] < 0: - msg = "radius must be >= 0" - raise ValueError(msg) - self.radius = radius - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - xy = self.radius - if isinstance(xy, (int, float)): - xy = (xy, xy) - if xy == (0, 0): - return image.copy() - return image.box_blur(xy) - - -class UnsharpMask(MultibandFilter): - """Unsharp mask filter. - - See Wikipedia's entry on `digital unsharp masking`_ for an explanation of - the parameters. - - :param radius: Blur Radius - :param percent: Unsharp strength, in percent - :param threshold: Threshold controls the minimum brightness change that - will be sharpened - - .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking - - """ - - name = "UnsharpMask" - - def __init__( - self, radius: float = 2, percent: int = 150, threshold: int = 3 - ) -> None: - self.radius = radius - self.percent = percent - self.threshold = threshold - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - return image.unsharp_mask(self.radius, self.percent, self.threshold) - - -class BLUR(BuiltinFilter): - name = "Blur" - # fmt: off - filterargs = (5, 5), 16, 0, ( - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - ) - # fmt: on - - -class CONTOUR(BuiltinFilter): - name = "Contour" - # fmt: off - filterargs = (3, 3), 1, 255, ( - -1, -1, -1, - -1, 8, -1, - -1, -1, -1, - ) - # fmt: on - - -class DETAIL(BuiltinFilter): - name = "Detail" - # fmt: off - filterargs = (3, 3), 6, 0, ( - 0, -1, 0, - -1, 10, -1, - 0, -1, 0, - ) - # fmt: on - - -class EDGE_ENHANCE(BuiltinFilter): - name = "Edge-enhance" - # fmt: off - filterargs = (3, 3), 2, 0, ( - -1, -1, -1, - -1, 10, -1, - -1, -1, -1, - ) - # fmt: on - - -class EDGE_ENHANCE_MORE(BuiltinFilter): - name = "Edge-enhance More" - # fmt: off - filterargs = (3, 3), 1, 0, ( - -1, -1, -1, - -1, 9, -1, - -1, -1, -1, - ) - # fmt: on - - -class EMBOSS(BuiltinFilter): - name = "Emboss" - # fmt: off - filterargs = (3, 3), 1, 128, ( - -1, 0, 0, - 0, 1, 0, - 0, 0, 0, - ) - # fmt: on - - -class FIND_EDGES(BuiltinFilter): - name = "Find Edges" - # fmt: off - filterargs = (3, 3), 1, 0, ( - -1, -1, -1, - -1, 8, -1, - -1, -1, -1, - ) - # fmt: on - - -class SHARPEN(BuiltinFilter): - name = "Sharpen" - # fmt: off - filterargs = (3, 3), 16, 0, ( - -2, -2, -2, - -2, 32, -2, - -2, -2, -2, - ) - # fmt: on - - -class SMOOTH(BuiltinFilter): - name = "Smooth" - # fmt: off - filterargs = (3, 3), 13, 0, ( - 1, 1, 1, - 1, 5, 1, - 1, 1, 1, - ) - # fmt: on - - -class SMOOTH_MORE(BuiltinFilter): - name = "Smooth More" - # fmt: off - filterargs = (5, 5), 100, 0, ( - 1, 1, 1, 1, 1, - 1, 5, 5, 5, 1, - 1, 5, 44, 5, 1, - 1, 5, 5, 5, 1, - 1, 1, 1, 1, 1, - ) - # fmt: on - - -class Color3DLUT(MultibandFilter): - """Three-dimensional color lookup table. - - Transforms 3-channel pixels using the values of the channels as coordinates - in the 3D lookup table and interpolating the nearest elements. - - This method allows you to apply almost any color transformation - in constant time by using pre-calculated decimated tables. - - .. versionadded:: 5.2.0 - - :param size: Size of the table. One int or tuple of (int, int, int). - Minimal size in any dimension is 2, maximum is 65. - :param table: Flat lookup table. A list of ``channels * size**3`` - float elements or a list of ``size**3`` channels-sized - tuples with floats. Channels are changed first, - then first dimension, then second, then third. - Value 0.0 corresponds lowest value of output, 1.0 highest. - :param channels: Number of channels in the table. Could be 3 or 4. - Default is 3. - :param target_mode: A mode for the result image. Should have not less - than ``channels`` channels. Default is ``None``, - which means that mode wouldn't be changed. - """ - - name = "Color 3D LUT" - - def __init__( - self, - size: int | tuple[int, int, int], - table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray, - channels: int = 3, - target_mode: str | None = None, - **kwargs: bool, - ) -> None: - if channels not in (3, 4): - msg = "Only 3 or 4 output channels are supported" - raise ValueError(msg) - self.size = size = self._check_size(size) - self.channels = channels - self.mode = target_mode - - # Hidden flag `_copy_table=False` could be used to avoid extra copying - # of the table if the table is specially made for the constructor. - copy_table = kwargs.get("_copy_table", True) - items = size[0] * size[1] * size[2] - wrong_size = False - - numpy: ModuleType | None = None - if hasattr(table, "shape"): - try: - import numpy - except ImportError: - pass - - if numpy and isinstance(table, numpy.ndarray): - numpy_table: NumpyArray = table - if copy_table: - numpy_table = numpy_table.copy() - - if numpy_table.shape in [ - (items * channels,), - (items, channels), - (size[2], size[1], size[0], channels), - ]: - table = numpy_table.reshape(items * channels) - else: - wrong_size = True - - else: - if copy_table: - table = list(table) - - # Convert to a flat list - if table and isinstance(table[0], (list, tuple)): - raw_table = cast(Sequence[Sequence[int]], table) - flat_table: list[int] = [] - for pixel in raw_table: - if len(pixel) != channels: - msg = ( - "The elements of the table should " - f"have a length of {channels}." - ) - raise ValueError(msg) - flat_table.extend(pixel) - table = flat_table - - if wrong_size or len(table) != items * channels: - msg = ( - "The table should have either channels * size**3 float items " - "or size**3 items of channels-sized tuples with floats. " - f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. " - f"Actual length: {len(table)}" - ) - raise ValueError(msg) - self.table = table - - @staticmethod - def _check_size(size: Any) -> tuple[int, int, int]: - try: - _, _, _ = size - except ValueError as e: - msg = "Size should be either an integer or a tuple of three integers." - raise ValueError(msg) from e - except TypeError: - size = (size, size, size) - size = tuple(int(x) for x in size) - for size_1d in size: - if not 2 <= size_1d <= 65: - msg = "Size should be in [2, 65] range." - raise ValueError(msg) - return size - - @classmethod - def generate( - cls, - size: int | tuple[int, int, int], - callback: Callable[[float, float, float], tuple[float, ...]], - channels: int = 3, - target_mode: str | None = None, - ) -> Color3DLUT: - """Generates new LUT using provided callback. - - :param size: Size of the table. Passed to the constructor. - :param callback: Function with three parameters which correspond - three color channels. Will be called ``size**3`` - times with values from 0.0 to 1.0 and should return - a tuple with ``channels`` elements. - :param channels: The number of channels which should return callback. - :param target_mode: Passed to the constructor of the resulting - lookup table. - """ - size_1d, size_2d, size_3d = cls._check_size(size) - if channels not in (3, 4): - msg = "Only 3 or 4 output channels are supported" - raise ValueError(msg) - - table: list[float] = [0] * (size_1d * size_2d * size_3d * channels) - idx_out = 0 - for b in range(size_3d): - for g in range(size_2d): - for r in range(size_1d): - table[idx_out : idx_out + channels] = callback( - r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1) - ) - idx_out += channels - - return cls( - (size_1d, size_2d, size_3d), - table, - channels=channels, - target_mode=target_mode, - _copy_table=False, - ) - - def transform( - self, - callback: Callable[..., tuple[float, ...]], - with_normals: bool = False, - channels: int | None = None, - target_mode: str | None = None, - ) -> Color3DLUT: - """Transforms the table values using provided callback and returns - a new LUT with altered values. - - :param callback: A function which takes old lookup table values - and returns a new set of values. The number - of arguments which function should take is - ``self.channels`` or ``3 + self.channels`` - if ``with_normals`` flag is set. - Should return a tuple of ``self.channels`` or - ``channels`` elements if it is set. - :param with_normals: If true, ``callback`` will be called with - coordinates in the color cube as the first - three arguments. Otherwise, ``callback`` - will be called only with actual color values. - :param channels: The number of channels in the resulting lookup table. - :param target_mode: Passed to the constructor of the resulting - lookup table. - """ - if channels not in (None, 3, 4): - msg = "Only 3 or 4 output channels are supported" - raise ValueError(msg) - ch_in = self.channels - ch_out = channels or ch_in - size_1d, size_2d, size_3d = self.size - - table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out) - idx_in = 0 - idx_out = 0 - for b in range(size_3d): - for g in range(size_2d): - for r in range(size_1d): - values = self.table[idx_in : idx_in + ch_in] - if with_normals: - values = callback( - r / (size_1d - 1), - g / (size_2d - 1), - b / (size_3d - 1), - *values, - ) - else: - values = callback(*values) - table[idx_out : idx_out + ch_out] = values - idx_in += ch_in - idx_out += ch_out - - return type(self)( - self.size, - table, - channels=ch_out, - target_mode=target_mode or self.mode, - _copy_table=False, - ) - - def __repr__(self) -> str: - r = [ - f"{self.__class__.__name__} from {self.table.__class__.__name__}", - "size={:d}x{:d}x{:d}".format(*self.size), - f"channels={self.channels:d}", - ] - if self.mode: - r.append(f"target_mode={self.mode}") - return "<{}>".format(" ".join(r)) - - def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: - from . import Image - - return image.color_lut_3d( - self.mode or image.mode, - Image.Resampling.BILINEAR, - self.channels, - self.size, - self.table, - ) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageFont.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageFont.py deleted file mode 100644 index 06ea035..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageFont.py +++ /dev/null @@ -1,1309 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PIL raster font management -# -# History: -# 1996-08-07 fl created (experimental) -# 1997-08-25 fl minor adjustments to handle fonts from pilfont 0.3 -# 1999-02-06 fl rewrote most font management stuff in C -# 1999-03-17 fl take pth files into account in load_path (from Richard Jones) -# 2001-02-17 fl added freetype support -# 2001-05-09 fl added TransposedFont wrapper class -# 2002-03-04 fl make sure we have a "L" or "1" font -# 2002-12-04 fl skip non-directory entries in the system path -# 2003-04-29 fl add embedded default font -# 2003-09-27 fl added support for truetype charmap encodings -# -# Todo: -# Adapt to PILFONT2 format (16-bit fonts, compressed, single file) -# -# Copyright (c) 1997-2003 by Secret Labs AB -# Copyright (c) 1996-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from __future__ import annotations - -import base64 -import os -import sys -import warnings -from enum import IntEnum -from io import BytesIO -from types import ModuleType -from typing import IO, Any, BinaryIO, TypedDict, cast - -from . import Image -from ._typing import StrOrBytesPath -from ._util import DeferredError, is_path - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import ImageFile - from ._imaging import ImagingFont - from ._imagingft import Font - - -class Axis(TypedDict): - minimum: int | None - default: int | None - maximum: int | None - name: bytes | None - - -class Layout(IntEnum): - BASIC = 0 - RAQM = 1 - - -MAX_STRING_LENGTH = 1_000_000 - - -core: ModuleType | DeferredError -try: - from . import _imagingft as core -except ImportError as ex: - core = DeferredError.new(ex) - - -def _string_length_check(text: str | bytes | bytearray) -> None: - if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: - msg = "too many characters in string" - raise ValueError(msg) - - -# FIXME: add support for pilfont2 format (see FontFile.py) - -# -------------------------------------------------------------------- -# Font metrics format: -# "PILfont" LF -# fontdescriptor LF -# (optional) key=value... LF -# "DATA" LF -# binary data: 256*10*2 bytes (dx, dy, dstbox, srcbox) -# -# To place a character, cut out srcbox and paste at dstbox, -# relative to the character position. Then move the character -# position according to dx, dy. -# -------------------------------------------------------------------- - - -class ImageFont: - """PIL font wrapper""" - - font: ImagingFont - - def _load_pilfont(self, filename: str) -> None: - with open(filename, "rb") as fp: - image: ImageFile.ImageFile | None = None - root = os.path.splitext(filename)[0] - - for ext in (".png", ".gif", ".pbm"): - if image: - image.close() - try: - fullname = root + ext - image = Image.open(fullname) - except Exception: - pass - else: - if image.mode in ("1", "L"): - break - else: - if image: - image.close() - - msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}" - raise OSError(msg) - - self.file = fullname - - self._load_pilfont_data(fp, image) - image.close() - - def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: - # check image - if image.mode not in ("1", "L"): - image.close() - - msg = "invalid font image mode" - raise TypeError(msg) - - # read PILfont header - if file.read(8) != b"PILfont\n": - image.close() - - msg = "Not a PILfont file" - raise SyntaxError(msg) - file.readline() - self.info = [] # FIXME: should be a dictionary - while True: - s = file.readline() - if not s or s == b"DATA\n": - break - self.info.append(s) - - # read PILfont metrics - data = file.read(256 * 20) - - self._load(image, data) - - def _load(self, image: Image.Image, data: bytes) -> None: - image.load() - - self.font = Image.core.font(image.im, data) - - def getmask( - self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any - ) -> Image.core.ImagingCore: - """ - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. Otherwise, it should have mode ``1``. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :return: An internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module. - """ - _string_length_check(text) - Image._decompression_bomb_check(self.font.getsize(text)) - return self.font.getmask(text, mode) - - def getbbox( - self, text: str | bytes | bytearray, *args: Any, **kwargs: Any - ) -> tuple[int, int, int, int]: - """ - Returns bounding box (in pixels) of given text. - - .. versionadded:: 9.2.0 - - :param text: Text to render. - - :return: ``(left, top, right, bottom)`` bounding box - """ - _string_length_check(text) - width, height = self.font.getsize(text) - return 0, 0, width, height - - def getlength( - self, text: str | bytes | bytearray, *args: Any, **kwargs: Any - ) -> int: - """ - Returns length (in pixels) of given text. - This is the amount by which following text should be offset. - - .. versionadded:: 9.2.0 - """ - _string_length_check(text) - width, height = self.font.getsize(text) - return width - - -## -# Wrapper for FreeType fonts. Application code should use the -# <b>truetype</b> factory function to create font objects. - - -class FreeTypeFont: - """FreeType font wrapper (requires _imagingft service)""" - - font: Font - font_bytes: bytes - - def __init__( - self, - font: StrOrBytesPath | BinaryIO, - size: float = 10, - index: int = 0, - encoding: str = "", - layout_engine: Layout | None = None, - ) -> None: - # FIXME: use service provider instead - - if isinstance(core, DeferredError): - raise core.ex - - if size <= 0: - msg = f"font size must be greater than 0, not {size}" - raise ValueError(msg) - - self.path = font - self.size = size - self.index = index - self.encoding = encoding - - if layout_engine not in (Layout.BASIC, Layout.RAQM): - layout_engine = Layout.BASIC - if core.HAVE_RAQM: - layout_engine = Layout.RAQM - elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: - warnings.warn( - "Raqm layout was requested, but Raqm is not available. " - "Falling back to basic layout." - ) - layout_engine = Layout.BASIC - - self.layout_engine = layout_engine - - def load_from_bytes(f: IO[bytes]) -> None: - self.font_bytes = f.read() - self.font = core.getfont( - "", size, index, encoding, self.font_bytes, layout_engine - ) - - if is_path(font): - font = os.fspath(font) - if sys.platform == "win32": - font_bytes_path = font if isinstance(font, bytes) else font.encode() - try: - font_bytes_path.decode("ascii") - except UnicodeDecodeError: - # FreeType cannot load fonts with non-ASCII characters on Windows - # So load it into memory first - with open(font, "rb") as f: - load_from_bytes(f) - return - self.font = core.getfont( - font, size, index, encoding, layout_engine=layout_engine - ) - else: - load_from_bytes(cast(IO[bytes], font)) - - def __getstate__(self) -> list[Any]: - return [self.path, self.size, self.index, self.encoding, self.layout_engine] - - def __setstate__(self, state: list[Any]) -> None: - path, size, index, encoding, layout_engine = state - FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine) - - def getname(self) -> tuple[str | None, str | None]: - """ - :return: A tuple of the font family (e.g. Helvetica) and the font style - (e.g. Bold) - """ - return self.font.family, self.font.style - - def getmetrics(self) -> tuple[int, int]: - """ - :return: A tuple of the font ascent (the distance from the baseline to - the highest outline point) and descent (the distance from the - baseline to the lowest outline point, a negative value) - """ - return self.font.ascent, self.font.descent - - def getlength( - self, - text: str | bytes, - mode: str = "", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - ) -> float: - """ - Returns length (in pixels with 1/64 precision) of given text when rendered - in font with provided direction, features, and language. - - This is the amount by which following text should be offset. - Text bounding box may extend past the length in some fonts, - e.g. when using italics or accents. - - The result is returned as a float; it is a whole number if using basic layout. - - Note that the sum of two lengths may not equal the length of a concatenated - string due to kerning. If you need to adjust for kerning, include the following - character and subtract its length. - - For example, instead of :: - - hello = font.getlength("Hello") - world = font.getlength("World") - hello_world = hello + world # not adjusted for kerning - assert hello_world == font.getlength("HelloWorld") # may fail - - use :: - - hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning - world = font.getlength("World") - hello_world = hello + world # adjusted for kerning - assert hello_world == font.getlength("HelloWorld") # True - - or disable kerning with (requires libraqm) :: - - hello = draw.textlength("Hello", font, features=["-kern"]) - world = draw.textlength("World", font, features=["-kern"]) - hello_world = hello + world # kerning is disabled, no need to adjust - assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) - - .. versionadded:: 8.0.0 - - :param text: Text to measure. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - <https://www.w3.org/International/articles/language-tags/>`_ - Requires libraqm. - - :return: Either width for horizontal text, or height for vertical text. - """ - _string_length_check(text) - return self.font.getlength(text, mode, direction, features, language) / 64 - - def getbbox( - self, - text: str | bytes, - mode: str = "", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - anchor: str | None = None, - ) -> tuple[float, float, float, float]: - """ - Returns bounding box (in pixels) of given text relative to given anchor - when rendered in font with provided direction, features, and language. - - Use :py:meth:`getlength()` to get the offset of following text with - 1/64 pixel precision. The bounding box includes extra margins for - some fonts, e.g. italics or accents. - - .. versionadded:: 8.0.0 - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - <https://www.w3.org/International/articles/language-tags/>`_ - Requires libraqm. - - :param stroke_width: The width of the text stroke. - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - - :return: ``(left, top, right, bottom)`` bounding box - """ - _string_length_check(text) - size, offset = self.font.getsize( - text, mode, direction, features, language, anchor - ) - left, top = offset[0] - stroke_width, offset[1] - stroke_width - width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width - return left, top, left + width, top + height - - def getmask( - self, - text: str | bytes, - mode: str = "", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - anchor: str | None = None, - ink: int = 0, - start: tuple[float, float] | None = None, - ) -> Image.core.ImagingCore: - """ - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. If the font has embedded color data, the bitmap - should have mode ``RGBA``. Otherwise, it should have mode ``1``. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - <https://www.w3.org/International/articles/language-tags/>`_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - - .. versionadded:: 8.0.0 - - :param ink: Foreground ink for rendering in RGBA mode. - - .. versionadded:: 8.0.0 - - :param start: Tuple of horizontal and vertical offset, as text may render - differently when starting at fractional coordinates. - - .. versionadded:: 9.4.0 - - :return: An internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module. - """ - return self.getmask2( - text, - mode, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - anchor=anchor, - ink=ink, - start=start, - )[0] - - def getmask2( - self, - text: str | bytes, - mode: str = "", - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - stroke_width: float = 0, - anchor: str | None = None, - ink: int = 0, - start: tuple[float, float] | None = None, - *args: Any, - **kwargs: Any, - ) -> tuple[Image.core.ImagingCore, tuple[int, int]]: - """ - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. If the font has embedded color data, the bitmap - should have mode ``RGBA``. Otherwise, it should have mode ``1``. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - <https://www.w3.org/International/articles/language-tags/>`_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - - .. versionadded:: 8.0.0 - - :param ink: Foreground ink for rendering in RGBA mode. - - .. versionadded:: 8.0.0 - - :param start: Tuple of horizontal and vertical offset, as text may render - differently when starting at fractional coordinates. - - .. versionadded:: 9.4.0 - - :return: A tuple of an internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module, and the text offset, the - gap between the starting coordinate and the first marking - """ - _string_length_check(text) - if start is None: - start = (0, 0) - - def fill(width: int, height: int) -> Image.core.ImagingCore: - size = (width, height) - Image._decompression_bomb_check(size) - return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - - return self.font.render( - text, - fill, - mode, - direction, - features, - language, - stroke_width, - kwargs.get("stroke_filled", False), - anchor, - ink, - start, - ) - - def font_variant( - self, - font: StrOrBytesPath | BinaryIO | None = None, - size: float | None = None, - index: int | None = None, - encoding: str | None = None, - layout_engine: Layout | None = None, - ) -> FreeTypeFont: - """ - Create a copy of this FreeTypeFont object, - using any specified arguments to override the settings. - - Parameters are identical to the parameters used to initialize this - object. - - :return: A FreeTypeFont object. - """ - if font is None: - try: - font = BytesIO(self.font_bytes) - except AttributeError: - font = self.path - return FreeTypeFont( - font=font, - size=self.size if size is None else size, - index=self.index if index is None else index, - encoding=self.encoding if encoding is None else encoding, - layout_engine=layout_engine or self.layout_engine, - ) - - def get_variation_names(self) -> list[bytes]: - """ - :returns: A list of the named styles in a variation font. - :exception OSError: If the font is not a variation font. - """ - names = [] - for name in self.font.getvarnames(): - name = name.replace(b"\x00", b"") - if name not in names: - names.append(name) - return names - - def set_variation_by_name(self, name: str | bytes) -> None: - """ - :param name: The name of the style. - :exception OSError: If the font is not a variation font. - """ - names = self.get_variation_names() - if not isinstance(name, bytes): - name = name.encode() - index = names.index(name) + 1 - - if index == getattr(self, "_last_variation_index", None): - # When the same name is set twice in a row, - # there is an 'unknown freetype error' - # https://savannah.nongnu.org/bugs/?56186 - return - self._last_variation_index = index - - self.font.setvarname(index) - - def get_variation_axes(self) -> list[Axis]: - """ - :returns: A list of the axes in a variation font. - :exception OSError: If the font is not a variation font. - """ - axes = self.font.getvaraxes() - for axis in axes: - if axis["name"]: - axis["name"] = axis["name"].replace(b"\x00", b"") - return axes - - def set_variation_by_axes(self, axes: list[float]) -> None: - """ - :param axes: A list of values for each axis. - :exception OSError: If the font is not a variation font. - """ - self.font.setvaraxes(axes) - - -class TransposedFont: - """Wrapper for writing rotated or mirrored text""" - - def __init__( - self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None - ): - """ - Wrapper that creates a transposed font from any existing font - object. - - :param font: A font object. - :param orientation: An optional orientation. If given, this should - be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM, - Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or - Image.Transpose.ROTATE_270. - """ - self.font = font - self.orientation = orientation # any 'transpose' argument, or None - - def getmask( - self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any - ) -> Image.core.ImagingCore: - im = self.font.getmask(text, mode, *args, **kwargs) - if self.orientation is not None: - return im.transpose(self.orientation) - return im - - def getbbox( - self, text: str | bytes, *args: Any, **kwargs: Any - ) -> tuple[int, int, float, float]: - # TransposedFont doesn't support getmask2, move top-left point to (0, 0) - # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont - left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) - width = right - left - height = bottom - top - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - return 0, 0, height, width - return 0, 0, width, height - - def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float: - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - msg = "text length is undefined for text rotated by 90 or 270 degrees" - raise ValueError(msg) - return self.font.getlength(text, *args, **kwargs) - - -def load(filename: str) -> ImageFont: - """ - Load a font file. This function loads a font object from the given - bitmap font file, and returns the corresponding font object. For loading TrueType - or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`. - - :param filename: Name of font file. - :return: A font object. - :exception OSError: If the file could not be read. - """ - f = ImageFont() - f._load_pilfont(filename) - return f - - -def truetype( - font: StrOrBytesPath | BinaryIO, - size: float = 10, - index: int = 0, - encoding: str = "", - layout_engine: Layout | None = None, -) -> FreeTypeFont: - """ - Load a TrueType or OpenType font from a file or file-like object, - and create a font object. This function loads a font object from the given - file or file-like object, and creates a font object for a font of the given - size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load` - and :py:func:`~PIL.ImageFont.load_path`. - - Pillow uses FreeType to open font files. On Windows, be aware that FreeType - will keep the file open as long as the FreeTypeFont object exists. Windows - limits the number of files that can be open in C at once to 512, so if many - fonts are opened simultaneously and that limit is approached, an - ``OSError`` may be thrown, reporting that FreeType "cannot open resource". - A workaround would be to copy the file(s) into memory, and open that instead. - - This function requires the _imagingft service. - - :param font: A filename or file-like object containing a TrueType font. - If the file is not found in this filename, the loader may also - search in other directories, such as: - - * The :file:`fonts/` directory on Windows, - * :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/` - and :file:`~/Library/Fonts/` on macOS. - * :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`, - and :file:`/usr/share/fonts` on Linux; or those specified by - the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables - for user-installed and system-wide fonts, respectively. - - :param size: The requested size, in pixels. - :param index: Which font face to load (default is first available face). - :param encoding: Which font encoding to use (default is Unicode). Possible - encodings include (see the FreeType documentation for more - information): - - * "unic" (Unicode) - * "symb" (Microsoft Symbol) - * "ADOB" (Adobe Standard) - * "ADBE" (Adobe Expert) - * "ADBC" (Adobe Custom) - * "armn" (Apple Roman) - * "sjis" (Shift JIS) - * "gb " (PRC) - * "big5" - * "wans" (Extended Wansung) - * "joha" (Johab) - * "lat1" (Latin-1) - - This specifies the character set to use. It does not alter the - encoding of any text provided in subsequent operations. - :param layout_engine: Which layout engine to use, if available: - :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`. - If it is available, Raqm layout will be used by default. - Otherwise, basic layout will be used. - - Raqm layout is recommended for all non-English text. If Raqm layout - is not required, basic layout will have better performance. - - You can check support for Raqm layout using - :py:func:`PIL.features.check_feature` with ``feature="raqm"``. - - .. versionadded:: 4.2.0 - :return: A font object. - :exception OSError: If the file could not be read. - :exception ValueError: If the font size is not greater than zero. - """ - - def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont: - return FreeTypeFont(font, size, index, encoding, layout_engine) - - try: - return freetype(font) - except OSError: - if not is_path(font): - raise - ttf_filename = os.path.basename(font) - - dirs = [] - if sys.platform == "win32": - # check the windows font repository - # NOTE: must use uppercase WINDIR, to work around bugs in - # 1.5.2's os.environ.get() - windir = os.environ.get("WINDIR") - if windir: - dirs.append(os.path.join(windir, "fonts")) - elif sys.platform in ("linux", "linux2"): - data_home = os.environ.get("XDG_DATA_HOME") - if not data_home: - # The freedesktop spec defines the following default directory for - # when XDG_DATA_HOME is unset or empty. This user-level directory - # takes precedence over system-level directories. - data_home = os.path.expanduser("~/.local/share") - xdg_dirs = [data_home] - - data_dirs = os.environ.get("XDG_DATA_DIRS") - if not data_dirs: - # Similarly, defaults are defined for the system-level directories - data_dirs = "/usr/local/share:/usr/share" - xdg_dirs += data_dirs.split(":") - - dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs] - elif sys.platform == "darwin": - dirs += [ - "/Library/Fonts", - "/System/Library/Fonts", - os.path.expanduser("~/Library/Fonts"), - ] - - ext = os.path.splitext(ttf_filename)[1] - first_font_with_a_different_extension = None - for directory in dirs: - for walkroot, walkdir, walkfilenames in os.walk(directory): - for walkfilename in walkfilenames: - if ext and walkfilename == ttf_filename: - return freetype(os.path.join(walkroot, walkfilename)) - elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: - fontpath = os.path.join(walkroot, walkfilename) - if os.path.splitext(fontpath)[1] == ".ttf": - return freetype(fontpath) - if not ext and first_font_with_a_different_extension is None: - first_font_with_a_different_extension = fontpath - if first_font_with_a_different_extension: - return freetype(first_font_with_a_different_extension) - raise - - -def load_path(filename: str | bytes) -> ImageFont: - """ - Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a - bitmap font along the Python path. - - :param filename: Name of font file. - :return: A font object. - :exception OSError: If the file could not be read. - """ - if not isinstance(filename, str): - filename = filename.decode("utf-8") - for directory in sys.path: - try: - return load(os.path.join(directory, filename)) - except OSError: # noqa: PERF203 - pass - msg = f'cannot find font file "{filename}" in sys.path' - if os.path.exists(filename): - msg += f', did you mean ImageFont.load("{filename}") instead?' - - raise OSError(msg) - - -def load_default_imagefont() -> ImageFont: - f = ImageFont() - f._load_pilfont_data( - # courB08 - BytesIO(base64.b64decode(b""" -UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA -BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL -AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA -AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB -ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A -BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB -//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA -AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH -AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA -ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv -AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/ -/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5 -AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA -AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG -AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA -BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA -AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA -2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF -AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA//// -+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA -////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA -BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv -AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA -AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA -AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA -BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP// -//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA -AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF -AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB -mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn -AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA -AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7 -AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA -Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB -//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA -AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ -AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC -DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ -AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/ -+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5 -AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/ -///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG -AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA -BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA -Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC -eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG -AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA//// -+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA -////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA -BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT -AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A -AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA -Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA -Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP// -//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA -AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ -AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA -LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5 -AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA -AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5 -AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA -AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG -AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA -EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK -AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA -pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG -AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// -+QAGAAIAzgAKANUAEw== -""")), - Image.open(BytesIO(base64.b64decode(b""" -iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u -Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 -M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g -LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F -IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA -Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791 -NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx -in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9 -SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY -AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt -y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG -ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY -lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H -/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3 -AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47 -c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/ -/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw -pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv -oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR -evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA -AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// -Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR -w7IkEbzhVQAAAABJRU5ErkJggg== -"""))), - ) - return f - - -def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: - """If FreeType support is available, load a version of Aileron Regular, - https://dotcolon.net/fonts/aileron, with a more limited character set. - - Otherwise, load a "better than nothing" font. - - .. versionadded:: 1.1.4 - - :param size: The font size of Aileron Regular. - - .. versionadded:: 10.1.0 - - :return: A font object. - """ - if isinstance(core, ModuleType) or size is not None: - return truetype( - BytesIO(base64.b64decode(b""" -AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA -AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA -MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh -tdHjjERZ8AAAB2AAAAdBsb2NhuOexrgAABVQAAADqbWF4cAC7AEYAAAFYAAAAIG5hbWUr+h5lAAAk -OAAAA6Jwb3N0D3oPTQAAJ9wAAAEKAAEAAAABGhxJDqIhXw889QALA+gAAAAA0Bqf2QAAAADhCh2h/ -2r/LgOxAyAAAAAIAAIAAAAAAAAAAQAAA8r/GgAAA7j/av9qA7EAAQAAAAAAAAAAAAAAAAAAAHQAAQ -AAAHQAQwAFAAAAAAACAAAAAQABAAAAQAAAAAAAAAADAfoBkAAFAAgCigJYAAAASwKKAlgAAAFeADI -BPgAAAAAFAAAAAAAAAAAAAAcAAAAAAAAAAAAAAABVS1dOAEAAIPsCAwL/GgDIA8oA5iAAAJMAAAAA -AhICsgAAACAAAwH0AAAAAAAAAU0AAADYAAAA8gA5AVMAVgJEAEYCRAA1AuQAKQKOAEAAsAArATsAZ -AE7AB4CMABVAkQAUADc/+EBEgAgANwAJQEv//sCRAApAkQAggJEADwCRAAtAkQAIQJEADkCRAArAk -QAMgJEACwCRAAxANwAJQDc/+ECRABnAkQAUAJEAEQB8wAjA1QANgJ/AB0CcwBkArsALwLFAGQCSwB -kAjcAZALGAC8C2gBkAQgAZAIgADcCYQBkAj8AZANiAGQCzgBkAuEALwJWAGQC3QAvAmsAZAJJADQC -ZAAiAqoAXgJuACADuAAaAnEAGQJFABMCTwAuATMAYgEv//sBJwAiAkQAUAH0ADIBLAApAhMAJAJjA -EoCEQAeAmcAHgIlAB4BIgAVAmcAHgJRAEoA7gA+AOn/8wIKAEoA9wBGA1cASgJRAEoCSgAeAmMASg -JnAB4BSgBKAcsAGAE5ABQCUABCAgIAAQMRAAEB4v/6AgEAAQHOABQBLwBAAPoAYAEvACECRABNA0Y -AJAItAHgBKgAcAkQAUAEsAHQAygAgAi0AOQD3ADYA9wAWAaEANgGhABYCbAAlAYMAeAGDADkA6/9q -AhsAFAIKABUB/QAVAAAAAwAAAAMAAAAcAAEAAAAAAKQAAwABAAAAHAAEAIgAAAAeABAAAwAOAH4Aq -QCrALEAtAC3ALsgGSAdICYgOiBEISL7Av//AAAAIACpAKsAsAC0ALcAuyAYIBwgJiA5IEQhIvsB// -//4/+5/7j/tP+y/7D/reBR4E/gR+A14CzfTwVxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAMEBQYHCAkKCwwNDg8QERIT -FBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMT -U5PUFFSU1RVVldYWVpbXF1eX2BhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAA -AAAAAAYnFmAAAAAABlAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2htAAAAAAAAAABrbGlqAAAAAHAAbm9 -ycwBnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACYAJgAmAD4AUgCCAMoBCgFO -AVwBcgGIAaYBvAHKAdYB6AH2AgwCIAJKAogCpgLWAw4DIgNkA5wDugPUA+gD/AQQBEYEogS8BPoFJ -gVSBWoFgAWwBcoF1gX6BhQGJAZMBmgGiga0BuIHGgdUB2YHkAeiB8AH3AfyCAoIHAgqCDoITghcCG -oIogjSCPoJKglYCXwJwgnqCgIKKApACl4Klgq8CtwLDAs8C1YLjAuyC9oL7gwMDCYMSAxgDKAMrAz -qDQoNTA1mDYQNoA2uDcAN2g3oDfYODA4iDkoOXA5sDnoOnA7EDvwAAAAFAAAAAAH0ArwAAwAGAAkA -DAAPAAAxESERAxMhExcRASELARETAfT6qv6syKr+jgFUqsiqArz9RAGLAP/+1P8B/v3VAP8BLP4CA -P8AAgA5//IAuQKyAAMACwAANyMDMwIyFhQGIiY0oE4MZk84JCQ4JLQB/v3AJDgkJDgAAgBWAeUBPA -LfAAMABwAAEyMnMxcjJzOmRgpagkYKWgHl+vr6AAAAAAIARgAAAf4CsgAbAB8AAAEHMxUjByM3Iwc -jNyM1MzcjNTM3MwczNzMHMxUrAQczAZgdZXEvOi9bLzovWmYdZXEvOi9bLzovWp9bHlsBn4w429vb -2ziMONvb29s4jAAAAAMANf+mAg4DDAAfACYALAAAJRQGBxUjNS4BJzMeARcRLgE0Njc1MxUeARcjJ -icVHgEBFBYXNQ4BExU+ATU0Ag5xWDpgcgRcBz41Xl9oVTpVYwpcC1ttXP6cLTQuM5szOrVRZwlOTQ -ZqVzZECAEAGlukZAlOTQdrUG8O7iNlAQgxNhDlCDj+8/YGOjReAAAAAAUAKf/yArsCvAAHAAsAFQA -dACcAABIyFhQGIiY0EyMBMwQiBhUUFjI2NTQSMhYUBiImNDYiBhUUFjI2NTR5iFBQiFCVVwHAV/5c -OiMjOiPmiFBQiFCxOiMjOiMCvFaSVlaS/ZoCsjIzMC80NC8w/uNWklZWkhozMC80NC8wAAAAAgBA/ -/ICbgLAACIALgAAARUjEQYjIiY1NDY3LgE1NDYzMhcVJiMiBhUUFhcWOwE1MxUFFBYzMjc1IyIHDg -ECbmBcYYOOVkg7R4hsQjY4Q0RNRD4SLDxW/pJUXzksPCkUUk0BgUb+zBVUZ0BkDw5RO1huCkULQzp -COAMBcHDHRz0J/AIHRQAAAAEAKwHlAIUC3wADAAATIycze0YKWgHl+gAAAAABAGT/sAEXAwwACQAA -EzMGEBcjLgE0Nt06dXU6OUBAAwzG/jDGVePs4wAAAAEAHv+wANEDDAAJAAATMx4BFAYHIzYQHjo5Q -EA5OnUDDFXj7ONVxgHQAAAAAQBVAFIB2wHbAA4AAAE3FwcXBycHJzcnNxcnMwEtmxOfcTJjYzJxnx -ObCj4BKD07KYolmZkliik7PbMAAQBQAFUB9AIlAAsAAAEjFSM1IzUzNTMVMwH0tTq1tTq1AR/Kyjj -OzgAAAAAB/+H/iACMAGQABAAANwcjNzOMWlFOXVrS3AAAAQAgAP8A8gE3AAMAABMjNTPy0tIA/zgA -AQAl//IApQByAAcAADYyFhQGIiY0STgkJDgkciQ4JCQ4AAAAAf/7/+IBNALQAAMAABcjEzM5Pvs+H -gLuAAAAAAIAKf/yAhsCwAADAAcAABIgECA2IBAgKQHy/g5gATL+zgLA/TJEAkYAAAAAAQCCAAABlg -KyAAgAAAERIxEHNTc2MwGWVr6SIygCsv1OAldxW1sWAAEAPAAAAg4CwAAZAAA3IRUhNRM+ATU0JiM -iDwEjNz4BMzIWFRQGB7kBUv4x+kI2QTt+EAFWAQp8aGVtSl5GRjEA/0RVLzlLmAoKa3FsUkNxXQAA -AAEALf/yAhYCwAAqAAABHgEVFAYjIi8BMxceATMyNjU0KwE1MzI2NTQmIyIGDwEjNz4BMzIWFRQGA -YxBSZJo2RUBVgEHV0JBUaQREUBUQzc5TQcBVgEKfGhfcEMBbxJbQl1x0AoKRkZHPn9GSD80QUVCCg -pfbGBPOlgAAAACACEAAAIkArIACgAPAAAlIxUjNSE1ATMRMyMRBg8BAiRXVv6qAVZWV60dHLCurq4 -rAdn+QgFLMibzAAABADn/8gIZArIAHQAAATIWFRQGIyIvATMXFjMyNjU0JiMiByMTIRUhBzc2ATNv -d5Fl1RQBVgIad0VSTkVhL1IwAYj+vh8rMAHHgGdtgcUKCoFXTU5bYgGRRvAuHQAAAAACACv/8gITA -sAAFwAjAAABMhYVFAYjIhE0NjMyFh8BIycmIyIDNzYTMjY1NCYjIgYVFBYBLmp7imr0l3RZdAgBXA -IYZ5wKJzU6QVNJSz5SUAHSgWltiQFGxcNlVQoKdv7sPiz+ZF1LTmJbU0lhAAAAAQAyAAACGgKyAAY -AAAEVASMBITUCGv6oXAFL/oECsij9dgJsRgAAAAMALP/xAhgCwAAWACAALAAAAR4BFRQGIyImNTQ2 -Ny4BNTQ2MhYVFAYmIgYVFBYyNjU0AzI2NTQmIyIGFRQWAZQ5S5BmbIpPOjA7ecp5P2F8Q0J8RIVJS -0pLTEtOAW0TXTxpZ2ZqPF0SE1A3VWVlVTdQ/UU0N0RENzT9/ko+Ok1NOj1LAAIAMf/yAhkCwAAXAC -MAAAEyERQGIyImLwEzFxYzMhMHBiMiJjU0NhMyNjU0JiMiBhUUFgEl9Jd0WXQIAVwCGGecCic1SWp -7imo+UlBAQVNJAsD+usXDZVUKCnYBFD4sgWltif5kW1NJYV1LTmIAAAACACX/8gClAiAABwAPAAAS -MhYUBiImNBIyFhQGIiY0STgkJDgkJDgkJDgkAiAkOCQkOP52JDgkJDgAAAAC/+H/iAClAiAABwAMA -AASMhYUBiImNBMHIzczSTgkJDgkaFpSTl4CICQ4JCQ4/mba5gAAAQBnAB4B+AH0AAYAAAENARUlNS -UB+P6qAVb+bwGRAbCmpkbJRMkAAAIAUAC7AfQBuwADAAcAAAEhNSERITUhAfT+XAGk/lwBpAGDOP8 -AOAABAEQAHgHVAfQABgAAARUFNS0BNQHV/m8BVv6qAStEyUSmpkYAAAAAAgAj//IB1ALAABgAIAAA -ATIWFRQHDgEHIz4BNz4BNTQmIyIGByM+ARIyFhQGIiY0AQRibmktIAJWBSEqNig+NTlHBFoDezQ4J -CQ4JALAZ1BjaS03JS1DMD5LLDQ/SUVgcv2yJDgkJDgAAAAAAgA2/5gDFgKYADYAQgAAAQMGFRQzMj -Y1NCYjIg4CFRQWMzI2NxcGIyImNTQ+AjMyFhUUBiMiJwcGIyImNTQ2MzIfATcHNzYmIyIGFRQzMjY -Cej8EJjJJlnBAfGQ+oHtAhjUYg5OPx0h2k06Os3xRWQsVLjY5VHtdPBwJETcJDyUoOkZEJz8B0f74 -EQ8kZl6EkTFZjVOLlyknMVm1pmCiaTq4lX6CSCknTVRmmR8wPdYnQzxuSWVGAAIAHQAAAncCsgAHA -AoAACUjByMTMxMjATMDAcj+UVz4dO5d/sjPZPT0ArL9TgE6ATQAAAADAGQAAAJMArIAEAAbACcAAA -EeARUUBgcGKwERMzIXFhUUJRUzMjc2NTQnJiMTPgE1NCcmKwEVMzIBvkdHZkwiNt7LOSGq/oeFHBt -hahIlSTM+cB8Yj5UWAW8QT0VYYgwFArIEF5Fv1eMED2NfDAL93AU+N24PBP0AAAAAAQAv//ICjwLA -ABsAAAEyFh8BIycmIyIGFRQWMzI/ATMHDgEjIiY1NDYBdX+PCwFWAiKiaHx5ZaIiAlYBCpWBk6a0A -sCAagoKpqN/gaOmCgplhcicn8sAAAIAZAAAAp8CsgAMABkAAAEeARUUBgcGKwERMzITPgE1NCYnJi -sBETMyAY59lJp8IzXN0jUVWmdjWRs5d3I4Aq4QqJWUug8EArL9mQ+PeHGHDgX92gAAAAABAGQAAAI -vArIACwAAJRUhESEVIRUhFSEVAi/+NQHB/pUBTf6zRkYCskbwRvAAAAABAGQAAAIlArIACQAAExUh -FSERIxEhFboBQ/69VgHBAmzwRv7KArJGAAAAAAEAL//yAo8CwAAfAAABMxEjNQcGIyImNTQ2MzIWH -wEjJyYjIgYVFBYzMjY1IwGP90wfPnWTprSSf48LAVYCIqJofHllVG+hAU3+s3hARsicn8uAagoKpq -N/gaN1XAAAAAEAZAAAAowCsgALAAABESMRIREjETMRIRECjFb+hFZWAXwCsv1OAS7+0gKy/sQBPAA -AAAABAGQAAAC6ArIAAwAAMyMRM7pWVgKyAAABADf/8gHoArIAEwAAAREUBw4BIyImLwEzFxYzMjc2 -NREB6AIFcGpgbQIBVgIHfXQKAQKy/lYxIltob2EpKYyEFD0BpwAAAAABAGQAAAJ0ArIACwAACQEjA -wcVIxEzEQEzATsBJ3ntQlZWAVVlAWH+nwEnR+ACsv6RAW8AAQBkAAACLwKyAAUAACUVIREzEQIv/j -VWRkYCsv2UAAABAGQAAAMUArIAFAAAAREjETQ3BgcDIwMmJxYVESMRMxsBAxRWAiMxemx8NxsCVo7 -MywKy/U4BY7ZLco7+nAFmoFxLtP6dArL9lwJpAAAAAAEAZAAAAoACsgANAAAhIwEWFREjETMBJjUR -MwKAhP67A1aEAUUDVAJeeov+pwKy/aJ5jAFZAAAAAgAv//ICuwLAAAkAEwAAEiAWFRQGICY1NBIyN -jU0JiIGFRTbATSsrP7MrNrYenrYegLAxaKhxsahov47nIeIm5uIhwACAGQAAAJHArIADgAYAAABHg -EVFAYHBisBESMRMzITNjQnJisBETMyAZRUX2VOHzuAVtY7GlxcGDWIiDUCrgtnVlVpCgT+5gKy/rU -V1BUF/vgAAAACAC//zAK9AsAAEgAcAAAlFhcHJiMiBwYjIiY1NDYgFhUUJRQWMjY1NCYiBgI9PUMx -UDcfKh8omqysATSs/dR62Hp62HpICTg7NgkHxqGixcWitbWHnJyHiJubAAIAZAAAAlgCsgAXACMAA -CUWFyMmJyYnJisBESMRMzIXHgEVFAYHFiUzMjc+ATU0JyYrAQIqDCJfGQwNWhAhglbiOx9QXEY1Tv -6bhDATMj1lGSyMtYgtOXR0BwH+1wKyBApbU0BSESRAAgVAOGoQBAABADT/8gIoAsAAJQAAATIWFyM -uASMiBhUUFhceARUUBiMiJiczHgEzMjY1NCYnLgE1NDYBOmd2ClwGS0E6SUNRdW+HZnKKC1wPWkQ9 -Uk1cZGuEAsBwXUJHNjQ3OhIbZVZZbm5kREo+NT5DFRdYUFdrAAAAAAEAIgAAAmQCsgAHAAABIxEjE -SM1IQJk9lb2AkICbP2UAmxGAAEAXv/yAmQCsgAXAAABERQHDgEiJicmNREzERQXHgEyNjc2NRECZA -IIgfCBCAJWAgZYmlgGAgKy/k0qFFxzc1wUKgGz/lUrEkRQUEQSKwGrAAAAAAEAIAAAAnoCsgAGAAA -hIwMzGwEzAYJ07l3N1FwCsv2PAnEAAAEAGgAAA7ECsgAMAAABAyMLASMDMxsBMxsBA7HAcZyicrZi -kaB0nJkCsv1OAlP9rQKy/ZsCW/2kAmYAAAEAGQAAAm8CsgALAAAhCwEjEwMzGwEzAxMCCsrEY/bkY -re+Y/D6AST+3AFcAVb+5gEa/q3+oQAAAQATAAACUQKyAAgAAAERIxEDMxsBMwFdVvRjwLphARD+8A -EQAaL+sQFPAAABAC4AAAI5ArIACQAAJRUhNQEhNSEVAQI5/fUBof57Aen+YUZGQgIqRkX92QAAAAA -BAGL/sAEFAwwABwAAARUjETMVIxEBBWlpowMMOP0UOANcAAAB//v/4gE0AtAAAwAABSMDMwE0Pvs+ -HgLuAAAAAQAi/7AAxQMMAAcAABcjNTMRIzUzxaNpaaNQOALsOAABAFAA1wH0AmgABgAAJQsBIxMzE -wGwjY1GsESw1wFZ/qcBkf5vAAAAAQAy/6oBwv/iAAMAAAUhNSEBwv5wAZBWOAAAAAEAKQJEALYCsg -ADAAATIycztjhVUAJEbgAAAAACACT/8gHQAiAAHQAlAAAhJwcGIyImNTQ2OwE1NCcmIyIHIz4BMzI -XFh0BFBcnMjY9ASYVFAF6CR0wVUtgkJoiAgdgaQlaBm1Zrg4DCuQ9R+5MOSFQR1tbDiwUUXBUXowf -J8c9SjRORzYSgVwAAAAAAgBK//ICRQLfABEAHgAAATIWFRQGIyImLwEVIxEzETc2EzI2NTQmIyIGH -QEUFgFUcYCVbiNJEyNWVigySElcU01JXmECIJd4i5QTEDRJAt/+3jkq/hRuZV55ZWsdX14AAQAe// -IB9wIgABgAAAEyFhcjJiMiBhUUFjMyNjczDgEjIiY1NDYBF152DFocbEJXU0A1Rw1aE3pbaoKQAiB -oWH5qZm1tPDlaXYuLgZcAAAACAB7/8gIZAt8AEQAeAAABESM1BwYjIiY1NDYzMhYfAREDMjY9ATQm -IyIGFRQWAhlWKDJacYCVbiNJEyOnSV5hQUlcUwLf/SFVOSqXeIuUExA0ARb9VWVrHV9ebmVeeQACA -B7/8gH9AiAAFQAbAAABFAchHgEzMjY3Mw4BIyImNTQ2MzIWJyIGByEmAf0C/oAGUkA1SwlaD4FXbI -WObmt45UBVBwEqDQEYFhNjWD84W16Oh3+akU9aU60AAAEAFQAAARoC8gAWAAATBh0BMxUjESMRIzU -zNTQ3PgEzMhcVJqcDbW1WOTkDB0k8Hx5oAngVITRC/jQBzEIsJRs5PwVHEwAAAAIAHv8uAhkCIAAi -AC8AAAERFAcOASMiLwEzFx4BMzI2NzY9AQcGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZAQSEd -NwRAVcBBU5DTlUDASgyWnGAlW4jSRMjp0leYUFJXFMCEv5wSh1zeq8KCTI8VU0ZIQk5Kpd4i5QTED -RJ/iJlax1fXm5lXnkAAQBKAAACCgLkABcAAAEWFREjETQnLgEHDgEdASMRMxE3NjMyFgIIAlYCBDs -6RVRWViE5UVViAYUbQP7WASQxGzI7AQJyf+kC5P7TPSxUAAACAD4AAACsAsAABwALAAASMhYUBiIm -NBMjETNeLiAgLiBiVlYCwCAuICAu/WACEgAC//P/LgCnAsAABwAVAAASMhYUBiImNBcRFAcGIyInN -RY3NjURWS4gIC4gYgMLcRwNSgYCAsAgLiAgLo79wCUbZAJGBzMOHgJEAAAAAQBKAAACCALfAAsAAC -EnBxUjETMREzMHEwGTwTJWVvdu9/rgN6kC3/4oAQv6/ugAAQBG//wA3gLfAA8AABMRFBceATcVBiM -iJicmNRGcAQIcIxkkKi4CAQLf/bkhERoSBD4EJC8SNAJKAAAAAQBKAAADEAIgACQAAAEWFREjETQn -JiMiFREjETQnJiMiFREjETMVNzYzMhYXNzYzMhYDCwVWBAxedFYEDF50VlYiJko7ThAvJkpEVAGfI -jn+vAEcQyRZ1v76ARxDJFnW/voCEk08HzYtRB9HAAAAAAEASgAAAgoCIAAWAAABFhURIxE0JyYjIg -YdASMRMxU3NjMyFgIIAlYCCXBEVVZWITlRVWIBhRtA/tYBJDEbbHR/6QISWz0sVAAAAAACAB7/8gI -sAiAABwARAAASIBYUBiAmNBIyNjU0JiIGFRSlAQCHh/8Ah7ieWlqeWgIgn/Cfn/D+s3ZfYHV1YF8A -AgBK/zwCRQIgABEAHgAAATIWFRQGIyImLwERIxEzFTc2EzI2NTQmIyIGHQEUFgFUcYCVbiNJEyNWV -igySElcU01JXmECIJd4i5QTEDT+8wLWVTkq/hRuZV55ZWsdX14AAgAe/zwCGQIgABEAHgAAAREjEQ -cGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZVigyWnGAlW4jSRMjp0leYUFJXFMCEv0qARk5Kpd -4i5QTEDRJ/iJlax1fXm5lXnkAAQBKAAABPgIeAA0AAAEyFxUmBhURIxEzFTc2ARoWDkdXVlYwIwIe -B0EFVlf+0gISU0cYAAEAGP/yAa0CIAAjAAATMhYXIyYjIgYVFBYXHgEVFAYjIiYnMxYzMjY1NCYnL -gE1NDbkV2MJWhNdKy04PF1XbVhWbgxaE2ktOjlEUllkAiBaS2MrJCUoEBlPQkhOVFZoKCUmLhIWSE -BIUwAAAAEAFP/4ARQCiQAXAAATERQXHgE3FQYjIiYnJjURIzUzNTMVMxWxAQMmMx8qMjMEAUdHVmM -BzP7PGw4mFgY/BSwxDjQBNUJ7e0IAAAABAEL/8gICAhIAFwAAAREjNQcGIyImJyY1ETMRFBceATMy -Nj0BAgJWITlRT2EKBVYEBkA1RFECEv3uWj4qTToiOQE+/tIlJC43c4DpAAAAAAEAAQAAAfwCEgAGA -AABAyMDMxsBAfzJaclfop8CEv3uAhL+LQHTAAABAAEAAAMLAhIADAAAAQMjCwEjAzMbATMbAQMLqW -Z2dmapY3t0a3Z7AhL97gG+/kICEv5AAcD+QwG9AAAB//oAAAHWAhIACwAAARMjJwcjEwMzFzczARq -8ZIuKY763ZoWFYwEO/vLV1QEMAQbNzQAAAQAB/y4B+wISABEAAAEDDgEjIic1FjMyNj8BAzMbAQH7 -2iFZQB8NDRIpNhQH02GenQIS/cFVUAJGASozEwIt/i4B0gABABQAAAGxAg4ACQAAJRUhNQEhNSEVA -QGx/mMBNP7iAYL+zkREQgGIREX+ewAAAAABAED/sAEOAwwALAAAASMiBhUUFxYVFAYHHgEVFAcGFR -QWOwEVIyImNTQ3NjU0JzU2NTQnJjU0NjsBAQ4MKiMLDS4pKS4NCyMqDAtERAwLUlILDERECwLUGBk -WTlsgKzUFBTcrIFtOFhkYOC87GFVMIkUIOAhFIkxVGDsvAAAAAAEAYP84AJoDIAADAAAXIxEzmjo6 -yAPoAAEAIf+wAO8DDAAsAAATFQYVFBcWFRQGKwE1MzI2NTQnJjU0NjcuATU0NzY1NCYrATUzMhYVF -AcGFRTvUgsMREQLDCojCw0uKSkuDQsjKgwLREQMCwF6OAhFIkxVGDsvOBgZFk5bICs1BQU3KyBbTh -YZGDgvOxhVTCJFAAABAE0A3wH2AWQAEwAAATMUIyImJyYjIhUjNDMyFhcWMzIBvjhuGywtQR0xOG4 -bLC1BHTEBZIURGCNMhREYIwAAAwAk/94DIgLoAAcAEQApAAAAIBYQBiAmECQgBhUUFiA2NTQlMhYX -IyYjIgYUFjMyNjczDgEjIiY1NDYBAQFE3d3+vN0CB/7wubkBELn+xVBnD1wSWDo+QTcqOQZcEmZWX -HN2Aujg/rbg4AFKpr+Mjb6+jYxbWEldV5ZZNShLVn5na34AAgB4AFIB9AGeAAUACwAAAQcXIyc3Mw -cXIyc3AUqJiUmJifOJiUmJiQGepqampqampqYAAAIAHAHSAQ4CwAAHAA8AABIyFhQGIiY0NiIGFBY -yNjRgakREakSTNCEhNCECwEJqQkJqCiM4IyM4AAAAAAIAUAAAAfQCCwALAA8AAAEzFSMVIzUjNTM1 -MxMhNSEBP7W1OrW1OrX+XAGkAVs4tLQ4sP31OAAAAQB0AkQBAQKyAAMAABMjNzOsOD1QAkRuAAAAA -AEAIADsAKoBdgAHAAASMhYUBiImNEg6KCg6KAF2KDooKDoAAAIAOQBSAbUBngAFAAsAACUHIzcnMw -UHIzcnMwELiUmJiUkBM4lJiYlJ+KampqampqYAAAABADYB5QDhAt8ABAAAEzczByM2Xk1OXQHv8Po -AAQAWAeUAwQLfAAQAABMHIzczwV5NTl0C1fD6AAIANgHlAYsC3wAEAAkAABM3MwcjPwEzByM2Xk1O -XapeTU5dAe/w+grw+gAAAgAWAeUBawLfAAQACQAAEwcjNzMXByM3M8FeTU5dql5NTl0C1fD6CvD6A -AADACX/8gI1AHIABwAPABcAADYyFhQGIiY0NjIWFAYiJjQ2MhYUBiImNEk4JCQ4JOw4JCQ4JOw4JC -Q4JHIkOCQkOCQkOCQkOCQkOCQkOAAAAAEAeABSAUoBngAFAAABBxcjJzcBSomJSYmJAZ6mpqamAAA -AAAEAOQBSAQsBngAFAAAlByM3JzMBC4lJiYlJ+KampgAAAf9qAAABgQKyAAMAACsBATM/VwHAVwKy -AAAAAAIAFAHIAdwClAAHABQAABMVIxUjNSM1BRUjNwcjJxcjNTMXN9pKMkoByDICKzQqATJLKysCl -CmjoykBy46KiY3Lm5sAAQAVAAABvALyABgAAAERIxEjESMRIzUzNTQ3NjMyFxUmBgcGHQEBvFbCVj -k5AxHHHx5iVgcDAg798gHM/jQBzEIOJRuWBUcIJDAVIRYAAAABABX//AHkAvIAJQAAJR4BNxUGIyI -mJyY1ESYjIgcGHQEzFSMRIxEjNTM1NDc2MzIXERQBowIcIxkkKi4CAR4nXgwDbW1WLy8DEbNdOmYa -EQQ/BCQvEjQCFQZWFSEWQv40AcxCDiUblhP9uSEAAAAAAAAWAQ4AAQAAAAAAAAATACgAAQAAAAAAA -QAHAEwAAQAAAAAAAgAHAGQAAQAAAAAAAwAaAKIAAQAAAAAABAAHAM0AAQAAAAAABQA8AU8AAQAAAA -AABgAPAawAAQAAAAAACAALAdQAAQAAAAAACQALAfgAAQAAAAAACwAXAjQAAQAAAAAADAAXAnwAAwA -BBAkAAAAmAAAAAwABBAkAAQAOADwAAwABBAkAAgAOAFQAAwABBAkAAwA0AGwAAwABBAkABAAOAL0A -AwABBAkABQB4ANUAAwABBAkABgAeAYwAAwABBAkACAAWAbwAAwABBAkACQAWAeAAAwABBAkACwAuA -gQAAwABBAkADAAuAkwATgBvACAAUgBpAGcAaAB0AHMAIABSAGUAcwBlAHIAdgBlAGQALgAATm8gUm -lnaHRzIFJlc2VydmVkLgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAUgBlAGcAdQBsAGEAcgAAUmV -ndWxhcgAAMQAuADEAMAAyADsAVQBLAFcATgA7AEEAaQBsAGUAcgBvAG4ALQBSAGUAZwB1AGwAYQBy -AAAxLjEwMjtVS1dOO0FpbGVyb24tUmVndWxhcgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAVgBlA -HIAcwBpAG8AbgAgADEALgAxADAAMgA7AFAAUwAgADAAMAAxAC4AMQAwADIAOwBoAG8AdABjAG8Abg -B2ACAAMQAuADAALgA3ADAAOwBtAGEAawBlAG8AdABmAC4AbABpAGIAMgAuADUALgA1ADgAMwAyADk -AAFZlcnNpb24gMS4xMDI7UFMgMDAxLjEwMjtob3Rjb252IDEuMC43MDttYWtlb3RmLmxpYjIuNS41 -ODMyOQAAQQBpAGwAZQByAG8AbgAtAFIAZQBnAHUAbABhAHIAAEFpbGVyb24tUmVndWxhcgAAUwBvA -HIAYQAgAFMAYQBnAGEAbgBvAABTb3JhIFNhZ2FubwAAUwBvAHIAYQAgAFMAYQBnAGEAbgBvAABTb3 -JhIFNhZ2FubwAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBsAG8AbgAuAG4AZQB0AAB -odHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBs -AG8AbgAuAG4AZQB0AABodHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAAAACAAAAAAAA/4MAMgAAAAAAA -AAAAAAAAAAAAAAAAAAAAHQAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATAB -QAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAA -xADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0A -TgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAIsAqQCDAJMAjQDDAKoAtgC3A -LQAtQCrAL4AvwC8AIwAwADBAAAAAAAB//8AAgABAAAADAAAABwAAAACAAIAAwBxAAEAcgBzAAIABA -AAAAIAAAABAAAACgBMAGYAAkRGTFQADmxhdG4AGgAEAAAAAP//AAEAAAAWAANDQVQgAB5NT0wgABZ -ST00gABYAAP//AAEAAAAA//8AAgAAAAEAAmxpZ2EADmxvY2wAFAAAAAEAAQAAAAEAAAACAAYAEAAG -AAAAAgASADQABAAAAAEATAADAAAAAgAQABYAAQAcAAAAAQABAE8AAQABAGcAAQABAE8AAwAAAAIAE -AAWAAEAHAAAAAEAAQAvAAEAAQBnAAEAAQAvAAEAGgABAAgAAgAGAAwAcwACAE8AcgACAEwAAQABAE -kAAAABAAAACgBGAGAAAkRGTFQADmxhdG4AHAAEAAAAAP//AAIAAAABABYAA0NBVCAAFk1PTCAAFlJ -PTSAAFgAA//8AAgAAAAEAAmNwc3AADmtlcm4AFAAAAAEAAAAAAAEAAQACAAYADgABAAAAAQASAAIA -AAACAB4ANgABAAoABQAFAAoAAgABACQAPQAAAAEAEgAEAAAAAQAMAAEAOP/nAAEAAQAkAAIGigAEA -AAFJAXKABoAGQAA//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAD/sv+4/+z/7v/MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAD/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9T/6AAAAAD/8QAA -ABD/vQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7gAAAAAAAAAAAAAAAAAA//MAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAP/5AAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAD/4AAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//L/9AAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAA/+gAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAD -/4gAA//AAAAAA//YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAP/OAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/zv/qAAAAAP/0AAAACAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAD/egAA/1kAAAAA/5D/rgAAAAAAAAAAAA -AAAAAAAAAAAAAAAAD/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAD/8AAA/7b/8P+wAAD/8P/E/98AAAAA/8P/+P/0//oAAAAAAAAAAAAA//gA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/w//C/9MAAP/SAAD/9wAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yAAA/+kAAAAA//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9wAAAAD//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAP/cAAAAAAAAAAAAAAAA/7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAkAFAAEAAAAAQACwAAABcA -BgAAAAAAAAAIAA4AAAAAAAsAEgAAAAAAAAATABkAAwANAAAAAQAJAAAAAAAAAAAAAAAAAAAAGAAAA -AAABwAAAAAAAAAAAAAAFQAFAAAAAAAYABgAAAAUAAAACgAAAAwAAgAPABEAFgAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAEAEQBdAAYAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAABwAAAAAACAAAAAAAAAAAAAcAAAAHAAAAEwAJ -ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA -gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC -YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA -AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ== -""")), - 10 if size is None else size, - layout_engine=Layout.BASIC, - ) - return load_default_imagefont() diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageGrab.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageGrab.py deleted file mode 100644 index 66ee6dd..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageGrab.py +++ /dev/null @@ -1,231 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# screen grabber -# -# History: -# 2001-04-26 fl created -# 2001-09-17 fl use builtin driver, if present -# 2002-11-19 fl added grabclipboard support -# -# Copyright (c) 2001-2002 by Secret Labs AB -# Copyright (c) 2001-2002 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import shutil -import subprocess -import sys -import tempfile - -from . import Image - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import ImageWin - - -def grab( - bbox: tuple[int, int, int, int] | None = None, - include_layered_windows: bool = False, - all_screens: bool = False, - xdisplay: str | None = None, - window: int | ImageWin.HWND | None = None, -) -> Image.Image: - im: Image.Image - if xdisplay is None: - if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - args = ["screencapture"] - if window is not None: - args += ["-l", str(window)] - elif bbox: - left, top, right, bottom = bbox - args += ["-R", f"{left},{top},{right-left},{bottom-top}"] - args += ["-x", filepath] - retcode = subprocess.call(args) - if retcode: - raise subprocess.CalledProcessError(retcode, args) - im = Image.open(filepath) - im.load() - os.unlink(filepath) - if bbox: - if window is not None: - # Determine if the window was in Retina mode or not - # by capturing it without the shadow, - # and checking how different the width is - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - args = ["screencapture", "-l", str(window), "-o", "-x", filepath] - retcode = subprocess.call(args) - if retcode: - raise subprocess.CalledProcessError(retcode, args) - with Image.open(filepath) as im_no_shadow: - retina = im.width - im_no_shadow.width > 100 - os.unlink(filepath) - - # Since screencapture's -R does not work with -l, - # crop the image manually - if retina: - left, top, right, bottom = bbox - im_cropped = im.resize( - (right - left, bottom - top), - box=tuple(coord * 2 for coord in bbox), - ) - else: - im_cropped = im.crop(bbox) - im.close() - return im_cropped - else: - im_resized = im.resize((right - left, bottom - top)) - im.close() - return im_resized - return im - elif sys.platform == "win32": - if window is not None: - all_screens = -1 - offset, size, data = Image.core.grabscreen_win32( - include_layered_windows, - all_screens, - int(window) if window is not None else 0, - ) - im = Image.frombytes( - "RGB", - size, - data, - # RGB, 32-bit line padding, origin lower left corner - "raw", - "BGR", - (size[0] * 3 + 3) & -4, - -1, - ) - if bbox: - x0, y0 = offset - left, top, right, bottom = bbox - im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) - return im - # Cast to Optional[str] needed for Windows and macOS. - display_name: str | None = xdisplay - try: - if not Image.core.HAVE_XCB: - msg = "Pillow was built without XCB support" - raise OSError(msg) - size, data = Image.core.grabscreen_x11(display_name) - except OSError: - if display_name is None and sys.platform not in ("darwin", "win32"): - if shutil.which("gnome-screenshot"): - args = ["gnome-screenshot", "-f"] - elif shutil.which("grim"): - args = ["grim"] - elif shutil.which("spectacle"): - args = ["spectacle", "-n", "-b", "-f", "-o"] - else: - raise - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - args.append(filepath) - retcode = subprocess.call(args) - if retcode: - raise subprocess.CalledProcessError(retcode, args) - im = Image.open(filepath) - im.load() - os.unlink(filepath) - if bbox: - im_cropped = im.crop(bbox) - im.close() - return im_cropped - return im - else: - raise - else: - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im - - -def grabclipboard() -> Image.Image | list[str] | None: - if sys.platform == "darwin": - p = subprocess.run( - ["osascript", "-e", "get the clipboard as «class PNGf»"], - capture_output=True, - ) - if p.returncode != 0: - return None - - import binascii - - data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3])) - return Image.open(data) - elif sys.platform == "win32": - fmt, data = Image.core.grabclipboard_win32() - if fmt == "file": # CF_HDROP - import struct - - o = struct.unpack_from("I", data)[0] - if data[16] == 0: - files = data[o:].decode("mbcs").split("\0") - else: - files = data[o:].decode("utf-16le").split("\0") - return files[: files.index("")] - if isinstance(data, bytes): - data = io.BytesIO(data) - if fmt == "png": - from . import PngImagePlugin - - return PngImagePlugin.PngImageFile(data) - elif fmt == "DIB": - from . import BmpImagePlugin - - return BmpImagePlugin.DibImageFile(data) - return None - else: - if os.getenv("WAYLAND_DISPLAY"): - session_type = "wayland" - elif os.getenv("DISPLAY"): - session_type = "x11" - else: # Session type check failed - session_type = None - - if shutil.which("wl-paste") and session_type in ("wayland", None): - args = ["wl-paste", "-t", "image"] - elif shutil.which("xclip") and session_type in ("x11", None): - args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] - else: - msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" - raise NotImplementedError(msg) - - p = subprocess.run(args, capture_output=True) - if p.returncode != 0: - err = p.stderr - for silent_error in [ - # wl-paste, when the clipboard is empty - b"Nothing is copied", - # Ubuntu/Debian wl-paste, when the clipboard is empty - b"No selection", - # Ubuntu/Debian wl-paste, when an image isn't available - b"No suitable type of content copied", - # wl-paste or Ubuntu/Debian xclip, when an image isn't available - b" not available", - # xclip, when an image isn't available - b"cannot convert ", - # xclip, when the clipboard isn't initialized - b"xclip: Error: There is no owner for the ", - ]: - if silent_error in err: - return None - msg = f"{args[0]} error" - if err: - msg += f": {err.strip().decode()}" - raise ChildProcessError(msg) - - data = io.BytesIO(p.stdout) - im = Image.open(data) - im.load() - return im diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageMath.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageMath.py deleted file mode 100644 index dfdc50c..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageMath.py +++ /dev/null @@ -1,314 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# a simple math add-on for the Python Imaging Library -# -# History: -# 1999-02-15 fl Original PIL Plus release -# 2005-05-05 fl Simplified and cleaned up for PIL 1.1.6 -# 2005-09-12 fl Fixed int() and float() for Python 2.4.1 -# -# Copyright (c) 1999-2005 by Secret Labs AB -# Copyright (c) 2005 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import builtins - -from . import Image, _imagingmath - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable - from types import CodeType - from typing import Any - - -class _Operand: - """Wraps an image operand, providing standard operators""" - - def __init__(self, im: Image.Image): - self.im = im - - def __fixup(self, im1: _Operand | float) -> Image.Image: - # convert image to suitable mode - if isinstance(im1, _Operand): - # argument was an image. - if im1.im.mode in ("1", "L"): - return im1.im.convert("I") - elif im1.im.mode in ("I", "F"): - return im1.im - else: - msg = f"unsupported mode: {im1.im.mode}" - raise ValueError(msg) - else: - # argument was a constant - if isinstance(im1, (int, float)) and self.im.mode in ("1", "L", "I"): - return Image.new("I", self.im.size, im1) - else: - return Image.new("F", self.im.size, im1) - - def apply( - self, - op: str, - im1: _Operand | float, - im2: _Operand | float | None = None, - mode: str | None = None, - ) -> _Operand: - im_1 = self.__fixup(im1) - if im2 is None: - # unary operation - out = Image.new(mode or im_1.mode, im_1.size, None) - try: - op = getattr(_imagingmath, f"{op}_{im_1.mode}") - except AttributeError as e: - msg = f"bad operand type for '{op}'" - raise TypeError(msg) from e - _imagingmath.unop(op, out.getim(), im_1.getim()) - else: - # binary operation - im_2 = self.__fixup(im2) - if im_1.mode != im_2.mode: - # convert both arguments to floating point - if im_1.mode != "F": - im_1 = im_1.convert("F") - if im_2.mode != "F": - im_2 = im_2.convert("F") - if im_1.size != im_2.size: - # crop both arguments to a common size - size = ( - min(im_1.size[0], im_2.size[0]), - min(im_1.size[1], im_2.size[1]), - ) - if im_1.size != size: - im_1 = im_1.crop((0, 0) + size) - if im_2.size != size: - im_2 = im_2.crop((0, 0) + size) - out = Image.new(mode or im_1.mode, im_1.size, None) - try: - op = getattr(_imagingmath, f"{op}_{im_1.mode}") - except AttributeError as e: - msg = f"bad operand type for '{op}'" - raise TypeError(msg) from e - _imagingmath.binop(op, out.getim(), im_1.getim(), im_2.getim()) - return _Operand(out) - - # unary operators - def __bool__(self) -> bool: - # an image is "true" if it contains at least one non-zero pixel - return self.im.getbbox() is not None - - def __abs__(self) -> _Operand: - return self.apply("abs", self) - - def __pos__(self) -> _Operand: - return self - - def __neg__(self) -> _Operand: - return self.apply("neg", self) - - # binary operators - def __add__(self, other: _Operand | float) -> _Operand: - return self.apply("add", self, other) - - def __radd__(self, other: _Operand | float) -> _Operand: - return self.apply("add", other, self) - - def __sub__(self, other: _Operand | float) -> _Operand: - return self.apply("sub", self, other) - - def __rsub__(self, other: _Operand | float) -> _Operand: - return self.apply("sub", other, self) - - def __mul__(self, other: _Operand | float) -> _Operand: - return self.apply("mul", self, other) - - def __rmul__(self, other: _Operand | float) -> _Operand: - return self.apply("mul", other, self) - - def __truediv__(self, other: _Operand | float) -> _Operand: - return self.apply("div", self, other) - - def __rtruediv__(self, other: _Operand | float) -> _Operand: - return self.apply("div", other, self) - - def __mod__(self, other: _Operand | float) -> _Operand: - return self.apply("mod", self, other) - - def __rmod__(self, other: _Operand | float) -> _Operand: - return self.apply("mod", other, self) - - def __pow__(self, other: _Operand | float) -> _Operand: - return self.apply("pow", self, other) - - def __rpow__(self, other: _Operand | float) -> _Operand: - return self.apply("pow", other, self) - - # bitwise - def __invert__(self) -> _Operand: - return self.apply("invert", self) - - def __and__(self, other: _Operand | float) -> _Operand: - return self.apply("and", self, other) - - def __rand__(self, other: _Operand | float) -> _Operand: - return self.apply("and", other, self) - - def __or__(self, other: _Operand | float) -> _Operand: - return self.apply("or", self, other) - - def __ror__(self, other: _Operand | float) -> _Operand: - return self.apply("or", other, self) - - def __xor__(self, other: _Operand | float) -> _Operand: - return self.apply("xor", self, other) - - def __rxor__(self, other: _Operand | float) -> _Operand: - return self.apply("xor", other, self) - - def __lshift__(self, other: _Operand | float) -> _Operand: - return self.apply("lshift", self, other) - - def __rshift__(self, other: _Operand | float) -> _Operand: - return self.apply("rshift", self, other) - - # logical - def __eq__(self, other: _Operand | float) -> _Operand: # type: ignore[override] - return self.apply("eq", self, other) - - def __ne__(self, other: _Operand | float) -> _Operand: # type: ignore[override] - return self.apply("ne", self, other) - - def __lt__(self, other: _Operand | float) -> _Operand: - return self.apply("lt", self, other) - - def __le__(self, other: _Operand | float) -> _Operand: - return self.apply("le", self, other) - - def __gt__(self, other: _Operand | float) -> _Operand: - return self.apply("gt", self, other) - - def __ge__(self, other: _Operand | float) -> _Operand: - return self.apply("ge", self, other) - - -# conversions -def imagemath_int(self: _Operand) -> _Operand: - return _Operand(self.im.convert("I")) - - -def imagemath_float(self: _Operand) -> _Operand: - return _Operand(self.im.convert("F")) - - -# logical -def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("eq", self, other, mode="I") - - -def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("ne", self, other, mode="I") - - -def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("min", self, other) - - -def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand: - return self.apply("max", self, other) - - -def imagemath_convert(self: _Operand, mode: str) -> _Operand: - return _Operand(self.im.convert(mode)) - - -ops = { - "int": imagemath_int, - "float": imagemath_float, - "equal": imagemath_equal, - "notequal": imagemath_notequal, - "min": imagemath_min, - "max": imagemath_max, - "convert": imagemath_convert, -} - - -def lambda_eval(expression: Callable[[dict[str, Any]], Any], **kw: Any) -> Any: - """ - Returns the result of an image function. - - :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band - images, use the :py:meth:`~PIL.Image.Image.split` method or - :py:func:`~PIL.Image.merge` function. - - :param expression: A function that receives a dictionary. - :param **kw: Values to add to the function's dictionary. - :return: The expression result. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - """ - - args: dict[str, Any] = ops.copy() - args.update(kw) - for k, v in args.items(): - if isinstance(v, Image.Image): - args[k] = _Operand(v) - - out = expression(args) - try: - return out.im - except AttributeError: - return out - - -def unsafe_eval(expression: str, **kw: Any) -> Any: - """ - Evaluates an image expression. This uses Python's ``eval()`` function to process - the expression string, and carries the security risks of doing so. It is not - recommended to process expressions without considering this. - :py:meth:`~lambda_eval` is a more secure alternative. - - :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band - images, use the :py:meth:`~PIL.Image.Image.split` method or - :py:func:`~PIL.Image.merge` function. - - :param expression: A string containing a Python-style expression. - :param **kw: Values to add to the evaluation context. - :return: The evaluated expression. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - """ - - # build execution namespace - args: dict[str, Any] = ops.copy() - for k in kw: - if "__" in k or hasattr(builtins, k): - msg = f"'{k}' not allowed" - raise ValueError(msg) - - args.update(kw) - for k, v in args.items(): - if isinstance(v, Image.Image): - args[k] = _Operand(v) - - compiled_code = compile(expression, "<string>", "eval") - - def scan(code: CodeType) -> None: - for const in code.co_consts: - if type(const) is type(compiled_code): - scan(const) - - for name in code.co_names: - if name not in args and name != "abs": - msg = f"'{name}' not allowed" - raise ValueError(msg) - - scan(compiled_code) - out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) - try: - return out.im - except AttributeError: - return out diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageMode.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageMode.py deleted file mode 100644 index b7c6c86..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageMode.py +++ /dev/null @@ -1,85 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard mode descriptors -# -# History: -# 2006-03-20 fl Added -# -# Copyright (c) 2006 by Secret Labs AB. -# Copyright (c) 2006 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import sys -from functools import lru_cache -from typing import NamedTuple - - -class ModeDescriptor(NamedTuple): - """Wrapper for mode strings.""" - - mode: str - bands: tuple[str, ...] - basemode: str - basetype: str - typestr: str - - def __str__(self) -> str: - return self.mode - - -@lru_cache -def getmode(mode: str) -> ModeDescriptor: - """Gets a mode descriptor for the given mode.""" - endian = "<" if sys.byteorder == "little" else ">" - - modes = { - # core modes - # Bits need to be extended to bytes - "1": ("L", "L", ("1",), "|b1"), - "L": ("L", "L", ("L",), "|u1"), - "I": ("L", "I", ("I",), f"{endian}i4"), - "F": ("L", "F", ("F",), f"{endian}f4"), - "P": ("P", "L", ("P",), "|u1"), - "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), - "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), - "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), - # UNDONE - unsigned |u1i1i1 - "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), - "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), - # extra experimental modes - "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "LA": ("L", "L", ("L", "A"), "|u1"), - "La": ("L", "L", ("L", "a"), "|u1"), - "PA": ("RGB", "L", ("P", "A"), "|u1"), - } - if mode in modes: - base_mode, base_type, bands, type_str = modes[mode] - return ModeDescriptor(mode, bands, base_mode, base_type, type_str) - - mapping_modes = { - # I;16 == I;16L, and I;32 == I;32L - "I;16": "<u2", - "I;16S": "<i2", - "I;16L": "<u2", - "I;16LS": "<i2", - "I;16B": ">u2", - "I;16BS": ">i2", - "I;16N": f"{endian}u2", - "I;16NS": f"{endian}i2", - "I;32": "<u4", - "I;32B": ">u4", - "I;32L": "<u4", - "I;32S": "<i4", - "I;32BS": ">i4", - "I;32LS": "<i4", - } - - type_str = mapping_modes[mode] - return ModeDescriptor(mode, ("I",), "L", "L", type_str) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageMorph.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageMorph.py deleted file mode 100644 index 9fcd8d7..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageMorph.py +++ /dev/null @@ -1,317 +0,0 @@ -# A binary morphology add-on for the Python Imaging Library -# -# History: -# 2014-06-04 Initial version. -# -# Copyright (c) 2014 Dov Grobgeld <dov.grobgeld@gmail.com> -from __future__ import annotations - -import re - -from . import Image, _imagingmorph - -LUT_SIZE = 1 << 9 - -# fmt: off -ROTATION_MATRIX = [ - 6, 3, 0, - 7, 4, 1, - 8, 5, 2, -] -MIRROR_MATRIX = [ - 2, 1, 0, - 5, 4, 3, - 8, 7, 6, -] -# fmt: on - - -class LutBuilder: - """A class for building a MorphLut from a descriptive language - - The input patterns is a list of a strings sequences like these:: - - 4:(... - .1. - 111)->1 - - (whitespaces including linebreaks are ignored). The option 4 - describes a series of symmetry operations (in this case a - 4-rotation), the pattern is described by: - - - . or X - Ignore - - 1 - Pixel is on - - 0 - Pixel is off - - The result of the operation is described after "->" string. - - The default is to return the current pixel value, which is - returned if no other match is found. - - Operations: - - - 4 - 4 way rotation - - N - Negate - - 1 - Dummy op for no other operation (an op must always be given) - - M - Mirroring - - Example:: - - lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) - lut = lb.build_lut() - - """ - - def __init__( - self, patterns: list[str] | None = None, op_name: str | None = None - ) -> None: - """ - :param patterns: A list of input patterns, or None. - :param op_name: The name of a known pattern. One of "corner", "dilation4", - "dilation8", "erosion4", "erosion8" or "edge". - :exception Exception: If the op_name is not recognized. - """ - self.lut: bytearray | None = None - if op_name is not None: - known_patterns = { - "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], - "dilation4": ["4:(... .0. .1.)->1"], - "dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"], - "erosion4": ["4:(... .1. .0.)->0"], - "erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"], - "edge": [ - "1:(... ... ...)->0", - "4:(.0. .1. ...)->1", - "4:(01. .1. ...)->1", - ], - } - if op_name not in known_patterns: - msg = f"Unknown pattern {op_name}!" - raise Exception(msg) - - self.patterns = known_patterns[op_name] - elif patterns is not None: - self.patterns = patterns - else: - self.patterns = [] - - def add_patterns(self, patterns: list[str]) -> None: - """ - Append to list of patterns. - - :param patterns: Additional patterns. - """ - self.patterns += patterns - - def build_default_lut(self) -> bytearray: - """ - Set the current LUT, and return it. - - This is the default LUT that patterns will be applied against when building. - """ - symbols = [0, 1] - m = 1 << 4 # pos of current pixel - self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) - return self.lut - - def get_lut(self) -> bytearray | None: - """ - Returns the current LUT - """ - return self.lut - - def _string_permute(self, pattern: str, permutation: list[int]) -> str: - """Takes a pattern and a permutation and returns the - string permuted according to the permutation list. - """ - assert len(permutation) == 9 - return "".join(pattern[p] for p in permutation) - - def _pattern_permute( - self, basic_pattern: str, options: str, basic_result: int - ) -> list[tuple[str, int]]: - """Takes a basic pattern and its result and clones - the pattern according to the modifications described in the $options - parameter. It returns a list of all cloned patterns.""" - patterns = [(basic_pattern, basic_result)] - - # rotations - if "4" in options: - res = patterns[-1][1] - for i in range(4): - patterns.append( - (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res) - ) - # mirror - if "M" in options: - n = len(patterns) - for pattern, res in patterns[:n]: - patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res)) - - # negate - if "N" in options: - n = len(patterns) - for pattern, res in patterns[:n]: - # Swap 0 and 1 - pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1") - res = 1 - int(res) - patterns.append((pattern, res)) - - return patterns - - def build_lut(self) -> bytearray: - """Compile all patterns into a morphology LUT, and return it. - - This is the data to be passed into MorphOp.""" - self.build_default_lut() - assert self.lut is not None - patterns = [] - - # Parse and create symmetries of the patterns strings - for p in self.patterns: - m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) - if not m: - msg = 'Syntax error in pattern "' + p + '"' - raise Exception(msg) - options = m.group(1) - pattern = m.group(2) - result = int(m.group(3)) - - # Get rid of spaces - pattern = pattern.replace(" ", "").replace("\n", "") - - patterns += self._pattern_permute(pattern, options, result) - - # Compile the patterns into regular expressions for speed - compiled_patterns = [] - for pattern in patterns: - p = pattern[0].replace(".", "X").replace("X", "[01]") - compiled_patterns.append((re.compile(p), pattern[1])) - - # Step through table and find patterns that match. - # Note that all the patterns are searched. The last one found takes priority - for i in range(LUT_SIZE): - # Build the bit pattern - bitpattern = bin(i)[2:] - bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] - - for pattern, r in compiled_patterns: - if pattern.match(bitpattern): - self.lut[i] = [0, 1][r] - - return self.lut - - -class MorphOp: - """A class for binary morphological operators""" - - def __init__( - self, - lut: bytearray | None = None, - op_name: str | None = None, - patterns: list[str] | None = None, - ) -> None: - """Create a binary morphological operator. - - If the LUT is not provided, then it is built using LutBuilder from the op_name - or the patterns. - - :param lut: The LUT data. - :param patterns: A list of input patterns, or None. - :param op_name: The name of a known pattern. One of "corner", "dilation4", - "dilation8", "erosion4", "erosion8", "edge". - :exception Exception: If the op_name is not recognized. - """ - if patterns is None and op_name is None: - self.lut = lut - else: - self.lut = LutBuilder(patterns, op_name).build_lut() - - def apply(self, image: Image.Image) -> tuple[int, Image.Image]: - """Run a single morphological operation on an image. - - Returns a tuple of the number of changed pixels and the - morphed image. - - :param image: A 1-mode or L-mode image. - :exception Exception: If the current operator is None. - :exception ValueError: If the image is not 1 or L mode.""" - if self.lut is None: - msg = "No operator loaded" - raise Exception(msg) - - if image.mode not in ("1", "L"): - msg = "Image mode must be 1 or L" - raise ValueError(msg) - outimage = Image.new(image.mode, image.size) - count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim()) - return count, outimage - - def match(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of coordinates matching the morphological operation on - an image. - - Returns a list of tuples of (x,y) coordinates of all matching pixels. See - :ref:`coordinate-system`. - - :param image: A 1-mode or L-mode image. - :exception Exception: If the current operator is None. - :exception ValueError: If the image is not 1 or L mode.""" - if self.lut is None: - msg = "No operator loaded" - raise Exception(msg) - - if image.mode not in ("1", "L"): - msg = "Image mode must be 1 or L" - raise ValueError(msg) - return _imagingmorph.match(bytes(self.lut), image.getim()) - - def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of all turned on pixels in a 1 or L mode image. - - Returns a list of tuples of (x,y) coordinates of all non-empty pixels. See - :ref:`coordinate-system`. - - :param image: A 1-mode or L-mode image. - :exception ValueError: If the image is not 1 or L mode.""" - - if image.mode not in ("1", "L"): - msg = "Image mode must be 1 or L" - raise ValueError(msg) - return _imagingmorph.get_on_pixels(image.getim()) - - def load_lut(self, filename: str) -> None: - """ - Load an operator from an mrl file - - :param filename: The file to read from. - :exception Exception: If the length of the file data is not 512. - """ - with open(filename, "rb") as f: - self.lut = bytearray(f.read()) - - if len(self.lut) != LUT_SIZE: - self.lut = None - msg = "Wrong size operator file!" - raise Exception(msg) - - def save_lut(self, filename: str) -> None: - """ - Save an operator to an mrl file. - - :param filename: The destination file. - :exception Exception: If the current operator is None. - """ - if self.lut is None: - msg = "No operator loaded" - raise Exception(msg) - with open(filename, "wb") as f: - f.write(self.lut) - - def set_lut(self, lut: bytearray | None) -> None: - """ - Set the LUT from an external source - - :param lut: A new LUT. - """ - self.lut = lut diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageOps.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageOps.py deleted file mode 100644 index 42b10bd..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageOps.py +++ /dev/null @@ -1,746 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# standard image operations -# -# History: -# 2001-10-20 fl Created -# 2001-10-23 fl Added autocontrast operator -# 2001-12-18 fl Added Kevin's fit operator -# 2004-03-14 fl Fixed potential division by zero in equalize -# 2005-05-05 fl Fixed equalize for low number of values -# -# Copyright (c) 2001-2004 by Secret Labs AB -# Copyright (c) 2001-2004 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import functools -import operator -import re -from collections.abc import Sequence -from typing import Literal, Protocol, cast, overload - -from . import ExifTags, Image, ImagePalette - -# -# helpers - - -def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: - if isinstance(border, tuple): - if len(border) == 2: - left, top = right, bottom = border - elif len(border) == 4: - left, top, right, bottom = border - else: - left = top = right = bottom = border - return left, top, right, bottom - - -def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: - if isinstance(color, str): - from . import ImageColor - - color = ImageColor.getcolor(color, mode) - return color - - -def _lut(image: Image.Image, lut: list[int]) -> Image.Image: - if image.mode == "P": - # FIXME: apply to lookup table, not image data - msg = "mode P support coming soon" - raise NotImplementedError(msg) - elif image.mode in ("L", "RGB"): - if image.mode == "RGB" and len(lut) == 256: - lut = lut + lut + lut - return image.point(lut) - else: - msg = f"not supported for mode {image.mode}" - raise OSError(msg) - - -# -# actions - - -def autocontrast( - image: Image.Image, - cutoff: float | tuple[float, float] = 0, - ignore: int | Sequence[int] | None = None, - mask: Image.Image | None = None, - preserve_tone: bool = False, -) -> Image.Image: - """ - Maximize (normalize) image contrast. This function calculates a - histogram of the input image (or mask region), removes ``cutoff`` percent of the - lightest and darkest pixels from the histogram, and remaps the image - so that the darkest pixel becomes black (0), and the lightest - becomes white (255). - - :param image: The image to process. - :param cutoff: The percent to cut off from the histogram on the low and - high ends. Either a tuple of (low, high), or a single - number for both. - :param ignore: The background pixel value (use None for no background). - :param mask: Histogram used in contrast operation is computed using pixels - within the mask. If no mask is given the entire image is used - for histogram computation. - :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast. - - .. versionadded:: 8.2.0 - - :return: An image. - """ - if preserve_tone: - histogram = image.convert("L").histogram(mask) - else: - histogram = image.histogram(mask) - - lut = [] - for layer in range(0, len(histogram), 256): - h = histogram[layer : layer + 256] - if ignore is not None: - # get rid of outliers - if isinstance(ignore, int): - h[ignore] = 0 - else: - for ix in ignore: - h[ix] = 0 - if cutoff: - # cut off pixels from both ends of the histogram - if not isinstance(cutoff, tuple): - cutoff = (cutoff, cutoff) - # get number of pixels - n = 0 - for ix in range(256): - n = n + h[ix] - # remove cutoff% pixels from the low end - cut = int(n * cutoff[0] // 100) - for lo in range(256): - if cut > h[lo]: - cut = cut - h[lo] - h[lo] = 0 - else: - h[lo] -= cut - cut = 0 - if cut <= 0: - break - # remove cutoff% samples from the high end - cut = int(n * cutoff[1] // 100) - for hi in range(255, -1, -1): - if cut > h[hi]: - cut = cut - h[hi] - h[hi] = 0 - else: - h[hi] -= cut - cut = 0 - if cut <= 0: - break - # find lowest/highest samples after preprocessing - for lo in range(256): - if h[lo]: - break - for hi in range(255, -1, -1): - if h[hi]: - break - if hi <= lo: - # don't bother - lut.extend(list(range(256))) - else: - scale = 255.0 / (hi - lo) - offset = -lo * scale - for ix in range(256): - ix = int(ix * scale + offset) - if ix < 0: - ix = 0 - elif ix > 255: - ix = 255 - lut.append(ix) - return _lut(image, lut) - - -def colorize( - image: Image.Image, - black: str | tuple[int, ...], - white: str | tuple[int, ...], - mid: str | int | tuple[int, ...] | None = None, - blackpoint: int = 0, - whitepoint: int = 255, - midpoint: int = 127, -) -> Image.Image: - """ - Colorize grayscale image. - This function calculates a color wedge which maps all black pixels in - the source image to the first color and all white pixels to the - second color. If ``mid`` is specified, it uses three-color mapping. - The ``black`` and ``white`` arguments should be RGB tuples or color names; - optionally you can use three-color mapping by also specifying ``mid``. - Mapping positions for any of the colors can be specified - (e.g. ``blackpoint``), where these parameters are the integer - value corresponding to where the corresponding color should be mapped. - These parameters must have logical order, such that - ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified). - - :param image: The image to colorize. - :param black: The color to use for black input pixels. - :param white: The color to use for white input pixels. - :param mid: The color to use for midtone input pixels. - :param blackpoint: an int value [0, 255] for the black mapping. - :param whitepoint: an int value [0, 255] for the white mapping. - :param midpoint: an int value [0, 255] for the midtone mapping. - :return: An image. - """ - - # Initial asserts - assert image.mode == "L" - if mid is None: - assert 0 <= blackpoint <= whitepoint <= 255 - else: - assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 - - # Define colors from arguments - rgb_black = cast(Sequence[int], _color(black, "RGB")) - rgb_white = cast(Sequence[int], _color(white, "RGB")) - rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None - - # Empty lists for the mapping - red = [] - green = [] - blue = [] - - # Create the low-end values - for i in range(blackpoint): - red.append(rgb_black[0]) - green.append(rgb_black[1]) - blue.append(rgb_black[2]) - - # Create the mapping (2-color) - if rgb_mid is None: - range_map = range(whitepoint - blackpoint) - - for i in range_map: - red.append( - rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) - ) - green.append( - rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) - ) - blue.append( - rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) - ) - - # Create the mapping (3-color) - else: - range_map1 = range(midpoint - blackpoint) - range_map2 = range(whitepoint - midpoint) - - for i in range_map1: - red.append( - rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) - ) - green.append( - rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) - ) - blue.append( - rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) - ) - for i in range_map2: - red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) - green.append( - rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) - ) - blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) - - # Create the high-end values - for i in range(256 - whitepoint): - red.append(rgb_white[0]) - green.append(rgb_white[1]) - blue.append(rgb_white[2]) - - # Return converted image - image = image.convert("RGB") - return _lut(image, red + green + blue) - - -def contain( - image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC -) -> Image.Image: - """ - Returns a resized version of the image, set to the maximum width and height - within the requested size, while maintaining the original aspect ratio. - - :param image: The image to resize. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :return: An image. - """ - - im_ratio = image.width / image.height - dest_ratio = size[0] / size[1] - - if im_ratio != dest_ratio: - if im_ratio > dest_ratio: - new_height = round(image.height / image.width * size[0]) - if new_height != size[1]: - size = (size[0], new_height) - else: - new_width = round(image.width / image.height * size[1]) - if new_width != size[0]: - size = (new_width, size[1]) - return image.resize(size, resample=method) - - -def cover( - image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC -) -> Image.Image: - """ - Returns a resized version of the image, so that the requested size is - covered, while maintaining the original aspect ratio. - - :param image: The image to resize. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :return: An image. - """ - - im_ratio = image.width / image.height - dest_ratio = size[0] / size[1] - - if im_ratio != dest_ratio: - if im_ratio < dest_ratio: - new_height = round(image.height / image.width * size[0]) - if new_height != size[1]: - size = (size[0], new_height) - else: - new_width = round(image.width / image.height * size[1]) - if new_width != size[0]: - size = (new_width, size[1]) - return image.resize(size, resample=method) - - -def pad( - image: Image.Image, - size: tuple[int, int], - method: int = Image.Resampling.BICUBIC, - color: str | int | tuple[int, ...] | None = None, - centering: tuple[float, float] = (0.5, 0.5), -) -> Image.Image: - """ - Returns a resized and padded version of the image, expanded to fill the - requested aspect ratio and size. - - :param image: The image to resize and crop. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :param color: The background color of the padded image. - :param centering: Control the position of the original image within the - padded version. - - (0.5, 0.5) will keep the image centered - (0, 0) will keep the image aligned to the top left - (1, 1) will keep the image aligned to the bottom - right - :return: An image. - """ - - resized = contain(image, size, method) - if resized.size == size: - out = resized - else: - out = Image.new(image.mode, size, color) - if resized.palette: - palette = resized.getpalette() - if palette is not None: - out.putpalette(palette) - if resized.width != size[0]: - x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) - out.paste(resized, (x, 0)) - else: - y = round((size[1] - resized.height) * max(0, min(centering[1], 1))) - out.paste(resized, (0, y)) - return out - - -def crop(image: Image.Image, border: int = 0) -> Image.Image: - """ - Remove border from image. The same amount of pixels are removed - from all four sides. This function works on all image modes. - - .. seealso:: :py:meth:`~PIL.Image.Image.crop` - - :param image: The image to crop. - :param border: The number of pixels to remove. - :return: An image. - """ - left, top, right, bottom = _border(border) - return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) - - -def scale( - image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC -) -> Image.Image: - """ - Returns a rescaled image by a specific factor given in parameter. - A factor greater than 1 expands the image, between 0 and 1 contracts the - image. - - :param image: The image to rescale. - :param factor: The expansion factor, as a float. - :param resample: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - if factor == 1: - return image.copy() - elif factor <= 0: - msg = "the factor must be greater than 0" - raise ValueError(msg) - else: - size = (round(factor * image.width), round(factor * image.height)) - return image.resize(size, resample) - - -class SupportsGetMesh(Protocol): - """ - An object that supports the ``getmesh`` method, taking an image as an - argument, and returning a list of tuples. Each tuple contains two tuples, - the source box as a tuple of 4 integers, and a tuple of 8 integers for the - final quadrilateral, in order of top left, bottom left, bottom right, top - right. - """ - - def getmesh( - self, image: Image.Image - ) -> list[ - tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] - ]: ... - - -def deform( - image: Image.Image, - deformer: SupportsGetMesh, - resample: int = Image.Resampling.BILINEAR, -) -> Image.Image: - """ - Deform the image. - - :param image: The image to deform. - :param deformer: A deformer object. Any object that implements a - ``getmesh`` method can be used. - :param resample: An optional resampling filter. Same values possible as - in the PIL.Image.transform function. - :return: An image. - """ - return image.transform( - image.size, Image.Transform.MESH, deformer.getmesh(image), resample - ) - - -def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: - """ - Equalize the image histogram. This function applies a non-linear - mapping to the input image, in order to create a uniform - distribution of grayscale values in the output image. - - :param image: The image to equalize. - :param mask: An optional mask. If given, only the pixels selected by - the mask are included in the analysis. - :return: An image. - """ - if image.mode == "P": - image = image.convert("RGB") - h = image.histogram(mask) - lut = [] - for b in range(0, len(h), 256): - histo = [_f for _f in h[b : b + 256] if _f] - if len(histo) <= 1: - lut.extend(list(range(256))) - else: - step = (functools.reduce(operator.add, histo) - histo[-1]) // 255 - if not step: - lut.extend(list(range(256))) - else: - n = step // 2 - for i in range(256): - lut.append(n // step) - n = n + h[i + b] - return _lut(image, lut) - - -def expand( - image: Image.Image, - border: int | tuple[int, ...] = 0, - fill: str | int | tuple[int, ...] = 0, -) -> Image.Image: - """ - Add border to the image - - :param image: The image to expand. - :param border: Border width, in pixels. - :param fill: Pixel fill value (a color value). Default is 0 (black). - :return: An image. - """ - left, top, right, bottom = _border(border) - width = left + image.size[0] + right - height = top + image.size[1] + bottom - color = _color(fill, image.mode) - if image.palette: - mode = image.palette.mode - palette = ImagePalette.ImagePalette(mode, image.getpalette(mode)) - if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4): - color = palette.getcolor(color) - else: - palette = None - out = Image.new(image.mode, (width, height), color) - if palette: - out.putpalette(palette.palette, mode) - out.paste(image, (left, top)) - return out - - -def fit( - image: Image.Image, - size: tuple[int, int], - method: int = Image.Resampling.BICUBIC, - bleed: float = 0.0, - centering: tuple[float, float] = (0.5, 0.5), -) -> Image.Image: - """ - Returns a resized and cropped version of the image, cropped to the - requested aspect ratio and size. - - This function was contributed by Kevin Cazabon. - - :param image: The image to resize and crop. - :param size: The requested output size in pixels, given as a - (width, height) tuple. - :param method: Resampling method to use. Default is - :py:attr:`~PIL.Image.Resampling.BICUBIC`. - See :ref:`concept-filters`. - :param bleed: Remove a border around the outside of the image from all - four edges. The value is a decimal percentage (use 0.01 for - one percent). The default value is 0 (no border). - Cannot be greater than or equal to 0.5. - :param centering: Control the cropping position. Use (0.5, 0.5) for - center cropping (e.g. if cropping the width, take 50% off - of the left side, and therefore 50% off the right side). - (0.0, 0.0) will crop from the top left corner (i.e. if - cropping the width, take all of the crop off of the right - side, and if cropping the height, take all of it off the - bottom). (1.0, 0.0) will crop from the bottom left - corner, etc. (i.e. if cropping the width, take all of the - crop off the left side, and if cropping the height take - none from the top, and therefore all off the bottom). - :return: An image. - """ - - # by Kevin Cazabon, Feb 17/2000 - # kevin@cazabon.com - # https://www.cazabon.com - - centering_x, centering_y = centering - - if not 0.0 <= centering_x <= 1.0: - centering_x = 0.5 - if not 0.0 <= centering_y <= 1.0: - centering_y = 0.5 - - if not 0.0 <= bleed < 0.5: - bleed = 0.0 - - # calculate the area to use for resizing and cropping, subtracting - # the 'bleed' around the edges - - # number of pixels to trim off on Top and Bottom, Left and Right - bleed_pixels = (bleed * image.size[0], bleed * image.size[1]) - - live_size = ( - image.size[0] - bleed_pixels[0] * 2, - image.size[1] - bleed_pixels[1] * 2, - ) - - # calculate the aspect ratio of the live_size - live_size_ratio = live_size[0] / live_size[1] - - # calculate the aspect ratio of the output image - output_ratio = size[0] / size[1] - - # figure out if the sides or top/bottom will be cropped off - if live_size_ratio == output_ratio: - # live_size is already the needed ratio - crop_width = live_size[0] - crop_height = live_size[1] - elif live_size_ratio >= output_ratio: - # live_size is wider than what's needed, crop the sides - crop_width = output_ratio * live_size[1] - crop_height = live_size[1] - else: - # live_size is taller than what's needed, crop the top and bottom - crop_width = live_size[0] - crop_height = live_size[0] / output_ratio - - # make the crop - crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x - crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y - - crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) - - # resize the image and return it - return image.resize(size, method, box=crop) - - -def flip(image: Image.Image) -> Image.Image: - """ - Flip the image vertically (top to bottom). - - :param image: The image to flip. - :return: An image. - """ - return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) - - -def grayscale(image: Image.Image) -> Image.Image: - """ - Convert the image to grayscale. - - :param image: The image to convert. - :return: An image. - """ - return image.convert("L") - - -def invert(image: Image.Image) -> Image.Image: - """ - Invert (negate) the image. - - :param image: The image to invert. - :return: An image. - """ - lut = list(range(255, -1, -1)) - return image.point(lut) if image.mode == "1" else _lut(image, lut) - - -def mirror(image: Image.Image) -> Image.Image: - """ - Flip image horizontally (left to right). - - :param image: The image to mirror. - :return: An image. - """ - return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - - -def posterize(image: Image.Image, bits: int) -> Image.Image: - """ - Reduce the number of bits for each color channel. - - :param image: The image to posterize. - :param bits: The number of bits to keep for each channel (1-8). - :return: An image. - """ - mask = ~(2 ** (8 - bits) - 1) - lut = [i & mask for i in range(256)] - return _lut(image, lut) - - -def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: - """ - Invert all pixel values above a threshold. - - :param image: The image to solarize. - :param threshold: All pixels above this grayscale level are inverted. - :return: An image. - """ - lut = [] - for i in range(256): - if i < threshold: - lut.append(i) - else: - lut.append(255 - i) - return _lut(image, lut) - - -@overload -def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ... - - -@overload -def exif_transpose( - image: Image.Image, *, in_place: Literal[False] = False -) -> Image.Image: ... - - -def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: - """ - If an image has an EXIF Orientation tag, other than 1, transpose the image - accordingly, and remove the orientation data. - - :param image: The image to transpose. - :param in_place: Boolean. Keyword-only argument. - If ``True``, the original image is modified in-place, and ``None`` is returned. - If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned - with the transposition applied. If there is no transposition, a copy of the - image will be returned. - """ - image.load() - image_exif = image.getexif() - orientation = image_exif.get(ExifTags.Base.Orientation, 1) - method = { - 2: Image.Transpose.FLIP_LEFT_RIGHT, - 3: Image.Transpose.ROTATE_180, - 4: Image.Transpose.FLIP_TOP_BOTTOM, - 5: Image.Transpose.TRANSPOSE, - 6: Image.Transpose.ROTATE_270, - 7: Image.Transpose.TRANSVERSE, - 8: Image.Transpose.ROTATE_90, - }.get(orientation) - if method is not None: - if in_place: - image.im = image.im.transpose(method) - image._size = image.im.size - else: - transposed_image = image.transpose(method) - exif_image = image if in_place else transposed_image - - exif = exif_image.getexif() - if ExifTags.Base.Orientation in exif: - del exif[ExifTags.Base.Orientation] - if "exif" in exif_image.info: - exif_image.info["exif"] = exif.tobytes() - elif "Raw profile type exif" in exif_image.info: - exif_image.info["Raw profile type exif"] = exif.tobytes().hex() - for key in ("XML:com.adobe.xmp", "xmp"): - if key in exif_image.info: - for pattern in ( - r'tiff:Orientation="([0-9])"', - r"<tiff:Orientation>([0-9])</tiff:Orientation>", - ): - value = exif_image.info[key] - if isinstance(value, str): - value = re.sub(pattern, "", value) - elif isinstance(value, tuple): - value = tuple( - re.sub(pattern.encode(), b"", v) for v in value - ) - else: - value = re.sub(pattern.encode(), b"", value) - exif_image.info[key] = value - if not in_place: - return transposed_image - elif not in_place: - return image.copy() - return None diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImagePalette.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImagePalette.py deleted file mode 100644 index 2abbd46..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImagePalette.py +++ /dev/null @@ -1,290 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# image palette object -# -# History: -# 1996-03-11 fl Rewritten. -# 1997-01-03 fl Up and running. -# 1997-08-23 fl Added load hack -# 2001-04-16 fl Fixed randint shadow bug in random() -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import array -from collections.abc import Sequence -from typing import IO - -from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile - -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import Image - - -class ImagePalette: - """ - Color palette for palette mapped images - - :param mode: The mode to use for the palette. See: - :ref:`concept-modes`. Defaults to "RGB" - :param palette: An optional palette. If given, it must be a bytearray, - an array or a list of ints between 0-255. The list must consist of - all channels for one color followed by the next color (e.g. RGBRGBRGB). - Defaults to an empty palette. - """ - - def __init__( - self, - mode: str = "RGB", - palette: Sequence[int] | bytes | bytearray | None = None, - ) -> None: - self.mode = mode - self.rawmode: str | None = None # if set, palette contains raw data - self.palette = palette or bytearray() - self.dirty: int | None = None - - @property - def palette(self) -> Sequence[int] | bytes | bytearray: - return self._palette - - @palette.setter - def palette(self, palette: Sequence[int] | bytes | bytearray) -> None: - self._colors: dict[tuple[int, ...], int] | None = None - self._palette = palette - - @property - def colors(self) -> dict[tuple[int, ...], int]: - if self._colors is None: - mode_len = len(self.mode) - self._colors = {} - for i in range(0, len(self.palette), mode_len): - color = tuple(self.palette[i : i + mode_len]) - if color in self._colors: - continue - self._colors[color] = i // mode_len - return self._colors - - @colors.setter - def colors(self, colors: dict[tuple[int, ...], int]) -> None: - self._colors = colors - - def copy(self) -> ImagePalette: - new = ImagePalette() - - new.mode = self.mode - new.rawmode = self.rawmode - if self.palette is not None: - new.palette = self.palette[:] - new.dirty = self.dirty - - return new - - def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]: - """ - Get palette contents in format suitable for the low-level - ``im.putpalette`` primitive. - - .. warning:: This method is experimental. - """ - if self.rawmode: - return self.rawmode, self.palette - return self.mode, self.tobytes() - - def tobytes(self) -> bytes: - """Convert palette to bytes. - - .. warning:: This method is experimental. - """ - if self.rawmode: - msg = "palette contains raw palette data" - raise ValueError(msg) - if isinstance(self.palette, bytes): - return self.palette - arr = array.array("B", self.palette) - return arr.tobytes() - - # Declare tostring as an alias for tobytes - tostring = tobytes - - def _new_color_index( - self, image: Image.Image | None = None, e: Exception | None = None - ) -> int: - if not isinstance(self.palette, bytearray): - self._palette = bytearray(self.palette) - index = len(self.palette) // len(self.mode) - special_colors: tuple[int | tuple[int, ...] | None, ...] = () - if image: - special_colors = ( - image.info.get("background"), - image.info.get("transparency"), - ) - while index in special_colors: - index += 1 - if index >= 256: - if image: - # Search for an unused index - for i, count in reversed(list(enumerate(image.histogram()))): - if count == 0 and i not in special_colors: - index = i - break - if index >= 256: - msg = "cannot allocate more than 256 colors" - raise ValueError(msg) from e - return index - - def getcolor( - self, - color: tuple[int, ...], - image: Image.Image | None = None, - ) -> int: - """Given an rgb tuple, allocate palette entry. - - .. warning:: This method is experimental. - """ - if self.rawmode: - msg = "palette contains raw palette data" - raise ValueError(msg) - if isinstance(color, tuple): - if self.mode == "RGB": - if len(color) == 4: - if color[3] != 255: - msg = "cannot add non-opaque RGBA color to RGB palette" - raise ValueError(msg) - color = color[:3] - elif self.mode == "RGBA": - if len(color) == 3: - color += (255,) - try: - return self.colors[color] - except KeyError as e: - # allocate new color slot - index = self._new_color_index(image, e) - assert isinstance(self._palette, bytearray) - self.colors[color] = index - mode_len = len(self.mode) - if index * mode_len < len(self.palette): - self._palette = ( - self._palette[: index * mode_len] - + bytes(color) - + self._palette[index * mode_len + mode_len :] - ) - else: - self._palette += bytes(color) - self.dirty = 1 - return index - else: - msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable] - raise ValueError(msg) - - def save(self, fp: str | IO[str]) -> None: - """Save palette to text file. - - .. warning:: This method is experimental. - """ - if self.rawmode: - msg = "palette contains raw palette data" - raise ValueError(msg) - open_fp = False - if isinstance(fp, str): - fp = open(fp, "w") - open_fp = True - try: - fp.write("# Palette\n") - fp.write(f"# Mode: {self.mode}\n") - palette_len = len(self.palette) - for i in range(256): - fp.write(f"{i}") - for j in range(i * len(self.mode), (i + 1) * len(self.mode)): - fp.write(f" {self.palette[j] if j < palette_len else 0}") - fp.write("\n") - finally: - if open_fp: - fp.close() - - -# -------------------------------------------------------------------- -# Internal - - -def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette: - palette = ImagePalette() - palette.rawmode = rawmode - palette.palette = data - palette.dirty = 1 - return palette - - -# -------------------------------------------------------------------- -# Factories - - -def make_linear_lut(black: int, white: float) -> list[int]: - if black == 0: - return [int(white * i // 255) for i in range(256)] - - msg = "unavailable when black is non-zero" - raise NotImplementedError(msg) # FIXME - - -def make_gamma_lut(exp: float) -> list[int]: - return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] - - -def negative(mode: str = "RGB") -> ImagePalette: - palette = list(range(256 * len(mode))) - palette.reverse() - return ImagePalette(mode, [i // len(mode) for i in palette]) - - -def random(mode: str = "RGB") -> ImagePalette: - from random import randint - - palette = [randint(0, 255) for _ in range(256 * len(mode))] - return ImagePalette(mode, palette) - - -def sepia(white: str = "#fff0c0") -> ImagePalette: - bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] - return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) - - -def wedge(mode: str = "RGB") -> ImagePalette: - palette = list(range(256 * len(mode))) - return ImagePalette(mode, [i // len(mode) for i in palette]) - - -def load(filename: str) -> tuple[bytes, str]: - # FIXME: supports GIMP gradients only - - with open(filename, "rb") as fp: - paletteHandlers: list[ - type[ - GimpPaletteFile.GimpPaletteFile - | GimpGradientFile.GimpGradientFile - | PaletteFile.PaletteFile - ] - ] = [ - GimpPaletteFile.GimpPaletteFile, - GimpGradientFile.GimpGradientFile, - PaletteFile.PaletteFile, - ] - for paletteHandler in paletteHandlers: - try: - fp.seek(0) - lut = paletteHandler(fp).getpalette() - if lut: - break - except (SyntaxError, ValueError): - pass - else: - msg = "cannot load palette" - raise OSError(msg) - - return lut # data, rawmode diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImagePath.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImagePath.py deleted file mode 100644 index 77e8a60..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImagePath.py +++ /dev/null @@ -1,20 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# path interface -# -# History: -# 1996-11-04 fl Created -# 2002-04-14 fl Added documentation stub class -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image - -Path = Image.core.path diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageQt.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageQt.py deleted file mode 100644 index af4d074..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageQt.py +++ /dev/null @@ -1,219 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a simple Qt image interface. -# -# history: -# 2006-06-03 fl: created -# 2006-06-04 fl: inherit from QImage instead of wrapping it -# 2006-06-05 fl: removed toimage helper; move string support to ImageQt -# 2013-11-13 fl: add support for Qt5 (aurelien.ballier@cyclonit.com) -# -# Copyright (c) 2006 by Secret Labs AB -# Copyright (c) 2006 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import sys -from io import BytesIO - -from . import Image -from ._util import is_path - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any - - from . import ImageFile - - QBuffer: type - -qt_version: str | None -qt_versions = [ - ["6", "PyQt6"], - ["side6", "PySide6"], -] - -# If a version has already been imported, attempt it first -qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) -for version, qt_module in qt_versions: - try: - qRgba: Callable[[int, int, int, int], int] - if qt_module == "PyQt6": - from PyQt6.QtCore import QBuffer, QByteArray, QIODevice - from PyQt6.QtGui import QImage, QPixmap, qRgba - elif qt_module == "PySide6": - from PySide6.QtCore import ( # type: ignore[assignment] - QBuffer, - QByteArray, - QIODevice, - ) - from PySide6.QtGui import QImage, QPixmap, qRgba # type: ignore[assignment] - except (ImportError, RuntimeError): - continue - qt_is_installed = True - qt_version = version - break -else: - qt_is_installed = False - qt_version = None - - -def rgb(r: int, g: int, b: int, a: int = 255) -> int: - """(Internal) Turns an RGB color into a Qt compatible color integer.""" - # use qRgb to pack the colors, and then turn the resulting long - # into a negative integer with the same bitpattern. - return qRgba(r, g, b, a) & 0xFFFFFFFF - - -def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile: - """ - :param im: QImage or PIL ImageQt object - """ - buffer = QBuffer() - qt_openmode: object - if qt_version == "6": - try: - qt_openmode = getattr(QIODevice, "OpenModeFlag") - except AttributeError: - qt_openmode = getattr(QIODevice, "OpenMode") - else: - qt_openmode = QIODevice - buffer.open(getattr(qt_openmode, "ReadWrite")) - # preserve alpha channel with png - # otherwise ppm is more friendly with Image.open - if im.hasAlphaChannel(): - im.save(buffer, "png") - else: - im.save(buffer, "ppm") - - b = BytesIO() - b.write(buffer.data()) - buffer.close() - b.seek(0) - - return Image.open(b) - - -def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile: - return fromqimage(im) - - -def align8to32(bytes: bytes, width: int, mode: str) -> bytes: - """ - converts each scanline of data from 8 bit to 32 bit aligned - """ - - bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode] - - # calculate bytes per line and the extra padding if needed - bits_per_line = bits_per_pixel * width - full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8) - bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0) - - extra_padding = -bytes_per_line % 4 - - # already 32 bit aligned by luck - if not extra_padding: - return bytes - - new_data = [ - bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding - for i in range(len(bytes) // bytes_per_line) - ] - - return b"".join(new_data) - - -def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: - data = None - colortable = None - exclusive_fp = False - - # handle filename, if given instead of image name - if hasattr(im, "toUtf8"): - # FIXME - is this really the best way to do this? - im = str(im.toUtf8(), "utf-8") - if is_path(im): - im = Image.open(im) - exclusive_fp = True - assert isinstance(im, Image.Image) - - qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage - if im.mode == "1": - format = getattr(qt_format, "Format_Mono") - elif im.mode == "L": - format = getattr(qt_format, "Format_Indexed8") - colortable = [rgb(i, i, i) for i in range(256)] - elif im.mode == "P": - format = getattr(qt_format, "Format_Indexed8") - palette = im.getpalette() - assert palette is not None - colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] - elif im.mode == "RGB": - # Populate the 4th channel with 255 - im = im.convert("RGBA") - - data = im.tobytes("raw", "BGRA") - format = getattr(qt_format, "Format_RGB32") - elif im.mode == "RGBA": - data = im.tobytes("raw", "BGRA") - format = getattr(qt_format, "Format_ARGB32") - elif im.mode == "I;16": - im = im.point(lambda i: i * 256) - - format = getattr(qt_format, "Format_Grayscale16") - else: - if exclusive_fp: - im.close() - msg = f"unsupported image mode {repr(im.mode)}" - raise ValueError(msg) - - size = im.size - __data = data or align8to32(im.tobytes(), size[0], im.mode) - if exclusive_fp: - im.close() - return {"data": __data, "size": size, "format": format, "colortable": colortable} - - -if qt_is_installed: - - class ImageQt(QImage): - def __init__(self, im: Image.Image | str | QByteArray) -> None: - """ - An PIL image wrapper for Qt. This is a subclass of PyQt's QImage - class. - - :param im: A PIL Image object, or a file name (given either as - Python string or a PyQt string object). - """ - im_data = _toqclass_helper(im) - # must keep a reference, or Qt will crash! - # All QImage constructors that take data operate on an existing - # buffer, so this buffer has to hang on for the life of the image. - # Fixes https://github.com/python-pillow/Pillow/issues/1370 - self.__data = im_data["data"] - super().__init__( - self.__data, - im_data["size"][0], - im_data["size"][1], - im_data["format"], - ) - if im_data["colortable"]: - self.setColorTable(im_data["colortable"]) - - -def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: - return ImageQt(im) - - -def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: - qimage = toqimage(im) - pixmap = getattr(QPixmap, "fromImage")(qimage) - if qt_version == "6": - pixmap.detach() - return pixmap diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageSequence.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageSequence.py deleted file mode 100644 index 361be48..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageSequence.py +++ /dev/null @@ -1,88 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# sequence support classes -# -# history: -# 1997-02-20 fl Created -# -# Copyright (c) 1997 by Secret Labs AB. -# Copyright (c) 1997 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -## -from __future__ import annotations - -from . import Image - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable - - -class Iterator: - """ - This class implements an iterator object that can be used to loop - over an image sequence. - - You can use the ``[]`` operator to access elements by index. This operator - will raise an :py:exc:`IndexError` if you try to access a nonexistent - frame. - - :param im: An image object. - """ - - def __init__(self, im: Image.Image) -> None: - if not hasattr(im, "seek"): - msg = "im must have seek method" - raise AttributeError(msg) - self.im = im - self.position = getattr(self.im, "_min_frame", 0) - - def __getitem__(self, ix: int) -> Image.Image: - try: - self.im.seek(ix) - return self.im - except EOFError as e: - msg = "end of sequence" - raise IndexError(msg) from e - - def __iter__(self) -> Iterator: - return self - - def __next__(self) -> Image.Image: - try: - self.im.seek(self.position) - self.position += 1 - return self.im - except EOFError as e: - msg = "end of sequence" - raise StopIteration(msg) from e - - -def all_frames( - im: Image.Image | list[Image.Image], - func: Callable[[Image.Image], Image.Image] | None = None, -) -> list[Image.Image]: - """ - Applies a given function to all frames in an image or a list of images. - The frames are returned as a list of separate images. - - :param im: An image, or a list of images. - :param func: The function to apply to all of the image frames. - :returns: A list of images. - """ - if not isinstance(im, list): - im = [im] - - ims = [] - for imSequence in im: - current = imSequence.tell() - - ims += [im_frame.copy() for im_frame in Iterator(imSequence)] - - imSequence.seek(current) - return [func(im) for im in ims] if func else ims diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageShow.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageShow.py deleted file mode 100644 index 7705608..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageShow.py +++ /dev/null @@ -1,362 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# im.show() drivers -# -# History: -# 2008-04-06 fl Created -# -# Copyright (c) Secret Labs AB 2008. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import abc -import os -import shutil -import subprocess -import sys -from shlex import quote -from typing import Any - -from . import Image - -_viewers = [] - - -def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None: - """ - The :py:func:`register` function is used to register additional viewers:: - - from PIL import ImageShow - ImageShow.register(MyViewer()) # MyViewer will be used as a last resort - ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised - ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised - - :param viewer: The viewer to be registered. - :param order: - Zero or a negative integer to prepend this viewer to the list, - a positive integer to append it. - """ - if isinstance(viewer, type) and issubclass(viewer, Viewer): - viewer = viewer() - if order > 0: - _viewers.append(viewer) - else: - _viewers.insert(0, viewer) - - -def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: - r""" - Display a given image. - - :param image: An image object. - :param title: Optional title. Not all viewers can display the title. - :param \**options: Additional viewer options. - :returns: ``True`` if a suitable viewer was found, ``False`` otherwise. - """ - for viewer in _viewers: - if viewer.show(image, title=title, **options): - return True - return False - - -class Viewer: - """Base class for viewers.""" - - # main api - - def show(self, image: Image.Image, **options: Any) -> int: - """ - The main function for displaying an image. - Converts the given image to the target format and displays it. - """ - - if not ( - image.mode in ("1", "RGBA") - or (self.format == "PNG" and image.mode in ("I;16", "LA")) - ): - base = Image.getmodebase(image.mode) - if image.mode != base: - image = image.convert(base) - - return self.show_image(image, **options) - - # hook methods - - format: str | None = None - """The format to convert the image into.""" - options: dict[str, Any] = {} - """Additional options used to convert the image.""" - - def get_format(self, image: Image.Image) -> str | None: - """Return format name, or ``None`` to save as PGM/PPM.""" - return self.format - - def get_command(self, file: str, **options: Any) -> str: - """ - Returns the command used to display the file. - Not implemented in the base class. - """ - msg = "unavailable in base viewer" - raise NotImplementedError(msg) - - def save_image(self, image: Image.Image) -> str: - """Save to temporary file and return filename.""" - return image._dump(format=self.get_format(image), **self.options) - - def show_image(self, image: Image.Image, **options: Any) -> int: - """Display the given image.""" - return self.show_file(self.save_image(image), **options) - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - os.system(self.get_command(path, **options)) # nosec - return 1 - - -# -------------------------------------------------------------------- - - -class WindowsViewer(Viewer): - """The default viewer on Windows is the default system application for PNG files.""" - - format = "PNG" - options = {"compress_level": 1, "save_all": True} - - def get_command(self, file: str, **options: Any) -> str: - return ( - f'start "Pillow" /WAIT "{file}" ' - "&& ping -n 4 127.0.0.1 >NUL " - f'&& del /f "{file}"' - ) - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.Popen( - self.get_command(path, **options), - shell=True, - creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), - ) # nosec - return 1 - - -if sys.platform == "win32": - register(WindowsViewer) - - -class MacViewer(Viewer): - """The default viewer on macOS using ``Preview.app``.""" - - format = "PNG" - options = {"compress_level": 1, "save_all": True} - - def get_command(self, file: str, **options: Any) -> str: - # on darwin open returns immediately resulting in the temp - # file removal while app is opening - command = "open -a Preview.app" - command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" - return command - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.call(["open", "-a", "Preview.app", path]) - - pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") - executable = (not pyinstaller and sys.executable) or shutil.which("python3") - if executable: - subprocess.Popen( - [ - executable, - "-c", - "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", - path, - ] - ) - return 1 - - -if sys.platform == "darwin": - register(MacViewer) - - -class UnixViewer(abc.ABC, Viewer): - format = "PNG" - options = {"compress_level": 1, "save_all": True} - - @abc.abstractmethod - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass - - def get_command(self, file: str, **options: Any) -> str: - command = self.get_command_ex(file, **options)[0] - return f"{command} {quote(file)}" - - -class XDGViewer(UnixViewer): - """ - The freedesktop.org ``xdg-open`` command. - """ - - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - command = executable = "xdg-open" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.Popen(["xdg-open", path]) - return 1 - - -class DisplayViewer(UnixViewer): - """ - The ImageMagick ``display`` command. - This viewer supports the ``title`` parameter. - """ - - def get_command_ex( - self, file: str, title: str | None = None, **options: Any - ) -> tuple[str, str]: - command = executable = "display" - if title: - command += f" -title {quote(title)}" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - args = ["display"] - title = options.get("title") - if title: - args += ["-title", title] - args.append(path) - - subprocess.Popen(args) - return 1 - - -class GmDisplayViewer(UnixViewer): - """The GraphicsMagick ``gm display`` command.""" - - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - executable = "gm" - command = "gm display" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.Popen(["gm", "display", path]) - return 1 - - -class EogViewer(UnixViewer): - """The GNOME Image Viewer ``eog`` command.""" - - def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - executable = "eog" - command = "eog -n" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - subprocess.Popen(["eog", "-n", path]) - return 1 - - -class XVViewer(UnixViewer): - """ - The X Viewer ``xv`` command. - This viewer supports the ``title`` parameter. - """ - - def get_command_ex( - self, file: str, title: str | None = None, **options: Any - ) -> tuple[str, str]: - # note: xv is pretty outdated. most modern systems have - # imagemagick's display command instead. - command = executable = "xv" - if title: - command += f" -name {quote(title)}" - return command, executable - - def show_file(self, path: str, **options: Any) -> int: - """ - Display given file. - """ - if not os.path.exists(path): - raise FileNotFoundError - args = ["xv"] - title = options.get("title") - if title: - args += ["-name", title] - args.append(path) - - subprocess.Popen(args) - return 1 - - -if sys.platform not in ("win32", "darwin"): # unixoids - if shutil.which("xdg-open"): - register(XDGViewer) - if shutil.which("display"): - register(DisplayViewer) - if shutil.which("gm"): - register(GmDisplayViewer) - if shutil.which("eog"): - register(EogViewer) - if shutil.which("xv"): - register(XVViewer) - - -class IPythonViewer(Viewer): - """The viewer for IPython frontends.""" - - def show_image(self, image: Image.Image, **options: Any) -> int: - ipython_display(image) - return 1 - - -try: - from IPython.display import display as ipython_display -except ImportError: - pass -else: - register(IPythonViewer) - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python3 ImageShow.py imagefile [title]") - sys.exit() - - with Image.open(sys.argv[1]) as im: - print(show(im, *sys.argv[2:])) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageStat.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageStat.py deleted file mode 100644 index 3a1044b..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageStat.py +++ /dev/null @@ -1,167 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# global image statistics -# -# History: -# 1996-04-05 fl Created -# 1997-05-21 fl Added mask; added rms, var, stddev attributes -# 1997-08-05 fl Added median -# 1998-07-05 hk Fixed integer overflow error -# -# Notes: -# This class shows how to implement delayed evaluation of attributes. -# To get a certain value, simply access the corresponding attribute. -# The __getattr__ dispatcher takes care of the rest. -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996-97. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import math -from functools import cached_property - -from . import Image - - -class Stat: - def __init__( - self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None - ) -> None: - """ - Calculate statistics for the given image. If a mask is included, - only the regions covered by that mask are included in the - statistics. You can also pass in a previously calculated histogram. - - :param image: A PIL image, or a precalculated histogram. - - .. note:: - - For a PIL image, calculations rely on the - :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are - grouped into 256 bins, even if the image has more than 8 bits per - channel. So ``I`` and ``F`` mode images have a maximum ``mean``, - ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum - of more than 255. - - :param mask: An optional mask. - """ - if isinstance(image_or_list, Image.Image): - self.h = image_or_list.histogram(mask) - elif isinstance(image_or_list, list): - self.h = image_or_list - else: - msg = "first argument must be image or list" # type: ignore[unreachable] - raise TypeError(msg) - self.bands = list(range(len(self.h) // 256)) - - @cached_property - def extrema(self) -> list[tuple[int, int]]: - """ - Min/max values for each band in the image. - - .. note:: - This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and - simply returns the low and high bins used. This is correct for - images with 8 bits per channel, but fails for other modes such as - ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to - return per-band extrema for the image. This is more correct and - efficient because, for non-8-bit modes, the histogram method uses - :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. - """ - - def minmax(histogram: list[int]) -> tuple[int, int]: - res_min, res_max = 255, 0 - for i in range(256): - if histogram[i]: - res_min = i - break - for i in range(255, -1, -1): - if histogram[i]: - res_max = i - break - return res_min, res_max - - return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)] - - @cached_property - def count(self) -> list[int]: - """Total number of pixels for each band in the image.""" - return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)] - - @cached_property - def sum(self) -> list[float]: - """Sum of all pixels for each band in the image.""" - - v = [] - for i in range(0, len(self.h), 256): - layer_sum = 0.0 - for j in range(256): - layer_sum += j * self.h[i + j] - v.append(layer_sum) - return v - - @cached_property - def sum2(self) -> list[float]: - """Squared sum of all pixels for each band in the image.""" - - v = [] - for i in range(0, len(self.h), 256): - sum2 = 0.0 - for j in range(256): - sum2 += (j**2) * float(self.h[i + j]) - v.append(sum2) - return v - - @cached_property - def mean(self) -> list[float]: - """Average (arithmetic mean) pixel level for each band in the image.""" - return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands] - - @cached_property - def median(self) -> list[int]: - """Median pixel level for each band in the image.""" - - v = [] - for i in self.bands: - s = 0 - half = self.count[i] // 2 - b = i * 256 - for j in range(256): - s = s + self.h[b + j] - if s > half: - break - v.append(j) - return v - - @cached_property - def rms(self) -> list[float]: - """RMS (root-mean-square) for each band in the image.""" - return [ - math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0 - for i in self.bands - ] - - @cached_property - def var(self) -> list[float]: - """Variance for each band in the image.""" - return [ - ( - (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] - if self.count[i] - else 0 - ) - for i in self.bands - ] - - @cached_property - def stddev(self) -> list[float]: - """Standard deviation for each band in the image.""" - return [math.sqrt(self.var[i]) for i in self.bands] - - -Global = Stat # compatibility diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageText.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageText.py deleted file mode 100644 index 008d20d..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageText.py +++ /dev/null @@ -1,508 +0,0 @@ -from __future__ import annotations - -import math -import re -from typing import AnyStr, Generic, NamedTuple - -from . import ImageFont -from ._typing import _Ink - -Font = ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont - - -class _Line(NamedTuple): - x: float - y: float - anchor: str - text: str | bytes - - -class _Wrap(Generic[AnyStr]): - lines: list[AnyStr] = [] - position = 0 - offset = 0 - - def __init__( - self, - text: Text[AnyStr], - width: int, - height: int | None = None, - font: Font | None = None, - ) -> None: - self.text: Text[AnyStr] = text - self.width = width - self.height = height - self.font = font - - input_text = self.text.text - emptystring = "" if isinstance(input_text, str) else b"" - line = emptystring - - for word in re.findall( - r"\s*\S+" if isinstance(input_text, str) else rb"\s*\S+", input_text - ): - newlines = re.findall( - r"[^\S\n]*\n" if isinstance(input_text, str) else rb"[^\S\n]*\n", word - ) - if newlines: - if not self.add_line(line): - break - for i, line in enumerate(newlines): - if i != 0 and not self.add_line(emptystring): - break - self.position += len(line) - word = word[len(line) :] - line = emptystring - - new_line = line + word - if self.text._get_bbox(new_line, self.font)[2] <= width: - # This word fits on the line - line = new_line - continue - - # This word does not fit on the line - if line and not self.add_line(line): - break - - original_length = len(word) - word = word.lstrip() - self.offset = original_length - len(word) - - if self.text._get_bbox(word, self.font)[2] > width: - if font is None: - msg = "Word does not fit within line" - raise ValueError(msg) - break - line = word - else: - if line: - self.add_line(line) - self.remaining_text: AnyStr = input_text[self.position :] - - def add_line(self, line: AnyStr) -> bool: - lines = self.lines + [line] - if self.height is not None: - last_line_y = self.text._split(lines=lines)[-1].y - last_line_height = self.text._get_bbox(line, self.font)[3] - if last_line_y + last_line_height > self.height: - return False - - self.lines = lines - self.position += len(line) + self.offset - self.offset = 0 - return True - - -class Text(Generic[AnyStr]): - def __init__( - self, - text: AnyStr, - font: Font | None = None, - mode: str = "RGB", - spacing: float = 4, - direction: str | None = None, - features: list[str] | None = None, - language: str | None = None, - ) -> None: - """ - :param text: String to be drawn. - :param font: Either an :py:class:`~PIL.ImageFont.ImageFont` instance, - :py:class:`~PIL.ImageFont.FreeTypeFont` instance, - :py:class:`~PIL.ImageFont.TransposedFont` instance or ``None``. If - ``None``, the default font from :py:meth:`.ImageFont.load_default` - will be used. - :param mode: The image mode this will be used with. - :param spacing: The number of pixels between lines. - :param direction: Direction of the text. It can be ``"rtl"`` (right to left), - ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional font features - that are not enabled by default, for example ``"dlig"`` or - ``"ss01"``, but can be also used to turn off default font - features, for example ``"-liga"`` to disable ligatures or - ``"-kern"`` to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - """ - self.text: AnyStr = text - self.font = font or ImageFont.load_default() - - self.mode = mode - self.spacing = spacing - self.direction = direction - self.features = features - self.language = language - - self.embedded_color = False - - self.stroke_width: float = 0 - self.stroke_fill: _Ink | None = None - - def embed_color(self) -> None: - """ - Use embedded color glyphs (COLR, CBDT, SBIX). - """ - if self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - self.embedded_color = True - - def stroke(self, width: float = 0, fill: _Ink | None = None) -> None: - """ - :param width: The width of the text stroke. - :param fill: Color to use for the text stroke when drawing. If not given, will - default to the ``fill`` parameter from - :py:meth:`.ImageDraw.ImageDraw.text`. - """ - self.stroke_width = width - self.stroke_fill = fill - - def _get_fontmode(self) -> str: - if self.mode in ("1", "P", "I", "F"): - return "1" - elif self.embedded_color: - return "RGBA" - else: - return "L" - - def wrap( - self, - width: int, - height: int | None = None, - scaling: str | tuple[str, int] | None = None, - ) -> Text[AnyStr] | None: - """ - Wrap text to fit within a given width. - - :param width: The width to fit within. - :param height: An optional height limit. Any text that does not fit within this - will be returned as a new :py:class:`.Text` object. - :param scaling: An optional directive to scale the text, either "grow" as much - as possible within the given dimensions, or "shrink" until it - fits. It can also be a tuple of (direction, limit), with an - integer limit to stop scaling at. - - :returns: An :py:class:`.Text` object, or None. - """ - if isinstance(self.font, ImageFont.TransposedFont): - msg = "TransposedFont not supported" - raise ValueError(msg) - if self.direction not in (None, "ltr"): - msg = "Only ltr direction supported" - raise ValueError(msg) - - if scaling is None: - wrap = _Wrap(self, width, height) - else: - if not isinstance(self.font, ImageFont.FreeTypeFont): - msg = "'scaling' only supports FreeTypeFont" - raise ValueError(msg) - if height is None: - msg = "'scaling' requires 'height'" - raise ValueError(msg) - - if isinstance(scaling, str): - limit = 1 - else: - scaling, limit = scaling - - font = self.font - wrap = _Wrap(self, width, height, font) - if scaling == "shrink": - if not wrap.remaining_text: - return None - - size = math.ceil(font.size) - while wrap.remaining_text: - if size == max(limit, 1): - msg = "Text could not be scaled" - raise ValueError(msg) - size -= 1 - font = self.font.font_variant(size=size) - wrap = _Wrap(self, width, height, font) - self.font = font - else: - if wrap.remaining_text: - msg = "Text could not be scaled" - raise ValueError(msg) - - size = math.floor(font.size) - while not wrap.remaining_text: - if size == limit: - msg = "Text could not be scaled" - raise ValueError(msg) - size += 1 - font = self.font.font_variant(size=size) - last_wrap = wrap - wrap = _Wrap(self, width, height, font) - size -= 1 - if size != self.font.size: - self.font = self.font.font_variant(size=size) - wrap = last_wrap - - if wrap.remaining_text: - text = Text( - text=wrap.remaining_text, - font=self.font, - mode=self.mode, - spacing=self.spacing, - direction=self.direction, - features=self.features, - language=self.language, - ) - text.embedded_color = self.embedded_color - text.stroke_width = self.stroke_width - text.stroke_fill = self.stroke_fill - else: - text = None - - newline = "\n" if isinstance(self.text, str) else b"\n" - self.text = newline.join(wrap.lines) - return text - - def get_length(self) -> float: - """ - Returns length (in pixels with 1/64 precision) of text. - - This is the amount by which following text should be offset. - Text bounding box may extend past the length in some fonts, - e.g. when using italics or accents. - - The result is returned as a float; it is a whole number if using basic layout. - - Note that the sum of two lengths may not equal the length of a concatenated - string due to kerning. If you need to adjust for kerning, include the following - character and subtract its length. - - For example, instead of:: - - hello = ImageText.Text("Hello", font).get_length() - world = ImageText.Text("World", font).get_length() - helloworld = ImageText.Text("HelloWorld", font).get_length() - assert hello + world == helloworld - - use:: - - hello = ( - ImageText.Text("HelloW", font).get_length() - - ImageText.Text("W", font).get_length() - ) # adjusted for kerning - world = ImageText.Text("World", font).get_length() - helloworld = ImageText.Text("HelloWorld", font).get_length() - assert hello + world == helloworld - - or disable kerning with (requires libraqm):: - - hello = ImageText.Text("Hello", font, features=["-kern"]).get_length() - world = ImageText.Text("World", font, features=["-kern"]).get_length() - helloworld = ImageText.Text( - "HelloWorld", font, features=["-kern"] - ).get_length() - assert hello + world == helloworld - - :return: Either width for horizontal text, or height for vertical text. - """ - if isinstance(self.text, str): - multiline = "\n" in self.text - else: - multiline = b"\n" in self.text - if multiline: - msg = "can't measure length of multiline text" - raise ValueError(msg) - return self.font.getlength( - self.text, - self._get_fontmode(), - self.direction, - self.features, - self.language, - ) - - def _split( - self, - xy: tuple[float, float] = (0, 0), - anchor: str | None = None, - align: str = "left", - lines: list[str] | list[bytes] | None = None, - ) -> list[_Line]: - if anchor is None: - anchor = "lt" if self.direction == "ttb" else "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - - if lines is None: - lines = ( - self.text.split("\n") - if isinstance(self.text, str) - else self.text.split(b"\n") - ) - if len(lines) == 1: - return [_Line(xy[0], xy[1], anchor, lines[0])] - - if anchor[1] in "tb" and self.direction != "ttb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - fontmode = self._get_fontmode() - line_spacing = ( - self.font.getbbox( - "A", - fontmode, - None, - self.features, - self.language, - self.stroke_width, - )[3] - + self.stroke_width - + self.spacing - ) - - top = xy[1] - parts = [] - if self.direction == "ttb": - left = xy[0] - for line in lines: - parts.append(_Line(left, top, anchor, line)) - left += line_spacing - else: - widths = [] - max_width: float = 0 - for line in lines: - line_width = self.font.getlength( - line, fontmode, self.direction, self.features, self.language - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - if anchor[1] == "m": - top -= (len(lines) - 1) * line_spacing / 2.0 - elif anchor[1] == "d": - top -= (len(lines) - 1) * line_spacing - - idx = -1 - for line in lines: - left = xy[0] - idx += 1 - width_difference = max_width - widths[idx] - - # align by align parameter - if align in ("left", "justify"): - pass - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center", "right" or "justify"' - raise ValueError(msg) - - if ( - align == "justify" - and width_difference != 0 - and idx != len(lines) - 1 - ): - words = ( - line.split(" ") if isinstance(line, str) else line.split(b" ") - ) - if len(words) > 1: - # align left by anchor - if anchor[0] == "m": - left -= max_width / 2.0 - elif anchor[0] == "r": - left -= max_width - - word_widths = [ - self.font.getlength( - word, - fontmode, - self.direction, - self.features, - self.language, - ) - for word in words - ] - word_anchor = "l" + anchor[1] - width_difference = max_width - sum(word_widths) - i = 0 - for word in words: - parts.append(_Line(left, top, word_anchor, word)) - left += word_widths[i] + width_difference / (len(words) - 1) - i += 1 - top += line_spacing - continue - - # align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - parts.append(_Line(left, top, anchor, line)) - top += line_spacing - - return parts - - def _get_bbox( - self, text: str | bytes, font: Font | None = None, anchor: str | None = None - ) -> tuple[float, float, float, float]: - return (font or self.font).getbbox( - text, - self._get_fontmode(), - self.direction, - self.features, - self.language, - self.stroke_width, - anchor, - ) - - def get_bbox( - self, - xy: tuple[float, float] = (0, 0), - anchor: str | None = None, - align: str = "left", - ) -> tuple[float, float, float, float]: - """ - Returns bounding box (in pixels) of text. - - Use :py:meth:`get_length` to get the offset of following text with 1/64 pixel - precision. The bounding box includes extra margins for some fonts, e.g. italics - or accents. - - :param xy: The anchor coordinates of the text. - :param anchor: The text anchor alignment. Determines the relative location of - the anchor to the text. The default alignment is top left, - specifically ``la`` for horizontal text and ``lt`` for - vertical text. See :ref:`text-anchors` for details. - :param align: For multiline text, ``"left"``, ``"center"``, ``"right"`` or - ``"justify"`` determines the relative alignment of lines. Use the - ``anchor`` parameter to specify the alignment to ``xy``. - - :return: ``(left, top, right, bottom)`` bounding box - """ - bbox: tuple[float, float, float, float] | None = None - for x, y, anchor, text in self._split(xy, anchor, align): - bbox_line = self._get_bbox(text, anchor=anchor) - bbox_line = ( - bbox_line[0] + x, - bbox_line[1] + y, - bbox_line[2] + x, - bbox_line[3] + y, - ) - if bbox is None: - bbox = bbox_line - else: - bbox = ( - min(bbox[0], bbox_line[0]), - min(bbox[1], bbox_line[1]), - max(bbox[2], bbox_line[2]), - max(bbox[3], bbox_line[3]), - ) - - assert bbox is not None - return bbox diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageTk.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageTk.py deleted file mode 100644 index 3a4cb81..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageTk.py +++ /dev/null @@ -1,266 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a Tk display interface -# -# History: -# 96-04-08 fl Created -# 96-09-06 fl Added getimage method -# 96-11-01 fl Rewritten, removed image attribute and crop method -# 97-05-09 fl Use PyImagingPaste method instead of image type -# 97-05-12 fl Minor tweaks to match the IFUNC95 interface -# 97-05-17 fl Support the "pilbitmap" booster patch -# 97-06-05 fl Added file= and data= argument to image constructors -# 98-03-09 fl Added width and height methods to Image classes -# 98-07-02 fl Use default mode for "P" images without palette attribute -# 98-07-02 fl Explicitly destroy Tkinter image objects -# 99-07-24 fl Support multiple Tk interpreters (from Greg Couch) -# 99-07-26 fl Automatically hook into Tkinter (if possible) -# 99-08-15 fl Hook uses _imagingtk instead of _imaging -# -# Copyright (c) 1997-1999 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import tkinter -from io import BytesIO -from typing import Any - -from . import Image, ImageFile - -TYPE_CHECKING = False -if TYPE_CHECKING: - from ._typing import CapsuleType - -# -------------------------------------------------------------------- -# Check for Tkinter interface hooks - - -def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: - source = None - if "file" in kw: - source = kw.pop("file") - elif "data" in kw: - source = BytesIO(kw.pop("data")) - if not source: - return None - return Image.open(source) - - -def _pyimagingtkcall( - command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType -) -> None: - tk = photo.tk - try: - tk.call(command, photo, repr(ptr)) - except tkinter.TclError: - # activate Tkinter hook - # may raise an error if it cannot attach to Tkinter - from . import _imagingtk - - _imagingtk.tkinit(tk.interpaddr()) - tk.call(command, photo, repr(ptr)) - - -# -------------------------------------------------------------------- -# PhotoImage - - -class PhotoImage: - """ - A Tkinter-compatible photo image. This can be used - everywhere Tkinter expects an image object. If the image is an RGBA - image, pixels having alpha 0 are treated as transparent. - - The constructor takes either a PIL image, or a mode and a size. - Alternatively, you can use the ``file`` or ``data`` options to initialize - the photo image object. - - :param image: Either a PIL image, or a mode string. If a mode string is - used, a size must also be given. - :param size: If the first argument is a mode string, this defines the size - of the image. - :keyword file: A filename to load the image from (using - ``Image.open(file)``). - :keyword data: An 8-bit string containing image data (as loaded from an - image file). - """ - - def __init__( - self, - image: Image.Image | str | None = None, - size: tuple[int, int] | None = None, - **kw: Any, - ) -> None: - # Tk compatibility: file or data - if image is None: - image = _get_image_from_kw(kw) - - if image is None: - msg = "Image is required" - raise ValueError(msg) - elif isinstance(image, str): - mode = image - image = None - - if size is None: - msg = "If first argument is mode, size is required" - raise ValueError(msg) - else: - # got an image instead of a mode - mode = image.mode - if mode == "P": - # palette mapped data - image.apply_transparency() - image.load() - mode = image.palette.mode if image.palette else "RGB" - size = image.size - kw["width"], kw["height"] = size - - if mode not in ["1", "L", "RGB", "RGBA"]: - mode = Image.getmodebase(mode) - - self.__mode = mode - self.__size = size - self.__photo = tkinter.PhotoImage(**kw) - self.tk = self.__photo.tk - if image: - self.paste(image) - - def __del__(self) -> None: - try: - name = self.__photo.name - except AttributeError: - return - self.__photo.name = None - try: - self.__photo.tk.call("image", "delete", name) - except Exception: - pass # ignore internal errors - - def __str__(self) -> str: - """ - Get the Tkinter photo image identifier. This method is automatically - called by Tkinter whenever a PhotoImage object is passed to a Tkinter - method. - - :return: A Tkinter photo image identifier (a string). - """ - return str(self.__photo) - - def width(self) -> int: - """ - Get the width of the image. - - :return: The width, in pixels. - """ - return self.__size[0] - - def height(self) -> int: - """ - Get the height of the image. - - :return: The height, in pixels. - """ - return self.__size[1] - - def paste(self, im: Image.Image) -> None: - """ - Paste a PIL image into the photo image. Note that this can - be very slow if the photo image is displayed. - - :param im: A PIL image. The size must match the target region. If the - mode does not match, the image is converted to the mode of - the bitmap image. - """ - # convert to blittable - ptr = im.getim() - image = im.im - if not image.isblock() or im.mode != self.__mode: - block = Image.core.new_block(self.__mode, im.size) - image.convert2(block, image) # convert directly between buffers - ptr = block.ptr - - _pyimagingtkcall("PyImagingPhoto", self.__photo, ptr) - - -# -------------------------------------------------------------------- -# BitmapImage - - -class BitmapImage: - """ - A Tkinter-compatible bitmap image. This can be used everywhere Tkinter - expects an image object. - - The given image must have mode "1". Pixels having value 0 are treated as - transparent. Options, if any, are passed on to Tkinter. The most commonly - used option is ``foreground``, which is used to specify the color for the - non-transparent parts. See the Tkinter documentation for information on - how to specify colours. - - :param image: A PIL image. - """ - - def __init__(self, image: Image.Image | None = None, **kw: Any) -> None: - # Tk compatibility: file or data - if image is None: - image = _get_image_from_kw(kw) - - if image is None: - msg = "Image is required" - raise ValueError(msg) - self.__mode = image.mode - self.__size = image.size - - self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw) - - def __del__(self) -> None: - try: - name = self.__photo.name - except AttributeError: - return - self.__photo.name = None - try: - self.__photo.tk.call("image", "delete", name) - except Exception: - pass # ignore internal errors - - def width(self) -> int: - """ - Get the width of the image. - - :return: The width, in pixels. - """ - return self.__size[0] - - def height(self) -> int: - """ - Get the height of the image. - - :return: The height, in pixels. - """ - return self.__size[1] - - def __str__(self) -> str: - """ - Get the Tkinter bitmap image identifier. This method is automatically - called by Tkinter whenever a BitmapImage object is passed to a Tkinter - method. - - :return: A Tkinter bitmap image identifier (a string). - """ - return str(self.__photo) - - -def getimage(photo: PhotoImage) -> Image.Image: - """Copies the contents of a PhotoImage to a PIL image memory.""" - im = Image.new("RGBA", (photo.width(), photo.height())) - - _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim()) - - return im diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageTransform.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageTransform.py deleted file mode 100644 index fb144ff..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageTransform.py +++ /dev/null @@ -1,136 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# transform wrappers -# -# History: -# 2002-04-08 fl Created -# -# Copyright (c) 2002 by Secret Labs AB -# Copyright (c) 2002 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from collections.abc import Sequence -from typing import Any - -from . import Image - - -class Transform(Image.ImageTransformHandler): - """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" - - method: Image.Transform - - def __init__(self, data: Sequence[Any]) -> None: - self.data = data - - def getdata(self) -> tuple[Image.Transform, Sequence[int]]: - return self.method, self.data - - def transform( - self, - size: tuple[int, int], - image: Image.Image, - **options: Any, - ) -> Image.Image: - """Perform the transform. Called from :py:meth:`.Image.transform`.""" - # can be overridden - method, data = self.getdata() - return image.transform(size, method, data, **options) - - -class AffineTransform(Transform): - """ - Define an affine image transform. - - This function takes a 6-tuple (a, b, c, d, e, f) which contain the first - two rows from the inverse of an affine transform matrix. For each pixel - (x, y) in the output image, the new value is taken from a position (a x + - b y + c, d x + e y + f) in the input image, rounded to nearest pixel. - - This function can be used to scale, translate, rotate, and shear the - original image. - - See :py:meth:`.Image.transform` - - :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows - from the inverse of an affine transform matrix. - """ - - method = Image.Transform.AFFINE - - -class PerspectiveTransform(Transform): - """ - Define a perspective image transform. - - This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel - (x, y) in the output image, the new value is taken from a position - ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in - the input image, rounded to nearest pixel. - - This function can be used to scale, translate, rotate, and shear the - original image. - - See :py:meth:`.Image.transform` - - :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). - """ - - method = Image.Transform.PERSPECTIVE - - -class ExtentTransform(Transform): - """ - Define a transform to extract a subregion from an image. - - Maps a rectangle (defined by two corners) from the image to a rectangle of - the given size. The resulting image will contain data sampled from between - the corners, such that (x0, y0) in the input image will end up at (0,0) in - the output image, and (x1, y1) at size. - - This method can be used to crop, stretch, shrink, or mirror an arbitrary - rectangle in the current image. It is slightly slower than crop, but about - as fast as a corresponding resize operation. - - See :py:meth:`.Image.transform` - - :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the - input image's coordinate system. See :ref:`coordinate-system`. - """ - - method = Image.Transform.EXTENT - - -class QuadTransform(Transform): - """ - Define a quad image transform. - - Maps a quadrilateral (a region defined by four corners) from the image to a - rectangle of the given size. - - See :py:meth:`.Image.transform` - - :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the - upper left, lower left, lower right, and upper right corner of the - source quadrilateral. - """ - - method = Image.Transform.QUAD - - -class MeshTransform(Transform): - """ - Define a mesh image transform. A mesh transform consists of one or more - individual quad transforms. - - See :py:meth:`.Image.transform` - - :param data: A list of (bbox, quad) tuples. - """ - - method = Image.Transform.MESH diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageWin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImageWin.py deleted file mode 100644 index 98c28f2..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImageWin.py +++ /dev/null @@ -1,247 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# a Windows DIB display interface -# -# History: -# 1996-05-20 fl Created -# 1996-09-20 fl Fixed subregion exposure -# 1997-09-21 fl Added draw primitive (for tzPrint) -# 2003-05-21 fl Added experimental Window/ImageWindow classes -# 2003-09-05 fl Added fromstring/tostring methods -# -# Copyright (c) Secret Labs AB 1997-2003. -# Copyright (c) Fredrik Lundh 1996-2003. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image - - -class HDC: - """ - Wraps an HDC integer. The resulting object can be passed to the - :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` - methods. - """ - - def __init__(self, dc: int) -> None: - self.dc = dc - - def __int__(self) -> int: - return self.dc - - -class HWND: - """ - Wraps an HWND integer. The resulting object can be passed to the - :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` - methods, instead of a DC. - """ - - def __init__(self, wnd: int) -> None: - self.wnd = wnd - - def __int__(self) -> int: - return self.wnd - - -class Dib: - """ - A Windows bitmap with the given mode and size. The mode can be one of "1", - "L", "P", or "RGB". - - If the display requires a palette, this constructor creates a suitable - palette and associates it with the image. For an "L" image, 128 graylevels - are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together - with 20 graylevels. - - To make sure that palettes work properly under Windows, you must call the - ``palette`` method upon certain events from Windows. - - :param image: Either a PIL image, or a mode string. If a mode string is - used, a size must also be given. The mode can be one of "1", - "L", "P", or "RGB". - :param size: If the first argument is a mode string, this - defines the size of the image. - """ - - def __init__( - self, image: Image.Image | str, size: tuple[int, int] | None = None - ) -> None: - if isinstance(image, str): - mode = image - image = "" - if size is None: - msg = "If first argument is mode, size is required" - raise ValueError(msg) - else: - mode = image.mode - size = image.size - if mode not in ["1", "L", "P", "RGB"]: - mode = Image.getmodebase(mode) - self.image = Image.core.display(mode, size) - self.mode = mode - self.size = size - if image: - assert not isinstance(image, str) - self.paste(image) - - def expose(self, handle: int | HDC | HWND) -> None: - """ - Copy the bitmap contents to a device context. - - :param handle: Device context (HDC), cast to a Python integer, or an - HDC or HWND instance. In PythonWin, you can use - ``CDC.GetHandleAttrib()`` to get a suitable handle. - """ - handle_int = int(handle) - if isinstance(handle, HWND): - dc = self.image.getdc(handle_int) - try: - self.image.expose(dc) - finally: - self.image.releasedc(handle_int, dc) - else: - self.image.expose(handle_int) - - def draw( - self, - handle: int | HDC | HWND, - dst: tuple[int, int, int, int], - src: tuple[int, int, int, int] | None = None, - ) -> None: - """ - Same as expose, but allows you to specify where to draw the image, and - what part of it to draw. - - The destination and source areas are given as 4-tuple rectangles. If - the source is omitted, the entire image is copied. If the source and - the destination have different sizes, the image is resized as - necessary. - """ - if src is None: - src = (0, 0) + self.size - handle_int = int(handle) - if isinstance(handle, HWND): - dc = self.image.getdc(handle_int) - try: - self.image.draw(dc, dst, src) - finally: - self.image.releasedc(handle_int, dc) - else: - self.image.draw(handle_int, dst, src) - - def query_palette(self, handle: int | HDC | HWND) -> int: - """ - Installs the palette associated with the image in the given device - context. - - This method should be called upon **QUERYNEWPALETTE** and - **PALETTECHANGED** events from Windows. If this method returns a - non-zero value, one or more display palette entries were changed, and - the image should be redrawn. - - :param handle: Device context (HDC), cast to a Python integer, or an - HDC or HWND instance. - :return: The number of entries that were changed (if one or more entries, - this indicates that the image should be redrawn). - """ - handle_int = int(handle) - if isinstance(handle, HWND): - handle = self.image.getdc(handle_int) - try: - result = self.image.query_palette(handle) - finally: - self.image.releasedc(handle, handle) - else: - result = self.image.query_palette(handle_int) - return result - - def paste( - self, im: Image.Image, box: tuple[int, int, int, int] | None = None - ) -> None: - """ - Paste a PIL image into the bitmap image. - - :param im: A PIL image. The size must match the target region. - If the mode does not match, the image is converted to the - mode of the bitmap image. - :param box: A 4-tuple defining the left, upper, right, and - lower pixel coordinate. See :ref:`coordinate-system`. If - None is given instead of a tuple, all of the image is - assumed. - """ - im.load() - if self.mode != im.mode: - im = im.convert(self.mode) - if box: - self.image.paste(im.im, box) - else: - self.image.paste(im.im) - - def frombytes(self, buffer: bytes) -> None: - """ - Load display memory contents from byte data. - - :param buffer: A buffer containing display data (usually - data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) - """ - self.image.frombytes(buffer) - - def tobytes(self) -> bytes: - """ - Copy display memory contents to bytes object. - - :return: A bytes object containing display data. - """ - return self.image.tobytes() - - -class Window: - """Create a Window with the given title size.""" - - def __init__( - self, title: str = "PIL", width: int | None = None, height: int | None = None - ) -> None: - self.hwnd = Image.core.createwindow( - title, self.__dispatcher, width or 0, height or 0 - ) - - def __dispatcher(self, action: str, *args: int) -> None: - getattr(self, f"ui_handle_{action}")(*args) - - def ui_handle_clear(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: - pass - - def ui_handle_damage(self, x0: int, y0: int, x1: int, y1: int) -> None: - pass - - def ui_handle_destroy(self) -> None: - pass - - def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: - pass - - def ui_handle_resize(self, width: int, height: int) -> None: - pass - - def mainloop(self) -> None: - Image.core.eventloop() - - -class ImageWindow(Window): - """Create an image window which displays the given image.""" - - def __init__(self, image: Image.Image | Dib, title: str = "PIL") -> None: - if not isinstance(image, Dib): - image = Dib(image) - self.image = image - width, height = image.size - super().__init__(title, width=width, height=height) - - def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: - self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/ImtImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/ImtImagePlugin.py deleted file mode 100644 index c4eccee..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/ImtImagePlugin.py +++ /dev/null @@ -1,103 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# IM Tools support for PIL -# -# history: -# 1996-05-27 fl Created (read 8-bit images only) -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.2) -# -# Copyright (c) Secret Labs AB 1997-2001. -# Copyright (c) Fredrik Lundh 1996-2001. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re - -from . import Image, ImageFile - -# -# -------------------------------------------------------------------- - -field = re.compile(rb"([a-z]*) ([^ \r\n]*)") - - -## -# Image plugin for IM Tools images. - - -class ImtImageFile(ImageFile.ImageFile): - format = "IMT" - format_description = "IM Tools" - - def _open(self) -> None: - # Quick rejection: if there's not a LF among the first - # 100 bytes, this is (probably) not a text header. - - assert self.fp is not None - - buffer = self.fp.read(100) - if b"\n" not in buffer: - msg = "not an IM file" - raise SyntaxError(msg) - - xsize = ysize = 0 - - while True: - if buffer: - s = buffer[:1] - buffer = buffer[1:] - else: - s = self.fp.read(1) - if not s: - break - - if s == b"\x0c": - # image data begins - self.tile = [ - ImageFile._Tile( - "raw", - (0, 0) + self.size, - self.fp.tell() - len(buffer), - self.mode, - ) - ] - - break - - else: - # read key/value pair - if b"\n" not in buffer: - buffer += self.fp.read(100) - lines = buffer.split(b"\n") - s += lines.pop(0) - buffer = b"\n".join(lines) - if len(s) == 1 or len(s) > 100: - break - if s[0] == ord(b"*"): - continue # comment - - m = field.match(s) - if not m: - break - k, v = m.group(1, 2) - if k == b"width": - xsize = int(v) - self._size = xsize, ysize - elif k == b"height": - ysize = int(v) - self._size = xsize, ysize - elif k == b"pixel" and v == b"n8": - self._mode = "L" - - -# -# -------------------------------------------------------------------- - -Image.register_open(ImtImageFile.format, ImtImageFile) - -# -# no extension registered (".im" is simply too common) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/IptcImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/IptcImagePlugin.py deleted file mode 100644 index 9c8be8b..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/IptcImagePlugin.py +++ /dev/null @@ -1,226 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# IPTC/NAA file handling -# -# history: -# 1995-10-01 fl Created -# 1998-03-09 fl Cleaned up and added to PIL -# 2002-06-18 fl Added getiptcinfo helper -# -# Copyright (c) Secret Labs AB 1997-2002. -# Copyright (c) Fredrik Lundh 1995. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from io import BytesIO -from typing import cast - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import i32be as i32 - -COMPRESSION = {1: "raw", 5: "jpeg"} - - -# -# Helpers - - -def _i(c: bytes) -> int: - return i32((b"\0\0\0\0" + c)[-4:]) - - -## -# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields -# from TIFF and JPEG files, use the <b>getiptcinfo</b> function. - - -class IptcImageFile(ImageFile.ImageFile): - format = "IPTC" - format_description = "IPTC/NAA" - - def getint(self, key: tuple[int, int]) -> int: - return _i(self.info[key]) - - def field(self) -> tuple[tuple[int, int] | None, int]: - # - # get a IPTC field header - assert self.fp is not None - s = self.fp.read(5) - if not s.strip(b"\x00"): - return None, 0 - - tag = s[1], s[2] - - # syntax - if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]: - msg = "invalid IPTC/NAA file" - raise SyntaxError(msg) - - # field size - size = s[3] - if size > 132: - msg = "illegal field length in IPTC/NAA file" - raise OSError(msg) - elif size == 128: - size = 0 - elif size > 128: - size = _i(self.fp.read(size - 128)) - else: - size = i16(s, 3) - - return tag, size - - def _open(self) -> None: - # load descriptive fields - assert self.fp is not None - while True: - offset = self.fp.tell() - tag, size = self.field() - if not tag or tag == (8, 10): - break - if size: - tagdata = self.fp.read(size) - else: - tagdata = None - if tag in self.info: - if isinstance(self.info[tag], list): - self.info[tag].append(tagdata) - else: - self.info[tag] = [self.info[tag], tagdata] - else: - self.info[tag] = tagdata - - # mode - layers = self.info[(3, 60)][0] - component = self.info[(3, 60)][1] - if layers == 1 and not component: - self._mode = "L" - band = None - else: - if layers == 3 and component: - self._mode = "RGB" - elif layers == 4 and component: - self._mode = "CMYK" - if (3, 65) in self.info: - band = self.info[(3, 65)][0] - 1 - else: - band = 0 - - # size - self._size = self.getint((3, 20)), self.getint((3, 30)) - - # compression - try: - compression = COMPRESSION[self.getint((3, 120))] - except KeyError as e: - msg = "Unknown IPTC image compression" - raise OSError(msg) from e - - # tile - if tag == (8, 10): - self.tile = [ - ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band)) - ] - - def load(self) -> Image.core.PixelAccess | None: - if self.tile: - args = self.tile[0].args - assert isinstance(args, tuple) - compression, band = args - - assert self.fp is not None - self.fp.seek(self.tile[0].offset) - - # Copy image data to temporary file - o = BytesIO() - if compression == "raw": - # To simplify access to the extracted file, - # prepend a PPM header - o.write(b"P5\n%d %d\n255\n" % self.size) - while True: - type, size = self.field() - if type != (8, 10): - break - while size > 0: - s = self.fp.read(min(size, 8192)) - if not s: - break - o.write(s) - size -= len(s) - - with Image.open(o) as _im: - if band is not None: - bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode) - bands[band] = _im - im = Image.merge(self.mode, bands) - else: - im = _im - im.load() - self.im = im.im - self.tile = [] - return ImageFile.ImageFile.load(self) - - -Image.register_open(IptcImageFile.format, IptcImageFile) - -Image.register_extension(IptcImageFile.format, ".iim") - - -def getiptcinfo( - im: ImageFile.ImageFile, -) -> dict[tuple[int, int], bytes | list[bytes]] | None: - """ - Get IPTC information from TIFF, JPEG, or IPTC file. - - :param im: An image containing IPTC data. - :returns: A dictionary containing IPTC information, or None if - no IPTC information block was found. - """ - from . import JpegImagePlugin, TiffImagePlugin - - data = None - - if isinstance(im, IptcImageFile): - # return info dictionary right away - return {k: v for k, v in im.info.items() if isinstance(k, tuple)} - - elif isinstance(im, JpegImagePlugin.JpegImageFile): - # extract the IPTC/NAA resource - photoshop = im.info.get("photoshop") - if photoshop: - data = photoshop.get(0x0404) - - elif isinstance(im, TiffImagePlugin.TiffImageFile): - # get raw data from the IPTC/NAA tag (PhotoShop tags the data - # as 4-byte integers, so we cannot use the get method...) - try: - data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] - except KeyError: - pass - - if data is None: - return None # no properties - - # create an IptcImagePlugin object without initializing it - class FakeImage: - pass - - fake_im = FakeImage() - fake_im.__class__ = IptcImageFile # type: ignore[assignment] - iptc_im = cast(IptcImageFile, fake_im) - - # parse the IPTC information chunk - iptc_im.info = {} - iptc_im.fp = BytesIO(data) - - try: - iptc_im._open() - except (IndexError, KeyError): - pass # expected failure - - return {k: v for k, v in iptc_im.info.items() if isinstance(k, tuple)} diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/Jpeg2KImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/Jpeg2KImagePlugin.py deleted file mode 100644 index cb37735..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/Jpeg2KImagePlugin.py +++ /dev/null @@ -1,460 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# JPEG2000 file handling -# -# History: -# 2014-03-12 ajh Created -# 2021-06-30 rogermb Extract dpi information from the 'resc' header box -# -# Copyright (c) 2014 Coriolis Systems Limited -# Copyright (c) 2014 Alastair Houghton -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import os -import struct -from typing import cast - -from . import Image, ImageFile, ImagePalette, _binary - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable - from typing import IO - - -class BoxReader: - """ - A small helper class to read fields stored in JPEG2000 header boxes - and to easily step into and read sub-boxes. - """ - - def __init__(self, fp: IO[bytes], length: int = -1) -> None: - self.fp = fp - self.has_length = length >= 0 - self.length = length - self.remaining_in_box = -1 - - def _can_read(self, num_bytes: int) -> bool: - if self.has_length and self.fp.tell() + num_bytes > self.length: - # Outside box: ensure we don't read past the known file length - return False - if self.remaining_in_box >= 0: - # Inside box contents: ensure read does not go past box boundaries - return num_bytes <= self.remaining_in_box - else: - return True # No length known, just read - - def _read_bytes(self, num_bytes: int) -> bytes: - if not self._can_read(num_bytes): - msg = "Not enough data in header" - raise SyntaxError(msg) - - data = self.fp.read(num_bytes) - if len(data) < num_bytes: - msg = f"Expected to read {num_bytes} bytes but only got {len(data)}." - raise OSError(msg) - - if self.remaining_in_box > 0: - self.remaining_in_box -= num_bytes - return data - - def read_fields(self, field_format: str) -> tuple[int | bytes, ...]: - size = struct.calcsize(field_format) - data = self._read_bytes(size) - return struct.unpack(field_format, data) - - def read_boxes(self) -> BoxReader: - size = self.remaining_in_box - data = self._read_bytes(size) - return BoxReader(io.BytesIO(data), size) - - def has_next_box(self) -> bool: - if self.has_length: - return self.fp.tell() + self.remaining_in_box < self.length - else: - return True - - def next_box_type(self) -> bytes: - # Skip the rest of the box if it has not been read - if self.remaining_in_box > 0: - self.fp.seek(self.remaining_in_box, os.SEEK_CUR) - self.remaining_in_box = -1 - - # Read the length and type of the next box - lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s")) - if lbox == 1: - lbox = cast(int, self.read_fields(">Q")[0]) - hlen = 16 - else: - hlen = 8 - - if lbox < hlen or not self._can_read(lbox - hlen): - msg = "Invalid header length" - raise SyntaxError(msg) - - self.remaining_in_box = lbox - hlen - return tbox - - -def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]: - """Parse the JPEG 2000 codestream to extract the size and component - count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" - - hdr = fp.read(2) - lsiz = _binary.i16be(hdr) - siz = hdr + fp.read(lsiz - 2) - lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( - ">HHIIIIIIIIH", siz - ) - - size = (xsiz - xosiz, ysiz - yosiz) - if csiz == 1: - ssiz = struct.unpack_from(">B", siz, 38) - if (ssiz[0] & 0x7F) + 1 > 8: - mode = "I;16" - else: - mode = "L" - elif csiz == 2: - mode = "LA" - elif csiz == 3: - mode = "RGB" - elif csiz == 4: - mode = "RGBA" - else: - msg = "unable to determine J2K image mode" - raise SyntaxError(msg) - - return size, mode - - -def _res_to_dpi(num: int, denom: int, exp: int) -> float | None: - """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, - calculated as (num / denom) * 10^exp and stored in dots per meter, - to floating-point dots per inch.""" - if denom == 0: - return None - return (254 * num * (10**exp)) / (10000 * denom) - - -def _parse_jp2_header( - fp: IO[bytes], -) -> tuple[ - tuple[int, int], - str, - str | None, - tuple[float, float] | None, - ImagePalette.ImagePalette | None, -]: - """Parse the JP2 header box to extract size, component count, - color space information, and optionally DPI information, - returning a (size, mode, mimetype, dpi) tuple.""" - - # Find the JP2 header box - reader = BoxReader(fp) - header = None - mimetype = None - while reader.has_next_box(): - tbox = reader.next_box_type() - - if tbox == b"jp2h": - header = reader.read_boxes() - break - elif tbox == b"ftyp": - if reader.read_fields(">4s")[0] == b"jpx ": - mimetype = "image/jpx" - assert header is not None - - size = None - mode = None - bpc = None - nc = None - dpi = None # 2-tuple of DPI info, or None - palette = None - colr = None - - while header.has_next_box(): - tbox = header.next_box_type() - - if tbox == b"ihdr": - height, width, nc, bpc = header.read_fields(">IIHB") - assert isinstance(height, int) - assert isinstance(width, int) - assert isinstance(bpc, int) - size = (width, height) - if nc == 1 and (bpc & 0x7F) > 8: - mode = "I;16" - elif nc == 1: - mode = "L" - elif nc == 2: - mode = "LA" - elif nc == 3: - mode = "RGB" - elif nc == 4: - mode = "RGBA" - elif tbox == b"colr": - meth, _, _, enumcs = header.read_fields(">BBBI") - if meth == 1: - if enumcs in (0, 15): - colr = "1" - elif enumcs == 12: - colr = "CMYK" - if nc == 4: - mode = "CMYK" - elif enumcs == 17: - colr = "L" - elif tbox == b"pclr" and mode in ("L", "LA") and colr not in ("1", "L"): - ne, npc = header.read_fields(">HB") - assert isinstance(ne, int) - assert isinstance(npc, int) - max_bitdepth = 0 - for bitdepth in header.read_fields(">" + ("B" * npc)): - assert isinstance(bitdepth, int) - if bitdepth > max_bitdepth: - max_bitdepth = bitdepth - if max_bitdepth <= 8: - if npc == 4: - palette_mode = "CMYK" if colr == "CMYK" else "RGBA" - else: - palette_mode = "RGB" - palette = ImagePalette.ImagePalette(palette_mode) - for i in range(ne): - color: list[int] = [] - for value in header.read_fields(">" + ("B" * npc)): - assert isinstance(value, int) - color.append(value) - palette.getcolor(tuple(color)) - mode = "P" if mode == "L" else "PA" - elif tbox == b"res ": - res = header.read_boxes() - while res.has_next_box(): - tres = res.next_box_type() - if tres == b"resc": - vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") - assert isinstance(vrcn, int) - assert isinstance(vrcd, int) - assert isinstance(hrcn, int) - assert isinstance(hrcd, int) - assert isinstance(vrce, int) - assert isinstance(hrce, int) - hres = _res_to_dpi(hrcn, hrcd, hrce) - vres = _res_to_dpi(vrcn, vrcd, vrce) - if hres is not None and vres is not None: - dpi = (hres, vres) - break - - if size is None or mode is None: - msg = "Malformed JP2 header" - raise SyntaxError(msg) - - return size, mode, mimetype, dpi, palette - - -## -# Image plugin for JPEG2000 images. - - -class Jpeg2KImageFile(ImageFile.ImageFile): - format = "JPEG2000" - format_description = "JPEG 2000 (ISO 15444)" - - def _open(self) -> None: - assert self.fp is not None - sig = self.fp.read(4) - if sig == b"\xff\x4f\xff\x51": - self.codec = "j2k" - self._size, self._mode = _parse_codestream(self.fp) - self._parse_comment() - else: - sig = sig + self.fp.read(8) - - if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": - self.codec = "jp2" - header = _parse_jp2_header(self.fp) - self._size, self._mode, self.custom_mimetype, dpi, self.palette = header - if dpi is not None: - self.info["dpi"] = dpi - if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - self.fp.seek(length - 2, os.SEEK_CUR) - self._parse_comment() - else: - msg = "not a JPEG 2000 file" - raise SyntaxError(msg) - - self._reduce = 0 - self.layers = 0 - - fd = -1 - length = -1 - - try: - fd = self.fp.fileno() - length = os.fstat(fd).st_size - except Exception: - fd = -1 - try: - pos = self.fp.tell() - self.fp.seek(0, io.SEEK_END) - length = self.fp.tell() - self.fp.seek(pos) - except Exception: - length = -1 - - self.tile = [ - ImageFile._Tile( - "jpeg2k", - (0, 0) + self.size, - 0, - (self.codec, self._reduce, self.layers, fd, length), - ) - ] - - def _parse_comment(self) -> None: - assert self.fp is not None - while True: - marker = self.fp.read(2) - if not marker: - break - typ = marker[1] - if typ in (0x90, 0xD9): - # Start of tile or end of codestream - break - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - if typ == 0x64: - # Comment - self.info["comment"] = self.fp.read(length - 2)[2:] - break - else: - self.fp.seek(length - 2, os.SEEK_CUR) - - @property # type: ignore[override] - def reduce( - self, - ) -> ( - Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image] - | int - ): - # https://github.com/python-pillow/Pillow/issues/4343 found that the - # new Image 'reduce' method was shadowed by this plugin's 'reduce' - # property. This attempts to allow for both scenarios - return self._reduce or super().reduce - - @reduce.setter - def reduce(self, value: int) -> None: - self._reduce = value - - def load(self) -> Image.core.PixelAccess | None: - if self.tile and self._reduce: - power = 1 << self._reduce - adjust = power >> 1 - self._size = ( - int((self.size[0] + adjust) / power), - int((self.size[1] + adjust) / power), - ) - - # Update the reduce and layers settings - t = self.tile[0] - assert isinstance(t[3], tuple) - t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) - self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)] - - return ImageFile.ImageFile.load(self) - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith( - (b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a") - ) - - -# ------------------------------------------------------------ -# Save support - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # Get the keyword arguments - info = im.encoderinfo - - if isinstance(filename, str): - filename = filename.encode() - if filename.endswith(b".j2k") or info.get("no_jp2", False): - kind = "j2k" - else: - kind = "jp2" - - offset = info.get("offset", None) - tile_offset = info.get("tile_offset", None) - tile_size = info.get("tile_size", None) - quality_mode = info.get("quality_mode", "rates") - quality_layers = info.get("quality_layers", None) - if quality_layers is not None and not ( - isinstance(quality_layers, (list, tuple)) - and all( - isinstance(quality_layer, (int, float)) for quality_layer in quality_layers - ) - ): - msg = "quality_layers must be a sequence of numbers" - raise ValueError(msg) - - num_resolutions = info.get("num_resolutions", 0) - cblk_size = info.get("codeblock_size", None) - precinct_size = info.get("precinct_size", None) - irreversible = info.get("irreversible", False) - progression = info.get("progression", "LRCP") - cinema_mode = info.get("cinema_mode", "no") - mct = info.get("mct", 0) - signed = info.get("signed", False) - comment = info.get("comment") - if isinstance(comment, str): - comment = comment.encode() - plt = info.get("plt", False) - - fd = -1 - if hasattr(fp, "fileno"): - try: - fd = fp.fileno() - except Exception: - fd = -1 - - im.encoderconfig = ( - offset, - tile_offset, - tile_size, - quality_mode, - quality_layers, - num_resolutions, - cblk_size, - precinct_size, - irreversible, - progression, - cinema_mode, - mct, - signed, - fd, - comment, - plt, - ) - - ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)]) - - -# ------------------------------------------------------------ -# Registry stuff - - -Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) -Image.register_save(Jpeg2KImageFile.format, _save) - -Image.register_extensions( - Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"] -) - -Image.register_mime(Jpeg2KImageFile.format, "image/jp2") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/JpegImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/JpegImagePlugin.py deleted file mode 100644 index 46320eb..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/JpegImagePlugin.py +++ /dev/null @@ -1,889 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# JPEG (JFIF) file handling -# -# See "Digital Compression and Coding of Continuous-Tone Still Images, -# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1) -# -# History: -# 1995-09-09 fl Created -# 1995-09-13 fl Added full parser -# 1996-03-25 fl Added hack to use the IJG command line utilities -# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug -# 1996-05-28 fl Added draft support, JFIF version (0.1) -# 1996-12-30 fl Added encoder options, added progression property (0.2) -# 1997-08-27 fl Save mode 1 images as BW (0.3) -# 1998-07-12 fl Added YCbCr to draft and save methods (0.4) -# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1) -# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2) -# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3) -# 2003-04-25 fl Added experimental EXIF decoder (0.5) -# 2003-06-06 fl Added experimental EXIF GPSinfo decoder -# 2003-09-13 fl Extract COM markers -# 2009-09-06 fl Added icc_profile support (from Florian Hoech) -# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6) -# 2009-03-08 fl Added subsampling support (from Justin Huff). -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import array -import io -import math -import os -import struct -import subprocess -import sys -import tempfile -import warnings - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import o8 -from ._binary import o16be as o16 -from .JpegPresets import presets - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import IO, Any - - from .MpoImagePlugin import MpoImageFile - -# -# Parser - - -def Skip(self: JpegImageFile, marker: int) -> None: - assert self.fp is not None - n = i16(self.fp.read(2)) - 2 - ImageFile._safe_read(self.fp, n) - - -def APP(self: JpegImageFile, marker: int) -> None: - # - # Application marker. Store these in the APP dictionary. - # Also look for well-known application markers. - - assert self.fp is not None - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - - app = f"APP{marker & 15}" - - self.app[app] = s # compatibility - self.applist.append((app, s)) - - if marker == 0xFFE0 and s.startswith(b"JFIF"): - # extract JFIF information - self.info["jfif"] = version = i16(s, 5) # version - self.info["jfif_version"] = divmod(version, 256) - # extract JFIF properties - try: - jfif_unit = s[7] - jfif_density = i16(s, 8), i16(s, 10) - except Exception: - pass - else: - if jfif_unit == 1: - self.info["dpi"] = jfif_density - elif jfif_unit == 2: # cm - # 1 dpcm = 2.54 dpi - self.info["dpi"] = tuple(d * 2.54 for d in jfif_density) - self.info["jfif_unit"] = jfif_unit - self.info["jfif_density"] = jfif_density - elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"): - # extract EXIF information - if "exif" in self.info: - self.info["exif"] += s[6:] - else: - self.info["exif"] = s - self._exif_offset = self.fp.tell() - n + 6 - elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"): - self.info["xmp"] = s.split(b"\x00", 1)[1] - elif marker == 0xFFE2 and s.startswith(b"FPXR\0"): - # extract FlashPix information (incomplete) - self.info["flashpix"] = s # FIXME: value will change - elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"): - # Since an ICC profile can be larger than the maximum size of - # a JPEG marker (64K), we need provisions to split it into - # multiple markers. The format defined by the ICC specifies - # one or more APP2 markers containing the following data: - # Identifying string ASCII "ICC_PROFILE\0" (12 bytes) - # Marker sequence number 1, 2, etc (1 byte) - # Number of markers Total of APP2's used (1 byte) - # Profile data (remainder of APP2 data) - # Decoders should use the marker sequence numbers to - # reassemble the profile, rather than assuming that the APP2 - # markers appear in the correct sequence. - self.icclist.append(s) - elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"): - # parse the image resource block - offset = 14 - photoshop = self.info.setdefault("photoshop", {}) - try: - while s[offset : offset + 4] == b"8BIM": - offset += 4 - # resource code - code = i16(s, offset) - offset += 2 - # resource name (usually empty) - name_len = s[offset] - # name = s[offset+1:offset+1+name_len] - offset += 1 + name_len - offset += offset & 1 # align - # resource data block - size = i32(s, offset) - offset += 4 - data = s[offset : offset + size] - if code == 0x03ED: # ResolutionInfo - photoshop[code] = { - "XResolution": i32(data, 0) / 65536, - "DisplayedUnitsX": i16(data, 4), - "YResolution": i32(data, 8) / 65536, - "DisplayedUnitsY": i16(data, 12), - } - else: - photoshop[code] = data - offset += size - offset += offset & 1 # align - except struct.error: - pass # insufficient data - - elif marker == 0xFFEE and s.startswith(b"Adobe"): - self.info["adobe"] = i16(s, 5) - # extract Adobe custom properties - try: - adobe_transform = s[11] - except IndexError: - pass - else: - self.info["adobe_transform"] = adobe_transform - elif marker == 0xFFE2 and s.startswith(b"MPF\0"): - # extract MPO information - self.info["mp"] = s[4:] - # offset is current location minus buffer size - # plus constant header size - self.info["mpoffset"] = self.fp.tell() - n + 4 - - -def COM(self: JpegImageFile, marker: int) -> None: - # - # Comment marker. Store these in the APP dictionary. - assert self.fp is not None - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - - self.info["comment"] = s - self.app["COM"] = s # compatibility - self.applist.append(("COM", s)) - - -def SOF(self: JpegImageFile, marker: int) -> None: - # - # Start of frame marker. Defines the size and mode of the - # image. JPEG is colour blind, so we use some simple - # heuristics to map the number of layers to an appropriate - # mode. Note that this could be made a bit brighter, by - # looking for JFIF and Adobe APP markers. - - assert self.fp is not None - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - self._size = i16(s, 3), i16(s, 1) - if self._im is not None and self.size != self.im.size: - self._im = None - - self.bits = s[0] - if self.bits != 8: - msg = f"cannot handle {self.bits}-bit layers" - raise SyntaxError(msg) - - self.layers = s[5] - if self.layers == 1: - self._mode = "L" - elif self.layers == 3: - self._mode = "RGB" - elif self.layers == 4: - self._mode = "CMYK" - else: - msg = f"cannot handle {self.layers}-layer images" - raise SyntaxError(msg) - - if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: - self.info["progressive"] = self.info["progression"] = 1 - - if self.icclist: - # fixup icc profile - self.icclist.sort() # sort by sequence number - if self.icclist[0][13] == len(self.icclist): - profile = [p[14:] for p in self.icclist] - icc_profile = b"".join(profile) - else: - icc_profile = None # wrong number of fragments - self.info["icc_profile"] = icc_profile - self.icclist = [] - - for i in range(6, len(s), 3): - t = s[i : i + 3] - # 4-tuples: id, vsamp, hsamp, qtable - self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) - - -def DQT(self: JpegImageFile, marker: int) -> None: - # - # Define quantization table. Note that there might be more - # than one table in each marker. - - # FIXME: The quantization tables can be used to estimate the - # compression quality. - - assert self.fp is not None - n = i16(self.fp.read(2)) - 2 - s = ImageFile._safe_read(self.fp, n) - while len(s): - v = s[0] - precision = 1 if (v // 16 == 0) else 2 # in bytes - qt_length = 1 + precision * 64 - if len(s) < qt_length: - msg = "bad quantization table marker" - raise SyntaxError(msg) - data = array.array("B" if precision == 1 else "H", s[1:qt_length]) - if sys.byteorder == "little" and precision > 1: - data.byteswap() # the values are always big-endian - self.quantization[v & 15] = [data[i] for i in zigzag_index] - s = s[qt_length:] - - -# -# JPEG marker table - -MARKER = { - 0xFFC0: ("SOF0", "Baseline DCT", SOF), - 0xFFC1: ("SOF1", "Extended Sequential DCT", SOF), - 0xFFC2: ("SOF2", "Progressive DCT", SOF), - 0xFFC3: ("SOF3", "Spatial lossless", SOF), - 0xFFC4: ("DHT", "Define Huffman table", Skip), - 0xFFC5: ("SOF5", "Differential sequential DCT", SOF), - 0xFFC6: ("SOF6", "Differential progressive DCT", SOF), - 0xFFC7: ("SOF7", "Differential spatial", SOF), - 0xFFC8: ("JPG", "Extension", None), - 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF), - 0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF), - 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF), - 0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip), - 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF), - 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF), - 0xFFCF: ("SOF15", "Differential spatial (AC)", SOF), - 0xFFD0: ("RST0", "Restart 0", None), - 0xFFD1: ("RST1", "Restart 1", None), - 0xFFD2: ("RST2", "Restart 2", None), - 0xFFD3: ("RST3", "Restart 3", None), - 0xFFD4: ("RST4", "Restart 4", None), - 0xFFD5: ("RST5", "Restart 5", None), - 0xFFD6: ("RST6", "Restart 6", None), - 0xFFD7: ("RST7", "Restart 7", None), - 0xFFD8: ("SOI", "Start of image", None), - 0xFFD9: ("EOI", "End of image", None), - 0xFFDA: ("SOS", "Start of scan", Skip), - 0xFFDB: ("DQT", "Define quantization table", DQT), - 0xFFDC: ("DNL", "Define number of lines", Skip), - 0xFFDD: ("DRI", "Define restart interval", Skip), - 0xFFDE: ("DHP", "Define hierarchical progression", SOF), - 0xFFDF: ("EXP", "Expand reference component", Skip), - 0xFFE0: ("APP0", "Application segment 0", APP), - 0xFFE1: ("APP1", "Application segment 1", APP), - 0xFFE2: ("APP2", "Application segment 2", APP), - 0xFFE3: ("APP3", "Application segment 3", APP), - 0xFFE4: ("APP4", "Application segment 4", APP), - 0xFFE5: ("APP5", "Application segment 5", APP), - 0xFFE6: ("APP6", "Application segment 6", APP), - 0xFFE7: ("APP7", "Application segment 7", APP), - 0xFFE8: ("APP8", "Application segment 8", APP), - 0xFFE9: ("APP9", "Application segment 9", APP), - 0xFFEA: ("APP10", "Application segment 10", APP), - 0xFFEB: ("APP11", "Application segment 11", APP), - 0xFFEC: ("APP12", "Application segment 12", APP), - 0xFFED: ("APP13", "Application segment 13", APP), - 0xFFEE: ("APP14", "Application segment 14", APP), - 0xFFEF: ("APP15", "Application segment 15", APP), - 0xFFF0: ("JPG0", "Extension 0", None), - 0xFFF1: ("JPG1", "Extension 1", None), - 0xFFF2: ("JPG2", "Extension 2", None), - 0xFFF3: ("JPG3", "Extension 3", None), - 0xFFF4: ("JPG4", "Extension 4", None), - 0xFFF5: ("JPG5", "Extension 5", None), - 0xFFF6: ("JPG6", "Extension 6", None), - 0xFFF7: ("JPG7", "Extension 7", None), - 0xFFF8: ("JPG8", "Extension 8", None), - 0xFFF9: ("JPG9", "Extension 9", None), - 0xFFFA: ("JPG10", "Extension 10", None), - 0xFFFB: ("JPG11", "Extension 11", None), - 0xFFFC: ("JPG12", "Extension 12", None), - 0xFFFD: ("JPG13", "Extension 13", None), - 0xFFFE: ("COM", "Comment", COM), -} - - -def _accept(prefix: bytes) -> bool: - # Magic number was taken from https://en.wikipedia.org/wiki/JPEG - return prefix.startswith(b"\xff\xd8\xff") - - -## -# Image plugin for JPEG and JFIF images. - - -class JpegImageFile(ImageFile.ImageFile): - format = "JPEG" - format_description = "JPEG (ISO 10918)" - - def _open(self) -> None: - assert self.fp is not None - s = self.fp.read(3) - - if not _accept(s): - msg = "not a JPEG file" - raise SyntaxError(msg) - s = b"\xff" - - # Create attributes - self.bits = self.layers = 0 - self._exif_offset = 0 - - # JPEG specifics (internal) - self.layer: list[tuple[int, int, int, int]] = [] - self._huffman_dc: dict[Any, Any] = {} - self._huffman_ac: dict[Any, Any] = {} - self.quantization: dict[int, list[int]] = {} - self.app: dict[str, bytes] = {} # compatibility - self.applist: list[tuple[str, bytes]] = [] - self.icclist: list[bytes] = [] - - while True: - i = s[0] - if i == 0xFF: - s = s + self.fp.read(1) - i = i16(s) - else: - # Skip non-0xFF junk - s = self.fp.read(1) - continue - - if i in MARKER: - name, description, handler = MARKER[i] - if handler is not None: - handler(self, i) - if i == 0xFFDA: # start of scan - rawmode = self.mode - if self.mode == "CMYK": - rawmode = "CMYK;I" # assume adobe conventions - self.tile = [ - ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, "")) - ] - # self.__offset = self.fp.tell() - break - s = self.fp.read(1) - elif i in {0, 0xFFFF}: - # padded marker or junk; move on - s = b"\xff" - elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) - s = self.fp.read(1) - else: - msg = "no marker found" - raise SyntaxError(msg) - - self._read_dpi_from_exif() - - def __getstate__(self) -> list[Any]: - return super().__getstate__() + [self.layers, self.layer] - - def __setstate__(self, state: list[Any]) -> None: - self.layers, self.layer = state[6:] - super().__setstate__(state) - - def load_read(self, read_bytes: int) -> bytes: - """ - internal: read more image data - For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker - so libjpeg can finish decoding - """ - assert self.fp is not None - s = self.fp.read(read_bytes) - - if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): - # Premature EOF. - # Pretend file is finished adding EOI marker - self._ended = True - return b"\xff\xd9" - - return s - - def draft( - self, mode: str | None, size: tuple[int, int] | None - ) -> tuple[str, tuple[int, int, float, float]] | None: - if len(self.tile) != 1: - return None - - # Protect from second call - if self.decoderconfig: - return None - - d, e, o, a = self.tile[0] - scale = 1 - original_size = self.size - - assert isinstance(a, tuple) - if a[0] == "RGB" and mode in ["L", "YCbCr"]: - self._mode = mode - a = mode, "" - - if size: - scale = min(self.size[0] // size[0], self.size[1] // size[1]) - for s in [8, 4, 2, 1]: - if scale >= s: - break - assert e is not None - e = ( - e[0], - e[1], - (e[2] - e[0] + s - 1) // s + e[0], - (e[3] - e[1] + s - 1) // s + e[1], - ) - self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) - scale = s - - self.tile = [ImageFile._Tile(d, e, o, a)] - self.decoderconfig = (scale, 0) - - box = (0, 0, original_size[0] / scale, original_size[1] / scale) - return self.mode, box - - def load_djpeg(self) -> None: - # ALTERNATIVE: handle JPEGs via the IJG command line utilities - - f, path = tempfile.mkstemp() - os.close(f) - if os.path.exists(self.filename): - subprocess.check_call(["djpeg", "-outfile", path, self.filename]) - else: - try: - os.unlink(path) - except OSError: - pass - - msg = "Invalid Filename" - raise ValueError(msg) - - try: - with Image.open(path) as _im: - _im.load() - self.im = _im.im - finally: - try: - os.unlink(path) - except OSError: - pass - - self._mode = self.im.mode - self._size = self.im.size - - self.tile = [] - - def _getexif(self) -> dict[int, Any] | None: - return _getexif(self) - - def _read_dpi_from_exif(self) -> None: - # If DPI isn't in JPEG header, fetch from EXIF - if "dpi" in self.info or "exif" not in self.info: - return - try: - exif = self.getexif() - resolution_unit = exif[0x0128] - x_resolution = exif[0x011A] - try: - dpi = float(x_resolution[0]) / x_resolution[1] - except TypeError: - dpi = x_resolution - if math.isnan(dpi): - msg = "DPI is not a number" - raise ValueError(msg) - if resolution_unit == 3: # cm - # 1 dpcm = 2.54 dpi - dpi *= 2.54 - self.info["dpi"] = dpi, dpi - except ( - struct.error, # truncated EXIF - KeyError, # dpi not included - SyntaxError, # invalid/unreadable EXIF - TypeError, # dpi is an invalid float - ValueError, # dpi is an invalid float - ZeroDivisionError, # invalid dpi rational value - ): - self.info["dpi"] = 72, 72 - - def _getmp(self) -> dict[int, Any] | None: - return _getmp(self) - - -def _getexif(self: JpegImageFile) -> dict[int, Any] | None: - if "exif" not in self.info: - return None - return self.getexif()._get_merged_dict() - - -def _getmp(self: JpegImageFile) -> dict[int, Any] | None: - # Extract MP information. This method was inspired by the "highly - # experimental" _getexif version that's been in use for years now, - # itself based on the ImageFileDirectory class in the TIFF plugin. - - # The MP record essentially consists of a TIFF file embedded in a JPEG - # application marker. - try: - data = self.info["mp"] - except KeyError: - return None - file_contents = io.BytesIO(data) - head = file_contents.read(8) - endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<" - # process dictionary - from . import TiffImagePlugin - - try: - info = TiffImagePlugin.ImageFileDirectory_v2(head) - file_contents.seek(info.next) - info.load(file_contents) - mp = dict(info) - except Exception as e: - msg = "malformed MP Index (unreadable directory)" - raise SyntaxError(msg) from e - # it's an error not to have a number of images - try: - quant = mp[0xB001] - except KeyError as e: - msg = "malformed MP Index (no number of images)" - raise SyntaxError(msg) from e - # get MP entries - mpentries = [] - try: - rawmpentries = mp[0xB002] - for entrynum in range(quant): - unpackedentry = struct.unpack_from( - f"{endianness}LLLHH", rawmpentries, entrynum * 16 - ) - labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2") - mpentry = dict(zip(labels, unpackedentry)) - mpentryattr = { - "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)), - "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)), - "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)), - "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27, - "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24, - "MPType": mpentry["Attribute"] & 0x00FFFFFF, - } - if mpentryattr["ImageDataFormat"] == 0: - mpentryattr["ImageDataFormat"] = "JPEG" - else: - msg = "unsupported picture format in MPO" - raise SyntaxError(msg) - mptypemap = { - 0x000000: "Undefined", - 0x010001: "Large Thumbnail (VGA Equivalent)", - 0x010002: "Large Thumbnail (Full HD Equivalent)", - 0x020001: "Multi-Frame Image (Panorama)", - 0x020002: "Multi-Frame Image: (Disparity)", - 0x020003: "Multi-Frame Image: (Multi-Angle)", - 0x030000: "Baseline MP Primary Image", - } - mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown") - mpentry["Attribute"] = mpentryattr - mpentries.append(mpentry) - mp[0xB002] = mpentries - except KeyError as e: - msg = "malformed MP Index (bad MP Entry)" - raise SyntaxError(msg) from e - # Next we should try and parse the individual image unique ID list; - # we don't because I've never seen this actually used in a real MPO - # file and so can't test it. - return mp - - -# -------------------------------------------------------------------- -# stuff to save JPEG files - -RAWMODE = { - "1": "L", - "L": "L", - "RGB": "RGB", - "RGBX": "RGB", - "CMYK": "CMYK;I", # assume adobe conventions - "YCbCr": "YCbCr", -} - -# fmt: off -zigzag_index = ( - 0, 1, 5, 6, 14, 15, 27, 28, - 2, 4, 7, 13, 16, 26, 29, 42, - 3, 8, 12, 17, 25, 30, 41, 43, - 9, 11, 18, 24, 31, 40, 44, 53, - 10, 19, 23, 32, 39, 45, 52, 54, - 20, 22, 33, 38, 46, 51, 55, 60, - 21, 34, 37, 47, 50, 56, 59, 61, - 35, 36, 48, 49, 57, 58, 62, 63, -) - -samplings = { - (1, 1, 1, 1, 1, 1): 0, - (2, 1, 1, 1, 1, 1): 1, - (2, 2, 1, 1, 1, 1): 2, -} -# fmt: on - - -def get_sampling(im: Image.Image) -> int: - # There's no subsampling when images have only 1 layer - # (grayscale images) or when they are CMYK (4 layers), - # so set subsampling to the default value. - # - # NOTE: currently Pillow can't encode JPEG to YCCK format. - # If YCCK support is added in the future, subsampling code will have - # to be updated (here and in JpegEncode.c) to deal with 4 layers. - if not isinstance(im, JpegImageFile) or im.layers in (1, 4): - return -1 - sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] - return samplings.get(sampling, -1) - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - rawmode = RAWMODE[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as JPEG" - raise OSError(msg) from e - - info = im.encoderinfo - - dpi = [round(x) for x in info.get("dpi", (0, 0))] - - quality = info.get("quality", -1) - subsampling = info.get("subsampling", -1) - qtables = info.get("qtables") - - if quality == "keep": - quality = -1 - subsampling = "keep" - qtables = "keep" - elif quality in presets: - preset = presets[quality] - quality = -1 - subsampling = preset.get("subsampling", -1) - qtables = preset.get("quantization") - elif not isinstance(quality, int): - msg = "Invalid quality setting" - raise ValueError(msg) - else: - if subsampling in presets: - subsampling = presets[subsampling].get("subsampling", -1) - if isinstance(qtables, str) and qtables in presets: - qtables = presets[qtables].get("quantization") - - if subsampling == "4:4:4": - subsampling = 0 - elif subsampling == "4:2:2": - subsampling = 1 - elif subsampling == "4:2:0": - subsampling = 2 - elif subsampling == "4:1:1": - # For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0. - # Set 4:2:0 if someone is still using that value. - subsampling = 2 - elif subsampling == "keep": - if im.format != "JPEG": - msg = "Cannot use 'keep' when original image is not a JPEG" - raise ValueError(msg) - subsampling = get_sampling(im) - - def validate_qtables( - qtables: ( - str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None - ), - ) -> list[list[int]] | None: - if qtables is None: - return qtables - if isinstance(qtables, str): - try: - lines = [ - int(num) - for line in qtables.splitlines() - for num in line.split("#", 1)[0].split() - ] - except ValueError as e: - msg = "Invalid quantization table" - raise ValueError(msg) from e - else: - qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] - if isinstance(qtables, (tuple, list, dict)): - if isinstance(qtables, dict): - qtables = [ - qtables[key] for key in range(len(qtables)) if key in qtables - ] - elif isinstance(qtables, tuple): - qtables = list(qtables) - if not (0 < len(qtables) < 5): - msg = "None or too many quantization tables" - raise ValueError(msg) - try: - for idx, table in enumerate(qtables): - if len(table) != 64: - msg = "Invalid quantization table" - raise TypeError(msg) - qtables[idx] = list(array.array("H", table)) - except TypeError as e: - msg = "Invalid quantization table" - raise ValueError(msg) from e - return qtables - - if qtables == "keep": - if im.format != "JPEG": - msg = "Cannot use 'keep' when original image is not a JPEG" - raise ValueError(msg) - qtables = getattr(im, "quantization", None) - qtables = validate_qtables(qtables) - - extra = info.get("extra", b"") - - MAX_BYTES_IN_MARKER = 65533 - if xmp := info.get("xmp"): - overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" - max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len - if len(xmp) > max_data_bytes_in_marker: - msg = "XMP data is too long" - raise ValueError(msg) - size = o16(2 + overhead_len + len(xmp)) - extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp - - if icc_profile := info.get("icc_profile"): - overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) - max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len - markers = [] - while icc_profile: - markers.append(icc_profile[:max_data_bytes_in_marker]) - icc_profile = icc_profile[max_data_bytes_in_marker:] - i = 1 - for marker in markers: - size = o16(2 + overhead_len + len(marker)) - extra += ( - b"\xff\xe2" - + size - + b"ICC_PROFILE\0" - + o8(i) - + o8(len(markers)) - + marker - ) - i += 1 - - comment = info.get("comment", im.info.get("comment")) - - # "progressive" is the official name, but older documentation - # says "progression" - # FIXME: issue a warning if the wrong form is used (post-1.1.7) - progressive = info.get("progressive", False) or info.get("progression", False) - - optimize = info.get("optimize", False) - - exif = info.get("exif", b"") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - if len(exif) > MAX_BYTES_IN_MARKER: - msg = "EXIF data is too long" - raise ValueError(msg) - - # get keyword arguments - im.encoderconfig = ( - quality, - progressive, - info.get("smooth", 0), - optimize, - info.get("keep_rgb", False), - info.get("streamtype", 0), - dpi, - subsampling, - info.get("restart_marker_blocks", 0), - info.get("restart_marker_rows", 0), - qtables, - comment, - extra, - exif, - ) - - # if we optimize, libjpeg needs a buffer big enough to hold the whole image - # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is - # channels*size, this is a value that's been used in a django patch. - # https://github.com/matthewwithanm/django-imagekit/issues/50 - if optimize or progressive: - # CMYK can be bigger - if im.mode == "CMYK": - bufsize = 4 * im.size[0] * im.size[1] - # keep sets quality to -1, but the actual value may be high. - elif quality >= 95 or quality == -1: - bufsize = 2 * im.size[0] * im.size[1] - else: - bufsize = im.size[0] * im.size[1] - if exif: - bufsize += len(exif) + 5 - if extra: - bufsize += len(extra) + 1 - else: - # The EXIF info needs to be written as one block, + APP1, + one spare byte. - # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(len(exif) + 5, len(extra) + 1) - - ImageFile._save( - im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize - ) - - -## -# Factory for making JPEG and MPO instances -def jpeg_factory( - fp: IO[bytes], filename: str | bytes | None = None -) -> JpegImageFile | MpoImageFile: - im = JpegImageFile(fp, filename) - try: - mpheader = im._getmp() - if mpheader is not None and mpheader[45057] > 1: - for segment, content in im.applist: - if segment == "APP1" and b' hdrgm:Version="' in content: - # Ultra HDR images are not yet supported - return im - # It's actually an MPO - from .MpoImagePlugin import MpoImageFile - - # Don't reload everything, just convert it. - im = MpoImageFile.adopt(im, mpheader) - except (TypeError, IndexError): - # It is really a JPEG - pass - except SyntaxError: - warnings.warn( - "Image appears to be a malformed MPO file, it will be " - "interpreted as a base JPEG file" - ) - return im - - -# --------------------------------------------------------------------- -# Registry stuff - -Image.register_open(JpegImageFile.format, jpeg_factory, _accept) -Image.register_save(JpegImageFile.format, _save) - -Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"]) - -Image.register_mime(JpegImageFile.format, "image/jpeg") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/JpegPresets.py b/presentation/.venv/lib/python3.12/site-packages/PIL/JpegPresets.py deleted file mode 100644 index d0e64a3..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/JpegPresets.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -JPEG quality settings equivalent to the Photoshop settings. -Can be used when saving JPEG files. - -The following presets are available by default: -``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``, -``low``, ``medium``, ``high``, ``maximum``. -More presets can be added to the :py:data:`presets` dict if needed. - -To apply the preset, specify:: - - quality="preset_name" - -To apply only the quantization table:: - - qtables="preset_name" - -To apply only the subsampling setting:: - - subsampling="preset_name" - -Example:: - - im.save("image_name.jpg", quality="web_high") - -Subsampling ------------ - -Subsampling is the practice of encoding images by implementing less resolution -for chroma information than for luma information. -(ref.: https://en.wikipedia.org/wiki/Chroma_subsampling) - -Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and -4:2:0. - -You can get the subsampling of a JPEG with the -:func:`.JpegImagePlugin.get_sampling` function. - -In JPEG compressed data a JPEG marker is used instead of an EXIF tag. -(ref.: https://exiv2.org/tags.html) - - -Quantization tables -------------------- - -They are values use by the DCT (Discrete cosine transform) to remove -*unnecessary* information from the image (the lossy part of the compression). -(ref.: https://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices, -https://en.wikipedia.org/wiki/JPEG#Quantization) - -You can get the quantization tables of a JPEG with:: - - im.quantization - -This will return a dict with a number of lists. You can pass this dict -directly as the qtables argument when saving a JPEG. - -The quantization table format in presets is a list with sublists. These formats -are interchangeable. - -Libjpeg ref.: -https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html - -""" - -from __future__ import annotations - -# fmt: off -presets = { - 'web_low': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [20, 16, 25, 39, 50, 46, 62, 68, - 16, 18, 23, 38, 38, 53, 65, 68, - 25, 23, 31, 38, 53, 65, 68, 68, - 39, 38, 38, 53, 65, 68, 68, 68, - 50, 38, 53, 65, 68, 68, 68, 68, - 46, 53, 65, 68, 68, 68, 68, 68, - 62, 65, 68, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68], - [21, 25, 32, 38, 54, 68, 68, 68, - 25, 28, 24, 38, 54, 68, 68, 68, - 32, 24, 32, 43, 66, 68, 68, 68, - 38, 38, 43, 53, 68, 68, 68, 68, - 54, 54, 66, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68, - 68, 68, 68, 68, 68, 68, 68, 68] - ]}, - 'web_medium': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [16, 11, 11, 16, 23, 27, 31, 30, - 11, 12, 12, 15, 20, 23, 23, 30, - 11, 12, 13, 16, 23, 26, 35, 47, - 16, 15, 16, 23, 26, 37, 47, 64, - 23, 20, 23, 26, 39, 51, 64, 64, - 27, 23, 26, 37, 51, 64, 64, 64, - 31, 23, 35, 47, 64, 64, 64, 64, - 30, 30, 47, 64, 64, 64, 64, 64], - [17, 15, 17, 21, 20, 26, 38, 48, - 15, 19, 18, 17, 20, 26, 35, 43, - 17, 18, 20, 22, 26, 30, 46, 53, - 21, 17, 22, 28, 30, 39, 53, 64, - 20, 20, 26, 30, 39, 48, 64, 64, - 26, 26, 30, 39, 48, 63, 64, 64, - 38, 35, 46, 53, 64, 64, 64, 64, - 48, 43, 53, 64, 64, 64, 64, 64] - ]}, - 'web_high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 14, 19, - 6, 6, 6, 11, 12, 15, 19, 28, - 9, 8, 10, 12, 16, 20, 27, 31, - 11, 10, 12, 15, 20, 27, 31, 31, - 12, 12, 14, 19, 27, 31, 31, 31, - 16, 12, 19, 28, 31, 31, 31, 31], - [7, 7, 13, 24, 26, 31, 31, 31, - 7, 12, 16, 21, 31, 31, 31, 31, - 13, 16, 17, 31, 31, 31, 31, 31, - 24, 21, 31, 31, 31, 31, 31, 31, - 26, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31, - 31, 31, 31, 31, 31, 31, 31, 31] - ]}, - 'web_very_high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 11, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 11, 14, 12, 12, 12, 12, 12, - 13, 14, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'web_maximum': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 2, - 1, 1, 1, 1, 1, 1, 2, 2, - 1, 1, 1, 1, 1, 2, 2, 3, - 1, 1, 1, 1, 2, 2, 3, 3, - 1, 1, 1, 2, 2, 3, 3, 3, - 1, 1, 2, 2, 3, 3, 3, 3], - [1, 1, 1, 2, 2, 3, 3, 3, - 1, 1, 1, 2, 3, 3, 3, 3, - 1, 1, 1, 3, 3, 3, 3, 3, - 2, 2, 3, 3, 3, 3, 3, 3, - 2, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3] - ]}, - 'low': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [18, 14, 14, 21, 30, 35, 34, 17, - 14, 16, 16, 19, 26, 23, 12, 12, - 14, 16, 17, 21, 23, 12, 12, 12, - 21, 19, 21, 23, 12, 12, 12, 12, - 30, 26, 23, 12, 12, 12, 12, 12, - 35, 23, 12, 12, 12, 12, 12, 12, - 34, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12], - [20, 19, 22, 27, 20, 20, 17, 17, - 19, 25, 23, 14, 14, 12, 12, 12, - 22, 23, 14, 14, 12, 12, 12, 12, - 27, 14, 14, 12, 12, 12, 12, 12, - 20, 14, 12, 12, 12, 12, 12, 12, - 20, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'medium': {'subsampling': 2, # "4:2:0" - 'quantization': [ - [12, 8, 8, 12, 17, 21, 24, 17, - 8, 9, 9, 11, 15, 19, 12, 12, - 8, 9, 10, 12, 19, 12, 12, 12, - 12, 11, 12, 21, 12, 12, 12, 12, - 17, 15, 19, 12, 12, 12, 12, 12, - 21, 19, 12, 12, 12, 12, 12, 12, - 24, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12], - [13, 11, 13, 16, 20, 20, 17, 17, - 11, 14, 14, 14, 14, 12, 12, 12, - 13, 14, 14, 14, 12, 12, 12, 12, - 16, 14, 14, 12, 12, 12, 12, 12, - 20, 14, 12, 12, 12, 12, 12, 12, - 20, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 12, 12, - 6, 6, 6, 11, 12, 12, 12, 12, - 9, 8, 10, 12, 12, 12, 12, 12, - 11, 10, 12, 12, 12, 12, 12, 12, - 12, 12, 12, 12, 12, 12, 12, 12, - 16, 12, 12, 12, 12, 12, 12, 12], - [7, 7, 13, 24, 20, 20, 17, 17, - 7, 12, 16, 14, 14, 12, 12, 12, - 13, 16, 14, 14, 12, 12, 12, 12, - 24, 14, 14, 12, 12, 12, 12, 12, - 20, 14, 12, 12, 12, 12, 12, 12, - 20, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12, - 17, 12, 12, 12, 12, 12, 12, 12] - ]}, - 'maximum': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 10, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 10, 14, 12, 12, 12, 12, 12, - 13, 14, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12, - 15, 12, 12, 12, 12, 12, 12, 12] - ]}, -} -# fmt: on diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/McIdasImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/McIdasImagePlugin.py deleted file mode 100644 index 9a47933..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/McIdasImagePlugin.py +++ /dev/null @@ -1,78 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Basic McIdas support for PIL -# -# History: -# 1997-05-05 fl Created (8-bit images only) -# 2009-03-08 fl Added 16/32-bit support. -# -# Thanks to Richard Jones and Craig Swank for specs and samples. -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import struct - -from . import Image, ImageFile - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04") - - -## -# Image plugin for McIdas area images. - - -class McIdasImageFile(ImageFile.ImageFile): - format = "MCIDAS" - format_description = "McIdas area file" - - def _open(self) -> None: - # parse area file directory - assert self.fp is not None - - s = self.fp.read(256) - if not _accept(s) or len(s) != 256: - msg = "not an McIdas area file" - raise SyntaxError(msg) - - self.area_descriptor_raw = s - self.area_descriptor = w = [0, *struct.unpack("!64i", s)] - - # get mode - if w[11] == 1: - mode = rawmode = "L" - elif w[11] == 2: - mode = rawmode = "I;16B" - elif w[11] == 4: - # FIXME: add memory map support - mode = "I" - rawmode = "I;32B" - else: - msg = "unsupported McIdas format" - raise SyntaxError(msg) - - self._mode = mode - self._size = w[10], w[9] - - offset = w[34] + w[15] - stride = w[15] + w[10] * w[11] * w[14] - - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1)) - ] - - -# -------------------------------------------------------------------- -# registry - -Image.register_open(McIdasImageFile.format, McIdasImageFile, _accept) - -# no default extension diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/MicImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/MicImagePlugin.py deleted file mode 100644 index 99a07ba..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/MicImagePlugin.py +++ /dev/null @@ -1,103 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Microsoft Image Composer support for PIL -# -# Notes: -# uses TiffImagePlugin.py to read the actual image streams -# -# History: -# 97-01-20 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import olefile - -from . import Image, TiffImagePlugin - -# -# -------------------------------------------------------------------- - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(olefile.MAGIC) - - -## -# Image plugin for Microsoft's Image Composer file format. - - -class MicImageFile(TiffImagePlugin.TiffImageFile): - format = "MIC" - format_description = "Microsoft Image Composer" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # read the OLE directory and see if this is a likely - # to be a Microsoft Image Composer file - - try: - self.ole = olefile.OleFileIO(self.fp) - except OSError as e: - msg = "not an MIC file; invalid OLE file" - raise SyntaxError(msg) from e - - # find ACI subfiles with Image members (maybe not the - # best way to identify MIC files, but what the... ;-) - - self.images = [ - path - for path in self.ole.listdir() - if path[1:] and path[0].endswith(".ACI") and path[1] == "Image" - ] - - # if we didn't find any images, this is probably not - # an MIC file. - if not self.images: - msg = "not an MIC file; no image entries" - raise SyntaxError(msg) - - self.frame = -1 - self._n_frames = len(self.images) - self.is_animated = self._n_frames > 1 - - assert self.fp is not None - self.__fp = self.fp - self.seek(0) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - filename = self.images[frame] - self.fp = self.ole.openstream(filename) - - TiffImagePlugin.TiffImageFile._open(self) - - self.frame = frame - - def tell(self) -> int: - return self.frame - - def close(self) -> None: - self.__fp.close() - self.ole.close() - super().close() - - def __exit__(self, *args: object) -> None: - self.__fp.close() - self.ole.close() - super().__exit__() - - -# -# -------------------------------------------------------------------- - -Image.register_open(MicImageFile.format, MicImageFile, _accept) - -Image.register_extension(MicImageFile.format, ".mic") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/MpegImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/MpegImagePlugin.py deleted file mode 100644 index 47ebe9d..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/MpegImagePlugin.py +++ /dev/null @@ -1,84 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# MPEG file handling -# -# History: -# 95-09-09 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1995. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i8 -from ._typing import SupportsRead - -# -# Bitstream parser - - -class BitStream: - def __init__(self, fp: SupportsRead[bytes]) -> None: - self.fp = fp - self.bits = 0 - self.bitbuffer = 0 - - def next(self) -> int: - return i8(self.fp.read(1)) - - def peek(self, bits: int) -> int: - while self.bits < bits: - self.bitbuffer = (self.bitbuffer << 8) + self.next() - self.bits += 8 - return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 - - def skip(self, bits: int) -> None: - while self.bits < bits: - self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) - self.bits += 8 - self.bits = self.bits - bits - - def read(self, bits: int) -> int: - v = self.peek(bits) - self.bits = self.bits - bits - return v - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\x00\x00\x01\xb3") - - -## -# Image plugin for MPEG streams. This plugin can identify a stream, -# but it cannot read it. - - -class MpegImageFile(ImageFile.ImageFile): - format = "MPEG" - format_description = "MPEG" - - def _open(self) -> None: - assert self.fp is not None - - s = BitStream(self.fp) - if s.read(32) != 0x1B3: - msg = "not an MPEG file" - raise SyntaxError(msg) - - self._mode = "RGB" - self._size = s.read(12), s.read(12) - - -# -------------------------------------------------------------------- -# Registry stuff - -Image.register_open(MpegImageFile.format, MpegImageFile, _accept) - -Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"]) - -Image.register_mime(MpegImageFile.format, "video/mpeg") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/MpoImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/MpoImagePlugin.py deleted file mode 100644 index bee0a56..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/MpoImagePlugin.py +++ /dev/null @@ -1,203 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# MPO file handling -# -# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the -# Camera & Imaging Products Association) -# -# The multi-picture object combines multiple JPEG images (with a modified EXIF -# data format) into a single file. While it can theoretically be used much like -# a GIF animation, it is commonly used to represent 3D photographs and is (as -# of this writing) the most commonly used format by 3D cameras. -# -# History: -# 2014-03-13 Feneric Created -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -import struct -from typing import IO, Any, cast - -from . import ( - Image, - ImageFile, - ImageSequence, - JpegImagePlugin, - TiffImagePlugin, -) -from ._binary import o32le -from ._util import DeferredError - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - JpegImagePlugin._save(im, fp, filename) - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - append_images = im.encoderinfo.get("append_images", []) - if not append_images and not getattr(im, "is_animated", False): - _save(im, fp, filename) - return - - mpf_offset = 28 - offsets: list[int] = [] - im_sequences = [im, *append_images] - total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences) - for im_sequence in im_sequences: - for im_frame in ImageSequence.Iterator(im_sequence): - if not offsets: - # APP2 marker - ifd_length = 66 + 16 * total - im_frame.encoderinfo["extra"] = ( - b"\xff\xe2" - + struct.pack(">H", 6 + ifd_length) - + b"MPF\0" - + b" " * ifd_length - ) - if exif := im_frame.encoderinfo.get("exif"): - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - im_frame.encoderinfo["exif"] = exif - mpf_offset += 4 + len(exif) - - JpegImagePlugin._save(im_frame, fp, filename) - offsets.append(fp.tell()) - else: - encoderinfo = im_frame._attach_default_encoderinfo(im) - im_frame.save(fp, "JPEG") - im_frame.encoderinfo = encoderinfo - offsets.append(fp.tell() - offsets[-1]) - - ifd = TiffImagePlugin.ImageFileDirectory_v2() - ifd[0xB000] = b"0100" - ifd[0xB001] = len(offsets) - - mpentries = b"" - data_offset = 0 - for i, size in enumerate(offsets): - if i == 0: - mptype = 0x030000 # Baseline MP Primary Image - else: - mptype = 0x000000 # Undefined - mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0) - if i == 0: - data_offset -= mpf_offset - data_offset += size - ifd[0xB002] = mpentries - - fp.seek(mpf_offset) - fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8)) - fp.seek(0, os.SEEK_END) - - -## -# Image plugin for MPO images. - - -class MpoImageFile(JpegImagePlugin.JpegImageFile): - format = "MPO" - format_description = "MPO (CIPA DC-007)" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - assert self.fp is not None - self.fp.seek(0) # prep the fp in order to pass the JPEG test - JpegImagePlugin.JpegImageFile._open(self) - self._after_jpeg_open() - - def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None: - self.mpinfo = mpheader if mpheader is not None else self._getmp() - if self.mpinfo is None: - msg = "Image appears to be a malformed MPO file" - raise ValueError(msg) - self.n_frames = self.mpinfo[0xB001] - self.__mpoffsets = [ - mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] - ] - self.__mpoffsets[0] = 0 - # Note that the following assertion will only be invalid if something - # gets broken within JpegImagePlugin. - assert self.n_frames == len(self.__mpoffsets) - del self.info["mpoffset"] # no longer needed - self.is_animated = self.n_frames > 1 - assert self.fp is not None - self._fp = self.fp # FIXME: hack - self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame - self.__frame = 0 - self.offset = 0 - # for now we can only handle reading and individual frame extraction - self.readonly = 1 - - def load_seek(self, pos: int) -> None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self._fp.seek(pos) - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self.fp = self._fp - self.offset = self.__mpoffsets[frame] - - original_exif = self.info.get("exif") - if "exif" in self.info: - del self.info["exif"] - - self.fp.seek(self.offset + 2) # skip SOI marker - if not self.fp.read(2): - msg = "No data found for frame" - raise ValueError(msg) - self.fp.seek(self.offset) - JpegImagePlugin.JpegImageFile._open(self) - if self.info.get("exif") != original_exif: - self._reload_exif() - - self.tile = [ - ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1]) - ] - self.__frame = frame - - def tell(self) -> int: - return self.__frame - - @staticmethod - def adopt( - jpeg_instance: JpegImagePlugin.JpegImageFile, - mpheader: dict[int, Any] | None = None, - ) -> MpoImageFile: - """ - Transform the instance of JpegImageFile into - an instance of MpoImageFile. - After the call, the JpegImageFile is extended - to be an MpoImageFile. - - This is essentially useful when opening a JPEG - file that reveals itself as an MPO, to avoid - double call to _open. - """ - jpeg_instance.__class__ = MpoImageFile - mpo_instance = cast(MpoImageFile, jpeg_instance) - mpo_instance._after_jpeg_open(mpheader) - return mpo_instance - - -# --------------------------------------------------------------------- -# Registry stuff - -# Note that since MPO shares a factory with JPEG, we do not need to do a -# separate registration for it here. -# Image.register_open(MpoImageFile.format, -# JpegImagePlugin.jpeg_factory, _accept) -Image.register_save(MpoImageFile.format, _save) -Image.register_save_all(MpoImageFile.format, _save_all) - -Image.register_extension(MpoImageFile.format, ".mpo") - -Image.register_mime(MpoImageFile.format, "image/mpo") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/MspImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/MspImagePlugin.py deleted file mode 100644 index fa0f52f..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/MspImagePlugin.py +++ /dev/null @@ -1,200 +0,0 @@ -# -# The Python Imaging Library. -# -# MSP file handling -# -# This is the format used by the Paint program in Windows 1 and 2. -# -# History: -# 95-09-05 fl Created -# 97-01-03 fl Read/write MSP images -# 17-02-21 es Fixed RLE interpretation -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1995-97. -# Copyright (c) Eric Soroos 2017. -# -# See the README file for information on usage and redistribution. -# -# More info on this format: https://archive.org/details/gg243631 -# Page 313: -# Figure 205. Windows Paint Version 1: "DanM" Format -# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03 -# -# See also: https://www.fileformat.info/format/mspaint/egff.htm -from __future__ import annotations - -import io -import struct -from typing import IO - -from . import Image, ImageFile -from ._binary import i16le as i16 -from ._binary import o16le as o16 - -# -# read MSP files - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"DanM", b"LinS")) - - -## -# Image plugin for Windows MSP images. This plugin supports both -# uncompressed (Windows 1.0). - - -class MspImageFile(ImageFile.ImageFile): - format = "MSP" - format_description = "Windows Paint" - - def _open(self) -> None: - # Header - assert self.fp is not None - - s = self.fp.read(32) - if not _accept(s): - msg = "not an MSP file" - raise SyntaxError(msg) - - # Header checksum - checksum = 0 - for i in range(0, 32, 2): - checksum = checksum ^ i16(s, i) - if checksum != 0: - msg = "bad MSP checksum" - raise SyntaxError(msg) - - self._mode = "1" - self._size = i16(s, 4), i16(s, 6) - - if s.startswith(b"DanM"): - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] - else: - self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] - - -class MspDecoder(ImageFile.PyDecoder): - # The algo for the MSP decoder is from - # https://www.fileformat.info/format/mspaint/egff.htm - # cc-by-attribution -- That page references is taken from the - # Encyclopedia of Graphics File Formats and is licensed by - # O'Reilly under the Creative Common/Attribution license - # - # For RLE encoded files, the 32byte header is followed by a scan - # line map, encoded as one 16bit word of encoded byte length per - # line. - # - # NOTE: the encoded length of the line can be 0. This was not - # handled in the previous version of this encoder, and there's no - # mention of how to handle it in the documentation. From the few - # examples I've seen, I've assumed that it is a fill of the - # background color, in this case, white. - # - # - # Pseudocode of the decoder: - # Read a BYTE value as the RunType - # If the RunType value is zero - # Read next byte as the RunCount - # Read the next byte as the RunValue - # Write the RunValue byte RunCount times - # If the RunType value is non-zero - # Use this value as the RunCount - # Read and write the next RunCount bytes literally - # - # e.g.: - # 0x00 03 ff 05 00 01 02 03 04 - # would yield the bytes: - # 0xff ff ff 00 01 02 03 04 - # - # which are then interpreted as a bit packed mode '1' image - - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - - img = io.BytesIO() - blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) - try: - self.fd.seek(32) - rowmap = struct.unpack_from( - f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) - ) - except struct.error as e: - msg = "Truncated MSP file in row map" - raise OSError(msg) from e - - for x, rowlen in enumerate(rowmap): - try: - if rowlen == 0: - img.write(blank_line) - continue - row = self.fd.read(rowlen) - if len(row) != rowlen: - msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}" - raise OSError(msg) - idx = 0 - while idx < rowlen: - runtype = row[idx] - idx += 1 - if runtype == 0: - runcount, runval = struct.unpack_from("Bc", row, idx) - img.write(runval * runcount) - idx += 2 - else: - runcount = runtype - img.write(row[idx : idx + runcount]) - idx += runcount - - except struct.error as e: - msg = f"Corrupted MSP file in row {x}" - raise OSError(msg) from e - - self.set_as_raw(img.getvalue(), "1") - - return -1, 0 - - -Image.register_decoder("MSP", MspDecoder) - - -# -# write MSP files (uncompressed only) - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode != "1": - msg = f"cannot write mode {im.mode} as MSP" - raise OSError(msg) - - # create MSP header - header = [0] * 16 - - header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1 - header[2], header[3] = im.size - header[4], header[5] = 1, 1 - header[6], header[7] = 1, 1 - header[8], header[9] = im.size - - checksum = 0 - for h in header: - checksum = checksum ^ h - header[12] = checksum # FIXME: is this the right field? - - # header - for h in header: - fp.write(o16(h)) - - # image body - ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")]) - - -# -# registry - -Image.register_open(MspImageFile.format, MspImageFile, _accept) -Image.register_save(MspImageFile.format, _save) - -Image.register_extension(MspImageFile.format, ".msp") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PSDraw.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PSDraw.py deleted file mode 100644 index e6b74a9..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PSDraw.py +++ /dev/null @@ -1,238 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# Simple PostScript graphics interface -# -# History: -# 1996-04-20 fl Created -# 1999-01-10 fl Added gsave/grestore to image method -# 2005-05-04 fl Fixed floating point issue in image (from Eric Etheridge) -# -# Copyright (c) 1997-2005 by Secret Labs AB. All rights reserved. -# Copyright (c) 1996 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import sys -from typing import IO - -from . import EpsImagePlugin - -TYPE_CHECKING = False - - -## -# Simple PostScript graphics interface. - - -class PSDraw: - """ - Sets up printing to the given file. If ``fp`` is omitted, - ``sys.stdout.buffer`` is assumed. - """ - - def __init__(self, fp: IO[bytes] | None = None) -> None: - if not fp: - fp = sys.stdout.buffer - self.fp = fp - - def begin_document(self, id: str | None = None) -> None: - """Set up printing of a document. (Write PostScript DSC header.)""" - # FIXME: incomplete - self.fp.write( - b"%!PS-Adobe-3.0\n" - b"save\n" - b"/showpage { } def\n" - b"%%EndComments\n" - b"%%BeginDocument\n" - ) - # self.fp.write(ERROR_PS) # debugging! - self.fp.write(EDROFF_PS) - self.fp.write(VDI_PS) - self.fp.write(b"%%EndProlog\n") - self.isofont: dict[bytes, int] = {} - - def end_document(self) -> None: - """Ends printing. (Write PostScript DSC footer.)""" - self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") - if hasattr(self.fp, "flush"): - self.fp.flush() - - def setfont(self, font: str, size: int) -> None: - """ - Selects which font to use. - - :param font: A PostScript font name - :param size: Size in points. - """ - font_bytes = bytes(font, "UTF-8") - if font_bytes not in self.isofont: - # reencode font - self.fp.write( - b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes) - ) - self.isofont[font_bytes] = 1 - # rough - self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes)) - - def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None: - """ - Draws a line between the two points. Coordinates are given in - PostScript point coordinates (72 points per inch, (0, 0) is the lower - left corner of the page). - """ - self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) - - def rectangle(self, box: tuple[int, int, int, int]) -> None: - """ - Draws a rectangle. - - :param box: A tuple of four integers, specifying left, bottom, width and - height. - """ - self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) - - def text(self, xy: tuple[int, int], text: str) -> None: - """ - Draws text at the given position. You must use - :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. - """ - # The font is loaded as ISOLatin1Encoding, so use latin-1 here. - text_bytes = bytes(text, "latin-1") - text_bytes = b"\\(".join(text_bytes.split(b"(")) - text_bytes = b"\\)".join(text_bytes.split(b")")) - self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) - - if TYPE_CHECKING: - from . import Image - - def image( - self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None - ) -> None: - """Draw a PIL image, centered in the given box.""" - # default resolution depends on mode - if not dpi: - if im.mode == "1": - dpi = 200 # fax - else: - dpi = 100 # grayscale - # image size (on paper) - x = im.size[0] * 72 / dpi - y = im.size[1] * 72 / dpi - # max allowed size - xmax = float(box[2] - box[0]) - ymax = float(box[3] - box[1]) - if x > xmax: - y = y * xmax / x - x = xmax - if y > ymax: - x = x * ymax / y - y = ymax - dx = (xmax - x) / 2 + box[0] - dy = (ymax - y) / 2 + box[1] - self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy)) - if (x, y) != im.size: - # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) - sx = x / im.size[0] - sy = y / im.size[1] - self.fp.write(b"%f %f scale\n" % (sx, sy)) - EpsImagePlugin._save(im, self.fp, "", 0) - self.fp.write(b"\ngrestore\n") - - -# -------------------------------------------------------------------- -# PostScript driver - -# -# EDROFF.PS -- PostScript driver for Edroff 2 -# -# History: -# 94-01-25 fl: created (edroff 2.04) -# -# Copyright (c) Fredrik Lundh 1994. -# - - -EDROFF_PS = b"""\ -/S { show } bind def -/P { moveto show } bind def -/M { moveto } bind def -/X { 0 rmoveto } bind def -/Y { 0 exch rmoveto } bind def -/E { findfont - dup maxlength dict begin - { - 1 index /FID ne { def } { pop pop } ifelse - } forall - /Encoding exch def - dup /FontName exch def - currentdict end definefont pop -} bind def -/F { findfont exch scalefont dup setfont - [ exch /setfont cvx ] cvx bind def -} bind def -""" - -# -# VDI.PS -- PostScript driver for VDI meta commands -# -# History: -# 94-01-25 fl: created (edroff 2.04) -# -# Copyright (c) Fredrik Lundh 1994. -# - -VDI_PS = b"""\ -/Vm { moveto } bind def -/Va { newpath arcn stroke } bind def -/Vl { moveto lineto stroke } bind def -/Vc { newpath 0 360 arc closepath } bind def -/Vr { exch dup 0 rlineto - exch dup 0 exch rlineto - exch neg 0 rlineto - 0 exch neg rlineto - setgray fill } bind def -/Tm matrix def -/Ve { Tm currentmatrix pop - translate scale newpath 0 0 .5 0 360 arc closepath - Tm setmatrix -} bind def -/Vf { currentgray exch setgray fill setgray } bind def -""" - -# -# ERROR.PS -- Error handler -# -# History: -# 89-11-21 fl: created (pslist 1.10) -# - -ERROR_PS = b"""\ -/landscape false def -/errorBUF 200 string def -/errorNL { currentpoint 10 sub exch pop 72 exch moveto } def -errordict begin /handleerror { - initmatrix /Courier findfont 10 scalefont setfont - newpath 72 720 moveto $error begin /newerror false def - (PostScript Error) show errorNL errorNL - (Error: ) show - /errorname load errorBUF cvs show errorNL errorNL - (Command: ) show - /command load dup type /stringtype ne { errorBUF cvs } if show - errorNL errorNL - (VMstatus: ) show - vmstatus errorBUF cvs show ( bytes available, ) show - errorBUF cvs show ( bytes used at level ) show - errorBUF cvs show errorNL errorNL - (Operand stargck: ) show errorNL /ostargck load { - dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL - } forall errorNL - (Execution stargck: ) show errorNL /estargck load { - dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL - } forall - end showpage -} def end -""" diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PaletteFile.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PaletteFile.py deleted file mode 100644 index 2a26e5d..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PaletteFile.py +++ /dev/null @@ -1,54 +0,0 @@ -# -# Python Imaging Library -# $Id$ -# -# stuff to read simple, teragon-style palette files -# -# History: -# 97-08-23 fl Created -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from typing import IO - -from ._binary import o8 - - -class PaletteFile: - """File handler for Teragon-style palette files.""" - - rawmode = "RGB" - - def __init__(self, fp: IO[bytes]) -> None: - palette = [o8(i) * 3 for i in range(256)] - - while True: - s = fp.readline() - - if not s: - break - if s.startswith(b"#"): - continue - if len(s) > 100: - msg = "bad palette file" - raise SyntaxError(msg) - - v = [int(x) for x in s.split()] - try: - [i, r, g, b] = v - except ValueError: - [i, r] = v - g = b = r - - if 0 <= i <= 255: - palette[i] = o8(r) + o8(g) + o8(b) - - self.palette = b"".join(palette) - - def getpalette(self) -> tuple[bytes, str]: - return self.palette, self.rawmode diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PalmImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PalmImagePlugin.py deleted file mode 100644 index 232adf3..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PalmImagePlugin.py +++ /dev/null @@ -1,217 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# - -## -# Image plugin for Palm pixmap images (output only). -## -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile -from ._binary import o8 -from ._binary import o16be as o16b - -# fmt: off -_Palm8BitColormapValues = ( - (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), - (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), - (255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204), - (255, 255, 153), (255, 204, 153), (255, 153, 153), (255, 102, 153), - (255, 51, 153), (255, 0, 153), (204, 255, 255), (204, 204, 255), - (204, 153, 255), (204, 102, 255), (204, 51, 255), (204, 0, 255), - (204, 255, 204), (204, 204, 204), (204, 153, 204), (204, 102, 204), - (204, 51, 204), (204, 0, 204), (204, 255, 153), (204, 204, 153), - (204, 153, 153), (204, 102, 153), (204, 51, 153), (204, 0, 153), - (153, 255, 255), (153, 204, 255), (153, 153, 255), (153, 102, 255), - (153, 51, 255), (153, 0, 255), (153, 255, 204), (153, 204, 204), - (153, 153, 204), (153, 102, 204), (153, 51, 204), (153, 0, 204), - (153, 255, 153), (153, 204, 153), (153, 153, 153), (153, 102, 153), - (153, 51, 153), (153, 0, 153), (102, 255, 255), (102, 204, 255), - (102, 153, 255), (102, 102, 255), (102, 51, 255), (102, 0, 255), - (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204), - (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153), - (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153), - (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), - (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), - (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), - (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), - (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), - (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), - (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), - (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), - (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), - (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102), - (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51), - (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51), - (255, 255, 0), (255, 204, 0), (255, 153, 0), (255, 102, 0), - (255, 51, 0), (255, 0, 0), (204, 255, 102), (204, 204, 102), - (204, 153, 102), (204, 102, 102), (204, 51, 102), (204, 0, 102), - (204, 255, 51), (204, 204, 51), (204, 153, 51), (204, 102, 51), - (204, 51, 51), (204, 0, 51), (204, 255, 0), (204, 204, 0), - (204, 153, 0), (204, 102, 0), (204, 51, 0), (204, 0, 0), - (153, 255, 102), (153, 204, 102), (153, 153, 102), (153, 102, 102), - (153, 51, 102), (153, 0, 102), (153, 255, 51), (153, 204, 51), - (153, 153, 51), (153, 102, 51), (153, 51, 51), (153, 0, 51), - (153, 255, 0), (153, 204, 0), (153, 153, 0), (153, 102, 0), - (153, 51, 0), (153, 0, 0), (102, 255, 102), (102, 204, 102), - (102, 153, 102), (102, 102, 102), (102, 51, 102), (102, 0, 102), - (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51), - (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0), - (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0), - (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), - (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), - (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), - (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), - (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), - (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), - (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), - (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), - (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), - (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), - (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221), - (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128), - (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) -# fmt: on - - -# so build a prototype image to be used for palette resampling -def build_prototype_image() -> Image.Image: - image = Image.new("L", (1, len(_Palm8BitColormapValues))) - image.putdata(list(range(len(_Palm8BitColormapValues)))) - palettedata: tuple[int, ...] = () - for colormapValue in _Palm8BitColormapValues: - palettedata += colormapValue - palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) - image.putpalette(palettedata) - return image - - -Palm8BitColormapImage = build_prototype_image() - -# OK, we now have in Palm8BitColormapImage, -# a "P"-mode image with the right palette -# -# -------------------------------------------------------------------- - -_FLAGS = {"custom-colormap": 0x4000, "is-compressed": 0x8000, "has-transparent": 0x2000} - -_COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} - - -# -# -------------------------------------------------------------------- - -## -# (Internal) Image save plugin for the Palm format. - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode == "P": - rawmode = "P" - bpp = 8 - version = 1 - - elif im.mode == "L": - if im.encoderinfo.get("bpp") in (1, 2, 4): - # this is 8-bit grayscale, so we shift it to get the high-order bits, - # and invert it because - # Palm does grayscale from white (0) to black (1) - bpp = im.encoderinfo["bpp"] - maxval = (1 << bpp) - 1 - shift = 8 - bpp - im = im.point(lambda x: maxval - (x >> shift)) - elif im.info.get("bpp") in (1, 2, 4): - # here we assume that even though the inherent mode is 8-bit grayscale, - # only the lower bpp bits are significant. - # We invert them to match the Palm. - bpp = im.info["bpp"] - maxval = (1 << bpp) - 1 - im = im.point(lambda x: maxval - (x & maxval)) - else: - msg = f"cannot write mode {im.mode} as Palm" - raise OSError(msg) - - # we ignore the palette here - im._mode = "P" - rawmode = f"P;{bpp}" - version = 1 - - elif im.mode == "1": - # monochrome -- write it inverted, as is the Palm standard - rawmode = "1;I" - bpp = 1 - version = 0 - - else: - msg = f"cannot write mode {im.mode} as Palm" - raise OSError(msg) - - # - # make sure image data is available - im.load() - - # write header - - cols = im.size[0] - rows = im.size[1] - - rowbytes = int((cols + (16 // bpp - 1)) / (16 // bpp)) * 2 - transparent_index = 0 - compression_type = _COMPRESSION_TYPES["none"] - - flags = 0 - if im.mode == "P": - flags |= _FLAGS["custom-colormap"] - colormap = im.im.getpalette() - colors = len(colormap) // 3 - colormapsize = 4 * colors + 2 - else: - colormapsize = 0 - - if "offset" in im.info: - offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4 - else: - offset = 0 - - fp.write(o16b(cols) + o16b(rows) + o16b(rowbytes) + o16b(flags)) - fp.write(o8(bpp)) - fp.write(o8(version)) - fp.write(o16b(offset)) - fp.write(o8(transparent_index)) - fp.write(o8(compression_type)) - fp.write(o16b(0)) # reserved by Palm - - # now write colormap if necessary - - if colormapsize: - fp.write(o16b(colors)) - for i in range(colors): - fp.write(o8(i)) - fp.write(colormap[3 * i : 3 * i + 3]) - - # now convert data to raw form - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))] - ) - - if hasattr(fp, "flush"): - fp.flush() - - -# -# -------------------------------------------------------------------- - -Image.register_save("PALM", _save) - -Image.register_extension("PALM", ".palm") - -Image.register_mime("PALM", "image/palm") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PcdImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PcdImagePlugin.py deleted file mode 100644 index 296f377..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PcdImagePlugin.py +++ /dev/null @@ -1,68 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PCD file handling -# -# History: -# 96-05-10 fl Created -# 96-05-27 fl Added draft mode (128x192, 256x384) -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile - -## -# Image plugin for PhotoCD images. This plugin only reads the 768x512 -# image from the file; higher resolutions are encoded in a proprietary -# encoding. - - -class PcdImageFile(ImageFile.ImageFile): - format = "PCD" - format_description = "Kodak PhotoCD" - - def _open(self) -> None: - # rough - assert self.fp is not None - - self.fp.seek(2048) - s = self.fp.read(1539) - - if not s.startswith(b"PCD_"): - msg = "not a PCD file" - raise SyntaxError(msg) - - orientation = s[1538] & 3 - self.tile_post_rotate = None - if orientation == 1: - self.tile_post_rotate = 90 - elif orientation == 3: - self.tile_post_rotate = 270 - - self._mode = "RGB" - self._size = (512, 768) if orientation in (1, 3) else (768, 512) - self.tile = [ImageFile._Tile("pcd", (0, 0, 768, 512), 96 * 2048)] - - def load_prepare(self) -> None: - if self._im is None and self.tile_post_rotate: - self.im = Image.core.new(self.mode, (768, 512)) - ImageFile.ImageFile.load_prepare(self) - - def load_end(self) -> None: - if self.tile_post_rotate: - # Handle rotated PCDs - self.im = self.rotate(self.tile_post_rotate, expand=True).im - - -# -# registry - -Image.register_open(PcdImageFile.format, PcdImageFile) - -Image.register_extension(PcdImageFile.format, ".pcd") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PcfFontFile.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PcfFontFile.py deleted file mode 100644 index b923293..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PcfFontFile.py +++ /dev/null @@ -1,258 +0,0 @@ -# -# THIS IS WORK IN PROGRESS -# -# The Python Imaging Library -# $Id$ -# -# portable compiled font file parser -# -# history: -# 1997-08-19 fl created -# 2003-09-13 fl fixed loading of unicode fonts -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1997-2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io - -from . import FontFile, Image -from ._binary import i8 -from ._binary import i16be as b16 -from ._binary import i16le as l16 -from ._binary import i32be as b32 -from ._binary import i32le as l32 - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable - from typing import BinaryIO - -# -------------------------------------------------------------------- -# declarations - -PCF_MAGIC = 0x70636601 # "\x01fcp" - -PCF_PROPERTIES = 1 << 0 -PCF_ACCELERATORS = 1 << 1 -PCF_METRICS = 1 << 2 -PCF_BITMAPS = 1 << 3 -PCF_INK_METRICS = 1 << 4 -PCF_BDF_ENCODINGS = 1 << 5 -PCF_SWIDTHS = 1 << 6 -PCF_GLYPH_NAMES = 1 << 7 -PCF_BDF_ACCELERATORS = 1 << 8 - -BYTES_PER_ROW: list[Callable[[int], int]] = [ - lambda bits: ((bits + 7) >> 3), - lambda bits: ((bits + 15) >> 3) & ~1, - lambda bits: ((bits + 31) >> 3) & ~3, - lambda bits: ((bits + 63) >> 3) & ~7, -] - - -def sz(s: bytes, o: int) -> bytes: - return s[o : s.index(b"\0", o)] - - -class PcfFontFile(FontFile.FontFile): - """Font file plugin for the X11 PCF format.""" - - name = "name" - - def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"): - self.charset_encoding = charset_encoding - - magic = l32(fp.read(4)) - if magic != PCF_MAGIC: - msg = "not a PCF file" - raise SyntaxError(msg) - - super().__init__() - - count = l32(fp.read(4)) - self.toc = {} - for i in range(count): - type = l32(fp.read(4)) - self.toc[type] = l32(fp.read(4)), l32(fp.read(4)), l32(fp.read(4)) - - self.fp = fp - - self.info = self._load_properties() - - metrics = self._load_metrics() - bitmaps = self._load_bitmaps(metrics) - encoding = self._load_encoding() - - # - # create glyph structure - - for ch, ix in enumerate(encoding): - if ix is not None: - ( - xsize, - ysize, - left, - right, - width, - ascent, - descent, - attributes, - ) = metrics[ix] - self.glyph[ch] = ( - (width, 0), - (left, descent - ysize, xsize + left, descent), - (0, 0, xsize, ysize), - bitmaps[ix], - ) - - def _getformat( - self, tag: int - ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]: - format, size, offset = self.toc[tag] - - fp = self.fp - fp.seek(offset) - - format = l32(fp.read(4)) - - if format & 4: - i16, i32 = b16, b32 - else: - i16, i32 = l16, l32 - - return fp, format, i16, i32 - - def _load_properties(self) -> dict[bytes, bytes | int]: - # - # font properties - - properties = {} - - fp, format, i16, i32 = self._getformat(PCF_PROPERTIES) - - nprops = i32(fp.read(4)) - - # read property description - p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)] - - if nprops & 3: - fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad - - data = fp.read(i32(fp.read(4))) - - for k, s, v in p: - property_value: bytes | int = sz(data, v) if s else v - properties[sz(data, k)] = property_value - - return properties - - def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]: - # - # font metrics - - metrics: list[tuple[int, int, int, int, int, int, int, int]] = [] - - fp, format, i16, i32 = self._getformat(PCF_METRICS) - - append = metrics.append - - if (format & 0xFF00) == 0x100: - # "compressed" metrics - for i in range(i16(fp.read(2))): - left = i8(fp.read(1)) - 128 - right = i8(fp.read(1)) - 128 - width = i8(fp.read(1)) - 128 - ascent = i8(fp.read(1)) - 128 - descent = i8(fp.read(1)) - 128 - xsize = right - left - ysize = ascent + descent - append((xsize, ysize, left, right, width, ascent, descent, 0)) - - else: - # "jumbo" metrics - for i in range(i32(fp.read(4))): - left = i16(fp.read(2)) - right = i16(fp.read(2)) - width = i16(fp.read(2)) - ascent = i16(fp.read(2)) - descent = i16(fp.read(2)) - attributes = i16(fp.read(2)) - xsize = right - left - ysize = ascent + descent - append((xsize, ysize, left, right, width, ascent, descent, attributes)) - - return metrics - - def _load_bitmaps( - self, metrics: list[tuple[int, int, int, int, int, int, int, int]] - ) -> list[Image.Image]: - # - # bitmap data - - fp, format, i16, i32 = self._getformat(PCF_BITMAPS) - - nbitmaps = i32(fp.read(4)) - - if nbitmaps != len(metrics): - msg = "Wrong number of bitmaps" - raise OSError(msg) - - offsets = [i32(fp.read(4)) for _ in range(nbitmaps)] - - bitmap_sizes = [i32(fp.read(4)) for _ in range(4)] - - # byteorder = format & 4 # non-zero => MSB - bitorder = format & 8 # non-zero => MSB - padindex = format & 3 - - bitmapsize = bitmap_sizes[padindex] - offsets.append(bitmapsize) - - data = fp.read(bitmapsize) - - pad = BYTES_PER_ROW[padindex] - mode = "1;R" - if bitorder: - mode = "1" - - bitmaps = [] - for i in range(nbitmaps): - xsize, ysize = metrics[i][:2] - b, e = offsets[i : i + 2] - bitmaps.append( - Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) - ) - - return bitmaps - - def _load_encoding(self) -> list[int | None]: - fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) - - first_col, last_col = i16(fp.read(2)), i16(fp.read(2)) - first_row, last_row = i16(fp.read(2)), i16(fp.read(2)) - - i16(fp.read(2)) # default - - nencoding = (last_col - first_col + 1) * (last_row - first_row + 1) - - # map character code to bitmap index - encoding: list[int | None] = [None] * min(256, nencoding) - - encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)] - - for i in range(first_col, len(encoding)): - try: - encoding_offset = encoding_offsets[ - ord(bytearray([i]).decode(self.charset_encoding)) - ] - if encoding_offset != 0xFFFF: - encoding[i] = encoding_offset - except UnicodeDecodeError: # noqa: PERF203 - # character is not supported in selected encoding - pass - - return encoding diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PcxImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PcxImagePlugin.py deleted file mode 100644 index 3e34e3c..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PcxImagePlugin.py +++ /dev/null @@ -1,232 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PCX file handling -# -# This format was originally used by ZSoft's popular PaintBrush -# program for the IBM PC. It is also supported by many MS-DOS and -# Windows applications, including the Windows PaintBrush program in -# Windows 3. -# -# history: -# 1995-09-01 fl Created -# 1996-05-20 fl Fixed RGB support -# 1997-01-03 fl Fixed 2-bit and 4-bit support -# 1999-02-03 fl Fixed 8-bit support (broken in 1.0b1) -# 1999-02-07 fl Added write support -# 2002-06-09 fl Made 2-bit and 4-bit support a bit more robust -# 2002-07-30 fl Seek from to current position, not beginning of file -# 2003-06-03 fl Extract DPI settings (info["dpi"]) -# -# Copyright (c) 1997-2003 by Secret Labs AB. -# Copyright (c) 1995-2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import logging -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import o8 -from ._binary import o16le as o16 - -logger = logging.getLogger(__name__) - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 2 and prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] - - -## -# Image plugin for Paintbrush images. - - -class PcxImageFile(ImageFile.ImageFile): - format = "PCX" - format_description = "Paintbrush" - - def _open(self) -> None: - # header - assert self.fp is not None - - s = self.fp.read(68) - if not _accept(s): - msg = "not a PCX file" - raise SyntaxError(msg) - - # image - bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1 - if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: - msg = "bad PCX image size" - raise SyntaxError(msg) - logger.debug("BBox: %s %s %s %s", *bbox) - - offset = self.fp.tell() + 60 - - # format - version = s[1] - bits = s[3] - planes = s[65] - provided_stride = i16(s, 66) - logger.debug( - "PCX version %s, bits %s, planes %s, stride %s", - version, - bits, - planes, - provided_stride, - ) - - self.info["dpi"] = i16(s, 12), i16(s, 14) - - if bits == 1 and planes == 1: - mode = rawmode = "1" - - elif bits == 1 and planes in (2, 4): - mode = "P" - rawmode = f"P;{planes}L" - self.palette = ImagePalette.raw("RGB", s[16:64]) - - elif version == 5 and bits == 8 and planes == 1: - mode = rawmode = "L" - # FIXME: hey, this doesn't work with the incremental loader !!! - self.fp.seek(-769, io.SEEK_END) - s = self.fp.read(769) - if len(s) == 769 and s[0] == 12: - # check if the palette is linear grayscale - for i in range(256): - if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3: - mode = rawmode = "P" - break - if mode == "P": - self.palette = ImagePalette.raw("RGB", s[1:]) - - elif version == 5 and bits == 8 and planes == 3: - mode = "RGB" - rawmode = "RGB;L" - - else: - msg = "unknown PCX mode" - raise OSError(msg) - - self._mode = mode - self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] - - # Don't trust the passed in stride. - # Calculate the approximate position for ourselves. - # CVE-2020-35653 - stride = (self._size[0] * bits + 7) // 8 - - # While the specification states that this must be even, - # not all images follow this - if provided_stride != stride: - stride += stride % 2 - - bbox = (0, 0) + self.size - logger.debug("size: %sx%s", *self.size) - - self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))] - - -# -------------------------------------------------------------------- -# save PCX files - - -SAVE = { - # mode: (version, bits, planes, raw mode) - "1": (2, 1, 1, "1"), - "L": (5, 8, 1, "L"), - "P": (5, 8, 1, "P"), - "RGB": (5, 8, 3, "RGB;L"), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.width == 0 or im.height == 0: - msg = "Cannot write empty image as PCX" - raise ValueError(msg) - - try: - version, bits, planes, rawmode = SAVE[im.mode] - except KeyError as e: - msg = f"Cannot save {im.mode} images as PCX" - raise ValueError(msg) from e - - # bytes per plane - stride = (im.size[0] * bits + 7) // 8 - # stride should be even - stride += stride % 2 - # Stride needs to be kept in sync with the PcxEncode.c version. - # Ideally it should be passed in in the state, but the bytes value - # gets overwritten. - - logger.debug( - "PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", - im.size[0], - bits, - stride, - ) - - # under windows, we could determine the current screen size with - # "Image.core.display_mode()[1]", but I think that's overkill... - - screen = im.size - - dpi = 100, 100 - - # PCX header - fp.write( - o8(10) - + o8(version) - + o8(1) - + o8(bits) - + o16(0) - + o16(0) - + o16(im.size[0] - 1) - + o16(im.size[1] - 1) - + o16(dpi[0]) - + o16(dpi[1]) - + b"\0" * 24 - + b"\xff" * 24 - + b"\0" - + o8(planes) - + o16(stride) - + o16(1) - + o16(screen[0]) - + o16(screen[1]) - + b"\0" * 54 - ) - - assert fp.tell() == 128 - - ImageFile._save( - im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))] - ) - - if im.mode == "P": - # colour palette - fp.write(o8(12)) - palette = im.im.getpalette("RGB", "RGB") - palette += b"\x00" * (768 - len(palette)) - fp.write(palette) # 768 bytes - elif im.mode == "L": - # grayscale palette - fp.write(o8(12)) - for i in range(256): - fp.write(o8(i) * 3) - - -# -------------------------------------------------------------------- -# registry - - -Image.register_open(PcxImageFile.format, PcxImageFile, _accept) -Image.register_save(PcxImageFile.format, _save) - -Image.register_extension(PcxImageFile.format, ".pcx") - -Image.register_mime(PcxImageFile.format, "image/x-pcx") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PdfImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PdfImagePlugin.py deleted file mode 100644 index 5594c7e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PdfImagePlugin.py +++ /dev/null @@ -1,311 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PDF (Acrobat) file handling -# -# History: -# 1996-07-16 fl Created -# 1997-01-18 fl Fixed header -# 2004-02-21 fl Fixes for 1/L/CMYK images, etc. -# 2004-02-24 fl Fixes for 1 and P images. -# -# Copyright (c) 1997-2004 by Secret Labs AB. All rights reserved. -# Copyright (c) 1996-1997 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -## -# Image plugin for PDF images (output only). -## -from __future__ import annotations - -import io -import math -import os -import time -from typing import IO, Any - -from . import Image, ImageFile, ImageSequence, PdfParser, features - -# -# -------------------------------------------------------------------- - -# object ids: -# 1. catalogue -# 2. pages -# 3. image -# 4. page -# 5. page contents - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -## -# (Internal) Image save plugin for the PDF format. - - -def _write_image( - im: Image.Image, - filename: str | bytes, - existing_pdf: PdfParser.PdfParser, - image_refs: list[PdfParser.IndirectReference], -) -> tuple[PdfParser.IndirectReference, str]: - # FIXME: Should replace ASCIIHexDecode with RunLengthDecode - # (packbits) or LZWDecode (tiff/lzw compression). Note that - # PDF 1.2 also supports Flatedecode (zip compression). - - params = None - decode = None - - # - # Get image characteristics - - width, height = im.size - - dict_obj: dict[str, Any] = {"BitsPerComponent": 8} - if im.mode == "1": - if features.check("libtiff"): - decode_filter = "CCITTFaxDecode" - dict_obj["BitsPerComponent"] = 1 - params = PdfParser.PdfArray( - [ - PdfParser.PdfDict( - { - "K": -1, - "BlackIs1": True, - "Columns": width, - "Rows": height, - } - ) - ] - ) - else: - decode_filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "L": - decode_filter = "DCTDecode" - # params = f"<< /Predictor 15 /Columns {width-2} >>" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "LA": - decode_filter = "JPXDecode" - # params = f"<< /Predictor 15 /Columns {width-2} >>" - procset = "ImageB" # grayscale - dict_obj["SMaskInData"] = 1 - elif im.mode == "P": - decode_filter = "ASCIIHexDecode" - palette = im.getpalette() - assert palette is not None - dict_obj["ColorSpace"] = [ - PdfParser.PdfName("Indexed"), - PdfParser.PdfName("DeviceRGB"), - len(palette) // 3 - 1, - PdfParser.PdfBinary(palette), - ] - procset = "ImageI" # indexed color - - if "transparency" in im.info: - smask = im.convert("LA").getchannel("A") - smask.encoderinfo = {} - - image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] - dict_obj["SMask"] = image_ref - elif im.mode == "RGB": - decode_filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") - procset = "ImageC" # color images - elif im.mode == "RGBA": - decode_filter = "JPXDecode" - procset = "ImageC" # color images - dict_obj["SMaskInData"] = 1 - elif im.mode == "CMYK": - decode_filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") - procset = "ImageC" # color images - decode = [1, 0, 1, 0, 1, 0, 1, 0] - else: - msg = f"cannot save mode {im.mode}" - raise ValueError(msg) - - # - # image - - op = io.BytesIO() - - if decode_filter == "ASCIIHexDecode": - ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)]) - elif decode_filter == "CCITTFaxDecode": - im.save( - op, - "TIFF", - compression="group4", - # use a single strip - strip_size=math.ceil(width / 8) * height, - ) - elif decode_filter == "DCTDecode": - Image.SAVE["JPEG"](im, op, filename) - elif decode_filter == "JPXDecode": - del dict_obj["BitsPerComponent"] - Image.SAVE["JPEG2000"](im, op, filename) - else: - msg = f"unsupported PDF filter ({decode_filter})" - raise ValueError(msg) - - stream = op.getvalue() - filter: PdfParser.PdfArray | PdfParser.PdfName - if decode_filter == "CCITTFaxDecode": - stream = stream[8:] - filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)]) - else: - filter = PdfParser.PdfName(decode_filter) - - image_ref = image_refs.pop(0) - existing_pdf.write_obj( - image_ref, - stream=stream, - Type=PdfParser.PdfName("XObject"), - Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / x_resolution, - Height=height, # * 72.0 / y_resolution, - Filter=filter, - Decode=decode, - DecodeParms=params, - **dict_obj, - ) - - return image_ref, procset - - -def _save( - im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False -) -> None: - is_appending = im.encoderinfo.get("append", False) - filename_str = filename.decode() if isinstance(filename, bytes) else filename - if is_appending: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b") - else: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b") - - dpi = im.encoderinfo.get("dpi") - if dpi: - x_resolution = dpi[0] - y_resolution = dpi[1] - else: - x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) - - info = { - "title": ( - None if is_appending else os.path.splitext(os.path.basename(filename))[0] - ), - "author": None, - "subject": None, - "keywords": None, - "creator": None, - "producer": None, - "creationDate": None if is_appending else time.gmtime(), - "modDate": None if is_appending else time.gmtime(), - } - for k, default in info.items(): - v = im.encoderinfo.get(k) if k in im.encoderinfo else default - if v: - existing_pdf.info[k[0].upper() + k[1:]] = v - - # - # make sure image data is available - im.load() - - existing_pdf.start_writing() - existing_pdf.write_header() - existing_pdf.write_comment("created by Pillow PDF driver") - - # - # pages - ims = [im] - if save_all: - append_images = im.encoderinfo.get("append_images", []) - for append_im in append_images: - append_im.encoderinfo = im.encoderinfo.copy() - ims.append(append_im) - number_of_pages = 0 - image_refs = [] - page_refs = [] - contents_refs = [] - for im in ims: - im_number_of_pages = 1 - if save_all: - im_number_of_pages = getattr(im, "n_frames", 1) - number_of_pages += im_number_of_pages - for i in range(im_number_of_pages): - image_refs.append(existing_pdf.next_object_id(0)) - if im.mode == "P" and "transparency" in im.info: - image_refs.append(existing_pdf.next_object_id(0)) - - page_refs.append(existing_pdf.next_object_id(0)) - contents_refs.append(existing_pdf.next_object_id(0)) - existing_pdf.pages.append(page_refs[-1]) - - # - # catalog and list of pages - existing_pdf.write_catalog() - - page_number = 0 - for im_sequence in ims: - im_pages: ImageSequence.Iterator | list[Image.Image] = ( - ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] - ) - for im in im_pages: - image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) - - # - # page - - existing_pdf.write_page( - page_refs[page_number], - Resources=PdfParser.PdfDict( - ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], - XObject=PdfParser.PdfDict(image=image_ref), - ), - MediaBox=[ - 0, - 0, - im.width * 72.0 / x_resolution, - im.height * 72.0 / y_resolution, - ], - Contents=contents_refs[page_number], - ) - - # - # page contents - - page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - im.width * 72.0 / x_resolution, - im.height * 72.0 / y_resolution, - ) - - existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) - - page_number += 1 - - # - # trailer - existing_pdf.write_xref_and_trailer() - if hasattr(fp, "flush"): - fp.flush() - existing_pdf.close() - - -# -# -------------------------------------------------------------------- - - -Image.register_save("PDF", _save) -Image.register_save_all("PDF", _save_all) - -Image.register_extension("PDF", ".pdf") - -Image.register_mime("PDF", "application/pdf") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PdfParser.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PdfParser.py deleted file mode 100644 index f7f3a46..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PdfParser.py +++ /dev/null @@ -1,1081 +0,0 @@ -from __future__ import annotations - -import calendar -import codecs -import collections -import mmap -import os -import re -import time -import zlib -from typing import Any, NamedTuple - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import IO - - _DictBase = collections.UserDict[str | bytes, Any] -else: - _DictBase = collections.UserDict - - -# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set -# on page 656 -def encode_text(s: str) -> bytes: - return codecs.BOM_UTF16_BE + s.encode("utf_16_be") - - -PDFDocEncoding = { - 0x16: "\u0017", - 0x18: "\u02d8", - 0x19: "\u02c7", - 0x1A: "\u02c6", - 0x1B: "\u02d9", - 0x1C: "\u02dd", - 0x1D: "\u02db", - 0x1E: "\u02da", - 0x1F: "\u02dc", - 0x80: "\u2022", - 0x81: "\u2020", - 0x82: "\u2021", - 0x83: "\u2026", - 0x84: "\u2014", - 0x85: "\u2013", - 0x86: "\u0192", - 0x87: "\u2044", - 0x88: "\u2039", - 0x89: "\u203a", - 0x8A: "\u2212", - 0x8B: "\u2030", - 0x8C: "\u201e", - 0x8D: "\u201c", - 0x8E: "\u201d", - 0x8F: "\u2018", - 0x90: "\u2019", - 0x91: "\u201a", - 0x92: "\u2122", - 0x93: "\ufb01", - 0x94: "\ufb02", - 0x95: "\u0141", - 0x96: "\u0152", - 0x97: "\u0160", - 0x98: "\u0178", - 0x99: "\u017d", - 0x9A: "\u0131", - 0x9B: "\u0142", - 0x9C: "\u0153", - 0x9D: "\u0161", - 0x9E: "\u017e", - 0xA0: "\u20ac", -} - - -def decode_text(b: bytes) -> str: - if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: - return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") - else: - return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b) - - -class PdfFormatError(RuntimeError): - """An error that probably indicates a syntactic or semantic error in the - PDF file structure""" - - pass - - -def check_format_condition(condition: bool, error_message: str) -> None: - if not condition: - raise PdfFormatError(error_message) - - -class IndirectReferenceTuple(NamedTuple): - object_id: int - generation: int - - -class IndirectReference(IndirectReferenceTuple): - def __str__(self) -> str: - return f"{self.object_id} {self.generation} R" - - def __bytes__(self) -> bytes: - return self.__str__().encode("us-ascii") - - def __eq__(self, other: object) -> bool: - if self.__class__ is not other.__class__: - return False - assert isinstance(other, IndirectReference) - return other.object_id == self.object_id and other.generation == self.generation - - def __ne__(self, other: object) -> bool: - return not (self == other) - - def __hash__(self) -> int: - return hash((self.object_id, self.generation)) - - -class IndirectObjectDef(IndirectReference): - def __str__(self) -> str: - return f"{self.object_id} {self.generation} obj" - - -class XrefTable: - def __init__(self) -> None: - self.existing_entries: dict[int, tuple[int, int]] = ( - {} - ) # object ID => (offset, generation) - self.new_entries: dict[int, tuple[int, int]] = ( - {} - ) # object ID => (offset, generation) - self.deleted_entries = {0: 65536} # object ID => generation - self.reading_finished = False - - def __setitem__(self, key: int, value: tuple[int, int]) -> None: - if self.reading_finished: - self.new_entries[key] = value - else: - self.existing_entries[key] = value - if key in self.deleted_entries: - del self.deleted_entries[key] - - def __getitem__(self, key: int) -> tuple[int, int]: - try: - return self.new_entries[key] - except KeyError: - return self.existing_entries[key] - - def __delitem__(self, key: int) -> None: - if key in self.new_entries: - generation = self.new_entries[key][1] + 1 - del self.new_entries[key] - self.deleted_entries[key] = generation - elif key in self.existing_entries: - generation = self.existing_entries[key][1] + 1 - self.deleted_entries[key] = generation - elif key in self.deleted_entries: - generation = self.deleted_entries[key] - else: - msg = f"object ID {key} cannot be deleted because it doesn't exist" - raise IndexError(msg) - - def __contains__(self, key: int) -> bool: - return key in self.existing_entries or key in self.new_entries - - def __len__(self) -> int: - return len( - set(self.existing_entries.keys()) - | set(self.new_entries.keys()) - | set(self.deleted_entries.keys()) - ) - - def keys(self) -> set[int]: - return ( - set(self.existing_entries.keys()) - set(self.deleted_entries.keys()) - ) | set(self.new_entries.keys()) - - def write(self, f: IO[bytes]) -> int: - keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys())) - deleted_keys = sorted(set(self.deleted_entries.keys())) - startxref = f.tell() - f.write(b"xref\n") - while keys: - # find a contiguous sequence of object IDs - prev: int | None = None - for index, key in enumerate(keys): - if prev is None or prev + 1 == key: - prev = key - else: - contiguous_keys = keys[:index] - keys = keys[index:] - break - else: - contiguous_keys = keys - keys = [] - f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys))) - for object_id in contiguous_keys: - if object_id in self.new_entries: - f.write(b"%010d %05d n \n" % self.new_entries[object_id]) - else: - this_deleted_object_id = deleted_keys.pop(0) - check_format_condition( - object_id == this_deleted_object_id, - f"expected the next deleted object ID to be {object_id}, " - f"instead found {this_deleted_object_id}", - ) - try: - next_in_linked_list = deleted_keys[0] - except IndexError: - next_in_linked_list = 0 - f.write( - b"%010d %05d f \n" - % (next_in_linked_list, self.deleted_entries[object_id]) - ) - return startxref - - -class PdfName: - name: bytes - - def __init__(self, name: PdfName | bytes | str) -> None: - if isinstance(name, PdfName): - self.name = name.name - elif isinstance(name, bytes): - self.name = name - else: - self.name = name.encode("us-ascii") - - def name_as_str(self) -> str: - return self.name.decode("us-ascii") - - def __eq__(self, other: object) -> bool: - return ( - isinstance(other, PdfName) and other.name == self.name - ) or other == self.name - - def __hash__(self) -> int: - return hash(self.name) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({repr(self.name)})" - - @classmethod - def from_pdf_stream(cls, data: bytes) -> PdfName: - return cls(PdfParser.interpret_name(data)) - - allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} - - def __bytes__(self) -> bytes: - result = bytearray(b"/") - for b in self.name: - if b in self.allowed_chars: - result.append(b) - else: - result.extend(b"#%02X" % b) - return bytes(result) - - -class PdfArray(list[Any]): - def __bytes__(self) -> bytes: - return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" - - -class PdfDict(_DictBase): - def __setattr__(self, key: str, value: Any) -> None: - if key == "data": - collections.UserDict.__setattr__(self, key, value) - else: - self[key.encode("us-ascii")] = value - - def __getattr__(self, key: str) -> str | time.struct_time: - try: - value = self[key.encode("us-ascii")] - except KeyError as e: - raise AttributeError(key) from e - if isinstance(value, bytes): - value = decode_text(value) - if key.endswith("Date"): - if value.startswith("D:"): - value = value[2:] - - relationship = "Z" - if len(value) > 17: - relationship = value[14] - offset = int(value[15:17]) * 60 - if len(value) > 20: - offset += int(value[18:20]) - - format = "%Y%m%d%H%M%S"[: len(value) - 2] - value = time.strptime(value[: len(format) + 2], format) - if relationship in ["+", "-"]: - offset *= 60 - if relationship == "+": - offset *= -1 - value = time.gmtime(calendar.timegm(value) + offset) - return value - - def __bytes__(self) -> bytes: - out = bytearray(b"<<") - for key, value in self.items(): - if value is None: - continue - value = pdf_repr(value) - out.extend(b"\n") - out.extend(bytes(PdfName(key))) - out.extend(b" ") - out.extend(value) - out.extend(b"\n>>") - return bytes(out) - - -class PdfBinary: - def __init__(self, data: list[int] | bytes) -> None: - self.data = data - - def __bytes__(self) -> bytes: - return b"<%s>" % b"".join(b"%02X" % b for b in self.data) - - -class PdfStream: - def __init__(self, dictionary: PdfDict, buf: bytes) -> None: - self.dictionary = dictionary - self.buf = buf - - def decode(self) -> bytes: - try: - filter = self.dictionary[b"Filter"] - except KeyError: - return self.buf - if filter == b"FlateDecode": - try: - expected_length = self.dictionary[b"DL"] - except KeyError: - expected_length = self.dictionary[b"Length"] - return zlib.decompress(self.buf, bufsize=int(expected_length)) - else: - msg = f"stream filter {repr(filter)} unknown/unsupported" - raise NotImplementedError(msg) - - -def pdf_repr(x: Any) -> bytes: - if x is True: - return b"true" - elif x is False: - return b"false" - elif x is None: - return b"null" - elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): - return bytes(x) - elif isinstance(x, (int, float)): - return str(x).encode("us-ascii") - elif isinstance(x, time.struct_time): - return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" - elif isinstance(x, dict): - return bytes(PdfDict(x)) - elif isinstance(x, list): - return bytes(PdfArray(x)) - elif isinstance(x, str): - return pdf_repr(encode_text(x)) - elif isinstance(x, bytes): - # XXX escape more chars? handle binary garbage - x = x.replace(b"\\", b"\\\\") - x = x.replace(b"(", b"\\(") - x = x.replace(b")", b"\\)") - return b"(" + x + b")" - else: - return bytes(x) - - -class PdfParser: - """Based on - https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf - Supports PDF up to 1.4 - """ - - def __init__( - self, - filename: str | None = None, - f: IO[bytes] | None = None, - buf: bytes | bytearray | None = None, - start_offset: int = 0, - mode: str = "rb", - ) -> None: - if buf and f: - msg = "specify buf or f or filename, but not both buf and f" - raise RuntimeError(msg) - self.filename = filename - self.buf: bytes | bytearray | mmap.mmap | None = buf - self.f = f - self.start_offset = start_offset - self.should_close_buf = False - self.should_close_file = False - if filename is not None and f is None: - self.f = f = open(filename, mode) - self.should_close_file = True - if f is not None: - self.buf = self.get_buf_from_file(f) - self.should_close_buf = True - if not filename and hasattr(f, "name"): - self.filename = f.name - self.cached_objects: dict[IndirectReference, Any] = {} - self.root_ref: IndirectReference | None - self.info_ref: IndirectReference | None - self.pages_ref: IndirectReference | None - self.last_xref_section_offset: int | None - if self.buf: - self.read_pdf_info() - else: - self.file_size_total = self.file_size_this = 0 - self.root = PdfDict() - self.root_ref = None - self.info = PdfDict() - self.info_ref = None - self.page_tree_root = PdfDict() - self.pages: list[IndirectReference] = [] - self.orig_pages: list[IndirectReference] = [] - self.pages_ref = None - self.last_xref_section_offset = None - self.trailer_dict: dict[bytes, Any] = {} - self.xref_table = XrefTable() - self.xref_table.reading_finished = True - if f: - self.seek_end() - - def __enter__(self) -> PdfParser: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def start_writing(self) -> None: - self.close_buf() - self.seek_end() - - def close_buf(self) -> None: - if isinstance(self.buf, mmap.mmap): - self.buf.close() - self.buf = None - - def close(self) -> None: - if self.should_close_buf: - self.close_buf() - if self.f is not None and self.should_close_file: - self.f.close() - self.f = None - - def seek_end(self) -> None: - assert self.f is not None - self.f.seek(0, os.SEEK_END) - - def write_header(self) -> None: - assert self.f is not None - self.f.write(b"%PDF-1.4\n") - - def write_comment(self, s: str) -> None: - assert self.f is not None - self.f.write(f"% {s}\n".encode()) - - def write_catalog(self) -> IndirectReference: - assert self.f is not None - self.del_root() - self.root_ref = self.next_object_id(self.f.tell()) - self.pages_ref = self.next_object_id(0) - self.rewrite_pages() - self.write_obj(self.root_ref, Type=PdfName(b"Catalog"), Pages=self.pages_ref) - self.write_obj( - self.pages_ref, - Type=PdfName(b"Pages"), - Count=len(self.pages), - Kids=self.pages, - ) - return self.root_ref - - def rewrite_pages(self) -> None: - pages_tree_nodes_to_delete = [] - for i, page_ref in enumerate(self.orig_pages): - page_info = self.cached_objects[page_ref] - del self.xref_table[page_ref.object_id] - pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")]) - if page_ref not in self.pages: - # the page has been deleted - continue - # make dict keys into strings for passing to write_page - stringified_page_info = {} - for key, value in page_info.items(): - # key should be a PdfName - stringified_page_info[key.name_as_str()] = value - stringified_page_info["Parent"] = self.pages_ref - new_page_ref = self.write_page(None, **stringified_page_info) - for j, cur_page_ref in enumerate(self.pages): - if cur_page_ref == page_ref: - # replace the page reference with the new one - self.pages[j] = new_page_ref - # delete redundant Pages tree nodes from xref table - for pages_tree_node_ref in pages_tree_nodes_to_delete: - while pages_tree_node_ref: - pages_tree_node = self.cached_objects[pages_tree_node_ref] - if pages_tree_node_ref.object_id in self.xref_table: - del self.xref_table[pages_tree_node_ref.object_id] - pages_tree_node_ref = pages_tree_node.get(b"Parent", None) - self.orig_pages = [] - - def write_xref_and_trailer( - self, new_root_ref: IndirectReference | None = None - ) -> None: - assert self.f is not None - if new_root_ref: - self.del_root() - self.root_ref = new_root_ref - if self.info: - self.info_ref = self.write_obj(None, self.info) - start_xref = self.xref_table.write(self.f) - num_entries = len(self.xref_table) - trailer_dict: dict[str | bytes, Any] = { - b"Root": self.root_ref, - b"Size": num_entries, - } - if self.last_xref_section_offset is not None: - trailer_dict[b"Prev"] = self.last_xref_section_offset - if self.info: - trailer_dict[b"Info"] = self.info_ref - self.last_xref_section_offset = start_xref - self.f.write( - b"trailer\n" - + bytes(PdfDict(trailer_dict)) - + b"\nstartxref\n%d\n%%%%EOF" % start_xref - ) - - def write_page( - self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any - ) -> IndirectReference: - obj_ref = self.pages[ref] if isinstance(ref, int) else ref - if "Type" not in dict_obj: - dict_obj["Type"] = PdfName(b"Page") - if "Parent" not in dict_obj: - dict_obj["Parent"] = self.pages_ref - return self.write_obj(obj_ref, *objs, **dict_obj) - - def write_obj( - self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any - ) -> IndirectReference: - assert self.f is not None - f = self.f - if ref is None: - ref = self.next_object_id(f.tell()) - else: - self.xref_table[ref.object_id] = (f.tell(), ref.generation) - f.write(bytes(IndirectObjectDef(*ref))) - stream = dict_obj.pop("stream", None) - if stream is not None: - dict_obj["Length"] = len(stream) - if dict_obj: - f.write(pdf_repr(dict_obj)) - for obj in objs: - f.write(pdf_repr(obj)) - if stream is not None: - f.write(b"stream\n") - f.write(stream) - f.write(b"\nendstream\n") - f.write(b"endobj\n") - return ref - - def del_root(self) -> None: - if self.root_ref is None: - return - del self.xref_table[self.root_ref.object_id] - del self.xref_table[self.root[b"Pages"].object_id] - - @staticmethod - def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap: - if hasattr(f, "getbuffer"): - return f.getbuffer() - elif hasattr(f, "getvalue"): - return f.getvalue() - else: - try: - return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) - except ValueError: # cannot mmap an empty file - return b"" - - def read_pdf_info(self) -> None: - assert self.buf is not None - self.file_size_total = len(self.buf) - self.file_size_this = self.file_size_total - self.start_offset - self.read_trailer() - check_format_condition( - self.trailer_dict.get(b"Root") is not None, "Root is missing" - ) - self.root_ref = self.trailer_dict[b"Root"] - assert self.root_ref is not None - self.info_ref = self.trailer_dict.get(b"Info", None) - self.root = PdfDict(self.read_indirect(self.root_ref)) - if self.info_ref is None: - self.info = PdfDict() - else: - self.info = PdfDict(self.read_indirect(self.info_ref)) - check_format_condition(b"Type" in self.root, "/Type missing in Root") - check_format_condition( - self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog" - ) - check_format_condition( - self.root.get(b"Pages") is not None, "/Pages missing in Root" - ) - check_format_condition( - isinstance(self.root[b"Pages"], IndirectReference), - "/Pages in Root is not an indirect reference", - ) - self.pages_ref = self.root[b"Pages"] - assert self.pages_ref is not None - self.page_tree_root = self.read_indirect(self.pages_ref) - self.pages = self.linearize_page_tree(self.page_tree_root) - # save the original list of page references - # in case the user modifies, adds or deletes some pages - # and we need to rewrite the pages and their list - self.orig_pages = self.pages[:] - - def next_object_id(self, offset: int | None = None) -> IndirectReference: - try: - # TODO: support reuse of deleted objects - reference = IndirectReference(max(self.xref_table.keys()) + 1, 0) - except ValueError: - reference = IndirectReference(1, 0) - if offset is not None: - self.xref_table[reference.object_id] = (offset, 0) - return reference - - delimiter = rb"[][()<>{}/%]" - delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]" - whitespace = rb"[\000\011\012\014\015\040]" - whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]" - whitespace_optional = whitespace + b"*" - whitespace_mandatory = whitespace + b"+" - # No "\012" aka "\n" or "\015" aka "\r": - whitespace_optional_no_nl = rb"[\000\011\014\040]*" - newline_only = rb"[\r\n]+" - newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl - re_trailer_end = re.compile( - whitespace_mandatory - + rb"trailer" - + whitespace_optional - + rb"<<(.*>>)" - + newline - + rb"startxref" - + newline - + rb"([0-9]+)" - + newline - + rb"%%EOF" - + whitespace_optional - + rb"$", - re.DOTALL, - ) - re_trailer_prev = re.compile( - whitespace_optional - + rb"trailer" - + whitespace_optional - + rb"<<(.*?>>)" - + newline - + rb"startxref" - + newline - + rb"([0-9]+)" - + newline - + rb"%%EOF" - + whitespace_optional, - re.DOTALL, - ) - - def read_trailer(self) -> None: - assert self.buf is not None - search_start_offset = len(self.buf) - 16384 - if search_start_offset < self.start_offset: - search_start_offset = self.start_offset - m = self.re_trailer_end.search(self.buf, search_start_offset) - check_format_condition(m is not None, "trailer end not found") - # make sure we found the LAST trailer - last_match = m - while m: - last_match = m - m = self.re_trailer_end.search(self.buf, m.start() + 16) - if not m: - m = last_match - assert m is not None - trailer_data = m.group(1) - self.last_xref_section_offset = int(m.group(2)) - self.trailer_dict = self.interpret_trailer(trailer_data) - self.xref_table = XrefTable() - self.read_xref_table(xref_section_offset=self.last_xref_section_offset) - if b"Prev" in self.trailer_dict: - self.read_prev_trailer(self.trailer_dict[b"Prev"]) - - def read_prev_trailer( - self, xref_section_offset: int, processed_offsets: list[int] = [] - ) -> None: - assert self.buf is not None - trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) - m = self.re_trailer_prev.search( - self.buf[trailer_offset : trailer_offset + 16384] - ) - check_format_condition(m is not None, "previous trailer not found") - assert m is not None - trailer_data = m.group(1) - check_format_condition( - int(m.group(2)) == xref_section_offset, - "xref section offset in previous trailer doesn't match what was expected", - ) - trailer_dict = self.interpret_trailer(trailer_data) - if b"Prev" in trailer_dict: - processed_offsets.append(xref_section_offset) - check_format_condition( - trailer_dict[b"Prev"] not in processed_offsets, "trailer loop found" - ) - self.read_prev_trailer(trailer_dict[b"Prev"], processed_offsets) - - re_whitespace_optional = re.compile(whitespace_optional) - re_name = re.compile( - whitespace_optional - + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" - + delimiter_or_ws - + rb")" - ) - re_dict_start = re.compile(whitespace_optional + rb"<<") - re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional) - - @classmethod - def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]: - trailer = {} - offset = 0 - while True: - m = cls.re_name.match(trailer_data, offset) - if not m: - m = cls.re_dict_end.match(trailer_data, offset) - check_format_condition( - m is not None and m.end() == len(trailer_data), - "name not found in trailer, remaining data: " - + repr(trailer_data[offset:]), - ) - break - key = cls.interpret_name(m.group(1)) - assert isinstance(key, bytes) - value, value_offset = cls.get_value(trailer_data, m.end()) - trailer[key] = value - if value_offset is None: - break - offset = value_offset - check_format_condition( - b"Size" in trailer and isinstance(trailer[b"Size"], int), - "/Size not in trailer or not an integer", - ) - check_format_condition( - b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference), - "/Root not in trailer or not an indirect reference", - ) - return trailer - - re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") - - @classmethod - def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes: - name = b"" - for m in cls.re_hashes_in_name.finditer(raw): - if m.group(3): - name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii")) - else: - name += m.group(1) - if as_text: - return name.decode("utf-8") - else: - return bytes(name) - - re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")") - re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")") - re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")") - re_int = re.compile( - whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")" - ) - re_real = re.compile( - whitespace_optional - + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" - + delimiter_or_ws - + rb")" - ) - re_array_start = re.compile(whitespace_optional + rb"\[") - re_array_end = re.compile(whitespace_optional + rb"]") - re_string_hex = re.compile( - whitespace_optional + rb"<(" + whitespace_or_hex + rb"*)>" - ) - re_string_lit = re.compile(whitespace_optional + rb"\(") - re_indirect_reference = re.compile( - whitespace_optional - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"R(?=" - + delimiter_or_ws - + rb")" - ) - re_indirect_def_start = re.compile( - whitespace_optional - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"([-+]?[0-9]+)" - + whitespace_mandatory - + rb"obj(?=" - + delimiter_or_ws - + rb")" - ) - re_indirect_def_end = re.compile( - whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")" - ) - re_comment = re.compile( - rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*" - ) - re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n") - re_stream_end = re.compile( - whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")" - ) - - @classmethod - def get_value( - cls, - data: bytes | bytearray | mmap.mmap, - offset: int, - expect_indirect: IndirectReference | None = None, - max_nesting: int = -1, - ) -> tuple[Any, int | None]: - if max_nesting == 0: - return None, None - m = cls.re_comment.match(data, offset) - if m: - offset = m.end() - m = cls.re_indirect_def_start.match(data, offset) - if m: - check_format_condition( - int(m.group(1)) > 0, - "indirect object definition: object ID must be greater than 0", - ) - check_format_condition( - int(m.group(2)) >= 0, - "indirect object definition: generation must be non-negative", - ) - check_format_condition( - expect_indirect is None - or expect_indirect - == IndirectReference(int(m.group(1)), int(m.group(2))), - "indirect object definition different than expected", - ) - object, object_offset = cls.get_value( - data, m.end(), max_nesting=max_nesting - 1 - ) - if object_offset is None: - return object, None - m = cls.re_indirect_def_end.match(data, object_offset) - check_format_condition( - m is not None, "indirect object definition end not found" - ) - assert m is not None - return object, m.end() - check_format_condition( - not expect_indirect, "indirect object definition not found" - ) - m = cls.re_indirect_reference.match(data, offset) - if m: - check_format_condition( - int(m.group(1)) > 0, - "indirect object reference: object ID must be greater than 0", - ) - check_format_condition( - int(m.group(2)) >= 0, - "indirect object reference: generation must be non-negative", - ) - return IndirectReference(int(m.group(1)), int(m.group(2))), m.end() - m = cls.re_dict_start.match(data, offset) - if m: - offset = m.end() - result: dict[Any, Any] = {} - m = cls.re_dict_end.match(data, offset) - current_offset: int | None = offset - while not m: - assert current_offset is not None - key, current_offset = cls.get_value( - data, current_offset, max_nesting=max_nesting - 1 - ) - if current_offset is None: - return result, None - value, current_offset = cls.get_value( - data, current_offset, max_nesting=max_nesting - 1 - ) - result[key] = value - if current_offset is None: - return result, None - m = cls.re_dict_end.match(data, current_offset) - current_offset = m.end() - m = cls.re_stream_start.match(data, current_offset) - if m: - stream_len = result.get(b"Length") - if stream_len is None or not isinstance(stream_len, int): - msg = f"bad or missing Length in stream dict ({stream_len})" - raise PdfFormatError(msg) - stream_data = data[m.end() : m.end() + stream_len] - m = cls.re_stream_end.match(data, m.end() + stream_len) - check_format_condition(m is not None, "stream end not found") - assert m is not None - current_offset = m.end() - return PdfStream(PdfDict(result), stream_data), current_offset - return PdfDict(result), current_offset - m = cls.re_array_start.match(data, offset) - if m: - offset = m.end() - results = [] - m = cls.re_array_end.match(data, offset) - current_offset = offset - while not m: - assert current_offset is not None - value, current_offset = cls.get_value( - data, current_offset, max_nesting=max_nesting - 1 - ) - results.append(value) - if current_offset is None: - return results, None - m = cls.re_array_end.match(data, current_offset) - return results, m.end() - m = cls.re_null.match(data, offset) - if m: - return None, m.end() - m = cls.re_true.match(data, offset) - if m: - return True, m.end() - m = cls.re_false.match(data, offset) - if m: - return False, m.end() - m = cls.re_name.match(data, offset) - if m: - return PdfName(cls.interpret_name(m.group(1))), m.end() - m = cls.re_int.match(data, offset) - if m: - return int(m.group(1)), m.end() - m = cls.re_real.match(data, offset) - if m: - # XXX Decimal instead of float??? - return float(m.group(1)), m.end() - m = cls.re_string_hex.match(data, offset) - if m: - # filter out whitespace - hex_string = bytearray( - b for b in m.group(1) if b in b"0123456789abcdefABCDEF" - ) - if len(hex_string) % 2 == 1: - # append a 0 if the length is not even - yes, at the end - hex_string.append(ord(b"0")) - return bytearray.fromhex(hex_string.decode("us-ascii")), m.end() - m = cls.re_string_lit.match(data, offset) - if m: - return cls.get_literal_string(data, m.end()) - # return None, offset # fallback (only for debugging) - msg = f"unrecognized object: {repr(data[offset : offset + 32])}" - raise PdfFormatError(msg) - - re_lit_str_token = re.compile( - rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" - ) - escaped_chars = { - b"n": b"\n", - b"r": b"\r", - b"t": b"\t", - b"b": b"\b", - b"f": b"\f", - b"(": b"(", - b")": b")", - b"\\": b"\\", - ord(b"n"): b"\n", - ord(b"r"): b"\r", - ord(b"t"): b"\t", - ord(b"b"): b"\b", - ord(b"f"): b"\f", - ord(b"("): b"(", - ord(b")"): b")", - ord(b"\\"): b"\\", - } - - @classmethod - def get_literal_string( - cls, data: bytes | bytearray | mmap.mmap, offset: int - ) -> tuple[bytes, int]: - nesting_depth = 0 - result = bytearray() - for m in cls.re_lit_str_token.finditer(data, offset): - result.extend(data[offset : m.start()]) - if m.group(1): - result.extend(cls.escaped_chars[m.group(1)[1]]) - elif m.group(2): - result.append(int(m.group(2)[1:], 8)) - elif m.group(3): - pass - elif m.group(5): - result.extend(b"\n") - elif m.group(6): - result.extend(b"(") - nesting_depth += 1 - elif m.group(7): - if nesting_depth == 0: - return bytes(result), m.end() - result.extend(b")") - nesting_depth -= 1 - offset = m.end() - msg = "unfinished literal string" - raise PdfFormatError(msg) - - re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) - re_xref_subsection_start = re.compile( - whitespace_optional - + rb"([0-9]+)" - + whitespace_mandatory - + rb"([0-9]+)" - + whitespace_optional - + newline_only - ) - re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") - - def read_xref_table(self, xref_section_offset: int) -> int: - assert self.buf is not None - subsection_found = False - m = self.re_xref_section_start.match( - self.buf, xref_section_offset + self.start_offset - ) - check_format_condition(m is not None, "xref section start not found") - assert m is not None - offset = m.end() - while True: - m = self.re_xref_subsection_start.match(self.buf, offset) - if not m: - check_format_condition( - subsection_found, "xref subsection start not found" - ) - break - subsection_found = True - offset = m.end() - first_object = int(m.group(1)) - num_objects = int(m.group(2)) - for i in range(first_object, first_object + num_objects): - m = self.re_xref_entry.match(self.buf, offset) - check_format_condition(m is not None, "xref entry not found") - assert m is not None - offset = m.end() - is_free = m.group(3) == b"f" - if not is_free: - generation = int(m.group(2)) - new_entry = (int(m.group(1)), generation) - if i not in self.xref_table: - self.xref_table[i] = new_entry - return offset - - def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any: - offset, generation = self.xref_table[ref[0]] - check_format_condition( - generation == ref[1], - f"expected to find generation {ref[1]} for object ID {ref[0]} in xref " - f"table, instead found generation {generation} at offset {offset}", - ) - assert self.buf is not None - value = self.get_value( - self.buf, - offset + self.start_offset, - expect_indirect=IndirectReference(*ref), - max_nesting=max_nesting, - )[0] - self.cached_objects[ref] = value - return value - - def linearize_page_tree( - self, node: PdfDict | None = None - ) -> list[IndirectReference]: - page_node = node if node is not None else self.page_tree_root - check_format_condition( - page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" - ) - pages = [] - for kid in page_node[b"Kids"]: - kid_object = self.read_indirect(kid) - if kid_object[b"Type"] == b"Page": - pages.append(kid) - else: - pages.extend(self.linearize_page_tree(node=kid_object)) - return pages diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PixarImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PixarImagePlugin.py deleted file mode 100644 index d2b6d0a..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PixarImagePlugin.py +++ /dev/null @@ -1,72 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PIXAR raster support for PIL -# -# history: -# 97-01-29 fl Created -# -# notes: -# This is incomplete; it is based on a few samples created with -# Photoshop 2.5 and 3.0, and a summary description provided by -# Greg Coats <gcoats@labiris.er.usgs.gov>. Hopefully, "L" and -# "RGBA" support will be added in future versions. -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile -from ._binary import i16le as i16 - -# -# helpers - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"\200\350\000\000") - - -## -# Image plugin for PIXAR raster images. - - -class PixarImageFile(ImageFile.ImageFile): - format = "PIXAR" - format_description = "PIXAR raster image" - - def _open(self) -> None: - # assuming a 4-byte magic label - assert self.fp is not None - - s = self.fp.read(4) - if not _accept(s): - msg = "not a PIXAR file" - raise SyntaxError(msg) - - # read rest of header - s = s + self.fp.read(508) - - self._size = i16(s, 418), i16(s, 416) - - # get channel/depth descriptions - mode = i16(s, 424), i16(s, 426) - - if mode == (14, 2): - self._mode = "RGB" - # FIXME: to be continued... - - # create tile descriptor (assuming "dumped") - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)] - - -# -# -------------------------------------------------------------------- - -Image.register_open(PixarImageFile.format, PixarImageFile, _accept) - -Image.register_extension(PixarImageFile.format, ".pxr") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PngImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PngImagePlugin.py deleted file mode 100644 index 76a15bd..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PngImagePlugin.py +++ /dev/null @@ -1,1563 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PNG support code -# -# See "PNG (Portable Network Graphics) Specification, version 1.0; -# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.). -# -# history: -# 1996-05-06 fl Created (couldn't resist it) -# 1996-12-14 fl Upgraded, added read and verify support (0.2) -# 1996-12-15 fl Separate PNG stream parser -# 1996-12-29 fl Added write support, added getchunks -# 1996-12-30 fl Eliminated circular references in decoder (0.3) -# 1998-07-12 fl Read/write 16-bit images as mode I (0.4) -# 2001-02-08 fl Added transparency support (from Zircon) (0.5) -# 2001-04-16 fl Don't close data source in "open" method (0.6) -# 2004-02-24 fl Don't even pretend to support interlaced files (0.7) -# 2004-08-31 fl Do basic sanity check on chunk identifiers (0.8) -# 2004-09-20 fl Added PngInfo chunk container -# 2004-12-18 fl Added DPI read support (based on code by Niki Spahiev) -# 2008-08-13 fl Added tRNS support for RGB images -# 2009-03-06 fl Support for preserving ICC profiles (by Florian Hoech) -# 2009-03-08 fl Added zTXT support (from Lowell Alleman) -# 2009-03-29 fl Read interlaced PNG files (from Conrado Porto Lopes Gouvua) -# -# Copyright (c) 1997-2009 by Secret Labs AB -# Copyright (c) 1996 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import itertools -import logging -import re -import struct -import warnings -import zlib -from enum import IntEnum -from fractions import Fraction -from typing import IO, NamedTuple, cast - -from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import o8 -from ._binary import o16be as o16 -from ._binary import o32be as o32 -from ._deprecate import deprecate -from ._util import DeferredError - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, NoReturn - - from . import _imaging - -logger = logging.getLogger(__name__) - -is_cid = re.compile(rb"\w\w\w\w").match - - -_MAGIC = b"\211PNG\r\n\032\n" - - -_MODES = { - # supported bits/color combinations, and corresponding modes/rawmodes - # Grayscale - (1, 0): ("1", "1"), - (2, 0): ("L", "L;2"), - (4, 0): ("L", "L;4"), - (8, 0): ("L", "L"), - (16, 0): ("I;16", "I;16B"), - # Truecolour - (8, 2): ("RGB", "RGB"), - (16, 2): ("RGB", "RGB;16B"), - # Indexed-colour - (1, 3): ("P", "P;1"), - (2, 3): ("P", "P;2"), - (4, 3): ("P", "P;4"), - (8, 3): ("P", "P"), - # Grayscale with alpha - (8, 4): ("LA", "LA"), - (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available - # Truecolour with alpha - (8, 6): ("RGBA", "RGBA"), - (16, 6): ("RGBA", "RGBA;16B"), -} - - -_simple_palette = re.compile(b"^\xff*\x00\xff*$") - -MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK -""" -Maximum decompressed size for a iTXt or zTXt chunk. -Eliminates decompression bombs where compressed chunks can expand 1000x. -See :ref:`Text in PNG File Format<png-text>`. -""" -MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK -""" -Set the maximum total text chunk size. -See :ref:`Text in PNG File Format<png-text>`. -""" - - -# APNG frame disposal modes -class Disposal(IntEnum): - OP_NONE = 0 - """ - No disposal is done on this frame before rendering the next frame. - See :ref:`Saving APNG sequences<apng-saving>`. - """ - OP_BACKGROUND = 1 - """ - This frame’s modified region is cleared to fully transparent black before rendering - the next frame. - See :ref:`Saving APNG sequences<apng-saving>`. - """ - OP_PREVIOUS = 2 - """ - This frame’s modified region is reverted to the previous frame’s contents before - rendering the next frame. - See :ref:`Saving APNG sequences<apng-saving>`. - """ - - -# APNG frame blend modes -class Blend(IntEnum): - OP_SOURCE = 0 - """ - All color components of this frame, including alpha, overwrite the previous output - image contents. - See :ref:`Saving APNG sequences<apng-saving>`. - """ - OP_OVER = 1 - """ - This frame should be alpha composited with the previous output image contents. - See :ref:`Saving APNG sequences<apng-saving>`. - """ - - -def _safe_zlib_decompress(s: bytes) -> bytes: - dobj = zlib.decompressobj() - plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) - if dobj.unconsumed_tail: - msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK" - raise ValueError(msg) - return plaintext - - -def _crc32(data: bytes, seed: int = 0) -> int: - return zlib.crc32(data, seed) & 0xFFFFFFFF - - -# -------------------------------------------------------------------- -# Support classes. Suitable for PNG and related formats like MNG etc. - - -class ChunkStream: - def __init__(self, fp: IO[bytes]) -> None: - self.fp: IO[bytes] | None = fp - self.queue: list[tuple[bytes, int, int]] | None = [] - - def read(self) -> tuple[bytes, int, int]: - """Fetch a new chunk. Returns header information.""" - cid = None - - assert self.fp is not None - if self.queue: - cid, pos, length = self.queue.pop() - self.fp.seek(pos) - else: - s = self.fp.read(8) - cid = s[4:] - pos = self.fp.tell() - length = i32(s) - - if not is_cid(cid): - if not ImageFile.LOAD_TRUNCATED_IMAGES: - msg = f"broken PNG file (chunk {repr(cid)})" - raise SyntaxError(msg) - - return cid, pos, length - - def __enter__(self) -> ChunkStream: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self) -> None: - self.queue = self.fp = None - - def push(self, cid: bytes, pos: int, length: int) -> None: - assert self.queue is not None - self.queue.append((cid, pos, length)) - - def call(self, cid: bytes, pos: int, length: int) -> bytes: - """Call the appropriate chunk handler""" - - logger.debug("STREAM %r %s %s", cid, pos, length) - return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length) - - def crc(self, cid: bytes, data: bytes) -> None: - """Read and verify checksum""" - - # Skip CRC checks for ancillary chunks if allowed to load truncated - # images - # 5th byte of first char is 1 [specs, section 5.4] - if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1): - self.crc_skip(cid, data) - return - - assert self.fp is not None - try: - crc1 = _crc32(data, _crc32(cid)) - crc2 = i32(self.fp.read(4)) - if crc1 != crc2: - msg = f"broken PNG file (bad header checksum in {repr(cid)})" - raise SyntaxError(msg) - except struct.error as e: - msg = f"broken PNG file (incomplete checksum in {repr(cid)})" - raise SyntaxError(msg) from e - - def crc_skip(self, cid: bytes, data: bytes) -> None: - """Read checksum""" - - assert self.fp is not None - self.fp.read(4) - - def verify(self, endchunk: bytes = b"IEND") -> list[bytes]: - # Simple approach; just calculate checksum for all remaining - # blocks. Must be called directly after open. - - cids = [] - - assert self.fp is not None - while True: - try: - cid, pos, length = self.read() - except struct.error as e: - msg = "truncated PNG file" - raise OSError(msg) from e - - if cid == endchunk: - break - self.crc(cid, ImageFile._safe_read(self.fp, length)) - cids.append(cid) - - return cids - - -class iTXt(str): - """ - Subclass of string to allow iTXt chunks to look like strings while - keeping their extra information - - """ - - lang: str | bytes | None - tkey: str | bytes | None - - @staticmethod - def __new__( - cls, text: str, lang: str | None = None, tkey: str | None = None - ) -> iTXt: - """ - :param cls: the class to use when creating the instance - :param text: value for this key - :param lang: language code - :param tkey: UTF-8 version of the key name - """ - - self = str.__new__(cls, text) - self.lang = lang - self.tkey = tkey - return self - - -class PngInfo: - """ - PNG chunk container (for use with save(pnginfo=)) - - """ - - def __init__(self) -> None: - self.chunks: list[tuple[bytes, bytes, bool]] = [] - - def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None: - """Appends an arbitrary chunk. Use with caution. - - :param cid: a byte string, 4 bytes long. - :param data: a byte string of the encoded data - :param after_idat: for use with private chunks. Whether the chunk - should be written after IDAT - - """ - - self.chunks.append((cid, data, after_idat)) - - def add_itxt( - self, - key: str | bytes, - value: str | bytes, - lang: str | bytes = "", - tkey: str | bytes = "", - zip: bool = False, - ) -> None: - """Appends an iTXt chunk. - - :param key: latin-1 encodable text key name - :param value: value for this key - :param lang: language code - :param tkey: UTF-8 version of the key name - :param zip: compression flag - - """ - - if not isinstance(key, bytes): - key = key.encode("latin-1", "strict") - if not isinstance(value, bytes): - value = value.encode("utf-8", "strict") - if not isinstance(lang, bytes): - lang = lang.encode("utf-8", "strict") - if not isinstance(tkey, bytes): - tkey = tkey.encode("utf-8", "strict") - - if zip: - self.add( - b"iTXt", - key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value), - ) - else: - self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) - - def add_text( - self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False - ) -> None: - """Appends a text chunk. - - :param key: latin-1 encodable text key name - :param value: value for this key, text or an - :py:class:`PIL.PngImagePlugin.iTXt` instance - :param zip: compression flag - - """ - if isinstance(value, iTXt): - return self.add_itxt( - key, - value, - value.lang if value.lang is not None else b"", - value.tkey if value.tkey is not None else b"", - zip=zip, - ) - - # The tEXt chunk stores latin-1 text - if not isinstance(value, bytes): - try: - value = value.encode("latin-1", "strict") - except UnicodeError: - return self.add_itxt(key, value, zip=zip) - - if not isinstance(key, bytes): - key = key.encode("latin-1", "strict") - - if zip: - self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) - else: - self.add(b"tEXt", key + b"\0" + value) - - -# -------------------------------------------------------------------- -# PNG image stream (IHDR/IEND) - - -class _RewindState(NamedTuple): - info: dict[str | tuple[int, int], Any] - tile: list[ImageFile._Tile] - seq_num: int | None - - -class PngStream(ChunkStream): - def __init__(self, fp: IO[bytes]) -> None: - super().__init__(fp) - - # local copies of Image attributes - self.im_info: dict[str | tuple[int, int], Any] = {} - self.im_text: dict[str, str | iTXt] = {} - self.im_size = (0, 0) - self.im_mode = "" - self.im_tile: list[ImageFile._Tile] = [] - self.im_palette: tuple[str, bytes] | None = None - self.im_custom_mimetype: str | None = None - self.im_n_frames: int | None = None - self._seq_num: int | None = None - self.rewind_state = _RewindState({}, [], None) - - self.text_memory = 0 - - def check_text_memory(self, chunklen: int) -> None: - self.text_memory += chunklen - if self.text_memory > MAX_TEXT_MEMORY: - msg = ( - "Too much memory used in text chunks: " - f"{self.text_memory}>MAX_TEXT_MEMORY" - ) - raise ValueError(msg) - - def save_rewind(self) -> None: - self.rewind_state = _RewindState( - self.im_info.copy(), - self.im_tile, - self._seq_num, - ) - - def rewind(self) -> None: - self.im_info = self.rewind_state.info.copy() - self.im_tile = self.rewind_state.tile - self._seq_num = self.rewind_state.seq_num - - def chunk_iCCP(self, pos: int, length: int) -> bytes: - # ICC profile - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - # according to PNG spec, the iCCP chunk contains: - # Profile name 1-79 bytes (character string) - # Null separator 1 byte (null character) - # Compression method 1 byte (0) - # Compressed profile n bytes (zlib with deflate compression) - i = s.find(b"\0") - logger.debug("iCCP profile name %r", s[:i]) - comp_method = s[i + 1] - logger.debug("Compression method %s", comp_method) - if comp_method != 0: - msg = f"Unknown compression method {comp_method} in iCCP chunk" - raise SyntaxError(msg) - try: - icc_profile = _safe_zlib_decompress(s[i + 2 :]) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - icc_profile = None - else: - raise - except zlib.error: - icc_profile = None # FIXME - self.im_info["icc_profile"] = icc_profile - return s - - def chunk_IHDR(self, pos: int, length: int) -> bytes: - # image header - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 13: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "Truncated IHDR chunk" - raise ValueError(msg) - self.im_size = i32(s, 0), i32(s, 4) - try: - self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] - except Exception: - pass - if s[12]: - self.im_info["interlace"] = 1 - if s[11]: - msg = "unknown filter category" - raise SyntaxError(msg) - return s - - def chunk_IDAT(self, pos: int, length: int) -> NoReturn: - # image data - if "bbox" in self.im_info: - tile = [ImageFile._Tile("zip", self.im_info["bbox"], pos, self.im_rawmode)] - else: - if self.im_n_frames is not None: - self.im_info["default_image"] = True - tile = [ImageFile._Tile("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] - self.im_tile = tile - self.im_idat = length - msg = "image data found" - raise EOFError(msg) - - def chunk_IEND(self, pos: int, length: int) -> NoReturn: - msg = "end of PNG image" - raise EOFError(msg) - - def chunk_PLTE(self, pos: int, length: int) -> bytes: - # palette - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if self.im_mode == "P": - self.im_palette = "RGB", s - return s - - def chunk_tRNS(self, pos: int, length: int) -> bytes: - # transparency - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if self.im_mode == "P": - if _simple_palette.match(s): - # tRNS contains only one full-transparent entry, - # other entries are full opaque - i = s.find(b"\0") - if i >= 0: - self.im_info["transparency"] = i - else: - # otherwise, we have a byte string with one alpha value - # for each palette entry - self.im_info["transparency"] = s - elif self.im_mode == "1": - self.im_info["transparency"] = 255 if i16(s) else 0 - elif self.im_mode in ("L", "I;16"): - self.im_info["transparency"] = i16(s) - elif self.im_mode == "RGB": - self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) - return s - - def chunk_gAMA(self, pos: int, length: int) -> bytes: - # gamma setting - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - self.im_info["gamma"] = i32(s) / 100000.0 - return s - - def chunk_cHRM(self, pos: int, length: int) -> bytes: - # chromaticity, 8 unsigned ints, actual value is scaled by 100,000 - # WP x,y, Red x,y, Green x,y Blue x,y - - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - raw_vals = struct.unpack(f">{len(s) // 4}I", s) - self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) - return s - - def chunk_sRGB(self, pos: int, length: int) -> bytes: - # srgb rendering intent, 1 byte - # 0 perceptual - # 1 relative colorimetric - # 2 saturation - # 3 absolute colorimetric - - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 1: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "Truncated sRGB chunk" - raise ValueError(msg) - self.im_info["srgb"] = s[0] - return s - - def chunk_pHYs(self, pos: int, length: int) -> bytes: - # pixels per unit - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 9: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "Truncated pHYs chunk" - raise ValueError(msg) - px, py = i32(s, 0), i32(s, 4) - unit = s[8] - if unit == 1: # meter - dpi = px * 0.0254, py * 0.0254 - self.im_info["dpi"] = dpi - elif unit == 0: - self.im_info["aspect"] = px, py - return s - - def chunk_tEXt(self, pos: int, length: int) -> bytes: - # text - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - try: - k, v = s.split(b"\0", 1) - except ValueError: - # fallback for broken tEXt tags - k = s - v = b"" - if k: - k_str = k.decode("latin-1", "strict") - v_str = v.decode("latin-1", "replace") - - self.im_info[k_str] = v if k == b"exif" else v_str - self.im_text[k_str] = v_str - self.check_text_memory(len(v_str)) - - return s - - def chunk_zTXt(self, pos: int, length: int) -> bytes: - # compressed text - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - try: - k, v = s.split(b"\0", 1) - except ValueError: - k = s - v = b"" - if v: - comp_method = v[0] - else: - comp_method = 0 - if comp_method != 0: - msg = f"Unknown compression method {comp_method} in zTXt chunk" - raise SyntaxError(msg) - try: - v = _safe_zlib_decompress(v[1:]) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - v = b"" - else: - raise - except zlib.error: - v = b"" - - if k: - k_str = k.decode("latin-1", "strict") - v_str = v.decode("latin-1", "replace") - - self.im_info[k_str] = self.im_text[k_str] = v_str - self.check_text_memory(len(v_str)) - - return s - - def chunk_iTXt(self, pos: int, length: int) -> bytes: - # international text - assert self.fp is not None - r = s = ImageFile._safe_read(self.fp, length) - try: - k, r = r.split(b"\0", 1) - except ValueError: - return s - if len(r) < 2: - return s - cf, cm, r = r[0], r[1], r[2:] - try: - lang, tk, v = r.split(b"\0", 2) - except ValueError: - return s - if cf != 0: - if cm == 0: - try: - v = _safe_zlib_decompress(v) - except ValueError: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - else: - raise - except zlib.error: - return s - else: - return s - if k == b"XML:com.adobe.xmp": - self.im_info["xmp"] = v - try: - k_str = k.decode("latin-1", "strict") - lang_str = lang.decode("utf-8", "strict") - tk_str = tk.decode("utf-8", "strict") - v_str = v.decode("utf-8", "strict") - except UnicodeError: - return s - - self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str) - self.check_text_memory(len(v_str)) - - return s - - def chunk_eXIf(self, pos: int, length: int) -> bytes: - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - self.im_info["exif"] = b"Exif\x00\x00" + s - return s - - # APNG chunks - def chunk_acTL(self, pos: int, length: int) -> bytes: - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 8: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "APNG contains truncated acTL chunk" - raise ValueError(msg) - if self.im_n_frames is not None: - self.im_n_frames = None - warnings.warn("Invalid APNG, will use default PNG image if possible") - return s - n_frames = i32(s) - if n_frames == 0 or n_frames > 0x80000000: - warnings.warn("Invalid APNG, will use default PNG image if possible") - return s - self.im_n_frames = n_frames - self.im_info["loop"] = i32(s, 4) - self.im_custom_mimetype = "image/apng" - return s - - def chunk_fcTL(self, pos: int, length: int) -> bytes: - assert self.fp is not None - s = ImageFile._safe_read(self.fp, length) - if length < 26: - if ImageFile.LOAD_TRUNCATED_IMAGES: - return s - msg = "APNG contains truncated fcTL chunk" - raise ValueError(msg) - seq = i32(s) - if (self._seq_num is None and seq != 0) or ( - self._seq_num is not None and self._seq_num != seq - 1 - ): - msg = "APNG contains frame sequence errors" - raise SyntaxError(msg) - self._seq_num = seq - width, height = i32(s, 4), i32(s, 8) - px, py = i32(s, 12), i32(s, 16) - im_w, im_h = self.im_size - if px + width > im_w or py + height > im_h: - msg = "APNG contains invalid frames" - raise SyntaxError(msg) - self.im_info["bbox"] = (px, py, px + width, py + height) - delay_num, delay_den = i16(s, 20), i16(s, 22) - if delay_den == 0: - delay_den = 100 - self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000 - self.im_info["disposal"] = s[24] - self.im_info["blend"] = s[25] - return s - - def chunk_fdAT(self, pos: int, length: int) -> bytes: - assert self.fp is not None - if length < 4: - if ImageFile.LOAD_TRUNCATED_IMAGES: - s = ImageFile._safe_read(self.fp, length) - return s - msg = "APNG contains truncated fDAT chunk" - raise ValueError(msg) - s = ImageFile._safe_read(self.fp, 4) - seq = i32(s) - if self._seq_num != seq - 1: - msg = "APNG contains frame sequence errors" - raise SyntaxError(msg) - self._seq_num = seq - return self.chunk_IDAT(pos + 4, length - 4) - - -# -------------------------------------------------------------------- -# PNG reader - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(_MAGIC) - - -## -# Image plugin for PNG images. - - -class PngImageFile(ImageFile.ImageFile): - format = "PNG" - format_description = "Portable network graphics" - - def _open(self) -> None: - assert self.fp is not None - if not _accept(self.fp.read(8)): - msg = "not a PNG file" - raise SyntaxError(msg) - self._fp = self.fp - self.__frame = 0 - - # - # Parse headers up to the first IDAT or fDAT chunk - - self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = [] - self.png: PngStream | None = PngStream(self.fp) - - while True: - # - # get next chunk - - cid, pos, length = self.png.read() - - try: - s = self.png.call(cid, pos, length) - except EOFError: - break - except AttributeError: - logger.debug("%r %s %s (unknown)", cid, pos, length) - s = ImageFile._safe_read(self.fp, length) - if cid[1:2].islower(): - self.private_chunks.append((cid, s)) - - self.png.crc(cid, s) - - # - # Copy relevant attributes from the PngStream. An alternative - # would be to let the PngStream class modify these attributes - # directly, but that introduces circular references which are - # difficult to break if things go wrong in the decoder... - # (believe me, I've tried ;-) - - self._mode = self.png.im_mode - self._size = self.png.im_size - self.info = self.png.im_info - self._text: dict[str, str | iTXt] | None = None - self.tile = self.png.im_tile - self.custom_mimetype = self.png.im_custom_mimetype - self.n_frames = self.png.im_n_frames or 1 - self.default_image = self.info.get("default_image", False) - - if self.png.im_palette: - rawmode, data = self.png.im_palette - self.palette = ImagePalette.raw(rawmode, data) - - if cid == b"fdAT": - self.__prepare_idat = length - 4 - else: - self.__prepare_idat = length # used by load_prepare() - - if self.png.im_n_frames is not None: - self._close_exclusive_fp_after_loading = False - self.png.save_rewind() - self.__rewind_idat = self.__prepare_idat - self.__rewind = self._fp.tell() - if self.default_image: - # IDAT chunk contains default image and not first animation frame - self.n_frames += 1 - self._seek(0) - self.is_animated = self.n_frames > 1 - - @property - def text(self) -> dict[str, str | iTXt]: - # experimental - if self._text is None: - # iTxt, tEXt and zTXt chunks may appear at the end of the file - # So load the file to ensure that they are read - if self.is_animated: - frame = self.__frame - # for APNG, seek to the final frame before loading - self.seek(self.n_frames - 1) - self.load() - if self.is_animated: - self.seek(frame) - assert self._text is not None - return self._text - - def verify(self) -> None: - """Verify PNG file""" - - if self.fp is None: - msg = "verify must be called directly after open" - raise RuntimeError(msg) - - # back up to beginning of IDAT block - self.fp.seek(self.tile[0][2] - 8) - - assert self.png is not None - self.png.verify() - self.png.close() - - super().verify() - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - if frame < self.__frame: - self._seek(0, True) - - last_frame = self.__frame - try: - for f in range(self.__frame + 1, frame + 1): - self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in APNG file" - raise EOFError(msg) from e - - def _seek(self, frame: int, rewind: bool = False) -> None: - assert self.png is not None - if isinstance(self._fp, DeferredError): - raise self._fp.ex - - self.dispose: _imaging.ImagingCore | None - dispose_extent = None - if frame == 0: - if rewind: - self._fp.seek(self.__rewind) - self.png.rewind() - self.__prepare_idat = self.__rewind_idat - self._im = None - self.info = self.png.im_info - self.tile = self.png.im_tile - self.fp = self._fp - self._prev_im = None - self.dispose = None - self.default_image = self.info.get("default_image", False) - self.dispose_op = self.info.get("disposal") - self.blend_op = self.info.get("blend") - dispose_extent = self.info.get("bbox") - self.__frame = 0 - else: - if frame != self.__frame + 1: - msg = f"cannot seek to frame {frame}" - raise ValueError(msg) - - # ensure previous frame was loaded - self.load() - - if self.dispose: - self.im.paste(self.dispose, self.dispose_extent) - self._prev_im = self.im.copy() - - self.fp = self._fp - - # advance to the next frame - if self.__prepare_idat: - ImageFile._safe_read(self.fp, self.__prepare_idat) - self.__prepare_idat = 0 - frame_start = False - while True: - self.fp.read(4) # CRC - - try: - cid, pos, length = self.png.read() - except (struct.error, SyntaxError): - break - - if cid == b"IEND": - msg = "No more images in APNG file" - raise EOFError(msg) - if cid == b"fcTL": - if frame_start: - # there must be at least one fdAT chunk between fcTL chunks - msg = "APNG missing frame data" - raise SyntaxError(msg) - frame_start = True - - try: - self.png.call(cid, pos, length) - except UnicodeDecodeError: - break - except EOFError: - if cid == b"fdAT": - length -= 4 - if frame_start: - self.__prepare_idat = length - break - ImageFile._safe_read(self.fp, length) - except AttributeError: - logger.debug("%r %s %s (unknown)", cid, pos, length) - ImageFile._safe_read(self.fp, length) - - self.__frame = frame - self.tile = self.png.im_tile - self.dispose_op = self.info.get("disposal") - self.blend_op = self.info.get("blend") - dispose_extent = self.info.get("bbox") - - if not self.tile: - msg = "image not found in APNG frame" - raise EOFError(msg) - if dispose_extent: - self.dispose_extent: tuple[float, float, float, float] = dispose_extent - - # setup frame disposal (actual disposal done when needed in the next _seek()) - if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: - self.dispose_op = Disposal.OP_BACKGROUND - - self.dispose = None - if self.dispose_op == Disposal.OP_PREVIOUS: - if self._prev_im: - self.dispose = self._prev_im.copy() - self.dispose = self._crop(self.dispose, self.dispose_extent) - elif self.dispose_op == Disposal.OP_BACKGROUND: - self.dispose = Image.core.fill(self.mode, self.size) - self.dispose = self._crop(self.dispose, self.dispose_extent) - - def tell(self) -> int: - return self.__frame - - def load_prepare(self) -> None: - """internal: prepare to read PNG file""" - - if self.info.get("interlace"): - self.decoderconfig = self.decoderconfig + (1,) - - self.__idat = self.__prepare_idat # used by load_read() - ImageFile.ImageFile.load_prepare(self) - - def load_read(self, read_bytes: int) -> bytes: - """internal: read more image data""" - - assert self.png is not None - assert self.fp is not None - while self.__idat == 0: - # end of chunk, skip forward to next one - - self.fp.read(4) # CRC - - cid, pos, length = self.png.read() - - if cid not in [b"IDAT", b"DDAT", b"fdAT"]: - self.png.push(cid, pos, length) - return b"" - - if cid == b"fdAT": - try: - self.png.call(cid, pos, length) - except EOFError: - pass - self.__idat = length - 4 # sequence_num has already been read - else: - self.__idat = length # empty chunks are allowed - - # read more data from this chunk - if read_bytes <= 0: - read_bytes = self.__idat - else: - read_bytes = min(read_bytes, self.__idat) - - self.__idat = self.__idat - read_bytes - - return self.fp.read(read_bytes) - - def load_end(self) -> None: - """internal: finished reading image data""" - assert self.png is not None - assert self.fp is not None - if self.__idat != 0: - self.fp.read(self.__idat) - while True: - self.fp.read(4) # CRC - - try: - cid, pos, length = self.png.read() - except (struct.error, SyntaxError): - break - - if cid == b"IEND": - break - elif cid == b"fcTL" and self.is_animated: - # start of the next frame, stop reading - self.__prepare_idat = 0 - self.png.push(cid, pos, length) - break - - try: - self.png.call(cid, pos, length) - except UnicodeDecodeError: - break - except EOFError: - if cid == b"fdAT": - length -= 4 - try: - ImageFile._safe_read(self.fp, length) - except OSError as e: - if ImageFile.LOAD_TRUNCATED_IMAGES: - break - else: - raise e - except AttributeError: - logger.debug("%r %s %s (unknown)", cid, pos, length) - s = ImageFile._safe_read(self.fp, length) - if cid[1:2].islower(): - self.private_chunks.append((cid, s, True)) - self._text = self.png.im_text - if not self.is_animated: - self.png.close() - self.png = None - else: - if self._prev_im and self.blend_op == Blend.OP_OVER: - updated = self._crop(self.im, self.dispose_extent) - if self.im.mode == "RGB" and "transparency" in self.info: - mask = updated.convert_transparent( - "RGBA", self.info["transparency"] - ) - else: - if self.im.mode == "P" and "transparency" in self.info: - t = self.info["transparency"] - if isinstance(t, bytes): - updated.putpalettealphas(t) - elif isinstance(t, int): - updated.putpalettealpha(t) - mask = updated.convert("RGBA") - self._prev_im.paste(updated, self.dispose_extent, mask) - self.im = self._prev_im - - def _getexif(self) -> dict[int, Any] | None: - if "exif" not in self.info: - self.load() - if "exif" not in self.info and "Raw profile type exif" not in self.info: - return None - return self.getexif()._get_merged_dict() - - def getexif(self) -> Image.Exif: - if "exif" not in self.info: - self.load() - - return super().getexif() - - -# -------------------------------------------------------------------- -# PNG writer - -_OUTMODES = { - # supported PIL modes, and corresponding rawmode, bit depth and color type - "1": ("1", b"\x01", b"\x00"), - "L;1": ("L;1", b"\x01", b"\x00"), - "L;2": ("L;2", b"\x02", b"\x00"), - "L;4": ("L;4", b"\x04", b"\x00"), - "L": ("L", b"\x08", b"\x00"), - "LA": ("LA", b"\x08", b"\x04"), - "I": ("I;16B", b"\x10", b"\x00"), - "I;16": ("I;16B", b"\x10", b"\x00"), - "I;16B": ("I;16B", b"\x10", b"\x00"), - "P;1": ("P;1", b"\x01", b"\x03"), - "P;2": ("P;2", b"\x02", b"\x03"), - "P;4": ("P;4", b"\x04", b"\x03"), - "P": ("P", b"\x08", b"\x03"), - "RGB": ("RGB", b"\x08", b"\x02"), - "RGBA": ("RGBA", b"\x08", b"\x06"), -} - - -def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None: - """Write a PNG chunk (including CRC field)""" - - byte_data = b"".join(data) - - fp.write(o32(len(byte_data)) + cid) - fp.write(byte_data) - crc = _crc32(byte_data, _crc32(cid)) - fp.write(o32(crc)) - - -class _idat: - # wrap output from the encoder in IDAT chunks - - def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None: - self.fp = fp - self.chunk = chunk - - def write(self, data: bytes) -> None: - self.chunk(self.fp, b"IDAT", data) - - -class _fdat: - # wrap encoder output in fdAT chunks - - def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None: - self.fp = fp - self.chunk = chunk - self.seq_num = seq_num - - def write(self, data: bytes) -> None: - self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) - self.seq_num += 1 - - -def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None: - im.encoderconfig = ( - encoderinfo.get("optimize", False), - encoderinfo.get("compress_level", -1), - encoderinfo.get("compress_type", -1), - encoderinfo.get("dictionary", b""), - ) - - -class _Frame(NamedTuple): - im: Image.Image - bbox: tuple[int, int, int, int] | None - encoderinfo: dict[str, Any] - - -def _write_multiple_frames( - im: Image.Image, - fp: IO[bytes], - chunk: Callable[..., None], - mode: str, - rawmode: str, - default_image: Image.Image | None, - append_images: list[Image.Image], -) -> Image.Image | None: - duration = im.encoderinfo.get("duration") - loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) - disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) - blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) - - if default_image: - chain = itertools.chain(append_images) - else: - chain = itertools.chain([im], append_images) - - im_frames: list[_Frame] = [] - frame_count = 0 - for im_seq in chain: - for im_frame in ImageSequence.Iterator(im_seq): - if im_frame.mode == mode: - im_frame = im_frame.copy() - else: - im_frame = im_frame.convert(mode) - encoderinfo = im.encoderinfo.copy() - if isinstance(duration, (list, tuple)): - encoderinfo["duration"] = duration[frame_count] - elif duration is None and "duration" in im_frame.info: - encoderinfo["duration"] = im_frame.info["duration"] - if isinstance(disposal, (list, tuple)): - encoderinfo["disposal"] = disposal[frame_count] - if isinstance(blend, (list, tuple)): - encoderinfo["blend"] = blend[frame_count] - frame_count += 1 - - if im_frames: - previous = im_frames[-1] - prev_disposal = previous.encoderinfo.get("disposal") - prev_blend = previous.encoderinfo.get("blend") - if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: - prev_disposal = Disposal.OP_BACKGROUND - - if prev_disposal == Disposal.OP_BACKGROUND: - base_im = previous.im.copy() - dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) - bbox = previous.bbox - if bbox: - dispose = dispose.crop(bbox) - else: - bbox = (0, 0) + im.size - base_im.paste(dispose, bbox) - elif prev_disposal == Disposal.OP_PREVIOUS: - base_im = im_frames[-2].im - else: - base_im = previous.im - delta = ImageChops.subtract_modulo( - im_frame.convert("RGBA"), base_im.convert("RGBA") - ) - bbox = delta.getbbox(alpha_only=False) - if ( - not bbox - and prev_disposal == encoderinfo.get("disposal") - and prev_blend == encoderinfo.get("blend") - and "duration" in encoderinfo - ): - previous.encoderinfo["duration"] += encoderinfo["duration"] - continue - else: - bbox = None - im_frames.append(_Frame(im_frame, bbox, encoderinfo)) - - if len(im_frames) == 1 and not default_image: - return im_frames[0].im - - # animation control - chunk( - fp, - b"acTL", - o32(len(im_frames)), # 0: num_frames - o32(loop), # 4: num_plays - ) - - # default image IDAT (if it exists) - if default_image: - default_im = im if im.mode == mode else im.convert(mode) - _apply_encoderinfo(default_im, im.encoderinfo) - ImageFile._save( - default_im, - cast(IO[bytes], _idat(fp, chunk)), - [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], - ) - - seq_num = 0 - for frame, frame_data in enumerate(im_frames): - im_frame = frame_data.im - if not frame_data.bbox: - bbox = (0, 0) + im_frame.size - else: - bbox = frame_data.bbox - im_frame = im_frame.crop(bbox) - size = im_frame.size - encoderinfo = frame_data.encoderinfo - frame_duration = encoderinfo.get("duration", 0) - delay = Fraction(frame_duration / 1000).limit_denominator(65535) - if delay.numerator > 65535: - msg = "cannot write duration" - raise ValueError(msg) - frame_disposal = encoderinfo.get("disposal", disposal) - frame_blend = encoderinfo.get("blend", blend) - # frame control - chunk( - fp, - b"fcTL", - o32(seq_num), # sequence_number - o32(size[0]), # width - o32(size[1]), # height - o32(bbox[0]), # x_offset - o32(bbox[1]), # y_offset - o16(delay.numerator), # delay_numerator - o16(delay.denominator), # delay_denominator - o8(frame_disposal), # dispose_op - o8(frame_blend), # blend_op - ) - seq_num += 1 - # frame data - _apply_encoderinfo(im_frame, im.encoderinfo) - if frame == 0 and not default_image: - # first frame must be in IDAT chunks for backwards compatibility - ImageFile._save( - im_frame, - cast(IO[bytes], _idat(fp, chunk)), - [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], - ) - else: - fdat_chunks = _fdat(fp, chunk, seq_num) - ImageFile._save( - im_frame, - cast(IO[bytes], fdat_chunks), - [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], - ) - seq_num = fdat_chunks.seq_num - return None - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - _save(im, fp, filename, save_all=True) - - -def _save( - im: Image.Image, - fp: IO[bytes], - filename: str | bytes, - chunk: Callable[..., None] = putchunk, - save_all: bool = False, -) -> None: - # save an image to disk (called by the save method) - - if save_all: - default_image = im.encoderinfo.get( - "default_image", im.info.get("default_image") - ) - modes = set() - sizes = set() - append_images = im.encoderinfo.get("append_images", []) - for im_seq in itertools.chain([im], append_images): - for im_frame in ImageSequence.Iterator(im_seq): - modes.add(im_frame.mode) - sizes.add(im_frame.size) - for mode in ("RGBA", "RGB", "P"): - if mode in modes: - break - else: - mode = modes.pop() - size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2)) - else: - size = im.size - mode = im.mode - - outmode = mode - palette = [] - if im.palette: - palette = im.getpalette() or [] - if mode == "P": - # - # attempt to minimize storage requirements for palette images - if "bits" in im.encoderinfo: - # number of bits specified by user - colors = min(1 << im.encoderinfo["bits"], 256) - else: - # check palette contents - if im.palette: - colors = max(min(len(palette) // 3, 256), 1) - else: - colors = 256 - - if colors <= 16: - if colors <= 2: - bits = 1 - elif colors <= 4: - bits = 2 - else: - bits = 4 - outmode += f";{bits}" - - # get the corresponding PNG mode - try: - rawmode, bit_depth, color_type = _OUTMODES[outmode] - except KeyError as e: - msg = f"cannot write mode {mode} as PNG" - raise OSError(msg) from e - if outmode == "I": - deprecate("Saving I mode images as PNG", 13, stacklevel=4) - - # - # write minimal PNG file - - fp.write(_MAGIC) - - chunk( - fp, - b"IHDR", - o32(size[0]), # 0: size - o32(size[1]), - bit_depth, - color_type, - b"\0", # 10: compression - b"\0", # 11: filter category - b"\0", # 12: interlace flag - ) - - chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] - - if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")): - # ICC profile - # according to PNG spec, the iCCP chunk contains: - # Profile name 1-79 bytes (character string) - # Null separator 1 byte (null character) - # Compression method 1 byte (0) - # Compressed profile n bytes (zlib with deflate compression) - name = b"ICC Profile" - data = name + b"\0\0" + zlib.compress(icc) - chunk(fp, b"iCCP", data) - - # You must either have sRGB or iCCP. - # Disallow sRGB chunks when an iCCP-chunk has been emitted. - chunks.remove(b"sRGB") - - if info := im.encoderinfo.get("pnginfo"): - chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] - for info_chunk in info.chunks: - cid, data = info_chunk[:2] - if cid in chunks: - chunks.remove(cid) - chunk(fp, cid, data) - elif cid in chunks_multiple_allowed: - chunk(fp, cid, data) - elif cid[1:2].islower(): - # Private chunk - after_idat = len(info_chunk) == 3 and info_chunk[2] - if not after_idat: - chunk(fp, cid, data) - - if im.mode == "P": - palette_byte_number = colors * 3 - palette_bytes = bytes(palette[:palette_byte_number]) - while len(palette_bytes) < palette_byte_number: - palette_bytes += b"\0" - chunk(fp, b"PLTE", palette_bytes) - - transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None)) - - if transparency or transparency == 0: - if im.mode == "P": - # limit to actual palette size - alpha_bytes = colors - if isinstance(transparency, bytes): - chunk(fp, b"tRNS", transparency[:alpha_bytes]) - else: - transparency = max(0, min(255, transparency)) - alpha = b"\xff" * transparency + b"\0" - chunk(fp, b"tRNS", alpha[:alpha_bytes]) - elif im.mode in ("1", "L", "I", "I;16"): - transparency = max(0, min(65535, transparency)) - chunk(fp, b"tRNS", o16(transparency)) - elif im.mode == "RGB": - red, green, blue = transparency - chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) - else: - if "transparency" in im.encoderinfo: - # don't bother with transparency if it's an RGBA - # and it's in the info dict. It's probably just stale. - msg = "cannot use transparency for this mode" - raise OSError(msg) - else: - if im.mode == "P" and im.im.getpalettemode() == "RGBA": - alpha = im.im.getpalette("RGBA", "A") - alpha_bytes = colors - chunk(fp, b"tRNS", alpha[:alpha_bytes]) - - if dpi := im.encoderinfo.get("dpi"): - chunk( - fp, - b"pHYs", - o32(int(dpi[0] / 0.0254 + 0.5)), - o32(int(dpi[1] / 0.0254 + 0.5)), - b"\x01", - ) - - if info: - chunks = [b"bKGD", b"hIST"] - for info_chunk in info.chunks: - cid, data = info_chunk[:2] - if cid in chunks: - chunks.remove(cid) - chunk(fp, cid, data) - - if exif := im.encoderinfo.get("exif"): - if isinstance(exif, Image.Exif): - exif = exif.tobytes(8) - if exif.startswith(b"Exif\x00\x00"): - exif = exif[6:] - chunk(fp, b"eXIf", exif) - - single_im: Image.Image | None = im - if save_all: - single_im = _write_multiple_frames( - im, fp, chunk, mode, rawmode, default_image, append_images - ) - if single_im: - _apply_encoderinfo(single_im, im.encoderinfo) - ImageFile._save( - single_im, - cast(IO[bytes], _idat(fp, chunk)), - [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)], - ) - - if info: - for info_chunk in info.chunks: - cid, data = info_chunk[:2] - if cid[1:2].islower(): - # Private chunk - after_idat = len(info_chunk) == 3 and info_chunk[2] - if after_idat: - chunk(fp, cid, data) - - chunk(fp, b"IEND", b"") - - if hasattr(fp, "flush"): - fp.flush() - - -# -------------------------------------------------------------------- -# PNG chunk converter - - -def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]: - """Return a list of PNG chunks representing this image.""" - from io import BytesIO - - chunks = [] - - def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None: - byte_data = b"".join(data) - crc = o32(_crc32(byte_data, _crc32(cid))) - chunks.append((cid, byte_data, crc)) - - fp = BytesIO() - - try: - im.encoderinfo = params - _save(im, fp, "", append) - finally: - del im.encoderinfo - - return chunks - - -# -------------------------------------------------------------------- -# Registry - -Image.register_open(PngImageFile.format, PngImageFile, _accept) -Image.register_save(PngImageFile.format, _save) -Image.register_save_all(PngImageFile.format, _save_all) - -Image.register_extensions(PngImageFile.format, [".png", ".apng"]) - -Image.register_mime(PngImageFile.format, "image/png") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PpmImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PpmImagePlugin.py deleted file mode 100644 index 307bc97..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PpmImagePlugin.py +++ /dev/null @@ -1,375 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# PPM support for PIL -# -# History: -# 96-03-24 fl Created -# 98-03-06 fl Write RGBA images (as RGB, that is) -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import math -from typing import IO - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import o8 -from ._binary import o32le as o32 - -# -# -------------------------------------------------------------------- - -b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" - -MODES = { - # standard - b"P1": "1", - b"P2": "L", - b"P3": "RGB", - b"P4": "1", - b"P5": "L", - b"P6": "RGB", - # extensions - b"P0CMYK": "CMYK", - b"Pf": "F", - # PIL extensions (for test purposes only) - b"PyP": "P", - b"PyRGBA": "RGBA", - b"PyCMYK": "CMYK", -} - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 2 and prefix.startswith(b"P") and prefix[1] in b"0123456fy" - - -## -# Image plugin for PBM, PGM, and PPM images. - - -class PpmImageFile(ImageFile.ImageFile): - format = "PPM" - format_description = "Pbmplus image" - - def _read_magic(self) -> bytes: - assert self.fp is not None - - magic = b"" - # read until whitespace or longest available magic number - for _ in range(6): - c = self.fp.read(1) - if not c or c in b_whitespace: - break - magic += c - return magic - - def _read_token(self) -> bytes: - assert self.fp is not None - - token = b"" - while len(token) <= 10: # read until next whitespace or limit of 10 characters - c = self.fp.read(1) - if not c: - break - elif c in b_whitespace: # token ended - if not token: - # skip whitespace at start - continue - break - elif c == b"#": - # ignores rest of the line; stops at CR, LF or EOF - while self.fp.read(1) not in b"\r\n": - pass - continue - token += c - if not token: - # Token was not even 1 byte - msg = "Reached EOF while reading header" - raise ValueError(msg) - elif len(token) > 10: - msg_too_long = b"Token too long in file header: %s" % token - raise ValueError(msg_too_long) - return token - - def _open(self) -> None: - assert self.fp is not None - - magic_number = self._read_magic() - try: - mode = MODES[magic_number] - except KeyError: - msg = "not a PPM file" - raise SyntaxError(msg) - self._mode = mode - - if magic_number in (b"P1", b"P4"): - self.custom_mimetype = "image/x-portable-bitmap" - elif magic_number in (b"P2", b"P5"): - self.custom_mimetype = "image/x-portable-graymap" - elif magic_number in (b"P3", b"P6"): - self.custom_mimetype = "image/x-portable-pixmap" - - self._size = int(self._read_token()), int(self._read_token()) - - decoder_name = "raw" - if magic_number in (b"P1", b"P2", b"P3"): - decoder_name = "ppm_plain" - - args: str | tuple[str | int, ...] - if mode == "1": - args = "1;I" - elif mode == "F": - scale = float(self._read_token()) - if scale == 0.0 or not math.isfinite(scale): - msg = "scale must be finite and non-zero" - raise ValueError(msg) - self.info["scale"] = abs(scale) - - rawmode = "F;32F" if scale < 0 else "F;32BF" - args = (rawmode, 0, -1) - else: - maxval = int(self._read_token()) - if not 0 < maxval < 65536: - msg = "maxval must be greater than 0 and less than 65536" - raise ValueError(msg) - if maxval > 255 and mode == "L": - self._mode = "I" - - rawmode = mode - if decoder_name != "ppm_plain": - # If maxval matches a bit depth, use the raw decoder directly - if maxval == 65535 and mode == "L": - rawmode = "I;16B" - elif maxval != 255: - decoder_name = "ppm" - - args = rawmode if decoder_name == "raw" else (rawmode, maxval) - self.tile = [ - ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args) - ] - - -# -# -------------------------------------------------------------------- - - -class PpmPlainDecoder(ImageFile.PyDecoder): - _pulls_fd = True - _comment_spans: bool - - def _read_block(self) -> bytes: - assert self.fd is not None - - return self.fd.read(ImageFile.SAFEBLOCK) - - def _find_comment_end(self, block: bytes, start: int = 0) -> int: - a = block.find(b"\n", start) - b = block.find(b"\r", start) - return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) - - def _ignore_comments(self, block: bytes) -> bytes: - if self._comment_spans: - # Finish current comment - while block: - comment_end = self._find_comment_end(block) - if comment_end != -1: - # Comment ends in this block - # Delete tail of comment - block = block[comment_end + 1 :] - break - else: - # Comment spans whole block - # So read the next block, looking for the end - block = self._read_block() - - # Search for any further comments - self._comment_spans = False - while True: - comment_start = block.find(b"#") - if comment_start == -1: - # No comment found - break - comment_end = self._find_comment_end(block, comment_start) - if comment_end != -1: - # Comment ends in this block - # Delete comment - block = block[:comment_start] + block[comment_end + 1 :] - else: - # Comment continues to next block(s) - block = block[:comment_start] - self._comment_spans = True - break - return block - - def _decode_bitonal(self) -> bytearray: - """ - This is a separate method because in the plain PBM format, all data tokens are - exactly one byte, so the inter-token whitespace is optional. - """ - data = bytearray() - total_bytes = self.state.xsize * self.state.ysize - - while len(data) != total_bytes: - block = self._read_block() # read next block - if not block: - # eof - break - - block = self._ignore_comments(block) - - tokens = b"".join(block.split()) - for token in tokens: - if token not in (48, 49): - msg = b"Invalid token for this mode: %s" % bytes([token]) - raise ValueError(msg) - data = (data + tokens)[:total_bytes] - invert = bytes.maketrans(b"01", b"\xff\x00") - return data.translate(invert) - - def _decode_blocks(self, maxval: int) -> bytearray: - data = bytearray() - max_len = 10 - out_byte_count = 4 if self.mode == "I" else 1 - out_max = 65535 if self.mode == "I" else 255 - bands = Image.getmodebands(self.mode) - total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count - - half_token = b"" - while len(data) != total_bytes: - block = self._read_block() # read next block - if not block: - if half_token: - block = bytearray(b" ") # flush half_token - else: - # eof - break - - block = self._ignore_comments(block) - - if half_token: - block = half_token + block # stitch half_token to new block - half_token = b"" - - tokens = block.split() - - if block and not block[-1:].isspace(): # block might split token - half_token = tokens.pop() # save half token for later - if len(half_token) > max_len: # prevent buildup of half_token - msg = ( - b"Token too long found in data: %s" % half_token[: max_len + 1] - ) - raise ValueError(msg) - - for token in tokens: - if len(token) > max_len: - msg = b"Token too long found in data: %s" % token[: max_len + 1] - raise ValueError(msg) - value = int(token) - if value < 0: - msg_str = f"Channel value is negative: {value}" - raise ValueError(msg_str) - if value > maxval: - msg_str = f"Channel value too large for this mode: {value}" - raise ValueError(msg_str) - value = round(value / maxval * out_max) - data += o32(value) if self.mode == "I" else o8(value) - if len(data) == total_bytes: # finished! - break - return data - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - self._comment_spans = False - if self.mode == "1": - data = self._decode_bitonal() - rawmode = "1;8" - else: - maxval = self.args[-1] - data = self._decode_blocks(maxval) - rawmode = "I;32" if self.mode == "I" else self.mode - self.set_as_raw(bytes(data), rawmode) - return -1, 0 - - -class PpmDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - - data = bytearray() - maxval = self.args[-1] - in_byte_count = 1 if maxval < 256 else 2 - out_byte_count = 4 if self.mode == "I" else 1 - out_max = 65535 if self.mode == "I" else 255 - bands = Image.getmodebands(self.mode) - dest_length = self.state.xsize * self.state.ysize * bands * out_byte_count - while len(data) < dest_length: - pixels = self.fd.read(in_byte_count * bands) - if len(pixels) < in_byte_count * bands: - # eof - break - for b in range(bands): - value = ( - pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count) - ) - value = min(out_max, round(value / maxval * out_max)) - data += o32(value) if self.mode == "I" else o8(value) - rawmode = "I;32" if self.mode == "I" else self.mode - self.set_as_raw(bytes(data), rawmode) - return -1, 0 - - -# -# -------------------------------------------------------------------- - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode == "1": - rawmode, head = "1;I", b"P4" - elif im.mode == "L": - rawmode, head = "L", b"P5" - elif im.mode in ("I", "I;16"): - rawmode, head = "I;16B", b"P5" - elif im.mode in ("RGB", "RGBA"): - rawmode, head = "RGB", b"P6" - elif im.mode == "F": - rawmode, head = "F;32F", b"Pf" - else: - msg = f"cannot write mode {im.mode} as PPM" - raise OSError(msg) - fp.write(head + b"\n%d %d\n" % im.size) - if head == b"P6": - fp.write(b"255\n") - elif head == b"P5": - if rawmode == "L": - fp.write(b"255\n") - else: - fp.write(b"65535\n") - elif head == b"Pf": - fp.write(b"-1.0\n") - row_order = -1 if im.mode == "F" else 1 - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))] - ) - - -# -# -------------------------------------------------------------------- - - -Image.register_open(PpmImageFile.format, PpmImageFile, _accept) -Image.register_save(PpmImageFile.format, _save) - -Image.register_decoder("ppm", PpmDecoder) -Image.register_decoder("ppm_plain", PpmPlainDecoder) - -Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"]) - -Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/PsdImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/PsdImagePlugin.py deleted file mode 100644 index dd3d5ab..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/PsdImagePlugin.py +++ /dev/null @@ -1,337 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# Adobe PSD 2.5/3.0 file handling -# -# History: -# 1995-09-01 fl Created -# 1997-01-03 fl Read most PSD images -# 1997-01-18 fl Fixed P and CMYK support -# 2001-10-21 fl Added seek/tell support (for layers) -# -# Copyright (c) 1997-2001 by Secret Labs AB. -# Copyright (c) 1995-2001 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -from functools import cached_property -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i8 -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import si16be as si16 -from ._binary import si32be as si32 -from ._util import DeferredError - -MODES = { - # (photoshop mode, bits) -> (pil mode, required channels) - (0, 1): ("1", 1), - (0, 8): ("L", 1), - (1, 8): ("L", 1), - (2, 8): ("P", 1), - (3, 8): ("RGB", 3), - (4, 8): ("CMYK", 4), - (7, 8): ("L", 1), # FIXME: multilayer - (8, 8): ("L", 1), # duotone - (9, 8): ("LAB", 3), -} - - -# --------------------------------------------------------------------. -# read PSD images - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"8BPS") - - -## -# Image plugin for Photoshop images. - - -class PsdImageFile(ImageFile.ImageFile): - format = "PSD" - format_description = "Adobe Photoshop" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - assert self.fp is not None - read = self.fp.read - - # - # header - - s = read(26) - if not _accept(s) or i16(s, 4) != 1: - msg = "not a PSD file" - raise SyntaxError(msg) - - psd_bits = i16(s, 22) - psd_channels = i16(s, 12) - psd_mode = i16(s, 24) - - mode, channels = MODES[(psd_mode, psd_bits)] - - if channels > psd_channels: - msg = "not enough channels" - raise OSError(msg) - if mode == "RGB" and psd_channels == 4: - mode = "RGBA" - channels = 4 - - self._mode = mode - self._size = i32(s, 18), i32(s, 14) - - # - # color mode data - - size = i32(read(4)) - if size: - data = read(size) - if mode == "P" and size == 768: - self.palette = ImagePalette.raw("RGB;L", data) - - # - # image resources - - self.resources = [] - - size = i32(read(4)) - if size: - # load resources - end = self.fp.tell() + size - while self.fp.tell() < end: - read(4) # signature - id = i16(read(2)) - name = read(i8(read(1))) - if not (len(name) & 1): - read(1) # padding - data = read(i32(read(4))) - if len(data) & 1: - read(1) # padding - self.resources.append((id, name, data)) - if id == 1039: # ICC profile - self.info["icc_profile"] = data - - # - # layer and mask information - - self._layers_position = None - - size = i32(read(4)) - if size: - end = self.fp.tell() + size - size = i32(read(4)) - if size: - self._layers_position = self.fp.tell() - self._layers_size = size - self.fp.seek(end) - self._n_frames: int | None = None - - # - # image descriptor - - self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels) - - # keep the file open - self._fp = self.fp - self.frame = 1 - self._min_frame = 1 - - @cached_property - def layers( - self, - ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: - layers = [] - if self._layers_position is not None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self._fp.seek(self._layers_position) - _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size)) - layers = _layerinfo(_layer_data, self._layers_size) - self._n_frames = len(layers) - return layers - - @property - def n_frames(self) -> int: - if self._n_frames is None: - self._n_frames = len(self.layers) - return self._n_frames - - @property - def is_animated(self) -> bool: - return len(self.layers) > 1 - - def seek(self, layer: int) -> None: - if not self._seek_check(layer): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - - # seek to given layer (1..max) - if layer > len(self.layers): - msg = "no more images in PSD file" - raise EOFError(msg) - _, mode, _, tile = self.layers[layer - 1] - self._mode = mode - self.tile = tile - self.frame = layer - self.fp = self._fp - - def tell(self) -> int: - # return layer number (0=image, 1..max=layers) - return self.frame - - -def _layerinfo( - fp: IO[bytes], ct_bytes: int -) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: - # read layerinfo block - layers = [] - - def read(size: int) -> bytes: - return ImageFile._safe_read(fp, size) - - ct = si16(read(2)) - - # sanity check - if ct_bytes < (abs(ct) * 20): - msg = "Layer block too short for number of layers requested" - raise SyntaxError(msg) - - for _ in range(abs(ct)): - # bounding box - y0 = si32(read(4)) - x0 = si32(read(4)) - y1 = si32(read(4)) - x1 = si32(read(4)) - - # image info - bands = [] - ct_types = i16(read(2)) - if ct_types > 4: - fp.seek(ct_types * 6 + 12, io.SEEK_CUR) - size = i32(read(4)) - fp.seek(size, io.SEEK_CUR) - continue - - for _ in range(ct_types): - type = i16(read(2)) - - if type == 65535: - b = "A" - else: - b = "RGBA"[type] - - bands.append(b) - read(4) # size - - # figure out the image mode - bands.sort() - if bands == ["R"]: - mode = "L" - elif bands == ["B", "G", "R"]: - mode = "RGB" - elif bands == ["A", "B", "G", "R"]: - mode = "RGBA" - else: - mode = "" # unknown - - # skip over blend flags and extra information - read(12) # filler - name = "" - size = i32(read(4)) # length of the extra data field - if size: - data_end = fp.tell() + size - - length = i32(read(4)) - if length: - fp.seek(length - 16, io.SEEK_CUR) - - length = i32(read(4)) - if length: - fp.seek(length, io.SEEK_CUR) - - length = i8(read(1)) - if length: - # Don't know the proper encoding, - # Latin-1 should be a good guess - name = read(length).decode("latin-1", "replace") - - fp.seek(data_end) - layers.append((name, mode, (x0, y0, x1, y1))) - - # get tiles - layerinfo = [] - for i, (name, mode, bbox) in enumerate(layers): - tile = [] - for m in mode: - t = _maketile(fp, m, bbox, 1) - if t: - tile.extend(t) - layerinfo.append((name, mode, bbox, tile)) - - return layerinfo - - -def _maketile( - file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int -) -> list[ImageFile._Tile]: - tiles = [] - read = file.read - - compression = i16(read(2)) - - xsize = bbox[2] - bbox[0] - ysize = bbox[3] - bbox[1] - - offset = file.tell() - - if compression == 0: - # - # raw compression - for channel in range(channels): - layer = mode[channel] - if mode == "CMYK": - layer += ";I" - tiles.append(ImageFile._Tile("raw", bbox, offset, layer)) - offset = offset + xsize * ysize - - elif compression == 1: - # - # packbits compression - i = 0 - bytecount = read(channels * ysize * 2) - offset = file.tell() - for channel in range(channels): - layer = mode[channel] - if mode == "CMYK": - layer += ";I" - tiles.append(ImageFile._Tile("packbits", bbox, offset, layer)) - for y in range(ysize): - offset = offset + i16(bytecount, i) - i += 2 - - file.seek(offset) - - if offset & 1: - read(1) # padding - - return tiles - - -# -------------------------------------------------------------------- -# registry - - -Image.register_open(PsdImageFile.format, PsdImageFile, _accept) - -Image.register_extension(PsdImageFile.format, ".psd") - -Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/QoiImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/QoiImagePlugin.py deleted file mode 100644 index d0709b1..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/QoiImagePlugin.py +++ /dev/null @@ -1,235 +0,0 @@ -# -# The Python Imaging Library. -# -# QOI support for PIL -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -from typing import IO - -from . import Image, ImageFile -from ._binary import i32be as i32 -from ._binary import o8 -from ._binary import o32be as o32 - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"qoif") - - -class QoiImageFile(ImageFile.ImageFile): - format = "QOI" - format_description = "Quite OK Image" - - def _open(self) -> None: - assert self.fp is not None - if not _accept(self.fp.read(4)): - msg = "not a QOI file" - raise SyntaxError(msg) - - self._size = i32(self.fp.read(4)), i32(self.fp.read(4)) - - channels = self.fp.read(1)[0] - self._mode = "RGB" if channels == 3 else "RGBA" - - self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())] - - -class QoiDecoder(ImageFile.PyDecoder): - _pulls_fd = True - _previous_pixel: bytes | bytearray | None = None - _previously_seen_pixels: dict[int, bytes | bytearray] = {} - - def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: - self._previous_pixel = value - - r, g, b, a = value - hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 - self._previously_seen_pixels[hash_value] = value - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - - self._previously_seen_pixels = {} - self._previous_pixel = bytearray((0, 0, 0, 255)) - - data = bytearray() - bands = Image.getmodebands(self.mode) - dest_length = self.state.xsize * self.state.ysize * bands - while len(data) < dest_length: - byte = self.fd.read(1)[0] - value: bytes | bytearray - if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB - value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] - elif byte == 0b11111111: # QOI_OP_RGBA - value = self.fd.read(4) - else: - op = byte >> 6 - if op == 0: # QOI_OP_INDEX - op_index = byte & 0b00111111 - value = self._previously_seen_pixels.get( - op_index, bytearray((0, 0, 0, 0)) - ) - elif op == 1 and self._previous_pixel: # QOI_OP_DIFF - value = bytearray( - ( - (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) - % 256, - (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2) - % 256, - (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256, - self._previous_pixel[3], - ) - ) - elif op == 2 and self._previous_pixel: # QOI_OP_LUMA - second_byte = self.fd.read(1)[0] - diff_green = (byte & 0b00111111) - 32 - diff_red = ((second_byte & 0b11110000) >> 4) - 8 - diff_blue = (second_byte & 0b00001111) - 8 - - value = bytearray( - tuple( - (self._previous_pixel[i] + diff_green + diff) % 256 - for i, diff in enumerate((diff_red, 0, diff_blue)) - ) - ) - value += self._previous_pixel[3:] - elif op == 3 and self._previous_pixel: # QOI_OP_RUN - run_length = (byte & 0b00111111) + 1 - value = self._previous_pixel - if bands == 3: - value = value[:3] - data += value * run_length - continue - self._add_to_previous_pixels(value) - - if bands == 3: - value = value[:3] - data += value - self.set_as_raw(data) - return -1, 0 - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode == "RGB": - channels = 3 - elif im.mode == "RGBA": - channels = 4 - else: - msg = "Unsupported QOI image mode" - raise ValueError(msg) - - colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1 - - fp.write(b"qoif") - fp.write(o32(im.size[0])) - fp.write(o32(im.size[1])) - fp.write(o8(channels)) - fp.write(o8(colorspace)) - - ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)]) - - -class QoiEncoder(ImageFile.PyEncoder): - _pushes_fd = True - _previous_pixel: tuple[int, int, int, int] | None = None - _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {} - _run = 0 - - def _write_run(self) -> bytes: - data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN - self._run = 0 - return data - - def _delta(self, left: int, right: int) -> int: - result = (left - right) & 255 - if result >= 128: - result -= 256 - return result - - def encode(self, bufsize: int) -> tuple[int, int, bytes]: - assert self.im is not None - - self._previously_seen_pixels = {0: (0, 0, 0, 0)} - self._previous_pixel = (0, 0, 0, 255) - - data = bytearray() - w, h = self.im.size - bands = Image.getmodebands(self.mode) - - for y in range(h): - for x in range(w): - pixel = self.im.getpixel((x, y)) - if bands == 3: - pixel = (*pixel, 255) - - if pixel == self._previous_pixel: - self._run += 1 - if self._run == 62: - data += self._write_run() - else: - if self._run: - data += self._write_run() - - r, g, b, a = pixel - hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 - if self._previously_seen_pixels.get(hash_value) == pixel: - data += o8(hash_value) # QOI_OP_INDEX - elif self._previous_pixel: - self._previously_seen_pixels[hash_value] = pixel - - prev_r, prev_g, prev_b, prev_a = self._previous_pixel - if prev_a == a: - delta_r = self._delta(r, prev_r) - delta_g = self._delta(g, prev_g) - delta_b = self._delta(b, prev_b) - - if ( - -2 <= delta_r < 2 - and -2 <= delta_g < 2 - and -2 <= delta_b < 2 - ): - data += o8( - 0b01000000 - | (delta_r + 2) << 4 - | (delta_g + 2) << 2 - | (delta_b + 2) - ) # QOI_OP_DIFF - else: - delta_gr = self._delta(delta_r, delta_g) - delta_gb = self._delta(delta_b, delta_g) - if ( - -8 <= delta_gr < 8 - and -32 <= delta_g < 32 - and -8 <= delta_gb < 8 - ): - data += o8( - 0b10000000 | (delta_g + 32) - ) # QOI_OP_LUMA - data += o8((delta_gr + 8) << 4 | (delta_gb + 8)) - else: - data += o8(0b11111110) # QOI_OP_RGB - data += bytes(pixel[:3]) - else: - data += o8(0b11111111) # QOI_OP_RGBA - data += bytes(pixel) - - self._previous_pixel = pixel - - if self._run: - data += self._write_run() - data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding - - return len(data), 0, data - - -Image.register_open(QoiImageFile.format, QoiImageFile, _accept) -Image.register_decoder("qoi", QoiDecoder) -Image.register_extension(QoiImageFile.format, ".qoi") - -Image.register_save(QoiImageFile.format, _save) -Image.register_encoder("qoi", QoiEncoder) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/SgiImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/SgiImagePlugin.py deleted file mode 100644 index 8530221..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/SgiImagePlugin.py +++ /dev/null @@ -1,231 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# SGI image file handling -# -# See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli. -# <ftp://ftp.sgi.com/graphics/SGIIMAGESPEC> -# -# -# History: -# 2017-22-07 mb Add RLE decompression -# 2016-16-10 mb Add save method without compression -# 1995-09-10 fl Created -# -# Copyright (c) 2016 by Mickael Bonfill. -# Copyright (c) 2008 by Karsten Hiddemann. -# Copyright (c) 1997 by Secret Labs AB. -# Copyright (c) 1995 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -import struct -from typing import IO - -from . import Image, ImageFile -from ._binary import i16be as i16 -from ._binary import o8 - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 2 and i16(prefix) == 474 - - -MODES = { - (1, 1, 1): "L", - (1, 2, 1): "L", - (2, 1, 1): "L;16B", - (2, 2, 1): "L;16B", - (1, 3, 3): "RGB", - (2, 3, 3): "RGB;16B", - (1, 3, 4): "RGBA", - (2, 3, 4): "RGBA;16B", -} - - -## -# Image plugin for SGI images. -class SgiImageFile(ImageFile.ImageFile): - format = "SGI" - format_description = "SGI Image File Format" - - def _open(self) -> None: - # HEAD - assert self.fp is not None - - headlen = 512 - s = self.fp.read(headlen) - - if not _accept(s): - msg = "Not an SGI image file" - raise ValueError(msg) - - # compression : verbatim or RLE - compression = s[2] - - # bpc : 1 or 2 bytes (8bits or 16bits) - bpc = s[3] - - # dimension : 1, 2 or 3 (depending on xsize, ysize and zsize) - dimension = i16(s, 4) - - # xsize : width - xsize = i16(s, 6) - - # ysize : height - ysize = i16(s, 8) - - # zsize : channels count - zsize = i16(s, 10) - - # determine mode from bits/zsize - try: - rawmode = MODES[(bpc, dimension, zsize)] - except KeyError: - msg = "Unsupported SGI image mode" - raise ValueError(msg) - - self._size = xsize, ysize - self._mode = rawmode.split(";")[0] - if self.mode == "RGB": - self.custom_mimetype = "image/rgb" - - # orientation -1 : scanlines begins at the bottom-left corner - orientation = -1 - - # decoder info - if compression == 0: - pagesize = xsize * ysize * bpc - if bpc == 2: - self.tile = [ - ImageFile._Tile( - "SGI16", - (0, 0) + self.size, - headlen, - (self.mode, 0, orientation), - ) - ] - else: - self.tile = [] - offset = headlen - for layer in self.mode: - self.tile.append( - ImageFile._Tile( - "raw", (0, 0) + self.size, offset, (layer, 0, orientation) - ) - ) - offset += pagesize - elif compression == 1: - self.tile = [ - ImageFile._Tile( - "sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc) - ) - ] - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode not in {"RGB", "RGBA", "L"}: - msg = "Unsupported SGI image mode" - raise ValueError(msg) - - # Get the keyword arguments - info = im.encoderinfo - - # Byte-per-pixel precision, 1 = 8bits per pixel - bpc = info.get("bpc", 1) - - if bpc not in (1, 2): - msg = "Unsupported number of bytes per pixel" - raise ValueError(msg) - - # Flip the image, since the origin of SGI file is the bottom-left corner - orientation = -1 - # Define the file as SGI File Format - magic_number = 474 - # Run-Length Encoding Compression - Unsupported at this time - rle = 0 - - # X Dimension = width / Y Dimension = height - x, y = im.size - # Z Dimension: Number of channels - z = len(im.mode) - # Number of dimensions (x,y,z) - if im.mode == "L": - dimension = 1 if y == 1 else 2 - else: - dimension = 3 - - # Minimum Byte value - pinmin = 0 - # Maximum Byte value (255 = 8bits per pixel) - pinmax = 255 - # Image name (79 characters max, truncated below in write) - img_name = os.path.splitext(os.path.basename(filename))[0] - if isinstance(img_name, str): - img_name = img_name.encode("ascii", "ignore") - # Standard representation of pixel in the file - colormap = 0 - fp.write(struct.pack(">h", magic_number)) - fp.write(o8(rle)) - fp.write(o8(bpc)) - fp.write(struct.pack(">H", dimension)) - fp.write(struct.pack(">H", x)) - fp.write(struct.pack(">H", y)) - fp.write(struct.pack(">H", z)) - fp.write(struct.pack(">l", pinmin)) - fp.write(struct.pack(">l", pinmax)) - fp.write(struct.pack("4s", b"")) # dummy - fp.write(struct.pack("79s", img_name)) # truncates to 79 chars - fp.write(struct.pack("s", b"")) # force null byte after img_name - fp.write(struct.pack(">l", colormap)) - fp.write(struct.pack("404s", b"")) # dummy - - rawmode = "L" - if bpc == 2: - rawmode = "L;16B" - - for channel in im.split(): - fp.write(channel.tobytes("raw", rawmode, 0, orientation)) - - if hasattr(fp, "flush"): - fp.flush() - - -class SGI16Decoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - assert self.im is not None - - rawmode, stride, orientation = self.args - pagesize = self.state.xsize * self.state.ysize - zsize = len(self.mode) - self.fd.seek(512) - - for band in range(zsize): - channel = Image.new("L", (self.state.xsize, self.state.ysize)) - channel.frombytes( - self.fd.read(2 * pagesize), "raw", "L;16B", stride, orientation - ) - self.im.putband(channel.im, band) - - return -1, 0 - - -# -# registry - - -Image.register_decoder("SGI16", SGI16Decoder) -Image.register_open(SgiImageFile.format, SgiImageFile, _accept) -Image.register_save(SgiImageFile.format, _save) -Image.register_mime(SgiImageFile.format, "image/sgi") - -Image.register_extensions(SgiImageFile.format, [".bw", ".rgb", ".rgba", ".sgi"]) - -# End of file diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/SpiderImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/SpiderImagePlugin.py deleted file mode 100644 index 11d9069..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/SpiderImagePlugin.py +++ /dev/null @@ -1,332 +0,0 @@ -# -# The Python Imaging Library. -# -# SPIDER image file handling -# -# History: -# 2004-08-02 Created BB -# 2006-03-02 added save method -# 2006-03-13 added support for stack images -# -# Copyright (c) 2004 by Health Research Inc. (HRI) RENSSELAER, NY 12144. -# Copyright (c) 2004 by William Baxter. -# Copyright (c) 2004 by Secret Labs AB. -# Copyright (c) 2004 by Fredrik Lundh. -# - -## -# Image plugin for the Spider image format. This format is used -# by the SPIDER software, in processing image data from electron -# microscopy and tomography. -## - -# -# SpiderImagePlugin.py -# -# The Spider image format is used by SPIDER software, in processing -# image data from electron microscopy and tomography. -# -# Spider home page: -# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html -# -# Details about the Spider image format: -# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html -# -from __future__ import annotations - -import os -import struct -import sys -from typing import IO, Any, cast - -from . import Image, ImageFile -from ._util import DeferredError - -TYPE_CHECKING = False - - -def isInt(f: Any) -> int: - try: - i = int(f) - if f - i == 0: - return 1 - else: - return 0 - except (ValueError, OverflowError): - return 0 - - -iforms = [1, 3, -11, -12, -21, -22] - - -# There is no magic number to identify Spider files, so just check a -# series of header locations to see if they have reasonable values. -# Returns no. of bytes in the header, if it is a valid Spider header, -# otherwise returns 0 - - -def isSpiderHeader(t: tuple[float, ...]) -> int: - h = (99,) + t # add 1 value so can use spider header index start=1 - # header values 1,2,5,12,13,22,23 should be integers - for i in [1, 2, 5, 12, 13, 22, 23]: - if not isInt(h[i]): - return 0 - # check iform - iform = int(h[5]) - if iform not in iforms: - return 0 - # check other header values - labrec = int(h[13]) # no. records in file header - labbyt = int(h[22]) # total no. of bytes in header - lenbyt = int(h[23]) # record length in bytes - if labbyt != (labrec * lenbyt): - return 0 - # looks like a valid header - return labbyt - - -def isSpiderImage(filename: str) -> int: - with open(filename, "rb") as fp: - f = fp.read(92) # read 23 * 4 bytes - t = struct.unpack(">23f", f) # try big-endian first - hdrlen = isSpiderHeader(t) - if hdrlen == 0: - t = struct.unpack("<23f", f) # little-endian - hdrlen = isSpiderHeader(t) - return hdrlen - - -class SpiderImageFile(ImageFile.ImageFile): - format = "SPIDER" - format_description = "Spider 2D image" - _close_exclusive_fp_after_loading = False - - def _open(self) -> None: - # check header - n = 27 * 4 # read 27 float values - assert self.fp is not None - f = self.fp.read(n) - - try: - self.bigendian = 1 - t = struct.unpack(">27f", f) # try big-endian first - hdrlen = isSpiderHeader(t) - if hdrlen == 0: - self.bigendian = 0 - t = struct.unpack("<27f", f) # little-endian - hdrlen = isSpiderHeader(t) - if hdrlen == 0: - msg = "not a valid Spider file" - raise SyntaxError(msg) - except struct.error as e: - msg = "not a valid Spider file" - raise SyntaxError(msg) from e - - h = (99,) + t # add 1 value : spider header index starts at 1 - iform = int(h[5]) - if iform != 1: - msg = "not a Spider 2D image" - raise SyntaxError(msg) - - self._size = int(h[12]), int(h[2]) # size in pixels (width, height) - self.istack = int(h[24]) - self.imgnumber = int(h[27]) - - if self.istack == 0 and self.imgnumber == 0: - # stk=0, img=0: a regular 2D image - offset = hdrlen - self._nimages = 1 - elif self.istack > 0 and self.imgnumber == 0: - # stk>0, img=0: Opening the stack for the first time - self.imgbytes = int(h[12]) * int(h[2]) * 4 - self.hdrlen = hdrlen - self._nimages = int(h[26]) - # Point to the first image in the stack - offset = hdrlen * 2 - self.imgnumber = 1 - elif self.istack == 0 and self.imgnumber > 0: - # stk=0, img>0: an image within the stack - offset = hdrlen + self.stkoffset - self.istack = 2 # So Image knows it's still a stack - else: - msg = "inconsistent stack header values" - raise SyntaxError(msg) - - if self.bigendian: - self.rawmode = "F;32BF" - else: - self.rawmode = "F;32F" - self._mode = "F" - - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)] - self._fp = self.fp # FIXME: hack - - @property - def n_frames(self) -> int: - return self._nimages - - @property - def is_animated(self) -> bool: - return self._nimages > 1 - - # 1st image index is zero (although SPIDER imgnumber starts at 1) - def tell(self) -> int: - if self.imgnumber < 1: - return 0 - else: - return self.imgnumber - 1 - - def seek(self, frame: int) -> None: - if self.istack == 0: - msg = "attempt to seek in a non-stack file" - raise EOFError(msg) - if not self._seek_check(frame): - return - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) - self.fp = self._fp - self.fp.seek(self.stkoffset) - self._open() - - # returns a byte image after rescaling to 0..255 - def convert2byte(self, depth: int = 255) -> Image.Image: - extrema = self.getextrema() - assert isinstance(extrema[0], float) - minimum, maximum = cast(tuple[float, float], extrema) - m: float = 1 - if maximum != minimum: - m = depth / (maximum - minimum) - b = -m * minimum - return self.point(lambda i: i * m + b).convert("L") - - if TYPE_CHECKING: - from . import ImageTk - - # returns a ImageTk.PhotoImage object, after rescaling to 0..255 - def tkPhotoImage(self) -> ImageTk.PhotoImage: - from . import ImageTk - - return ImageTk.PhotoImage(self.convert2byte(), palette=256) - - -# -------------------------------------------------------------------- -# Image series - - -# given a list of filenames, return a list of images -def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None: - """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" - if filelist is None or len(filelist) < 1: - return None - - byte_imgs = [] - for img in filelist: - if not os.path.exists(img): - print(f"unable to find {img}") - continue - try: - with Image.open(img) as im: - assert isinstance(im, SpiderImageFile) - byte_im = im.convert2byte() - except Exception: - if not isSpiderImage(img): - print(f"{img} is not a Spider image file") - continue - byte_im.info["filename"] = img - byte_imgs.append(byte_im) - return byte_imgs - - -# -------------------------------------------------------------------- -# For saving images in Spider format - - -def makeSpiderHeader(im: Image.Image) -> list[bytes]: - nsam, nrow = im.size - lenbyt = max(1, nsam) * 4 # There are labrec records in the header - labrec = int(1024 / lenbyt) - if 1024 % lenbyt != 0: - labrec += 1 - labbyt = labrec * lenbyt - nvalues = int(labbyt / 4) - if nvalues < 23: - return [] - - hdr = [0.0] * nvalues - - # NB these are Fortran indices - hdr[1] = 1.0 # nslice (=1 for an image) - hdr[2] = float(nrow) # number of rows per slice - hdr[3] = float(nrow) # number of records in the image - hdr[5] = 1.0 # iform for 2D image - hdr[12] = float(nsam) # number of pixels per line - hdr[13] = float(labrec) # number of records in file header - hdr[22] = float(labbyt) # total number of bytes in header - hdr[23] = float(lenbyt) # record length in bytes - - # adjust for Fortran indexing - hdr = hdr[1:] - hdr.append(0.0) - # pack binary data into a string - return [struct.pack("f", v) for v in hdr] - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode != "F": - im = im.convert("F") - - hdr = makeSpiderHeader(im) - if len(hdr) < 256: - msg = "Error creating Spider header" - raise OSError(msg) - - # write the SPIDER header - fp.writelines(hdr) - - rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) - - -def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # get the filename extension and register it with Image - if filename_ext := os.path.splitext(filename)[1]: - ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext - Image.register_extension(SpiderImageFile.format, ext) - _save(im, fp, filename) - - -# -------------------------------------------------------------------- - - -Image.register_open(SpiderImageFile.format, SpiderImageFile) -Image.register_save(SpiderImageFile.format, _save_spider) - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") - sys.exit() - - filename = sys.argv[1] - if not isSpiderImage(filename): - print("input image must be in Spider format") - sys.exit() - - with Image.open(filename) as im: - print(f"image: {im}") - print(f"format: {im.format}") - print(f"size: {im.size}") - print(f"mode: {im.mode}") - print("max, min: ", end=" ") - print(im.getextrema()) - - if len(sys.argv) > 2: - outfile = sys.argv[2] - - # perform some image operation - transposed_im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - print( - f"saving a flipped version of {os.path.basename(filename)} " - f"as {outfile} " - ) - transposed_im.save(outfile, SpiderImageFile.format) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/SunImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/SunImagePlugin.py deleted file mode 100644 index 8912379..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/SunImagePlugin.py +++ /dev/null @@ -1,145 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Sun image file handling -# -# History: -# 1995-09-10 fl Created -# 1996-05-28 fl Fixed 32-bit alignment -# 1998-12-29 fl Import ImagePalette module -# 2001-12-18 fl Fixed palette loading (from Jean-Claude Rimbault) -# -# Copyright (c) 1997-2001 by Secret Labs AB -# Copyright (c) 1995-1996 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -from . import Image, ImageFile, ImagePalette -from ._binary import i32be as i32 - - -def _accept(prefix: bytes) -> bool: - return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 - - -## -# Image plugin for Sun raster files. - - -class SunImageFile(ImageFile.ImageFile): - format = "SUN" - format_description = "Sun Raster File" - - def _open(self) -> None: - # The Sun Raster file header is 32 bytes in length - # and has the following format: - - # typedef struct _SunRaster - # { - # DWORD MagicNumber; /* Magic (identification) number */ - # DWORD Width; /* Width of image in pixels */ - # DWORD Height; /* Height of image in pixels */ - # DWORD Depth; /* Number of bits per pixel */ - # DWORD Length; /* Size of image data in bytes */ - # DWORD Type; /* Type of raster file */ - # DWORD ColorMapType; /* Type of color map */ - # DWORD ColorMapLength; /* Size of the color map in bytes */ - # } SUNRASTER; - - assert self.fp is not None - - # HEAD - s = self.fp.read(32) - if not _accept(s): - msg = "not an SUN raster file" - raise SyntaxError(msg) - - offset = 32 - - self._size = i32(s, 4), i32(s, 8) - - depth = i32(s, 12) - # data_length = i32(s, 16) # unreliable, ignore. - file_type = i32(s, 20) - palette_type = i32(s, 24) # 0: None, 1: RGB, 2: Raw/arbitrary - palette_length = i32(s, 28) - - if depth == 1: - self._mode, rawmode = "1", "1;I" - elif depth == 4: - self._mode, rawmode = "L", "L;4" - elif depth == 8: - self._mode = rawmode = "L" - elif depth == 24: - if file_type == 3: - self._mode, rawmode = "RGB", "RGB" - else: - self._mode, rawmode = "RGB", "BGR" - elif depth == 32: - if file_type == 3: - self._mode, rawmode = "RGB", "RGBX" - else: - self._mode, rawmode = "RGB", "BGRX" - else: - msg = "Unsupported Mode/Bit Depth" - raise SyntaxError(msg) - - if palette_length: - if palette_length > 1024: - msg = "Unsupported Color Palette Length" - raise SyntaxError(msg) - - if palette_type != 1: - msg = "Unsupported Palette Type" - raise SyntaxError(msg) - - offset = offset + palette_length - self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) - if self.mode == "L": - self._mode = "P" - rawmode = rawmode.replace("L", "P") - - # 16 bit boundaries on stride - stride = ((self.size[0] * depth + 15) // 16) * 2 - - # file type: Type is the version (or flavor) of the bitmap - # file. The following values are typically found in the Type - # field: - # 0000h Old - # 0001h Standard - # 0002h Byte-encoded - # 0003h RGB format - # 0004h TIFF format - # 0005h IFF format - # FFFFh Experimental - - # Old and standard are the same, except for the length tag. - # byte-encoded is run-length-encoded - # RGB looks similar to standard, but RGB byte order - # TIFF and IFF mean that they were converted from T/IFF - # Experimental means that it's something else. - # (https://www.fileformat.info/format/sunraster/egff.htm) - - if file_type in (0, 1, 3, 4, 5): - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride)) - ] - elif file_type == 2: - self.tile = [ - ImageFile._Tile("sun_rle", (0, 0) + self.size, offset, rawmode) - ] - else: - msg = "Unsupported Sun Raster file type" - raise SyntaxError(msg) - - -# -# registry - - -Image.register_open(SunImageFile.format, SunImageFile, _accept) - -Image.register_extension(SunImageFile.format, ".ras") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/TarIO.py b/presentation/.venv/lib/python3.12/site-packages/PIL/TarIO.py deleted file mode 100644 index 86490a4..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/TarIO.py +++ /dev/null @@ -1,61 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# read files from within a tar file -# -# History: -# 95-06-18 fl Created -# 96-05-28 fl Open files in binary mode -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1995-96. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io - -from . import ContainerIO - - -class TarIO(ContainerIO.ContainerIO[bytes]): - """A file object that provides read access to a given member of a TAR file.""" - - def __init__(self, tarfile: str, file: str) -> None: - """ - Create file object. - - :param tarfile: Name of TAR file. - :param file: Name of member file. - """ - self.fh = open(tarfile, "rb") - - while True: - s = self.fh.read(512) - if len(s) != 512: - self.fh.close() - - msg = "unexpected end of tar file" - raise OSError(msg) - - name = s[:100].decode("utf-8") - i = name.find("\0") - if i == 0: - self.fh.close() - - msg = "cannot find subfile" - raise OSError(msg) - if i > 0: - name = name[:i] - - size = int(s[124:135], 8) - - if file == name: - break - - self.fh.seek((size + 511) & (~511), io.SEEK_CUR) - - # Open region - super().__init__(self.fh, self.fh.tell(), size) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/TgaImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/TgaImagePlugin.py deleted file mode 100644 index b2989a4..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/TgaImagePlugin.py +++ /dev/null @@ -1,280 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# TGA file handling -# -# History: -# 95-09-01 fl created (reads 24-bit files only) -# 97-01-04 fl support more TGA versions, including compressed images -# 98-07-04 fl fixed orientation and alpha layer bugs -# 98-09-11 fl fixed orientation for runlength decoder -# -# Copyright (c) Secret Labs AB 1997-98. -# Copyright (c) Fredrik Lundh 1995-97. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import os -import warnings -from typing import IO - -from . import Image, ImageFile, ImagePalette -from ._binary import i16le as i16 -from ._binary import i32le as i32 -from ._binary import o8 -from ._binary import o16le as o16 - -# -# -------------------------------------------------------------------- -# Read RGA file - - -MODES = { - # map imagetype/depth to rawmode - (1, 8): "P", - (3, 1): "1", - (3, 8): "L", - (3, 16): "LA", - (2, 16): "BGRA;15Z", - (2, 24): "BGR", - (2, 32): "BGRA", -} - - -## -# Image plugin for Targa files. - - -class TgaImageFile(ImageFile.ImageFile): - format = "TGA" - format_description = "Targa" - - def _open(self) -> None: - # process header - assert self.fp is not None - - s = self.fp.read(18) - - id_len = s[0] - - colormaptype = s[1] - imagetype = s[2] - - depth = s[16] - - flags = s[17] - - self._size = i16(s, 12), i16(s, 14) - - # validate header fields - if ( - colormaptype not in (0, 1) - or self.size[0] <= 0 - or self.size[1] <= 0 - or depth not in (1, 8, 16, 24, 32) - ): - msg = "not a TGA file" - raise SyntaxError(msg) - - # image mode - if imagetype in (3, 11): - self._mode = "L" - if depth == 1: - self._mode = "1" # ??? - elif depth == 16: - self._mode = "LA" - elif imagetype in (1, 9): - self._mode = "P" if colormaptype else "L" - elif imagetype in (2, 10): - self._mode = "RGB" if depth == 24 else "RGBA" - else: - msg = "unknown TGA mode" - raise SyntaxError(msg) - - # orientation - orientation = flags & 0x30 - self._flip_horizontally = orientation in [0x10, 0x30] - if orientation in [0x20, 0x30]: - orientation = 1 - elif orientation in [0, 0x10]: - orientation = -1 - else: - msg = "unknown TGA orientation" - raise SyntaxError(msg) - - self.info["orientation"] = orientation - - if imagetype & 8: - self.info["compression"] = "tga_rle" - - if id_len: - self.info["id_section"] = self.fp.read(id_len) - - if colormaptype: - # read palette - start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] - if mapdepth == 16: - self.palette = ImagePalette.raw( - "BGRA;15Z", bytes(2 * start) + self.fp.read(2 * size) - ) - self.palette.mode = "RGBA" - elif mapdepth == 24: - self.palette = ImagePalette.raw( - "BGR", bytes(3 * start) + self.fp.read(3 * size) - ) - elif mapdepth == 32: - self.palette = ImagePalette.raw( - "BGRA", bytes(4 * start) + self.fp.read(4 * size) - ) - else: - msg = "unknown TGA map depth" - raise SyntaxError(msg) - - # setup tile descriptor - try: - rawmode = MODES[(imagetype & 7, depth)] - if imagetype & 8: - # compressed - self.tile = [ - ImageFile._Tile( - "tga_rle", - (0, 0) + self.size, - self.fp.tell(), - (rawmode, orientation, depth), - ) - ] - else: - self.tile = [ - ImageFile._Tile( - "raw", - (0, 0) + self.size, - self.fp.tell(), - (rawmode, 0, orientation), - ) - ] - except KeyError: - pass # cannot decode - - def load_end(self) -> None: - if self.mode == "RGBA": - assert self.fp is not None - self.fp.seek(-26, os.SEEK_END) - footer = self.fp.read(26) - if footer.endswith(b"TRUEVISION-XFILE.\x00"): - # version 2 - extension_offset = i32(footer) - if extension_offset: - self.fp.seek(extension_offset + 494) - attributes_type = self.fp.read(1) - if attributes_type == b"\x00": - # No alpha - self.im.fillband(3, 255) - - if self._flip_horizontally: - self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - - -# -# -------------------------------------------------------------------- -# Write TGA file - - -SAVE = { - "1": ("1", 1, 0, 3), - "L": ("L", 8, 0, 3), - "LA": ("LA", 16, 0, 3), - "P": ("P", 8, 1, 1), - "RGB": ("BGR", 24, 0, 2), - "RGBA": ("BGRA", 32, 0, 2), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - rawmode, bits, colormaptype, imagetype = SAVE[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as TGA" - raise OSError(msg) from e - - if "rle" in im.encoderinfo: - rle = im.encoderinfo["rle"] - else: - compression = im.encoderinfo.get("compression", im.info.get("compression")) - rle = compression == "tga_rle" - if rle: - imagetype += 8 - - id_section = im.encoderinfo.get("id_section", im.info.get("id_section", "")) - id_len = len(id_section) - if id_len > 255: - id_len = 255 - id_section = id_section[:255] - warnings.warn("id_section has been trimmed to 255 characters") - - if colormaptype: - palette = im.im.getpalette("RGB", "BGR") - colormaplength, colormapentry = len(palette) // 3, 24 - else: - colormaplength, colormapentry = 0, 0 - - if im.mode in ("LA", "RGBA"): - flags = 8 - else: - flags = 0 - - orientation = im.encoderinfo.get("orientation", im.info.get("orientation", -1)) - if orientation > 0: - flags = flags | 0x20 - - fp.write( - o8(id_len) - + o8(colormaptype) - + o8(imagetype) - + o16(0) # colormapfirst - + o16(colormaplength) - + o8(colormapentry) - + o16(0) - + o16(0) - + o16(im.size[0]) - + o16(im.size[1]) - + o8(bits) - + o8(flags) - ) - - if id_section: - fp.write(id_section) - - if colormaptype: - fp.write(palette) - - if rle: - ImageFile._save( - im, - fp, - [ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))], - ) - else: - ImageFile._save( - im, - fp, - [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))], - ) - - # write targa version 2 footer - fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000") - - -# -# -------------------------------------------------------------------- -# Registry - - -Image.register_open(TgaImageFile.format, TgaImageFile) -Image.register_save(TgaImageFile.format, _save) - -Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"]) - -Image.register_mime(TgaImageFile.format, "image/x-tga") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/TiffImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/TiffImagePlugin.py deleted file mode 100644 index 5094faa..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/TiffImagePlugin.py +++ /dev/null @@ -1,2353 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# TIFF file handling -# -# TIFF is a flexible, if somewhat aged, image file format originally -# defined by Aldus. Although TIFF supports a wide variety of pixel -# layouts and compression methods, the name doesn't really stand for -# "thousands of incompatible file formats," it just feels that way. -# -# To read TIFF data from a stream, the stream must be seekable. For -# progressive decoding, make sure to use TIFF files where the tag -# directory is placed first in the file. -# -# History: -# 1995-09-01 fl Created -# 1996-05-04 fl Handle JPEGTABLES tag -# 1996-05-18 fl Fixed COLORMAP support -# 1997-01-05 fl Fixed PREDICTOR support -# 1997-08-27 fl Added support for rational tags (from Perry Stoll) -# 1998-01-10 fl Fixed seek/tell (from Jan Blom) -# 1998-07-15 fl Use private names for internal variables -# 1999-06-13 fl Rewritten for PIL 1.0 (1.0) -# 2000-10-11 fl Additional fixes for Python 2.0 (1.1) -# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) -# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) -# 2001-12-18 fl Added workaround for broken Matrox library -# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) -# 2003-05-19 fl Check FILLORDER tag -# 2003-09-26 fl Added RGBa support -# 2004-02-24 fl Added DPI support; fixed rational write support -# 2005-02-07 fl Added workaround for broken Corel Draw 10 files -# 2006-01-09 fl Added support for float/double tags (from Russell Nelson) -# -# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import io -import itertools -import logging -import math -import os -import struct -import warnings -from collections.abc import Callable, MutableMapping -from fractions import Fraction -from numbers import Number, Rational -from typing import IO, Any, cast - -from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags -from ._binary import i16be as i16 -from ._binary import i32be as i32 -from ._binary import o8 -from ._util import DeferredError, is_path -from .TiffTags import TYPES - -TYPE_CHECKING = False -if TYPE_CHECKING: - from collections.abc import Iterator - from typing import NoReturn - - from ._typing import Buffer, IntegralLike, StrOrBytesPath - -logger = logging.getLogger(__name__) - -# Set these to true to force use of libtiff for reading or writing. -READ_LIBTIFF = False -WRITE_LIBTIFF = False -STRIP_SIZE = 65536 - -II = b"II" # little-endian (Intel style) -MM = b"MM" # big-endian (Motorola style) - -# -# -------------------------------------------------------------------- -# Read TIFF files - -# a few tag names, just to make the code below a bit more readable -OSUBFILETYPE = 255 -IMAGEWIDTH = 256 -IMAGELENGTH = 257 -BITSPERSAMPLE = 258 -COMPRESSION = 259 -PHOTOMETRIC_INTERPRETATION = 262 -FILLORDER = 266 -IMAGEDESCRIPTION = 270 -STRIPOFFSETS = 273 -SAMPLESPERPIXEL = 277 -ROWSPERSTRIP = 278 -STRIPBYTECOUNTS = 279 -X_RESOLUTION = 282 -Y_RESOLUTION = 283 -PLANAR_CONFIGURATION = 284 -RESOLUTION_UNIT = 296 -TRANSFERFUNCTION = 301 -SOFTWARE = 305 -DATE_TIME = 306 -ARTIST = 315 -PREDICTOR = 317 -COLORMAP = 320 -TILEWIDTH = 322 -TILELENGTH = 323 -TILEOFFSETS = 324 -TILEBYTECOUNTS = 325 -SUBIFD = 330 -EXTRASAMPLES = 338 -SAMPLEFORMAT = 339 -JPEGTABLES = 347 -YCBCRSUBSAMPLING = 530 -REFERENCEBLACKWHITE = 532 -COPYRIGHT = 33432 -IPTC_NAA_CHUNK = 33723 # newsphoto properties -PHOTOSHOP_CHUNK = 34377 # photoshop properties -ICCPROFILE = 34675 -EXIFIFD = 34665 -XMP = 700 -JPEGQUALITY = 65537 # pseudo-tag by libtiff - -# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java -IMAGEJ_META_DATA_BYTE_COUNTS = 50838 -IMAGEJ_META_DATA = 50839 - -COMPRESSION_INFO = { - # Compression => pil compression name - 1: "raw", - 2: "tiff_ccitt", - 3: "group3", - 4: "group4", - 5: "tiff_lzw", - 6: "tiff_jpeg", # obsolete - 7: "jpeg", - 8: "tiff_adobe_deflate", - 32771: "tiff_raw_16", # 16-bit padding - 32773: "packbits", - 32809: "tiff_thunderscan", - 32946: "tiff_deflate", - 34676: "tiff_sgilog", - 34677: "tiff_sgilog24", - 34925: "lzma", - 50000: "zstd", - 50001: "webp", -} - -COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} - -OPEN_INFO = { - # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, - # ExtraSamples) => mode, rawmode - (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), - (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), - (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), - (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), - (II, 1, (1,), 1, (1,), ()): ("1", "1"), - (MM, 1, (1,), 1, (1,), ()): ("1", "1"), - (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), - (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), - (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), - (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), - (II, 1, (1,), 1, (2,), ()): ("L", "L;2"), - (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), - (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), - (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), - (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), - (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), - (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), - (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), - (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), - (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), - (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), - (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), - (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), - (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), - (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), - (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), - (II, 1, (1,), 1, (8,), ()): ("L", "L"), - (MM, 1, (1,), 1, (8,), ()): ("L", "L"), - (II, 1, (2,), 1, (8,), ()): ("L", "L"), - (MM, 1, (2,), 1, (8,), ()): ("L", "L"), - (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), - (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), - (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), - (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), - (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), - (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), - (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), - (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), - (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), - (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), - (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), - (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), - (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), - (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), - (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), - (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), - (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), - (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), - (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), - (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), - (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), - (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), - (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), - (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), - (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), - (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), - (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), - (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), - (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), - (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), - (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), - (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), - (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), - (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), - (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), - (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), - (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), - (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), - (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), - (II, 3, (1,), 1, (8,), ()): ("P", "P"), - (MM, 3, (1,), 1, (8,), ()): ("P", "P"), - (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), - (MM, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), - (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), - (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), - (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), - (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), - (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), - (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), - (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), - (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), - (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), - (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"), - (II, 6, (1,), 1, (8,), ()): ("L", "L"), - (MM, 6, (1,), 1, (8,), ()): ("L", "L"), - # JPEG compressed images handled by LibTiff and auto-converted to RGBX - # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel - (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), - (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), - (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), - (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), -} - -MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) - -PREFIXES = [ - b"MM\x00\x2a", # Valid TIFF header with big-endian byte order - b"II\x2a\x00", # Valid TIFF header with little-endian byte order - b"MM\x2a\x00", # Invalid TIFF header, assume big-endian - b"II\x00\x2a", # Invalid TIFF header, assume little-endian - b"MM\x00\x2b", # BigTIFF with big-endian byte order - b"II\x2b\x00", # BigTIFF with little-endian byte order -] - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(tuple(PREFIXES)) - - -def _limit_rational( - val: float | Fraction | IFDRational, max_val: int -) -> tuple[IntegralLike, IntegralLike]: - inv = abs(val) > 1 - n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) - return n_d[::-1] if inv else n_d - - -def _limit_signed_rational( - val: IFDRational, max_val: int, min_val: int -) -> tuple[IntegralLike, IntegralLike]: - frac = Fraction(val) - n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator - - if min(float(i) for i in n_d) < min_val: - n_d = _limit_rational(val, abs(min_val)) - - n_d_float = tuple(float(i) for i in n_d) - if max(n_d_float) > max_val: - n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val) - - return n_d - - -## -# Wrapper for TIFF IFDs. - -_load_dispatch = {} -_write_dispatch = {} - - -def _delegate(op: str) -> Any: - def delegate( - self: IFDRational, *args: tuple[float, ...] - ) -> bool | float | Fraction: - return getattr(self._val, op)(*args) - - return delegate - - -class IFDRational(Rational): - """Implements a rational class where 0/0 is a legal value to match - the in the wild use of exif rationals. - - e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used - """ - - """ If the denominator is 0, store this as a float('nan'), otherwise store - as a fractions.Fraction(). Delegate as appropriate - - """ - - __slots__ = ("_numerator", "_denominator", "_val") - - def __init__( - self, value: float | Fraction | IFDRational, denominator: int = 1 - ) -> None: - """ - :param value: either an integer numerator, a - float/rational/other number, or an IFDRational - :param denominator: Optional integer denominator - """ - self._val: Fraction | float - if isinstance(value, IFDRational): - self._numerator = value.numerator - self._denominator = value.denominator - self._val = value._val - return - - if isinstance(value, Fraction): - self._numerator = value.numerator - self._denominator = value.denominator - else: - if TYPE_CHECKING: - self._numerator = cast(IntegralLike, value) - else: - self._numerator = value - self._denominator = denominator - - if denominator == 0: - self._val = float("nan") - elif denominator == 1: - self._val = Fraction(value) - elif int(value) == value: - self._val = Fraction(int(value), denominator) - else: - self._val = Fraction(value / denominator) - - @property - def numerator(self) -> IntegralLike: - return self._numerator - - @property - def denominator(self) -> int: - return self._denominator - - def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]: - """ - - :param max_denominator: Integer, the maximum denominator value - :returns: Tuple of (numerator, denominator) - """ - - if self.denominator == 0: - return self.numerator, self.denominator - - assert isinstance(self._val, Fraction) - f = self._val.limit_denominator(max_denominator) - return f.numerator, f.denominator - - def __repr__(self) -> str: - return str(float(self._val)) - - def __hash__(self) -> int: # type: ignore[override] - return self._val.__hash__() - - def __eq__(self, other: object) -> bool: - val = self._val - if isinstance(other, IFDRational): - other = other._val - if isinstance(other, float): - val = float(val) - return val == other - - def __getstate__(self) -> list[float | Fraction | IntegralLike]: - return [self._val, self._numerator, self._denominator] - - def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: - IFDRational.__init__(self, 0) - _val, _numerator, _denominator = state - assert isinstance(_val, (float, Fraction)) - self._val = _val - if TYPE_CHECKING: - self._numerator = cast(IntegralLike, _numerator) - else: - self._numerator = _numerator - assert isinstance(_denominator, int) - self._denominator = _denominator - - """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', - 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', - 'mod','rmod', 'pow','rpow', 'pos', 'neg', - 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', - 'ceil', 'floor', 'round'] - print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) - """ - - __add__ = _delegate("__add__") - __radd__ = _delegate("__radd__") - __sub__ = _delegate("__sub__") - __rsub__ = _delegate("__rsub__") - __mul__ = _delegate("__mul__") - __rmul__ = _delegate("__rmul__") - __truediv__ = _delegate("__truediv__") - __rtruediv__ = _delegate("__rtruediv__") - __floordiv__ = _delegate("__floordiv__") - __rfloordiv__ = _delegate("__rfloordiv__") - __mod__ = _delegate("__mod__") - __rmod__ = _delegate("__rmod__") - __pow__ = _delegate("__pow__") - __rpow__ = _delegate("__rpow__") - __pos__ = _delegate("__pos__") - __neg__ = _delegate("__neg__") - __abs__ = _delegate("__abs__") - __trunc__ = _delegate("__trunc__") - __lt__ = _delegate("__lt__") - __gt__ = _delegate("__gt__") - __le__ = _delegate("__le__") - __ge__ = _delegate("__ge__") - __bool__ = _delegate("__bool__") - __ceil__ = _delegate("__ceil__") - __floor__ = _delegate("__floor__") - __round__ = _delegate("__round__") - # Python >= 3.11 - if hasattr(Fraction, "__int__"): - __int__ = _delegate("__int__") - - -_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] - - -def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]: - def decorator(func: _LoaderFunc) -> _LoaderFunc: - from .TiffTags import TYPES - - if func.__name__.startswith("load_"): - TYPES[idx] = func.__name__[5:].replace("_", " ") - _load_dispatch[idx] = size, func # noqa: F821 - return func - - return decorator - - -def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(func: Callable[..., Any]) -> Callable[..., Any]: - _write_dispatch[idx] = func # noqa: F821 - return func - - return decorator - - -def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: - from .TiffTags import TYPES - - idx, fmt, name = idx_fmt_name - TYPES[idx] = name - size = struct.calcsize(f"={fmt}") - - def basic_handler( - self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True - ) -> tuple[Any, ...]: - return self._unpack(f"{len(data) // size}{fmt}", data) - - _load_dispatch[idx] = size, basic_handler # noqa: F821 - _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 - b"".join(self._pack(fmt, value) for value in values) - ) - - -if TYPE_CHECKING: - _IFDv2Base = MutableMapping[int, Any] -else: - _IFDv2Base = MutableMapping - - -class ImageFileDirectory_v2(_IFDv2Base): - """This class represents a TIFF tag directory. To speed things up, we - don't decode tags unless they're asked for. - - Exposes a dictionary interface of the tags in the directory:: - - ifd = ImageFileDirectory_v2() - ifd[key] = 'Some Data' - ifd.tagtype[key] = TiffTags.ASCII - print(ifd[key]) - 'Some Data' - - Individual values are returned as the strings or numbers, sequences are - returned as tuples of the values. - - The tiff metadata type of each item is stored in a dictionary of - tag types in - :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types - are read from a tiff file, guessed from the type added, or added - manually. - - Data Structures: - - * ``self.tagtype = {}`` - - * Key: numerical TIFF tag number - * Value: integer corresponding to the data type from - :py:data:`.TiffTags.TYPES` - - .. versionadded:: 3.0.0 - - 'Internal' data structures: - - * ``self._tags_v2 = {}`` - - * Key: numerical TIFF tag number - * Value: decoded data, as tuple for multiple values - - * ``self._tagdata = {}`` - - * Key: numerical TIFF tag number - * Value: undecoded byte string from file - - * ``self._tags_v1 = {}`` - - * Key: numerical TIFF tag number - * Value: decoded data in the v1 format - - Tags will be found in the private attributes ``self._tagdata``, and in - ``self._tags_v2`` once decoded. - - ``self.legacy_api`` is a value for internal use, and shouldn't be changed - from outside code. In cooperation with - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api`` - is true, then decoded tags will be populated into both ``_tags_v1`` and - ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF - save routine. Tags should be read from ``_tags_v1`` if - ``legacy_api == true``. - - """ - - _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {} - _write_dispatch: dict[int, Callable[..., Any]] = {} - - def __init__( - self, - ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00", - prefix: bytes | None = None, - group: int | None = None, - ) -> None: - """Initialize an ImageFileDirectory. - - To construct an ImageFileDirectory from a real file, pass the 8-byte - magic header to the constructor. To only set the endianness, pass it - as the 'prefix' keyword argument. - - :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets - endianness. - :param prefix: Override the endianness of the file. - """ - if not _accept(ifh): - msg = f"not a TIFF file (header {repr(ifh)} not valid)" - raise SyntaxError(msg) - self._prefix = prefix if prefix is not None else ifh[:2] - if self._prefix == MM: - self._endian = ">" - elif self._prefix == II: - self._endian = "<" - else: - msg = "not a TIFF IFD" - raise SyntaxError(msg) - self._bigtiff = ifh[2] == 43 - self.group = group - self.tagtype: dict[int, int] = {} - """ Dictionary of tag types """ - self.reset() - self.next = ( - self._unpack("Q", ifh[8:])[0] - if self._bigtiff - else self._unpack("L", ifh[4:])[0] - ) - self._legacy_api = False - - prefix = property(lambda self: self._prefix) - offset = property(lambda self: self._offset) - - @property - def legacy_api(self) -> bool: - return self._legacy_api - - @legacy_api.setter - def legacy_api(self, value: bool) -> NoReturn: - msg = "Not allowing setting of legacy api" - raise Exception(msg) - - def reset(self) -> None: - self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false - self._tags_v2: dict[int, Any] = {} # main tag storage - self._tagdata: dict[int, bytes] = {} - self.tagtype = {} # added 2008-06-05 by Florian Hoech - self._next = None - self._offset: int | None = None - - def __str__(self) -> str: - return str(dict(self)) - - def named(self) -> dict[str, Any]: - """ - :returns: dict of name|key: value - - Returns the complete tag dictionary, with named tags where possible. - """ - return { - TiffTags.lookup(code, self.group).name: value - for code, value in self.items() - } - - def __len__(self) -> int: - return len(set(self._tagdata) | set(self._tags_v2)) - - def __getitem__(self, tag: int) -> Any: - if tag not in self._tags_v2: # unpack on the fly - data = self._tagdata[tag] - typ = self.tagtype[tag] - size, handler = self._load_dispatch[typ] - self[tag] = handler(self, data, self.legacy_api) # check type - val = self._tags_v2[tag] - if self.legacy_api and not isinstance(val, (tuple, bytes)): - val = (val,) - return val - - def __contains__(self, tag: object) -> bool: - return tag in self._tags_v2 or tag in self._tagdata - - def __setitem__(self, tag: int, value: Any) -> None: - self._setitem(tag, value, self.legacy_api) - - def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: - basetypes = (Number, bytes, str) - - info = TiffTags.lookup(tag, self.group) - values = [value] if isinstance(value, basetypes) else value - - if tag not in self.tagtype: - if info.type: - self.tagtype[tag] = info.type - else: - self.tagtype[tag] = TiffTags.UNDEFINED - if all(isinstance(v, IFDRational) for v in values): - for v in values: - assert isinstance(v, IFDRational) - if v < 0: - self.tagtype[tag] = TiffTags.SIGNED_RATIONAL - break - else: - self.tagtype[tag] = TiffTags.RATIONAL - elif all(isinstance(v, int) for v in values): - short = True - signed_short = True - long = True - for v in values: - assert isinstance(v, int) - if short and not (0 <= v < 2**16): - short = False - if signed_short and not (-(2**15) < v < 2**15): - signed_short = False - if long and v < 0: - long = False - if short: - self.tagtype[tag] = TiffTags.SHORT - elif signed_short: - self.tagtype[tag] = TiffTags.SIGNED_SHORT - elif long: - self.tagtype[tag] = TiffTags.LONG - else: - self.tagtype[tag] = TiffTags.SIGNED_LONG - elif all(isinstance(v, float) for v in values): - self.tagtype[tag] = TiffTags.DOUBLE - elif all(isinstance(v, str) for v in values): - self.tagtype[tag] = TiffTags.ASCII - elif all(isinstance(v, bytes) for v in values): - self.tagtype[tag] = TiffTags.BYTE - - if self.tagtype[tag] == TiffTags.UNDEFINED: - values = [ - v.encode("ascii", "replace") if isinstance(v, str) else v - for v in values - ] - elif self.tagtype[tag] == TiffTags.RATIONAL: - values = [float(v) if isinstance(v, int) else v for v in values] - - is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) - if not is_ifd: - values = tuple( - info.cvt_enum(value) if isinstance(value, str) else value - for value in values - ) - - dest = self._tags_v1 if legacy_api else self._tags_v2 - - # Three branches: - # Spec'd length == 1, Actual length 1, store as element - # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. - # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. - # Don't mess with the legacy api, since it's frozen. - if not is_ifd and ( - (info.length == 1) - or self.tagtype[tag] == TiffTags.BYTE - or (info.length is None and len(values) == 1 and not legacy_api) - ): - # Don't mess with the legacy api, since it's frozen. - if legacy_api and self.tagtype[tag] in [ - TiffTags.RATIONAL, - TiffTags.SIGNED_RATIONAL, - ]: # rationals - values = (values,) - try: - (dest[tag],) = values - except ValueError: - # We've got a builtin tag with 1 expected entry - warnings.warn( - f"Metadata Warning, tag {tag} had too many entries: " - f"{len(values)}, expected 1" - ) - dest[tag] = values[0] - - else: - # Spec'd length > 1 or undefined - # Unspec'd, and length > 1 - dest[tag] = values - - def __delitem__(self, tag: int) -> None: - self._tags_v2.pop(tag, None) - self._tags_v1.pop(tag, None) - self._tagdata.pop(tag, None) - - def __iter__(self) -> Iterator[int]: - return iter(set(self._tagdata) | set(self._tags_v2)) - - def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]: - return struct.unpack(self._endian + fmt, data) - - def _pack(self, fmt: str, *values: Any) -> bytes: - return struct.pack(self._endian + fmt, *values) - - list( - map( - _register_basic, - [ - (TiffTags.SHORT, "H", "short"), - (TiffTags.LONG, "L", "long"), - (TiffTags.SIGNED_BYTE, "b", "signed byte"), - (TiffTags.SIGNED_SHORT, "h", "signed short"), - (TiffTags.SIGNED_LONG, "l", "signed long"), - (TiffTags.FLOAT, "f", "float"), - (TiffTags.DOUBLE, "d", "double"), - (TiffTags.IFD, "L", "long"), - (TiffTags.LONG8, "Q", "long8"), - ], - ) - ) - - @_register_loader(1, 1) # Basic type, except for the legacy API. - def load_byte(self, data: bytes, legacy_api: bool = True) -> bytes: - return data - - @_register_writer(1) # Basic type, except for the legacy API. - def write_byte(self, data: bytes | int | IFDRational) -> bytes: - if isinstance(data, IFDRational): - data = int(data) - if isinstance(data, int): - data = bytes((data,)) - return data - - @_register_loader(2, 1) - def load_string(self, data: bytes, legacy_api: bool = True) -> str: - if data.endswith(b"\0"): - data = data[:-1] - return data.decode("latin-1", "replace") - - @_register_writer(2) - def write_string(self, value: str | bytes | int) -> bytes: - # remerge of https://github.com/python-pillow/Pillow/pull/1416 - if isinstance(value, int): - value = str(value) - if not isinstance(value, bytes): - value = value.encode("ascii", "replace") - return value + b"\0" - - @_register_loader(5, 8) - def load_rational( - self, data: bytes, legacy_api: bool = True - ) -> tuple[tuple[int, int] | IFDRational, ...]: - vals = self._unpack(f"{len(data) // 4}L", data) - - def combine(a: int, b: int) -> tuple[int, int] | IFDRational: - return (a, b) if legacy_api else IFDRational(a, b) - - return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) - - @_register_writer(5) - def write_rational(self, *values: IFDRational) -> bytes: - return b"".join( - self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values - ) - - @_register_loader(7, 1) - def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes: - return data - - @_register_writer(7) - def write_undefined(self, value: bytes | int | IFDRational) -> bytes: - if isinstance(value, IFDRational): - value = int(value) - if isinstance(value, int): - value = str(value).encode("ascii", "replace") - return value - - @_register_loader(10, 8) - def load_signed_rational( - self, data: bytes, legacy_api: bool = True - ) -> tuple[tuple[int, int] | IFDRational, ...]: - vals = self._unpack(f"{len(data) // 4}l", data) - - def combine(a: int, b: int) -> tuple[int, int] | IFDRational: - return (a, b) if legacy_api else IFDRational(a, b) - - return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) - - @_register_writer(10) - def write_signed_rational(self, *values: IFDRational) -> bytes: - return b"".join( - self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) - for frac in values - ) - - def _ensure_read(self, fp: IO[bytes], size: int) -> bytes: - ret = fp.read(size) - if len(ret) != size: - msg = ( - "Corrupt EXIF data. " - f"Expecting to read {size} bytes but only got {len(ret)}. " - ) - raise OSError(msg) - return ret - - def load(self, fp: IO[bytes]) -> None: - self.reset() - self._offset = fp.tell() - - try: - tag_count = ( - self._unpack("Q", self._ensure_read(fp, 8)) - if self._bigtiff - else self._unpack("H", self._ensure_read(fp, 2)) - )[0] - for i in range(tag_count): - tag, typ, count, data = ( - self._unpack("HHQ8s", self._ensure_read(fp, 20)) - if self._bigtiff - else self._unpack("HHL4s", self._ensure_read(fp, 12)) - ) - - tagname = TiffTags.lookup(tag, self.group).name - typname = TYPES.get(typ, "unknown") - msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" - - try: - unit_size, handler = self._load_dispatch[typ] - except KeyError: - logger.debug("%s - unsupported type %s", msg, typ) - continue # ignore unsupported type - size = count * unit_size - if size > (8 if self._bigtiff else 4): - here = fp.tell() - (offset,) = self._unpack("Q" if self._bigtiff else "L", data) - msg += f" Tag Location: {here} - Data Location: {offset}" - fp.seek(offset) - data = ImageFile._safe_read(fp, size) - fp.seek(here) - else: - data = data[:size] - - if len(data) != size: - warnings.warn( - "Possibly corrupt EXIF data. " - f"Expecting to read {size} bytes but only got {len(data)}." - f" Skipping tag {tag}" - ) - logger.debug(msg) - continue - - if not data: - logger.debug(msg) - continue - - self._tagdata[tag] = data - self.tagtype[tag] = typ - - msg += " - value: " - msg += f"<table: {size} bytes>" if size > 32 else repr(data) - - logger.debug(msg) - - (self.next,) = ( - self._unpack("Q", self._ensure_read(fp, 8)) - if self._bigtiff - else self._unpack("L", self._ensure_read(fp, 4)) - ) - except OSError as msg: - warnings.warn(str(msg)) - return - - def _get_ifh(self) -> bytes: - ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) - if self._bigtiff: - ifh += self._pack("HH", 8, 0) - ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8) - - return ifh - - def tobytes(self, offset: int = 0) -> bytes: - # FIXME What about tagdata? - result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) - - entries: list[tuple[int, int, int, bytes, bytes]] = [] - - fmt = "Q" if self._bigtiff else "L" - fmt_size = 8 if self._bigtiff else 4 - offset += ( - len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size - ) - stripoffsets = None - - # pass 1: convert tags to binary format - # always write tags in ascending order - for tag, value in sorted(self._tags_v2.items()): - if tag == STRIPOFFSETS: - stripoffsets = len(entries) - typ = self.tagtype[tag] - logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) - is_ifd = typ == TiffTags.LONG and isinstance(value, dict) - if is_ifd: - ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag) - values = self._tags_v2[tag] - for ifd_tag, ifd_value in values.items(): - ifd[ifd_tag] = ifd_value - data = ifd.tobytes(offset) - else: - values = value if isinstance(value, tuple) else (value,) - data = self._write_dispatch[typ](self, *values) - - tagname = TiffTags.lookup(tag, self.group).name - typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: " - msg += f"<table: {len(data)} bytes>" if len(data) >= 16 else str(values) - logger.debug(msg) - - # count is sum of lengths for string and arbitrary data - if is_ifd: - count = 1 - elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: - count = len(data) - else: - count = len(values) - # figure out if data fits into the entry - if len(data) <= fmt_size: - entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b"")) - else: - entries.append((tag, typ, count, self._pack(fmt, offset), data)) - offset += (len(data) + 1) // 2 * 2 # pad to word - - # update strip offset data to point beyond auxiliary data - if stripoffsets is not None: - tag, typ, count, value, data = entries[stripoffsets] - if data: - size, handler = self._load_dispatch[typ] - values = [val + offset for val in handler(self, data, self.legacy_api)] - data = self._write_dispatch[typ](self, *values) - else: - value = self._pack(fmt, self._unpack(fmt, value)[0] + offset) - entries[stripoffsets] = tag, typ, count, value, data - - # pass 2: write entries to file - for tag, typ, count, value, data in entries: - logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) - result += self._pack( - "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value - ) - - # -- overwrite here for multi-page -- - result += self._pack(fmt, 0) # end of entries - - # pass 3: write auxiliary data to file - for tag, typ, count, value, data in entries: - result += data - if len(data) & 1: - result += b"\0" - - return result - - def save(self, fp: IO[bytes]) -> int: - if fp.tell() == 0: # skip TIFF header on subsequent pages - fp.write(self._get_ifh()) - - offset = fp.tell() - result = self.tobytes(offset) - fp.write(result) - return offset + len(result) - - -ImageFileDirectory_v2._load_dispatch = _load_dispatch -ImageFileDirectory_v2._write_dispatch = _write_dispatch -for idx, name in TYPES.items(): - name = name.replace(" ", "_") - setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1]) - setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx]) -del _load_dispatch, _write_dispatch, idx, name - - -# Legacy ImageFileDirectory support. -class ImageFileDirectory_v1(ImageFileDirectory_v2): - """This class represents the **legacy** interface to a TIFF tag directory. - - Exposes a dictionary interface of the tags in the directory:: - - ifd = ImageFileDirectory_v1() - ifd[key] = 'Some Data' - ifd.tagtype[key] = TiffTags.ASCII - print(ifd[key]) - ('Some Data',) - - Also contains a dictionary of tag types as read from the tiff image file, - :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. - - Values are returned as a tuple. - - .. deprecated:: 3.0.0 - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._legacy_api = True - - tags = property(lambda self: self._tags_v1) - tagdata = property(lambda self: self._tagdata) - - # defined in ImageFileDirectory_v2 - tagtype: dict[int, int] - """Dictionary of tag types""" - - @classmethod - def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1: - """Returns an - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` - instance with the same data as is contained in the original - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - instance. - - :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` - - """ - - ifd = cls(prefix=original.prefix) - ifd._tagdata = original._tagdata - ifd.tagtype = original.tagtype - ifd.next = original.next # an indicator for multipage tiffs - return ifd - - def to_v2(self) -> ImageFileDirectory_v2: - """Returns an - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - instance with the same data as is contained in the original - :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` - instance. - - :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` - - """ - - ifd = ImageFileDirectory_v2(prefix=self.prefix) - ifd._tagdata = dict(self._tagdata) - ifd.tagtype = dict(self.tagtype) - ifd._tags_v2 = dict(self._tags_v2) - return ifd - - def __contains__(self, tag: object) -> bool: - return tag in self._tags_v1 or tag in self._tagdata - - def __len__(self) -> int: - return len(set(self._tagdata) | set(self._tags_v1)) - - def __iter__(self) -> Iterator[int]: - return iter(set(self._tagdata) | set(self._tags_v1)) - - def __setitem__(self, tag: int, value: Any) -> None: - for legacy_api in (False, True): - self._setitem(tag, value, legacy_api) - - def __getitem__(self, tag: int) -> Any: - if tag not in self._tags_v1: # unpack on the fly - data = self._tagdata[tag] - typ = self.tagtype[tag] - size, handler = self._load_dispatch[typ] - for legacy in (False, True): - self._setitem(tag, handler(self, data, legacy), legacy) - val = self._tags_v1[tag] - if not isinstance(val, (tuple, bytes)): - val = (val,) - return val - - -# undone -- switch this pointer -ImageFileDirectory = ImageFileDirectory_v1 - - -## -# Image plugin for TIFF files. - - -class TiffImageFile(ImageFile.ImageFile): - format = "TIFF" - format_description = "Adobe TIFF" - _close_exclusive_fp_after_loading = False - - def __init__( - self, - fp: StrOrBytesPath | IO[bytes], - filename: str | bytes | None = None, - ) -> None: - self.tag_v2: ImageFileDirectory_v2 - """ Image file directory (tag dictionary) """ - - self.tag: ImageFileDirectory_v1 - """ Legacy tag entries """ - - super().__init__(fp, filename) - - def _open(self) -> None: - """Open the first image in a TIFF file""" - - # Header - assert self.fp is not None - ifh = self.fp.read(8) - if ifh[2] == 43: - ifh += self.fp.read(8) - - self.tag_v2 = ImageFileDirectory_v2(ifh) - - # setup frame pointers - self.__first = self.__next = self.tag_v2.next - self.__frame = -1 - self._fp = self.fp - self._frame_pos: list[int] = [] - self._n_frames: int | None = None - - logger.debug("*** TiffImageFile._open ***") - logger.debug("- __first: %s", self.__first) - logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes) - - # and load the first frame - self._seek(0) - - @property - def n_frames(self) -> int: - current_n_frames = self._n_frames - if current_n_frames is None: - current = self.tell() - self._seek(len(self._frame_pos)) - while self._n_frames is None: - self._seek(self.tell() + 1) - self.seek(current) - assert self._n_frames is not None - return self._n_frames - - def seek(self, frame: int) -> None: - """Select a given frame as current image""" - if not self._seek_check(frame): - return - self._seek(frame) - if self._im is not None and ( - self.im.size != self._tile_size - or self.im.mode != self.mode - or self.readonly - ): - self._im = None - - def _seek(self, frame: int) -> None: - if isinstance(self._fp, DeferredError): - raise self._fp.ex - self.fp = self._fp - - while len(self._frame_pos) <= frame: - if not self.__next: - msg = "no more images in TIFF file" - raise EOFError(msg) - logger.debug( - "Seeking to frame %s, on frame %s, __next %s, location: %s", - frame, - self.__frame, - self.__next, - self.fp.tell(), - ) - if self.__next >= 2**63: - msg = "Unable to seek to frame" - raise ValueError(msg) - self.fp.seek(self.__next) - self._frame_pos.append(self.__next) - logger.debug("Loading tags, location: %s", self.fp.tell()) - self.tag_v2.load(self.fp) - if self.tag_v2.next in self._frame_pos: - # This IFD has already been processed - # Declare this to be the end of the image - self.__next = 0 - else: - self.__next = self.tag_v2.next - if self.__next == 0: - self._n_frames = frame + 1 - if len(self._frame_pos) == 1: - self.is_animated = self.__next != 0 - self.__frame += 1 - self.fp.seek(self._frame_pos[frame]) - self.tag_v2.load(self.fp) - if XMP in self.tag_v2: - xmp = self.tag_v2[XMP] - if isinstance(xmp, tuple) and len(xmp) == 1: - xmp = xmp[0] - self.info["xmp"] = xmp - elif "xmp" in self.info: - del self.info["xmp"] - self._reload_exif() - # fill the legacy tag/ifd entries - self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) - self.__frame = frame - self._setup() - - def tell(self) -> int: - """Return the current frame number""" - return self.__frame - - def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]: - """ - Returns a dictionary of Photoshop "Image Resource Blocks". - The keys are the image resource ID. For more information, see - https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727 - - :returns: Photoshop "Image Resource Blocks" in a dictionary. - """ - blocks = {} - val = self.tag_v2.get(ExifTags.Base.ImageResources) - if val: - while val.startswith(b"8BIM") and len(val) >= 12: - id = i16(val[4:6]) - n = math.ceil((val[6] + 1) / 2) * 2 - try: - size = i32(val[6 + n : 10 + n]) - except struct.error: - break - data = val[10 + n : 10 + n + size] - blocks[id] = {"data": data} - - val = val[math.ceil((10 + n + size) / 2) * 2 :] - return blocks - - def load(self) -> Image.core.PixelAccess | None: - if self.tile and self.use_load_libtiff: - return self._load_libtiff() - return super().load() - - def load_prepare(self) -> None: - if self._im is None: - Image._decompression_bomb_check(self._tile_size) - self.im = Image.core.new(self.mode, self._tile_size) - ImageFile.ImageFile.load_prepare(self) - - def load_end(self) -> None: - # allow closing if we're on the first frame, there's no next - # This is the ImageFile.load path only, libtiff specific below. - if not self.is_animated: - self._close_exclusive_fp_after_loading = True - - # load IFD data from fp before it is closed - exif = self.getexif() - for key in TiffTags.TAGS_V2_GROUPS: - if key not in exif: - continue - exif.get_ifd(key) - - ImageOps.exif_transpose(self, in_place=True) - if ExifTags.Base.Orientation in self.tag_v2: - del self.tag_v2[ExifTags.Base.Orientation] - - def _load_libtiff(self) -> Image.core.PixelAccess | None: - """Overload method triggered when we detect a compressed tiff - Calls out to libtiff""" - - Image.Image.load(self) - - self.load_prepare() - - if not len(self.tile) == 1: - msg = "Not exactly one tile" - raise OSError(msg) - - # (self._compression, (extents tuple), - # 0, (rawmode, self._compression, fp)) - extents = self.tile[0][1] - args = self.tile[0][3] - - # To be nice on memory footprint, if there's a - # file descriptor, use that instead of reading - # into a string in python. - assert self.fp is not None - try: - fp = hasattr(self.fp, "fileno") and self.fp.fileno() - # flush the file descriptor, prevents error on pypy 2.4+ - # should also eliminate the need for fp.tell - # in _seek - if hasattr(self.fp, "flush"): - self.fp.flush() - except OSError: - # io.BytesIO have a fileno, but returns an OSError if - # it doesn't use a file descriptor. - fp = False - - if fp: - assert isinstance(args, tuple) - args_list = list(args) - args_list[2] = fp - args = tuple(args_list) - - decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig) - try: - decoder.setimage(self.im, extents) - except ValueError as e: - msg = "Couldn't set the image" - raise OSError(msg) from e - - close_self_fp = self._exclusive_fp and not self.is_animated - if hasattr(self.fp, "getvalue"): - # We've got a stringio like thing passed in. Yay for all in memory. - # The decoder needs the entire file in one shot, so there's not - # a lot we can do here other than give it the entire file. - # unless we could do something like get the address of the - # underlying string for stringio. - # - # Rearranging for supporting byteio items, since they have a fileno - # that returns an OSError if there's no underlying fp. Easier to - # deal with here by reordering. - logger.debug("have getvalue. just sending in a string from getvalue") - n, err = decoder.decode(self.fp.getvalue()) - elif fp: - # we've got a actual file on disk, pass in the fp. - logger.debug("have fileno, calling fileno version of the decoder.") - if not close_self_fp: - self.fp.seek(0) - # Save and restore the file position, because libtiff will move it - # outside of the Python runtime, and that will confuse - # io.BufferedReader and possible others. - # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), - # because the buffer read head already may not equal the actual - # file position, and fp.seek() may just adjust it's internal - # pointer and not actually seek the OS file handle. - pos = os.lseek(fp, 0, os.SEEK_CUR) - # 4 bytes, otherwise the trace might error out - n, err = decoder.decode(b"fpfp") - os.lseek(fp, pos, os.SEEK_SET) - else: - # we have something else. - logger.debug("don't have fileno or getvalue. just reading") - self.fp.seek(0) - # UNDONE -- so much for that buffer size thing. - n, err = decoder.decode(self.fp.read()) - - self.tile = [] - self.readonly = 0 - - self.load_end() - - if close_self_fp: - self.fp.close() - self.fp = None # might be shared - - if err < 0: - msg = f"decoder error {err}" - raise OSError(msg) - - return Image.Image.load(self) - - def _setup(self) -> None: - """Setup this image object based on current tags""" - - if 0xBC01 in self.tag_v2: - msg = "Windows Media Photo files not yet supported" - raise OSError(msg) - - # extract relevant tags - self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] - self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) - - # photometric is a required tag, but not everyone is reading - # the specification - photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) - - # old style jpeg compression images most certainly are YCbCr - if self._compression == "tiff_jpeg": - photo = 6 - - fillorder = self.tag_v2.get(FILLORDER, 1) - - logger.debug("*** Summary ***") - logger.debug("- compression: %s", self._compression) - logger.debug("- photometric_interpretation: %s", photo) - logger.debug("- planar_configuration: %s", self._planar_configuration) - logger.debug("- fill_order: %s", fillorder) - logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) - - # size - try: - xsize = self.tag_v2[IMAGEWIDTH] - ysize = self.tag_v2[IMAGELENGTH] - except KeyError as e: - msg = "Missing dimensions" - raise TypeError(msg) from e - if not isinstance(xsize, int) or not isinstance(ysize, int): - msg = "Invalid dimensions" - raise ValueError(msg) - self._tile_size = xsize, ysize - orientation = self.tag_v2.get(ExifTags.Base.Orientation) - if orientation in (5, 6, 7, 8): - self._size = ysize, xsize - else: - self._size = xsize, ysize - - logger.debug("- size: %s", self.size) - - sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) - if len(sample_format) > 1 and max(sample_format) == min(sample_format): - # SAMPLEFORMAT is properly per band, so an RGB image will - # be (1,1,1). But, we don't support per band pixel types, - # and anything more than one band is a uint8. So, just - # take the first element. Revisit this if adding support - # for more exotic images. - sample_format = (sample_format[0],) - - bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) - extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) - samples_per_pixel = self.tag_v2.get( - SAMPLESPERPIXEL, - 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, - ) - if photo in (2, 6, 8): # RGB, YCbCr, LAB - bps_count = 3 - elif photo == 5: # CMYK - bps_count = 4 - else: - bps_count = 1 - if self._planar_configuration == 2 and extra_tuple and max(extra_tuple) == 0: - # If components are stored separately, - # then unspecified extra components at the end can be ignored - bps_tuple = bps_tuple[: -len(extra_tuple)] - samples_per_pixel -= len(extra_tuple) - extra_tuple = () - bps_count += len(extra_tuple) - bps_actual_count = len(bps_tuple) - - if samples_per_pixel > MAX_SAMPLESPERPIXEL: - # DOS check, samples_per_pixel can be a Long, and we extend the tuple below - logger.error( - "More samples per pixel than can be decoded: %s", samples_per_pixel - ) - msg = "Invalid value for samples per pixel" - raise SyntaxError(msg) - - if samples_per_pixel < bps_actual_count: - # If a file has more values in bps_tuple than expected, - # remove the excess. - bps_tuple = bps_tuple[:samples_per_pixel] - elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: - # If a file has only one value in bps_tuple, when it should have more, - # presume it is the same number of bits for all of the samples. - bps_tuple = bps_tuple * samples_per_pixel - - if len(bps_tuple) != samples_per_pixel: - msg = "unknown data organization" - raise SyntaxError(msg) - - # mode: check photometric interpretation and bits per pixel - key = ( - self.tag_v2.prefix, - photo, - sample_format, - fillorder, - bps_tuple, - extra_tuple, - ) - logger.debug("format key: %s", key) - try: - self._mode, rawmode = OPEN_INFO[key] - except KeyError as e: - logger.debug("- unsupported format") - msg = "unknown pixel mode" - raise SyntaxError(msg) from e - - logger.debug("- raw mode: %s", rawmode) - logger.debug("- pil mode: %s", self.mode) - - self.info["compression"] = self._compression - - xres = self.tag_v2.get(X_RESOLUTION, 1) - yres = self.tag_v2.get(Y_RESOLUTION, 1) - - if xres and yres: - resunit = self.tag_v2.get(RESOLUTION_UNIT) - if resunit == 2: # dots per inch - self.info["dpi"] = (xres, yres) - elif resunit == 3: # dots per centimeter. convert to dpi - self.info["dpi"] = (xres * 2.54, yres * 2.54) - elif resunit is None: # used to default to 1, but now 2) - self.info["dpi"] = (xres, yres) - # For backward compatibility, - # we also preserve the old behavior - self.info["resolution"] = xres, yres - else: # No absolute unit of measurement - self.info["resolution"] = xres, yres - - # build tile descriptors - x = y = layer = 0 - self.tile = [] - self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" - if self.use_load_libtiff: - # Decoder expects entire file as one tile. - # There's a buffer size limit in load (64k) - # so large g4 images will fail if we use that - # function. - # - # Setup the one tile for the whole image, then - # use the _load_libtiff function. - - # libtiff handles the fillmode for us, so 1;IR should - # actually be 1;I. Including the R double reverses the - # bits, so stripes of the image are reversed. See - # https://github.com/python-pillow/Pillow/issues/279 - if fillorder == 2: - # Replace fillorder with fillorder=1 - key = key[:3] + (1,) + key[4:] - logger.debug("format key: %s", key) - # this should always work, since all the - # fillorder==2 modes have a corresponding - # fillorder=1 mode - self._mode, rawmode = OPEN_INFO[key] - # YCbCr images with new jpeg compression with pixels in one plane - # unpacked straight into RGB values - if ( - photo == 6 - and self._compression == "jpeg" - and self._planar_configuration == 1 - ): - rawmode = "RGB" - # libtiff always returns the bytes in native order. - # we're expecting image byte order. So, if the rawmode - # contains I;16, we need to convert from native to image - # byte order. - elif rawmode == "I;16": - rawmode = "I;16N" - elif rawmode.endswith((";16B", ";16L")): - rawmode = rawmode[:-1] + "N" - - # Offset in the tile tuple is 0, we go from 0,0 to - # w,h, and we only do this once -- eds - a = (rawmode, self._compression, False, self.tag_v2.offset) - self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a)) - - elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: - # striped image - if STRIPOFFSETS in self.tag_v2: - offsets = self.tag_v2[STRIPOFFSETS] - h = self.tag_v2.get(ROWSPERSTRIP, ysize) - w = xsize - else: - # tiled image - offsets = self.tag_v2[TILEOFFSETS] - tilewidth = self.tag_v2.get(TILEWIDTH) - h = self.tag_v2.get(TILELENGTH) - if not isinstance(tilewidth, int) or not isinstance(h, int): - msg = "Invalid tile dimensions" - raise ValueError(msg) - w = tilewidth - - if w == xsize and h == ysize and self._planar_configuration != 2: - # Every tile covers the image. Only use the last offset - offsets = offsets[-1:] - - for offset in offsets: - if x + w > xsize: - stride = w * sum(bps_tuple) / 8 # bytes per line - else: - stride = 0 - - tile_rawmode = rawmode - if self._planar_configuration == 2: - # each band on it's own layer - tile_rawmode = rawmode[layer] - # adjust stride width accordingly - stride /= bps_count - - args = (tile_rawmode, int(stride), 1) - self.tile.append( - ImageFile._Tile( - self._compression, - (x, y, min(x + w, xsize), min(y + h, ysize)), - offset, - args, - ) - ) - x += w - if x >= xsize: - x, y = 0, y + h - if y >= ysize: - y = 0 - layer += 1 - else: - logger.debug("- unsupported data organization") - msg = "unknown data organization" - raise SyntaxError(msg) - - # Fix up info. - if ICCPROFILE in self.tag_v2: - self.info["icc_profile"] = self.tag_v2[ICCPROFILE] - - # fixup palette descriptor - - if self.mode in ["P", "PA"]: - palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] - self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) - - -# -# -------------------------------------------------------------------- -# Write TIFF files - -# little endian is default except for image modes with -# explicit big endian byte-order - -SAVE_INFO = { - # mode => rawmode, byteorder, photometrics, - # sampleformat, bitspersample, extra - "1": ("1", II, 1, 1, (1,), None), - "L": ("L", II, 1, 1, (8,), None), - "LA": ("LA", II, 1, 1, (8, 8), 2), - "P": ("P", II, 3, 1, (8,), None), - "PA": ("PA", II, 3, 1, (8, 8), 2), - "I": ("I;32S", II, 1, 2, (32,), None), - "I;16": ("I;16", II, 1, 1, (16,), None), - "I;16L": ("I;16L", II, 1, 1, (16,), None), - "F": ("F;32F", II, 1, 3, (32,), None), - "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), - "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), - "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), - "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), - "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), - "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), - "I;16B": ("I;16B", MM, 1, 1, (16,), None), -} - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - try: - rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] - except KeyError as e: - msg = f"cannot write mode {im.mode} as TIFF" - raise OSError(msg) from e - - encoderinfo = im.encoderinfo - encoderconfig = im.encoderconfig - - ifd = ImageFileDirectory_v2(prefix=prefix) - if encoderinfo.get("big_tiff"): - ifd._bigtiff = True - - try: - compression = encoderinfo["compression"] - except KeyError: - compression = im.info.get("compression") - if isinstance(compression, int): - # compression value may be from BMP. Ignore it - compression = None - if compression is None: - compression = "raw" - elif compression == "tiff_jpeg": - # OJPEG is obsolete, so use new-style JPEG compression instead - compression = "jpeg" - elif compression == "tiff_deflate": - compression = "tiff_adobe_deflate" - - libtiff = WRITE_LIBTIFF or compression != "raw" - - # required for color libtiff images - ifd[PLANAR_CONFIGURATION] = 1 - - ifd[IMAGEWIDTH] = im.size[0] - ifd[IMAGELENGTH] = im.size[1] - - # write any arbitrary tags passed in as an ImageFileDirectory - if "tiffinfo" in encoderinfo: - info = encoderinfo["tiffinfo"] - elif "exif" in encoderinfo: - info = encoderinfo["exif"] - if isinstance(info, bytes): - exif = Image.Exif() - exif.load(info) - info = exif - else: - info = {} - logger.debug("Tiffinfo Keys: %s", list(info)) - if isinstance(info, ImageFileDirectory_v1): - info = info.to_v2() - for key in info: - if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: - ifd[key] = info.get_ifd(key) - else: - ifd[key] = info.get(key) - try: - ifd.tagtype[key] = info.tagtype[key] - except Exception: - pass # might not be an IFD. Might not have populated type - - legacy_ifd = {} - if hasattr(im, "tag"): - legacy_ifd = im.tag.to_v2() - - supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})} - if supplied_tags.get(PLANAR_CONFIGURATION) == 2 and EXTRASAMPLES in supplied_tags: - # If the image used separate component planes, - # then EXTRASAMPLES should be ignored when saving contiguously - if SAMPLESPERPIXEL in supplied_tags: - supplied_tags[SAMPLESPERPIXEL] -= len(supplied_tags[EXTRASAMPLES]) - del supplied_tags[EXTRASAMPLES] - for tag in ( - # IFD offset that may not be correct in the saved image - EXIFIFD, - # Determined by the image format and should not be copied from legacy_ifd. - SAMPLEFORMAT, - ): - if tag in supplied_tags: - del supplied_tags[tag] - - # additions written by Greg Couch, gregc@cgl.ucsf.edu - # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com - if hasattr(im, "tag_v2"): - # preserve tags from original TIFF image file - for key in ( - RESOLUTION_UNIT, - X_RESOLUTION, - Y_RESOLUTION, - IPTC_NAA_CHUNK, - PHOTOSHOP_CHUNK, - XMP, - ): - if key in im.tag_v2: - if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in ( - TiffTags.BYTE, - TiffTags.UNDEFINED, - ): - del supplied_tags[key] - else: - ifd[key] = im.tag_v2[key] - ifd.tagtype[key] = im.tag_v2.tagtype[key] - - # preserve ICC profile (should also work when saving other formats - # which support profiles as TIFF) -- 2008-06-06 Florian Hoech - icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: - ifd[ICCPROFILE] = icc - - for key, name in [ - (IMAGEDESCRIPTION, "description"), - (X_RESOLUTION, "resolution"), - (Y_RESOLUTION, "resolution"), - (X_RESOLUTION, "x_resolution"), - (Y_RESOLUTION, "y_resolution"), - (RESOLUTION_UNIT, "resolution_unit"), - (SOFTWARE, "software"), - (DATE_TIME, "date_time"), - (ARTIST, "artist"), - (COPYRIGHT, "copyright"), - ]: - if name in encoderinfo: - ifd[key] = encoderinfo[name] - - dpi = encoderinfo.get("dpi") - if dpi: - ifd[RESOLUTION_UNIT] = 2 - ifd[X_RESOLUTION] = dpi[0] - ifd[Y_RESOLUTION] = dpi[1] - - if bits != (1,): - ifd[BITSPERSAMPLE] = bits - if len(bits) != 1: - ifd[SAMPLESPERPIXEL] = len(bits) - if extra is not None: - ifd[EXTRASAMPLES] = extra - if format != 1: - ifd[SAMPLEFORMAT] = format - - if PHOTOMETRIC_INTERPRETATION not in ifd: - ifd[PHOTOMETRIC_INTERPRETATION] = photo - elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: - if im.mode == "1": - inverted_im = im.copy() - px = inverted_im.load() - if px is not None: - for y in range(inverted_im.height): - for x in range(inverted_im.width): - px[x, y] = 0 if px[x, y] == 255 else 255 - im = inverted_im - else: - im = ImageOps.invert(im) - - if im.mode in ["P", "PA"]: - lut = im.im.getpalette("RGB", "RGB;L") - colormap = [] - colors = len(lut) // 3 - for i in range(3): - colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]] - colormap += [0] * (256 - colors) - ifd[COLORMAP] = colormap - # data orientation - w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] - stride = len(bits) * ((w * bits[0] + 7) // 8) - if ROWSPERSTRIP not in ifd: - # aim for given strip size (64 KB by default) when using libtiff writer - if libtiff: - im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) - rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) - # JPEG encoder expects multiple of 8 rows - if compression == "jpeg": - rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) - else: - rows_per_strip = h - if rows_per_strip == 0: - rows_per_strip = 1 - ifd[ROWSPERSTRIP] = rows_per_strip - strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] - strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] - if strip_byte_counts >= 2**16: - ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG - ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( - stride * h - strip_byte_counts * (strips_per_image - 1), - ) - ifd[STRIPOFFSETS] = tuple( - range(0, strip_byte_counts * strips_per_image, strip_byte_counts) - ) # this is adjusted by IFD writer - # no compression by default: - ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) - - if im.mode == "YCbCr": - for tag, default_value in { - YCBCRSUBSAMPLING: (1, 1), - REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), - }.items(): - ifd.setdefault(tag, default_value) - - blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] - if libtiff: - if "quality" in encoderinfo: - quality = encoderinfo["quality"] - if not isinstance(quality, int) or quality < 0 or quality > 100: - msg = "Invalid quality setting" - raise ValueError(msg) - if compression != "jpeg": - msg = "quality setting only supported for 'jpeg' compression" - raise ValueError(msg) - ifd[JPEGQUALITY] = quality - - logger.debug("Saving using libtiff encoder") - logger.debug("Items: %s", sorted(ifd.items())) - _fp = 0 - if hasattr(fp, "fileno"): - try: - fp.seek(0) - _fp = fp.fileno() - except io.UnsupportedOperation: - pass - - # optional types for non core tags - types = {} - # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library - # based on the data in the strip. - # OSUBFILETYPE is deprecated. - # The other tags expect arrays with a certain length (fixed or depending on - # BITSPERSAMPLE, etc), passing arrays with a different length will result in - # segfaults. Block these tags until we add extra validation. - # SUBIFD may also cause a segfault. - blocklist += [ - OSUBFILETYPE, - REFERENCEBLACKWHITE, - STRIPBYTECOUNTS, - STRIPOFFSETS, - TRANSFERFUNCTION, - SUBIFD, - ] - - # bits per sample is a single short in the tiff directory, not a list. - atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]} - # Merge the ones that we have with (optional) more bits from - # the original file, e.g x,y resolution so that we can - # save(load('')) == original file. - for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): - # Libtiff can only process certain core items without adding - # them to the custom dictionary. - # Custom items are supported for int, float, unicode, string and byte - # values. Other types and tuples require a tagtype. - if tag not in TiffTags.LIBTIFF_CORE: - if tag in TiffTags.TAGS_V2_GROUPS: - types[tag] = TiffTags.LONG8 - elif tag in ifd.tagtype: - types[tag] = ifd.tagtype[tag] - elif isinstance(value, (int, float, str, bytes)) or ( - isinstance(value, tuple) - and all(isinstance(v, (int, float, IFDRational)) for v in value) - ): - type = TiffTags.lookup(tag).type - if type: - types[tag] = type - if tag not in atts and tag not in blocklist: - if isinstance(value, str): - atts[tag] = value.encode("ascii", "replace") + b"\0" - elif isinstance(value, IFDRational): - atts[tag] = float(value) - else: - atts[tag] = value - - if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: - atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] - - logger.debug("Converted items: %s", sorted(atts.items())) - - # libtiff always expects the bytes in native order. - # we're storing image byte order. So, if the rawmode - # contains I;16, we need to convert from native to image - # byte order. - if im.mode in ("I;16", "I;16B", "I;16L"): - rawmode = "I;16N" - - # Pass tags as sorted list so that the tags are set in a fixed order. - # This is required by libtiff for some tags. For example, the JPEGQUALITY - # pseudo tag requires that the COMPRESS tag was already set. - tags = list(atts.items()) - tags.sort() - a = (rawmode, compression, _fp, filename, tags, types) - encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) - encoder.setimage(im.im, (0, 0) + im.size) - while True: - errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:] - if not _fp: - fp.write(data) - if errcode: - break - if errcode < 0: - msg = f"encoder error {errcode} when writing image file" - raise OSError(msg) - - else: - for tag in blocklist: - del ifd[tag] - offset = ifd.save(fp) - - ImageFile._save( - im, - fp, - [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))], - ) - - # -- helper for multi-page save -- - if "_debug_multipage" in encoderinfo: - # just to access o32 and o16 (using correct byte order) - setattr(im, "_debug_multipage", ifd) - - -class AppendingTiffWriter(io.BytesIO): - fieldSizes = [ - 0, # None - 1, # byte - 1, # ascii - 2, # short - 4, # long - 8, # rational - 1, # sbyte - 1, # undefined - 2, # sshort - 4, # slong - 8, # srational - 4, # float - 8, # double - 4, # ifd - 2, # unicode - 4, # complex - 8, # long8 - ] - - Tags = { - 273, # StripOffsets - 288, # FreeOffsets - 324, # TileOffsets - 519, # JPEGQTables - 520, # JPEGDCTables - 521, # JPEGACTables - } - - def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None: - self.f: IO[bytes] - if is_path(fn): - self.name = fn - self.close_fp = True - try: - self.f = open(fn, "w+b" if new else "r+b") - except OSError: - self.f = open(fn, "w+b") - else: - self.f = cast(IO[bytes], fn) - self.close_fp = False - self.beginning = self.f.tell() - self.setup() - - def setup(self) -> None: - # Reset everything. - self.f.seek(self.beginning, os.SEEK_SET) - - self.whereToWriteNewIFDOffset: int | None = None - self.offsetOfNewPage = 0 - - self.IIMM = iimm = self.f.read(4) - self._bigtiff = b"\x2b" in iimm - if not iimm: - # empty file - first page - self.isFirst = True - return - - self.isFirst = False - if iimm not in PREFIXES: - msg = "Invalid TIFF file header" - raise RuntimeError(msg) - - self.setEndian("<" if iimm.startswith(II) else ">") - - if self._bigtiff: - self.f.seek(4, os.SEEK_CUR) - self.skipIFDs() - self.goToEnd() - - def finalize(self) -> None: - if self.isFirst: - return - - # fix offsets - self.f.seek(self.offsetOfNewPage) - - iimm = self.f.read(4) - if not iimm: - # Make it easy to finish a frame without committing to a new one. - return - - if iimm != self.IIMM: - msg = "IIMM of new page doesn't match IIMM of first page" - raise RuntimeError(msg) - - if self._bigtiff: - self.f.seek(4, os.SEEK_CUR) - ifd_offset = self._read(8 if self._bigtiff else 4) - ifd_offset += self.offsetOfNewPage - assert self.whereToWriteNewIFDOffset is not None - self.f.seek(self.whereToWriteNewIFDOffset) - self._write(ifd_offset, 8 if self._bigtiff else 4) - self.f.seek(ifd_offset) - self.fixIFD() - - def newFrame(self) -> None: - # Call this to finish a frame. - self.finalize() - self.setup() - - def __enter__(self) -> AppendingTiffWriter: - return self - - def __exit__(self, *args: object) -> None: - if self.close_fp: - self.close() - - def tell(self) -> int: - return self.f.tell() - self.offsetOfNewPage - - def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: - """ - :param offset: Distance to seek. - :param whence: Whether the distance is relative to the start, - end or current position. - :returns: The resulting position, relative to the start. - """ - if whence == os.SEEK_SET: - offset += self.offsetOfNewPage - - self.f.seek(offset, whence) - return self.tell() - - def goToEnd(self) -> None: - self.f.seek(0, os.SEEK_END) - pos = self.f.tell() - - # pad to 16 byte boundary - pad_bytes = 16 - pos % 16 - if 0 < pad_bytes < 16: - self.f.write(bytes(pad_bytes)) - self.offsetOfNewPage = self.f.tell() - - def setEndian(self, endian: str) -> None: - self.endian = endian - self.longFmt = f"{self.endian}L" - self.shortFmt = f"{self.endian}H" - self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L") - - def skipIFDs(self) -> None: - while True: - ifd_offset = self._read(8 if self._bigtiff else 4) - if ifd_offset == 0: - self.whereToWriteNewIFDOffset = self.f.tell() - ( - 8 if self._bigtiff else 4 - ) - break - - self.f.seek(ifd_offset) - num_tags = self._read(8 if self._bigtiff else 2) - self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR) - - def write(self, data: Buffer, /) -> int: - return self.f.write(data) - - def _fmt(self, field_size: int) -> str: - try: - return {2: "H", 4: "L", 8: "Q"}[field_size] - except KeyError: - msg = "offset is not supported" - raise RuntimeError(msg) - - def _read(self, field_size: int) -> int: - (value,) = struct.unpack( - self.endian + self._fmt(field_size), self.f.read(field_size) - ) - return value - - def readShort(self) -> int: - return self._read(2) - - def readLong(self) -> int: - return self._read(4) - - @staticmethod - def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: - if bytes_written is not None and bytes_written != expected: - msg = f"wrote only {bytes_written} bytes but wanted {expected}" - raise RuntimeError(msg) - - def _rewriteLast( - self, value: int, field_size: int, new_field_size: int = 0 - ) -> None: - self.f.seek(-field_size, os.SEEK_CUR) - if not new_field_size: - new_field_size = field_size - bytes_written = self.f.write( - struct.pack(self.endian + self._fmt(new_field_size), value) - ) - self._verify_bytes_written(bytes_written, new_field_size) - - def rewriteLastShortToLong(self, value: int) -> None: - self._rewriteLast(value, 2, 4) - - def rewriteLastShort(self, value: int) -> None: - return self._rewriteLast(value, 2) - - def rewriteLastLong(self, value: int) -> None: - return self._rewriteLast(value, 4) - - def _write(self, value: int, field_size: int) -> None: - bytes_written = self.f.write( - struct.pack(self.endian + self._fmt(field_size), value) - ) - self._verify_bytes_written(bytes_written, field_size) - - def writeShort(self, value: int) -> None: - self._write(value, 2) - - def writeLong(self, value: int) -> None: - self._write(value, 4) - - def close(self) -> None: - self.finalize() - if self.close_fp: - self.f.close() - - def fixIFD(self) -> None: - num_tags = self._read(8 if self._bigtiff else 2) - - for i in range(num_tags): - tag, field_type, count = struct.unpack( - self.tagFormat, self.f.read(12 if self._bigtiff else 8) - ) - - field_size = self.fieldSizes[field_type] - total_size = field_size * count - fmt_size = 8 if self._bigtiff else 4 - is_local = total_size <= fmt_size - if not is_local: - offset = self._read(fmt_size) + self.offsetOfNewPage - self._rewriteLast(offset, fmt_size) - - if tag in self.Tags: - cur_pos = self.f.tell() - - logger.debug( - "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d", - TiffTags.lookup(tag).name, - tag, - TYPES.get(field_type, "unknown"), - field_type, - field_size, - count, - ) - - if is_local: - self._fixOffsets(count, field_size) - self.f.seek(cur_pos + fmt_size) - else: - self.f.seek(offset) - self._fixOffsets(count, field_size) - self.f.seek(cur_pos) - - elif is_local: - # skip the locally stored value that is not an offset - self.f.seek(fmt_size, os.SEEK_CUR) - - def _fixOffsets(self, count: int, field_size: int) -> None: - for i in range(count): - offset = self._read(field_size) - offset += self.offsetOfNewPage - - new_field_size = 0 - if self._bigtiff and field_size in (2, 4) and offset >= 2**32: - # offset is now too large - we must convert long to long8 - new_field_size = 8 - elif field_size == 2 and offset >= 2**16: - # offset is now too large - we must convert short to long - new_field_size = 4 - if new_field_size: - if count != 1: - msg = "not implemented" - raise RuntimeError(msg) # XXX TODO - - # simple case - the offset is just one and therefore it is - # local (not referenced with another offset) - self._rewriteLast(offset, field_size, new_field_size) - # Move back past the new offset, past 'count', and before 'field_type' - rewind = -new_field_size - 4 - 2 - self.f.seek(rewind, os.SEEK_CUR) - self.writeShort(new_field_size) # rewrite the type - self.f.seek(2 - rewind, os.SEEK_CUR) - else: - self._rewriteLast(offset, field_size) - - def fixOffsets( - self, count: int, isShort: bool = False, isLong: bool = False - ) -> None: - if isShort: - field_size = 2 - elif isLong: - field_size = 4 - else: - field_size = 0 - return self._fixOffsets(count, field_size) - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - append_images = list(im.encoderinfo.get("append_images", [])) - if not hasattr(im, "n_frames") and not append_images: - return _save(im, fp, filename) - - cur_idx = im.tell() - try: - with AppendingTiffWriter(fp) as tf: - for ims in [im] + append_images: - encoderinfo = ims._attach_default_encoderinfo(im) - if not hasattr(ims, "encoderconfig"): - ims.encoderconfig = () - nfr = getattr(ims, "n_frames", 1) - - for idx in range(nfr): - ims.seek(idx) - ims.load() - _save(ims, tf, filename) - tf.newFrame() - ims.encoderinfo = encoderinfo - finally: - im.seek(cur_idx) - - -# -# -------------------------------------------------------------------- -# Register - -Image.register_open(TiffImageFile.format, TiffImageFile, _accept) -Image.register_save(TiffImageFile.format, _save) -Image.register_save_all(TiffImageFile.format, _save_all) - -Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"]) - -Image.register_mime(TiffImageFile.format, "image/tiff") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/TiffTags.py b/presentation/.venv/lib/python3.12/site-packages/PIL/TiffTags.py deleted file mode 100644 index 613a3b7..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/TiffTags.py +++ /dev/null @@ -1,566 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# TIFF tags -# -# This module provides clear-text names for various well-known -# TIFF tags. the TIFF codec works just fine without it. -# -# Copyright (c) Secret Labs AB 1999. -# -# See the README file for information on usage and redistribution. -# - -## -# This module provides constants and clear-text names for various -# well-known TIFF tags. -## -from __future__ import annotations - -from typing import NamedTuple - - -class _TagInfo(NamedTuple): - value: int | None - name: str - type: int | None - length: int | None - enum: dict[str, int] - - -class TagInfo(_TagInfo): - __slots__: list[str] = [] - - def __new__( - cls, - value: int | None = None, - name: str = "unknown", - type: int | None = None, - length: int | None = None, - enum: dict[str, int] | None = None, - ) -> TagInfo: - return super().__new__(cls, value, name, type, length, enum or {}) - - def cvt_enum(self, value: str) -> int | str: - # Using get will call hash(value), which can be expensive - # for some types (e.g. Fraction). Since self.enum is rarely - # used, it's usually better to test it first. - return self.enum.get(value, value) if self.enum else value - - -def lookup(tag: int, group: int | None = None) -> TagInfo: - """ - :param tag: Integer tag number - :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in - - .. versionadded:: 8.3.0 - - :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, - otherwise just populating the value and name from ``TAGS``. - If the tag is not recognized, "unknown" is returned for the name - - """ - - if group is not None: - info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None - else: - info = TAGS_V2.get(tag) - return info or TagInfo(tag, TAGS.get(tag, "unknown")) - - -## -# Map tag numbers to tag info. -# -# id: (Name, Type, Length[, enum_values]) -# -# The length here differs from the length in the tiff spec. For -# numbers, the tiff spec is for the number of fields returned. We -# agree here. For string-like types, the tiff spec uses the length of -# field in bytes. In Pillow, we are using the number of expected -# fields, in general 1 for string-like types. - - -BYTE = 1 -ASCII = 2 -SHORT = 3 -LONG = 4 -RATIONAL = 5 -SIGNED_BYTE = 6 -UNDEFINED = 7 -SIGNED_SHORT = 8 -SIGNED_LONG = 9 -SIGNED_RATIONAL = 10 -FLOAT = 11 -DOUBLE = 12 -IFD = 13 -LONG8 = 16 - -_tags_v2: dict[int, tuple[str, int, int] | tuple[str, int, int, dict[str, int]]] = { - 254: ("NewSubfileType", LONG, 1), - 255: ("SubfileType", SHORT, 1), - 256: ("ImageWidth", LONG, 1), - 257: ("ImageLength", LONG, 1), - 258: ("BitsPerSample", SHORT, 0), - 259: ( - "Compression", - SHORT, - 1, - { - "Uncompressed": 1, - "CCITT 1d": 2, - "Group 3 Fax": 3, - "Group 4 Fax": 4, - "LZW": 5, - "JPEG": 6, - "PackBits": 32773, - }, - ), - 262: ( - "PhotometricInterpretation", - SHORT, - 1, - { - "WhiteIsZero": 0, - "BlackIsZero": 1, - "RGB": 2, - "RGB Palette": 3, - "Transparency Mask": 4, - "CMYK": 5, - "YCbCr": 6, - "CieLAB": 8, - "CFA": 32803, # TIFF/EP, Adobe DNG - "LinearRaw": 32892, # Adobe DNG - }, - ), - 263: ("Threshholding", SHORT, 1), - 264: ("CellWidth", SHORT, 1), - 265: ("CellLength", SHORT, 1), - 266: ("FillOrder", SHORT, 1), - 269: ("DocumentName", ASCII, 1), - 270: ("ImageDescription", ASCII, 1), - 271: ("Make", ASCII, 1), - 272: ("Model", ASCII, 1), - 273: ("StripOffsets", LONG, 0), - 274: ("Orientation", SHORT, 1), - 277: ("SamplesPerPixel", SHORT, 1), - 278: ("RowsPerStrip", LONG, 1), - 279: ("StripByteCounts", LONG, 0), - 280: ("MinSampleValue", SHORT, 0), - 281: ("MaxSampleValue", SHORT, 0), - 282: ("XResolution", RATIONAL, 1), - 283: ("YResolution", RATIONAL, 1), - 284: ("PlanarConfiguration", SHORT, 1, {"Contiguous": 1, "Separate": 2}), - 285: ("PageName", ASCII, 1), - 286: ("XPosition", RATIONAL, 1), - 287: ("YPosition", RATIONAL, 1), - 288: ("FreeOffsets", LONG, 1), - 289: ("FreeByteCounts", LONG, 1), - 290: ("GrayResponseUnit", SHORT, 1), - 291: ("GrayResponseCurve", SHORT, 0), - 292: ("T4Options", LONG, 1), - 293: ("T6Options", LONG, 1), - 296: ("ResolutionUnit", SHORT, 1, {"none": 1, "inch": 2, "cm": 3}), - 297: ("PageNumber", SHORT, 2), - 301: ("TransferFunction", SHORT, 0), - 305: ("Software", ASCII, 1), - 306: ("DateTime", ASCII, 1), - 315: ("Artist", ASCII, 1), - 316: ("HostComputer", ASCII, 1), - 317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}), - 318: ("WhitePoint", RATIONAL, 2), - 319: ("PrimaryChromaticities", RATIONAL, 6), - 320: ("ColorMap", SHORT, 0), - 321: ("HalftoneHints", SHORT, 2), - 322: ("TileWidth", LONG, 1), - 323: ("TileLength", LONG, 1), - 324: ("TileOffsets", LONG, 0), - 325: ("TileByteCounts", LONG, 0), - 330: ("SubIFDs", LONG, 0), - 332: ("InkSet", SHORT, 1), - 333: ("InkNames", ASCII, 1), - 334: ("NumberOfInks", SHORT, 1), - 336: ("DotRange", SHORT, 0), - 337: ("TargetPrinter", ASCII, 1), - 338: ("ExtraSamples", SHORT, 0), - 339: ("SampleFormat", SHORT, 0), - 340: ("SMinSampleValue", DOUBLE, 0), - 341: ("SMaxSampleValue", DOUBLE, 0), - 342: ("TransferRange", SHORT, 6), - 347: ("JPEGTables", UNDEFINED, 1), - # obsolete JPEG tags - 512: ("JPEGProc", SHORT, 1), - 513: ("JPEGInterchangeFormat", LONG, 1), - 514: ("JPEGInterchangeFormatLength", LONG, 1), - 515: ("JPEGRestartInterval", SHORT, 1), - 517: ("JPEGLosslessPredictors", SHORT, 0), - 518: ("JPEGPointTransforms", SHORT, 0), - 519: ("JPEGQTables", LONG, 0), - 520: ("JPEGDCTables", LONG, 0), - 521: ("JPEGACTables", LONG, 0), - 529: ("YCbCrCoefficients", RATIONAL, 3), - 530: ("YCbCrSubSampling", SHORT, 2), - 531: ("YCbCrPositioning", SHORT, 1), - 532: ("ReferenceBlackWhite", RATIONAL, 6), - 700: ("XMP", BYTE, 0), - # Four private SGI tags - 32995: ("Matteing", SHORT, 1), - 32996: ("DataType", SHORT, 0), - 32997: ("ImageDepth", LONG, 1), - 32998: ("TileDepth", LONG, 1), - 33432: ("Copyright", ASCII, 1), - 33723: ("IptcNaaInfo", UNDEFINED, 1), - 34377: ("PhotoshopInfo", BYTE, 0), - # FIXME add more tags here - 34665: ("ExifIFD", LONG, 1), - 34675: ("ICCProfile", UNDEFINED, 1), - 34853: ("GPSInfoIFD", LONG, 1), - 36864: ("ExifVersion", UNDEFINED, 1), - 37724: ("ImageSourceData", UNDEFINED, 1), - 40965: ("InteroperabilityIFD", LONG, 1), - 41730: ("CFAPattern", UNDEFINED, 1), - # MPInfo - 45056: ("MPFVersion", UNDEFINED, 1), - 45057: ("NumberOfImages", LONG, 1), - 45058: ("MPEntry", UNDEFINED, 1), - 45059: ("ImageUIDList", UNDEFINED, 0), # UNDONE, check - 45060: ("TotalFrames", LONG, 1), - 45313: ("MPIndividualNum", LONG, 1), - 45569: ("PanOrientation", LONG, 1), - 45570: ("PanOverlap_H", RATIONAL, 1), - 45571: ("PanOverlap_V", RATIONAL, 1), - 45572: ("BaseViewpointNum", LONG, 1), - 45573: ("ConvergenceAngle", SIGNED_RATIONAL, 1), - 45574: ("BaselineLength", RATIONAL, 1), - 45575: ("VerticalDivergence", SIGNED_RATIONAL, 1), - 45576: ("AxisDistance_X", SIGNED_RATIONAL, 1), - 45577: ("AxisDistance_Y", SIGNED_RATIONAL, 1), - 45578: ("AxisDistance_Z", SIGNED_RATIONAL, 1), - 45579: ("YawAngle", SIGNED_RATIONAL, 1), - 45580: ("PitchAngle", SIGNED_RATIONAL, 1), - 45581: ("RollAngle", SIGNED_RATIONAL, 1), - 40960: ("FlashPixVersion", UNDEFINED, 1), - 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), - 50780: ("BestQualityScale", RATIONAL, 1), - 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one - 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 -} -_tags_v2_groups = { - # ExifIFD - 34665: { - 36864: ("ExifVersion", UNDEFINED, 1), - 40960: ("FlashPixVersion", UNDEFINED, 1), - 40965: ("InteroperabilityIFD", LONG, 1), - 41730: ("CFAPattern", UNDEFINED, 1), - }, - # GPSInfoIFD - 34853: { - 0: ("GPSVersionID", BYTE, 4), - 1: ("GPSLatitudeRef", ASCII, 2), - 2: ("GPSLatitude", RATIONAL, 3), - 3: ("GPSLongitudeRef", ASCII, 2), - 4: ("GPSLongitude", RATIONAL, 3), - 5: ("GPSAltitudeRef", BYTE, 1), - 6: ("GPSAltitude", RATIONAL, 1), - 7: ("GPSTimeStamp", RATIONAL, 3), - 8: ("GPSSatellites", ASCII, 0), - 9: ("GPSStatus", ASCII, 2), - 10: ("GPSMeasureMode", ASCII, 2), - 11: ("GPSDOP", RATIONAL, 1), - 12: ("GPSSpeedRef", ASCII, 2), - 13: ("GPSSpeed", RATIONAL, 1), - 14: ("GPSTrackRef", ASCII, 2), - 15: ("GPSTrack", RATIONAL, 1), - 16: ("GPSImgDirectionRef", ASCII, 2), - 17: ("GPSImgDirection", RATIONAL, 1), - 18: ("GPSMapDatum", ASCII, 0), - 19: ("GPSDestLatitudeRef", ASCII, 2), - 20: ("GPSDestLatitude", RATIONAL, 3), - 21: ("GPSDestLongitudeRef", ASCII, 2), - 22: ("GPSDestLongitude", RATIONAL, 3), - 23: ("GPSDestBearingRef", ASCII, 2), - 24: ("GPSDestBearing", RATIONAL, 1), - 25: ("GPSDestDistanceRef", ASCII, 2), - 26: ("GPSDestDistance", RATIONAL, 1), - 27: ("GPSProcessingMethod", UNDEFINED, 0), - 28: ("GPSAreaInformation", UNDEFINED, 0), - 29: ("GPSDateStamp", ASCII, 11), - 30: ("GPSDifferential", SHORT, 1), - }, - # InteroperabilityIFD - 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)}, -} - -# Legacy Tags structure -# these tags aren't included above, but were in the previous versions -TAGS: dict[int | tuple[int, int], str] = { - 347: "JPEGTables", - 700: "XMP", - # Additional Exif Info - 32932: "Wang Annotation", - 33434: "ExposureTime", - 33437: "FNumber", - 33445: "MD FileTag", - 33446: "MD ScalePixel", - 33447: "MD ColorTable", - 33448: "MD LabName", - 33449: "MD SampleInfo", - 33450: "MD PrepDate", - 33451: "MD PrepTime", - 33452: "MD FileUnits", - 33550: "ModelPixelScaleTag", - 33723: "IptcNaaInfo", - 33918: "INGR Packet Data Tag", - 33919: "INGR Flag Registers", - 33920: "IrasB Transformation Matrix", - 33922: "ModelTiepointTag", - 34264: "ModelTransformationTag", - 34377: "PhotoshopInfo", - 34735: "GeoKeyDirectoryTag", - 34736: "GeoDoubleParamsTag", - 34737: "GeoAsciiParamsTag", - 34850: "ExposureProgram", - 34852: "SpectralSensitivity", - 34855: "ISOSpeedRatings", - 34856: "OECF", - 34864: "SensitivityType", - 34865: "StandardOutputSensitivity", - 34866: "RecommendedExposureIndex", - 34867: "ISOSpeed", - 34868: "ISOSpeedLatitudeyyy", - 34869: "ISOSpeedLatitudezzz", - 34908: "HylaFAX FaxRecvParams", - 34909: "HylaFAX FaxSubAddress", - 34910: "HylaFAX FaxRecvTime", - 36864: "ExifVersion", - 36867: "DateTimeOriginal", - 36868: "DateTimeDigitized", - 37121: "ComponentsConfiguration", - 37122: "CompressedBitsPerPixel", - 37724: "ImageSourceData", - 37377: "ShutterSpeedValue", - 37378: "ApertureValue", - 37379: "BrightnessValue", - 37380: "ExposureBiasValue", - 37381: "MaxApertureValue", - 37382: "SubjectDistance", - 37383: "MeteringMode", - 37384: "LightSource", - 37385: "Flash", - 37386: "FocalLength", - 37396: "SubjectArea", - 37500: "MakerNote", - 37510: "UserComment", - 37520: "SubSec", - 37521: "SubSecTimeOriginal", - 37522: "SubsecTimeDigitized", - 40960: "FlashPixVersion", - 40961: "ColorSpace", - 40962: "PixelXDimension", - 40963: "PixelYDimension", - 40964: "RelatedSoundFile", - 40965: "InteroperabilityIFD", - 41483: "FlashEnergy", - 41484: "SpatialFrequencyResponse", - 41486: "FocalPlaneXResolution", - 41487: "FocalPlaneYResolution", - 41488: "FocalPlaneResolutionUnit", - 41492: "SubjectLocation", - 41493: "ExposureIndex", - 41495: "SensingMethod", - 41728: "FileSource", - 41729: "SceneType", - 41730: "CFAPattern", - 41985: "CustomRendered", - 41986: "ExposureMode", - 41987: "WhiteBalance", - 41988: "DigitalZoomRatio", - 41989: "FocalLengthIn35mmFilm", - 41990: "SceneCaptureType", - 41991: "GainControl", - 41992: "Contrast", - 41993: "Saturation", - 41994: "Sharpness", - 41995: "DeviceSettingDescription", - 41996: "SubjectDistanceRange", - 42016: "ImageUniqueID", - 42032: "CameraOwnerName", - 42033: "BodySerialNumber", - 42034: "LensSpecification", - 42035: "LensMake", - 42036: "LensModel", - 42037: "LensSerialNumber", - 42112: "GDAL_METADATA", - 42113: "GDAL_NODATA", - 42240: "Gamma", - 50215: "Oce Scanjob Description", - 50216: "Oce Application Selector", - 50217: "Oce Identification Number", - 50218: "Oce ImageLogic Characteristics", - # Adobe DNG - 50706: "DNGVersion", - 50707: "DNGBackwardVersion", - 50708: "UniqueCameraModel", - 50709: "LocalizedCameraModel", - 50710: "CFAPlaneColor", - 50711: "CFALayout", - 50712: "LinearizationTable", - 50713: "BlackLevelRepeatDim", - 50714: "BlackLevel", - 50715: "BlackLevelDeltaH", - 50716: "BlackLevelDeltaV", - 50717: "WhiteLevel", - 50718: "DefaultScale", - 50719: "DefaultCropOrigin", - 50720: "DefaultCropSize", - 50721: "ColorMatrix1", - 50722: "ColorMatrix2", - 50723: "CameraCalibration1", - 50724: "CameraCalibration2", - 50725: "ReductionMatrix1", - 50726: "ReductionMatrix2", - 50727: "AnalogBalance", - 50728: "AsShotNeutral", - 50729: "AsShotWhiteXY", - 50730: "BaselineExposure", - 50731: "BaselineNoise", - 50732: "BaselineSharpness", - 50733: "BayerGreenSplit", - 50734: "LinearResponseLimit", - 50735: "CameraSerialNumber", - 50736: "LensInfo", - 50737: "ChromaBlurRadius", - 50738: "AntiAliasStrength", - 50740: "DNGPrivateData", - 50778: "CalibrationIlluminant1", - 50779: "CalibrationIlluminant2", - 50784: "Alias Layer Metadata", -} - -TAGS_V2: dict[int, TagInfo] = {} -TAGS_V2_GROUPS: dict[int, dict[int, TagInfo]] = {} - - -def _populate() -> None: - for k, v in _tags_v2.items(): - # Populate legacy structure. - TAGS[k] = v[0] - if len(v) == 4: - for sk, sv in v[3].items(): - TAGS[(k, sv)] = sk - - TAGS_V2[k] = TagInfo(k, *v) - - for group, tags in _tags_v2_groups.items(): - TAGS_V2_GROUPS[group] = {k: TagInfo(k, *v) for k, v in tags.items()} - - -_populate() -## -# Map type numbers to type names -- defined in ImageFileDirectory. - -TYPES: dict[int, str] = {} - -# -# These tags are handled by default in libtiff, without -# adding to the custom dictionary. From tif_dir.c, searching for -# case TIFFTAG in the _TIFFVSetField function: -# Line: item. -# 148: case TIFFTAG_SUBFILETYPE: -# 151: case TIFFTAG_IMAGEWIDTH: -# 154: case TIFFTAG_IMAGELENGTH: -# 157: case TIFFTAG_BITSPERSAMPLE: -# 181: case TIFFTAG_COMPRESSION: -# 202: case TIFFTAG_PHOTOMETRIC: -# 205: case TIFFTAG_THRESHHOLDING: -# 208: case TIFFTAG_FILLORDER: -# 214: case TIFFTAG_ORIENTATION: -# 221: case TIFFTAG_SAMPLESPERPIXEL: -# 228: case TIFFTAG_ROWSPERSTRIP: -# 238: case TIFFTAG_MINSAMPLEVALUE: -# 241: case TIFFTAG_MAXSAMPLEVALUE: -# 244: case TIFFTAG_SMINSAMPLEVALUE: -# 247: case TIFFTAG_SMAXSAMPLEVALUE: -# 250: case TIFFTAG_XRESOLUTION: -# 256: case TIFFTAG_YRESOLUTION: -# 262: case TIFFTAG_PLANARCONFIG: -# 268: case TIFFTAG_XPOSITION: -# 271: case TIFFTAG_YPOSITION: -# 274: case TIFFTAG_RESOLUTIONUNIT: -# 280: case TIFFTAG_PAGENUMBER: -# 284: case TIFFTAG_HALFTONEHINTS: -# 288: case TIFFTAG_COLORMAP: -# 294: case TIFFTAG_EXTRASAMPLES: -# 298: case TIFFTAG_MATTEING: -# 305: case TIFFTAG_TILEWIDTH: -# 316: case TIFFTAG_TILELENGTH: -# 327: case TIFFTAG_TILEDEPTH: -# 333: case TIFFTAG_DATATYPE: -# 344: case TIFFTAG_SAMPLEFORMAT: -# 361: case TIFFTAG_IMAGEDEPTH: -# 364: case TIFFTAG_SUBIFD: -# 376: case TIFFTAG_YCBCRPOSITIONING: -# 379: case TIFFTAG_YCBCRSUBSAMPLING: -# 383: case TIFFTAG_TRANSFERFUNCTION: -# 389: case TIFFTAG_REFERENCEBLACKWHITE: -# 393: case TIFFTAG_INKNAMES: - -# Following pseudo-tags are also handled by default in libtiff: -# TIFFTAG_JPEGQUALITY 65537 - -# some of these are not in our TAGS_V2 dict and were included from tiff.h - -# This list also exists in encode.c -LIBTIFF_CORE = { - 255, - 256, - 257, - 258, - 259, - 262, - 263, - 266, - 274, - 277, - 278, - 280, - 281, - 340, - 341, - 282, - 283, - 284, - 286, - 287, - 296, - 297, - 321, - 320, - 338, - 32995, - 322, - 323, - 32998, - 32996, - 339, - 32997, - 330, - 531, - 530, - 301, - 532, - 333, - # as above - 269, # this has been in our tests forever, and works - 65537, -} - -LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes -LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff -LIBTIFF_CORE.remove(323) # Tiled images - -# Note to advanced users: There may be combinations of these -# parameters and values that when added properly, will work and -# produce valid tiff images that may work in your application. -# It is safe to add and remove tags from this set from Pillow's point -# of view so long as you test against libtiff. diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/WalImageFile.py b/presentation/.venv/lib/python3.12/site-packages/PIL/WalImageFile.py deleted file mode 100644 index 07bbf74..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/WalImageFile.py +++ /dev/null @@ -1,129 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# WAL file handling -# -# History: -# 2003-04-23 fl created -# -# Copyright (c) 2003 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -""" -This reader is based on the specification available from: -https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml -and has been tested with a few sample files found using google. - -.. note:: - This format cannot be automatically recognized, so the reader - is not registered for use with :py:func:`PIL.Image.open()`. - To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. -""" - -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile -from ._binary import i32le as i32 -from ._typing import StrOrBytesPath - - -class WalImageFile(ImageFile.ImageFile): - format = "WAL" - format_description = "Quake2 Texture" - - def _open(self) -> None: - self._mode = "P" - - # read header fields - assert self.fp is not None - header = self.fp.read(32 + 24 + 32 + 12) - self._size = i32(header, 32), i32(header, 36) - Image._decompression_bomb_check(self.size) - - # load pixel data - offset = i32(header, 40) - self.fp.seek(offset) - - # strings are null-terminated - self.info["name"] = header[:32].split(b"\0", 1)[0] - if next_name := header[56 : 56 + 32].split(b"\0", 1)[0]: - self.info["next_name"] = next_name - - def load(self) -> Image.core.PixelAccess | None: - if self._im is None: - assert self.fp is not None - self.im = Image.core.new(self.mode, self.size) - self.frombytes(self.fp.read(self.size[0] * self.size[1])) - self.putpalette(quake2palette) - return Image.Image.load(self) - - -def open(filename: StrOrBytesPath | IO[bytes]) -> WalImageFile: - """ - Load texture from a Quake2 WAL texture file. - - By default, a Quake2 standard palette is attached to the texture. - To override the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. - - :param filename: WAL file name, or an opened file handle. - :returns: An image instance. - """ - return WalImageFile(filename) - - -quake2palette = ( - # default palette taken from piffo 0.93 by Hans Häggström - b"\x01\x01\x01\x0b\x0b\x0b\x12\x12\x12\x17\x17\x17\x1b\x1b\x1b\x1e" - b"\x1e\x1e\x22\x22\x22\x26\x26\x26\x29\x29\x29\x2c\x2c\x2c\x2f\x2f" - b"\x2f\x32\x32\x32\x35\x35\x35\x37\x37\x37\x3a\x3a\x3a\x3c\x3c\x3c" - b"\x24\x1e\x13\x22\x1c\x12\x20\x1b\x12\x1f\x1a\x10\x1d\x19\x10\x1b" - b"\x17\x0f\x1a\x16\x0f\x18\x14\x0d\x17\x13\x0d\x16\x12\x0d\x14\x10" - b"\x0b\x13\x0f\x0b\x10\x0d\x0a\x0f\x0b\x0a\x0d\x0b\x07\x0b\x0a\x07" - b"\x23\x23\x26\x22\x22\x25\x22\x20\x23\x21\x1f\x22\x20\x1e\x20\x1f" - b"\x1d\x1e\x1d\x1b\x1c\x1b\x1a\x1a\x1a\x19\x19\x18\x17\x17\x17\x16" - b"\x16\x14\x14\x14\x13\x13\x13\x10\x10\x10\x0f\x0f\x0f\x0d\x0d\x0d" - b"\x2d\x28\x20\x29\x24\x1c\x27\x22\x1a\x25\x1f\x17\x38\x2e\x1e\x31" - b"\x29\x1a\x2c\x25\x17\x26\x20\x14\x3c\x30\x14\x37\x2c\x13\x33\x28" - b"\x12\x2d\x24\x10\x28\x1f\x0f\x22\x1a\x0b\x1b\x14\x0a\x13\x0f\x07" - b"\x31\x1a\x16\x30\x17\x13\x2e\x16\x10\x2c\x14\x0d\x2a\x12\x0b\x27" - b"\x0f\x0a\x25\x0f\x07\x21\x0d\x01\x1e\x0b\x01\x1c\x0b\x01\x1a\x0b" - b"\x01\x18\x0a\x01\x16\x0a\x01\x13\x0a\x01\x10\x07\x01\x0d\x07\x01" - b"\x29\x23\x1e\x27\x21\x1c\x26\x20\x1b\x25\x1f\x1a\x23\x1d\x19\x21" - b"\x1c\x18\x20\x1b\x17\x1e\x19\x16\x1c\x18\x14\x1b\x17\x13\x19\x14" - b"\x10\x17\x13\x0f\x14\x10\x0d\x12\x0f\x0b\x0f\x0b\x0a\x0b\x0a\x07" - b"\x26\x1a\x0f\x23\x19\x0f\x20\x17\x0f\x1c\x16\x0f\x19\x13\x0d\x14" - b"\x10\x0b\x10\x0d\x0a\x0b\x0a\x07\x33\x22\x1f\x35\x29\x26\x37\x2f" - b"\x2d\x39\x35\x34\x37\x39\x3a\x33\x37\x39\x30\x34\x36\x2b\x31\x34" - b"\x27\x2e\x31\x22\x2b\x2f\x1d\x28\x2c\x17\x25\x2a\x0f\x20\x26\x0d" - b"\x1e\x25\x0b\x1c\x22\x0a\x1b\x20\x07\x19\x1e\x07\x17\x1b\x07\x14" - b"\x18\x01\x12\x16\x01\x0f\x12\x01\x0b\x0d\x01\x07\x0a\x01\x01\x01" - b"\x2c\x21\x21\x2a\x1f\x1f\x29\x1d\x1d\x27\x1c\x1c\x26\x1a\x1a\x24" - b"\x18\x18\x22\x17\x17\x21\x16\x16\x1e\x13\x13\x1b\x12\x12\x18\x10" - b"\x10\x16\x0d\x0d\x12\x0b\x0b\x0d\x0a\x0a\x0a\x07\x07\x01\x01\x01" - b"\x2e\x30\x29\x2d\x2e\x27\x2b\x2c\x26\x2a\x2a\x24\x28\x29\x23\x27" - b"\x27\x21\x26\x26\x1f\x24\x24\x1d\x22\x22\x1c\x1f\x1f\x1a\x1c\x1c" - b"\x18\x19\x19\x16\x17\x17\x13\x13\x13\x10\x0f\x0f\x0d\x0b\x0b\x0a" - b"\x30\x1e\x1b\x2d\x1c\x19\x2c\x1a\x17\x2a\x19\x14\x28\x17\x13\x26" - b"\x16\x10\x24\x13\x0f\x21\x12\x0d\x1f\x10\x0b\x1c\x0f\x0a\x19\x0d" - b"\x0a\x16\x0b\x07\x12\x0a\x07\x0f\x07\x01\x0a\x01\x01\x01\x01\x01" - b"\x28\x29\x38\x26\x27\x36\x25\x26\x34\x24\x24\x31\x22\x22\x2f\x20" - b"\x21\x2d\x1e\x1f\x2a\x1d\x1d\x27\x1b\x1b\x25\x19\x19\x21\x17\x17" - b"\x1e\x14\x14\x1b\x13\x12\x17\x10\x0f\x13\x0d\x0b\x0f\x0a\x07\x07" - b"\x2f\x32\x29\x2d\x30\x26\x2b\x2e\x24\x29\x2c\x21\x27\x2a\x1e\x25" - b"\x28\x1c\x23\x26\x1a\x21\x25\x18\x1e\x22\x14\x1b\x1f\x10\x19\x1c" - b"\x0d\x17\x1a\x0a\x13\x17\x07\x10\x13\x01\x0d\x0f\x01\x0a\x0b\x01" - b"\x01\x3f\x01\x13\x3c\x0b\x1b\x39\x10\x20\x35\x14\x23\x31\x17\x23" - b"\x2d\x18\x23\x29\x18\x3f\x3f\x3f\x3f\x3f\x39\x3f\x3f\x31\x3f\x3f" - b"\x2a\x3f\x3f\x20\x3f\x3f\x14\x3f\x3c\x12\x3f\x39\x0f\x3f\x35\x0b" - b"\x3f\x32\x07\x3f\x2d\x01\x3d\x2a\x01\x3b\x26\x01\x39\x21\x01\x37" - b"\x1d\x01\x34\x1a\x01\x32\x16\x01\x2f\x12\x01\x2d\x0f\x01\x2a\x0b" - b"\x01\x27\x07\x01\x23\x01\x01\x1d\x01\x01\x17\x01\x01\x10\x01\x01" - b"\x3d\x01\x01\x19\x19\x3f\x3f\x01\x01\x01\x01\x3f\x16\x16\x13\x10" - b"\x10\x0f\x0d\x0d\x0b\x3c\x2e\x2a\x36\x27\x20\x30\x21\x18\x29\x1b" - b"\x10\x3c\x39\x37\x37\x32\x2f\x31\x2c\x28\x2b\x26\x21\x30\x22\x20" -) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/WebPImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/WebPImagePlugin.py deleted file mode 100644 index e20e40d..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/WebPImagePlugin.py +++ /dev/null @@ -1,317 +0,0 @@ -from __future__ import annotations - -from io import BytesIO - -from . import Image, ImageFile - -try: - from . import _webp - - SUPPORTED = True -except ImportError: - SUPPORTED = False - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import IO, Any - -_VP8_MODES_BY_IDENTIFIER = { - b"VP8 ": "RGB", - b"VP8X": "RGBA", - b"VP8L": "RGBA", # lossless -} - - -def _accept(prefix: bytes) -> bool | str: - is_riff_file_format = prefix.startswith(b"RIFF") - is_webp_file = prefix[8:12] == b"WEBP" - is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER - - if is_riff_file_format and is_webp_file and is_valid_vp8_mode: - if not SUPPORTED: - return ( - "image file could not be identified because WEBP support not installed" - ) - return True - return False - - -class WebPImageFile(ImageFile.ImageFile): - format = "WEBP" - format_description = "WebP image" - __loaded = 0 - __logical_frame = 0 - - def _open(self) -> None: - # Use the newer AnimDecoder API to parse the (possibly) animated file, - # and access muxed chunks like ICC/EXIF/XMP. - assert self.fp is not None - self._decoder = _webp.WebPAnimDecoder(self.fp.read()) - - # Get info from decoder - self._size, self.info["loop"], bgcolor, self.n_frames, self.rawmode = ( - self._decoder.get_info() - ) - self.info["background"] = ( - (bgcolor >> 16) & 0xFF, # R - (bgcolor >> 8) & 0xFF, # G - bgcolor & 0xFF, # B - (bgcolor >> 24) & 0xFF, # A - ) - self.is_animated = self.n_frames > 1 - self._mode = "RGB" if self.rawmode == "RGBX" else self.rawmode - - # Attempt to read ICC / EXIF / XMP chunks from file - for key, chunk_name in { - "icc_profile": "ICCP", - "exif": "EXIF", - "xmp": "XMP ", - }.items(): - if value := self._decoder.get_chunk(chunk_name): - self.info[key] = value - - # Initialize seek state - self._reset(reset=False) - - def _getexif(self) -> dict[int, Any] | None: - if "exif" not in self.info: - return None - return self.getexif()._get_merged_dict() - - def seek(self, frame: int) -> None: - if not self._seek_check(frame): - return - - # Set logical frame to requested position - self.__logical_frame = frame - - def _reset(self, reset: bool = True) -> None: - if reset: - self._decoder.reset() - self.__physical_frame = 0 - self.__loaded = -1 - self.__timestamp = 0 - - def _get_next(self) -> tuple[bytes, int, int]: - # Get next frame - ret = self._decoder.get_next() - self.__physical_frame += 1 - - # Check if an error occurred - if ret is None: - self._reset() # Reset just to be safe - self.seek(0) - msg = "failed to decode next frame in WebP file" - raise EOFError(msg) - - # Compute duration - data, timestamp = ret - duration = timestamp - self.__timestamp - self.__timestamp = timestamp - - # libwebp gives frame end, adjust to start of frame - timestamp -= duration - return data, timestamp, duration - - def _seek(self, frame: int) -> None: - if self.__physical_frame == frame: - return # Nothing to do - if frame < self.__physical_frame: - self._reset() # Rewind to beginning - while self.__physical_frame < frame: - self._get_next() # Advance to the requested frame - - def load(self) -> Image.core.PixelAccess | None: - if self.__loaded != self.__logical_frame: - self._seek(self.__logical_frame) - - # We need to load the image data for this frame - data, self.info["timestamp"], self.info["duration"] = self._get_next() - self.__loaded = self.__logical_frame - - # Set tile - if self.fp and self._exclusive_fp: - self.fp.close() - self.fp = BytesIO(data) - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)] - - return super().load() - - def load_seek(self, pos: int) -> None: - pass - - def tell(self) -> int: - return self.__logical_frame - - -def _convert_frame(im: Image.Image) -> Image.Image: - # Make sure image mode is supported - if im.mode not in ("RGBX", "RGBA", "RGB"): - im = im.convert("RGBA" if im.has_transparency_data else "RGB") - return im - - -def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - encoderinfo = im.encoderinfo.copy() - append_images = list(encoderinfo.get("append_images", [])) - - # If total frame count is 1, then save using the legacy API, which - # will preserve non-alpha modes - total = 0 - for ims in [im] + append_images: - total += getattr(ims, "n_frames", 1) - if total == 1: - _save(im, fp, filename) - return - - background: int | tuple[int, ...] = (0, 0, 0, 0) - if "background" in encoderinfo: - background = encoderinfo["background"] - elif "background" in im.info: - background = im.info["background"] - if isinstance(background, int): - # GifImagePlugin stores a global color table index in - # info["background"]. So it must be converted to an RGBA value - palette = im.getpalette() - if palette: - r, g, b = palette[background * 3 : (background + 1) * 3] - background = (r, g, b, 255) - else: - background = (background, background, background, 255) - - duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) - loop = im.encoderinfo.get("loop", 0) - minimize_size = im.encoderinfo.get("minimize_size", False) - kmin = im.encoderinfo.get("kmin", None) - kmax = im.encoderinfo.get("kmax", None) - allow_mixed = im.encoderinfo.get("allow_mixed", False) - verbose = False - lossless = im.encoderinfo.get("lossless", False) - quality = im.encoderinfo.get("quality", 80) - alpha_quality = im.encoderinfo.get("alpha_quality", 100) - method = im.encoderinfo.get("method", 0) - icc_profile = im.encoderinfo.get("icc_profile") or "" - exif = im.encoderinfo.get("exif", "") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - xmp = im.encoderinfo.get("xmp", "") - if allow_mixed: - lossless = False - - # Sensible keyframe defaults are from gif2webp.c script - if kmin is None: - kmin = 9 if lossless else 3 - if kmax is None: - kmax = 17 if lossless else 5 - - # Validate background color - if ( - not isinstance(background, (list, tuple)) - or len(background) != 4 - or not all(0 <= v < 256 for v in background) - ): - msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}" - raise OSError(msg) - - # Convert to packed uint - bg_r, bg_g, bg_b, bg_a = background - background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0) - - # Setup the WebP animation encoder - enc = _webp.WebPAnimEncoder( - im.size, - background, - loop, - minimize_size, - kmin, - kmax, - allow_mixed, - verbose, - ) - - # Add each frame - frame_idx = 0 - timestamp = 0 - cur_idx = im.tell() - try: - for ims in [im] + append_images: - # Get number of frames in this image - nfr = getattr(ims, "n_frames", 1) - - for idx in range(nfr): - ims.seek(idx) - - frame = _convert_frame(ims) - - # Append the frame to the animation encoder - enc.add( - frame.getim(), - round(timestamp), - lossless, - quality, - alpha_quality, - method, - ) - - # Update timestamp and frame index - if isinstance(duration, (list, tuple)): - timestamp += duration[frame_idx] - else: - timestamp += duration - frame_idx += 1 - - finally: - im.seek(cur_idx) - - # Force encoder to flush frames - enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0) - - # Get the final output from the encoder - data = enc.assemble(icc_profile, exif, xmp) - if data is None: - msg = "cannot write file as WebP (encoder returned None)" - raise OSError(msg) - - fp.write(data) - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - lossless = im.encoderinfo.get("lossless", False) - quality = im.encoderinfo.get("quality", 80) - alpha_quality = im.encoderinfo.get("alpha_quality", 100) - icc_profile = im.encoderinfo.get("icc_profile") or "" - exif = im.encoderinfo.get("exif", b"") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - if exif.startswith(b"Exif\x00\x00"): - exif = exif[6:] - xmp = im.encoderinfo.get("xmp", "") - method = im.encoderinfo.get("method", 4) - exact = 1 if im.encoderinfo.get("exact") else 0 - - im = _convert_frame(im) - - data = _webp.WebPEncode( - im.getim(), - lossless, - float(quality), - float(alpha_quality), - icc_profile, - method, - exact, - exif, - xmp, - ) - if data is None: - msg = "cannot write file as WebP (encoder returned None)" - raise OSError(msg) - - fp.write(data) - - -Image.register_open(WebPImageFile.format, WebPImageFile, _accept) -if SUPPORTED: - Image.register_save(WebPImageFile.format, _save) - Image.register_save_all(WebPImageFile.format, _save_all) - Image.register_extension(WebPImageFile.format, ".webp") - Image.register_mime(WebPImageFile.format, "image/webp") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/WmfImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/WmfImagePlugin.py deleted file mode 100644 index f5e2447..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/WmfImagePlugin.py +++ /dev/null @@ -1,183 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# WMF stub codec -# -# history: -# 1996-12-14 fl Created -# 2004-02-22 fl Turned into a stub driver -# 2004-02-23 fl Added EMF support -# -# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. -# Copyright (c) Fredrik Lundh 1996. -# -# See the README file for information on usage and redistribution. -# -# WMF/EMF reference documentation: -# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-WMF/[MS-WMF].pdf -# http://wvware.sourceforge.net/caolan/index.html -# http://wvware.sourceforge.net/caolan/ora-wmf.html -from __future__ import annotations - -from typing import IO - -from . import Image, ImageFile -from ._binary import i16le as word -from ._binary import si16le as short -from ._binary import si32le as _long - -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific WMF image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -if hasattr(Image.core, "drawwmf"): - # install default handler (windows only) - - class WmfHandler(ImageFile.StubHandler): - def open(self, im: ImageFile.StubImageFile) -> None: - self.bbox = im.info["wmf_bbox"] - - def load(self, im: ImageFile.StubImageFile) -> Image.Image: - assert im.fp is not None - im.fp.seek(0) # rewind - return Image.frombytes( - "RGB", - im.size, - Image.core.drawwmf(im.fp.read(), im.size, self.bbox), - "raw", - "BGR", - (im.size[0] * 3 + 3) & -4, - -1, - ) - - register_handler(WmfHandler()) - -# -# -------------------------------------------------------------------- -# Read WMF file - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith((b"\xd7\xcd\xc6\x9a\x00\x00", b"\x01\x00\x00\x00")) - - -## -# Image plugin for Windows metafiles. - - -class WmfStubImageFile(ImageFile.StubImageFile): - format = "WMF" - format_description = "Windows Metafile" - - def _open(self) -> None: - # check placeable header - assert self.fp is not None - s = self.fp.read(44) - - if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): - # placeable windows metafile - - # get units per inch - inch = word(s, 14) - if inch == 0: - msg = "Invalid inch" - raise ValueError(msg) - self._inch: tuple[float, float] = inch, inch - - # get bounding box - x0 = short(s, 6) - y0 = short(s, 8) - x1 = short(s, 10) - y1 = short(s, 12) - - # normalize size to 72 dots per inch - self.info["dpi"] = 72 - size = ( - (x1 - x0) * self.info["dpi"] // inch, - (y1 - y0) * self.info["dpi"] // inch, - ) - - self.info["wmf_bbox"] = x0, y0, x1, y1 - - # sanity check (standard metafile header) - if s[22:26] != b"\x01\x00\t\x00": - msg = "Unsupported WMF file format" - raise SyntaxError(msg) - - elif s.startswith(b"\x01\x00\x00\x00") and s[40:44] == b" EMF": - # enhanced metafile - - # get bounding box - x0 = _long(s, 8) - y0 = _long(s, 12) - x1 = _long(s, 16) - y1 = _long(s, 20) - - # get frame (in 0.01 millimeter units) - frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36) - - size = x1 - x0, y1 - y0 - - # calculate dots per inch from bbox and frame - xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0]) - ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) - - self.info["wmf_bbox"] = x0, y0, x1, y1 - - if xdpi == ydpi: - self.info["dpi"] = xdpi - else: - self.info["dpi"] = xdpi, ydpi - self._inch = xdpi, ydpi - - else: - msg = "Unsupported file format" - raise SyntaxError(msg) - - self._mode = "RGB" - self._size = size - - def _load(self) -> ImageFile.StubHandler | None: - return _handler - - def load( - self, dpi: float | tuple[float, float] | None = None - ) -> Image.core.PixelAccess | None: - if dpi is not None: - self.info["dpi"] = dpi - x0, y0, x1, y1 = self.info["wmf_bbox"] - if not isinstance(dpi, tuple): - dpi = dpi, dpi - self._size = ( - int((x1 - x0) * dpi[0] / self._inch[0]), - int((y1 - y0) * dpi[1] / self._inch[1]), - ) - return super().load() - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): - msg = "WMF save handler not installed" - raise OSError(msg) - _handler.save(im, fp, filename) - - -# -# -------------------------------------------------------------------- -# Registry stuff - - -Image.register_open(WmfStubImageFile.format, WmfStubImageFile, _accept) -Image.register_save(WmfStubImageFile.format, _save) - -Image.register_extensions(WmfStubImageFile.format, [".wmf", ".emf"]) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/XVThumbImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/XVThumbImagePlugin.py deleted file mode 100644 index 192c041..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/XVThumbImagePlugin.py +++ /dev/null @@ -1,83 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# XV Thumbnail file handler by Charles E. "Gene" Cash -# (gcash@magicnet.net) -# -# see xvcolor.c and xvbrowse.c in the sources to John Bradley's XV, -# available from ftp://ftp.cis.upenn.edu/pub/xv/ -# -# history: -# 98-08-15 cec created (b/w only) -# 98-12-09 cec added color palette -# 98-12-28 fl added to PIL (with only a few very minor modifications) -# -# To do: -# FIXME: make save work (this requires quantization support) -# -from __future__ import annotations - -from . import Image, ImageFile, ImagePalette -from ._binary import o8 - -_MAGIC = b"P7 332" - -# standard color palette for thumbnails (RGB332) -PALETTE = b"" -for r in range(8): - for g in range(8): - for b in range(4): - PALETTE = PALETTE + ( - o8((r * 255) // 7) + o8((g * 255) // 7) + o8((b * 255) // 3) - ) - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(_MAGIC) - - -## -# Image plugin for XV thumbnail images. - - -class XVThumbImageFile(ImageFile.ImageFile): - format = "XVThumb" - format_description = "XV thumbnail image" - - def _open(self) -> None: - # check magic - assert self.fp is not None - - if not _accept(self.fp.read(6)): - msg = "not an XV thumbnail file" - raise SyntaxError(msg) - - # Skip to beginning of next line - self.fp.readline() - - # skip info comments - while True: - s = self.fp.readline() - if not s: - msg = "Unexpected EOF reading XV thumbnail file" - raise SyntaxError(msg) - if s[0] != 35: # ie. when not a comment: '#' - break - - # parse header line (already read) - w, h = s.strip().split(maxsplit=2)[:2] - - self._mode = "P" - self._size = int(w), int(h) - - self.palette = ImagePalette.raw("RGB", PALETTE) - - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode) - ] - - -# -------------------------------------------------------------------- - -Image.register_open(XVThumbImageFile.format, XVThumbImageFile, _accept) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/XbmImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/XbmImagePlugin.py deleted file mode 100644 index 1e57aa1..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/XbmImagePlugin.py +++ /dev/null @@ -1,98 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# XBM File handling -# -# History: -# 1995-09-08 fl Created -# 1996-11-01 fl Added save support -# 1997-07-07 fl Made header parser more tolerant -# 1997-07-22 fl Fixed yet another parser bug -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) -# 2001-05-13 fl Added hotspot handling (based on code from Bernhard Herzog) -# 2004-02-24 fl Allow some whitespace before first #define -# -# Copyright (c) 1997-2004 by Secret Labs AB -# Copyright (c) 1996-1997 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re -from typing import IO - -from . import Image, ImageFile - -# XBM header -xbm_head = re.compile( - rb"\s*#define[ \t]+.*_width[ \t]+(?P<width>[0-9]+)[\r\n]+" - b"#define[ \t]+.*_height[ \t]+(?P<height>[0-9]+)[\r\n]+" - b"(?P<hotspot>" - b"#define[ \t]+[^_]*_x_hot[ \t]+(?P<xhot>[0-9]+)[\r\n]+" - b"#define[ \t]+[^_]*_y_hot[ \t]+(?P<yhot>[0-9]+)[\r\n]+" - b")?" - rb"[\000-\377]*_bits\[]" -) - - -def _accept(prefix: bytes) -> bool: - return prefix.lstrip().startswith(b"#define") - - -## -# Image plugin for X11 bitmaps. - - -class XbmImageFile(ImageFile.ImageFile): - format = "XBM" - format_description = "X11 Bitmap" - - def _open(self) -> None: - assert self.fp is not None - - m = xbm_head.match(self.fp.read(512)) - - if not m: - msg = "not a XBM file" - raise SyntaxError(msg) - - xsize = int(m.group("width")) - ysize = int(m.group("height")) - - if m.group("hotspot"): - self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) - - self._mode = "1" - self._size = xsize, ysize - - self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end())] - - -def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode != "1": - msg = f"cannot write mode {im.mode} as XBM" - raise OSError(msg) - - fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) - fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) - - hotspot = im.encoderinfo.get("hotspot") - if hotspot: - fp.write(f"#define im_x_hot {hotspot[0]}\n".encode("ascii")) - fp.write(f"#define im_y_hot {hotspot[1]}\n".encode("ascii")) - - fp.write(b"static char im_bits[] = {\n") - - ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size)]) - - fp.write(b"};\n") - - -Image.register_open(XbmImageFile.format, XbmImageFile, _accept) -Image.register_save(XbmImageFile.format, _save) - -Image.register_extension(XbmImageFile.format, ".xbm") - -Image.register_mime(XbmImageFile.format, "image/xbm") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/XpmImagePlugin.py b/presentation/.venv/lib/python3.12/site-packages/PIL/XpmImagePlugin.py deleted file mode 100644 index 3be240f..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/XpmImagePlugin.py +++ /dev/null @@ -1,157 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# XPM File handling -# -# History: -# 1996-12-29 fl Created -# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) -# -# Copyright (c) Secret Labs AB 1997-2001. -# Copyright (c) Fredrik Lundh 1996-2001. -# -# See the README file for information on usage and redistribution. -# -from __future__ import annotations - -import re - -from . import Image, ImageFile, ImagePalette -from ._binary import o8 - -# XPM header -xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') - - -def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"/* XPM */") - - -## -# Image plugin for X11 pixel maps. - - -class XpmImageFile(ImageFile.ImageFile): - format = "XPM" - format_description = "X11 Pixel Map" - - def _open(self) -> None: - assert self.fp is not None - if not _accept(self.fp.read(9)): - msg = "not an XPM file" - raise SyntaxError(msg) - - # skip forward to next string - while True: - line = self.fp.readline() - if not line: - msg = "broken XPM file" - raise SyntaxError(msg) - m = xpm_head.match(line) - if m: - break - - self._size = int(m.group(1)), int(m.group(2)) - - palette_length = int(m.group(3)) - bpp = int(m.group(4)) - - # - # load palette description - - palette = {} - - for _ in range(palette_length): - line = self.fp.readline().rstrip() - - c = line[1 : bpp + 1] - s = line[bpp + 1 : -2].split() - - for i in range(0, len(s), 2): - if s[i] == b"c": - # process colour key - rgb = s[i + 1] - if rgb == b"None": - self.info["transparency"] = c - elif rgb.startswith(b"#"): - rgb_int = int(rgb[1:], 16) - palette[c] = ( - o8((rgb_int >> 16) & 255) - + o8((rgb_int >> 8) & 255) - + o8(rgb_int & 255) - ) - else: - # unknown colour - msg = "cannot read this XPM file" - raise ValueError(msg) - break - - else: - # missing colour key - msg = "cannot read this XPM file" - raise ValueError(msg) - - args: tuple[int, dict[bytes, bytes] | tuple[bytes, ...]] - if palette_length > 256: - self._mode = "RGB" - args = (bpp, palette) - else: - self._mode = "P" - self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) - args = (bpp, tuple(palette.keys())) - - self.tile = [ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), args)] - - def load_read(self, read_bytes: int) -> bytes: - # - # load all image data in one chunk - - xsize, ysize = self.size - - assert self.fp is not None - s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] - - return b"".join(s) - - -class XpmDecoder(ImageFile.PyDecoder): - _pulls_fd = True - - def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: - assert self.fd is not None - - data = bytearray() - bpp, palette = self.args - dest_length = self.state.xsize * self.state.ysize - if self.mode == "RGB": - dest_length *= 3 - pixel_header = False - while len(data) < dest_length: - line = self.fd.readline() - if not line: - break - if line.rstrip() == b"/* pixels */" and not pixel_header: - pixel_header = True - continue - line = b'"'.join(line.split(b'"')[1:-1]) - for i in range(0, len(line), bpp): - key = line[i : i + bpp] - if self.mode == "RGB": - data += palette[key] - else: - data += o8(palette.index(key)) - self.set_as_raw(bytes(data)) - return -1, 0 - - -# -# Registry - - -Image.register_open(XpmImageFile.format, XpmImageFile, _accept) -Image.register_decoder("xpm", XpmDecoder) - -Image.register_extension(XpmImageFile.format, ".xpm") - -Image.register_mime(XpmImageFile.format, "image/xpm") diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__init__.py b/presentation/.venv/lib/python3.12/site-packages/PIL/__init__.py deleted file mode 100644 index faf3e76..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/__init__.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Pillow (Fork of the Python Imaging Library) - -Pillow is the friendly PIL fork by Jeffrey 'Alex' Clark and contributors. - https://github.com/python-pillow/Pillow/ - -Pillow is forked from PIL 1.1.7. - -PIL is the Python Imaging Library by Fredrik Lundh and contributors. -Copyright (c) 1999 by Secret Labs AB. - -Use PIL.__version__ for this Pillow version. - -;-) -""" - -from __future__ import annotations - -from . import _version - -# VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION was removed in Pillow 9.0.0. -# Use __version__ instead. -__version__ = _version.__version__ -del _version - - -_plugins = [ - "AvifImagePlugin", - "BlpImagePlugin", - "BmpImagePlugin", - "BufrStubImagePlugin", - "CurImagePlugin", - "DcxImagePlugin", - "DdsImagePlugin", - "EpsImagePlugin", - "FitsImagePlugin", - "FliImagePlugin", - "FpxImagePlugin", - "FtexImagePlugin", - "GbrImagePlugin", - "GifImagePlugin", - "GribStubImagePlugin", - "Hdf5StubImagePlugin", - "IcnsImagePlugin", - "IcoImagePlugin", - "ImImagePlugin", - "ImtImagePlugin", - "IptcImagePlugin", - "JpegImagePlugin", - "Jpeg2KImagePlugin", - "McIdasImagePlugin", - "MicImagePlugin", - "MpegImagePlugin", - "MpoImagePlugin", - "MspImagePlugin", - "PalmImagePlugin", - "PcdImagePlugin", - "PcxImagePlugin", - "PdfImagePlugin", - "PixarImagePlugin", - "PngImagePlugin", - "PpmImagePlugin", - "PsdImagePlugin", - "QoiImagePlugin", - "SgiImagePlugin", - "SpiderImagePlugin", - "SunImagePlugin", - "TgaImagePlugin", - "TiffImagePlugin", - "WebPImagePlugin", - "WmfImagePlugin", - "XbmImagePlugin", - "XpmImagePlugin", - "XVThumbImagePlugin", -] - - -class UnidentifiedImageError(OSError): - """ - Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. - - If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` - to true may allow the image to be opened after all. The setting will ignore missing - data and checksum failures. - """ - - pass diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__main__.py b/presentation/.venv/lib/python3.12/site-packages/PIL/__main__.py deleted file mode 100644 index 043156e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/__main__.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import annotations - -import sys - -from .features import pilinfo - -pilinfo(supported_formats="--report" not in sys.argv) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/AvifImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/AvifImagePlugin.cpython-312.pyc deleted file mode 100644 index ef99f14..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/AvifImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BdfFontFile.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BdfFontFile.cpython-312.pyc deleted file mode 100644 index 8121dbf..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BdfFontFile.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-312.pyc deleted file mode 100644 index e4947a9..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-312.pyc deleted file mode 100644 index 0a5bc5b..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BufrStubImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BufrStubImagePlugin.cpython-312.pyc deleted file mode 100644 index a576438..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/BufrStubImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ContainerIO.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ContainerIO.cpython-312.pyc deleted file mode 100644 index 6fe560a..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ContainerIO.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/CurImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/CurImagePlugin.cpython-312.pyc deleted file mode 100644 index 5227279..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/CurImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-312.pyc deleted file mode 100644 index b8d238b..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-312.pyc deleted file mode 100644 index d4bec9a..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-312.pyc deleted file mode 100644 index 3cb8d2b..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ExifTags.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ExifTags.cpython-312.pyc deleted file mode 100644 index ad4eabe..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ExifTags.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FitsImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FitsImagePlugin.cpython-312.pyc deleted file mode 100644 index 2724cd4..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FitsImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FliImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FliImagePlugin.cpython-312.pyc deleted file mode 100644 index 9d28a15..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FliImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FontFile.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FontFile.cpython-312.pyc deleted file mode 100644 index f0dc0e7..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FontFile.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FpxImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FpxImagePlugin.cpython-312.pyc deleted file mode 100644 index 95088ad..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FpxImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FtexImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FtexImagePlugin.cpython-312.pyc deleted file mode 100644 index e5f1858..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/FtexImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GbrImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GbrImagePlugin.cpython-312.pyc deleted file mode 100644 index a6368cf..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GbrImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GdImageFile.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GdImageFile.cpython-312.pyc deleted file mode 100644 index 7517ab3..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GdImageFile.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GifImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GifImagePlugin.cpython-312.pyc deleted file mode 100644 index 8df75e7..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GifImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GimpGradientFile.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GimpGradientFile.cpython-312.pyc deleted file mode 100644 index afbe29f..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GimpGradientFile.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GimpPaletteFile.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GimpPaletteFile.cpython-312.pyc deleted file mode 100644 index 1225c0b..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GimpPaletteFile.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GribStubImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GribStubImagePlugin.cpython-312.pyc deleted file mode 100644 index 293fe89..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/GribStubImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/Hdf5StubImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/Hdf5StubImagePlugin.cpython-312.pyc deleted file mode 100644 index 558f2e0..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/Hdf5StubImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/IcnsImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/IcnsImagePlugin.cpython-312.pyc deleted file mode 100644 index da2e813..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/IcnsImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/IcoImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/IcoImagePlugin.cpython-312.pyc deleted file mode 100644 index 6b71db7..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/IcoImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImImagePlugin.cpython-312.pyc deleted file mode 100644 index 700f9e1..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/Image.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/Image.cpython-312.pyc deleted file mode 100644 index 420e927..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/Image.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageChops.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageChops.cpython-312.pyc deleted file mode 100644 index ff644bb..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageChops.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageCms.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageCms.cpython-312.pyc deleted file mode 100644 index 9096567..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageCms.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageColor.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageColor.cpython-312.pyc deleted file mode 100644 index 408d64e..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageColor.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageDraw.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageDraw.cpython-312.pyc deleted file mode 100644 index 48461a1..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageDraw.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageDraw2.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageDraw2.cpython-312.pyc deleted file mode 100644 index 51b6dc4..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageDraw2.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageEnhance.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageEnhance.cpython-312.pyc deleted file mode 100644 index bb88cf9..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageEnhance.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageFile.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageFile.cpython-312.pyc deleted file mode 100644 index b30d9e0..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageFile.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageFilter.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageFilter.cpython-312.pyc deleted file mode 100644 index 3f56f4b..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageFilter.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageFont.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageFont.cpython-312.pyc deleted file mode 100644 index e29421e..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageFont.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageGrab.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageGrab.cpython-312.pyc deleted file mode 100644 index d3a642f..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageGrab.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageMath.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageMath.cpython-312.pyc deleted file mode 100644 index c222c77..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageMath.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageMode.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageMode.cpython-312.pyc deleted file mode 100644 index a7bb7ed..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageMode.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageMorph.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageMorph.cpython-312.pyc deleted file mode 100644 index e90749c..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageMorph.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageOps.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageOps.cpython-312.pyc deleted file mode 100644 index 6a36d91..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageOps.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImagePalette.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImagePalette.cpython-312.pyc deleted file mode 100644 index 748304c..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImagePalette.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImagePath.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImagePath.cpython-312.pyc deleted file mode 100644 index 20b1a2f..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImagePath.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageQt.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageQt.cpython-312.pyc deleted file mode 100644 index f179edd..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageQt.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageSequence.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageSequence.cpython-312.pyc deleted file mode 100644 index 0e5642f..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageSequence.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageShow.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageShow.cpython-312.pyc deleted file mode 100644 index 296b532..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageShow.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageStat.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageStat.cpython-312.pyc deleted file mode 100644 index 2718a63..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageStat.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageText.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageText.cpython-312.pyc deleted file mode 100644 index 9f1b8a6..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageText.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageTk.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageTk.cpython-312.pyc deleted file mode 100644 index 091b858..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageTk.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageTransform.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageTransform.cpython-312.pyc deleted file mode 100644 index 6794c61..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageTransform.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageWin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageWin.cpython-312.pyc deleted file mode 100644 index 96ea6ff..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImageWin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImtImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImtImagePlugin.cpython-312.pyc deleted file mode 100644 index ec4e202..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/ImtImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/IptcImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/IptcImagePlugin.cpython-312.pyc deleted file mode 100644 index 0c093ba..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/IptcImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/Jpeg2KImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/Jpeg2KImagePlugin.cpython-312.pyc deleted file mode 100644 index 91ab79b..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/Jpeg2KImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/JpegImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/JpegImagePlugin.cpython-312.pyc deleted file mode 100644 index 8efe778..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/JpegImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/JpegPresets.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/JpegPresets.cpython-312.pyc deleted file mode 100644 index b5a9159..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/JpegPresets.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/McIdasImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/McIdasImagePlugin.cpython-312.pyc deleted file mode 100644 index faf1143..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/McIdasImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MicImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MicImagePlugin.cpython-312.pyc deleted file mode 100644 index c0a3e56..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MicImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MpegImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MpegImagePlugin.cpython-312.pyc deleted file mode 100644 index 8eb4e5b..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MpegImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MpoImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MpoImagePlugin.cpython-312.pyc deleted file mode 100644 index 36bb191..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MpoImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MspImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MspImagePlugin.cpython-312.pyc deleted file mode 100644 index ad119d6..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/MspImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PSDraw.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PSDraw.cpython-312.pyc deleted file mode 100644 index f670d5c..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PSDraw.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PaletteFile.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PaletteFile.cpython-312.pyc deleted file mode 100644 index b9f1601..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PaletteFile.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PalmImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PalmImagePlugin.cpython-312.pyc deleted file mode 100644 index 14e8942..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PalmImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PcdImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PcdImagePlugin.cpython-312.pyc deleted file mode 100644 index 17568aa..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PcdImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PcfFontFile.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PcfFontFile.cpython-312.pyc deleted file mode 100644 index eca15b4..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PcfFontFile.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PcxImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PcxImagePlugin.cpython-312.pyc deleted file mode 100644 index 01369e3..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PcxImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PdfImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PdfImagePlugin.cpython-312.pyc deleted file mode 100644 index 088bbde..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PdfImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PdfParser.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PdfParser.cpython-312.pyc deleted file mode 100644 index d7fce5b..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PdfParser.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PixarImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PixarImagePlugin.cpython-312.pyc deleted file mode 100644 index 36fae83..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PixarImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PngImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PngImagePlugin.cpython-312.pyc deleted file mode 100644 index ca53dc7..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PngImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PpmImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PpmImagePlugin.cpython-312.pyc deleted file mode 100644 index ebb052a..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PpmImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PsdImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PsdImagePlugin.cpython-312.pyc deleted file mode 100644 index f601df5..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/PsdImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/QoiImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/QoiImagePlugin.cpython-312.pyc deleted file mode 100644 index 5bce0c5..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/QoiImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/SgiImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/SgiImagePlugin.cpython-312.pyc deleted file mode 100644 index 695aca5..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/SgiImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/SpiderImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/SpiderImagePlugin.cpython-312.pyc deleted file mode 100644 index 91725db..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/SpiderImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/SunImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/SunImagePlugin.cpython-312.pyc deleted file mode 100644 index ec11792..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/SunImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TarIO.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TarIO.cpython-312.pyc deleted file mode 100644 index 02b1bb6..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TarIO.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TgaImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TgaImagePlugin.cpython-312.pyc deleted file mode 100644 index be3d3d2..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TgaImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TiffImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TiffImagePlugin.cpython-312.pyc deleted file mode 100644 index 220e97c..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TiffImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TiffTags.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TiffTags.cpython-312.pyc deleted file mode 100644 index 74f1db1..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/TiffTags.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/WalImageFile.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/WalImageFile.cpython-312.pyc deleted file mode 100644 index d45d7d8..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/WalImageFile.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/WebPImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/WebPImagePlugin.cpython-312.pyc deleted file mode 100644 index 677a30f..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/WebPImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/WmfImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/WmfImagePlugin.cpython-312.pyc deleted file mode 100644 index 35770d1..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/WmfImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/XVThumbImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/XVThumbImagePlugin.cpython-312.pyc deleted file mode 100644 index fe9a5b7..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/XVThumbImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/XbmImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/XbmImagePlugin.cpython-312.pyc deleted file mode 100644 index 2ca444d..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/XbmImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/XpmImagePlugin.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/XpmImagePlugin.cpython-312.pyc deleted file mode 100644 index 44fdc0c..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/XpmImagePlugin.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/__init__.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 570f75e..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/__main__.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/__main__.cpython-312.pyc deleted file mode 100644 index 46416b2..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/__main__.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_binary.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_binary.cpython-312.pyc deleted file mode 100644 index 36d1f3d..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_binary.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_deprecate.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_deprecate.cpython-312.pyc deleted file mode 100644 index c5e6083..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_deprecate.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_tkinter_finder.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_tkinter_finder.cpython-312.pyc deleted file mode 100644 index 2d74b76..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_tkinter_finder.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_typing.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_typing.cpython-312.pyc deleted file mode 100644 index 4ce9350..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_typing.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_util.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_util.cpython-312.pyc deleted file mode 100644 index d245690..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_util.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_version.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_version.cpython-312.pyc deleted file mode 100644 index 308d36b..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/_version.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/features.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/features.cpython-312.pyc deleted file mode 100644 index e076337..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/features.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/report.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/report.cpython-312.pyc deleted file mode 100644 index fdc853f..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/__pycache__/report.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_avif.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/PIL/_avif.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index 6718d47..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/_avif.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_avif.pyi b/presentation/.venv/lib/python3.12/site-packages/PIL/_avif.pyi deleted file mode 100644 index e27843e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_avif.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_binary.py b/presentation/.venv/lib/python3.12/site-packages/PIL/_binary.py deleted file mode 100644 index d3236c1..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_binary.py +++ /dev/null @@ -1,113 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# Binary input/output support routines. -# -# Copyright (c) 1997-2003 by Secret Labs AB -# Copyright (c) 1995-2003 by Fredrik Lundh -# Copyright (c) 2012 by Brian Crowell -# -# See the README file for information on usage and redistribution. -# - - -"""Binary input/output support routines.""" - -from __future__ import annotations - -from struct import pack, unpack_from - - -def i8(c: bytes) -> int: - return c[0] - - -def o8(i: int) -> bytes: - return bytes((i & 255,)) - - -# Input, le = little endian, be = big endian -def i16le(c: bytes, o: int = 0) -> int: - """ - Converts a 2-bytes (16 bits) string to an unsigned integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from("<H", c, o)[0] - - -def si16le(c: bytes, o: int = 0) -> int: - """ - Converts a 2-bytes (16 bits) string to a signed integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from("<h", c, o)[0] - - -def si16be(c: bytes, o: int = 0) -> int: - """ - Converts a 2-bytes (16 bits) string to a signed integer, big endian. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(">h", c, o)[0] - - -def i32le(c: bytes, o: int = 0) -> int: - """ - Converts a 4-bytes (32 bits) string to an unsigned integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from("<I", c, o)[0] - - -def si32le(c: bytes, o: int = 0) -> int: - """ - Converts a 4-bytes (32 bits) string to a signed integer. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from("<i", c, o)[0] - - -def si32be(c: bytes, o: int = 0) -> int: - """ - Converts a 4-bytes (32 bits) string to a signed integer, big endian. - - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string - """ - return unpack_from(">i", c, o)[0] - - -def i16be(c: bytes, o: int = 0) -> int: - return unpack_from(">H", c, o)[0] - - -def i32be(c: bytes, o: int = 0) -> int: - return unpack_from(">I", c, o)[0] - - -# Output, le = little endian, be = big endian -def o16le(i: int) -> bytes: - return pack("<H", i) - - -def o32le(i: int) -> bytes: - return pack("<I", i) - - -def o16be(i: int) -> bytes: - return pack(">H", i) - - -def o32be(i: int) -> bytes: - return pack(">I", i) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_deprecate.py b/presentation/.venv/lib/python3.12/site-packages/PIL/_deprecate.py deleted file mode 100644 index 711c62a..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_deprecate.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations - -import warnings - -from . import __version__ - - -def deprecate( - deprecated: str, - when: int | None, - replacement: str | None = None, - *, - action: str | None = None, - plural: bool = False, - stacklevel: int = 3, -) -> None: - """ - Deprecations helper. - - :param deprecated: Name of thing to be deprecated. - :param when: Pillow major version to be removed in. - :param replacement: Name of replacement. - :param action: Instead of "replacement", give a custom call to action - e.g. "Upgrade to new thing". - :param plural: if the deprecated thing is plural, needing "are" instead of "is". - - Usually of the form: - - "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). - Use [replacement] instead." - - You can leave out the replacement sentence: - - "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd)" - - Or with another call to action: - - "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). - [action]." - """ - - is_ = "are" if plural else "is" - - if when is None: - removed = "a future version" - elif when <= int(__version__.split(".")[0]): - msg = f"{deprecated} {is_} deprecated and should be removed." - raise RuntimeError(msg) - elif when == 13: - removed = "Pillow 13 (2026-10-15)" - elif when == 14: - removed = "Pillow 14 (2027-10-15)" - else: - msg = f"Unknown removal version: {when}. Update {__name__}?" - raise ValueError(msg) - - if replacement and action: - msg = "Use only one of 'replacement' and 'action'" - raise ValueError(msg) - - if replacement: - action = f". Use {replacement} instead." - elif action: - action = f". {action.rstrip('.')}." - else: - action = "" - - warnings.warn( - f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", - DeprecationWarning, - stacklevel=stacklevel, - ) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imaging.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/PIL/_imaging.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index 49f1901..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/_imaging.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imaging.pyi b/presentation/.venv/lib/python3.12/site-packages/PIL/_imaging.pyi deleted file mode 100644 index 81028a5..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_imaging.pyi +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Any - -class ImagingCore: - def __getitem__(self, index: int) -> float | tuple[int, ...] | None: ... - def __getattr__(self, name: str) -> Any: ... - -class ImagingFont: - def __getattr__(self, name: str) -> Any: ... - -class ImagingDraw: - def __getattr__(self, name: str) -> Any: ... - -class PixelAccess: - def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: ... - def __setitem__( - self, xy: tuple[int, int], color: float | tuple[int, ...] - ) -> None: ... - -class ImagingDecoder: - def __getattr__(self, name: str) -> Any: ... - -class ImagingEncoder: - def __getattr__(self, name: str) -> Any: ... - -class _Outline: - def close(self) -> None: ... - def __getattr__(self, name: str) -> Any: ... - -def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ... -def outline() -> _Outline: ... -def __getattr__(name: str) -> Any: ... diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingcms.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingcms.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index ea34a87..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingcms.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingcms.pyi b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingcms.pyi deleted file mode 100644 index 4fc0d60..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingcms.pyi +++ /dev/null @@ -1,143 +0,0 @@ -import datetime -import sys -from typing import Literal, SupportsFloat, TypeAlias, TypedDict - -from ._typing import CapsuleType - -littlecms_version: str | None - -_Tuple3f: TypeAlias = tuple[float, float, float] -_Tuple2x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f] -_Tuple3x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f, _Tuple3f] - -class _IccMeasurementCondition(TypedDict): - observer: int - backing: _Tuple3f - geo: str - flare: float - illuminant_type: str - -class _IccViewingCondition(TypedDict): - illuminant: _Tuple3f - surround: _Tuple3f - illuminant_type: str - -class CmsProfile: - @property - def rendering_intent(self) -> int: ... - @property - def creation_date(self) -> datetime.datetime | None: ... - @property - def copyright(self) -> str | None: ... - @property - def target(self) -> str | None: ... - @property - def manufacturer(self) -> str | None: ... - @property - def model(self) -> str | None: ... - @property - def profile_description(self) -> str | None: ... - @property - def screening_description(self) -> str | None: ... - @property - def viewing_condition(self) -> str | None: ... - @property - def version(self) -> float: ... - @property - def icc_version(self) -> int: ... - @property - def attributes(self) -> int: ... - @property - def header_flags(self) -> int: ... - @property - def header_manufacturer(self) -> str: ... - @property - def header_model(self) -> str: ... - @property - def device_class(self) -> str: ... - @property - def connection_space(self) -> str: ... - @property - def xcolor_space(self) -> str: ... - @property - def profile_id(self) -> bytes: ... - @property - def is_matrix_shaper(self) -> bool: ... - @property - def technology(self) -> str | None: ... - @property - def colorimetric_intent(self) -> str | None: ... - @property - def perceptual_rendering_intent_gamut(self) -> str | None: ... - @property - def saturation_rendering_intent_gamut(self) -> str | None: ... - @property - def red_colorant(self) -> _Tuple2x3f | None: ... - @property - def green_colorant(self) -> _Tuple2x3f | None: ... - @property - def blue_colorant(self) -> _Tuple2x3f | None: ... - @property - def red_primary(self) -> _Tuple2x3f | None: ... - @property - def green_primary(self) -> _Tuple2x3f | None: ... - @property - def blue_primary(self) -> _Tuple2x3f | None: ... - @property - def media_white_point_temperature(self) -> float | None: ... - @property - def media_white_point(self) -> _Tuple2x3f | None: ... - @property - def media_black_point(self) -> _Tuple2x3f | None: ... - @property - def luminance(self) -> _Tuple2x3f | None: ... - @property - def chromatic_adaptation(self) -> tuple[_Tuple3x3f, _Tuple3x3f] | None: ... - @property - def chromaticity(self) -> _Tuple3x3f | None: ... - @property - def colorant_table(self) -> list[str] | None: ... - @property - def colorant_table_out(self) -> list[str] | None: ... - @property - def intent_supported(self) -> dict[int, tuple[bool, bool, bool]] | None: ... - @property - def clut(self) -> dict[int, tuple[bool, bool, bool]] | None: ... - @property - def icc_measurement_condition(self) -> _IccMeasurementCondition | None: ... - @property - def icc_viewing_condition(self) -> _IccViewingCondition | None: ... - def is_intent_supported(self, intent: int, direction: int, /) -> int: ... - -class CmsTransform: - def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ... - -def profile_open(profile: str, /) -> CmsProfile: ... -def profile_frombytes(profile: bytes, /) -> CmsProfile: ... -def profile_tobytes(profile: CmsProfile, /) -> bytes: ... -def buildTransform( - input_profile: CmsProfile, - output_profile: CmsProfile, - in_mode: str, - out_mode: str, - rendering_intent: int = 0, - cms_flags: int = 0, - /, -) -> CmsTransform: ... -def buildProofTransform( - input_profile: CmsProfile, - output_profile: CmsProfile, - proof_profile: CmsProfile, - in_mode: str, - out_mode: str, - rendering_intent: int = 0, - proof_intent: int = 0, - cms_flags: int = 0, - /, -) -> CmsTransform: ... -def createProfile( - color_space: Literal["LAB", "XYZ", "sRGB"], color_temp: SupportsFloat = 0.0, / -) -> CmsProfile: ... - -if sys.platform == "win32": - def get_display_profile_win32(handle: int = 0, is_dc: int = 0, /) -> str | None: ... diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingft.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingft.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index 7e8d46e..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingft.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingft.pyi b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingft.pyi deleted file mode 100644 index 2136810..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingft.pyi +++ /dev/null @@ -1,70 +0,0 @@ -from collections.abc import Callable -from typing import Any - -from . import ImageFont, _imaging - -class Font: - @property - def family(self) -> str | None: ... - @property - def style(self) -> str | None: ... - @property - def ascent(self) -> int: ... - @property - def descent(self) -> int: ... - @property - def height(self) -> int: ... - @property - def x_ppem(self) -> int: ... - @property - def y_ppem(self) -> int: ... - @property - def glyphs(self) -> int: ... - def render( - self, - string: str | bytes, - fill: Callable[[int, int], _imaging.ImagingCore], - mode: str, - dir: str | None, - features: list[str] | None, - lang: str | None, - stroke_width: float, - stroke_filled: bool, - anchor: str | None, - foreground_ink_long: int, - start: tuple[float, float], - /, - ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... - def getsize( - self, - string: str | bytes | bytearray, - mode: str, - dir: str | None, - features: list[str] | None, - lang: str | None, - anchor: str | None, - /, - ) -> tuple[tuple[int, int], tuple[int, int]]: ... - def getlength( - self, - string: str | bytes, - mode: str, - dir: str | None, - features: list[str] | None, - lang: str | None, - /, - ) -> float: ... - def getvarnames(self) -> list[bytes]: ... - def getvaraxes(self) -> list[ImageFont.Axis]: ... - def setvarname(self, instance_index: int, /) -> None: ... - def setvaraxes(self, axes: list[float], /) -> None: ... - -def getfont( - filename: str | bytes, - size: float, - index: int, - encoding: str, - font_bytes: bytes, - layout_engine: int, -) -> Font: ... -def __getattr__(name: str) -> Any: ... diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmath.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmath.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index 5224936..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmath.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmath.pyi b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmath.pyi deleted file mode 100644 index e27843e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmath.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmorph.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmorph.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index d7c5236..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmorph.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmorph.pyi b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmorph.pyi deleted file mode 100644 index e27843e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingmorph.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingtk.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingtk.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index 16e18d2..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingtk.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingtk.pyi b/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingtk.pyi deleted file mode 100644 index e27843e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_imagingtk.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_tkinter_finder.py b/presentation/.venv/lib/python3.12/site-packages/PIL/_tkinter_finder.py deleted file mode 100644 index 9c01430..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_tkinter_finder.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Find compiled module linking to Tcl / Tk libraries""" - -from __future__ import annotations - -import sys -import tkinter - -tk = getattr(tkinter, "_tkinter") - -try: - if hasattr(sys, "pypy_find_executable"): - TKINTER_LIB = tk.tklib_cffi.__file__ - else: - TKINTER_LIB = tk.__file__ -except AttributeError: - # _tkinter may be compiled directly into Python, in which case __file__ is - # not available. load_tkinter_funcs will check the binary first in any case. - TKINTER_LIB = None - -tk_version = str(tkinter.TkVersion) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_typing.py b/presentation/.venv/lib/python3.12/site-packages/PIL/_typing.py deleted file mode 100644 index a941f89..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_typing.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -import os -import sys -from collections.abc import Sequence -from typing import Any, Protocol, TypeVar - -TYPE_CHECKING = False -if TYPE_CHECKING: - from numbers import _IntegralLike as IntegralLike - - try: - import numpy.typing as npt - - NumpyArray = npt.NDArray[Any] - except ImportError: - pass - -if sys.version_info >= (3, 13): - from types import CapsuleType -else: - CapsuleType = object - -if sys.version_info >= (3, 12): - from collections.abc import Buffer -else: - Buffer = Any - - -_Ink = float | tuple[int, ...] | str - -Coords = Sequence[float] | Sequence[Sequence[float]] - - -_T_co = TypeVar("_T_co", covariant=True) - - -class SupportsRead(Protocol[_T_co]): - def read(self, length: int = ..., /) -> _T_co: ... - - -StrOrBytesPath = str | bytes | os.PathLike[str] | os.PathLike[bytes] - - -__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead"] diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_util.py b/presentation/.venv/lib/python3.12/site-packages/PIL/_util.py deleted file mode 100644 index b1fa6a0..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_util.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -import os - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Any, NoReturn, TypeGuard - - from ._typing import StrOrBytesPath - - -def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: - return isinstance(f, (bytes, str, os.PathLike)) - - -class DeferredError: - def __init__(self, ex: BaseException): - self.ex = ex - - def __getattr__(self, elt: str) -> NoReturn: - raise self.ex - - @staticmethod - def new(ex: BaseException) -> Any: - """ - Creates an object that raises the wrapped exception ``ex`` when used, - and casts it to :py:obj:`~typing.Any` type. - """ - return DeferredError(ex) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_version.py b/presentation/.venv/lib/python3.12/site-packages/PIL/_version.py deleted file mode 100644 index 72d11ae..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_version.py +++ /dev/null @@ -1,4 +0,0 @@ -# Master version for Pillow -from __future__ import annotations - -__version__ = "12.2.0" diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_webp.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/PIL/_webp.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index 0afd77c..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/PIL/_webp.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/_webp.pyi b/presentation/.venv/lib/python3.12/site-packages/PIL/_webp.pyi deleted file mode 100644 index e27843e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/_webp.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/features.py b/presentation/.venv/lib/python3.12/site-packages/PIL/features.py deleted file mode 100644 index ff32c25..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/features.py +++ /dev/null @@ -1,343 +0,0 @@ -from __future__ import annotations - -import collections -import os -import sys -import warnings -from typing import IO - -import PIL - -from . import Image - -modules = { - "pil": ("PIL._imaging", "PILLOW_VERSION"), - "tkinter": ("PIL._tkinter_finder", "tk_version"), - "freetype2": ("PIL._imagingft", "freetype2_version"), - "littlecms2": ("PIL._imagingcms", "littlecms_version"), - "webp": ("PIL._webp", "webpdecoder_version"), - "avif": ("PIL._avif", "libavif_version"), -} - - -def check_module(feature: str) -> bool: - """ - Checks if a module is available. - - :param feature: The module to check for. - :returns: ``True`` if available, ``False`` otherwise. - :raises ValueError: If the module is not defined in this version of Pillow. - """ - if feature not in modules: - msg = f"Unknown module {feature}" - raise ValueError(msg) - - module, ver = modules[feature] - - try: - __import__(module) - return True - except ModuleNotFoundError: - return False - except ImportError as ex: - warnings.warn(str(ex)) - return False - - -def version_module(feature: str) -> str | None: - """ - :param feature: The module to check for. - :returns: - The loaded version number as a string, or ``None`` if unknown or not available. - :raises ValueError: If the module is not defined in this version of Pillow. - """ - if not check_module(feature): - return None - - module, ver = modules[feature] - - return getattr(__import__(module, fromlist=[ver]), ver) - - -def get_supported_modules() -> list[str]: - """ - :returns: A list of all supported modules. - """ - return [f for f in modules if check_module(f)] - - -codecs = { - "jpg": ("jpeg", "jpeglib"), - "jpg_2000": ("jpeg2k", "jp2klib"), - "zlib": ("zip", "zlib"), - "libtiff": ("libtiff", "libtiff"), -} - - -def check_codec(feature: str) -> bool: - """ - Checks if a codec is available. - - :param feature: The codec to check for. - :returns: ``True`` if available, ``False`` otherwise. - :raises ValueError: If the codec is not defined in this version of Pillow. - """ - if feature not in codecs: - msg = f"Unknown codec {feature}" - raise ValueError(msg) - - codec, lib = codecs[feature] - - return f"{codec}_encoder" in dir(Image.core) - - -def version_codec(feature: str) -> str | None: - """ - :param feature: The codec to check for. - :returns: - The version number as a string, or ``None`` if not available. - Checked at compile time for ``jpg``, run-time otherwise. - :raises ValueError: If the codec is not defined in this version of Pillow. - """ - if not check_codec(feature): - return None - - codec, lib = codecs[feature] - - version = getattr(Image.core, f"{lib}_version") - - if feature == "libtiff": - return version.split("\n")[0].split("Version ")[1] - - return version - - -def get_supported_codecs() -> list[str]: - """ - :returns: A list of all supported codecs. - """ - return [f for f in codecs if check_codec(f)] - - -features: dict[str, tuple[str, str, str | None]] = { - "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), - "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), - "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), - "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), - "mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"), - "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), - "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), - "xcb": ("PIL._imaging", "HAVE_XCB", None), -} - - -def check_feature(feature: str) -> bool | None: - """ - Checks if a feature is available. - - :param feature: The feature to check for. - :returns: ``True`` if available, ``False`` if unavailable, ``None`` if unknown. - :raises ValueError: If the feature is not defined in this version of Pillow. - """ - if feature not in features: - msg = f"Unknown feature {feature}" - raise ValueError(msg) - - module, flag, ver = features[feature] - - try: - imported_module = __import__(module, fromlist=["PIL"]) - return getattr(imported_module, flag) - except ModuleNotFoundError: - return None - except ImportError as ex: - warnings.warn(str(ex)) - return None - - -def version_feature(feature: str) -> str | None: - """ - :param feature: The feature to check for. - :returns: The version number as a string, or ``None`` if not available. - :raises ValueError: If the feature is not defined in this version of Pillow. - """ - if not check_feature(feature): - return None - - module, flag, ver = features[feature] - - if ver is None: - return None - - return getattr(__import__(module, fromlist=[ver]), ver) - - -def get_supported_features() -> list[str]: - """ - :returns: A list of all supported features. - """ - return [f for f in features if check_feature(f)] - - -def check(feature: str) -> bool | None: - """ - :param feature: A module, codec, or feature name. - :returns: - ``True`` if the module, codec, or feature is available, - ``False`` or ``None`` otherwise. - """ - - if feature in modules: - return check_module(feature) - if feature in codecs: - return check_codec(feature) - if feature in features: - return check_feature(feature) - warnings.warn(f"Unknown feature '{feature}'.", stacklevel=2) - return False - - -def version(feature: str) -> str | None: - """ - :param feature: - The module, codec, or feature to check for. - :returns: - The version number as a string, or ``None`` if unknown or not available. - """ - if feature in modules: - return version_module(feature) - if feature in codecs: - return version_codec(feature) - if feature in features: - return version_feature(feature) - return None - - -def get_supported() -> list[str]: - """ - :returns: A list of all supported modules, features, and codecs. - """ - - ret = get_supported_modules() - ret.extend(get_supported_features()) - ret.extend(get_supported_codecs()) - return ret - - -def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: - """ - Prints information about this installation of Pillow. - This function can be called with ``python3 -m PIL``. - It can also be called with ``python3 -m PIL.report`` or ``python3 -m PIL --report`` - to have "supported_formats" set to ``False``, omitting the list of all supported - image file formats. - - :param out: - The output stream to print to. Defaults to ``sys.stdout`` if ``None``. - :param supported_formats: - If ``True``, a list of all supported image file formats will be printed. - """ - - if out is None: - out = sys.stdout - - Image.init() - - print("-" * 68, file=out) - print(f"Pillow {PIL.__version__}", file=out) - py_version_lines = sys.version.splitlines() - print(f"Python {py_version_lines[0].strip()}", file=out) - for py_version in py_version_lines[1:]: - print(f" {py_version.strip()}", file=out) - print("-" * 68, file=out) - print(f"Python executable is {sys.executable or 'unknown'}", file=out) - if sys.prefix != sys.base_prefix: - print(f"Environment Python files loaded from {sys.prefix}", file=out) - print(f"System Python files loaded from {sys.base_prefix}", file=out) - print("-" * 68, file=out) - print( - f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}", - file=out, - ) - print( - f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}", - file=out, - ) - print("-" * 68, file=out) - - for name, feature in [ - ("pil", "PIL CORE"), - ("tkinter", "TKINTER"), - ("freetype2", "FREETYPE2"), - ("littlecms2", "LITTLECMS2"), - ("webp", "WEBP"), - ("avif", "AVIF"), - ("jpg", "JPEG"), - ("jpg_2000", "OPENJPEG (JPEG2000)"), - ("zlib", "ZLIB (PNG/ZIP)"), - ("libtiff", "LIBTIFF"), - ("raqm", "RAQM (Bidirectional Text)"), - ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), - ("xcb", "XCB (X protocol)"), - ]: - if check(name): - v: str | None = None - if name == "jpg": - libjpeg_turbo_version = version_feature("libjpeg_turbo") - if libjpeg_turbo_version is not None: - v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo" - v += " " + libjpeg_turbo_version - if v is None: - v = version(name) - if v is not None: - version_static = name in ("pil", "jpg") - if name == "littlecms2": - # this check is also in src/_imagingcms.c:setup_module() - version_static = tuple(int(x) for x in v.split(".")) < (2, 7) - t = "compiled for" if version_static else "loaded" - if name == "zlib": - zlib_ng_version = version_feature("zlib_ng") - if zlib_ng_version is not None: - v += ", compiled for zlib-ng " + zlib_ng_version - elif name == "raqm": - for f in ("fribidi", "harfbuzz"): - v2 = version_feature(f) - if v2 is not None: - v += f", {f} {v2}" - print("---", feature, "support ok,", t, v, file=out) - else: - print("---", feature, "support ok", file=out) - else: - print("***", feature, "support not installed", file=out) - print("-" * 68, file=out) - - if supported_formats: - extensions = collections.defaultdict(list) - for ext, i in Image.EXTENSION.items(): - extensions[i].append(ext) - - for i in sorted(Image.ID): - line = f"{i}" - if i in Image.MIME: - line = f"{line} {Image.MIME[i]}" - print(line, file=out) - - if i in extensions: - print( - "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out - ) - - features = [] - if i in Image.OPEN: - features.append("open") - if i in Image.SAVE: - features.append("save") - if i in Image.SAVE_ALL: - features.append("save_all") - if i in Image.DECODERS: - features.append("decode") - if i in Image.ENCODERS: - features.append("encode") - - print("Features: {}".format(", ".join(features)), file=out) - print("-" * 68, file=out) diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/py.typed b/presentation/.venv/lib/python3.12/site-packages/PIL/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/presentation/.venv/lib/python3.12/site-packages/PIL/report.py b/presentation/.venv/lib/python3.12/site-packages/PIL/report.py deleted file mode 100644 index d2815e8..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/PIL/report.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import annotations - -from .features import pilinfo - -pilinfo(supported_formats=False) diff --git a/presentation/.venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc deleted file mode 100644 index b8af5f6..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/INSTALLER b/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/INSTALLER deleted file mode 100644 index a1b589e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/METADATA b/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/METADATA deleted file mode 100644 index 805cbb8..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/METADATA +++ /dev/null @@ -1,104 +0,0 @@ -Metadata-Version: 2.4 -Name: lxml -Version: 6.1.0 -Summary: Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API. -Home-page: https://lxml.de/ -Author: lxml dev team -Author-email: lxml@lxml.de -Maintainer: lxml dev team -Maintainer-email: lxml@lxml.de -License: BSD-3-Clause -Project-URL: Source, https://github.com/lxml/lxml -Project-URL: Bug Tracker, https://bugs.launchpad.net/lxml -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: Intended Audience :: Information Technology -Classifier: Programming Language :: Cython -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: 3.14 -Classifier: Programming Language :: C -Classifier: Operating System :: OS Independent -Classifier: Topic :: Text Processing :: Markup :: HTML -Classifier: Topic :: Text Processing :: Markup :: XML -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Requires-Python: >=3.8 -License-File: LICENSE.txt -License-File: LICENSES.txt -Provides-Extra: source -Provides-Extra: cssselect -Requires-Dist: cssselect>=0.7; extra == "cssselect" -Provides-Extra: html5 -Requires-Dist: html5lib; extra == "html5" -Provides-Extra: htmlsoup -Requires-Dist: BeautifulSoup4; extra == "htmlsoup" -Provides-Extra: html-clean -Requires-Dist: lxml_html_clean; extra == "html-clean" -Dynamic: author -Dynamic: author-email -Dynamic: classifier -Dynamic: description -Dynamic: home-page -Dynamic: license -Dynamic: license-file -Dynamic: maintainer -Dynamic: maintainer-email -Dynamic: project-url -Dynamic: provides-extra -Dynamic: requires-python -Dynamic: summary - -lxml is a Pythonic, mature binding for the libxml2 and libxslt libraries. -It provides safe and convenient access to these libraries using the -ElementTree API. - -It extends the ElementTree API significantly to offer support for XPath, -RelaxNG, XML Schema, XSLT, C14N and much more. - -To contact the project, go to the `project home page <https://lxml.de/>`_ -or see our bug tracker at https://launchpad.net/lxml - -In case you want to use the current in-development version of lxml, -you can get it from the github repository at -https://github.com/lxml/lxml . Note that this requires Cython to -build the sources, see the build instructions on the project home page. - - -After an official release of a new stable series, bug fixes may become available at -https://github.com/lxml/lxml/tree/lxml-6.1 . -Running ``pip install https://github.com/lxml/lxml/archive/refs/heads/lxml-6.1.tar.gz`` -will install the unreleased branch state as soon as a maintenance branch has been established. -Note that this requires Cython to be installed at an appropriate version for the build. - -6.1.0 (2026-04-17) -================== - -This release fixes a possible external entity injection (XXE) vulnerability in -``iterparse()`` and the ``ETCompatXMLParser``. - -Features added --------------- - -* GH#486: The HTML ARIA accessibility attributes were added to the set of safe attributes - in ``lxml.html.defs``. This allows ``lxml_html_clean`` to pass them through. - Patch by oomsveta. - -* The default chunk size for reading from file-likes in ``iterparse()`` is now configurable - with a new ``chunk_size`` argument. - -Bugs fixed ----------- - -* LP#2146291: The ``resolve_entities`` option was still set to ``True`` for - ``iterparse`` and ``ETCompatXMLParser``, allowing for external entity injection (XXE) - when using these parsers without setting this option explicitly. - The default was now changed to ``'internal'`` only (as for the normal XML and HTML parsers - since lxml 5.0). - Issue found by Sihao Qiu as CVE-2026-41066. - - diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/RECORD b/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/RECORD deleted file mode 100644 index 65bdcb2..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/RECORD +++ /dev/null @@ -1,204 +0,0 @@ -lxml-6.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -lxml-6.1.0.dist-info/METADATA,sha256=C22o5J3b-pRFqvsDJ8BZIZgShZshbyM4dD_WThX15FU,3953 -lxml-6.1.0.dist-info/RECORD,, -lxml-6.1.0.dist-info/WHEEL,sha256=rSHxYvtox9WA2CLg-qPNRHTwscksbjj7CEInt0-zzg4,152 -lxml-6.1.0.dist-info/licenses/LICENSE.txt,sha256=j8K1aBM1FuRoRdIUeRet7uFkjnCumrXtbFQXr-9M6FU,1507 -lxml-6.1.0.dist-info/licenses/LICENSES.txt,sha256=QdSd1AaqDhVIptXyGjDWv2OLPNlutyid00jYPtLkA5I,1514 -lxml-6.1.0.dist-info/top_level.txt,sha256=NjD988wqaKq512nshNdLt-uDxsjkp4Bh51m6N-dhUrk,5 -lxml/ElementInclude.py,sha256=PSLeZFvCa76WHJulPLxcZXJtCI2-4dK2CtqPRiYOAQg,8560 -lxml/__init__.py,sha256=nNZhK4w0_VI-AvQ3n1b4cWN1fFhYFdteghBR1mR7j1M,574 -lxml/__pycache__/ElementInclude.cpython-312.pyc,, -lxml/__pycache__/__init__.cpython-312.pyc,, -lxml/__pycache__/_elementpath.cpython-312.pyc,, -lxml/__pycache__/builder.cpython-312.pyc,, -lxml/__pycache__/cssselect.cpython-312.pyc,, -lxml/__pycache__/doctestcompare.cpython-312.pyc,, -lxml/__pycache__/pyclasslookup.cpython-312.pyc,, -lxml/__pycache__/sax.cpython-312.pyc,, -lxml/__pycache__/usedoctest.cpython-312.pyc,, -lxml/_elementpath.cpython-312-x86_64-linux-gnu.so,sha256=Gngj3IsCTtH8jsFJTJKW0x4oAbFMPSQIFgVbMUZ-PvU,209096 -lxml/_elementpath.py,sha256=b80hM3ndAkTtRX6v54za3LkkAqCcd0700BbMPZHnTBU,10959 -lxml/apihelpers.pxi,sha256=CugqfQgyn6aFtwp7eH144RUpV2v1ItxoalhJcFbORTQ,64604 -lxml/builder.cpython-312-x86_64-linux-gnu.so,sha256=MSiyw-7TTv91-yEn-dq2qc5QIFmTW2knqs64u7GoUvs,120880 -lxml/builder.py,sha256=KI1HxHTd4wJqqVfmTRtSbXBQdl2T-P36ih4hT-J3MNw,8485 -lxml/classlookup.pxi,sha256=Tax8Vhbm5C6UCjgmRFsYjW0pFHxIuTthH1MOgASDLgc,22435 -lxml/cleanup.pxi,sha256=ZNEpbv7qx_ICPzsxhCaMUHCOfiznOoZ_u3jlYXHAuh4,8454 -lxml/cssselect.py,sha256=_wZdX-B9p5MeIYABmENIYRWEkwXwX-7jO8Dkf-1rUZU,3306 -lxml/debug.pxi,sha256=KTcpR8-slUYvmIPbE35GoHDNTb-gjTEvD7bw6LltM4c,1125 -lxml/docloader.pxi,sha256=fcoVd_mwTFZhjItCv_TfwXpgVek7JlvFObSfRxmVqs0,5790 -lxml/doctestcompare.py,sha256=40EDnkwpcvW86qNa86990OXF42xdHaosSZoiBsEjkzU,17731 -lxml/dtd.pxi,sha256=IAKkmA4ZoC68sqAWcTqoS8jEGYcPQrVMCZgn4iLBYko,15281 -lxml/etree.cpython-312-x86_64-linux-gnu.so,sha256=2TuGzdSHoOS0lBiobIcVea95xwGLxrKj0y8AIXHJ-Yo,5272432 -lxml/etree.h,sha256=hWgHumv7DeTpH0nHhbsCIe693cIcimtLabExxwBxR-c,9792 -lxml/etree.pyx,sha256=zgobCeGw6lw902OA0FWuwlOpqA_0eZeLM2baWi1pzio,138216 -lxml/etree_api.h,sha256=UG16a1ThLt259FSG3vUy6ka6OIat-6LLJyVBLz6d3OM,17710 -lxml/extensions.pxi,sha256=saxxc1mZyA8WdwIIL90B4lQb351j8-G7ttLPI5k-8Hw,32248 -lxml/html/ElementSoup.py,sha256=s_dLobLMuKn2DhexR-iDXdZrMFg1RjLy1feHsIeZMpw,320 -lxml/html/__init__.py,sha256=CC5WdsvSptZhr9MZya1qsL6JKVbviYdrHOhXrGhmORg,64425 -lxml/html/__pycache__/ElementSoup.cpython-312.pyc,, -lxml/html/__pycache__/__init__.cpython-312.pyc,, -lxml/html/__pycache__/_diffcommand.cpython-312.pyc,, -lxml/html/__pycache__/_difflib.cpython-312.pyc,, -lxml/html/__pycache__/_html5builder.cpython-312.pyc,, -lxml/html/__pycache__/_setmixin.cpython-312.pyc,, -lxml/html/__pycache__/builder.cpython-312.pyc,, -lxml/html/__pycache__/clean.cpython-312.pyc,, -lxml/html/__pycache__/defs.cpython-312.pyc,, -lxml/html/__pycache__/diff.cpython-312.pyc,, -lxml/html/__pycache__/formfill.cpython-312.pyc,, -lxml/html/__pycache__/html5parser.cpython-312.pyc,, -lxml/html/__pycache__/soupparser.cpython-312.pyc,, -lxml/html/__pycache__/usedoctest.cpython-312.pyc,, -lxml/html/_diffcommand.py,sha256=kz_7EP9PmYWuczlZcGiw74_rG0eTKvQ2lrO0rkiwlYE,2081 -lxml/html/_difflib.cpython-312-x86_64-linux-gnu.so,sha256=0Za0L59Q9ZDfTG86x9enqvkdutV0Z29YDEjG4nftBLQ,533400 -lxml/html/_difflib.py,sha256=GgH_jVrZQC8tI8WV_lFZQsXFJ3mOTAPup1zjBJNvkPo,84954 -lxml/html/_html5builder.py,sha256=NLaT-Ev-aBgJpeQl-6ZbJChLZK5GV-znDkHOJD5VQC4,3230 -lxml/html/_setmixin.py,sha256=8IFIOLmVz0G-XzsD2tCEkSFWO-dgPBHgvHufC8ni67s,1188 -lxml/html/builder.py,sha256=Uz3r5uiuCdoN0UPa7ngoLMwAadVIhslzGvlRPGigY_M,6187 -lxml/html/clean.py,sha256=WU0KfXAyjDFn2ojE7T1dfk4cl_FduBu5ZbFSTc9Up_o,503 -lxml/html/defs.py,sha256=IHIW70bC-m8RdCtGOyYak58hFiBqyhegTC83QlsO5As,5322 -lxml/html/diff.cpython-312-x86_64-linux-gnu.so,sha256=EoLnmB8DWTm1fvtjq5IQR6j-bXa-h3nnNRt4E_F-LzA,361400 -lxml/html/diff.py,sha256=Za0By-yeYlQEjUu7m7xKB288kKiy8VBS5gT0RPOaFY0,32989 -lxml/html/formfill.py,sha256=umgk0BbkAI1W6q9musFbL-cDnI_aap2NsLBJqk0UmVI,9681 -lxml/html/html5parser.py,sha256=dnyC4cqHxywjZSzk0mu2L7THTZjxhg4yF4pncjusa_w,8634 -lxml/html/soupparser.py,sha256=xo8VvNeOEb-SChuXLKCRECh8J7HBiJLE9sAbEskoUUQ,10197 -lxml/html/usedoctest.py,sha256=tPlmVz4KK1GRKV5DJLrdVECeqsT9PlDzSqqTodVi5s0,249 -lxml/includes/__init__.pxd,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -lxml/includes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -lxml/includes/__pycache__/__init__.cpython-312.pyc,, -lxml/includes/c14n.pxd,sha256=DBQcOJ0c_YS245ohMb8fmuEC1kFyv1LrNY_8Mf-syZg,1110 -lxml/includes/config.pxd,sha256=H6Mrl8It21hzRI2hzMId9W48QqkYYkoLT4dniLNmdTw,96 -lxml/includes/dtdvalid.pxd,sha256=Nv0OykjYehv2lO-Zj--q6jS3TAC_dvQVPSgPMuse1NM,689 -lxml/includes/etree_defs.h,sha256=h_UjJTmNUqPyKNNrWB9hxmt6v4CF7_83XVY8dOfxqW0,14524 -lxml/includes/etreepublic.pxd,sha256=Bn4d3JkWPqXputXqI-eJ0xmPrwNFPTfDCa7axgjB7FM,10184 -lxml/includes/extlibs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -lxml/includes/extlibs/__pycache__/__init__.cpython-312.pyc,, -lxml/includes/extlibs/libcharset.h,sha256=GA0FumrbNI4VDGlzq3lf5CLaCwXgn4unw2l0btGQFwI,1510 -lxml/includes/extlibs/localcharset.h,sha256=Z_AagaQeq0aDE7NPsVOqEf4nO4KcUp46ggo4d0ONIOQ,6338 -lxml/includes/extlibs/zconf.h,sha256=BxihG-sylbNF-ymmO0S2VLKC2CxM0FE_YiVYfyspuLs,16921 -lxml/includes/extlibs/zlib.h,sha256=gYZn1qtqN_50acsGp_DLLCyy8slIoD5azPGkp0vzAgo,103848 -lxml/includes/htmlparser.pxd,sha256=9uASkP5dU7OE2lCOLT-z2e01qSbFlp4ehgwdostF_qk,2802 -lxml/includes/libexslt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -lxml/includes/libexslt/__pycache__/__init__.cpython-312.pyc,, -lxml/includes/libexslt/exslt.h,sha256=eSW5tMJAewSUANLqk7AGEiU8b2BbCNRyauHnez7nKSU,3114 -lxml/includes/libexslt/exsltconfig.h,sha256=QHxzEbRlv_h0USBvpr0Zrl0Muzlc71VCrvgR6lqnLEY,1172 -lxml/includes/libexslt/exsltexports.h,sha256=1Jm9KTXm2FUUJIZ6V6-Uw55yG0BMULX3_goyxDd2LL8,1077 -lxml/includes/libxml/HTMLparser.h,sha256=sU4xGqj-vBtEvzlxA3hBPWJboifvkc4F1hynKXmsl3k,9569 -lxml/includes/libxml/HTMLtree.h,sha256=Q7UBKFbQ8fx4d_dMnmR6ay8JmfOhopFkDp2B63YkLDU,3517 -lxml/includes/libxml/SAX.h,sha256=SFnG27EFrYGUB9HDL_xSIGBwEns5pl07rApXWThFZFM,386 -lxml/includes/libxml/SAX2.h,sha256=RfFP5o3le-Rg8bnA2GW7L7L9_pfXCs3TieODcv1DTWY,4240 -lxml/includes/libxml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -lxml/includes/libxml/__pycache__/__init__.cpython-312.pyc,, -lxml/includes/libxml/c14n.h,sha256=BSBXw6nIZutC8mWvbRrLLmoWjw3wRt-nM93vjXGMCm8,2742 -lxml/includes/libxml/catalog.h,sha256=H9ssTCaBjtDqc-AZqCk1R7h8F2iD9szqLjJyHpaczXg,4633 -lxml/includes/libxml/chvalid.h,sha256=TZcceNp6Cw0QlYwIqK9GxyYqL5UiAjpQyjt_yrZGTQE,5087 -lxml/includes/libxml/debugXML.h,sha256=XXRNI39gJW7bGRC4SzE4ad-SJ906BsUGz3AwOtkKuS4,1667 -lxml/includes/libxml/dict.h,sha256=SweaPGMtTTf4je6dNTIoEzcfEvpsAT9_PhR7FC0K-rQ,1770 -lxml/includes/libxml/encoding.h,sha256=haL7ratww2wkIERGmtwUqU2BbTVe52FZFU7MmrOpsPk,9623 -lxml/includes/libxml/entities.h,sha256=LEOCA826-0f8dhRJzC_2hvUVsSH7lKQjrea9hSTdBbo,4419 -lxml/includes/libxml/globals.h,sha256=NH8zyRI5cXJJGp5k2aLxOm-reJEGOFX6LYP82GBXRlY,583 -lxml/includes/libxml/hash.h,sha256=KIIpAYKBfGUU3ydWhGehUyfuauZz_Ps0gyambzQo_rc,7017 -lxml/includes/libxml/list.h,sha256=oh7iJNQajRA_cHsNk9CcFPYkaW2smf4J_MpedPPjC4k,3128 -lxml/includes/libxml/nanoftp.h,sha256=22PBtWhJueYLFvwukt4oFooRct_xJA83hbluHRBNXUM,302 -lxml/includes/libxml/nanohttp.h,sha256=bLbzYjAyAKmP3ComMOPH6XaUImu6bNAESF1HrVtRve0,2124 -lxml/includes/libxml/parser.h,sha256=Uq7-ce55UUAsvo4n6CiBlNQpmowewvWhOsQtgGM1UQ8,48498 -lxml/includes/libxml/parserInternals.h,sha256=8_Wr6UgRzm8BRn1RPLxyBkw6BagAdDvVqMA_e181_EI,14539 -lxml/includes/libxml/relaxng.h,sha256=VXZ74r5Yja06KqypdBHc8neDwPxQ2aMrsWHSdRt5oi4,5991 -lxml/includes/libxml/schemasInternals.h,sha256=V8M4In3zf24EX55Yt4dcfxwp7NpHGYViKnLKwtyrPJ4,26233 -lxml/includes/libxml/schematron.h,sha256=8EhPDhvtlMxl9e0C5rSbEruOvzJS5BC_OOFbq9RXZnY,4255 -lxml/includes/libxml/threads.h,sha256=mT3CgK4lXK7-NDnUOFXqYuCK6fyY70S3BsHF-TnT45k,1619 -lxml/includes/libxml/tree.h,sha256=zTRLt6h5x6ApyeXgs90CKQZSAl2hKm7b5NxtPKUQFAE,36106 -lxml/includes/libxml/uri.h,sha256=J9teJHme5z883c4twF5oImEYY-E3xSvhdSGpyRVtvIg,2855 -lxml/includes/libxml/valid.h,sha256=By61IbPvk_eLux7a8x0mOaly7oclFaSGaFE8b2xZcUE,13226 -lxml/includes/libxml/xinclude.h,sha256=K3I5jhw2zAMj26LuRNZc15Bwv2JE2hWxwVn4TCqv2b4,3258 -lxml/includes/libxml/xlink.h,sha256=TVLOkISrcKDelo9n_XIUyPiStDYa8NxuF2dz70aBFCI,5062 -lxml/includes/libxml/xmlIO.h,sha256=FvbuMYTy1-S5PScabE03wz0oWKf626pmXvOPZNuLm-w,11948 -lxml/includes/libxml/xmlautomata.h,sha256=7Sc3YgPz1ZIBKCHPSxs5oAwJEZWQ1RT2kyUw85pUtmU,4004 -lxml/includes/libxml/xmlerror.h,sha256=mMfltMxUza6kiSBfP2QfnY3UlMP_rEXKfX0wruBLl4A,37561 -lxml/includes/libxml/xmlexports.h,sha256=IyV3AMeQVbOl0wkjlnNX4B8WUZ-5GNKQmxZc6-maWUU,2025 -lxml/includes/libxml/xmlmemory.h,sha256=m7wGvVMxNzZiuOAo3vkjxaVWstc8aQLzb6obbjPsebE,4658 -lxml/includes/libxml/xmlmodule.h,sha256=ERUHUmDdZRmh6NjLYWUpse51rLWR8qNjPHOtdgmlLF0,1198 -lxml/includes/libxml/xmlreader.h,sha256=BAHinlSOTXX3DEax9BniaIIPAXJyLGfzym9R-27LCcU,12387 -lxml/includes/libxml/xmlregexp.h,sha256=_q6C1XRy8DS3kSmLbEKpvkKQciTgjTJgGc_zUQ6m22M,2632 -lxml/includes/libxml/xmlsave.h,sha256=zcEQr9sO5CsFrnoOLshhdsqMEr8k4AeFhbkYyNfO9Fs,2934 -lxml/includes/libxml/xmlschemas.h,sha256=5AfLnYUcfmxHRzg0dVpdHig--4ui1-XDwDgpKGDKCiU,7067 -lxml/includes/libxml/xmlschemastypes.h,sha256=MYwlGmoKAo3lHRaaKgnCXiLmPT9KRjdxyCJ7TEyZ6jM,4583 -lxml/includes/libxml/xmlstring.h,sha256=d5PpqxP1I1sfmCUHvVJtjoC9h7hLHcAAQ5ok_Rtf50I,5271 -lxml/includes/libxml/xmlunicode.h,sha256=8sq3wEW2AiyTCuc3ZceOEkce7lfrI7VnkRfwEQgc6pU,278 -lxml/includes/libxml/xmlversion.h,sha256=oVpaE_xbttaeZNFKSuSfcLOceWz7LQgKP71Z1msXZNo,5112 -lxml/includes/libxml/xmlwriter.h,sha256=BEUwYNKx3xymDE9vepksEK7yVq9SXYm1d2pQnzlPy90,20688 -lxml/includes/libxml/xpath.h,sha256=CQv6X_pRhuXoCVpqoDXYB7FfusLK7AuPxCNigwhNYAA,16156 -lxml/includes/libxml/xpathInternals.h,sha256=mc9B5tdpfssyz_NPUzww6dKuWCtBybBiBRJkTe4AE4U,18504 -lxml/includes/libxml/xpointer.h,sha256=DAxMsfPp2SSZgXFrPbxBA84RwTMRf35Qg_LBbUzPQhA,1026 -lxml/includes/libxslt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -lxml/includes/libxslt/__pycache__/__init__.cpython-312.pyc,, -lxml/includes/libxslt/attributes.h,sha256=qKwzfGf7r89esLC65s96iYJWRA-s-Ezss2_V6Mmo1hk,957 -lxml/includes/libxslt/documents.h,sha256=kBihgH5pqRvFalhm_fOFHtJTFhTpBcm681yT5dxgwfw,2704 -lxml/includes/libxslt/extensions.h,sha256=W5UMyJqUP_1zt6sXZ0mgc0gAIwDJrZ8gjByhyrWqvd8,6899 -lxml/includes/libxslt/extra.h,sha256=6X3Wu3NdPtrlqz-Koo7dB-rccnnszi6j3zg599gTByg,1640 -lxml/includes/libxslt/functions.h,sha256=fc4CZj-9KeBHzO9-WWU_bNqmaEZAz3n7NNwClIBXk14,1972 -lxml/includes/libxslt/imports.h,sha256=18kIjoGqdFXR63Ce3ZtzxsTiYV3XGKpchYakMUPDuUI,1840 -lxml/includes/libxslt/keys.h,sha256=16v25VEluS7jYhgg6gYFwVxgGMn-1ctnlhhWWT4RcBY,1155 -lxml/includes/libxslt/namespaces.h,sha256=VofSn2Kkn-a5JyRKCmY3jPp7amQy3n09vzy0KUQt4q0,1666 -lxml/includes/libxslt/numbersInternals.h,sha256=Eg5gYZ5p3h0_e5wyI61S-0E6_ArVJzv0yr63j6BU2fc,2019 -lxml/includes/libxslt/pattern.h,sha256=tJ-BPfs9UYgiZMMoQZbhij3g7xVppYq7TrrOu25eR7Q,2110 -lxml/includes/libxslt/preproc.h,sha256=D_LjEdHhsdyBnEAvflnwFgoR4hGUb72kgEhXkkmPRsw,896 -lxml/includes/libxslt/security.h,sha256=fUD1cy_WxFCTvTNAF0WOQIU4p5CNWn1LHFyZJd-Fx5U,2652 -lxml/includes/libxslt/templates.h,sha256=bnt6Jqui6KU5pNUdMNPbQZkZ5d-VTWqC0TMGkOlVoIo,2268 -lxml/includes/libxslt/transform.h,sha256=ICT7meUV0OTAx27WaKVrKj-aUmR9LSpTNaOAJd2UStg,6311 -lxml/includes/libxslt/variables.h,sha256=cQAgPe4QCcK2uKbWg7Iz-9peM9xWGm7m3M6jQm0sjIA,3143 -lxml/includes/libxslt/xslt.h,sha256=wmFx2Q31Pd8Iq2phAQpY9J3QQatb8lWg3gABtqKFgEw,1964 -lxml/includes/libxslt/xsltInternals.h,sha256=2EbEKYmnYZq0HjGnUMAlpqnqZJurRXzjlgk5Js1WYaY,57949 -lxml/includes/libxslt/xsltconfig.h,sha256=cV5scdRK6xmOHeOg3OCw6hBfcQ_nrtNs_tKefX67304,2910 -lxml/includes/libxslt/xsltexports.h,sha256=1-luH-0bCIgBAlKAXhV-dqHBfwOAQNDamiYbxIlTf0k,1124 -lxml/includes/libxslt/xsltlocale.h,sha256=ppxGEmJfZIJgwRQzCM0_77p9WNekEWq1NrdYZrQl4IE,942 -lxml/includes/libxslt/xsltutils.h,sha256=1eguYgR9-jeNOVlBUktHboaq-VLX6JXraO80TfbARKM,9085 -lxml/includes/lxml-version.h,sha256=7Jwmesey8JmjKayzDkLgkjKNi77OGhOzjJ_JC41Oj_I,71 -lxml/includes/relaxng.pxd,sha256=HzHlQ6mCcf_tj_JZ9NAVJTVAv8ScCkE8Ifq15y3bS0c,2615 -lxml/includes/schematron.pxd,sha256=Hob7xh-K-MKqp7WiG8thMagf5EkQzmgfi4ds0EF91JA,1604 -lxml/includes/tree.pxd,sha256=XApzMRy_LSqCtQ-OTS-vNSW7CT_OWstybfIT2H84LsA,20179 -lxml/includes/uri.pxd,sha256=3vOXw6AbSPxAM9uo71T1qnfx-wd9ezXLDQtWsb2zX0I,145 -lxml/includes/xinclude.pxd,sha256=CuO_XZNB6E2JK1qXXWn11APrjFQV5kA6SMyb77WZn0A,804 -lxml/includes/xmlerror.pxd,sha256=OQqayytkV0NigAPbsQCCcvmy7luRe0XhVzpTdzJjP3g,58837 -lxml/includes/xmlparser.pxd,sha256=eDGyU5kZyNVksK0dUhMIi7rnE-LSevXsqyl72v99Ess,13730 -lxml/includes/xmlschema.pxd,sha256=OLZPd2WDJyopiXJJyo-dAyyYHaeSYFiMAI4tqIiv-Ik,1702 -lxml/includes/xpath.pxd,sha256=e8-ZYUbRG7N1mHETAlknJ_QqAteOosrYLRgpH-OsTkg,5603 -lxml/includes/xslt.pxd,sha256=4yl3pOu7pAvsx5Tc-W4IWCoB8wgtSSR62HI1jqu6jko,8241 -lxml/isoschematron/__init__.py,sha256=uauerYeKTlWFCJSqieIHhF5l6rYV2myeEJ0Imd1LzRc,13274 -lxml/isoschematron/__pycache__/__init__.cpython-312.pyc,, -lxml/isoschematron/resources/rng/iso-schematron.rng,sha256=VsWxPyi3iViJDDbjJJw0wWkEHkLrz9zoCA8zJLor9N4,18337 -lxml/isoschematron/resources/xsl/RNG2Schtrn.xsl,sha256=ObebsB8Wt-d3uIA_U5NU85TpnQ3PxPX38TdOAqosMac,3172 -lxml/isoschematron/resources/xsl/XSD2Schtrn.xsl,sha256=QweRrIIM-zFcgg98GXA2CaWfIbgVE0XKEeYSfvv67A0,4563 -lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_abstract_expand.xsl,sha256=xSZ_Ekq_I-62ZpiE5AqYYHwFW_qh855zt9V4_s7rbkY,11703 -lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_dsdl_include.xsl,sha256=x42QJ-dxQ1waPzydsCoQnp2Xj15y53nW43O7BuoDRHk,39957 -lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_schematron_message.xsl,sha256=Tr9BnO6pzjVWwhqJfm10UlvAy95EgfSCz2iMlrVGT6Q,2015 -lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_schematron_skeleton_for_xslt1.xsl,sha256=ue8q_88X4e_jsJizo31GRNBxNhdxkEE9fY20oq0Iqwk,71764 -lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_svrl_for_xslt1.xsl,sha256=BBAdsVSi5zAzeGepuN6gS1saQINDqITXKplmmj4dTWg,20382 -lxml/isoschematron/resources/xsl/iso-schematron-xslt1/readme.txt,sha256=OGLiFswuLJEW5EPYKOeoauuCJFEtVa6jyzBE1OcJI98,3310 -lxml/iterparse.pxi,sha256=rERnJ9D0Cvn87YDMSq8V880DKV2WthXs5BXSSY2Baj4,16845 -lxml/lxml.etree.h,sha256=hWgHumv7DeTpH0nHhbsCIe693cIcimtLabExxwBxR-c,9792 -lxml/lxml.etree_api.h,sha256=O_s_RIicKl7_h5U0aKu9dKSzJCTYSDJI6jlX7b-2v0Q,17715 -lxml/nsclasses.pxi,sha256=5pzNBhBtlqObPdThL9QIGRs1Dxj1qnr0PyXuTCURqTg,9129 -lxml/objectify.cpython-312-x86_64-linux-gnu.so,sha256=iTDWB90lJBk_EVv0x5DwNpvB4cbOEqPDMKg-511TN5w,2916760 -lxml/objectify.pyx,sha256=8lIjgklL3LJ3PYAcWZ5hPLuLFa7oLqEspco4bSz4EI4,75850 -lxml/objectpath.pxi,sha256=s5TNG2-EbaWWKLFAiX303B95zK_Ui8ausB__3QvFFGw,11450 -lxml/parser.pxi,sha256=lw230Za_GcJe-CFfznB1As9MSEi0d6gsMKidQxUR-7E,85817 -lxml/parsertarget.pxi,sha256=v1PidxRaG5giwXcTDkpBI7PDFmsZuOcK0y9LdkQaY8M,6326 -lxml/proxy.pxi,sha256=LnqqSZVUsJ5MnuVrhYrLwMbAKWuv1eOn-XYslut6CLM,24015 -lxml/public-api.pxi,sha256=yVPuZV2LwN5Ki73mD85V9hnUXxOs5VMeFMLtQMNf0Qk,6756 -lxml/pyclasslookup.py,sha256=gLD1HM2HtITYYiGzjEOewSwbB7XkVx_NZv_quCt79Oc,92 -lxml/readonlytree.pxi,sha256=n2MRjz81EADWBXJx0miosN8PJ4i5y3guQJT_RZF6qKg,19008 -lxml/relaxng.pxi,sha256=3OQ-fZMzP-KF5vM6HTozT_9ee3J0DJnpj9RcHC8LoMw,6339 -lxml/sax.cpython-312-x86_64-linux-gnu.so,sha256=_is26aCl78oYdPU_iyAE_CxAfev44Uir8RK03edgTVk,182096 -lxml/sax.py,sha256=yrNvKD6rlon48jrR-1qpFXER8j4psYC2R5yt0u6TWLs,9706 -lxml/saxparser.pxi,sha256=yV67FWD60DfmFi7hbV_eLyJIMu7X2MOfV2g4X99iUR4,33538 -lxml/schematron.pxi,sha256=F2OHKZUl57-byBk_wWtPTnHZ1fwlj0FtwG3VuGtG-UY,6064 -lxml/serializer.pxi,sha256=iIXfechFHfvFs2sTk7wMIy3sDJxmaMPbNO33mkLLBUE,68063 -lxml/usedoctest.py,sha256=qRgZKQVcAZcl-zN0AIXVJnOsETUXz2nPXkxuzs1lGgk,230 -lxml/xinclude.pxi,sha256=7eBrI_OK47mmrHQ0ixbixRI8pKqQ1nwkMV-OmKUVlD4,2456 -lxml/xmlerror.pxi,sha256=MAQv92NXFoaMOzT8WeeM0L3YyksFoo48tpz3KZHTqcY,50234 -lxml/xmlid.pxi,sha256=5zf9oR6bsCtavGiOmilNyHqYwgG_bnrIabSd2SURtm0,6073 -lxml/xmlschema.pxi,sha256=mumNoHni5S3BQPtcmOHRd61KRaVWu4eOie2wQeB0e6E,8490 -lxml/xpath.pxi,sha256=aqW24V817dUxps4Gnc8h7Tm3QVlITKvxU5_9WgJUIFg,19132 -lxml/xslt.pxi,sha256=lLVwvlhMKL970rVqThP6Jnn79L3MK9UG0DGnXnNy7YI,36356 -lxml/xsltext.pxi,sha256=TImDiAPlAezC07P7RY1N9YChA7AuKFH-G53hXdel9yc,11088 diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/WHEEL b/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/WHEEL deleted file mode 100644 index 43257e7..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/WHEEL +++ /dev/null @@ -1,6 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (82.0.1) -Root-Is-Purelib: false -Tag: cp312-cp312-manylinux_2_26_x86_64 -Tag: cp312-cp312-manylinux_2_28_x86_64 - diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/licenses/LICENSE.txt b/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/licenses/LICENSE.txt deleted file mode 100644 index 0bdf039..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/licenses/LICENSE.txt +++ /dev/null @@ -1,31 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2004 Infrae. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - - 3. Neither the name of Infrae nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INFRAE OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/licenses/LICENSES.txt b/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/licenses/LICENSES.txt deleted file mode 100644 index 9f97c18..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/licenses/LICENSES.txt +++ /dev/null @@ -1,29 +0,0 @@ -lxml is copyright Infrae and distributed under the BSD license (see -doc/licenses/BSD.txt), with the following exceptions: - -Some code, such a selftest.py, selftest2.py and -src/lxml/_elementpath.py are derived from ElementTree and -cElementTree. See doc/licenses/elementtree.txt for the license text. - -lxml.cssselect and lxml.html are copyright Ian Bicking and distributed -under the BSD license (see doc/licenses/BSD.txt). - -test.py, the test-runner script, is GPL and copyright Shuttleworth -Foundation. See doc/licenses/GPL.txt. It is believed the unchanged -inclusion of test.py to run the unit test suite falls under the -"aggregation" clause of the GPL and thus does not affect the license -of the rest of the package. - -The isoschematron implementation uses several XSL and RelaxNG resources: - * The (XML syntax) RelaxNG schema for schematron, copyright International - Organization for Standardization (see - src/lxml/isoschematron/resources/rng/iso-schematron.rng for the license - text) - * The skeleton iso-schematron-xlt1 pure-xslt schematron implementation - xsl stylesheets, copyright Rick Jelliffe and Academia Sinica Computing - Center, Taiwan (see the xsl files here for the license text: - src/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/) - * The xsd/rng schema schematron extraction xsl transformations are unlicensed - and copyright the respective authors as noted (see - src/lxml/isoschematron/resources/xsl/RNG2Schtrn.xsl and - src/lxml/isoschematron/resources/xsl/XSD2Schtrn.xsl) diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/top_level.txt b/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/top_level.txt deleted file mode 100644 index ab90481..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml-6.1.0.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -lxml diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/ElementInclude.py b/presentation/.venv/lib/python3.12/site-packages/lxml/ElementInclude.py deleted file mode 100644 index 2188433..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/ElementInclude.py +++ /dev/null @@ -1,244 +0,0 @@ -# -# ElementTree -# $Id: ElementInclude.py 1862 2004-06-18 07:31:02Z Fredrik $ -# -# limited xinclude support for element trees -# -# history: -# 2003-08-15 fl created -# 2003-11-14 fl fixed default loader -# -# Copyright (c) 2003-2004 by Fredrik Lundh. All rights reserved. -# -# fredrik@pythonware.com -# http://www.pythonware.com -# -# -------------------------------------------------------------------- -# The ElementTree toolkit is -# -# Copyright (c) 1999-2004 by Fredrik Lundh -# -# By obtaining, using, and/or copying this software and/or its -# associated documentation, you agree that you have read, understood, -# and will comply with the following terms and conditions: -# -# Permission to use, copy, modify, and distribute this software and -# its associated documentation for any purpose and without fee is -# hereby granted, provided that the above copyright notice appears in -# all copies, and that both that copyright notice and this permission -# notice appear in supporting documentation, and that the name of -# Secret Labs AB or the author not be used in advertising or publicity -# pertaining to distribution of the software without specific, written -# prior permission. -# -# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD -# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- -# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR -# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY -# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS -# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -# OF THIS SOFTWARE. -# -------------------------------------------------------------------- - -""" -Limited XInclude support for the ElementTree package. - -While lxml.etree has full support for XInclude (see -`etree.ElementTree.xinclude()`), this module provides a simpler, pure -Python, ElementTree compatible implementation that supports a simple -form of custom URL resolvers. -""" - -from lxml import etree -try: - from urlparse import urljoin - from urllib2 import urlopen -except ImportError: - # Python 3 - from urllib.parse import urljoin - from urllib.request import urlopen - -XINCLUDE = "{http://www.w3.org/2001/XInclude}" - -XINCLUDE_INCLUDE = XINCLUDE + "include" -XINCLUDE_FALLBACK = XINCLUDE + "fallback" -XINCLUDE_ITER_TAG = XINCLUDE + "*" - -# For security reasons, the inclusion depth is limited to this read-only value by default. -DEFAULT_MAX_INCLUSION_DEPTH = 6 - - -## -# Fatal include error. - -class FatalIncludeError(etree.LxmlSyntaxError): - pass - - -class LimitedRecursiveIncludeError(FatalIncludeError): - pass - - -## -# ET compatible default loader. -# This loader reads an included resource from disk. -# -# @param href Resource reference. -# @param parse Parse mode. Either "xml" or "text". -# @param encoding Optional text encoding. -# @return The expanded resource. If the parse mode is "xml", this -# is an ElementTree instance. If the parse mode is "text", this -# is a Unicode string. If the loader fails, it can return None -# or raise an IOError exception. -# @throws IOError If the loader fails to load the resource. - -def default_loader(href, parse, encoding=None): - file = open(href, 'rb') - if parse == "xml": - data = etree.parse(file).getroot() - else: - data = file.read() - if not encoding: - encoding = 'utf-8' - data = data.decode(encoding) - file.close() - return data - - -## -# Default loader used by lxml.etree - handles custom resolvers properly -# - -def _lxml_default_loader(href, parse, encoding=None, parser=None): - if parse == "xml": - data = etree.parse(href, parser).getroot() - else: - if "://" in href: - f = urlopen(href) - else: - f = open(href, 'rb') - data = f.read() - f.close() - if not encoding: - encoding = 'utf-8' - data = data.decode(encoding) - return data - - -## -# Wrapper for ET compatibility - drops the parser - -def _wrap_et_loader(loader): - def load(href, parse, encoding=None, parser=None): - return loader(href, parse, encoding) - return load - - -## -# Expand XInclude directives. -# -# @param elem Root element. -# @param loader Optional resource loader. If omitted, it defaults -# to {@link default_loader}. If given, it should be a callable -# that implements the same interface as <b>default_loader</b>. -# @param base_url The base URL of the original file, to resolve -# relative include file references. -# @param max_depth The maximum number of recursive inclusions. -# Limited to reduce the risk of malicious content explosion. -# Pass None to disable the limitation. -# @throws LimitedRecursiveIncludeError If the {@link max_depth} was exceeded. -# @throws FatalIncludeError If the function fails to include a given -# resource, or if the tree contains malformed XInclude elements. -# @throws IOError If the function fails to load a given resource. -# @returns the node or its replacement if it was an XInclude node - -def include(elem, loader=None, base_url=None, - max_depth=DEFAULT_MAX_INCLUSION_DEPTH): - if max_depth is None: - max_depth = -1 - elif max_depth < 0: - raise ValueError("expected non-negative depth or None for 'max_depth', got %r" % max_depth) - - if base_url is None: - if hasattr(elem, 'getroot'): - tree = elem - elem = elem.getroot() - else: - tree = elem.getroottree() - if hasattr(tree, 'docinfo'): - base_url = tree.docinfo.URL - elif hasattr(elem, 'getroot'): - elem = elem.getroot() - _include(elem, loader, base_url, max_depth) - - -def _include(elem, loader=None, base_url=None, - max_depth=DEFAULT_MAX_INCLUSION_DEPTH, _parent_hrefs=None): - if loader is not None: - load_include = _wrap_et_loader(loader) - else: - load_include = _lxml_default_loader - - if _parent_hrefs is None: - _parent_hrefs = set() - - parser = elem.getroottree().parser - - include_elements = list( - elem.iter(XINCLUDE_ITER_TAG)) - - for e in include_elements: - if e.tag == XINCLUDE_INCLUDE: - # process xinclude directive - href = urljoin(base_url, e.get("href")) - parse = e.get("parse", "xml") - parent = e.getparent() - if parse == "xml": - if href in _parent_hrefs: - raise FatalIncludeError( - "recursive include of %r detected" % href - ) - if max_depth == 0: - raise LimitedRecursiveIncludeError( - "maximum xinclude depth reached when including file %s" % href) - node = load_include(href, parse, parser=parser) - if node is None: - raise FatalIncludeError( - "cannot load %r as %r" % (href, parse) - ) - node = _include(node, loader, href, max_depth - 1, {href} | _parent_hrefs) - if e.tail: - node.tail = (node.tail or "") + e.tail - if parent is None: - return node # replaced the root node! - parent.replace(e, node) - elif parse == "text": - text = load_include(href, parse, encoding=e.get("encoding")) - if text is None: - raise FatalIncludeError( - "cannot load %r as %r" % (href, parse) - ) - predecessor = e.getprevious() - if predecessor is not None: - predecessor.tail = (predecessor.tail or "") + text - elif parent is None: - return text # replaced the root node! - else: - parent.text = (parent.text or "") + text + (e.tail or "") - parent.remove(e) - else: - raise FatalIncludeError( - "unknown parse type in xi:include tag (%r)" % parse - ) - elif e.tag == XINCLUDE_FALLBACK: - parent = e.getparent() - if parent is not None and parent.tag != XINCLUDE_INCLUDE: - raise FatalIncludeError( - "xi:fallback tag must be child of xi:include (%r)" % e.tag - ) - else: - raise FatalIncludeError( - "Invalid element found in XInclude namespace (%r)" % e.tag - ) - return elem diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__init__.py b/presentation/.venv/lib/python3.12/site-packages/lxml/__init__.py deleted file mode 100644 index 873696e..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# this is a package - -__version__ = "6.1.0" - - -def get_include(): - """ - Returns a list of header include paths (for lxml itself, libxml2 - and libxslt) needed to compile C code against lxml if it was built - with statically linked libraries. - """ - import os - lxml_path = __path__[0] - include_path = os.path.join(lxml_path, 'includes') - includes = [include_path, lxml_path] - - for name in os.listdir(include_path): - path = os.path.join(include_path, name) - if os.path.isdir(path): - includes.append(path) - - return includes diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/ElementInclude.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/ElementInclude.cpython-312.pyc deleted file mode 100644 index 18b79df..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/ElementInclude.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/__init__.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 6883e79..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/_elementpath.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/_elementpath.cpython-312.pyc deleted file mode 100644 index a52c29e..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/_elementpath.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/builder.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/builder.cpython-312.pyc deleted file mode 100644 index c6602f8..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/builder.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/cssselect.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/cssselect.cpython-312.pyc deleted file mode 100644 index 6530e70..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/cssselect.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/doctestcompare.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/doctestcompare.cpython-312.pyc deleted file mode 100644 index 8f6493d..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/doctestcompare.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/pyclasslookup.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/pyclasslookup.cpython-312.pyc deleted file mode 100644 index 6825ae9..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/pyclasslookup.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/sax.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/sax.cpython-312.pyc deleted file mode 100644 index 67503e1..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/sax.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/usedoctest.cpython-312.pyc b/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/usedoctest.cpython-312.pyc deleted file mode 100644 index 3ee1328..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/__pycache__/usedoctest.cpython-312.pyc and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/_elementpath.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/lxml/_elementpath.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index 3e602bb..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/_elementpath.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/_elementpath.py b/presentation/.venv/lib/python3.12/site-packages/lxml/_elementpath.py deleted file mode 100644 index 760a1e0..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/_elementpath.py +++ /dev/null @@ -1,343 +0,0 @@ -# cython: language_level=3 - -# -# ElementTree -# $Id: ElementPath.py 3375 2008-02-13 08:05:08Z fredrik $ -# -# limited xpath support for element trees -# -# history: -# 2003-05-23 fl created -# 2003-05-28 fl added support for // etc -# 2003-08-27 fl fixed parsing of periods in element names -# 2007-09-10 fl new selection engine -# 2007-09-12 fl fixed parent selector -# 2007-09-13 fl added iterfind; changed findall to return a list -# 2007-11-30 fl added namespaces support -# 2009-10-30 fl added child element value filter -# -# Copyright (c) 2003-2009 by Fredrik Lundh. All rights reserved. -# -# fredrik@pythonware.com -# http://www.pythonware.com -# -# -------------------------------------------------------------------- -# The ElementTree toolkit is -# -# Copyright (c) 1999-2009 by Fredrik Lundh -# -# By obtaining, using, and/or copying this software and/or its -# associated documentation, you agree that you have read, understood, -# and will comply with the following terms and conditions: -# -# Permission to use, copy, modify, and distribute this software and -# its associated documentation for any purpose and without fee is -# hereby granted, provided that the above copyright notice appears in -# all copies, and that both that copyright notice and this permission -# notice appear in supporting documentation, and that the name of -# Secret Labs AB or the author not be used in advertising or publicity -# pertaining to distribution of the software without specific, written -# prior permission. -# -# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD -# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- -# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR -# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY -# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS -# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -# OF THIS SOFTWARE. -# -------------------------------------------------------------------- - -## -# Implementation module for XPath support. There's usually no reason -# to import this module directly; the <b>ElementTree</b> does this for -# you, if needed. -## - - -import re - -xpath_tokenizer_re = re.compile( - "(" - "'[^']*'|\"[^\"]*\"|" - "::|" - "//?|" - r"\.\.|" - r"\(\)|" - r"[/.*:\[\]\(\)@=])|" - r"((?:\{[^}]+\})?[^/\[\]\(\)@=\s]+)|" - r"\s+" - ) - -def xpath_tokenizer(pattern, namespaces=None, with_prefixes=True): - # ElementTree uses '', lxml used None originally. - default_namespace = (namespaces.get(None) or namespaces.get('')) if namespaces else None - parsing_attribute = False - for token in xpath_tokenizer_re.findall(pattern): - ttype, tag = token - if tag and tag[0] != "{": - if ":" in tag and with_prefixes: - prefix, uri = tag.split(":", 1) - try: - if not namespaces: - raise KeyError - yield ttype, "{%s}%s" % (namespaces[prefix], uri) - except KeyError: - raise SyntaxError("prefix %r not found in prefix map" % prefix) - elif tag.isdecimal(): - yield token # index - elif default_namespace and not parsing_attribute: - yield ttype, "{%s}%s" % (default_namespace, tag) - else: - yield token - parsing_attribute = False - else: - yield token - parsing_attribute = ttype == '@' - - -def prepare_child(next, token): - tag = token[1] - def select(result): - for elem in result: - yield from elem.iterchildren(tag) - return select - -def prepare_star(next, token): - def select(result): - for elem in result: - yield from elem.iterchildren('*') - return select - -def prepare_self(next, token): - def select(result): - return result - return select - -def prepare_descendant(next, token): - token = next() - if token[0] == "*": - tag = "*" - elif not token[0]: - tag = token[1] - else: - raise SyntaxError("invalid descendant") - def select(result): - for elem in result: - yield from elem.iterdescendants(tag) - return select - -def prepare_parent(next, token): - def select(result): - for elem in result: - parent = elem.getparent() - if parent is not None: - yield parent - return select - -def prepare_predicate(next, token): - # FIXME: replace with real parser!!! refs: - # http://effbot.org/zone/simple-iterator-parser.htm - # http://javascript.crockford.com/tdop/tdop.html - signature = '' - predicate = [] - while 1: - token = next() - if token[0] == "]": - break - if token == ('', ''): - # ignore whitespace - continue - if token[0] and token[0][:1] in "'\"": - token = "'", token[0][1:-1] - signature += token[0] or "-" - predicate.append(token[1]) - - # use signature to determine predicate type - if signature == "@-": - # [@attribute] predicate - key = predicate[1] - def select(result): - for elem in result: - if elem.get(key) is not None: - yield elem - return select - if signature == "@-='": - # [@attribute='value'] - key = predicate[1] - value = predicate[-1] - def select(result): - for elem in result: - if elem.get(key) == value: - yield elem - return select - if signature == "-" and not re.match(r"-?\d+$", predicate[0]): - # [tag] - tag = predicate[0] - def select(result): - for elem in result: - for _ in elem.iterchildren(tag): - yield elem - break - return select - if signature == ".='" or (signature == "-='" and not re.match(r"-?\d+$", predicate[0])): - # [.='value'] or [tag='value'] - tag = predicate[0] - value = predicate[-1] - if tag: - def select(result): - for elem in result: - for e in elem.iterchildren(tag): - if "".join(e.itertext()) == value: - yield elem - break - else: - def select(result): - for elem in result: - if "".join(elem.itertext()) == value: - yield elem - return select - if signature == "-" or signature == "-()" or signature == "-()-": - # [index] or [last()] or [last()-index] - if signature == "-": - # [index] - index = int(predicate[0]) - 1 - if index < 0: - if index == -1: - raise SyntaxError( - "indices in path predicates are 1-based, not 0-based") - else: - raise SyntaxError("path index >= 1 expected") - else: - if predicate[0] != "last": - raise SyntaxError("unsupported function") - if signature == "-()-": - try: - index = int(predicate[2]) - 1 - except ValueError: - raise SyntaxError("unsupported expression") - else: - index = -1 - def select(result): - for elem in result: - parent = elem.getparent() - if parent is None: - continue - try: - # FIXME: what if the selector is "*" ? - elems = list(parent.iterchildren(elem.tag)) - if elems[index] is elem: - yield elem - except IndexError: - pass - return select - raise SyntaxError("invalid predicate") - -ops = { - "": prepare_child, - "*": prepare_star, - ".": prepare_self, - "..": prepare_parent, - "//": prepare_descendant, - "[": prepare_predicate, -} - - -# -------------------------------------------------------------------- - -_cache = {} - - -def _build_path_iterator(path, namespaces, with_prefixes=True): - """compile selector pattern""" - if path[-1:] == "/": - path += "*" # implicit all (FIXME: keep this?) - - cache_key = (path,) - if namespaces: - # lxml originally used None for the default namespace but ElementTree uses the - # more convenient (all-strings-dict) empty string, so we support both here, - # preferring the more convenient '', as long as they aren't ambiguous. - if None in namespaces: - if '' in namespaces and namespaces[None] != namespaces['']: - raise ValueError("Ambiguous default namespace provided: %r versus %r" % ( - namespaces[None], namespaces[''])) - cache_key += (namespaces[None],) + tuple(sorted( - item for item in namespaces.items() if item[0] is not None)) - else: - cache_key += tuple(sorted(namespaces.items())) - - try: - return _cache[cache_key] - except KeyError: - pass - if len(_cache) > 100: - _cache.clear() - - if path[:1] == "/": - raise SyntaxError("cannot use absolute path on element") - stream = iter(xpath_tokenizer(path, namespaces, with_prefixes=with_prefixes)) - try: - _next = stream.next - except AttributeError: - # Python 3 - _next = stream.__next__ - try: - token = _next() - except StopIteration: - raise SyntaxError("empty path expression") - selector = [] - while 1: - try: - selector.append(ops[token[0]](_next, token)) - except StopIteration: - raise SyntaxError("invalid path") - try: - token = _next() - if token[0] == "/": - token = _next() - except StopIteration: - break - _cache[cache_key] = selector - return selector - - -## -# Iterate over the matching nodes - -def iterfind(elem, path, namespaces=None, with_prefixes=True): - selector = _build_path_iterator(path, namespaces, with_prefixes=with_prefixes) - result = iter((elem,)) - for select in selector: - result = select(result) - return result - - -## -# Find first matching object. - -def find(elem, path, namespaces=None, with_prefixes=True): - it = iterfind(elem, path, namespaces, with_prefixes=with_prefixes) - try: - return next(it) - except StopIteration: - return None - - -## -# Find all matching objects. - -def findall(elem, path, namespaces=None, with_prefixes=True): - return list(iterfind(elem, path, namespaces)) - - -## -# Find text for first matching object. - -def findtext(elem, path, default=None, namespaces=None, with_prefixes=True): - el = find(elem, path, namespaces, with_prefixes=with_prefixes) - if el is None: - return default - else: - return el.text or '' diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/apihelpers.pxi b/presentation/.venv/lib/python3.12/site-packages/lxml/apihelpers.pxi deleted file mode 100644 index 87a27d9..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/apihelpers.pxi +++ /dev/null @@ -1,1819 +0,0 @@ -# Private/public helper functions for API functions - -from lxml.includes cimport uri - - -cdef void displayNode(xmlNode* c_node, indent) noexcept: - # to help with debugging - cdef xmlNode* c_child - try: - print(indent * ' ', <long>c_node) - c_child = c_node.children - while c_child is not NULL: - displayNode(c_child, indent + 1) - c_child = c_child.next - finally: - return # swallow any exceptions - -cdef inline bint _isHtmlDocument(_Element element) except -1: - cdef xmlNode* c_node = element._c_node - return ( - c_node is not NULL and c_node.doc is not NULL and - c_node.doc.properties & tree.XML_DOC_HTML != 0 - ) - -cdef inline int _assertValidNode(_Element element) except -1: - assert element._c_node is not NULL, "invalid Element proxy at %s" % id(element) - -cdef inline int _assertValidDoc(_Document doc) except -1: - assert doc._c_doc is not NULL, "invalid Document proxy at %s" % id(doc) - -cdef _Document _documentOrRaise(object input): - """Call this to get the document of a _Document, _ElementTree or _Element - object, or to raise an exception if it can't be determined. - - Should be used in all API functions for consistency. - """ - cdef _Document doc - if isinstance(input, _ElementTree): - if (<_ElementTree>input)._context_node is not None: - doc = (<_ElementTree>input)._context_node._doc - else: - doc = None - elif isinstance(input, _Element): - doc = (<_Element>input)._doc - elif isinstance(input, _Document): - doc = <_Document>input - else: - raise TypeError, f"Invalid input object: {python._fqtypename(input).decode('utf8')}" - if doc is None: - raise ValueError, f"Input object has no document: {python._fqtypename(input).decode('utf8')}" - _assertValidDoc(doc) - return doc - -cdef _Element _rootNodeOrRaise(object input): - """Call this to get the root node of a _Document, _ElementTree or - _Element object, or to raise an exception if it can't be determined. - - Should be used in all API functions for consistency. - """ - cdef _Element node - if isinstance(input, _ElementTree): - node = (<_ElementTree>input)._context_node - elif isinstance(input, _Element): - node = <_Element>input - elif isinstance(input, _Document): - node = (<_Document>input).getroot() - else: - raise TypeError, f"Invalid input object: {python._fqtypename(input).decode('utf8')}" - if (node is None or not node._c_node or - node._c_node.type != tree.XML_ELEMENT_NODE): - raise ValueError, f"Input object is not an XML element: {python._fqtypename(input).decode('utf8')}" - _assertValidNode(node) - return node - -cdef bint _isAncestorOrSame(xmlNode* c_ancestor, xmlNode* c_node) noexcept: - while c_node: - if c_node is c_ancestor: - return True - c_node = c_node.parent - return False - -cdef _Element _makeElement(tag, xmlDoc* c_doc, _Document doc, - _BaseParser parser, text, tail, attrib, nsmap, - dict extra_attrs): - """Create a new element and initialize text content, namespaces and - attributes. - - This helper function will reuse as much of the existing document as - possible: - - If 'parser' is None, the parser will be inherited from 'doc' or the - default parser will be used. - - If 'doc' is None, 'c_doc' is used to create a new _Document and the new - element is made its root node. - - If 'c_doc' is also NULL, a new xmlDoc will be created. - """ - cdef xmlNode* c_node - if doc is not None: - c_doc = doc._c_doc - ns_utf, name_utf = _getNsTag(tag) - if parser is not None and parser._for_html: - _htmlTagValidOrRaise(name_utf) - if c_doc is NULL: - c_doc = _newHTMLDoc() - else: - _tagValidOrRaise(name_utf) - if c_doc is NULL: - c_doc = _newXMLDoc() - c_node = _createElement(c_doc, name_utf) - if c_node is NULL: - if doc is None and c_doc is not NULL: - tree.xmlFreeDoc(c_doc) - raise MemoryError() - try: - if doc is None: - tree.xmlDocSetRootElement(c_doc, c_node) - doc = _documentFactory(c_doc, parser) - if text is not None: - _setNodeText(c_node, text) - if tail is not None: - _setTailText(c_node, tail) - # add namespaces to node if necessary - _setNodeNamespaces(c_node, doc, ns_utf, nsmap) - _initNodeAttributes(c_node, doc, attrib, extra_attrs) - return _elementFactory(doc, c_node) - except: - # free allocated c_node/c_doc unless Python does it for us - if c_node.doc is not c_doc: - # node not yet in document => will not be freed by document - if tail is not None: - _removeText(c_node.next) # tail - tree.xmlFreeNode(c_node) - if doc is None: - # c_doc will not be freed by doc - tree.xmlFreeDoc(c_doc) - raise - -cdef int _initNewElement(_Element element, bint is_html, name_utf, ns_utf, - _BaseParser parser, attrib, nsmap, dict extra_attrs) except -1: - """Initialise a new Element object. - - This is used when users instantiate a Python Element subclass - directly, without it being mapped to an existing XML node. - """ - cdef xmlDoc* c_doc - cdef xmlNode* c_node - cdef _Document doc - if is_html: - _htmlTagValidOrRaise(name_utf) - c_doc = _newHTMLDoc() - else: - _tagValidOrRaise(name_utf) - c_doc = _newXMLDoc() - c_node = _createElement(c_doc, name_utf) - if c_node is NULL: - if c_doc is not NULL: - tree.xmlFreeDoc(c_doc) - raise MemoryError() - tree.xmlDocSetRootElement(c_doc, c_node) - doc = _documentFactory(c_doc, parser) - # add namespaces to node if necessary - _setNodeNamespaces(c_node, doc, ns_utf, nsmap) - _initNodeAttributes(c_node, doc, attrib, extra_attrs) - _registerProxy(element, doc, c_node) - element._init() - return 0 - -cdef _Element _makeSubElement(_Element parent, tag, text, tail, - attrib, nsmap, dict extra_attrs): - """Create a new child element and initialize text content, namespaces and - attributes. - """ - cdef xmlNode* c_node - cdef xmlDoc* c_doc - if parent is None or parent._doc is None: - return None - _assertValidNode(parent) - ns_utf, name_utf = _getNsTag(tag) - c_doc = parent._doc._c_doc - - if parent._doc._parser is not None and parent._doc._parser._for_html: - _htmlTagValidOrRaise(name_utf) - else: - _tagValidOrRaise(name_utf) - - c_node = _createElement(c_doc, name_utf) - if c_node is NULL: - raise MemoryError() - tree.xmlAddChild(parent._c_node, c_node) - - try: - if text is not None: - _setNodeText(c_node, text) - if tail is not None: - _setTailText(c_node, tail) - - # add namespaces to node if necessary - _setNodeNamespaces(c_node, parent._doc, ns_utf, nsmap) - _initNodeAttributes(c_node, parent._doc, attrib, extra_attrs) - return _elementFactory(parent._doc, c_node) - except: - # make sure we clean up in case of an error - _removeNode(parent._doc, c_node) - raise - - -cdef int _setNodeNamespaces(xmlNode* c_node, _Document doc, - object node_ns_utf, object nsmap) except -1: - """Lookup current namespace prefixes, then set namespace structure for - node (if 'node_ns_utf' was provided) and register new ns-prefix mappings. - - 'node_ns_utf' should only be passed for a newly created node. - """ - cdef xmlNs* c_ns - cdef list nsdefs - - if nsmap: - for prefix, href in _iter_nsmap(nsmap): - href_utf = _utf8(href) - _uriValidOrRaise(href_utf) - c_href = _xcstr(href_utf) - if prefix is not None: - prefix_utf = _utf8(prefix) - _prefixValidOrRaise(prefix_utf) - c_prefix = _xcstr(prefix_utf) - else: - c_prefix = <const_xmlChar*>NULL - # add namespace with prefix if it is not already known - c_ns = tree.xmlSearchNs(doc._c_doc, c_node, c_prefix) - if c_ns is NULL or \ - c_ns.href is NULL or \ - tree.xmlStrcmp(c_ns.href, c_href) != 0: - c_ns = tree.xmlNewNs(c_node, c_href, c_prefix) - if c_ns is NULL: - # libxml2 has two error conditions: "out of memory" and "prefix exists already". - # We ignore the latter for compatibility reasons. It currently only appears - # during namespace cleanup. - c_ns = c_node.nsDef - while c_ns is not NULL: - if c_prefix is NULL: - if c_ns.prefix is NULL: - break - elif tree.xmlStrcmp(c_ns.prefix, c_prefix) == 0: - break - c_ns = c_ns.next - else: - raise MemoryError() - if href_utf == node_ns_utf: - tree.xmlSetNs(c_node, c_ns) - node_ns_utf = None - - if node_ns_utf is not None: - _uriValidOrRaise(node_ns_utf) - doc._setNodeNs(c_node, _xcstr(node_ns_utf)) - return 0 - - -cdef dict _build_nsmap(xmlNode* c_node): - """ - Namespace prefix->URI mapping known in the context of this Element. - This includes all namespace declarations of the parents. - """ - cdef xmlNs* c_ns - nsmap = {} - while c_node is not NULL and c_node.type == tree.XML_ELEMENT_NODE: - c_ns = c_node.nsDef - while c_ns is not NULL: - if c_ns.prefix or c_ns.href: - prefix = funicodeOrNone(c_ns.prefix) - if prefix not in nsmap: - nsmap[prefix] = funicodeOrNone(c_ns.href) - c_ns = c_ns.next - c_node = c_node.parent - return nsmap - - -cdef _iter_nsmap(nsmap): - """ - Create a reproducibly ordered iterable from an nsmap mapping. - Tries to preserve an existing order and sorts if it assumes no order. - - The difference to _iter_attrib() is that None doesn't sort with strings - in Py3.x. - """ - if isinstance(nsmap, dict): - # dicts are insertion-ordered in Py3.6+ => keep the user provided order. - return nsmap.items() - if len(nsmap) <= 1: - return nsmap.items() - if isinstance(nsmap, OrderedDict): - return nsmap.items() # keep existing order - if None not in nsmap: - return sorted(nsmap.items()) - - # Move the default namespace to the end. This makes sure libxml2 - # prefers a prefix if the ns is defined redundantly on the same - # element. That way, users can work around a problem themselves - # where default namespace attributes on non-default namespaced - # elements serialise without prefix (i.e. into the non-default - # namespace). - default_ns = nsmap[None] - nsdefs = [(k, v) for k, v in nsmap.items() if k is not None] - nsdefs.sort() - nsdefs.append((None, default_ns)) - return nsdefs - - -cdef _iter_attrib(attrib): - """ - Create a reproducibly ordered iterable from an attrib mapping. - Tries to preserve an existing order and sorts if it assumes no order. - """ - # dicts are insertion-ordered in Py3.6+ => keep the user provided order. - if isinstance(attrib, (dict, _Attrib, OrderedDict)): - return attrib.items() - # assume it's an unordered mapping of some kind - return sorted(attrib.items()) - - -cdef _initNodeAttributes(xmlNode* c_node, _Document doc, attrib, dict extra): - """Initialise the attributes of an element node. - """ - cdef bint is_html - cdef xmlNs* c_ns - if attrib is not None and not hasattr(attrib, 'items'): - raise TypeError, f"Invalid attribute dictionary: {python._fqtypename(attrib).decode('utf8')}" - if not attrib and not extra: - return # nothing to do - is_html = doc._parser._for_html - seen = set() - if extra: - for name, value in extra.items(): - _addAttributeToNode(c_node, doc, is_html, name, value, seen) - if attrib: - for name, value in _iter_attrib(attrib): - _addAttributeToNode(c_node, doc, is_html, name, value, seen) - - -cdef int _addAttributeToNode(xmlNode* c_node, _Document doc, bint is_html, - name, value, set seen_tags) except -1: - ns_utf, name_utf = tag = _getNsTag(name) - if tag in seen_tags: - return 0 - seen_tags.add(tag) - if not is_html: - _attributeValidOrRaise(name_utf) - value_utf = _utf8(value) - if ns_utf is None: - new_attr = tree.xmlNewProp(c_node, _xcstr(name_utf), _xcstr(value_utf)) - else: - _uriValidOrRaise(ns_utf) - c_ns = doc._findOrBuildNodeNs(c_node, _xcstr(ns_utf), NULL, 1) - new_attr = tree.xmlNewNsProp(c_node, c_ns, _xcstr(name_utf), _xcstr(value_utf)) - if new_attr is NULL: - raise MemoryError() - return 0 - - -ctypedef struct _ns_node_ref: - xmlNs* ns - xmlNode* node - - -cdef int _collectNsDefs(xmlNode* c_element, _ns_node_ref **_c_ns_list, - size_t *_c_ns_list_len, size_t *_c_ns_list_size) except -1: - c_ns_list = _c_ns_list[0] - cdef size_t c_ns_list_len = _c_ns_list_len[0] - cdef size_t c_ns_list_size = _c_ns_list_size[0] - - c_nsdef = c_element.nsDef - while c_nsdef is not NULL: - if c_ns_list_len >= c_ns_list_size: - if c_ns_list is NULL: - c_ns_list_size = 20 - else: - c_ns_list_size *= 2 - c_nsref_ptr = <_ns_node_ref*> python.lxml_realloc( - c_ns_list, c_ns_list_size, sizeof(_ns_node_ref)) - if c_nsref_ptr is NULL: - if c_ns_list is not NULL: - python.lxml_free(c_ns_list) - _c_ns_list[0] = NULL - raise MemoryError() - c_ns_list = c_nsref_ptr - - c_ns_list[c_ns_list_len] = _ns_node_ref(c_nsdef, c_element) - c_ns_list_len += 1 - c_nsdef = c_nsdef.next - - _c_ns_list_size[0] = c_ns_list_size - _c_ns_list_len[0] = c_ns_list_len - _c_ns_list[0] = c_ns_list - - -cdef int _removeUnusedNamespaceDeclarations(xmlNode* c_element, set prefixes_to_keep) except -1: - """Remove any namespace declarations from a subtree that are not used by - any of its elements (or attributes). - - If a 'prefixes_to_keep' is provided, it must be a set of prefixes. - Any corresponding namespace mappings will not be removed as part of the cleanup. - """ - cdef xmlNode* c_node - cdef _ns_node_ref* c_ns_list = NULL - cdef size_t c_ns_list_size = 0 - cdef size_t c_ns_list_len = 0 - cdef size_t i - - if c_element.parent and c_element.parent.type == tree.XML_DOCUMENT_NODE: - # include declarations on the document node - _collectNsDefs(c_element.parent, &c_ns_list, &c_ns_list_len, &c_ns_list_size) - - tree.BEGIN_FOR_EACH_ELEMENT_FROM(c_element, c_element, 1) - # collect all new namespace declarations into the ns list - if c_element.nsDef: - _collectNsDefs(c_element, &c_ns_list, &c_ns_list_len, &c_ns_list_size) - - # remove all namespace declarations from the list that are referenced - if c_ns_list_len and c_element.type == tree.XML_ELEMENT_NODE: - c_node = c_element - while c_node and c_ns_list_len: - if c_node.ns: - for i in range(c_ns_list_len): - if c_node.ns is c_ns_list[i].ns: - c_ns_list_len -= 1 - c_ns_list[i] = c_ns_list[c_ns_list_len] - #c_ns_list[c_ns_list_len] = _ns_node_ref(NULL, NULL) - break - if c_node is c_element: - # continue with attributes - c_node = <xmlNode*>c_element.properties - else: - c_node = c_node.next - tree.END_FOR_EACH_ELEMENT_FROM(c_element) - - if c_ns_list is NULL: - return 0 - - # free all namespace declarations that remained in the list, - # except for those we should keep explicitly - cdef xmlNs* c_nsdef - for i in range(c_ns_list_len): - if prefixes_to_keep is not None: - if c_ns_list[i].ns.prefix and c_ns_list[i].ns.prefix in prefixes_to_keep: - continue - c_node = c_ns_list[i].node - c_nsdef = c_node.nsDef - if c_nsdef is c_ns_list[i].ns: - c_node.nsDef = c_node.nsDef.next - else: - while c_nsdef.next is not c_ns_list[i].ns: - c_nsdef = c_nsdef.next - c_nsdef.next = c_nsdef.next.next - tree.xmlFreeNs(c_ns_list[i].ns) - - if c_ns_list is not NULL: - python.lxml_free(c_ns_list) - return 0 - -cdef xmlNs* _searchNsByHref(xmlNode* c_node, const_xmlChar* c_href, bint is_attribute) noexcept: - """Search a namespace declaration that covers a node (element or - attribute). - - For attributes, try to find a prefixed namespace declaration - instead of the default namespaces. This helps in supporting - round-trips for attributes on elements with a different namespace. - """ - cdef xmlNs* c_ns - cdef xmlNs* c_default_ns = NULL - cdef xmlNode* c_element - if c_href is NULL or c_node is NULL or c_node.type == tree.XML_ENTITY_REF_NODE: - return NULL - if tree.xmlStrcmp(c_href, tree.XML_XML_NAMESPACE) == 0: - # no special cases here, let libxml2 handle this - return tree.xmlSearchNsByHref(c_node.doc, c_node, c_href) - if c_node.type == tree.XML_ATTRIBUTE_NODE: - is_attribute = 1 - while c_node is not NULL and c_node.type != tree.XML_ELEMENT_NODE: - c_node = c_node.parent - c_element = c_node - while c_node is not NULL: - if c_node.type == tree.XML_ELEMENT_NODE: - c_ns = c_node.nsDef - while c_ns is not NULL: - if c_ns.href is not NULL and tree.xmlStrcmp(c_href, c_ns.href) == 0: - if c_ns.prefix is NULL and is_attribute: - # for attributes, continue searching a named - # prefix, but keep the first default namespace - # declaration that we found - if c_default_ns is NULL: - c_default_ns = c_ns - elif tree.xmlSearchNs( - c_element.doc, c_element, c_ns.prefix) is c_ns: - # start node is in namespace scope => found! - return c_ns - c_ns = c_ns.next - if c_node is not c_element and c_node.ns is not NULL: - # optimise: the node may have the namespace itself - c_ns = c_node.ns - if c_ns.href is not NULL and tree.xmlStrcmp(c_href, c_ns.href) == 0: - if c_ns.prefix is NULL and is_attribute: - # for attributes, continue searching a named - # prefix, but keep the first default namespace - # declaration that we found - if c_default_ns is NULL: - c_default_ns = c_ns - elif tree.xmlSearchNs( - c_element.doc, c_element, c_ns.prefix) is c_ns: - # start node is in namespace scope => found! - return c_ns - c_node = c_node.parent - # nothing found => use a matching default namespace or fail - if c_default_ns is not NULL: - if tree.xmlSearchNs(c_element.doc, c_element, NULL) is c_default_ns: - return c_default_ns - return NULL - -cdef int _replaceNodeByChildren(_Document doc, xmlNode* c_node) except -1: - # NOTE: this does not deallocate the node, just unlink it! - cdef xmlNode* c_parent - cdef xmlNode* c_child - if c_node.children is NULL: - tree.xmlUnlinkNode(c_node) - return 0 - - c_parent = c_node.parent - # fix parent links of children - c_child = c_node.children - while c_child is not NULL: - c_child.parent = c_parent - c_child = c_child.next - - # fix namespace references of children if their parent's namespace - # declarations get lost - if c_node.nsDef is not NULL: - c_child = c_node.children - while c_child is not NULL: - moveNodeToDocument(doc, doc._c_doc, c_child) - c_child = c_child.next - - # fix sibling links to/from child slice - if c_node.prev is NULL: - c_parent.children = c_node.children - else: - c_node.prev.next = c_node.children - c_node.children.prev = c_node.prev - if c_node.next is NULL: - c_parent.last = c_node.last - else: - c_node.next.prev = c_node.last - c_node.last.next = c_node.next - - # unlink c_node - c_node.children = c_node.last = NULL - c_node.parent = c_node.next = c_node.prev = NULL - return 0 - -cdef unicode _attributeValue(xmlNode* c_element, xmlAttr* c_attrib_node): - c_href = _getNs(<xmlNode*>c_attrib_node) - value = tree.xmlGetNsProp(c_element, c_attrib_node.name, c_href) - try: - result = funicode(value) - finally: - tree.xmlFree(value) - return result - -cdef unicode _attributeValueFromNsName(xmlNode* c_element, - const_xmlChar* c_href, const_xmlChar* c_name): - c_result = tree.xmlGetNsProp(c_element, c_name, c_href) - if c_result is NULL: - return None - try: - result = funicode(c_result) - finally: - tree.xmlFree(c_result) - return result - -cdef object _getNodeAttributeValue(xmlNode* c_node, key, default): - ns, tag = _getNsTag(key) - c_href = <const_xmlChar*>NULL if ns is None else _xcstr(ns) - c_result = tree.xmlGetNsProp(c_node, _xcstr(tag), c_href) - if c_result is NULL: - # XXX free namespace that is not in use..? - return default - try: - result = funicode(c_result) - finally: - tree.xmlFree(c_result) - return result - -cdef inline object _getAttributeValue(_Element element, key, default): - return _getNodeAttributeValue(element._c_node, key, default) - -cdef int _setAttributeValue(_Element element, key, value) except -1: - cdef const_xmlChar* c_value - cdef xmlNs* c_ns - ns, tag = _getNsTag(key) - is_html = element._doc._parser._for_html - if not is_html: - _attributeValidOrRaise(tag) - c_tag = _xcstr(tag) - if value is None and is_html: - c_value = NULL - else: - if isinstance(value, QName): - value = _resolveQNameText(element, value) - else: - value = _utf8(value) - c_value = _xcstr(value) - if ns is None: - c_ns = NULL - else: - c_ns = element._doc._findOrBuildNodeNs(element._c_node, _xcstr(ns), NULL, 1) - tree.xmlSetNsProp(element._c_node, c_ns, c_tag, c_value) - return 0 - -cdef int _delAttribute(_Element element, key) except -1: - ns, tag = _getNsTag(key) - c_href = <const_xmlChar*>NULL if ns is None else _xcstr(ns) - if _delAttributeFromNsName(element._c_node, c_href, _xcstr(tag)): - raise KeyError, key - return 0 - -cdef int _delAttributeFromNsName(xmlNode* c_node, const_xmlChar* c_href, const_xmlChar* c_name) noexcept: - c_attr = tree.xmlHasNsProp(c_node, c_name, c_href) - if c_attr is NULL: - # XXX free namespace that is not in use..? - return -1 - tree.xmlRemoveProp(c_attr) - return 0 - -cdef list _collectAttributes(xmlNode* c_node, int collecttype): - """Collect all attributes of a node in a list. Depending on collecttype, - it collects either the name (1), the value (2) or the name-value tuples. - """ - cdef Py_ssize_t count - c_attr = c_node.properties - count = 0 - while c_attr is not NULL: - if c_attr.type == tree.XML_ATTRIBUTE_NODE: - count += 1 - c_attr = c_attr.next - - if not count: - return [] - - attributes = [None] * count - c_attr = c_node.properties - count = 0 - while c_attr is not NULL: - if c_attr.type == tree.XML_ATTRIBUTE_NODE: - if collecttype == 1: - item = _namespacedName(<xmlNode*>c_attr) - elif collecttype == 2: - item = _attributeValue(c_node, c_attr) - else: - item = (_namespacedName(<xmlNode*>c_attr), - _attributeValue(c_node, c_attr)) - attributes[count] = item - count += 1 - c_attr = c_attr.next - return attributes - -cdef object __RE_XML_ENCODING = re.compile( - r'^(<\?xml[^>]+)\s+encoding\s*=\s*["\'][^"\']*["\'](\s*\?>|)', re.U) - -cdef object __REPLACE_XML_ENCODING = __RE_XML_ENCODING.sub -cdef object __HAS_XML_ENCODING = __RE_XML_ENCODING.match - -cdef object _stripEncodingDeclaration(object xml_string): - # this is a hack to remove the XML encoding declaration from unicode - return __REPLACE_XML_ENCODING(r'\g<1>\g<2>', xml_string) - -cdef bint _hasEncodingDeclaration(object xml_string) except -1: - # check if a (unicode) string has an XML encoding declaration - return __HAS_XML_ENCODING(xml_string) is not None - -cdef inline bint _hasText(xmlNode* c_node) noexcept: - return c_node is not NULL and _textNodeOrSkip(c_node.children) is not NULL - -cdef inline bint _hasTail(xmlNode* c_node) noexcept: - return c_node is not NULL and _textNodeOrSkip(c_node.next) is not NULL - -cdef inline bint _hasNonWhitespaceTail(xmlNode* c_node) except -1: - return _hasNonWhitespaceText(c_node, tail=True) - -cdef bint _hasNonWhitespaceText(xmlNode* c_node, bint tail=False) except -1: - c_text_node = c_node and _textNodeOrSkip(c_node.next if tail else c_node.children) - if c_text_node is NULL: - return False - while c_text_node is not NULL: - if c_text_node.content[0] != c'\0' and not _collectText(c_text_node).isspace(): - return True - c_text_node = _textNodeOrSkip(c_text_node.next) - return False - -cdef unicode _collectText(xmlNode* c_node): - """Collect all text nodes and return them as a unicode string. - - Start collecting at c_node. - - If there was no text to collect, return None - """ - cdef Py_ssize_t scount - cdef xmlChar* c_text - cdef xmlNode* c_node_cur - # check for multiple text nodes - scount = 0 - c_text = NULL - c_node_cur = c_node = _textNodeOrSkip(c_node) - while c_node_cur is not NULL: - if c_node_cur.content[0] != c'\0': - c_text = c_node_cur.content - scount += 1 - c_node_cur = _textNodeOrSkip(c_node_cur.next) - - # handle two most common cases first - if c_text is NULL: - return '' if scount > 0 else None - if scount == 1: - return funicode(c_text) - - # the rest is not performance critical anymore - result = b'' - while c_node is not NULL: - result += <unsigned char*>c_node.content - c_node = _textNodeOrSkip(c_node.next) - return funicode(<const_xmlChar*><unsigned char*>result) - -cdef void _removeText(xmlNode* c_node) noexcept: - """Remove all text nodes. - - Start removing at c_node. - """ - cdef xmlNode* c_next - c_node = _textNodeOrSkip(c_node) - while c_node is not NULL: - c_next = _textNodeOrSkip(c_node.next) - tree.xmlUnlinkNode(c_node) - tree.xmlFreeNode(c_node) - c_node = c_next - -cdef xmlNode* _createTextNode(xmlDoc* doc, value) except NULL: - cdef xmlNode* c_text_node - if isinstance(value, CDATA): - c_text_node = tree.xmlNewCDataBlock( - doc, _xcstr((<CDATA>value)._utf8_data), - python.PyBytes_GET_SIZE((<CDATA>value)._utf8_data)) - else: - text = _utf8(value) - c_text_node = tree.xmlNewDocText(doc, _xcstr(text)) - if not c_text_node: - raise MemoryError() - return c_text_node - -cdef int _setNodeText(xmlNode* c_node, value) except -1: - # remove all text nodes at the start first - _removeText(c_node.children) - if value is None: - return 0 - # now add new text node with value at start - c_text_node = _createTextNode(c_node.doc, value) - if c_node.children is NULL: - tree.xmlAddChild(c_node, c_text_node) - else: - tree.xmlAddPrevSibling(c_node.children, c_text_node) - return 0 - -cdef int _setTailText(xmlNode* c_node, value) except -1: - # remove all text nodes at the start first - _removeText(c_node.next) - if value is None: - return 0 - # now append new text node with value - c_text_node = _createTextNode(c_node.doc, value) - tree.xmlAddNextSibling(c_node, c_text_node) - return 0 - -cdef bytes _resolveQNameText(_Element element, value): - cdef xmlNs* c_ns - ns, tag = _getNsTag(value) - if ns is None: - return tag - else: - c_ns = element._doc._findOrBuildNodeNs( - element._c_node, _xcstr(ns), NULL, 0) - return python.PyBytes_FromFormat('%s:%s', c_ns.prefix, _cstr(tag)) - -cdef inline bint _hasChild(xmlNode* c_node) noexcept: - return c_node is not NULL and _findChildForwards(c_node, 0) is not NULL - -cdef inline Py_ssize_t _countElements(xmlNode* c_node) noexcept: - "Counts the elements within the following siblings and the node itself." - cdef Py_ssize_t count - count = 0 - while c_node is not NULL: - if _isElement(c_node): - count += 1 - c_node = c_node.next - return count - - -cdef int _findChildSlice( - slice sliceobject, xmlNode* c_parent, - xmlNode** c_start_node, Py_ssize_t* c_step, Py_ssize_t* c_length) except -1: - """Resolve a children slice. - - Returns the start node, step size and the slice length in the - pointer arguments. - """ - cdef Py_ssize_t start = 0, stop = 0, childcount - childcount = _countElements(c_parent.children) - if childcount == 0: - c_start_node[0] = NULL - c_length[0] = 0 - if sliceobject.step is None: - c_step[0] = 1 - else: - python._PyEval_SliceIndex(sliceobject.step, c_step) - return 0 - - python.PySlice_GetIndicesEx( - sliceobject, childcount, &start, &stop, c_step, c_length) - - if start > childcount // 2: - c_start_node[0] = _findChildBackwards(c_parent, childcount - start - 1) - else: - c_start_node[0] = _findChild(c_parent, start) - return 0 - - -cdef bint _isFullSlice(slice sliceobject) except -1: - """Conservative guess if this slice is a full slice as in ``s[:]``. - """ - cdef Py_ssize_t step = 0 - if sliceobject is None: - return 0 - if sliceobject.start is None and \ - sliceobject.stop is None: - if sliceobject.step is None: - return 1 - python._PyEval_SliceIndex(sliceobject.step, &step) - if step == 1: - return 1 - return 0 - return 0 - -cdef _collectChildren(_Element element): - cdef xmlNode* c_node - cdef list result = [] - c_node = element._c_node.children - if c_node is not NULL: - if not _isElement(c_node): - c_node = _nextElement(c_node) - while c_node is not NULL: - result.append(_elementFactory(element._doc, c_node)) - c_node = _nextElement(c_node) - return result - -cdef inline xmlNode* _findChild(xmlNode* c_node, Py_ssize_t index) noexcept: - if index < 0: - return _findChildBackwards(c_node, -index - 1) - else: - return _findChildForwards(c_node, index) - -cdef inline xmlNode* _findChildForwards(xmlNode* c_node, Py_ssize_t index) noexcept: - """Return child element of c_node with index, or return NULL if not found. - """ - cdef xmlNode* c_child - cdef Py_ssize_t c - c_child = c_node.children - c = 0 - while c_child is not NULL: - if _isElement(c_child): - if c == index: - return c_child - c += 1 - c_child = c_child.next - return NULL - -cdef inline xmlNode* _findChildBackwards(xmlNode* c_node, Py_ssize_t index) noexcept: - """Return child element of c_node with index, or return NULL if not found. - Search from the end. - """ - cdef xmlNode* c_child - cdef Py_ssize_t c - c_child = c_node.last - c = 0 - while c_child is not NULL: - if _isElement(c_child): - if c == index: - return c_child - c += 1 - c_child = c_child.prev - return NULL - -cdef inline xmlNode* _textNodeOrSkip(xmlNode* c_node) noexcept nogil: - """Return the node if it's a text node. Skip over ignorable nodes in a - series of text nodes. Return NULL if a non-ignorable node is found. - - This is used to skip over XInclude nodes when collecting adjacent text - nodes. - """ - while c_node is not NULL: - if c_node.type == tree.XML_TEXT_NODE or \ - c_node.type == tree.XML_CDATA_SECTION_NODE: - return c_node - elif c_node.type == tree.XML_XINCLUDE_START or \ - c_node.type == tree.XML_XINCLUDE_END: - c_node = c_node.next - else: - return NULL - return NULL - -cdef inline xmlNode* _nextElement(xmlNode* c_node) noexcept: - """Given a node, find the next sibling that is an element. - """ - if c_node is NULL: - return NULL - c_node = c_node.next - while c_node is not NULL: - if _isElement(c_node): - return c_node - c_node = c_node.next - return NULL - -cdef inline xmlNode* _previousElement(xmlNode* c_node) noexcept: - """Given a node, find the next sibling that is an element. - """ - if c_node is NULL: - return NULL - c_node = c_node.prev - while c_node is not NULL: - if _isElement(c_node): - return c_node - c_node = c_node.prev - return NULL - -cdef inline xmlNode* _parentElement(xmlNode* c_node) noexcept: - "Given a node, find the parent element." - if c_node is NULL or not _isElement(c_node): - return NULL - c_node = c_node.parent - if c_node is NULL or not _isElement(c_node): - return NULL - return c_node - -cdef inline bint _tagMatches(xmlNode* c_node, const_xmlChar* c_href, const_xmlChar* c_name) noexcept: - """Tests if the node matches namespace URI and tag name. - - A node matches if it matches both c_href and c_name. - - A node matches c_href if any of the following is true: - * c_href is NULL - * its namespace is NULL and c_href is the empty string - * its namespace string equals the c_href string - - A node matches c_name if any of the following is true: - * c_name is NULL - * its name string equals the c_name string - """ - if c_node is NULL: - return 0 - if c_node.type != tree.XML_ELEMENT_NODE: - # not an element, only succeed if we match everything - return c_name is NULL and c_href is NULL - if c_name is NULL: - if c_href is NULL: - # always match - return 1 - else: - c_node_href = _getNs(c_node) - if c_node_href is NULL: - return c_href[0] == c'\0' - else: - return tree.xmlStrcmp(c_node_href, c_href) == 0 - elif c_href is NULL: - if _getNs(c_node) is not NULL: - return 0 - return c_node.name == c_name or tree.xmlStrcmp(c_node.name, c_name) == 0 - elif c_node.name == c_name or tree.xmlStrcmp(c_node.name, c_name) == 0: - c_node_href = _getNs(c_node) - if c_node_href is NULL: - return c_href[0] == c'\0' - else: - return tree.xmlStrcmp(c_node_href, c_href) == 0 - else: - return 0 - -cdef inline bint _tagMatchesExactly(xmlNode* c_node, qname* c_qname) noexcept: - """Tests if the node matches namespace URI and tag name. - - This differs from _tagMatches() in that it does not consider a - NULL value in qname.href a wildcard, and that it expects the c_name - to be taken from the doc dict, i.e. it only compares the names by - address. - - A node matches if it matches both href and c_name of the qname. - - A node matches c_href if any of the following is true: - * its namespace is NULL and c_href is the empty string - * its namespace string equals the c_href string - - A node matches c_name if any of the following is true: - * c_name is NULL - * its name string points to the same address (!) as c_name - """ - return _nsTagMatchesExactly(_getNs(c_node), c_node.name, c_qname) - -cdef inline bint _nsTagMatchesExactly(const_xmlChar* c_node_href, - const_xmlChar* c_node_name, - qname* c_qname) noexcept: - """Tests if name and namespace URI match those of c_qname. - - This differs from _tagMatches() in that it does not consider a - NULL value in qname.href a wildcard, and that it expects the c_name - to be taken from the doc dict, i.e. it only compares the names by - address. - - A node matches if it matches both href and c_name of the qname. - - A node matches c_href if any of the following is true: - * its namespace is NULL and c_href is the empty string - * its namespace string equals the c_href string - - A node matches c_name if any of the following is true: - * c_name is NULL - * its name string points to the same address (!) as c_name - """ - cdef char* c_href - if c_qname.c_name is not NULL and c_qname.c_name is not c_node_name: - return 0 - if c_qname.href is NULL: - return 1 - c_href = python.__cstr(c_qname.href) - if c_href[0] == b'\0': - return c_node_href is NULL or c_node_href[0] == b'\0' - elif c_node_href is NULL: - return 0 - else: - return tree.xmlStrcmp(<const_xmlChar*>c_href, c_node_href) == 0 - -cdef Py_ssize_t _mapTagsToQnameMatchArray(xmlDoc* c_doc, list ns_tags, - qname* c_ns_tags, bint force_into_dict) except -1: - """Map a sequence of (name, namespace) pairs to a qname array for efficient - matching with _tagMatchesExactly() above. - - Note that each qname struct in the array owns its href byte string object - if it is not NULL. - """ - cdef Py_ssize_t count = 0, i, c_tag_len - cdef bytes ns, tag - cdef const_xmlChar* c_tag - - for ns, tag in ns_tags: - if tag is None: - c_tag = <const_xmlChar*> NULL - else: - c_tag_len = len(tag) - if c_tag_len > limits.INT_MAX: - # too long, not in the dict => not in the document - continue - elif force_into_dict: - c_tag = tree.xmlDictLookup(c_doc.dict, _xcstr(tag), <int> c_tag_len) - if c_tag is NULL: - # clean up before raising the error - for i in xrange(count): - cpython.ref.Py_XDECREF(c_ns_tags[i].href) - raise MemoryError() - else: - c_tag = tree.xmlDictExists(c_doc.dict, _xcstr(tag), <int> c_tag_len) - if c_tag is NULL: - # not in the dict => not in the document - continue - - c_ns_tags[count].c_name = c_tag - if ns is None: - c_ns_tags[count].href = NULL - else: - cpython.ref.Py_INCREF(ns) # keep an owned reference! - c_ns_tags[count].href = <python.PyObject*>ns - count += 1 - return count - -cdef int _removeNode(_Document doc, xmlNode* c_node) except -1: - """Unlink and free a node and subnodes if possible. Otherwise, make sure - it's self-contained. - """ - cdef xmlNode* c_next - c_next = c_node.next - tree.xmlUnlinkNode(c_node) - _moveTail(c_next, c_node) - if not attemptDeallocation(c_node): - # make namespaces absolute - moveNodeToDocument(doc, c_node.doc, c_node) - return 0 - -cdef int _removeSiblings(xmlNode* c_element, tree.xmlElementType node_type, bint with_tail) except -1: - cdef xmlNode* c_node - cdef xmlNode* c_next - c_node = c_element.next - while c_node is not NULL: - c_next = _nextElement(c_node) - if c_node.type == node_type: - if with_tail: - _removeText(c_node.next) - tree.xmlUnlinkNode(c_node) - attemptDeallocation(c_node) - c_node = c_next - c_node = c_element.prev - while c_node is not NULL: - c_next = _previousElement(c_node) - if c_node.type == node_type: - if with_tail: - _removeText(c_node.next) - tree.xmlUnlinkNode(c_node) - attemptDeallocation(c_node) - c_node = c_next - return 0 - -cdef void _moveTail(xmlNode* c_tail, xmlNode* c_target) noexcept: - cdef xmlNode* c_next - # tail support: look for any text nodes trailing this node and - # move them too - c_tail = _textNodeOrSkip(c_tail) - while c_tail is not NULL: - c_next = _textNodeOrSkip(c_tail.next) - c_target = tree.xmlAddNextSibling(c_target, c_tail) - c_tail = c_next - -cdef int _copyTail(xmlNode* c_tail, xmlNode* c_target) except -1: - cdef xmlNode* c_new_tail - # tail copying support: look for any text nodes trailing this node and - # copy it to the target node - c_tail = _textNodeOrSkip(c_tail) - while c_tail is not NULL: - if c_target.doc is not c_tail.doc: - c_new_tail = tree.xmlDocCopyNode(c_tail, c_target.doc, 0) - else: - c_new_tail = tree.xmlCopyNode(c_tail, 0) - if c_new_tail is NULL: - raise MemoryError() - c_target = tree.xmlAddNextSibling(c_target, c_new_tail) - c_tail = _textNodeOrSkip(c_tail.next) - return 0 - -cdef int _copyNonElementSiblings(xmlNode* c_node, xmlNode* c_target) except -1: - cdef xmlNode* c_copy - cdef xmlNode* c_sibling = c_node - while c_sibling.prev != NULL and \ - (c_sibling.prev.type == tree.XML_PI_NODE or - c_sibling.prev.type == tree.XML_COMMENT_NODE or - c_sibling.prev.type == tree.XML_DTD_NODE): - c_sibling = c_sibling.prev - while c_sibling != c_node: - if c_sibling.type == tree.XML_DTD_NODE: - c_copy = <xmlNode*>_copyDtd(<tree.xmlDtd*>c_sibling) - if c_sibling == <xmlNode*>c_node.doc.intSubset: - c_target.doc.intSubset = <tree.xmlDtd*>c_copy - else: # c_sibling == c_node.doc.extSubset - c_target.doc.extSubset = <tree.xmlDtd*>c_copy - else: - c_copy = tree.xmlDocCopyNode(c_sibling, c_target.doc, 1) - if c_copy is NULL: - raise MemoryError() - tree.xmlAddPrevSibling(c_target, c_copy) - c_sibling = c_sibling.next - while c_sibling.next != NULL and \ - (c_sibling.next.type == tree.XML_PI_NODE or - c_sibling.next.type == tree.XML_COMMENT_NODE): - c_sibling = c_sibling.next - c_copy = tree.xmlDocCopyNode(c_sibling, c_target.doc, 1) - if c_copy is NULL: - raise MemoryError() - tree.xmlAddNextSibling(c_target, c_copy) - -cdef int _deleteSlice(_Document doc, xmlNode* c_node, - Py_ssize_t count, Py_ssize_t step) except -1: - """Delete slice, ``count`` items starting with ``c_node`` with a step - width of ``step``. - """ - cdef xmlNode* c_next - cdef Py_ssize_t c, i - cdef _node_to_node_function next_element - if c_node is NULL: - return 0 - if step > 0: - next_element = _nextElement - else: - step = -step if step != python.PY_SSIZE_T_MIN else python.PY_SSIZE_T_MAX - next_element = _previousElement - # now start deleting nodes - c = 0 - c_next = c_node - while c_node is not NULL and c < count: - for i in range(step): - c_next = next_element(c_next) - if c_next is NULL: - break - _removeNode(doc, c_node) - c += 1 - c_node = c_next - return 0 - -cdef int _replaceSlice(_Element parent, xmlNode* c_node, - Py_ssize_t slicelength, Py_ssize_t step, - bint left_to_right, elements) except -1: - """Replace the slice of ``count`` elements starting at ``c_node`` with - positive step width ``step`` by the Elements in ``elements``. The - direction is given by the boolean argument ``left_to_right``. - - ``c_node`` may be NULL to indicate the end of the children list. - """ - cdef xmlNode* c_orig_neighbour - cdef xmlNode* c_next - cdef xmlDoc* c_source_doc - cdef _Element element - cdef Py_ssize_t seqlength, i, c - cdef _node_to_node_function next_element - assert step > 0, step - if left_to_right: - next_element = _nextElement - else: - next_element = _previousElement - - if not isinstance(elements, (list, tuple)): - elements = list(elements) - - if step != 1 or not left_to_right: - # *replacing* children stepwise with list => check size! - seqlength = len(elements) - if seqlength != slicelength: - raise ValueError, f"attempt to assign sequence of size {seqlength} " \ - f"to extended slice of size {slicelength}" - - if c_node is NULL: - # no children yet => add all elements straight away - if left_to_right: - for element in elements: - assert element is not None, "Node must not be None" - _appendChild(parent, element) - else: - for element in elements: - assert element is not None, "Node must not be None" - _prependChild(parent, element) - return 0 - - # remove the elements first as some might be re-added - if left_to_right: - # L->R, remember left neighbour - c_orig_neighbour = _previousElement(c_node) - else: - # R->L, remember right neighbour - c_orig_neighbour = _nextElement(c_node) - - # We remove the original slice elements one by one. Since we hold - # a Python reference to all elements that we will insert, it is - # safe to let _removeNode() try (and fail) to free them even if - # the element itself or one of its descendents will be reinserted. - c = 0 - c_next = c_node - while c_node is not NULL and c < slicelength: - for i in range(step): - c_next = next_element(c_next) - if c_next is NULL: - break - _removeNode(parent._doc, c_node) - c += 1 - c_node = c_next - - # make sure each element is inserted only once - elements = iter(elements) - - # find the first node right of the new insertion point - if left_to_right: - if c_orig_neighbour is not NULL: - c_node = next_element(c_orig_neighbour) - else: - # before the first element - c_node = _findChildForwards(parent._c_node, 0) - elif c_orig_neighbour is NULL: - # at the end, but reversed stepping - # append one element and go to the next insertion point - for element in elements: - assert element is not None, "Node must not be None" - _appendChild(parent, element) - c_node = element._c_node - if slicelength > 0: - slicelength -= 1 - for i in range(1, step): - c_node = next_element(c_node) - if c_node is NULL: - break - break - else: - c_node = c_orig_neighbour - - if left_to_right: - # adjust step size after removing slice as we are not stepping - # over the newly inserted elements - step -= 1 - - # now insert elements where we removed them - if c_node is not NULL: - for element in elements: - assert element is not None, "Node must not be None" - _assertValidNode(element) - # move element and tail over - c_source_doc = element._c_node.doc - c_next = element._c_node.next - tree.xmlAddPrevSibling(c_node, element._c_node) - _moveTail(c_next, element._c_node) - - # integrate element into new document - moveNodeToDocument(parent._doc, c_source_doc, element._c_node) - - # stop at the end of the slice - if slicelength > 0: - slicelength -= 1 - for i in range(step): - c_node = next_element(c_node) - if c_node is NULL: - break - if c_node is NULL: - break - else: - # everything inserted - return 0 - - # append the remaining elements at the respective end - if left_to_right: - for element in elements: - assert element is not None, "Node must not be None" - _assertValidNode(element) - _appendChild(parent, element) - else: - for element in elements: - assert element is not None, "Node must not be None" - _assertValidNode(element) - _prependChild(parent, element) - - return 0 - - -cdef int _linkChild(xmlNode* c_parent, xmlNode* c_node) except -1: - """Adaptation of 'xmlAddChild()' that deep-fix the document links iteratively. - """ - assert _isElement(c_node) - c_node.parent = c_parent - if c_parent.children is NULL: - c_parent.children = c_parent.last = c_node - else: - c_node.prev = c_parent.last - c_parent.last.next = c_node - c_parent.last = c_node - - _setTreeDoc(c_node, c_parent.doc) - return 0 - - -cdef int _appendChild(_Element parent, _Element child) except -1: - """Append a new child to a parent element. - """ - c_node = child._c_node - c_source_doc = c_node.doc - # prevent cycles - if _isAncestorOrSame(c_node, parent._c_node): - raise ValueError("cannot append parent to itself") - # store possible text node - c_next = c_node.next - # move node itself - tree.xmlUnlinkNode(c_node) - # do not call xmlAddChild() here since it would deep-traverse the tree - _linkChild(parent._c_node, c_node) - _moveTail(c_next, c_node) - # uh oh, elements may be pointing to different doc when - # parent element has moved; change them too.. - moveNodeToDocument(parent._doc, c_source_doc, c_node) - return 0 - -cdef int _prependChild(_Element parent, _Element child) except -1: - """Prepend a new child to a parent element. - """ - c_node = child._c_node - c_source_doc = c_node.doc - # prevent cycles - if _isAncestorOrSame(c_node, parent._c_node): - raise ValueError("cannot append parent to itself") - # store possible text node - c_next = c_node.next - # move node itself - c_child = _findChildForwards(parent._c_node, 0) - if c_child is NULL: - tree.xmlUnlinkNode(c_node) - # do not call xmlAddChild() here since it would deep-traverse the tree - _linkChild(parent._c_node, c_node) - else: - tree.xmlAddPrevSibling(c_child, c_node) - _moveTail(c_next, c_node) - # uh oh, elements may be pointing to different doc when - # parent element has moved; change them too.. - moveNodeToDocument(parent._doc, c_source_doc, c_node) - return 0 - -cdef int _appendSibling(_Element element, _Element sibling) except -1: - """Add a new sibling behind an element. - """ - return _addSibling(element, sibling, as_next=True) - -cdef int _prependSibling(_Element element, _Element sibling) except -1: - """Add a new sibling before an element. - """ - return _addSibling(element, sibling, as_next=False) - -cdef int _addSibling(_Element element, _Element sibling, bint as_next) except -1: - c_node = sibling._c_node - c_source_doc = c_node.doc - # prevent cycles - if _isAncestorOrSame(c_node, element._c_node): - if element._c_node is c_node: - return 0 # nothing to do - raise ValueError("cannot add ancestor as sibling, please break cycle first") - # store possible text node - c_next = c_node.next - # move node itself - if as_next: - # must insert after any tail text - c_next_node = _nextElement(element._c_node) - if c_next_node is NULL: - c_next_node = element._c_node - while c_next_node.next: - c_next_node = c_next_node.next - tree.xmlAddNextSibling(c_next_node, c_node) - else: - tree.xmlAddPrevSibling(c_next_node, c_node) - else: - tree.xmlAddPrevSibling(element._c_node, c_node) - _moveTail(c_next, c_node) - # uh oh, elements may be pointing to different doc when - # parent element has moved; change them too.. - moveNodeToDocument(element._doc, c_source_doc, c_node) - return 0 - -cdef inline bint isutf8(const_xmlChar* s) noexcept: - cdef xmlChar c = s[0] - while c != c'\0': - if c & 0x80: - return True - s += 1 - c = s[0] - return False - -cdef bint isutf8l(const_xmlChar* s, size_t length) noexcept: - """ - Search for non-ASCII characters in the string, knowing its length in advance. - """ - cdef unsigned int i - cdef unsigned long non_ascii_mask - cdef const unsigned long *lptr = <const unsigned long*> s - - cdef const unsigned long *end = lptr + length // sizeof(unsigned long) - if length >= sizeof(non_ascii_mask): - # Build constant 0x80808080... mask (and let the C compiler fold it). - non_ascii_mask = 0 - for i in range(sizeof(non_ascii_mask) // 2): - non_ascii_mask = (non_ascii_mask << 16) | 0x8080 - - # Advance to long-aligned character before we start reading longs. - while (<size_t>s) % sizeof(unsigned long) and s < <const_xmlChar *>end: - if s[0] & 0x80: - return True - s += 1 - - # Read one long at a time - lptr = <const unsigned long*> s - while lptr < end: - if lptr[0] & non_ascii_mask: - return True - lptr += 1 - s = <const_xmlChar *>lptr - - while s < (<const_xmlChar *>end + length % sizeof(unsigned long)): - if s[0] & 0x80: - return True - s += 1 - - return False - -cdef int _is_valid_xml_ascii(bytes pystring) except -1: - """Check if a string is XML ascii content.""" - cdef signed char ch - # When ch is a *signed* char, non-ascii characters are negative integers - # and xmlIsChar_ch does not accept them. - for ch in pystring: - if not tree.xmlIsChar_ch(ch): - return 0 - return 1 - -cdef bint _is_valid_xml_utf8(bytes pystring) except -1: - """Check if a string is like valid UTF-8 XML content.""" - cdef const_xmlChar* s = _xcstr(pystring) - cdef const_xmlChar* c_end = s + len(pystring) - cdef unsigned long next3 = 0 - if s < c_end - 2: - next3 = (s[0] << 8) | (s[1]) - - while s < c_end - 2: - next3 = 0x00ffffff & ((next3 << 8) | s[2]) - if s[0] & 0x80: - # 0xefbfbe and 0xefbfbf are utf-8 encodings of - # forbidden characters \ufffe and \uffff - if next3 == 0x00efbfbe or next3 == 0x00efbfbf: - return 0 - # 0xeda080 and 0xedbfbf are utf-8 encodings of - # \ud800 and \udfff. Anything between them (inclusive) - # is forbidden, because they are surrogate blocks in utf-16. - if 0x00eda080 <= next3 <= 0x00edbfbf: - return 0 - elif not tree.xmlIsChar_ch(s[0]): - return 0 # invalid ascii char - s += 1 - - while s < c_end: - if not s[0] & 0x80 and not tree.xmlIsChar_ch(s[0]): - return 0 # invalid ascii char - s += 1 - - return 1 - -cdef inline unicode funicodeOrNone(const_xmlChar* s): - return funicode(s) if s is not NULL else None - -cdef inline unicode funicodeOrEmpty(const_xmlChar* s): - return funicode(s) if s is not NULL else '' - -cdef unicode funicode(const_xmlChar* s): - return s.decode('UTF-8') - -cdef bytes _utf8(object s): - """Test if a string is valid user input and encode it to UTF-8. - Reject all bytes/unicode input that contains non-XML characters. - Reject all bytes input that contains non-ASCII characters. - """ - cdef int valid - cdef bytes utf8_string - if isinstance(s, unicode): - utf8_string = (<unicode>s).encode('utf8') - valid = _is_valid_xml_utf8(utf8_string) - elif isinstance(s, (bytes, bytearray)): - utf8_string = s if type(s) is bytes else bytes(s) - valid = _is_valid_xml_ascii(utf8_string) - else: - raise TypeError("Argument must be bytes or unicode, got '%.200s'" % type(s).__name__) - if not valid: - raise ValueError( - "All strings must be XML compatible: Unicode or ASCII, no NULL bytes or control characters") - return utf8_string - - -cdef bytes _utf8orNone(object s): - return _utf8(s) if s is not None else None - - -cdef enum: - NO_FILE_PATH = 0 - ABS_UNIX_FILE_PATH = 1 - ABS_WIN_FILE_PATH = 2 - REL_FILE_PATH = 3 - - -cdef bint _isFilePath(const_xmlChar* c_path) noexcept: - "simple heuristic to see if a path is a filename" - cdef xmlChar c - # test if it looks like an absolute Unix path or a Windows network path - if c_path[0] == c'/': - return ABS_UNIX_FILE_PATH - - # test if it looks like an absolute Windows path or URL - if c'a' <= c_path[0] <= c'z' or c'A' <= c_path[0] <= c'Z': - c_path += 1 - if c_path[0] == c':' and c_path[1] in b'\0\\': - return ABS_WIN_FILE_PATH # C: or C:\... - - # test if it looks like a URL with scheme:// - while c'a' <= c_path[0] <= c'z' or c'A' <= c_path[0] <= c'Z': - c_path += 1 - if c_path[0] == c':' and c_path[1] == c'/' and c_path[2] == c'/': - return NO_FILE_PATH - - # assume it's a relative path - return REL_FILE_PATH - - -cdef object _getFSPathOrObject(object obj): - """ - Get the __fspath__ attribute of an object if it exists. - Otherwise, the original object is returned. - """ - if _isString(obj): - return obj - try: - return python.PyOS_FSPath(obj) - except TypeError: - return obj - - -cdef object _encodeFilename(object filename): - """Make sure a filename is 8-bit encoded (or None). - """ - if filename is None: - return None - elif isinstance(filename, bytes): - return filename - elif isinstance(filename, unicode): - filename8 = (<unicode>filename).encode('utf8') - if _isFilePath(<unsigned char*>filename8): - try: - return python.PyUnicode_AsEncodedString( - filename, _C_FILENAME_ENCODING, NULL) - except UnicodeEncodeError: - pass - return filename8 - else: - raise TypeError("Argument must be string or unicode.") - -cdef object _decodeFilename(const_xmlChar* c_path): - """Make the filename a unicode string if we are in Py3. - """ - return _decodeFilenameWithLength(c_path, tree.xmlStrlen(c_path)) - -cdef object _decodeFilenameWithLength(const_xmlChar* c_path, size_t c_len): - """Make the filename a unicode string if we are in Py3. - """ - if _isFilePath(c_path): - try: - return python.PyUnicode_Decode( - <const_char*>c_path, c_len, _C_FILENAME_ENCODING, NULL) - except UnicodeDecodeError: - pass - try: - return (<unsigned char*>c_path)[:c_len].decode('UTF-8') - except UnicodeDecodeError: - # this is a stupid fallback, but it might still work... - return (<unsigned char*>c_path)[:c_len].decode('latin-1', 'replace') - -cdef object _encodeFilenameUTF8(object filename): - """Recode filename as UTF-8. Tries ASCII, local filesystem encoding and - UTF-8 as source encoding. - """ - cdef char* c_filename - if filename is None: - return None - elif isinstance(filename, bytes): - if not isutf8l(<bytes>filename, len(<bytes>filename)): - # plain ASCII! - return filename - c_filename = _cstr(<bytes>filename) - try: - # try to decode with default encoding - filename = python.PyUnicode_Decode( - c_filename, len(<bytes>filename), - _C_FILENAME_ENCODING, NULL) - except UnicodeDecodeError as decode_exc: - try: - # try if it's proper UTF-8 - (<bytes>filename).decode('utf8') - return filename - except UnicodeDecodeError: - raise decode_exc # otherwise re-raise original exception - if isinstance(filename, unicode): - return (<unicode>filename).encode('utf8') - else: - raise TypeError("Argument must be string or unicode.") - -cdef tuple _getNsTag(tag): - """Given a tag, find namespace URI and tag name. - Return None for NS uri if no namespace URI provided. - """ - return __getNsTag(tag, 0) - -cdef tuple _getNsTagWithEmptyNs(tag): - """Given a tag, find namespace URI and tag name. Return None for NS uri - if no namespace URI provided, or the empty string if namespace - part is '{}'. - """ - return __getNsTag(tag, 1) - -cdef tuple __getNsTag(tag, bint empty_ns): - cdef char* c_tag - cdef char* c_ns_end - cdef Py_ssize_t taglen - cdef Py_ssize_t nslen - cdef bytes ns = None - # _isString() is much faster than isinstance() - if not _isString(tag) and isinstance(tag, QName): - tag = (<QName>tag).text - tag = _utf8(tag) - c_tag = _cstr(tag) - if c_tag[0] == c'{': - c_tag += 1 - c_ns_end = cstring_h.strchr(c_tag, c'}') - if c_ns_end is NULL: - raise ValueError, "Invalid tag name" - nslen = c_ns_end - c_tag - taglen = python.PyBytes_GET_SIZE(tag) - nslen - 2 - if taglen == 0: - raise ValueError, "Empty tag name" - if nslen > 0: - ns = <bytes>c_tag[:nslen] - elif empty_ns: - ns = b'' - tag = <bytes>c_ns_end[1:taglen+1] - elif python.PyBytes_GET_SIZE(tag) == 0: - raise ValueError, "Empty tag name" - return ns, tag - -cdef inline int _pyXmlNameIsValid(name_utf8): - return _xmlNameIsValid(_xcstr(name_utf8)) and b':' not in name_utf8 - -cdef inline int _pyHtmlNameIsValid(name_utf8): - return _htmlNameIsValid(_xcstr(name_utf8)) - -cdef inline int _xmlNameIsValid(const_xmlChar* c_name) noexcept: - return tree.xmlValidateNameValue(c_name) - -cdef int _htmlNameIsValid(const_xmlChar* c_name) noexcept: - if c_name is NULL or c_name[0] == c'\0': - return 0 - while c_name[0] != c'\0': - if c_name[0] in b'&<>/"\'\t\n\x0B\x0C\r ': - return 0 - c_name += 1 - return 1 - -cdef bint _characterReferenceIsValid(const_xmlChar* c_name) noexcept: - cdef bint is_hex - if c_name[0] == c'x': - c_name += 1 - is_hex = 1 - else: - is_hex = 0 - if c_name[0] == c'\0': - return 0 - while c_name[0] != c'\0': - if c_name[0] < c'0' or c_name[0] > c'9': - if not is_hex: - return 0 - if not (c'a' <= c_name[0] <= c'f'): - if not (c'A' <= c_name[0] <= c'F'): - return 0 - c_name += 1 - return 1 - -cdef int _tagValidOrRaise(tag_utf) except -1: - if not _pyXmlNameIsValid(tag_utf): - raise ValueError(f"Invalid tag name {(<bytes>tag_utf).decode('utf8')!r}") - return 0 - -cdef int _htmlTagValidOrRaise(tag_utf) except -1: - if not _pyHtmlNameIsValid(tag_utf): - raise ValueError(f"Invalid HTML tag name {(<bytes>tag_utf).decode('utf8')!r}") - return 0 - -cdef int _attributeValidOrRaise(name_utf) except -1: - if not _pyXmlNameIsValid(name_utf): - raise ValueError(f"Invalid attribute name {(<bytes>name_utf).decode('utf8')!r}") - return 0 - -cdef int _prefixValidOrRaise(tag_utf) except -1: - if not _pyXmlNameIsValid(tag_utf): - raise ValueError(f"Invalid namespace prefix {(<bytes>tag_utf).decode('utf8')!r}") - return 0 - -cdef int _uriValidOrRaise(uri_utf) except -1: - cdef uri.xmlURI* c_uri = uri.xmlParseURI(_cstr(uri_utf)) - if c_uri is NULL: - raise ValueError(f"Invalid namespace URI {(<bytes>uri_utf).decode('utf8')!r}") - uri.xmlFreeURI(c_uri) - return 0 - -cdef inline unicode _namespacedName(xmlNode* c_node): - return _namespacedNameFromNsName(_getNs(c_node), c_node.name) - - -cdef unicode _namespacedNameFromNsName(const_xmlChar* c_href, const_xmlChar* c_name): - name = funicode(c_name) - if c_href is NULL: - return name - href = funicode(c_href) - return f"{{{href}}}{name}" - - -cdef _getFilenameForFile(source): - """Given a Python File or Gzip object, give filename back. - - Returns None if not a file object. - """ - # urllib2 provides a geturl() method - try: - return source.geturl() - except: - pass - # file instances have a name attribute - try: - filename = source.name - if _isString(filename): - return os_path_abspath(filename) - except: - pass - # gzip file instances have a filename attribute (before Py3k) - try: - filename = source.filename - if _isString(filename): - return os_path_abspath(filename) - except: - pass - # can't determine filename - return None diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/builder.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/lxml/builder.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index 9dd5bf7..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/builder.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/builder.py b/presentation/.venv/lib/python3.12/site-packages/lxml/builder.py deleted file mode 100644 index f5831fb..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/builder.py +++ /dev/null @@ -1,243 +0,0 @@ -# cython: language_level=2 - -# -# Element generator factory by Fredrik Lundh. -# -# Source: -# http://online.effbot.org/2006_11_01_archive.htm#et-builder -# http://effbot.python-hosting.com/file/stuff/sandbox/elementlib/builder.py -# -# -------------------------------------------------------------------- -# The ElementTree toolkit is -# -# Copyright (c) 1999-2004 by Fredrik Lundh -# -# By obtaining, using, and/or copying this software and/or its -# associated documentation, you agree that you have read, understood, -# and will comply with the following terms and conditions: -# -# Permission to use, copy, modify, and distribute this software and -# its associated documentation for any purpose and without fee is -# hereby granted, provided that the above copyright notice appears in -# all copies, and that both that copyright notice and this permission -# notice appear in supporting documentation, and that the name of -# Secret Labs AB or the author not be used in advertising or publicity -# pertaining to distribution of the software without specific, written -# prior permission. -# -# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD -# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- -# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR -# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY -# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS -# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -# OF THIS SOFTWARE. -# -------------------------------------------------------------------- - -""" -The ``E`` Element factory for generating XML documents. -""" - - -import lxml.etree as ET -_QName = ET.QName - -from functools import partial - -try: - from types import GenericAlias as _GenericAlias -except ImportError: - # Python 3.8 - we only need this as return value from "__class_getitem__" - def _GenericAlias(cls, item): - return f"{cls.__name__}[{item.__name__}]" - -try: - basestring -except NameError: - basestring = str - -try: - unicode -except NameError: - unicode = str - - -class ElementMaker: - """Element generator factory. - - Unlike the ordinary Element factory, the E factory allows you to pass in - more than just a tag and some optional attributes; you can also pass in - text and other elements. The text is added as either text or tail - attributes, and elements are inserted at the right spot. Some small - examples:: - - >>> from lxml import etree as ET - >>> from lxml.builder import E - - >>> ET.tostring(E("tag")) - '<tag/>' - >>> ET.tostring(E("tag", "text")) - '<tag>text</tag>' - >>> ET.tostring(E("tag", "text", key="value")) - '<tag key="value">text</tag>' - >>> ET.tostring(E("tag", E("subtag", "text"), "tail")) - '<tag><subtag>text</subtag>tail</tag>' - - For simple tags, the factory also allows you to write ``E.tag(...)`` instead - of ``E('tag', ...)``:: - - >>> ET.tostring(E.tag()) - '<tag/>' - >>> ET.tostring(E.tag("text")) - '<tag>text</tag>' - >>> ET.tostring(E.tag(E.subtag("text"), "tail")) - '<tag><subtag>text</subtag>tail</tag>' - - Here's a somewhat larger example; this shows how to generate HTML - documents, using a mix of prepared factory functions for inline elements, - nested ``E.tag`` calls, and embedded XHTML fragments:: - - # some common inline elements - A = E.a - I = E.i - B = E.b - - def CLASS(v): - # helper function, 'class' is a reserved word - return {'class': v} - - page = ( - E.html( - E.head( - E.title("This is a sample document") - ), - E.body( - E.h1("Hello!", CLASS("title")), - E.p("This is a paragraph with ", B("bold"), " text in it!"), - E.p("This is another paragraph, with a ", - A("link", href="http://www.python.org"), "."), - E.p("Here are some reserved characters: <spam&egg>."), - ET.XML("<p>And finally, here is an embedded XHTML fragment.</p>"), - ) - ) - ) - - print ET.tostring(page) - - Here's a prettyprinted version of the output from the above script:: - - <html> - <head> - <title>This is a sample document - - -

Hello!

-

This is a paragraph with bold text in it!

-

This is another paragraph, with link.

-

Here are some reserved characters: <spam&egg>.

-

And finally, here is an embedded XHTML fragment.

- - - - For namespace support, you can pass a namespace map (``nsmap``) - and/or a specific target ``namespace`` to the ElementMaker class:: - - >>> E = ElementMaker(namespace="http://my.ns/") - >>> print(ET.tostring( E.test )) - - - >>> E = ElementMaker(namespace="http://my.ns/", nsmap={'p':'http://my.ns/'}) - >>> print(ET.tostring( E.test )) - - """ - - def __init__(self, typemap=None, - namespace=None, nsmap=None, makeelement=None): - self._namespace = '{' + namespace + '}' if namespace is not None else None - self._nsmap = dict(nsmap) if nsmap else None - - assert makeelement is None or callable(makeelement) - self._makeelement = makeelement if makeelement is not None else ET.Element - - # initialize the default type map functions for this element factory - typemap = dict(typemap) if typemap else {} - - def add_text(elem, item): - try: - last_child = elem[-1] - except IndexError: - elem.text = (elem.text or "") + item - else: - last_child.tail = (last_child.tail or "") + item - - def add_cdata(elem, cdata): - if elem.text: - raise ValueError("Can't add a CDATA section. Element already has some text: %r" % elem.text) - elem.text = cdata - - if str not in typemap: - typemap[str] = add_text - if unicode not in typemap: - typemap[unicode] = add_text - if ET.CDATA not in typemap: - typemap[ET.CDATA] = add_cdata - - def add_dict(elem, item): - attrib = elem.attrib - for k, v in item.items(): - if isinstance(v, basestring): - attrib[k] = v - else: - attrib[k] = typemap[type(v)](None, v) - - if dict not in typemap: - typemap[dict] = add_dict - - self._typemap = typemap - - def __call__(self, tag, *children, **attrib): - typemap = self._typemap - - # We'll usually get a 'str', and the compiled type check is very fast. - if not isinstance(tag, str) and isinstance(tag, _QName): - # A QName is explicitly qualified, do not look at self._namespace. - tag = tag.text - elif self._namespace is not None and tag[0] != '{': - tag = self._namespace + tag - elem = self._makeelement(tag, nsmap=self._nsmap) - if attrib: - typemap[dict](elem, attrib) - - for item in children: - if callable(item): - item = item() - t = typemap.get(type(item)) - if t is None: - if ET.iselement(item): - elem.append(item) - continue - for basetype in type(item).__mro__: - # See if the typemap knows of any of this type's bases. - t = typemap.get(basetype) - if t is not None: - break - else: - raise TypeError("bad argument type: %s(%r)" % - (type(item).__name__, item)) - v = t(elem, item) - if v: - typemap.get(type(v))(elem, v) - - return elem - - def __getattr__(self, tag): - return partial(self, tag) - - # Allow subscripting ElementMaker in type annotions (PEP 560) - def __class_getitem__(cls, item): - return _GenericAlias(cls, item) - - -# create factory object -E = ElementMaker() diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/classlookup.pxi b/presentation/.venv/lib/python3.12/site-packages/lxml/classlookup.pxi deleted file mode 100644 index 92d1d47..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/classlookup.pxi +++ /dev/null @@ -1,580 +0,0 @@ -# Configurable Element class lookup - -################################################################################ -# Custom Element classes - -cdef public class ElementBase(_Element) [ type LxmlElementBaseType, - object LxmlElementBase ]: - """ElementBase(*children, attrib=None, nsmap=None, **_extra) - - The public Element class. All custom Element classes must inherit - from this one. To create an Element, use the `Element()` factory. - - BIG FAT WARNING: Subclasses *must not* override __init__ or - __new__ as it is absolutely undefined when these objects will be - created or destroyed. All persistent state of Elements must be - stored in the underlying XML. If you really need to initialize - the object after creation, you can implement an ``_init(self)`` - method that will be called directly after object creation. - - Subclasses of this class can be instantiated to create a new - Element. By default, the tag name will be the class name and the - namespace will be empty. You can modify this with the following - class attributes: - - * TAG - the tag name, possibly containing a namespace in Clark - notation - - * NAMESPACE - the default namespace URI, unless provided as part - of the TAG attribute. - - * HTML - flag if the class is an HTML tag, as opposed to an XML - tag. This only applies to un-namespaced tags and defaults to - false (i.e. XML). - - * PARSER - the parser that provides the configuration for the - newly created document. Providing an HTML parser here will - default to creating an HTML element. - - In user code, the latter three are commonly inherited in class - hierarchies that implement a common namespace. - """ - def __init__(self, *children, attrib=None, nsmap=None, **_extra): - """ElementBase(*children, attrib=None, nsmap=None, **_extra) - """ - cdef bint is_html = 0 - cdef _BaseParser parser - cdef _Element last_child - # don't use normal attribute access as it might be overridden - _getattr = object.__getattribute__ - try: - namespace = _utf8(_getattr(self, 'NAMESPACE')) - except AttributeError: - namespace = None - try: - ns, tag = _getNsTag(_getattr(self, 'TAG')) - if ns is not None: - namespace = ns - except AttributeError: - tag = _utf8(_getattr(_getattr(self, '__class__'), '__name__')) - if b'.' in tag: - tag = tag.split(b'.')[-1] - try: - parser = _getattr(self, 'PARSER') - except AttributeError: - parser = None - for child in children: - if isinstance(child, _Element): - parser = (<_Element>child)._doc._parser - break - if isinstance(parser, HTMLParser): - is_html = 1 - if namespace is None: - try: - is_html = _getattr(self, 'HTML') - except AttributeError: - pass - _initNewElement(self, is_html, tag, namespace, parser, - attrib, nsmap, _extra) - last_child = None - for child in children: - if _isString(child): - if last_child is None: - _setNodeText(self._c_node, - (_collectText(self._c_node.children) or '') + child) - else: - _setTailText(last_child._c_node, - (_collectText(last_child._c_node.next) or '') + child) - elif isinstance(child, _Element): - last_child = child - _appendChild(self, last_child) - elif isinstance(child, type) and issubclass(child, ElementBase): - last_child = child() - _appendChild(self, last_child) - else: - raise TypeError, f"Invalid child type: {type(child)!r}" - -cdef class CommentBase(_Comment): - """All custom Comment classes must inherit from this one. - - To create an XML Comment instance, use the ``Comment()`` factory. - - Subclasses *must not* override __init__ or __new__ as it is - absolutely undefined when these objects will be created or - destroyed. All persistent state of Comments must be stored in the - underlying XML. If you really need to initialize the object after - creation, you can implement an ``_init(self)`` method that will be - called after object creation. - """ - def __init__(self, text): - # copied from Comment() factory - cdef _Document doc - cdef xmlDoc* c_doc - if text is None: - text = b'' - else: - text = _utf8(text) - c_doc = _newXMLDoc() - doc = _documentFactory(c_doc, None) - self._c_node = _createComment(c_doc, _xcstr(text)) - if self._c_node is NULL: - raise MemoryError() - tree.xmlAddChild(c_doc, self._c_node) - _registerProxy(self, doc, self._c_node) - self._init() - -cdef class PIBase(_ProcessingInstruction): - """All custom Processing Instruction classes must inherit from this one. - - To create an XML ProcessingInstruction instance, use the ``PI()`` - factory. - - Subclasses *must not* override __init__ or __new__ as it is - absolutely undefined when these objects will be created or - destroyed. All persistent state of PIs must be stored in the - underlying XML. If you really need to initialize the object after - creation, you can implement an ``_init(self)`` method that will be - called after object creation. - """ - def __init__(self, target, text=None): - # copied from PI() factory - cdef _Document doc - cdef xmlDoc* c_doc - target = _utf8(target) - if text is None: - text = b'' - else: - text = _utf8(text) - c_doc = _newXMLDoc() - doc = _documentFactory(c_doc, None) - self._c_node = _createPI(c_doc, _xcstr(target), _xcstr(text)) - if self._c_node is NULL: - raise MemoryError() - tree.xmlAddChild(c_doc, self._c_node) - _registerProxy(self, doc, self._c_node) - self._init() - -cdef class EntityBase(_Entity): - """All custom Entity classes must inherit from this one. - - To create an XML Entity instance, use the ``Entity()`` factory. - - Subclasses *must not* override __init__ or __new__ as it is - absolutely undefined when these objects will be created or - destroyed. All persistent state of Entities must be stored in the - underlying XML. If you really need to initialize the object after - creation, you can implement an ``_init(self)`` method that will be - called after object creation. - """ - def __init__(self, name): - cdef _Document doc - cdef xmlDoc* c_doc - name_utf = _utf8(name) - c_name = _xcstr(name_utf) - if c_name[0] == c'#': - if not _characterReferenceIsValid(c_name + 1): - raise ValueError, f"Invalid character reference: '{name}'" - elif not _xmlNameIsValid(c_name): - raise ValueError, f"Invalid entity reference: '{name}'" - c_doc = _newXMLDoc() - doc = _documentFactory(c_doc, None) - self._c_node = _createEntity(c_doc, c_name) - if self._c_node is NULL: - raise MemoryError() - tree.xmlAddChild(c_doc, self._c_node) - _registerProxy(self, doc, self._c_node) - self._init() - - -cdef int _validateNodeClass(xmlNode* c_node, cls) except -1: - if c_node.type == tree.XML_ELEMENT_NODE: - expected = ElementBase - elif c_node.type == tree.XML_COMMENT_NODE: - expected = CommentBase - elif c_node.type == tree.XML_ENTITY_REF_NODE: - expected = EntityBase - elif c_node.type == tree.XML_PI_NODE: - expected = PIBase - else: - assert False, f"Unknown node type: {c_node.type}" - - if not (isinstance(cls, type) and issubclass(cls, expected)): - raise TypeError( - f"result of class lookup must be subclass of {type(expected)}, got {type(cls)}") - return 0 - - -################################################################################ -# Element class lookup - -ctypedef public object (*_element_class_lookup_function)(object, _Document, xmlNode*) - -# class to store element class lookup functions -cdef public class ElementClassLookup [ type LxmlElementClassLookupType, - object LxmlElementClassLookup ]: - """ElementClassLookup(self) - Superclass of Element class lookups. - """ - cdef _element_class_lookup_function _lookup_function - - -cdef public class FallbackElementClassLookup(ElementClassLookup) \ - [ type LxmlFallbackElementClassLookupType, - object LxmlFallbackElementClassLookup ]: - """FallbackElementClassLookup(self, fallback=None) - - Superclass of Element class lookups with additional fallback. - """ - cdef readonly ElementClassLookup fallback - cdef _element_class_lookup_function _fallback_function - def __cinit__(self): - # fall back to default lookup - self._fallback_function = _lookupDefaultElementClass - - def __init__(self, ElementClassLookup fallback=None): - if fallback is not None: - self._setFallback(fallback) - else: - self._fallback_function = _lookupDefaultElementClass - - cdef void _setFallback(self, ElementClassLookup lookup): - """Sets the fallback scheme for this lookup method. - """ - self.fallback = lookup - self._fallback_function = lookup._lookup_function - if self._fallback_function is NULL: - self._fallback_function = _lookupDefaultElementClass - - def set_fallback(self, ElementClassLookup lookup not None): - """set_fallback(self, lookup) - - Sets the fallback scheme for this lookup method. - """ - self._setFallback(lookup) - -cdef inline object _callLookupFallback(FallbackElementClassLookup lookup, - _Document doc, xmlNode* c_node): - return lookup._fallback_function(lookup.fallback, doc, c_node) - - -################################################################################ -# default lookup scheme - -cdef class ElementDefaultClassLookup(ElementClassLookup): - """ElementDefaultClassLookup(self, element=None, comment=None, pi=None, entity=None) - Element class lookup scheme that always returns the default Element - class. - - The keyword arguments ``element``, ``comment``, ``pi`` and ``entity`` - accept the respective Element classes. - """ - cdef readonly object element_class - cdef readonly object comment_class - cdef readonly object pi_class - cdef readonly object entity_class - def __cinit__(self): - self._lookup_function = _lookupDefaultElementClass - - def __init__(self, element=None, comment=None, pi=None, entity=None): - if element is None: - self.element_class = _Element - elif issubclass(element, ElementBase): - self.element_class = element - else: - raise TypeError, "element class must be subclass of ElementBase" - - if comment is None: - self.comment_class = _Comment - elif issubclass(comment, CommentBase): - self.comment_class = comment - else: - raise TypeError, "comment class must be subclass of CommentBase" - - if entity is None: - self.entity_class = _Entity - elif issubclass(entity, EntityBase): - self.entity_class = entity - else: - raise TypeError, "Entity class must be subclass of EntityBase" - - if pi is None: - self.pi_class = None # special case, see below - elif issubclass(pi, PIBase): - self.pi_class = pi - else: - raise TypeError, "PI class must be subclass of PIBase" - -cdef object _lookupDefaultElementClass(state, _Document _doc, xmlNode* c_node): - "Trivial class lookup function that always returns the default class." - if c_node.type == tree.XML_ELEMENT_NODE: - if state is not None: - return (state).element_class - else: - return _Element - elif c_node.type == tree.XML_COMMENT_NODE: - if state is not None: - return (state).comment_class - else: - return _Comment - elif c_node.type == tree.XML_ENTITY_REF_NODE: - if state is not None: - return (state).entity_class - else: - return _Entity - elif c_node.type == tree.XML_PI_NODE: - if state is None or (state).pi_class is None: - # special case XSLT-PI - if c_node.name is not NULL and c_node.content is not NULL: - if tree.xmlStrcmp(c_node.name, "xml-stylesheet") == 0: - if tree.xmlStrstr(c_node.content, "text/xsl") is not NULL or \ - tree.xmlStrstr(c_node.content, "text/xml") is not NULL: - return _XSLTProcessingInstruction - return _ProcessingInstruction - else: - return (state).pi_class - else: - assert False, f"Unknown node type: {c_node.type}" - - -################################################################################ -# attribute based lookup scheme - -cdef class AttributeBasedElementClassLookup(FallbackElementClassLookup): - """AttributeBasedElementClassLookup(self, attribute_name, class_mapping, fallback=None) - Checks an attribute of an Element and looks up the value in a - class dictionary. - - Arguments: - - attribute name - '{ns}name' style string - - class mapping - Python dict mapping attribute values to Element classes - - fallback - optional fallback lookup mechanism - - A None key in the class mapping will be checked if the attribute is - missing. - """ - cdef object _class_mapping - cdef tuple _pytag - cdef const_xmlChar* _c_ns - cdef const_xmlChar* _c_name - def __cinit__(self): - self._lookup_function = _attribute_class_lookup - - def __init__(self, attribute_name, class_mapping, - ElementClassLookup fallback=None): - self._pytag = _getNsTag(attribute_name) - ns, name = self._pytag - if ns is None: - self._c_ns = NULL - else: - self._c_ns = _xcstr(ns) - self._c_name = _xcstr(name) - self._class_mapping = dict(class_mapping) - - FallbackElementClassLookup.__init__(self, fallback) - -cdef object _attribute_class_lookup(state, _Document doc, xmlNode* c_node): - cdef AttributeBasedElementClassLookup lookup - cdef python.PyObject* dict_result - - lookup = state - if c_node.type == tree.XML_ELEMENT_NODE: - value = _attributeValueFromNsName( - c_node, lookup._c_ns, lookup._c_name) - dict_result = python.PyDict_GetItem(lookup._class_mapping, value) - if dict_result is not NULL: - cls = dict_result - _validateNodeClass(c_node, cls) - return cls - return _callLookupFallback(lookup, doc, c_node) - - -################################################################################ -# per-parser lookup scheme - -cdef class ParserBasedElementClassLookup(FallbackElementClassLookup): - """ParserBasedElementClassLookup(self, fallback=None) - Element class lookup based on the XML parser. - """ - def __cinit__(self): - self._lookup_function = _parser_class_lookup - -cdef object _parser_class_lookup(state, _Document doc, xmlNode* c_node): - if doc._parser._class_lookup is not None: - return doc._parser._class_lookup._lookup_function( - doc._parser._class_lookup, doc, c_node) - return _callLookupFallback(state, doc, c_node) - - -################################################################################ -# custom class lookup based on node type, namespace, name - -cdef class CustomElementClassLookup(FallbackElementClassLookup): - """CustomElementClassLookup(self, fallback=None) - Element class lookup based on a subclass method. - - You can inherit from this class and override the method:: - - lookup(self, type, doc, namespace, name) - - to lookup the element class for a node. Arguments of the method: - * type: one of 'element', 'comment', 'PI', 'entity' - * doc: document that the node is in - * namespace: namespace URI of the node (or None for comments/PIs/entities) - * name: name of the element/entity, None for comments, target for PIs - - If you return None from this method, the fallback will be called. - """ - def __cinit__(self): - self._lookup_function = _custom_class_lookup - - def lookup(self, type, doc, namespace, name): - "lookup(self, type, doc, namespace, name)" - return None - -cdef object _custom_class_lookup(state, _Document doc, xmlNode* c_node): - cdef CustomElementClassLookup lookup - - lookup = state - - if c_node.type == tree.XML_ELEMENT_NODE: - element_type = "element" - elif c_node.type == tree.XML_COMMENT_NODE: - element_type = "comment" - elif c_node.type == tree.XML_PI_NODE: - element_type = "PI" - elif c_node.type == tree.XML_ENTITY_REF_NODE: - element_type = "entity" - else: - element_type = "element" - if c_node.name is NULL: - name = None - else: - name = funicode(c_node.name) - c_str = tree._getNs(c_node) - ns = funicode(c_str) if c_str is not NULL else None - - cls = lookup.lookup(element_type, doc, ns, name) - if cls is not None: - _validateNodeClass(c_node, cls) - return cls - return _callLookupFallback(lookup, doc, c_node) - - -################################################################################ -# read-only tree based class lookup - -cdef class PythonElementClassLookup(FallbackElementClassLookup): - """PythonElementClassLookup(self, fallback=None) - Element class lookup based on a subclass method. - - This class lookup scheme allows access to the entire XML tree in - read-only mode. To use it, re-implement the ``lookup(self, doc, - root)`` method in a subclass:: - - from lxml import etree, pyclasslookup - - class MyElementClass(etree.ElementBase): - honkey = True - - class MyLookup(pyclasslookup.PythonElementClassLookup): - def lookup(self, doc, root): - if root.tag == "sometag": - return MyElementClass - else: - for child in root: - if child.tag == "someothertag": - return MyElementClass - # delegate to default - return None - - If you return None from this method, the fallback will be called. - - The first argument is the opaque document instance that contains - the Element. The second argument is a lightweight Element proxy - implementation that is only valid during the lookup. Do not try - to keep a reference to it. Once the lookup is done, the proxy - will be invalid. - - Also, you cannot wrap such a read-only Element in an ElementTree, - and you must take care not to keep a reference to them outside of - the `lookup()` method. - - Note that the API of the Element objects is not complete. It is - purely read-only and does not support all features of the normal - `lxml.etree` API (such as XPath, extended slicing or some - iteration methods). - - See https://lxml.de/element_classes.html - """ - def __cinit__(self): - self._lookup_function = _python_class_lookup - - def lookup(self, doc, element): - """lookup(self, doc, element) - - Override this method to implement your own lookup scheme. - """ - return None - -cdef object _python_class_lookup(state, _Document doc, tree.xmlNode* c_node): - cdef PythonElementClassLookup lookup - cdef _ReadOnlyProxy proxy - lookup = state - - proxy = _newReadOnlyProxy(None, c_node) - cls = lookup.lookup(doc, proxy) - _freeReadOnlyProxies(proxy) - - if cls is not None: - _validateNodeClass(c_node, cls) - return cls - return _callLookupFallback(lookup, doc, c_node) - -################################################################################ -# Global setup - -cdef _element_class_lookup_function LOOKUP_ELEMENT_CLASS -cdef object ELEMENT_CLASS_LOOKUP_STATE - -cdef void _setElementClassLookupFunction( - _element_class_lookup_function function, object state): - global LOOKUP_ELEMENT_CLASS, ELEMENT_CLASS_LOOKUP_STATE - if function is NULL: - state = DEFAULT_ELEMENT_CLASS_LOOKUP - function = DEFAULT_ELEMENT_CLASS_LOOKUP._lookup_function - - ELEMENT_CLASS_LOOKUP_STATE = state - LOOKUP_ELEMENT_CLASS = function - -def set_element_class_lookup(ElementClassLookup lookup = None): - """set_element_class_lookup(lookup = None) - - Set the global element class lookup method. - - This defines the main entry point for looking up element implementations. - The standard implementation uses the :class:`ParserBasedElementClassLookup` - to delegate to different lookup schemes for each parser. - - .. warning:: - - This should only be changed by applications, not by library packages. - In most cases, parser specific lookups should be preferred, - which can be configured via - :meth:`~lxml.etree.XMLParser.set_element_class_lookup` - (and the same for HTML parsers). - - Globally replacing the element class lookup by something other than a - :class:`ParserBasedElementClassLookup` will prevent parser specific lookup - schemes from working. Several tools rely on parser specific lookups, - including :mod:`lxml.html` and :mod:`lxml.objectify`. - """ - if lookup is None or lookup._lookup_function is NULL: - _setElementClassLookupFunction(NULL, None) - else: - _setElementClassLookupFunction(lookup._lookup_function, lookup) - -# default setup: parser delegation -cdef ParserBasedElementClassLookup DEFAULT_ELEMENT_CLASS_LOOKUP -DEFAULT_ELEMENT_CLASS_LOOKUP = ParserBasedElementClassLookup() - -set_element_class_lookup(DEFAULT_ELEMENT_CLASS_LOOKUP) diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/cleanup.pxi b/presentation/.venv/lib/python3.12/site-packages/lxml/cleanup.pxi deleted file mode 100644 index 8e266b3..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/cleanup.pxi +++ /dev/null @@ -1,215 +0,0 @@ -# functions for tree cleanup and removing elements from subtrees - -def cleanup_namespaces(tree_or_element, top_nsmap=None, keep_ns_prefixes=None): - """cleanup_namespaces(tree_or_element, top_nsmap=None, keep_ns_prefixes=None) - - Remove all namespace declarations from a subtree that are not used - by any of the elements or attributes in that tree. - - If a 'top_nsmap' is provided, it must be a mapping from prefixes - to namespace URIs. These namespaces will be declared on the top - element of the subtree before running the cleanup, which allows - moving namespace declarations to the top of the tree. - - If a 'keep_ns_prefixes' is provided, it must be a list of prefixes. - These prefixes will not be removed as part of the cleanup. - """ - element = _rootNodeOrRaise(tree_or_element) - c_element = element._c_node - - if top_nsmap: - doc = element._doc - # declare namespaces from nsmap, then apply them to the subtree - _setNodeNamespaces(c_element, doc, None, top_nsmap) - moveNodeToDocument(doc, c_element.doc, c_element) - - keep_ns_prefixes = ( - set([_utf8(prefix) for prefix in keep_ns_prefixes]) - if keep_ns_prefixes else None) - - _removeUnusedNamespaceDeclarations(c_element, keep_ns_prefixes) - - -def strip_attributes(tree_or_element, *attribute_names): - """strip_attributes(tree_or_element, *attribute_names) - - Delete all attributes with the provided attribute names from an - Element (or ElementTree) and its descendants. - - Attribute names can contain wildcards as in `_Element.iter`. - - Example usage:: - - strip_attributes(root_element, - 'simpleattr', - '{http://some/ns}attrname', - '{http://other/ns}*') - """ - cdef _MultiTagMatcher matcher - element = _rootNodeOrRaise(tree_or_element) - if not attribute_names: - return - - matcher = _MultiTagMatcher.__new__(_MultiTagMatcher, attribute_names) - matcher.cacheTags(element._doc) - if matcher.rejectsAllAttributes(): - return - _strip_attributes(element._c_node, matcher) - - -cdef _strip_attributes(xmlNode* c_node, _MultiTagMatcher matcher): - cdef xmlAttr* c_attr - cdef xmlAttr* c_next_attr - tree.BEGIN_FOR_EACH_ELEMENT_FROM(c_node, c_node, 1) - if c_node.type == tree.XML_ELEMENT_NODE: - c_attr = c_node.properties - while c_attr is not NULL: - c_next_attr = c_attr.next - if matcher.matchesAttribute(c_attr): - tree.xmlRemoveProp(c_attr) - c_attr = c_next_attr - tree.END_FOR_EACH_ELEMENT_FROM(c_node) - - -def strip_elements(tree_or_element, *tag_names, bint with_tail=True): - """strip_elements(tree_or_element, *tag_names, with_tail=True) - - Delete all elements with the provided tag names from a tree or - subtree. This will remove the elements and their entire subtree, - including all their attributes, text content and descendants. It - will also remove the tail text of the element unless you - explicitly set the ``with_tail`` keyword argument option to False. - - Tag names can contain wildcards as in `_Element.iter`. - - Note that this will not delete the element (or ElementTree root - element) that you passed even if it matches. It will only treat - its descendants. If you want to include the root element, check - its tag name directly before even calling this function. - - Example usage:: - - strip_elements(some_element, - 'simpletagname', # non-namespaced tag - '{http://some/ns}tagname', # namespaced tag - '{http://some/other/ns}*' # any tag from a namespace - lxml.etree.Comment # comments - ) - """ - cdef _MultiTagMatcher matcher - doc = _documentOrRaise(tree_or_element) - element = _rootNodeOrRaise(tree_or_element) - if not tag_names: - return - - matcher = _MultiTagMatcher.__new__(_MultiTagMatcher, tag_names) - matcher.cacheTags(doc) - if matcher.rejectsAll(): - return - - if isinstance(tree_or_element, _ElementTree): - # include PIs and comments next to the root node - if matcher.matchesType(tree.XML_COMMENT_NODE): - _removeSiblings(element._c_node, tree.XML_COMMENT_NODE, with_tail) - if matcher.matchesType(tree.XML_PI_NODE): - _removeSiblings(element._c_node, tree.XML_PI_NODE, with_tail) - _strip_elements(doc, element._c_node, matcher, with_tail) - -cdef _strip_elements(_Document doc, xmlNode* c_node, _MultiTagMatcher matcher, - bint with_tail): - cdef xmlNode* c_child - cdef xmlNode* c_next - - tree.BEGIN_FOR_EACH_ELEMENT_FROM(c_node, c_node, 1) - if c_node.type == tree.XML_ELEMENT_NODE: - # we run through the children here to prevent any problems - # with the tree iteration which would occur if we unlinked the - # c_node itself - c_child = _findChildForwards(c_node, 0) - while c_child is not NULL: - c_next = _nextElement(c_child) - if matcher.matches(c_child): - if c_child.type == tree.XML_ELEMENT_NODE: - if not with_tail: - tree.xmlUnlinkNode(c_child) - _removeNode(doc, c_child) - else: - if with_tail: - _removeText(c_child.next) - tree.xmlUnlinkNode(c_child) - attemptDeallocation(c_child) - c_child = c_next - tree.END_FOR_EACH_ELEMENT_FROM(c_node) - - -def strip_tags(tree_or_element, *tag_names): - """strip_tags(tree_or_element, *tag_names) - - Delete all elements with the provided tag names from a tree or - subtree. This will remove the elements and their attributes, but - *not* their text/tail content or descendants. Instead, it will - merge the text content and children of the element into its - parent. - - Tag names can contain wildcards as in `_Element.iter`. - - Note that this will not delete the element (or ElementTree root - element) that you passed even if it matches. It will only treat - its descendants. - - Example usage:: - - strip_tags(some_element, - 'simpletagname', # non-namespaced tag - '{http://some/ns}tagname', # namespaced tag - '{http://some/other/ns}*' # any tag from a namespace - Comment # comments (including their text!) - ) - """ - cdef _MultiTagMatcher matcher - doc = _documentOrRaise(tree_or_element) - element = _rootNodeOrRaise(tree_or_element) - if not tag_names: - return - - matcher = _MultiTagMatcher.__new__(_MultiTagMatcher, tag_names) - matcher.cacheTags(doc) - if matcher.rejectsAll(): - return - - if isinstance(tree_or_element, _ElementTree): - # include PIs and comments next to the root node - if matcher.matchesType(tree.XML_COMMENT_NODE): - _removeSiblings(element._c_node, tree.XML_COMMENT_NODE, 0) - if matcher.matchesType(tree.XML_PI_NODE): - _removeSiblings(element._c_node, tree.XML_PI_NODE, 0) - _strip_tags(doc, element._c_node, matcher) - -cdef _strip_tags(_Document doc, xmlNode* c_node, _MultiTagMatcher matcher): - cdef xmlNode* c_child - cdef xmlNode* c_next - - tree.BEGIN_FOR_EACH_ELEMENT_FROM(c_node, c_node, 1) - if c_node.type == tree.XML_ELEMENT_NODE: - # we run through the children here to prevent any problems - # with the tree iteration which would occur if we unlinked the - # c_node itself - c_child = _findChildForwards(c_node, 0) - while c_child is not NULL: - if not matcher.matches(c_child): - c_child = _nextElement(c_child) - continue - if c_child.type == tree.XML_ELEMENT_NODE: - c_next = _findChildForwards(c_child, 0) or _nextElement(c_child) - _replaceNodeByChildren(doc, c_child) - if not attemptDeallocation(c_child): - if c_child.nsDef is not NULL: - # make namespaces absolute - moveNodeToDocument(doc, doc._c_doc, c_child) - c_child = c_next - else: - c_next = _nextElement(c_child) - tree.xmlUnlinkNode(c_child) - attemptDeallocation(c_child) - c_child = c_next - tree.END_FOR_EACH_ELEMENT_FROM(c_node) diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/cssselect.py b/presentation/.venv/lib/python3.12/site-packages/lxml/cssselect.py deleted file mode 100644 index 54cd75a..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/cssselect.py +++ /dev/null @@ -1,101 +0,0 @@ -"""CSS Selectors based on XPath. - -This module supports selecting XML/HTML tags based on CSS selectors. -See the `CSSSelector` class for details. - -This is a thin wrapper around cssselect 0.7 or later. -""" - - -from . import etree -try: - import cssselect as external_cssselect -except ImportError: - raise ImportError( - 'cssselect does not seem to be installed. ' - 'See https://pypi.org/project/cssselect/') - - -SelectorSyntaxError = external_cssselect.SelectorSyntaxError -ExpressionError = external_cssselect.ExpressionError -SelectorError = external_cssselect.SelectorError - - -__all__ = ['SelectorSyntaxError', 'ExpressionError', 'SelectorError', - 'CSSSelector'] - - -class LxmlTranslator(external_cssselect.GenericTranslator): - """ - A custom CSS selector to XPath translator with lxml-specific extensions. - """ - def xpath_contains_function(self, xpath, function): - # Defined there, removed in later drafts: - # http://www.w3.org/TR/2001/CR-css3-selectors-20011113/#content-selectors - if function.argument_types() not in (['STRING'], ['IDENT']): - raise ExpressionError( - "Expected a single string or ident for :contains(), got %r" - % function.arguments) - value = function.arguments[0].value - return xpath.add_condition( - 'contains(__lxml_internal_css:lower-case(string(.)), %s)' - % self.xpath_literal(value.lower())) - - -class LxmlHTMLTranslator(LxmlTranslator, external_cssselect.HTMLTranslator): - """ - lxml extensions + HTML support. - """ - - -def _make_lower_case(context, s): - return s.lower() - -ns = etree.FunctionNamespace('http://codespeak.net/lxml/css/') -ns.prefix = '__lxml_internal_css' -ns['lower-case'] = _make_lower_case - - -class CSSSelector(etree.XPath): - """A CSS selector. - - Usage:: - - >>> from lxml import etree, cssselect - >>> select = cssselect.CSSSelector("a tag > child") - - >>> root = etree.XML("TEXT") - >>> [ el.tag for el in select(root) ] - ['child'] - - To use CSS namespaces, you need to pass a prefix-to-namespace - mapping as ``namespaces`` keyword argument:: - - >>> rdfns = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' - >>> select_ns = cssselect.CSSSelector('root > rdf|Description', - ... namespaces={'rdf': rdfns}) - - >>> rdf = etree.XML(( - ... '' - ... 'blah' - ... '') % rdfns) - >>> [(el.tag, el.text) for el in select_ns(rdf)] - [('{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description', 'blah')] - - """ - def __init__(self, css, namespaces=None, translator='xml'): - if translator == 'xml': - translator = LxmlTranslator() - elif translator == 'html': - translator = LxmlHTMLTranslator() - elif translator == 'xhtml': - translator = LxmlHTMLTranslator(xhtml=True) - path = translator.css_to_xpath(css) - super().__init__(path, namespaces=namespaces) - self.css = css - - def __repr__(self): - return '<%s %x for %r>' % ( - self.__class__.__name__, - abs(id(self)), - self.css) diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/debug.pxi b/presentation/.venv/lib/python3.12/site-packages/lxml/debug.pxi deleted file mode 100644 index d728e84..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/debug.pxi +++ /dev/null @@ -1,36 +0,0 @@ -@cython.final -@cython.internal -cdef class _MemDebug: - """Debugging support for the memory allocation in libxml2. - """ - def bytes_used(self): - """bytes_used(self) - - Returns the total amount of memory (in bytes) currently used by libxml2. - Note that libxml2 constrains this value to a C int, which limits - the accuracy on 64 bit systems. - """ - return tree.xmlMemUsed() - - def blocks_used(self): - """blocks_used(self) - - Returns the total number of memory blocks currently allocated by libxml2. - Note that libxml2 constrains this value to a C int, which limits - the accuracy on 64 bit systems. - """ - return tree.xmlMemBlocks() - - def dict_size(self): - """dict_size(self) - - Returns the current size of the global name dictionary used by libxml2 - for the current thread. Each thread has its own dictionary. - """ - c_dict = __GLOBAL_PARSER_CONTEXT._getThreadDict(NULL) - if c_dict is NULL: - raise MemoryError() - return tree.xmlDictSize(c_dict) - - -memory_debugger = _MemDebug() diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/docloader.pxi b/presentation/.venv/lib/python3.12/site-packages/lxml/docloader.pxi deleted file mode 100644 index 07e0cd7..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/docloader.pxi +++ /dev/null @@ -1,179 +0,0 @@ -# Custom resolver API - -ctypedef enum _InputDocumentDataType: - PARSER_DATA_INVALID - PARSER_DATA_EMPTY - PARSER_DATA_STRING - PARSER_DATA_FILENAME - PARSER_DATA_FILE - -@cython.final -@cython.internal -cdef class _InputDocument: - cdef _InputDocumentDataType _type - cdef bytes _data_bytes - cdef object _filename - cdef object _file - cdef bint _close_file - - def __cinit__(self): - self._type = PARSER_DATA_INVALID - - -cdef class Resolver: - "This is the base class of all resolvers." - def resolve(self, system_url, public_id, context): - """resolve(self, system_url, public_id, context) - - Override this method to resolve an external source by - ``system_url`` and ``public_id``. The third argument is an - opaque context object. - - Return the result of one of the ``resolve_*()`` methods. - """ - return None - - def resolve_empty(self, context): - """resolve_empty(self, context) - - Return an empty input document. - - Pass context as parameter. - """ - cdef _InputDocument doc_ref - doc_ref = _InputDocument() - doc_ref._type = PARSER_DATA_EMPTY - return doc_ref - - def resolve_string(self, string, context, *, base_url=None): - """resolve_string(self, string, context, base_url=None) - - Return a parsable string as input document. - - Pass data string and context as parameters. You can pass the - source URL or filename through the ``base_url`` keyword - argument. - """ - cdef _InputDocument doc_ref - if isinstance(string, unicode): - string = (string).encode('utf8') - elif not isinstance(string, bytes): - raise TypeError, "argument must be a byte string or unicode string" - doc_ref = _InputDocument() - doc_ref._type = PARSER_DATA_STRING - doc_ref._data_bytes = string - if base_url is not None: - doc_ref._filename = _encodeFilename(base_url) - return doc_ref - - def resolve_filename(self, filename, context): - """resolve_filename(self, filename, context) - - Return the name of a parsable file as input document. - - Pass filename and context as parameters. You can also pass a - URL with an HTTP, FTP or file target. - """ - cdef _InputDocument doc_ref - doc_ref = _InputDocument() - doc_ref._type = PARSER_DATA_FILENAME - doc_ref._filename = _encodeFilename(filename) - return doc_ref - - def resolve_file(self, f, context, *, base_url=None, bint close=True): - """resolve_file(self, f, context, base_url=None, close=True) - - Return an open file-like object as input document. - - Pass open file and context as parameters. You can pass the - base URL or filename of the file through the ``base_url`` - keyword argument. If the ``close`` flag is True (the - default), the file will be closed after reading. - - Note that using ``.resolve_filename()`` is more efficient, - especially in threaded environments. - """ - cdef _InputDocument doc_ref - try: - f.read - except AttributeError: - raise TypeError, "Argument is not a file-like object" - doc_ref = _InputDocument() - doc_ref._type = PARSER_DATA_FILE - if base_url is not None: - doc_ref._filename = _encodeFilename(base_url) - else: - doc_ref._filename = _getFilenameForFile(f) - doc_ref._close_file = close - doc_ref._file = f - return doc_ref - -@cython.final -@cython.internal -cdef class _ResolverRegistry: - cdef object _resolvers - cdef Resolver _default_resolver - def __cinit__(self, Resolver default_resolver=None): - self._resolvers = set() - self._default_resolver = default_resolver - - def add(self, Resolver resolver not None): - """add(self, resolver) - - Register a resolver. - - For each requested entity, the 'resolve' method of the resolver will - be called and the result will be passed to the parser. If this method - returns None, the request will be delegated to other resolvers or the - default resolver. The resolvers will be tested in an arbitrary order - until the first match is found. - """ - self._resolvers.add(resolver) - - def remove(self, resolver): - "remove(self, resolver)" - self._resolvers.discard(resolver) - - cdef _ResolverRegistry _copy(self): - cdef _ResolverRegistry registry - registry = _ResolverRegistry(self._default_resolver) - registry._resolvers = self._resolvers.copy() - return registry - - def copy(self): - "copy(self)" - return self._copy() - - def resolve(self, system_url, public_id, context): - "resolve(self, system_url, public_id, context)" - for resolver in self._resolvers: - result = resolver.resolve(system_url, public_id, context) - if result is not None: - return result - if self._default_resolver is None: - return None - return self._default_resolver.resolve(system_url, public_id, context) - - def __repr__(self): - return repr(self._resolvers) - - -@cython.internal -cdef class _ResolverContext(_ExceptionContext): - cdef _ResolverRegistry _resolvers - cdef _TempStore _storage - - @cython.final - cdef int clear(self) except -1: - _ExceptionContext.clear(self) - self._storage.clear() - return 0 - - -cdef _initResolverContext(_ResolverContext context, - _ResolverRegistry resolvers): - if resolvers is None: - context._resolvers = _ResolverRegistry() - else: - context._resolvers = resolvers - context._storage = _TempStore() diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/doctestcompare.py b/presentation/.venv/lib/python3.12/site-packages/lxml/doctestcompare.py deleted file mode 100644 index 8099771..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/doctestcompare.py +++ /dev/null @@ -1,488 +0,0 @@ -""" -lxml-based doctest output comparison. - -Note: normally, you should just import the `lxml.usedoctest` and -`lxml.html.usedoctest` modules from within a doctest, instead of this -one:: - - >>> import lxml.usedoctest # for XML output - - >>> import lxml.html.usedoctest # for HTML output - -To use this module directly, you must call ``lxmldoctest.install()``, -which will cause doctest to use this in all subsequent calls. - -This changes the way output is checked and comparisons are made for -XML or HTML-like content. - -XML or HTML content is noticed because the example starts with ``<`` -(it's HTML if it starts with ```` or include an ``any`` -attribute in the tag. An ``any`` tag matches any tag, while the -attribute matches any and all attributes. - -When a match fails, the reformatted example and gotten text is -displayed (indented), and a rough diff-like output is given. Anything -marked with ``+`` is in the output but wasn't supposed to be, and -similarly ``-`` means its in the example but wasn't in the output. - -You can disable parsing on one line with ``# doctest:+NOPARSE_MARKUP`` -""" - -from lxml import etree -import sys -import re -import doctest -try: - from html import escape as html_escape -except ImportError: - from cgi import escape as html_escape - -__all__ = ['PARSE_HTML', 'PARSE_XML', 'NOPARSE_MARKUP', 'LXMLOutputChecker', - 'LHTMLOutputChecker', 'install', 'temp_install'] - -PARSE_HTML = doctest.register_optionflag('PARSE_HTML') -PARSE_XML = doctest.register_optionflag('PARSE_XML') -NOPARSE_MARKUP = doctest.register_optionflag('NOPARSE_MARKUP') - -OutputChecker = doctest.OutputChecker - -def strip(v): - if v is None: - return None - else: - return v.strip() - -def norm_whitespace(v): - return _norm_whitespace_re.sub(' ', v) - -_html_parser = etree.HTMLParser(recover=False, remove_blank_text=True) - -def html_fromstring(html): - return etree.fromstring(html, _html_parser) - -# We use this to distinguish repr()s from elements: -_repr_re = re.compile(r'^<[^>]+ (at|object) ') -_norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+') - -class LXMLOutputChecker(OutputChecker): - - empty_tags = ( - 'param', 'img', 'area', 'br', 'basefont', 'input', - 'base', 'meta', 'link', 'col') - - def get_default_parser(self): - return etree.XML - - def check_output(self, want, got, optionflags): - alt_self = getattr(self, '_temp_override_self', None) - if alt_self is not None: - super_method = self._temp_call_super_check_output - self = alt_self - else: - super_method = OutputChecker.check_output - parser = self.get_parser(want, got, optionflags) - if not parser: - return super_method( - self, want, got, optionflags) - try: - want_doc = parser(want) - except etree.XMLSyntaxError: - return False - try: - got_doc = parser(got) - except etree.XMLSyntaxError: - return False - return self.compare_docs(want_doc, got_doc) - - def get_parser(self, want, got, optionflags): - parser = None - if NOPARSE_MARKUP & optionflags: - return None - if PARSE_HTML & optionflags: - parser = html_fromstring - elif PARSE_XML & optionflags: - parser = etree.XML - elif (want.strip().lower().startswith('' % el.tag - return '<%s %s>' % (el.tag, ' '.join(attrs)) - - def format_end_tag(self, el): - if isinstance(el, etree.CommentBase): - # FIXME: probably PIs should be handled specially too? - return '-->' - return '' % el.tag - - def collect_diff(self, want, got, html, indent): - parts = [] - if not len(want) and not len(got): - parts.append(' '*indent) - parts.append(self.collect_diff_tag(want, got)) - if not self.html_empty_tag(got, html): - parts.append(self.collect_diff_text(want.text, got.text)) - parts.append(self.collect_diff_end_tag(want, got)) - parts.append(self.collect_diff_text(want.tail, got.tail)) - parts.append('\n') - return ''.join(parts) - parts.append(' '*indent) - parts.append(self.collect_diff_tag(want, got)) - parts.append('\n') - if strip(want.text) or strip(got.text): - parts.append(' '*indent) - parts.append(self.collect_diff_text(want.text, got.text)) - parts.append('\n') - want_children = list(want) - got_children = list(got) - while want_children or got_children: - if not want_children: - parts.append(self.format_doc(got_children.pop(0), html, indent+2, '+')) - continue - if not got_children: - parts.append(self.format_doc(want_children.pop(0), html, indent+2, '-')) - continue - parts.append(self.collect_diff( - want_children.pop(0), got_children.pop(0), html, indent+2)) - parts.append(' '*indent) - parts.append(self.collect_diff_end_tag(want, got)) - parts.append('\n') - if strip(want.tail) or strip(got.tail): - parts.append(' '*indent) - parts.append(self.collect_diff_text(want.tail, got.tail)) - parts.append('\n') - return ''.join(parts) - - def collect_diff_tag(self, want, got): - if not self.tag_compare(want.tag, got.tag): - tag = '%s (got: %s)' % (want.tag, got.tag) - else: - tag = got.tag - attrs = [] - any = want.tag == 'any' or 'any' in want.attrib - for name, value in sorted(got.attrib.items()): - if name not in want.attrib and not any: - attrs.append('+%s="%s"' % (name, self.format_text(value, False))) - else: - if name in want.attrib: - text = self.collect_diff_text(want.attrib[name], value, False) - else: - text = self.format_text(value, False) - attrs.append('%s="%s"' % (name, text)) - if not any: - for name, value in sorted(want.attrib.items()): - if name in got.attrib: - continue - attrs.append('-%s="%s"' % (name, self.format_text(value, False))) - if attrs: - tag = '<%s %s>' % (tag, ' '.join(attrs)) - else: - tag = '<%s>' % tag - return tag - - def collect_diff_end_tag(self, want, got): - if want.tag != got.tag: - tag = '%s (got: %s)' % (want.tag, got.tag) - else: - tag = got.tag - return '' % tag - - def collect_diff_text(self, want, got, strip=True): - if self.text_compare(want, got, strip): - if not got: - return '' - return self.format_text(got, strip) - text = '%s (got: %s)' % (want, got) - return self.format_text(text, strip) - -class LHTMLOutputChecker(LXMLOutputChecker): - def get_default_parser(self): - return html_fromstring - -def install(html=False): - """ - Install doctestcompare for all future doctests. - - If html is true, then by default the HTML parser will be used; - otherwise the XML parser is used. - """ - if html: - doctest.OutputChecker = LHTMLOutputChecker - else: - doctest.OutputChecker = LXMLOutputChecker - -def temp_install(html=False, del_module=None): - """ - Use this *inside* a doctest to enable this checker for this - doctest only. - - If html is true, then by default the HTML parser will be used; - otherwise the XML parser is used. - """ - if html: - Checker = LHTMLOutputChecker - else: - Checker = LXMLOutputChecker - frame = _find_doctest_frame() - dt_self = frame.f_locals['self'] - checker = Checker() - old_checker = dt_self._checker - dt_self._checker = checker - # The unfortunate thing is that there is a local variable 'check' - # in the function that runs the doctests, that is a bound method - # into the output checker. We have to update that. We can't - # modify the frame, so we have to modify the object in place. The - # only way to do this is to actually change the func_code - # attribute of the method. We change it, and then wait for - # __record_outcome to be run, which signals the end of the __run - # method, at which point we restore the previous check_output - # implementation. - check_func = frame.f_locals['check'].__func__ - checker_check_func = checker.check_output.__func__ - # Because we can't patch up func_globals, this is the only global - # in check_output that we care about: - doctest.etree = etree - _RestoreChecker(dt_self, old_checker, checker, - check_func, checker_check_func, - del_module) - -class _RestoreChecker: - def __init__(self, dt_self, old_checker, new_checker, check_func, clone_func, - del_module): - self.dt_self = dt_self - self.checker = old_checker - self.checker._temp_call_super_check_output = self.call_super - self.checker._temp_override_self = new_checker - self.check_func = check_func - self.clone_func = clone_func - self.del_module = del_module - self.install_clone() - self.install_dt_self() - def install_clone(self): - self.func_code = self.check_func.__code__ - self.func_globals = self.check_func.__globals__ - self.check_func.__code__ = self.clone_func.__code__ - def uninstall_clone(self): - self.check_func.__code__ = self.func_code - def install_dt_self(self): - self.prev_func = self.dt_self._DocTestRunner__record_outcome - self.dt_self._DocTestRunner__record_outcome = self - def uninstall_dt_self(self): - self.dt_self._DocTestRunner__record_outcome = self.prev_func - def uninstall_module(self): - if self.del_module: - import sys - del sys.modules[self.del_module] - if '.' in self.del_module: - package, module = self.del_module.rsplit('.', 1) - package_mod = sys.modules[package] - delattr(package_mod, module) - def __call__(self, *args, **kw): - self.uninstall_clone() - self.uninstall_dt_self() - del self.checker._temp_override_self - del self.checker._temp_call_super_check_output - result = self.prev_func(*args, **kw) - self.uninstall_module() - return result - def call_super(self, *args, **kw): - self.uninstall_clone() - try: - return self.check_func(*args, **kw) - finally: - self.install_clone() - -def _find_doctest_frame(): - import sys - frame = sys._getframe(1) - while frame: - l = frame.f_locals - if 'BOOM' in l: - # Sign of doctest - return frame - frame = frame.f_back - raise LookupError( - "Could not find doctest (only use this function *inside* a doctest)") - -__test__ = { - 'basic': ''' - >>> temp_install() - >>> print """stuff""" - ... - >>> print """""" - - - - >>> print """blahblahblah""" # doctest: +NOPARSE_MARKUP, +ELLIPSIS - ...foo /> - '''} - -if __name__ == '__main__': - import doctest - doctest.testmod() - - diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/dtd.pxi b/presentation/.venv/lib/python3.12/site-packages/lxml/dtd.pxi deleted file mode 100644 index ee1b3d4..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/dtd.pxi +++ /dev/null @@ -1,479 +0,0 @@ -# support for DTD validation -from lxml.includes cimport dtdvalid - -cdef class DTDError(LxmlError): - """Base class for DTD errors. - """ - -cdef class DTDParseError(DTDError): - """Error while parsing a DTD. - """ - -cdef class DTDValidateError(DTDError): - """Error while validating an XML document with a DTD. - """ - - -cdef inline int _assertValidDTDNode(node, void *c_node) except -1: - assert c_node is not NULL, "invalid DTD proxy at %s" % id(node) - - -@cython.final -@cython.internal -@cython.freelist(8) -cdef class _DTDElementContentDecl: - cdef DTD _dtd - cdef tree.xmlElementContent* _c_node - - def __repr__(self): - return "<%s.%s object name=%r type=%r occur=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.name, self.type, self.occur, id(self)) - - @property - def name(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.name) - - @property - def type(self): - _assertValidDTDNode(self, self._c_node) - cdef int type = self._c_node.type - if type == tree.XML_ELEMENT_CONTENT_PCDATA: - return "pcdata" - elif type == tree.XML_ELEMENT_CONTENT_ELEMENT: - return "element" - elif type == tree.XML_ELEMENT_CONTENT_SEQ: - return "seq" - elif type == tree.XML_ELEMENT_CONTENT_OR: - return "or" - else: - return None - - @property - def occur(self): - _assertValidDTDNode(self, self._c_node) - cdef int occur = self._c_node.ocur - if occur == tree.XML_ELEMENT_CONTENT_ONCE: - return "once" - elif occur == tree.XML_ELEMENT_CONTENT_OPT: - return "opt" - elif occur == tree.XML_ELEMENT_CONTENT_MULT: - return "mult" - elif occur == tree.XML_ELEMENT_CONTENT_PLUS: - return "plus" - else: - return None - - @property - def left(self): - _assertValidDTDNode(self, self._c_node) - c1 = self._c_node.c1 - if c1: - node = <_DTDElementContentDecl>_DTDElementContentDecl.__new__(_DTDElementContentDecl) - node._dtd = self._dtd - node._c_node = c1 - return node - else: - return None - - @property - def right(self): - _assertValidDTDNode(self, self._c_node) - c2 = self._c_node.c2 - if c2: - node = <_DTDElementContentDecl>_DTDElementContentDecl.__new__(_DTDElementContentDecl) - node._dtd = self._dtd - node._c_node = c2 - return node - else: - return None - - -@cython.final -@cython.internal -@cython.freelist(8) -cdef class _DTDAttributeDecl: - cdef DTD _dtd - cdef tree.xmlAttribute* _c_node - - def __repr__(self): - return "<%s.%s object name=%r elemname=%r prefix=%r type=%r default=%r default_value=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.name, self.elemname, self.prefix, self.type, self.default, self.default_value, id(self)) - - @property - def name(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.name) - - @property - def elemname(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.elem) - - @property - def prefix(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.prefix) - - @property - def type(self): - _assertValidDTDNode(self, self._c_node) - cdef int type = self._c_node.atype - if type == tree.XML_ATTRIBUTE_CDATA: - return "cdata" - elif type == tree.XML_ATTRIBUTE_ID: - return "id" - elif type == tree.XML_ATTRIBUTE_IDREF: - return "idref" - elif type == tree.XML_ATTRIBUTE_IDREFS: - return "idrefs" - elif type == tree.XML_ATTRIBUTE_ENTITY: - return "entity" - elif type == tree.XML_ATTRIBUTE_ENTITIES: - return "entities" - elif type == tree.XML_ATTRIBUTE_NMTOKEN: - return "nmtoken" - elif type == tree.XML_ATTRIBUTE_NMTOKENS: - return "nmtokens" - elif type == tree.XML_ATTRIBUTE_ENUMERATION: - return "enumeration" - elif type == tree.XML_ATTRIBUTE_NOTATION: - return "notation" - else: - return None - - @property - def default(self): - _assertValidDTDNode(self, self._c_node) - cdef int default = self._c_node.def_ - if default == tree.XML_ATTRIBUTE_NONE: - return "none" - elif default == tree.XML_ATTRIBUTE_REQUIRED: - return "required" - elif default == tree.XML_ATTRIBUTE_IMPLIED: - return "implied" - elif default == tree.XML_ATTRIBUTE_FIXED: - return "fixed" - else: - return None - - @property - def default_value(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.defaultValue) - - def itervalues(self): - _assertValidDTDNode(self, self._c_node) - cdef tree.xmlEnumeration *c_node = self._c_node.tree - while c_node is not NULL: - yield funicode(c_node.name) - c_node = c_node.next - - def values(self): - return list(self.itervalues()) - - -@cython.final -@cython.internal -@cython.freelist(8) -cdef class _DTDElementDecl: - cdef DTD _dtd - cdef tree.xmlElement* _c_node - - def __repr__(self): - return "<%s.%s object name=%r prefix=%r type=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.name, self.prefix, self.type, id(self)) - - @property - def name(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.name) - - @property - def prefix(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.prefix) - - @property - def type(self): - _assertValidDTDNode(self, self._c_node) - cdef int type = self._c_node.etype - if type == tree.XML_ELEMENT_TYPE_UNDEFINED: - return "undefined" - elif type == tree.XML_ELEMENT_TYPE_EMPTY: - return "empty" - elif type == tree.XML_ELEMENT_TYPE_ANY: - return "any" - elif type == tree.XML_ELEMENT_TYPE_MIXED: - return "mixed" - elif type == tree.XML_ELEMENT_TYPE_ELEMENT: - return "element" - else: - return None - - @property - def content(self): - _assertValidDTDNode(self, self._c_node) - cdef tree.xmlElementContent *content = self._c_node.content - if content: - node = <_DTDElementContentDecl>_DTDElementContentDecl.__new__(_DTDElementContentDecl) - node._dtd = self._dtd - node._c_node = content - return node - else: - return None - - def iterattributes(self): - _assertValidDTDNode(self, self._c_node) - cdef tree.xmlAttribute *c_node = self._c_node.attributes - while c_node: - node = <_DTDAttributeDecl>_DTDAttributeDecl.__new__(_DTDAttributeDecl) - node._dtd = self._dtd - node._c_node = c_node - yield node - c_node = c_node.nexth - - def attributes(self): - return list(self.iterattributes()) - - -@cython.final -@cython.internal -@cython.freelist(8) -cdef class _DTDEntityDecl: - cdef DTD _dtd - cdef tree.xmlEntity* _c_node - def __repr__(self): - return "<%s.%s object name=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.name, id(self)) - - @property - def name(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.name) - - @property - def orig(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.orig) - - @property - def content(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.content) - - @property - def system_url(self): - _assertValidDTDNode(self, self._c_node) - return funicodeOrNone(self._c_node.SystemID) - - -################################################################################ -# DTD - -cdef class DTD(_Validator): - """DTD(self, file=None, external_id=None) - A DTD validator. - - Can load from filesystem directly given a filename or file-like object. - Alternatively, pass the keyword parameter ``external_id`` to load from a - catalog. - """ - cdef tree.xmlDtd* _c_dtd - def __init__(self, file=None, *, external_id=None): - _Validator.__init__(self) - if file is not None: - file = _getFSPathOrObject(file) - if _isString(file): - file = _encodeFilename(file) - with self._error_log: - orig_loader = _register_document_loader() - self._c_dtd = xmlparser.xmlParseDTD(NULL, _xcstr(file)) - _reset_document_loader(orig_loader) - elif hasattr(file, 'read'): - orig_loader = _register_document_loader() - self._c_dtd = _parseDtdFromFilelike(file) - _reset_document_loader(orig_loader) - else: - raise DTDParseError, "file must be a filename, file-like or path-like object" - elif external_id is not None: - external_id_utf = _utf8(external_id) - with self._error_log: - orig_loader = _register_document_loader() - self._c_dtd = xmlparser.xmlParseDTD(external_id_utf, NULL) - _reset_document_loader(orig_loader) - else: - raise DTDParseError, "either filename or external ID required" - - if self._c_dtd is NULL: - raise DTDParseError( - self._error_log._buildExceptionMessage("error parsing DTD"), - self._error_log) - - @property - def name(self): - if self._c_dtd is NULL: - return None - return funicodeOrNone(self._c_dtd.name) - - @property - def external_id(self): - if self._c_dtd is NULL: - return None - return funicodeOrNone(self._c_dtd.ExternalID) - - @property - def system_url(self): - if self._c_dtd is NULL: - return None - return funicodeOrNone(self._c_dtd.SystemID) - - def iterelements(self): - cdef tree.xmlNode *c_node = self._c_dtd.children if self._c_dtd is not NULL else NULL - while c_node is not NULL: - if c_node.type == tree.XML_ELEMENT_DECL: - node = _DTDElementDecl() - node._dtd = self - node._c_node = c_node - yield node - c_node = c_node.next - - def elements(self): - return list(self.iterelements()) - - def iterentities(self): - cdef tree.xmlNode *c_node = self._c_dtd.children if self._c_dtd is not NULL else NULL - while c_node is not NULL: - if c_node.type == tree.XML_ENTITY_DECL: - node = _DTDEntityDecl() - node._dtd = self - node._c_node = c_node - yield node - c_node = c_node.next - - def entities(self): - return list(self.iterentities()) - - def __dealloc__(self): - tree.xmlFreeDtd(self._c_dtd) - - def __call__(self, etree): - """__call__(self, etree) - - Validate doc using the DTD. - - Returns true if the document is valid, false if not. - """ - cdef _Document doc - cdef _Element root_node - cdef xmlDoc* c_doc - cdef dtdvalid.xmlValidCtxt* valid_ctxt - cdef int ret = -1 - - assert self._c_dtd is not NULL, "DTD not initialised" - doc = _documentOrRaise(etree) - root_node = _rootNodeOrRaise(etree) - - valid_ctxt = dtdvalid.xmlNewValidCtxt() - if valid_ctxt is NULL: - raise DTDError("Failed to create validation context") - - # work around error reporting bug in libxml2 <= 2.9.1 (and later?) - # https://bugzilla.gnome.org/show_bug.cgi?id=724903 - valid_ctxt.error = _nullGenericErrorFunc - valid_ctxt.userData = NULL - - try: - with self._error_log: - c_doc = _fakeRootDoc(doc._c_doc, root_node._c_node) - ret = dtdvalid.xmlValidateDtd(valid_ctxt, c_doc, self._c_dtd) - _destroyFakeDoc(doc._c_doc, c_doc) - finally: - dtdvalid.xmlFreeValidCtxt(valid_ctxt) - - if ret == -1: - raise DTDValidateError("Internal error in DTD validation", - self._error_log) - return ret == 1 - - -cdef tree.xmlDtd* _parseDtdFromFilelike(file) except NULL: - cdef _ExceptionContext exc_context - cdef _FileReaderContext dtd_parser - cdef _ErrorLog error_log - cdef tree.xmlDtd* c_dtd = NULL - exc_context = _ExceptionContext() - dtd_parser = _FileReaderContext(file, exc_context, None) - error_log = _ErrorLog() - - with error_log: - c_dtd = dtd_parser._readDtd() - - exc_context._raise_if_stored() - if c_dtd is NULL: - raise DTDParseError("error parsing DTD", error_log) - return c_dtd - -cdef DTD _dtdFactory(tree.xmlDtd* c_dtd): - # do not run through DTD.__init__()! - cdef DTD dtd - if c_dtd is NULL: - return None - dtd = DTD.__new__(DTD) - dtd._c_dtd = _copyDtd(c_dtd) - _Validator.__init__(dtd) - return dtd - - -cdef tree.xmlDtd* _copyDtd(tree.xmlDtd* c_orig_dtd) except NULL: - """ - Copy a DTD. libxml2 (currently) fails to set up the element->attributes - links when copying DTDs, so we have to rebuild them here. - """ - c_dtd = tree.xmlCopyDtd(c_orig_dtd) - if not c_dtd: - raise MemoryError - cdef tree.xmlNode* c_node = c_dtd.children - while c_node: - if c_node.type == tree.XML_ATTRIBUTE_DECL: - _linkDtdAttribute(c_dtd, c_node) - c_node = c_node.next - return c_dtd - - -cdef void _linkDtdAttribute(tree.xmlDtd* c_dtd, tree.xmlAttribute* c_attr) noexcept: - """ - Create the link to the DTD attribute declaration from the corresponding - element declaration. - """ - c_elem = dtdvalid.xmlGetDtdElementDesc(c_dtd, c_attr.elem) - if not c_elem: - # no such element? something is wrong with the DTD ... - return - c_pos = c_elem.attributes - if not c_pos: - c_elem.attributes = c_attr - c_attr.nexth = NULL - return - # libxml2 keeps namespace declarations first, and we need to make - # sure we don't re-insert attributes that are already there - if _isDtdNsDecl(c_attr): - if not _isDtdNsDecl(c_pos): - c_elem.attributes = c_attr - c_attr.nexth = c_pos - return - while c_pos != c_attr and c_pos.nexth and _isDtdNsDecl(c_pos.nexth): - c_pos = c_pos.nexth - else: - # append at end - while c_pos != c_attr and c_pos.nexth: - c_pos = c_pos.nexth - if c_pos == c_attr: - return - c_attr.nexth = c_pos.nexth - c_pos.nexth = c_attr - - -cdef bint _isDtdNsDecl(tree.xmlAttribute* c_attr) noexcept: - if cstring_h.strcmp(c_attr.name, "xmlns") == 0: - return True - if (c_attr.prefix is not NULL and - cstring_h.strcmp(c_attr.prefix, "xmlns") == 0): - return True - return False diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/etree.cpython-312-x86_64-linux-gnu.so b/presentation/.venv/lib/python3.12/site-packages/lxml/etree.cpython-312-x86_64-linux-gnu.so deleted file mode 100755 index 71293b0..0000000 Binary files a/presentation/.venv/lib/python3.12/site-packages/lxml/etree.cpython-312-x86_64-linux-gnu.so and /dev/null differ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/etree.h b/presentation/.venv/lib/python3.12/site-packages/lxml/etree.h deleted file mode 100644 index 10a6039..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/etree.h +++ /dev/null @@ -1,244 +0,0 @@ -/* Generated by Cython 3.2.4 */ - -#ifndef __PYX_HAVE__lxml__etree -#define __PYX_HAVE__lxml__etree - -#include "Python.h" -struct LxmlDocument; -struct LxmlElement; -struct LxmlElementTree; -struct LxmlElementTagMatcher; -struct LxmlElementIterator; -struct LxmlElementBase; -struct LxmlElementClassLookup; -struct LxmlFallbackElementClassLookup; - -/* "lxml/etree.pyx":456 - * - * # type of a function that steps from node to node - * ctypedef public xmlNode* (*_node_to_node_function)(xmlNode*) # <<<<<<<<<<<<<< - * - * -*/ -typedef xmlNode *(*_node_to_node_function)(xmlNode *); - -/* "lxml/etree.pyx":470 - * # Public Python API - * - * @cython.final # <<<<<<<<<<<<<< - * @cython.freelist(8) - * cdef public class _Document [ type LxmlDocumentType, object LxmlDocument ]: -*/ -struct LxmlDocument { - PyObject_HEAD - struct __pyx_vtabstruct_4lxml_5etree__Document *__pyx_vtab; - int _ns_counter; - PyObject *_prefix_tail; - xmlDoc *_c_doc; - struct __pyx_obj_4lxml_5etree__BaseParser *_parser; -}; - -/* "lxml/etree.pyx":822 - * - * - * @cython.no_gc_clear # <<<<<<<<<<<<<< - * cdef public class _Element [ type LxmlElementType, object LxmlElement ]: - * """Element class. -*/ -struct LxmlElement { - PyObject_HEAD - struct LxmlDocument *_doc; - xmlNode *_c_node; - PyObject *_tag; -}; - -/* "lxml/etree.pyx":1996 - * - * - * cdef public class _ElementTree [ type LxmlElementTreeType, # <<<<<<<<<<<<<< - * object LxmlElementTree ]: - * cdef _Document _doc -*/ -struct LxmlElementTree { - PyObject_HEAD - struct __pyx_vtabstruct_4lxml_5etree__ElementTree *__pyx_vtab; - struct LxmlDocument *_doc; - struct LxmlElement *_context_node; -}; - -/* "lxml/etree.pyx":2770 - * - * - * cdef public class _ElementTagMatcher [ object LxmlElementTagMatcher, # <<<<<<<<<<<<<< - * type LxmlElementTagMatcherType ]: - * """ -*/ -struct LxmlElementTagMatcher { - PyObject_HEAD - struct __pyx_vtabstruct_4lxml_5etree__ElementTagMatcher *__pyx_vtab; - PyObject *_pystrings; - int _node_type; - char *_href; - char *_name; -}; - -/* "lxml/etree.pyx":2801 - * self._name = NULL - * - * cdef public class _ElementIterator(_ElementTagMatcher) [ # <<<<<<<<<<<<<< - * object LxmlElementIterator, type LxmlElementIteratorType ]: - * """ -*/ -struct LxmlElementIterator { - struct LxmlElementTagMatcher __pyx_base; - struct LxmlElement *_node; - _node_to_node_function _next_element; -}; - -/* "src/lxml/classlookup.pxi":6 - * # Custom Element classes - * - * cdef public class ElementBase(_Element) [ type LxmlElementBaseType, # <<<<<<<<<<<<<< - * object LxmlElementBase ]: - * """ElementBase(*children, attrib=None, nsmap=None, **_extra) -*/ -struct LxmlElementBase { - struct LxmlElement __pyx_base; -}; - -/* "src/lxml/classlookup.pxi":210 - * # Element class lookup - * - * ctypedef public object (*_element_class_lookup_function)(object, _Document, xmlNode*) # <<<<<<<<<<<<<< - * - * # class to store element class lookup functions -*/ -typedef PyObject *(*_element_class_lookup_function)(PyObject *, struct LxmlDocument *, xmlNode *); - -/* "src/lxml/classlookup.pxi":213 - * - * # class to store element class lookup functions - * cdef public class ElementClassLookup [ type LxmlElementClassLookupType, # <<<<<<<<<<<<<< - * object LxmlElementClassLookup ]: - * """ElementClassLookup(self) -*/ -struct LxmlElementClassLookup { - PyObject_HEAD - _element_class_lookup_function _lookup_function; -}; - -/* "src/lxml/classlookup.pxi":221 - * - * - * cdef public class FallbackElementClassLookup(ElementClassLookup) \ # <<<<<<<<<<<<<< - * [ type LxmlFallbackElementClassLookupType, - * object LxmlFallbackElementClassLookup ]: -*/ -struct LxmlFallbackElementClassLookup { - struct LxmlElementClassLookup __pyx_base; - struct __pyx_vtabstruct_4lxml_5etree_FallbackElementClassLookup *__pyx_vtab; - struct LxmlElementClassLookup *fallback; - _element_class_lookup_function _fallback_function; -}; - -#ifndef __PYX_HAVE_API__lxml__etree - -#ifdef CYTHON_EXTERN_C - #undef __PYX_EXTERN_C - #define __PYX_EXTERN_C CYTHON_EXTERN_C -#elif defined(__PYX_EXTERN_C) - #ifdef _MSC_VER - #pragma message ("Please do not define the '__PYX_EXTERN_C' macro externally. Use 'CYTHON_EXTERN_C' instead.") - #else - #warning Please do not define the '__PYX_EXTERN_C' macro externally. Use 'CYTHON_EXTERN_C' instead. - #endif -#else - #ifdef __cplusplus - #define __PYX_EXTERN_C extern "C" - #else - #define __PYX_EXTERN_C extern - #endif -#endif - -#ifndef DL_IMPORT - #define DL_IMPORT(_T) _T -#endif - -__PYX_EXTERN_C DL_IMPORT(PyTypeObject) LxmlDocumentType; -__PYX_EXTERN_C DL_IMPORT(PyTypeObject) LxmlElementType; -__PYX_EXTERN_C DL_IMPORT(PyTypeObject) LxmlElementTreeType; -__PYX_EXTERN_C DL_IMPORT(PyTypeObject) LxmlElementTagMatcherType; -__PYX_EXTERN_C DL_IMPORT(PyTypeObject) LxmlElementIteratorType; -__PYX_EXTERN_C DL_IMPORT(PyTypeObject) LxmlElementBaseType; -__PYX_EXTERN_C DL_IMPORT(PyTypeObject) LxmlElementClassLookupType; -__PYX_EXTERN_C DL_IMPORT(PyTypeObject) LxmlFallbackElementClassLookupType; - -__PYX_EXTERN_C struct LxmlElement *deepcopyNodeToDocument(struct LxmlDocument *, xmlNode *); -__PYX_EXTERN_C struct LxmlElementTree *elementTreeFactory(struct LxmlElement *); -__PYX_EXTERN_C struct LxmlElementTree *newElementTree(struct LxmlElement *, PyObject *); -__PYX_EXTERN_C struct LxmlElementTree *adoptExternalDocument(xmlDoc *, PyObject *, int); -__PYX_EXTERN_C struct LxmlElement *elementFactory(struct LxmlDocument *, xmlNode *); -__PYX_EXTERN_C struct LxmlElement *makeElement(PyObject *, struct LxmlDocument *, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *); -__PYX_EXTERN_C struct LxmlElement *makeSubElement(struct LxmlElement *, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *); -__PYX_EXTERN_C void setElementClassLookupFunction(_element_class_lookup_function, PyObject *); -__PYX_EXTERN_C PyObject *lookupDefaultElementClass(PyObject *, PyObject *, xmlNode *); -__PYX_EXTERN_C PyObject *lookupNamespaceElementClass(PyObject *, PyObject *, xmlNode *); -__PYX_EXTERN_C PyObject *callLookupFallback(struct LxmlFallbackElementClassLookup *, struct LxmlDocument *, xmlNode *); -__PYX_EXTERN_C int tagMatches(xmlNode *, const xmlChar *, const xmlChar *); -__PYX_EXTERN_C struct LxmlDocument *documentOrRaise(PyObject *); -__PYX_EXTERN_C struct LxmlElement *rootNodeOrRaise(PyObject *); -__PYX_EXTERN_C int hasText(xmlNode *); -__PYX_EXTERN_C int hasTail(xmlNode *); -__PYX_EXTERN_C PyObject *textOf(xmlNode *); -__PYX_EXTERN_C PyObject *tailOf(xmlNode *); -__PYX_EXTERN_C int setNodeText(xmlNode *, PyObject *); -__PYX_EXTERN_C int setTailText(xmlNode *, PyObject *); -__PYX_EXTERN_C PyObject *attributeValue(xmlNode *, xmlAttr *); -__PYX_EXTERN_C PyObject *attributeValueFromNsName(xmlNode *, const xmlChar *, const xmlChar *); -__PYX_EXTERN_C PyObject *getAttributeValue(struct LxmlElement *, PyObject *, PyObject *); -__PYX_EXTERN_C PyObject *iterattributes(struct LxmlElement *, int); -__PYX_EXTERN_C PyObject *collectAttributes(xmlNode *, int); -__PYX_EXTERN_C int setAttributeValue(struct LxmlElement *, PyObject *, PyObject *); -__PYX_EXTERN_C int delAttribute(struct LxmlElement *, PyObject *); -__PYX_EXTERN_C int delAttributeFromNsName(xmlNode *, const xmlChar *, const xmlChar *); -__PYX_EXTERN_C int hasChild(xmlNode *); -__PYX_EXTERN_C xmlNode *findChild(xmlNode *, Py_ssize_t); -__PYX_EXTERN_C xmlNode *findChildForwards(xmlNode *, Py_ssize_t); -__PYX_EXTERN_C xmlNode *findChildBackwards(xmlNode *, Py_ssize_t); -__PYX_EXTERN_C xmlNode *nextElement(xmlNode *); -__PYX_EXTERN_C xmlNode *previousElement(xmlNode *); -__PYX_EXTERN_C void appendChild(struct LxmlElement *, struct LxmlElement *); -__PYX_EXTERN_C int appendChildToElement(struct LxmlElement *, struct LxmlElement *); -__PYX_EXTERN_C PyObject *pyunicode(const xmlChar *); -__PYX_EXTERN_C PyObject *utf8(PyObject *); -__PYX_EXTERN_C PyObject *getNsTag(PyObject *); -__PYX_EXTERN_C PyObject *getNsTagWithEmptyNs(PyObject *); -__PYX_EXTERN_C PyObject *namespacedName(xmlNode *); -__PYX_EXTERN_C PyObject *namespacedNameFromNsName(const xmlChar *, const xmlChar *); -__PYX_EXTERN_C void iteratorStoreNext(struct LxmlElementIterator *, struct LxmlElement *); -__PYX_EXTERN_C void initTagMatch(struct LxmlElementTagMatcher *, PyObject *); -__PYX_EXTERN_C xmlNs *findOrBuildNodeNsPrefix(struct LxmlDocument *, xmlNode *, const xmlChar *, const xmlChar *); - -#endif /* !__PYX_HAVE_API__lxml__etree */ - -/* WARNING: the interface of the module init function changed in CPython 3.5. */ -/* It now returns a PyModuleDef instance instead of a PyModule instance. */ - -/* WARNING: Use PyImport_AppendInittab("etree", PyInit_etree) instead of calling PyInit_etree directly from Python 3.5 */ -PyMODINIT_FUNC PyInit_etree(void); - -#if PY_VERSION_HEX >= 0x03050000 && (defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER) || (defined(__cplusplus) && __cplusplus >= 201402L)) -#if defined(__cplusplus) && __cplusplus >= 201402L -[[deprecated("Use PyImport_AppendInittab(\"etree\", PyInit_etree) instead of calling PyInit_etree directly.")]] inline -#elif defined(__GNUC__) || defined(__clang__) -__attribute__ ((__deprecated__("Use PyImport_AppendInittab(\"etree\", PyInit_etree) instead of calling PyInit_etree directly."), __unused__)) __inline__ -#elif defined(_MSC_VER) -__declspec(deprecated("Use PyImport_AppendInittab(\"etree\", PyInit_etree) instead of calling PyInit_etree directly.")) __inline -#endif -static PyObject* __PYX_WARN_IF_PyInit_etree_INIT_CALLED(PyObject* res) { - return res; -} -#define PyInit_etree() __PYX_WARN_IF_PyInit_etree_INIT_CALLED(PyInit_etree()) -#endif - -#endif /* !__PYX_HAVE__lxml__etree */ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/etree.pyx b/presentation/.venv/lib/python3.12/site-packages/lxml/etree.pyx deleted file mode 100644 index 149a414..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/etree.pyx +++ /dev/null @@ -1,3858 +0,0 @@ -# cython: binding=True -# cython: auto_pickle=False -# cython: language_level=3 - -""" -The ``lxml.etree`` module implements the extended ElementTree API for XML. -""" - -__docformat__ = "restructuredtext en" - -__all__ = [ - 'AttributeBasedElementClassLookup', 'C14NError', 'C14NWriterTarget', 'CDATA', - 'Comment', 'CommentBase', 'CustomElementClassLookup', 'DEBUG', - 'DTD', 'DTDError', 'DTDParseError', 'DTDValidateError', - 'DocumentInvalid', 'ETCompatXMLParser', 'ETXPath', 'Element', - 'ElementBase', 'ElementClassLookup', 'ElementDefaultClassLookup', - 'ElementNamespaceClassLookup', 'ElementTree', 'Entity', 'EntityBase', - 'Error', 'ErrorDomains', 'ErrorLevels', 'ErrorTypes', 'Extension', - 'FallbackElementClassLookup', 'FunctionNamespace', 'HTML', 'HTMLParser', - 'ICONV_COMPILED_VERSION', - 'LIBXML_COMPILED_VERSION', 'LIBXML_VERSION', - 'LIBXML_FEATURES', - 'LIBXSLT_COMPILED_VERSION', 'LIBXSLT_VERSION', - 'LXML_VERSION', - 'LxmlError', 'LxmlRegistryError', 'LxmlSyntaxError', - 'NamespaceRegistryError', 'PI', 'PIBase', 'ParseError', - 'ParserBasedElementClassLookup', 'ParserError', 'ProcessingInstruction', - 'PyErrorLog', 'PythonElementClassLookup', 'QName', 'RelaxNG', - 'RelaxNGError', 'RelaxNGErrorTypes', 'RelaxNGParseError', - 'RelaxNGValidateError', 'Resolver', 'Schematron', 'SchematronError', - 'SchematronParseError', 'SchematronValidateError', 'SerialisationError', - 'SubElement', 'TreeBuilder', 'XInclude', 'XIncludeError', 'XML', - 'XMLDTDID', 'XMLID', 'XMLParser', 'XMLSchema', 'XMLSchemaError', - 'XMLSchemaParseError', 'XMLSchemaValidateError', 'XMLSyntaxError', - 'XMLTreeBuilder', 'XPath', 'XPathDocumentEvaluator', 'XPathError', - 'XPathEvalError', 'XPathEvaluator', 'XPathFunctionError', 'XPathResultError', - 'XPathSyntaxError', 'XSLT', 'XSLTAccessControl', 'XSLTApplyError', - 'XSLTError', 'XSLTExtension', 'XSLTExtensionError', 'XSLTParseError', - 'XSLTSaveError', 'canonicalize', - 'cleanup_namespaces', 'clear_error_log', 'dump', - 'fromstring', 'fromstringlist', 'get_default_parser', 'iselement', - 'iterparse', 'iterwalk', 'parse', 'parseid', 'register_namespace', - 'set_default_parser', 'set_element_class_lookup', 'strip_attributes', - 'strip_elements', 'strip_tags', 'tostring', 'tostringlist', 'tounicode', - 'use_global_python_log' - ] - -cimport cython - -from lxml cimport python -from lxml.includes cimport tree, config -from lxml.includes.tree cimport xmlDoc, xmlNode, xmlAttr, xmlNs, _isElement, _getNs -from lxml.includes.tree cimport const_xmlChar, xmlChar, _xcstr -from lxml.python cimport _cstr, _isString -from lxml.includes cimport xpath -from lxml.includes cimport c14n - -# Cython's standard declarations -cimport cpython.mem -cimport cpython.ref -from libc cimport limits, stdio, stdlib -from libc cimport string as cstring_h # not to be confused with stdlib 'string' -from libc.string cimport const_char - -cdef object os_path_abspath -from os.path import abspath as os_path_abspath - -cdef object BytesIO, StringIO -from io import BytesIO, StringIO - -cdef object OrderedDict -from collections import OrderedDict - -cdef object _elementpath -from lxml import _elementpath - -cdef object sys -import sys - -cdef object re -import re - -cdef object partial -from functools import partial - -cdef object islice -from itertools import islice - -cdef object ITER_EMPTY = iter(()) - -cdef object MutableMapping -from collections.abc import MutableMapping - -class _ImmutableMapping(MutableMapping): - def __getitem__(self, key): - raise KeyError, key - - def __setitem__(self, key, value): - raise KeyError, key - - def __delitem__(self, key): - raise KeyError, key - - def __contains__(self, key): - return False - - def __len__(self): - return 0 - - def __iter__(self): - return ITER_EMPTY - iterkeys = itervalues = iteritems = __iter__ - -cdef object IMMUTABLE_EMPTY_MAPPING = _ImmutableMapping() -del _ImmutableMapping - - -# the rules -# --------- -# any libxml C argument/variable is prefixed with c_ -# any non-public function/class is prefixed with an underscore -# instance creation is always through factories - -# what to do with libxml2/libxslt error messages? -# 0 : drop -# 1 : use log -DEF __DEBUG = 1 - -# maximum number of lines in the libxml2/xslt log if __DEBUG == 1 -DEF __MAX_LOG_SIZE = 100 - -# make the compiled-in debug state publicly available -DEBUG = __DEBUG - -# A struct to store a cached qualified tag name+href pair. -# While we can borrow the c_name from the document dict, -# PyPy requires us to store a Python reference for the -# namespace in order to keep the byte buffer alive. -cdef struct qname: - const_xmlChar* c_name - python.PyObject* href - -# initialize parser (and threading) -xmlparser.xmlInitParser() - -# global per-thread setup -tree.xmlThrDefIndentTreeOutput(1) -tree.xmlThrDefLineNumbersDefaultValue(1) - -_initThreadLogging() - -# filename encoding -cdef bytes _FILENAME_ENCODING = (sys.getfilesystemencoding() or sys.getdefaultencoding() or 'ascii').encode("UTF-8") -cdef char* _C_FILENAME_ENCODING = _cstr(_FILENAME_ENCODING) - -# set up some default namespace prefixes -cdef dict _DEFAULT_NAMESPACE_PREFIXES = { - b"http://www.w3.org/XML/1998/namespace": b'xml', - b"http://www.w3.org/1999/xhtml": b"html", - b"http://www.w3.org/1999/XSL/Transform": b"xsl", - b"http://www.w3.org/1999/02/22-rdf-syntax-ns#": b"rdf", - b"http://schemas.xmlsoap.org/wsdl/": b"wsdl", - # xml schema - b"http://www.w3.org/2001/XMLSchema": b"xs", - b"http://www.w3.org/2001/XMLSchema-instance": b"xsi", - # dublin core - b"http://purl.org/dc/elements/1.1/": b"dc", - # objectify - b"http://codespeak.net/lxml/objectify/pytype" : b"py", -} - -# To avoid runtime encoding overhead, we keep a Unicode copy -# of the uri-prefix mapping as (str, str) items view. -cdef object _DEFAULT_NAMESPACE_PREFIXES_ITEMS = [] - -cdef _update_default_namespace_prefixes_items(): - cdef bytes ns, prefix - global _DEFAULT_NAMESPACE_PREFIXES_ITEMS - _DEFAULT_NAMESPACE_PREFIXES_ITEMS = { - ns.decode('utf-8') : prefix.decode('utf-8') - for ns, prefix in _DEFAULT_NAMESPACE_PREFIXES.items() - }.items() - -_update_default_namespace_prefixes_items() - -cdef object _check_internal_prefix = re.compile(br"ns\d+$").match - -def register_namespace(prefix, uri): - """Registers a namespace prefix that newly created Elements in that - namespace will use. The registry is global, and any existing - mapping for either the given prefix or the namespace URI will be - removed. - """ - prefix_utf, uri_utf = _utf8(prefix), _utf8(uri) - if _check_internal_prefix(prefix_utf): - raise ValueError("Prefix format reserved for internal use") - _tagValidOrRaise(prefix_utf) - _uriValidOrRaise(uri_utf) - if (uri_utf == b"http://www.w3.org/XML/1998/namespace" and prefix_utf != b'xml' - or prefix_utf == b'xml' and uri_utf != b"http://www.w3.org/XML/1998/namespace"): - raise ValueError("Cannot change the 'xml' prefix of the XML namespace") - for k, v in list(_DEFAULT_NAMESPACE_PREFIXES.items()): - if k == uri_utf or v == prefix_utf: - del _DEFAULT_NAMESPACE_PREFIXES[k] - _DEFAULT_NAMESPACE_PREFIXES[uri_utf] = prefix_utf - _update_default_namespace_prefixes_items() - - -# Error superclass for ElementTree compatibility -cdef class Error(Exception): - pass - -# module level superclass for all exceptions -cdef class LxmlError(Error): - """Main exception base class for lxml. All other exceptions inherit from - this one. - """ - def __init__(self, message, error_log=None): - super(_Error, self).__init__(message) - if error_log is None: - self.error_log = __copyGlobalErrorLog() - else: - self.error_log = error_log.copy() - -cdef object _Error = Error - - -# superclass for all syntax errors -class LxmlSyntaxError(LxmlError, SyntaxError): - """Base class for all syntax errors. - """ - -cdef class C14NError(LxmlError): - """Error during C14N serialisation. - """ - -# version information -cdef tuple __unpackDottedVersion(version): - version_list = [] - l = (version.decode("ascii").replace('-', '.').split('.') + [0]*4)[:4] - for item in l: - try: - item = int(item) - except ValueError: - if item.startswith('dev'): - count = item[3:] - item = -300 - elif item.startswith('alpha'): - count = item[5:] - item = -200 - elif item.startswith('beta'): - count = item[4:] - item = -100 - else: - count = 0 - if count: - item += int(count) - version_list.append(item) - return tuple(version_list) - -cdef tuple __unpackIntVersion(int c_version, int base=100): - return ( - ((c_version // (base*base)) % base), - ((c_version // base) % base), - (c_version % base) - ) - -cdef int _LIBXML_VERSION_INT -try: - _LIBXML_VERSION_INT = int( - re.match('[0-9]+', (tree.xmlParserVersion).decode("ascii")).group(0)) -except Exception: - print("Unknown libxml2 version: " + (tree.xmlParserVersion).decode("latin1")) - _LIBXML_VERSION_INT = 0 - -LIBXML_VERSION = __unpackIntVersion(_LIBXML_VERSION_INT) -LIBXML_COMPILED_VERSION = __unpackIntVersion(tree.LIBXML_VERSION) -LXML_VERSION = __unpackDottedVersion(tree.LXML_VERSION_STRING) - -__version__ = tree.LXML_VERSION_STRING.decode("ascii") - -cdef extern from *: - """ - #ifdef ZLIB_VERNUM - #define __lxml_zlib_version (ZLIB_VERNUM >> 4) - #else - #define __lxml_zlib_version 0 - #endif - #ifdef _LIBICONV_VERSION - #define __lxml_iconv_version (_LIBICONV_VERSION << 8) - #else - #define __lxml_iconv_version 0 - #endif - """ - # zlib isn't included automatically by libxml2's headers - #long ZLIB_HEX_VERSION "__lxml_zlib_version" - long LIBICONV_HEX_VERSION "__lxml_iconv_version" - -#ZLIB_COMPILED_VERSION = __unpackIntVersion(ZLIB_HEX_VERSION, base=0x10) -ICONV_COMPILED_VERSION = __unpackIntVersion(LIBICONV_HEX_VERSION, base=0x100)[:2] - - -cdef extern from "libxml/xmlversion.h": - """ - static const char* const _lxml_lib_features[] = { -#ifdef LIBXML_HTML_ENABLED - "html", -#endif -#ifdef LIBXML_FTP_ENABLED - "ftp", -#endif -#ifdef LIBXML_HTTP_ENABLED - "http", -#endif -#ifdef LIBXML_CATALOG_ENABLED - "catalog", -#endif -#ifdef LIBXML_XPATH_ENABLED - "xpath", -#endif -#ifdef LIBXML_ICONV_ENABLED - "iconv", -#endif -#ifdef LIBXML_ICU_ENABLED - "icu", -#endif -#ifdef LIBXML_REGEXP_ENABLED - "regexp", -#endif -#ifdef LIBXML_SCHEMAS_ENABLED - "xmlschema", -#endif -#ifdef LIBXML_SCHEMATRON_ENABLED - "schematron", -#endif -#ifdef LIBXML_ZLIB_ENABLED - "zlib", -#endif -#ifdef LIBXML_LZMA_ENABLED - "lzma", -#endif - 0 - }; - """ - const char* const* _LXML_LIB_FEATURES "_lxml_lib_features" - - -cdef set _copy_lib_features(): - features = set() - feature = _LXML_LIB_FEATURES - while feature[0]: - features.add(feature[0].decode('ASCII')) - feature += 1 - return features - -LIBXML_COMPILED_FEATURES = _copy_lib_features() -LIBXML_FEATURES = { - feature_name for feature_id, feature_name in [ - #XML_WITH_THREAD = 1 - #XML_WITH_TREE = 2 - #XML_WITH_OUTPUT = 3 - #XML_WITH_PUSH = 4 - #XML_WITH_READER = 5 - #XML_WITH_PATTERN = 6 - #XML_WITH_WRITER = 7 - #XML_WITH_SAX1 = 8 - (xmlparser.XML_WITH_FTP, "ftp"), # XML_WITH_FTP = 9 - (xmlparser.XML_WITH_HTTP, "http"), # XML_WITH_HTTP = 10 - #XML_WITH_VALID = 11 - (xmlparser.XML_WITH_HTML, "html"), # XML_WITH_HTML = 12 - #XML_WITH_LEGACY = 13 - #XML_WITH_C14N = 14 - (xmlparser.XML_WITH_CATALOG, "catalog"), # XML_WITH_CATALOG = 15 - (xmlparser.XML_WITH_XPATH, "xpath"), # XML_WITH_XPATH = 16 - #XML_WITH_XPTR = 17 - #XML_WITH_XINCLUDE = 18 - (xmlparser.XML_WITH_ICONV, "iconv"), # XML_WITH_ICONV = 19 - #XML_WITH_ISO8859X = 20 - #XML_WITH_UNICODE = 21 - (xmlparser.XML_WITH_REGEXP, "regexp"), # XML_WITH_REGEXP = 22 - #XML_WITH_AUTOMATA = 23 - #XML_WITH_EXPR = 24 - (xmlparser.XML_WITH_SCHEMAS, "xmlschema"), # XML_WITH_SCHEMAS = 25 - (xmlparser.XML_WITH_SCHEMATRON, "schematron"), # XML_WITH_SCHEMATRON = 26 - #XML_WITH_MODULES = 27 - #XML_WITH_DEBUG = 28 - #XML_WITH_DEBUG_MEM = 29 - #XML_WITH_DEBUG_RUN = 30 # unused - (xmlparser.XML_WITH_ZLIB, "zlib"), # XML_WITH_ZLIB = 31 - (xmlparser.XML_WITH_ICU, "icu"), # XML_WITH_ICU = 32 - (xmlparser.XML_WITH_LZMA, "lzma"), # XML_WITH_LZMA = 33 - ] if xmlparser.xmlHasFeature(feature_id) -} - -cdef bint HAS_ZLIB_COMPRESSION = xmlparser.xmlHasFeature(xmlparser.XML_WITH_ZLIB) - - -# class for temporary storage of Python references, -# used e.g. for XPath results -@cython.final -@cython.internal -cdef class _TempStore: - cdef list _storage - def __init__(self): - self._storage = [] - - cdef int add(self, obj) except -1: - self._storage.append(obj) - return 0 - - cdef int clear(self) except -1: - del self._storage[:] - return 0 - - -# class for temporarily storing exceptions raised in extensions -@cython.internal -cdef class _ExceptionContext: - cdef object _exc_info - - cdef int clear(self) except -1: - self._exc_info = None - return 0 - - @cython.final - cdef void _store_raised(self) noexcept: - try: - self._exc_info = sys.exc_info() - except BaseException as e: - self._store_exception(e) - finally: - return # and swallow any further exceptions - - @cython.final - cdef int _store_exception(self, exception) except -1: - self._exc_info = (exception, None, None) - return 0 - - @cython.final - cdef bint _has_raised(self) except -1: - return self._exc_info is not None - - @cython.final - cdef int _raise_if_stored(self) except -1: - if self._exc_info is None: - return 0 - type, value, traceback = self._exc_info - self._exc_info = None - if value is None and traceback is None: - raise type - else: - raise type, value, traceback - - -# type of a function that steps from node to node -ctypedef public xmlNode* (*_node_to_node_function)(xmlNode*) - - -################################################################################ -# Include submodules - -include "proxy.pxi" # Proxy handling (element backpointers/memory/etc.) -include "apihelpers.pxi" # Private helper functions -include "xmlerror.pxi" # Error and log handling - - -################################################################################ -# Public Python API - -@cython.final -@cython.freelist(8) -cdef public class _Document [ type LxmlDocumentType, object LxmlDocument ]: - """Internal base class to reference a libxml document. - - When instances of this class are garbage collected, the libxml - document is cleaned up. - """ - cdef int _ns_counter - cdef bytes _prefix_tail - cdef xmlDoc* _c_doc - cdef _BaseParser _parser - - def __dealloc__(self): - # if there are no more references to the document, it is safe - # to clean the whole thing up, as all nodes have a reference to - # the document - tree.xmlFreeDoc(self._c_doc) - - @cython.final - cdef getroot(self): - # return an element proxy for the document root - cdef xmlNode* c_node - c_node = tree.xmlDocGetRootElement(self._c_doc) - if c_node is NULL: - return None - return _elementFactory(self, c_node) - - @cython.final - cdef bint hasdoctype(self) noexcept: - # DOCTYPE gets parsed into internal subset (xmlDTD*) - return self._c_doc is not NULL and self._c_doc.intSubset is not NULL - - @cython.final - cdef getdoctype(self): - # get doctype info: root tag, public/system ID (or None if not known) - cdef tree.xmlDtd* c_dtd - cdef xmlNode* c_root_node - public_id = None - sys_url = None - c_dtd = self._c_doc.intSubset - if c_dtd is not NULL: - if c_dtd.ExternalID is not NULL: - public_id = funicode(c_dtd.ExternalID) - if c_dtd.SystemID is not NULL: - sys_url = funicode(c_dtd.SystemID) - c_dtd = self._c_doc.extSubset - if c_dtd is not NULL: - if not public_id and c_dtd.ExternalID is not NULL: - public_id = funicode(c_dtd.ExternalID) - if not sys_url and c_dtd.SystemID is not NULL: - sys_url = funicode(c_dtd.SystemID) - c_root_node = tree.xmlDocGetRootElement(self._c_doc) - if c_root_node is NULL: - root_name = None - else: - root_name = funicode(c_root_node.name) - return root_name, public_id, sys_url - - @cython.final - cdef getxmlinfo(self): - # return XML version and encoding (or None if not known) - cdef xmlDoc* c_doc = self._c_doc - if c_doc.version is NULL: - version = None - else: - version = funicode(c_doc.version) - if c_doc.encoding is NULL: - encoding = None - else: - encoding = funicode(c_doc.encoding) - return version, encoding - - @cython.final - cdef isstandalone(self): - # returns True for "standalone=true", - # False for "standalone=false", None if not provided - if self._c_doc.standalone == -1: - return None - else: - return (self._c_doc.standalone == 1) - - @cython.final - cdef bytes buildNewPrefix(self): - # get a new unique prefix ("nsX") for this document - cdef bytes ns - if self._ns_counter < len(_PREFIX_CACHE): - ns = _PREFIX_CACHE[self._ns_counter] - else: - ns = python.PyBytes_FromFormat("ns%d", self._ns_counter) - if self._prefix_tail is not None: - ns += self._prefix_tail - self._ns_counter += 1 - if self._ns_counter < 0: - # overflow! - self._ns_counter = 0 - if self._prefix_tail is None: - self._prefix_tail = b"A" - else: - self._prefix_tail += b"A" - return ns - - @cython.final - cdef xmlNs* _findOrBuildNodeNs(self, xmlNode* c_node, - const_xmlChar* c_href, const_xmlChar* c_prefix, - bint is_attribute) except NULL: - """Get or create namespace structure for a node. Reuses the prefix if - possible. - """ - cdef xmlNs* c_ns - cdef xmlNs* c_doc_ns - cdef python.PyObject* dict_result - if c_node.type != tree.XML_ELEMENT_NODE: - assert c_node.type == tree.XML_ELEMENT_NODE, \ - "invalid node type %d, expected %d" % ( - c_node.type, tree.XML_ELEMENT_NODE) - # look for existing ns declaration - c_ns = _searchNsByHref(c_node, c_href, is_attribute) - if c_ns is not NULL: - if is_attribute and c_ns.prefix is NULL: - # do not put namespaced attributes into the default - # namespace as this would break serialisation - pass - else: - return c_ns - - # none found => determine a suitable new prefix - if c_prefix is NULL: - dict_result = python.PyDict_GetItem( - _DEFAULT_NAMESPACE_PREFIXES, c_href) - if dict_result is not NULL: - prefix = dict_result - else: - prefix = self.buildNewPrefix() - c_prefix = _xcstr(prefix) - - # make sure the prefix is not in use already - while tree.xmlSearchNs(self._c_doc, c_node, c_prefix) is not NULL: - prefix = self.buildNewPrefix() - c_prefix = _xcstr(prefix) - - # declare the namespace and return it - c_ns = tree.xmlNewNs(c_node, c_href, c_prefix) - if c_ns is NULL: - raise MemoryError() - return c_ns - - @cython.final - cdef int _setNodeNs(self, xmlNode* c_node, const_xmlChar* c_href) except -1: - "Lookup namespace structure and set it for the node." - c_ns = self._findOrBuildNodeNs(c_node, c_href, NULL, 0) - tree.xmlSetNs(c_node, c_ns) - - -cdef tuple __initPrefixCache(): - cdef int i - return tuple([ python.PyBytes_FromFormat("ns%d", i) - for i in range(26) ]) - -cdef tuple _PREFIX_CACHE = __initPrefixCache() - - -cdef _Document _documentFactory(xmlDoc* c_doc, _BaseParser parser): - cdef _Document result - result = _Document.__new__(_Document) - result._c_doc = c_doc - result._ns_counter = 0 - result._prefix_tail = None - if parser is None: - parser = __GLOBAL_PARSER_CONTEXT.getDefaultParser() - result._parser = parser - return result - - -cdef object _find_invalid_public_id_characters = re.compile( - ur"[^\x20\x0D\x0Aa-zA-Z0-9'()+,./:=?;!*#@$_%-]+").search - - -cdef class DocInfo: - "Document information provided by parser and DTD." - cdef _Document _doc - def __cinit__(self, tree): - "Create a DocInfo object for an ElementTree object or root Element." - self._doc = _documentOrRaise(tree) - root_name, public_id, system_url = self._doc.getdoctype() - if not root_name and (public_id or system_url): - raise ValueError, "Could not find root node" - - @property - def root_name(self): - """Returns the name of the root node as defined by the DOCTYPE.""" - root_name, public_id, system_url = self._doc.getdoctype() - return root_name - - @cython.final - cdef tree.xmlDtd* _get_c_dtd(self) noexcept: - """"Return the DTD. Create it if it does not yet exist.""" - cdef xmlDoc* c_doc = self._doc._c_doc - cdef xmlNode* c_root_node - cdef const_xmlChar* c_name - - if c_doc.intSubset: - return c_doc.intSubset - - c_root_node = tree.xmlDocGetRootElement(c_doc) - c_name = c_root_node.name if c_root_node else NULL - return tree.xmlCreateIntSubset(c_doc, c_name, NULL, NULL) - - def clear(self): - """Removes DOCTYPE and internal subset from the document.""" - cdef xmlDoc* c_doc = self._doc._c_doc - cdef tree.xmlNode* c_dtd = c_doc.intSubset - if c_dtd is NULL: - return - tree.xmlUnlinkNode(c_dtd) - tree.xmlFreeNode(c_dtd) - - property public_id: - """Public ID of the DOCTYPE. - - Mutable. May be set to a valid string or None. If a DTD does not - exist, setting this variable (even to None) will create one. - """ - def __get__(self): - root_name, public_id, system_url = self._doc.getdoctype() - return public_id - - def __set__(self, value): - cdef xmlChar* c_value = NULL - if value is not None: - match = _find_invalid_public_id_characters(value) - if match: - raise ValueError, f'Invalid character(s) {match.group(0)!r} in public_id.' - value = _utf8(value) - c_value = tree.xmlStrdup(_xcstr(value)) - if not c_value: - raise MemoryError() - - c_dtd = self._get_c_dtd() - if not c_dtd: - tree.xmlFree(c_value) - raise MemoryError() - if c_dtd.ExternalID: - tree.xmlFree(c_dtd.ExternalID) - c_dtd.ExternalID = c_value - - property system_url: - """System ID of the DOCTYPE. - - Mutable. May be set to a valid string or None. If a DTD does not - exist, setting this variable (even to None) will create one. - """ - def __get__(self): - root_name, public_id, system_url = self._doc.getdoctype() - return system_url - - def __set__(self, value): - cdef xmlChar* c_value = NULL - if value is not None: - bvalue = _utf8(value) - # sys_url may be any valid unicode string that can be - # enclosed in single quotes or quotes. - if b"'" in bvalue and b'"' in bvalue: - raise ValueError( - 'System URL may not contain both single (\') and double quotes (").') - c_value = tree.xmlStrdup(_xcstr(bvalue)) - if not c_value: - raise MemoryError() - - c_dtd = self._get_c_dtd() - if not c_dtd: - tree.xmlFree(c_value) - raise MemoryError() - if c_dtd.SystemID: - tree.xmlFree(c_dtd.SystemID) - c_dtd.SystemID = c_value - - @property - def xml_version(self): - """Returns the XML version as declared by the document.""" - xml_version, encoding = self._doc.getxmlinfo() - return xml_version - - @property - def encoding(self): - """Returns the encoding name as declared by the document.""" - xml_version, encoding = self._doc.getxmlinfo() - return encoding - - @property - def standalone(self): - """Returns the standalone flag as declared by the document. The possible - values are True (``standalone='yes'``), False - (``standalone='no'`` or flag not provided in the declaration), - and None (unknown or no declaration found). Note that a - normal truth test on this value will always tell if the - ``standalone`` flag was set to ``'yes'`` or not. - """ - return self._doc.isstandalone() - - property URL: - "The source URL of the document (or None if unknown)." - def __get__(self): - if self._doc._c_doc.URL is NULL: - return None - return _decodeFilename(self._doc._c_doc.URL) - def __set__(self, url): - url = _encodeFilename(url) - c_oldurl = self._doc._c_doc.URL - if url is None: - self._doc._c_doc.URL = NULL - else: - self._doc._c_doc.URL = tree.xmlStrdup(_xcstr(url)) - if c_oldurl is not NULL: - tree.xmlFree(c_oldurl) - - @property - def doctype(self): - """Returns a DOCTYPE declaration string for the document.""" - root_name, public_id, system_url = self._doc.getdoctype() - if system_url: - # If '"' in system_url, we must escape it with single - # quotes, otherwise escape with double quotes. If url - # contains both a single quote and a double quote, XML - # standard is being violated. - if '"' in system_url: - quoted_system_url = f"'{system_url}'" - else: - quoted_system_url = f'"{system_url}"' - if public_id: - if system_url: - return f'' - else: - return f'' - elif system_url: - return f'' - elif self._doc.hasdoctype(): - return f'' - else: - return '' - - @property - def internalDTD(self): - """Returns a DTD validator based on the internal subset of the document.""" - return _dtdFactory(self._doc._c_doc.intSubset) - - @property - def externalDTD(self): - """Returns a DTD validator based on the external subset of the document.""" - return _dtdFactory(self._doc._c_doc.extSubset) - - -@cython.no_gc_clear -cdef public class _Element [ type LxmlElementType, object LxmlElement ]: - """Element class. - - References a document object and a libxml node. - - By pointing to a Document instance, a reference is kept to - _Document as long as there is some pointer to a node in it. - """ - cdef _Document _doc - cdef xmlNode* _c_node - cdef object _tag - - def _init(self): - """_init(self) - - Called after object initialisation. Custom subclasses may override - this if they recursively call _init() in the superclasses. - """ - - @cython.linetrace(False) - @cython.profile(False) - def __dealloc__(self): - #print("trying to free node:", self._c_node) - #displayNode(self._c_node, 0) - if self._c_node is not NULL: - _unregisterProxy(self) - attemptDeallocation(self._c_node) - - # MANIPULATORS - - def __setitem__(self, x, value): - """__setitem__(self, x, value) - - Replaces the given subelement index or slice. - """ - cdef xmlNode* c_node = NULL - cdef xmlNode* c_next - cdef xmlDoc* c_source_doc - cdef _Element element - cdef bint left_to_right - cdef Py_ssize_t slicelength = 0, step = 0 - _assertValidNode(self) - if value is None: - raise ValueError, "cannot assign None" - if isinstance(x, slice): - # slice assignment - _findChildSlice(x, self._c_node, &c_node, &step, &slicelength) - if step > 0: - left_to_right = 1 - else: - left_to_right = 0 - step = -step if step != python.PY_SSIZE_T_MIN else python.PY_SSIZE_T_MAX - _replaceSlice(self, c_node, slicelength, step, left_to_right, value) - return - else: - # otherwise: normal item assignment - element = value - _assertValidNode(element) - c_node = _findChild(self._c_node, x) - if c_node is NULL: - raise IndexError, "list index out of range" - c_source_doc = element._c_node.doc - c_next = element._c_node.next - _removeText(c_node.next) - tree.xmlReplaceNode(c_node, element._c_node) - _moveTail(c_next, element._c_node) - moveNodeToDocument(self._doc, c_source_doc, element._c_node) - if not attemptDeallocation(c_node): - moveNodeToDocument(self._doc, c_node.doc, c_node) - - def __delitem__(self, x): - """__delitem__(self, x) - - Deletes the given subelement or a slice. - """ - cdef xmlNode* c_node = NULL - cdef xmlNode* c_next - cdef Py_ssize_t step = 0, slicelength = 0 - _assertValidNode(self) - if isinstance(x, slice): - # slice deletion - if _isFullSlice(x): - c_node = self._c_node.children - if c_node is not NULL: - if not _isElement(c_node): - c_node = _nextElement(c_node) - while c_node is not NULL: - c_next = _nextElement(c_node) - _removeNode(self._doc, c_node) - c_node = c_next - else: - _findChildSlice(x, self._c_node, &c_node, &step, &slicelength) - _deleteSlice(self._doc, c_node, slicelength, step) - else: - # item deletion - c_node = _findChild(self._c_node, x) - if c_node is NULL: - raise IndexError, f"index out of range: {x}" - _removeNode(self._doc, c_node) - - def __deepcopy__(self, memo): - "__deepcopy__(self, memo)" - return self.__copy__() - - def __copy__(self): - "__copy__(self)" - cdef xmlDoc* c_doc - cdef xmlNode* c_node - cdef _Document new_doc - _assertValidNode(self) - c_doc = _copyDocRoot(self._doc._c_doc, self._c_node) # recursive - new_doc = _documentFactory(c_doc, self._doc._parser) - root = new_doc.getroot() - if root is not None: - return root - # Comment/PI - c_node = c_doc.children - while c_node is not NULL and c_node.type != self._c_node.type: - c_node = c_node.next - if c_node is NULL: - return None - return _elementFactory(new_doc, c_node) - - def set(self, key, value): - """set(self, key, value) - - Sets an element attribute. - In HTML documents (not XML or XHTML), the value None is allowed and creates - an attribute without value (just the attribute name). - """ - _assertValidNode(self) - _setAttributeValue(self, key, value) - - def append(self, _Element element not None): - """append(self, element) - - Adds a subelement to the end of this element. - """ - _assertValidNode(self) - _assertValidNode(element) - _appendChild(self, element) - - def addnext(self, _Element element not None): - """addnext(self, element) - - Adds the element as a following sibling directly after this - element. - - This is normally used to set a processing instruction or comment after - the root node of a document. Note that tail text is automatically - discarded when adding at the root level. - """ - _assertValidNode(self) - _assertValidNode(element) - if self._c_node.parent != NULL and not _isElement(self._c_node.parent): - if element._c_node.type not in (tree.XML_PI_NODE, tree.XML_COMMENT_NODE): - raise TypeError, "Only processing instructions and comments can be siblings of the root element" - element.tail = None - _appendSibling(self, element) - - def addprevious(self, _Element element not None): - """addprevious(self, element) - - Adds the element as a preceding sibling directly before this - element. - - This is normally used to set a processing instruction or comment - before the root node of a document. Note that tail text is - automatically discarded when adding at the root level. - """ - _assertValidNode(self) - _assertValidNode(element) - if self._c_node.parent != NULL and not _isElement(self._c_node.parent): - if element._c_node.type != tree.XML_PI_NODE: - if element._c_node.type != tree.XML_COMMENT_NODE: - raise TypeError, "Only processing instructions and comments can be siblings of the root element" - element.tail = None - _prependSibling(self, element) - - def extend(self, elements): - """extend(self, elements) - - Extends the current children by the elements in the iterable. - """ - cdef _Element element - _assertValidNode(self) - for element in elements: - if element is None: - raise TypeError, "Node must not be None" - _assertValidNode(element) - _appendChild(self, element) - - def clear(self, bint keep_tail=False): - """clear(self, keep_tail=False) - - Resets an element. This function removes all subelements, clears - all attributes and sets the text and tail properties to None. - - Pass ``keep_tail=True`` to leave the tail text untouched. - """ - cdef xmlAttr* c_attr - cdef xmlAttr* c_attr_next - cdef xmlNode* c_node - cdef xmlNode* c_node_next - _assertValidNode(self) - c_node = self._c_node - # remove self.text and self.tail - _removeText(c_node.children) - if not keep_tail: - _removeText(c_node.next) - # remove all attributes - c_attr = c_node.properties - if c_attr: - c_node.properties = NULL - tree.xmlFreePropList(c_attr) - # remove all subelements - c_node = c_node.children - if c_node and not _isElement(c_node): - c_node = _nextElement(c_node) - while c_node is not NULL: - c_node_next = _nextElement(c_node) - _removeNode(self._doc, c_node) - c_node = c_node_next - - def insert(self, index: int, _Element element not None): - """insert(self, index, element) - - Inserts a subelement at the given position in this element - """ - cdef xmlNode* c_node - cdef xmlNode* c_next - cdef xmlDoc* c_source_doc - _assertValidNode(self) - _assertValidNode(element) - c_node = _findChild(self._c_node, index) - if c_node is NULL: - _appendChild(self, element) - return - # prevent cycles - if _isAncestorOrSame(element._c_node, self._c_node): - raise ValueError("cannot append parent to itself") - c_source_doc = element._c_node.doc - c_next = element._c_node.next - tree.xmlAddPrevSibling(c_node, element._c_node) - _moveTail(c_next, element._c_node) - moveNodeToDocument(self._doc, c_source_doc, element._c_node) - - def remove(self, _Element element not None): - """remove(self, element) - - Removes a matching subelement. Unlike the find methods, this - method compares elements based on identity, not on tag value - or contents. - """ - cdef xmlNode* c_node - cdef xmlNode* c_next - _assertValidNode(self) - _assertValidNode(element) - c_node = element._c_node - if c_node.parent is not self._c_node: - raise ValueError, "Element is not a child of this node." - c_next = element._c_node.next - tree.xmlUnlinkNode(c_node) - _moveTail(c_next, c_node) - # fix namespace declarations - moveNodeToDocument(self._doc, c_node.doc, c_node) - - def replace(self, _Element old_element not None, - _Element new_element not None): - """replace(self, old_element, new_element) - - Replaces a subelement with the element passed as second argument. - """ - cdef xmlNode* c_old_node - cdef xmlNode* c_old_next - cdef xmlNode* c_new_node - cdef xmlNode* c_new_next - cdef xmlDoc* c_source_doc - _assertValidNode(self) - _assertValidNode(old_element) - _assertValidNode(new_element) - c_old_node = old_element._c_node - if c_old_node.parent is not self._c_node: - raise ValueError, "Element is not a child of this node." - c_new_node = new_element._c_node - # prevent cycles - if _isAncestorOrSame(c_new_node, self._c_node): - raise ValueError("cannot append parent to itself") - # replace node - c_old_next = c_old_node.next - c_new_next = c_new_node.next - c_source_doc = c_new_node.doc - tree.xmlReplaceNode(c_old_node, c_new_node) - _moveTail(c_new_next, c_new_node) - _moveTail(c_old_next, c_old_node) - moveNodeToDocument(self._doc, c_source_doc, c_new_node) - # fix namespace declarations - moveNodeToDocument(self._doc, c_old_node.doc, c_old_node) - - # PROPERTIES - property tag: - """Element tag - """ - def __get__(self): - if self._tag is not None: - return self._tag - _assertValidNode(self) - self._tag = _namespacedName(self._c_node) - return self._tag - - def __set__(self, value): - cdef _BaseParser parser - _assertValidNode(self) - ns, name = _getNsTag(value) - parser = self._doc._parser - if parser is not None and parser._for_html: - _htmlTagValidOrRaise(name) - else: - _tagValidOrRaise(name) - self._tag = value - tree.xmlNodeSetName(self._c_node, _xcstr(name)) - if ns is None: - self._c_node.ns = NULL - else: - self._doc._setNodeNs(self._c_node, _xcstr(ns)) - - @property - def attrib(self): - """Element attribute dictionary. Where possible, use get(), set(), - keys(), values() and items() to access element attributes. - """ - return _Attrib.__new__(_Attrib, self) - - property text: - """Text before the first subelement. This is either a string or - the value None, if there was no text. - """ - def __get__(self): - _assertValidNode(self) - return _collectText(self._c_node.children) - - def __set__(self, value): - _assertValidNode(self) - if isinstance(value, QName): - value = _resolveQNameText(self, value).decode('utf8') - _setNodeText(self._c_node, value) - - # using 'del el.text' is the wrong thing to do - #def __del__(self): - # _setNodeText(self._c_node, None) - - property tail: - """Text after this element's end tag, but before the next sibling - element's start tag. This is either a string or the value None, if - there was no text. - """ - def __get__(self): - _assertValidNode(self) - return _collectText(self._c_node.next) - - def __set__(self, value): - _assertValidNode(self) - _setTailText(self._c_node, value) - - # using 'del el.tail' is the wrong thing to do - #def __del__(self): - # _setTailText(self._c_node, None) - - # not in ElementTree, read-only - @property - def prefix(self): - """Namespace prefix or None. - """ - if self._c_node.ns is not NULL: - if self._c_node.ns.prefix is not NULL: - return funicode(self._c_node.ns.prefix) - return None - - # not in ElementTree, read-only - property sourceline: - """Original line number as found by the parser or None if unknown. - """ - def __get__(self): - cdef long line - _assertValidNode(self) - line = tree.xmlGetLineNo(self._c_node) - return line if line > 0 else None - - def __set__(self, line): - _assertValidNode(self) - if line <= 0: - self._c_node.line = 0 - else: - self._c_node.line = line - - # not in ElementTree, read-only - @property - def nsmap(self): - """Namespace prefix->URI mapping known in the context of this - Element. This includes all namespace declarations of the - parents. - - Note that changing the returned dict has no effect on the Element. - """ - _assertValidNode(self) - return _build_nsmap(self._c_node) - - # not in ElementTree, read-only - property base: - """The base URI of the Element (xml:base or HTML base URL). - None if the base URI is unknown. - - Note that the value depends on the URL of the document that - holds the Element if there is no xml:base attribute on the - Element or its ancestors. - - Setting this property will set an xml:base attribute on the - Element, regardless of the document type (XML or HTML). - """ - def __get__(self): - _assertValidNode(self) - c_base = tree.xmlNodeGetBase(self._doc._c_doc, self._c_node) - if c_base is NULL: - if self._doc._c_doc.URL is NULL: - return None - return _decodeFilename(self._doc._c_doc.URL) - try: - base = _decodeFilename(c_base) - finally: - tree.xmlFree(c_base) - return base - - def __set__(self, url): - _assertValidNode(self) - if url is None: - c_base = NULL - else: - url = _encodeFilename(url) - c_base = _xcstr(url) - tree.xmlNodeSetBase(self._c_node, c_base) - - # ACCESSORS - def __repr__(self): - "__repr__(self)" - return "" % (self.tag, id(self)) - - def __getitem__(self, x): - """Returns the subelement at the given position or the requested - slice. - """ - cdef xmlNode* c_node = NULL - cdef Py_ssize_t step = 0, slicelength = 0 - cdef Py_ssize_t c, i - cdef _node_to_node_function next_element - cdef list result - _assertValidNode(self) - if isinstance(x, slice): - # slicing - if _isFullSlice(x): - return _collectChildren(self) - _findChildSlice(x, self._c_node, &c_node, &step, &slicelength) - if c_node is NULL: - return [] - if step > 0: - next_element = _nextElement - else: - step = -step if step != python.PY_SSIZE_T_MIN else python.PY_SSIZE_T_MAX - next_element = _previousElement - result = [] - c = 0 - while c_node is not NULL and c < slicelength: - result.append(_elementFactory(self._doc, c_node)) - c += 1 - for i in range(step): - c_node = next_element(c_node) - if c_node is NULL: - break - return result - else: - # indexing - c_node = _findChild(self._c_node, x) - if c_node is NULL: - raise IndexError, "list index out of range" - return _elementFactory(self._doc, c_node) - - def __len__(self): - """__len__(self) - - Returns the number of subelements. - """ - _assertValidNode(self) - return _countElements(self._c_node.children) - - def __bool__(self): - """__bool__(self)""" - import warnings - warnings.warn( - "Truth-testing of elements was a source of confusion and will always " - "return True in future versions. " - "Use specific 'len(elem)' or 'elem is not None' test instead.", - FutureWarning - ) - # emulate old behaviour - _assertValidNode(self) - return _hasChild(self._c_node) - - def __contains__(self, element): - "__contains__(self, element)" - cdef xmlNode* c_node - _assertValidNode(self) - if not isinstance(element, _Element): - return 0 - c_node = (<_Element>element)._c_node - return c_node is not NULL and c_node.parent is self._c_node - - def __iter__(self): - "__iter__(self)" - return ElementChildIterator(self) - - def __reversed__(self): - "__reversed__(self)" - return ElementChildIterator(self, reversed=True) - - def index(self, child: _Element, start: int = None, stop: int = None): - """index(self, child, start=None, stop=None) - - Find the position of the child within the parent. - - This method is not part of the original ElementTree API. - """ - cdef Py_ssize_t k, l - cdef Py_ssize_t c_start, c_stop - cdef xmlNode* c_child - cdef xmlNode* c_start_node - _assertValidNode(self) - _assertValidNode(child) - c_child = child._c_node - if c_child.parent is not self._c_node: - raise ValueError, "Element is not a child of this node." - - # handle the unbounded search straight away (normal case) - if stop is None and (start is None or start == 0): - k = 0 - c_child = c_child.prev - while c_child is not NULL: - if _isElement(c_child): - k += 1 - c_child = c_child.prev - return k - - # check indices - if start is None: - c_start = 0 - else: - c_start = start - if stop is None: - c_stop = 0 - else: - c_stop = stop - if c_stop == 0 or \ - c_start >= c_stop and (c_stop > 0 or c_start < 0): - raise ValueError, "list.index(x): x not in slice" - - # for negative slice indices, check slice before searching index - if c_start < 0 or c_stop < 0: - # start from right, at most up to leftmost(c_start, c_stop) - if c_start < c_stop: - k = -c_start - else: - k = -c_stop - c_start_node = self._c_node.last - l = 1 - while c_start_node != c_child and l < k: - if _isElement(c_start_node): - l += 1 - c_start_node = c_start_node.prev - if c_start_node == c_child: - # found! before slice end? - if c_stop < 0 and l <= -c_stop: - raise ValueError, "list.index(x): x not in slice" - elif c_start < 0: - raise ValueError, "list.index(x): x not in slice" - - # now determine the index backwards from child - c_child = c_child.prev - k = 0 - if c_stop > 0: - # we can optimize: stop after c_stop elements if not found - while c_child != NULL and k < c_stop: - if _isElement(c_child): - k += 1 - c_child = c_child.prev - if k < c_stop: - return k - else: - # traverse all - while c_child != NULL: - if _isElement(c_child): - k = k + 1 - c_child = c_child.prev - if c_start > 0: - if k >= c_start: - return k - else: - return k - if c_start != 0 or c_stop != 0: - raise ValueError, "list.index(x): x not in slice" - else: - raise ValueError, "list.index(x): x not in list" - - def get(self, key, default=None): - """get(self, key, default=None) - - Gets an element attribute. - """ - _assertValidNode(self) - return _getAttributeValue(self, key, default) - - def keys(self): - """keys(self) - - Gets a list of attribute names. The names are returned in an - arbitrary order (just like for an ordinary Python dictionary). - """ - _assertValidNode(self) - return _collectAttributes(self._c_node, 1) - - def values(self): - """values(self) - - Gets element attribute values as a sequence of strings. The - attributes are returned in an arbitrary order. - """ - _assertValidNode(self) - return _collectAttributes(self._c_node, 2) - - def items(self): - """items(self) - - Gets element attributes, as a sequence. The attributes are returned in - an arbitrary order. - """ - _assertValidNode(self) - return _collectAttributes(self._c_node, 3) - - def getchildren(self): - """getchildren(self) - - Returns all direct children. The elements are returned in document - order. - - :deprecated: Note that this method has been deprecated as of - ElementTree 1.3 and lxml 2.0. New code should use - ``list(element)`` or simply iterate over elements. - """ - _assertValidNode(self) - return _collectChildren(self) - - def getparent(self): - """getparent(self) - - Returns the parent of this element or None for the root element. - """ - cdef xmlNode* c_node - #_assertValidNode(self) # not needed - c_node = _parentElement(self._c_node) - if c_node is NULL: - return None - return _elementFactory(self._doc, c_node) - - def getnext(self): - """getnext(self) - - Returns the following sibling of this element or None. - """ - cdef xmlNode* c_node - #_assertValidNode(self) # not needed - c_node = _nextElement(self._c_node) - if c_node is NULL: - return None - return _elementFactory(self._doc, c_node) - - def getprevious(self): - """getprevious(self) - - Returns the preceding sibling of this element or None. - """ - cdef xmlNode* c_node - #_assertValidNode(self) # not needed - c_node = _previousElement(self._c_node) - if c_node is NULL: - return None - return _elementFactory(self._doc, c_node) - - def itersiblings(self, tag=None, *tags, preceding=False): - """itersiblings(self, tag=None, *tags, preceding=False) - - Iterate over the following or preceding siblings of this element. - - The direction is determined by the 'preceding' keyword which - defaults to False, i.e. forward iteration over the following - siblings. When True, the iterator yields the preceding - siblings in reverse document order, i.e. starting right before - the current element and going backwards. - - Can be restricted to find only elements with specific tags, - see `iter`. - """ - if preceding: - if self._c_node and not self._c_node.prev: - return ITER_EMPTY - elif self._c_node and not self._c_node.next: - return ITER_EMPTY - if tag is not None: - tags += (tag,) - return SiblingsIterator(self, tags, preceding=preceding) - - def iterancestors(self, tag=None, *tags): - """iterancestors(self, tag=None, *tags) - - Iterate over the ancestors of this element (from parent to parent). - - Can be restricted to find only elements with specific tags, - see `iter`. - """ - if self._c_node and not self._c_node.parent: - return ITER_EMPTY - if tag is not None: - tags += (tag,) - return AncestorsIterator(self, tags) - - def iterdescendants(self, tag=None, *tags): - """iterdescendants(self, tag=None, *tags) - - Iterate over the descendants of this element in document order. - - As opposed to ``el.iter()``, this iterator does not yield the element - itself. The returned elements can be restricted to find only elements - with specific tags, see `iter`. - """ - if self._c_node and not self._c_node.children: - return ITER_EMPTY - if tag is not None: - tags += (tag,) - return ElementDepthFirstIterator(self, tags, inclusive=False) - - def iterchildren(self, tag=None, *tags, reversed=False): - """iterchildren(self, tag=None, *tags, reversed=False) - - Iterate over the children of this element. - - As opposed to using normal iteration on this element, the returned - elements can be reversed with the 'reversed' keyword and restricted - to find only elements with specific tags, see `iter`. - """ - if self._c_node and not self._c_node.children: - return ITER_EMPTY - if tag is not None: - tags += (tag,) - return ElementChildIterator(self, tags, reversed=reversed) - - def getroottree(self): - """getroottree(self) - - Return an ElementTree for the root node of the document that - contains this element. - - This is the same as following element.getparent() up the tree until it - returns None (for the root element) and then build an ElementTree for - the last parent that was returned.""" - _assertValidDoc(self._doc) - return _elementTreeFactory(self._doc, None) - - def getiterator(self, tag=None, *tags): - """getiterator(self, tag=None, *tags) - - Returns a sequence or iterator of all elements in the subtree in - document order (depth first pre-order), starting with this - element. - - Can be restricted to find only elements with specific tags, - see `iter`. - - :deprecated: Note that this method is deprecated as of - ElementTree 1.3 and lxml 2.0. It returns an iterator in - lxml, which diverges from the original ElementTree - behaviour. If you want an efficient iterator, use the - ``element.iter()`` method instead. You should only use this - method in new code if you require backwards compatibility - with older versions of lxml or ElementTree. - """ - if tag is not None: - tags += (tag,) - return ElementDepthFirstIterator(self, tags) - - def iter(self, tag=None, *tags): - """iter(self, tag=None, *tags) - - Iterate over all elements in the subtree in document order (depth - first pre-order), starting with this element. - - Can be restricted to find only elements with specific tags: - pass ``"{ns}localname"`` as tag. Either or both of ``ns`` and - ``localname`` can be ``*`` for a wildcard; ``ns`` can be empty - for no namespace. ``"localname"`` is equivalent to ``"{}localname"`` - (i.e. no namespace) but ``"*"`` is ``"{*}*"`` (any or no namespace), - not ``"{}*"``. - - You can also pass the Element, Comment, ProcessingInstruction and - Entity factory functions to look only for the specific element type. - - Passing multiple tags (or a sequence of tags) instead of a single tag - will let the iterator return all elements matching any of these tags, - in document order. - """ - if tag is not None: - tags += (tag,) - return ElementDepthFirstIterator(self, tags) - - def itertext(self, tag=None, *tags, with_tail=True): - """itertext(self, tag=None, *tags, with_tail=True) - - Iterates over the text content of a subtree. - - You can pass tag names to restrict text content to specific elements, - see `iter`. - - You can set the ``with_tail`` keyword argument to ``False`` to skip - over tail text. - """ - if tag is not None: - tags += (tag,) - return ElementTextIterator(self, tags, with_tail=with_tail) - - def makeelement(self, _tag, attrib=None, nsmap=None, **_extra): - """makeelement(self, _tag, attrib=None, nsmap=None, **_extra) - - Creates a new element associated with the same document. - """ - _assertValidDoc(self._doc) - return _makeElement(_tag, NULL, self._doc, None, None, None, - attrib, nsmap, _extra) - - def find(self, path, namespaces=None): - """find(self, path, namespaces=None) - - Finds the first matching subelement, by tag name or path. - - The optional ``namespaces`` argument accepts a - prefix-to-namespace mapping that allows the usage of XPath - prefixes in the path expression. - """ - if isinstance(path, QName): - path = (path).text - return _elementpath.find(self, path, namespaces, with_prefixes=not _isHtmlDocument(self)) - - def findtext(self, path, default=None, namespaces=None): - """findtext(self, path, default=None, namespaces=None) - - Finds text for the first matching subelement, by tag name or path. - - The optional ``namespaces`` argument accepts a - prefix-to-namespace mapping that allows the usage of XPath - prefixes in the path expression. - """ - if isinstance(path, QName): - path = (path).text - return _elementpath.findtext(self, path, default, namespaces, with_prefixes=not _isHtmlDocument(self)) - - def findall(self, path, namespaces=None): - """findall(self, path, namespaces=None) - - Finds all matching subelements, by tag name or path. - - The optional ``namespaces`` argument accepts a - prefix-to-namespace mapping that allows the usage of XPath - prefixes in the path expression. - """ - if isinstance(path, QName): - path = (path).text - return _elementpath.findall(self, path, namespaces, with_prefixes=not _isHtmlDocument(self)) - - def iterfind(self, path, namespaces=None): - """iterfind(self, path, namespaces=None) - - Iterates over all matching subelements, by tag name or path. - - The optional ``namespaces`` argument accepts a - prefix-to-namespace mapping that allows the usage of XPath - prefixes in the path expression. - """ - if isinstance(path, QName): - path = (path).text - return _elementpath.iterfind(self, path, namespaces, with_prefixes=not _isHtmlDocument(self)) - - def xpath(self, _path, *, namespaces=None, extensions=None, - smart_strings=True, **_variables): - """xpath(self, _path, namespaces=None, extensions=None, smart_strings=True, **_variables) - - Evaluate an xpath expression using the element as context node. - """ - evaluator = XPathElementEvaluator(self, namespaces=namespaces, - extensions=extensions, - smart_strings=smart_strings) - return evaluator(_path, **_variables) - - def cssselect(self, expr, *, translator='xml'): - """ - Run the CSS expression on this element and its children, - returning a list of the results. - - Equivalent to lxml.cssselect.CSSSelect(expr)(self) -- note - that pre-compiling the expression can provide a substantial - speedup. - """ - # Do the import here to make the dependency optional. - from lxml.cssselect import CSSSelector - return CSSSelector(expr, translator=translator)(self) - - -@cython.linetrace(False) -cdef _Element _elementFactory(_Document doc, xmlNode* c_node): - cdef _Element result - result = getProxy(c_node) - if result is not None: - return result - if c_node is NULL: - return None - - element_class = LOOKUP_ELEMENT_CLASS( - ELEMENT_CLASS_LOOKUP_STATE, doc, c_node) - if type(element_class) is not type: - if not isinstance(element_class, type): - raise TypeError(f"Element class is not a type, got {type(element_class)}") - if hasProxy(c_node): - # prevent re-entry race condition - we just called into Python - return getProxy(c_node) - result = element_class.__new__(element_class) - if hasProxy(c_node): - # prevent re-entry race condition - we just called into Python - result._c_node = NULL - return getProxy(c_node) - - _registerProxy(result, doc, c_node) - if element_class is not _Element: - result._init() - return result - - -@cython.internal -cdef class __ContentOnlyElement(_Element): - cdef int _raiseImmutable(self) except -1: - raise TypeError, "this element does not have children or attributes" - - def set(self, key, value): - "set(self, key, value)" - self._raiseImmutable() - - def append(self, value): - "append(self, value)" - self._raiseImmutable() - - def insert(self, index, value): - "insert(self, index, value)" - self._raiseImmutable() - - def __setitem__(self, index, value): - "__setitem__(self, index, value)" - self._raiseImmutable() - - @property - def attrib(self): - return IMMUTABLE_EMPTY_MAPPING - - property text: - def __get__(self): - _assertValidNode(self) - return funicodeOrEmpty(self._c_node.content) - - def __set__(self, value): - cdef tree.xmlDict* c_dict - _assertValidNode(self) - if value is None: - c_text = NULL - else: - value = _utf8(value) - c_text = _xcstr(value) - tree.xmlNodeSetContent(self._c_node, c_text) - - # ACCESSORS - def __getitem__(self, x): - "__getitem__(self, x)" - if isinstance(x, slice): - return [] - else: - raise IndexError, "list index out of range" - - def __len__(self): - "__len__(self)" - return 0 - - def get(self, key, default=None): - "get(self, key, default=None)" - return None - - def keys(self): - "keys(self)" - return [] - - def items(self): - "items(self)" - return [] - - def values(self): - "values(self)" - return [] - -cdef class _Comment(__ContentOnlyElement): - @property - def tag(self): - return Comment - - def __repr__(self): - return "" % self.text - -cdef class _ProcessingInstruction(__ContentOnlyElement): - @property - def tag(self): - return ProcessingInstruction - - property target: - # not in ElementTree - def __get__(self): - _assertValidNode(self) - return funicode(self._c_node.name) - - def __set__(self, value): - _assertValidNode(self) - value = _utf8(value) - c_text = _xcstr(value) - tree.xmlNodeSetName(self._c_node, c_text) - - def __repr__(self): - text = self.text - if text: - return "" % (self.target, text) - else: - return "" % self.target - - def get(self, key, default=None): - """get(self, key, default=None) - - Try to parse pseudo-attributes from the text content of the - processing instruction, search for one with the given key as - name and return its associated value. - - Note that this is only a convenience method for the most - common case that all text content is structured in - attribute-like name-value pairs with properly quoted values. - It is not guaranteed to work for all possible text content. - """ - return self.attrib.get(key, default) - - @property - def attrib(self): - """Returns a dict containing all pseudo-attributes that can be - parsed from the text content of this processing instruction. - Note that modifying the dict currently has no effect on the - XML node, although this is not guaranteed to stay this way. - """ - return { attr : (value1 or value2) - for attr, value1, value2 in _FIND_PI_ATTRIBUTES(' ' + self.text) } - -cdef object _FIND_PI_ATTRIBUTES = re.compile(r'\s+(\w+)\s*=\s*(?:\'([^\']*)\'|"([^"]*)")', re.U).findall - -cdef class _Entity(__ContentOnlyElement): - @property - def tag(self): - return Entity - - property name: - # not in ElementTree - def __get__(self): - _assertValidNode(self) - return funicode(self._c_node.name) - - def __set__(self, value): - _assertValidNode(self) - value_utf = _utf8(value) - if b'&' in value_utf or b';' in value_utf: - raise ValueError, f"Invalid entity name '{value}'" - tree.xmlNodeSetName(self._c_node, _xcstr(value_utf)) - - @property - def text(self): - # FIXME: should this be None or '&[VALUE];' or the resolved - # entity value ? - _assertValidNode(self) - return f'&{funicode(self._c_node.name)};' - - def __repr__(self): - return "&%s;" % self.name - - -cdef class QName: - """QName(text_or_uri_or_element, tag=None) - - QName wrapper for qualified XML names. - - Pass a tag name by itself or a namespace URI and a tag name to - create a qualified name. Alternatively, pass an Element to - extract its tag name. ``None`` as first argument is ignored in - order to allow for generic 2-argument usage. - - The ``text`` property holds the qualified name in - ``{namespace}tagname`` notation. The ``namespace`` and - ``localname`` properties hold the respective parts of the tag - name. - - You can pass QName objects wherever a tag name is expected. Also, - setting Element text from a QName will resolve the namespace prefix - on assignment and set a qualified text value. This is helpful in XML - languages like SOAP or XML-Schema that use prefixed tag names in - their text content. - """ - cdef readonly unicode text - cdef readonly unicode localname - cdef readonly unicode namespace - def __init__(self, text_or_uri_or_element, tag=None): - if text_or_uri_or_element is None: - # Allow None as no namespace. - text_or_uri_or_element, tag = tag, None - if not _isString(text_or_uri_or_element): - if isinstance(text_or_uri_or_element, _Element): - text_or_uri_or_element = (<_Element>text_or_uri_or_element).tag - if not _isString(text_or_uri_or_element): - raise ValueError, f"Invalid input tag of type {type(text_or_uri_or_element)!r}" - elif isinstance(text_or_uri_or_element, QName): - text_or_uri_or_element = (text_or_uri_or_element).text - elif text_or_uri_or_element is not None: - text_or_uri_or_element = unicode(text_or_uri_or_element) - else: - raise ValueError, f"Invalid input tag of type {type(text_or_uri_or_element)!r}" - - ns_utf, tag_utf = _getNsTag(text_or_uri_or_element) - if tag is not None: - # either ('ns', 'tag') or ('{ns}oldtag', 'newtag') - if ns_utf is None: - ns_utf = tag_utf # case 1: namespace ended up as tag name - tag_utf = _utf8(tag) - _tagValidOrRaise(tag_utf) - self.localname = (tag_utf).decode('utf8') - if ns_utf is None: - self.namespace = None - self.text = self.localname - else: - self.namespace = (ns_utf).decode('utf8') - self.text = "{%s}%s" % (self.namespace, self.localname) - def __str__(self): - return self.text - def __hash__(self): - return hash(self.text) - def __richcmp__(self, other, int op): - try: - if type(other) is QName: - other = (other).text - elif not isinstance(other, unicode): - other = unicode(other) - except (ValueError, UnicodeDecodeError): - return NotImplemented - return python.PyObject_RichCompare(self.text, other, op) - - -cdef public class _ElementTree [ type LxmlElementTreeType, - object LxmlElementTree ]: - cdef _Document _doc - cdef _Element _context_node - - # Note that _doc is only used to store the original document if we do not - # have a _context_node. All methods should prefer self._context_node._doc - # to honour tree restructuring. _doc can happily be None! - - @cython.final - cdef int _assertHasRoot(self) except -1: - """We have to take care here: the document may not have a root node! - This can happen if ElementTree() is called without any argument and - the caller 'forgets' to call parse() afterwards, so this is a bug in - the caller program. - """ - assert self._context_node is not None, \ - "ElementTree not initialized, missing root" - return 0 - - def parse(self, source, _BaseParser parser=None, *, base_url=None): - """parse(self, source, parser=None, base_url=None) - - Updates self with the content of source and returns its root. - """ - cdef _Document doc = None - try: - doc = _parseDocument(source, parser, base_url) - except _TargetParserResult as result_container: - # raises a TypeError if we don't get an _Element - self._context_node = result_container.result - else: - self._context_node = doc.getroot() - self._doc = None if self._context_node is not None else doc - return self._context_node - - def _setroot(self, _Element root not None): - """_setroot(self, root) - - Relocate the ElementTree to a new root node. - """ - _assertValidNode(root) - if root._c_node.type != tree.XML_ELEMENT_NODE: - raise TypeError, "Only elements can be the root of an ElementTree" - self._context_node = root - self._doc = None - - def getroot(self): - """getroot(self) - - Gets the root element for this tree. - """ - return self._context_node - - def __copy__(self): - return _elementTreeFactory(self._doc, self._context_node) - - def __deepcopy__(self, memo): - cdef _Element root - cdef _Document doc - cdef xmlDoc* c_doc - if self._context_node is not None: - root = self._context_node.__copy__() - assert root is not None - _assertValidNode(root) - _copyNonElementSiblings(self._context_node._c_node, root._c_node) - return _elementTreeFactory(None, root) - elif self._doc is not None: - _assertValidDoc(self._doc) - c_doc = tree.xmlCopyDoc(self._doc._c_doc, 1) - if c_doc is NULL: - raise MemoryError() - doc = _documentFactory(c_doc, self._doc._parser) - return _elementTreeFactory(doc, None) - else: - # so what ... - return self - - # not in ElementTree - @property - def docinfo(self) -> DocInfo: - """Information about the document provided by parser and DTD.""" - self._assertHasRoot() - return DocInfo(self._context_node._doc) - - # not in ElementTree, read-only - @property - def parser(self): - """The parser that was used to parse the document in this ElementTree. - """ - if self._context_node is not None and \ - self._context_node._doc is not None: - return self._context_node._doc._parser - if self._doc is not None: - return self._doc._parser - return None - - def write(self, file, *, encoding=None, method="xml", - bint pretty_print=False, xml_declaration=None, bint with_tail=True, - standalone=None, doctype=None, compression=0, - bint exclusive=False, inclusive_ns_prefixes=None, - bint with_comments=True, bint strip_text=False, - docstring=None): - """write(self, file, encoding=None, method="xml", - pretty_print=False, xml_declaration=None, with_tail=True, - standalone=None, doctype=None, compression=0, - exclusive=False, inclusive_ns_prefixes=None, - with_comments=True, strip_text=False) - - Write the tree to a filename, file or file-like object. - - Defaults to ASCII encoding and writing a declaration as needed. - - The keyword argument 'method' selects the output method: - 'xml', 'html', 'text', 'c14n' or 'c14n2'. Default is 'xml'. - - With ``method="c14n"`` (C14N version 1), the options ``exclusive``, - ``with_comments`` and ``inclusive_ns_prefixes`` request exclusive - C14N, include comments, and list the inclusive prefixes respectively. - - With ``method="c14n2"`` (C14N version 2), the ``with_comments`` and - ``strip_text`` options control the output of comments and text space - according to C14N 2.0. - - Passing a boolean value to the ``standalone`` option will - output an XML declaration with the corresponding - ``standalone`` flag. - - The ``doctype`` option allows passing in a plain string that will - be serialised before the XML tree. Note that passing in non - well-formed content here will make the XML output non well-formed. - Also, an existing doctype in the document tree will not be removed - when serialising an ElementTree instance. - - The ``compression`` option enables GZip compression level 1-9. - - The ``inclusive_ns_prefixes`` should be a list of namespace strings - (i.e. ['xs', 'xsi']) that will be promoted to the top-level element - during exclusive C14N serialisation. This parameter is ignored if - exclusive mode=False. - - If exclusive=True and no list is provided, a namespace will only be - rendered if it is used by the immediate parent or one of its attributes - and its prefix and values have not already been rendered by an ancestor - of the namespace node's parent element. - """ - cdef bint write_declaration - cdef int is_standalone - - self._assertHasRoot() - _assertValidNode(self._context_node) - if compression is None or compression < 0: - compression = 0 - - # C14N serialisation - if method in ('c14n', 'c14n2'): - if encoding is not None: - raise ValueError("Cannot specify encoding with C14N") - if xml_declaration: - raise ValueError("Cannot enable XML declaration in C14N") - - if method == 'c14n': - _tofilelikeC14N(file, self._context_node, exclusive, with_comments, - compression, inclusive_ns_prefixes) - else: # c14n2 - with _open_utf8_file(file, compression=compression) as f: - target = C14NWriterTarget( - f.write, with_comments=with_comments, strip_text=strip_text) - _tree_to_target(self, target) - return - - if not with_comments: - raise ValueError("Can only discard comments in C14N serialisation") - # suppress decl. in default case (purely for ElementTree compatibility) - if xml_declaration is not None: - write_declaration = xml_declaration - if encoding is None: - encoding = 'ASCII' - else: - encoding = encoding.upper() - elif encoding is None: - encoding = 'ASCII' - write_declaration = 0 - else: - encoding = encoding.upper() - write_declaration = encoding not in ( - 'US-ASCII', 'ASCII', 'UTF8', 'UTF-8') - if standalone is None: - is_standalone = -1 - elif standalone: - write_declaration = 1 - is_standalone = 1 - else: - write_declaration = 1 - is_standalone = 0 - - if docstring is not None and doctype is None: - import warnings - warnings.warn( - "The 'docstring' option is deprecated. Use 'doctype' instead.", - DeprecationWarning) - doctype = docstring - - _tofilelike(file, self._context_node, encoding, doctype, method, - write_declaration, 1, pretty_print, with_tail, - is_standalone, compression) - - def getpath(self, _Element element not None): - """getpath(self, element) - - Returns a structural, absolute XPath expression to find the element. - - For namespaced elements, the expression uses prefixes from the - document, which therefore need to be provided in order to make any - use of the expression in XPath. - - Also see the method getelementpath(self, element), which returns a - self-contained ElementPath expression. - """ - cdef _Document doc - cdef _Element root - cdef xmlDoc* c_doc - _assertValidNode(element) - if self._context_node is not None: - root = self._context_node - doc = root._doc - elif self._doc is not None: - doc = self._doc - root = doc.getroot() - else: - raise ValueError, "Element is not in this tree." - _assertValidDoc(doc) - _assertValidNode(root) - if element._doc is not doc: - raise ValueError, "Element is not in this tree." - - c_doc = _fakeRootDoc(doc._c_doc, root._c_node) - c_path = tree.xmlGetNodePath(element._c_node) - _destroyFakeDoc(doc._c_doc, c_doc) - if c_path is NULL: - raise MemoryError() - path = funicode(c_path) - tree.xmlFree(c_path) - return path - - def getelementpath(self, _Element element not None): - """getelementpath(self, element) - - Returns a structural, absolute ElementPath expression to find the - element. This path can be used in the .find() method to look up - the element, provided that the elements along the path and their - list of immediate children were not modified in between. - - ElementPath has the advantage over an XPath expression (as returned - by the .getpath() method) that it does not require additional prefix - declarations. It is always self-contained. - """ - cdef _Element root - cdef Py_ssize_t count - _assertValidNode(element) - if element._c_node.type != tree.XML_ELEMENT_NODE: - raise ValueError, "input is not an Element" - if self._context_node is not None: - root = self._context_node - elif self._doc is not None: - root = self._doc.getroot() - else: - raise ValueError, "Element is not in this tree" - _assertValidNode(root) - if element._doc is not root._doc: - raise ValueError, "Element is not in this tree" - - path = [] - c_element = element._c_node - while c_element is not root._c_node: - c_name = c_element.name - c_href = _getNs(c_element) - tag = _namespacedNameFromNsName(c_href, c_name) - if c_href is NULL: - c_href = b'' # no namespace (NULL is wildcard) - # use tag[N] if there are preceding siblings with the same tag - count = 0 - c_node = c_element.prev - while c_node is not NULL: - if c_node.type == tree.XML_ELEMENT_NODE: - if _tagMatches(c_node, c_href, c_name): - count += 1 - c_node = c_node.prev - if count: - tag = f'{tag}[{count+1}]' - else: - # use tag[1] if there are following siblings with the same tag - c_node = c_element.next - while c_node is not NULL: - if c_node.type == tree.XML_ELEMENT_NODE: - if _tagMatches(c_node, c_href, c_name): - tag += '[1]' - break - c_node = c_node.next - - path.append(tag) - c_element = c_element.parent - if c_element is NULL or c_element.type != tree.XML_ELEMENT_NODE: - raise ValueError, "Element is not in this tree." - if not path: - return '.' - path.reverse() - return '/'.join(path) - - def getiterator(self, tag=None, *tags): - """getiterator(self, *tags, tag=None) - - Returns a sequence or iterator of all elements in document order - (depth first pre-order), starting with the root element. - - Can be restricted to find only elements with specific tags, - see `_Element.iter`. - - :deprecated: Note that this method is deprecated as of - ElementTree 1.3 and lxml 2.0. It returns an iterator in - lxml, which diverges from the original ElementTree - behaviour. If you want an efficient iterator, use the - ``tree.iter()`` method instead. You should only use this - method in new code if you require backwards compatibility - with older versions of lxml or ElementTree. - """ - root = self.getroot() - if root is None: - return ITER_EMPTY - if tag is not None: - tags += (tag,) - return root.getiterator(*tags) - - def iter(self, tag=None, *tags): - """iter(self, tag=None, *tags) - - Creates an iterator for the root element. The iterator loops over - all elements in this tree, in document order. Note that siblings - of the root element (comments or processing instructions) are not - returned by the iterator. - - Can be restricted to find only elements with specific tags, - see `_Element.iter`. - """ - root = self.getroot() - if root is None: - return ITER_EMPTY - if tag is not None: - tags += (tag,) - return root.iter(*tags) - - def find(self, path, namespaces=None): - """find(self, path, namespaces=None) - - Finds the first toplevel element with given tag. Same as - ``tree.getroot().find(path)``. - - The optional ``namespaces`` argument accepts a - prefix-to-namespace mapping that allows the usage of XPath - prefixes in the path expression. - """ - self._assertHasRoot() - root = self.getroot() - if _isString(path): - if path[:1] == "/": - path = "." + path - from warnings import warn - warn( - "This search incorrectly ignores the root element, and will be " - "fixed in a future version. If you rely on the current " - f"behaviour, change it to {path!r}", - FutureWarning, stacklevel=1 - ) - return root.find(path, namespaces) - - def findtext(self, path, default=None, namespaces=None): - """findtext(self, path, default=None, namespaces=None) - - Finds the text for the first element matching the ElementPath - expression. Same as getroot().findtext(path) - - The optional ``namespaces`` argument accepts a - prefix-to-namespace mapping that allows the usage of XPath - prefixes in the path expression. - """ - self._assertHasRoot() - root = self.getroot() - if _isString(path): - if path[:1] == "/": - path = "." + path - from warnings import warn - warn( - "This search incorrectly ignores the root element, and will be " - "fixed in a future version. If you rely on the current " - f"behaviour, change it to {path!r}", - FutureWarning, stacklevel=1 - ) - return root.findtext(path, default, namespaces) - - def findall(self, path, namespaces=None): - """findall(self, path, namespaces=None) - - Finds all elements matching the ElementPath expression. Same as - getroot().findall(path). - - The optional ``namespaces`` argument accepts a - prefix-to-namespace mapping that allows the usage of XPath - prefixes in the path expression. - """ - self._assertHasRoot() - root = self.getroot() - if _isString(path): - if path[:1] == "/": - path = "." + path - from warnings import warn - warn( - "This search incorrectly ignores the root element, and will be " - "fixed in a future version. If you rely on the current " - f"behaviour, change it to {path!r}", - FutureWarning, stacklevel=1 - ) - return root.findall(path, namespaces) - - def iterfind(self, path, namespaces=None): - """iterfind(self, path, namespaces=None) - - Iterates over all elements matching the ElementPath expression. - Same as getroot().iterfind(path). - - The optional ``namespaces`` argument accepts a - prefix-to-namespace mapping that allows the usage of XPath - prefixes in the path expression. - """ - self._assertHasRoot() - root = self.getroot() - if _isString(path): - if path[:1] == "/": - path = "." + path - from warnings import warn - warn( - "This search incorrectly ignores the root element, and will be " - "fixed in a future version. If you rely on the current " - f"behaviour, change it to {path!r}", - FutureWarning, stacklevel=1 - ) - return root.iterfind(path, namespaces) - - def xpath(self, _path, *, namespaces=None, extensions=None, - smart_strings=True, **_variables): - """xpath(self, _path, namespaces=None, extensions=None, smart_strings=True, **_variables) - - XPath evaluate in context of document. - - ``namespaces`` is an optional dictionary with prefix to namespace URI - mappings, used by XPath. ``extensions`` defines additional extension - functions. - - Returns a list (nodeset), or bool, float or string. - - In case of a list result, return Element for element nodes, - string for text and attribute values. - - Note: if you are going to apply multiple XPath expressions - against the same document, it is more efficient to use - XPathEvaluator directly. - """ - self._assertHasRoot() - evaluator = XPathDocumentEvaluator(self, namespaces=namespaces, - extensions=extensions, - smart_strings=smart_strings) - return evaluator(_path, **_variables) - - def xslt(self, _xslt, extensions=None, access_control=None, **_kw): - """xslt(self, _xslt, extensions=None, access_control=None, **_kw) - - Transform this document using other document. - - xslt is a tree that should be XSLT - keyword parameters are XSLT transformation parameters. - - Returns the transformed tree. - - Note: if you are going to apply the same XSLT stylesheet against - multiple documents, it is more efficient to use the XSLT - class directly. - """ - self._assertHasRoot() - style = XSLT(_xslt, extensions=extensions, - access_control=access_control) - return style(self, **_kw) - - def relaxng(self, relaxng): - """relaxng(self, relaxng) - - Validate this document using other document. - - The relaxng argument is a tree that should contain a Relax NG schema. - - Returns True or False, depending on whether validation - succeeded. - - Note: if you are going to apply the same Relax NG schema against - multiple documents, it is more efficient to use the RelaxNG - class directly. - """ - self._assertHasRoot() - schema = RelaxNG(relaxng) - return schema.validate(self) - - def xmlschema(self, xmlschema): - """xmlschema(self, xmlschema) - - Validate this document using other document. - - The xmlschema argument is a tree that should contain an XML Schema. - - Returns True or False, depending on whether validation - succeeded. - - Note: If you are going to apply the same XML Schema against - multiple documents, it is more efficient to use the XMLSchema - class directly. - """ - self._assertHasRoot() - schema = XMLSchema(xmlschema) - return schema.validate(self) - - def xinclude(self): - """xinclude(self) - - Process the XInclude nodes in this document and include the - referenced XML fragments. - - There is support for loading files through the file system, HTTP and - FTP. - - Note that XInclude does not support custom resolvers in Python space - due to restrictions of libxml2 <= 2.6.29. - """ - self._assertHasRoot() - XInclude()(self._context_node) - - def write_c14n(self, file, *, bint exclusive=False, bint with_comments=True, - compression=0, inclusive_ns_prefixes=None): - """write_c14n(self, file, exclusive=False, with_comments=True, - compression=0, inclusive_ns_prefixes=None) - - C14N write of document. Always writes UTF-8. - - The ``compression`` option enables GZip compression level 1-9. - - The ``inclusive_ns_prefixes`` should be a list of namespace strings - (i.e. ['xs', 'xsi']) that will be promoted to the top-level element - during exclusive C14N serialisation. This parameter is ignored if - exclusive mode=False. - - If exclusive=True and no list is provided, a namespace will only be - rendered if it is used by the immediate parent or one of its attributes - and its prefix and values have not already been rendered by an ancestor - of the namespace node's parent element. - - NOTE: This method is deprecated as of lxml 4.4 and will be removed in a - future release. Use ``.write(f, method="c14n")`` instead. - """ - self._assertHasRoot() - _assertValidNode(self._context_node) - if compression is None or compression < 0: - compression = 0 - - _tofilelikeC14N(file, self._context_node, exclusive, with_comments, - compression, inclusive_ns_prefixes) - -cdef _ElementTree _elementTreeFactory(_Document doc, _Element context_node): - return _newElementTree(doc, context_node, _ElementTree) - -cdef _ElementTree _newElementTree(_Document doc, _Element context_node, - object baseclass): - cdef _ElementTree result - result = baseclass() - if context_node is None and doc is not None: - context_node = doc.getroot() - if context_node is None: - _assertValidDoc(doc) - result._doc = doc - else: - _assertValidNode(context_node) - result._context_node = context_node - return result - - -@cython.final -@cython.freelist(16) -cdef class _Attrib: - """A dict-like proxy for the ``Element.attrib`` property. - """ - cdef _Element _element - def __cinit__(self, _Element element not None): - _assertValidNode(element) - self._element = element - - # MANIPULATORS - def __setitem__(self, key, value): - _assertValidNode(self._element) - _setAttributeValue(self._element, key, value) - - def __delitem__(self, key): - _assertValidNode(self._element) - _delAttribute(self._element, key) - - def update(self, sequence_or_dict): - _assertValidNode(self._element) - if isinstance(sequence_or_dict, (dict, _Attrib)): - sequence_or_dict = sequence_or_dict.items() - for key, value in sequence_or_dict: - _setAttributeValue(self._element, key, value) - - def pop(self, key, *default): - if len(default) > 1: - raise TypeError, f"pop expected at most 2 arguments, got {len(default)+1}" - _assertValidNode(self._element) - result = _getAttributeValue(self._element, key, None) - if result is None: - if not default: - raise KeyError, key - result = default[0] - else: - _delAttribute(self._element, key) - return result - - def clear(self): - _assertValidNode(self._element) - c_attrs = self._element._c_node.properties - if c_attrs: - self._element._c_node.properties = NULL - tree.xmlFreePropList(c_attrs) - - # ACCESSORS - def __repr__(self): - _assertValidNode(self._element) - return repr(dict( _collectAttributes(self._element._c_node, 3) )) - - def __copy__(self): - _assertValidNode(self._element) - return dict(_collectAttributes(self._element._c_node, 3)) - - def __deepcopy__(self, memo): - _assertValidNode(self._element) - return dict(_collectAttributes(self._element._c_node, 3)) - - def __getitem__(self, key): - _assertValidNode(self._element) - result = _getAttributeValue(self._element, key, None) - if result is None: - raise KeyError, key - return result - - def __bool__(self): - _assertValidNode(self._element) - cdef xmlAttr* c_attr = self._element._c_node.properties - while c_attr is not NULL: - if c_attr.type == tree.XML_ATTRIBUTE_NODE: - return 1 - c_attr = c_attr.next - return 0 - - def __len__(self): - _assertValidNode(self._element) - cdef xmlAttr* c_attr = self._element._c_node.properties - cdef Py_ssize_t c = 0 - while c_attr is not NULL: - if c_attr.type == tree.XML_ATTRIBUTE_NODE: - c += 1 - c_attr = c_attr.next - return c - - def get(self, key, default=None): - _assertValidNode(self._element) - return _getAttributeValue(self._element, key, default) - - def keys(self): - _assertValidNode(self._element) - return _collectAttributes(self._element._c_node, 1) - - def __iter__(self): - _assertValidNode(self._element) - return iter(_collectAttributes(self._element._c_node, 1)) - - def iterkeys(self): - _assertValidNode(self._element) - return iter(_collectAttributes(self._element._c_node, 1)) - - def values(self): - _assertValidNode(self._element) - return _collectAttributes(self._element._c_node, 2) - - def itervalues(self): - _assertValidNode(self._element) - return iter(_collectAttributes(self._element._c_node, 2)) - - def items(self): - _assertValidNode(self._element) - return _collectAttributes(self._element._c_node, 3) - - def iteritems(self): - _assertValidNode(self._element) - return iter(_collectAttributes(self._element._c_node, 3)) - - def has_key(self, key): - _assertValidNode(self._element) - return key in self - - def __contains__(self, key): - _assertValidNode(self._element) - cdef xmlNode* c_node - ns, tag = _getNsTag(key) - c_node = self._element._c_node - c_href = NULL if ns is None else _xcstr(ns) - return 1 if tree.xmlHasNsProp(c_node, _xcstr(tag), c_href) else 0 - - def __richcmp__(self, other, int op): - try: - one = dict(self.items()) - if not isinstance(other, dict): - other = dict(other) - except (TypeError, ValueError): - return NotImplemented - return python.PyObject_RichCompare(one, other, op) - -MutableMapping.register(_Attrib) - - -@cython.final -@cython.internal -cdef class _AttribIterator: - """Attribute iterator - for internal use only! - """ - # XML attributes must not be removed while running! - cdef _Element _node - cdef xmlAttr* _c_attr - cdef int _keysvalues # 1 - keys, 2 - values, 3 - items (key, value) - def __iter__(self): - return self - - def __next__(self): - cdef xmlAttr* c_attr - if self._node is None: - raise StopIteration - c_attr = self._c_attr - while c_attr is not NULL and c_attr.type != tree.XML_ATTRIBUTE_NODE: - c_attr = c_attr.next - if c_attr is NULL: - self._node = None - raise StopIteration - - self._c_attr = c_attr.next - if self._keysvalues == 1: - return _namespacedName(c_attr) - elif self._keysvalues == 2: - return _attributeValue(self._node._c_node, c_attr) - else: - return (_namespacedName(c_attr), - _attributeValue(self._node._c_node, c_attr)) - -cdef object _attributeIteratorFactory(_Element element, int keysvalues): - cdef _AttribIterator attribs - if element._c_node.properties is NULL: - return ITER_EMPTY - attribs = _AttribIterator() - attribs._node = element - attribs._c_attr = element._c_node.properties - attribs._keysvalues = keysvalues - return attribs - - -cdef public class _ElementTagMatcher [ object LxmlElementTagMatcher, - type LxmlElementTagMatcherType ]: - """ - Dead but public. :) - """ - cdef object _pystrings - cdef int _node_type - cdef char* _href - cdef char* _name - cdef _initTagMatch(self, tag): - self._href = NULL - self._name = NULL - if tag is None: - self._node_type = 0 - elif tag is Comment: - self._node_type = tree.XML_COMMENT_NODE - elif tag is ProcessingInstruction: - self._node_type = tree.XML_PI_NODE - elif tag is Entity: - self._node_type = tree.XML_ENTITY_REF_NODE - elif tag is Element: - self._node_type = tree.XML_ELEMENT_NODE - else: - self._node_type = tree.XML_ELEMENT_NODE - self._pystrings = _getNsTag(tag) - if self._pystrings[0] is not None: - self._href = _cstr(self._pystrings[0]) - self._name = _cstr(self._pystrings[1]) - if self._name[0] == c'*' and self._name[1] == c'\0': - self._name = NULL - -cdef public class _ElementIterator(_ElementTagMatcher) [ - object LxmlElementIterator, type LxmlElementIteratorType ]: - """ - Dead but public. :) - """ - # we keep Python references here to control GC - cdef _Element _node - cdef _node_to_node_function _next_element - def __iter__(self): - return self - - cdef void _storeNext(self, _Element node): - cdef xmlNode* c_node - c_node = self._next_element(node._c_node) - while c_node is not NULL and \ - self._node_type != 0 and \ - (self._node_type != c_node.type or - not _tagMatches(c_node, self._href, self._name)): - c_node = self._next_element(c_node) - if c_node is NULL: - self._node = None - else: - # Python ref: - self._node = _elementFactory(node._doc, c_node) - - def __next__(self): - cdef xmlNode* c_node - cdef _Element current_node - if self._node is None: - raise StopIteration - # Python ref: - current_node = self._node - self._storeNext(current_node) - return current_node - -@cython.final -@cython.internal -cdef class _MultiTagMatcher: - """ - Match an xmlNode against a list of tags. - """ - cdef list _py_tags - cdef qname* _cached_tags - cdef size_t _tag_count - cdef size_t _cached_size - cdef _Document _cached_doc - cdef int _node_types - - def __cinit__(self, tags): - self._py_tags = [] - self.initTagMatch(tags) - - def __dealloc__(self): - self._clear() - - cdef bint rejectsAll(self) noexcept: - return not self._tag_count and not self._node_types - - cdef bint rejectsAllAttributes(self) noexcept: - return not self._tag_count - - cdef bint matchesType(self, int node_type) noexcept: - if node_type == tree.XML_ELEMENT_NODE and self._tag_count: - return True - return self._node_types & (1 << node_type) - - cdef void _clear(self) noexcept: - cdef size_t i, count - count = self._tag_count - self._tag_count = 0 - if self._cached_tags: - for i in range(count): - cpython.ref.Py_XDECREF(self._cached_tags[i].href) - python.lxml_free(self._cached_tags) - self._cached_tags = NULL - - cdef initTagMatch(self, tags): - self._cached_doc = None - del self._py_tags[:] - self._clear() - if tags is None or tags == (): - # no selection in tags argument => match anything - self._node_types = ( - 1 << tree.XML_COMMENT_NODE | - 1 << tree.XML_PI_NODE | - 1 << tree.XML_ENTITY_REF_NODE | - 1 << tree.XML_ELEMENT_NODE) - else: - self._node_types = 0 - self._storeTags(tags, set()) - - cdef _storeTags(self, tag, set seen): - if tag is Comment: - self._node_types |= 1 << tree.XML_COMMENT_NODE - elif tag is ProcessingInstruction: - self._node_types |= 1 << tree.XML_PI_NODE - elif tag is Entity: - self._node_types |= 1 << tree.XML_ENTITY_REF_NODE - elif tag is Element: - self._node_types |= 1 << tree.XML_ELEMENT_NODE - elif python._isString(tag): - if tag in seen: - return - seen.add(tag) - if tag in ('*', '{*}*'): - self._node_types |= 1 << tree.XML_ELEMENT_NODE - else: - href, name = _getNsTag(tag) - if name == b'*': - name = None - if href is None: - href = b'' # no namespace - elif href == b'*': - href = None # wildcard: any namespace, including none - self._py_tags.append((href, name)) - elif isinstance(tag, QName): - self._storeTags(tag.text, seen) - else: - # support a sequence of tags - for item in tag: - self._storeTags(item, seen) - - cdef inline int cacheTags(self, _Document doc, bint force_into_dict=False) except -1: - """ - Look up the tag names in the doc dict to enable string pointer comparisons. - """ - cdef size_t dict_size = tree.xmlDictSize(doc._c_doc.dict) - if doc is self._cached_doc and dict_size == self._cached_size: - # doc and dict didn't change => names already cached - return 0 - self._tag_count = 0 - if not self._py_tags: - self._cached_doc = doc - self._cached_size = dict_size - return 0 - if not self._cached_tags: - self._cached_tags = python.lxml_malloc(len(self._py_tags), sizeof(qname)) - if not self._cached_tags: - self._cached_doc = None - raise MemoryError() - self._tag_count = _mapTagsToQnameMatchArray( - doc._c_doc, self._py_tags, self._cached_tags, force_into_dict) - self._cached_doc = doc - self._cached_size = dict_size - return 0 - - cdef inline bint matches(self, xmlNode* c_node) noexcept: - cdef qname* c_qname - if self._node_types & (1 << c_node.type): - return True - elif c_node.type == tree.XML_ELEMENT_NODE: - for c_qname in self._cached_tags[:self._tag_count]: - if _tagMatchesExactly(c_node, c_qname): - return True - return False - - cdef inline bint matchesNsTag(self, const_xmlChar* c_href, - const_xmlChar* c_name) noexcept: - cdef qname* c_qname - if self._node_types & (1 << tree.XML_ELEMENT_NODE): - return True - for c_qname in self._cached_tags[:self._tag_count]: - if _nsTagMatchesExactly(c_href, c_name, c_qname): - return True - return False - - cdef inline bint matchesAttribute(self, xmlAttr* c_attr) noexcept: - """Attribute matches differ from Element matches in that they do - not care about node types. - """ - cdef qname* c_qname - for c_qname in self._cached_tags[:self._tag_count]: - if _tagMatchesExactly(c_attr, c_qname): - return True - return False - -cdef class _ElementMatchIterator: - cdef _Element _node - cdef _node_to_node_function _next_element - cdef _MultiTagMatcher _matcher - - @cython.final - cdef _initTagMatcher(self, tags): - self._matcher = _MultiTagMatcher.__new__(_MultiTagMatcher, tags) - - def __iter__(self): - return self - - @cython.final - cdef int _storeNext(self, _Element node) except -1: - self._matcher.cacheTags(node._doc) - c_node = self._next_element(node._c_node) - while c_node is not NULL and not self._matcher.matches(c_node): - c_node = self._next_element(c_node) - # store Python ref to next node to make sure it's kept alive - self._node = _elementFactory(node._doc, c_node) if c_node is not NULL else None - return 0 - - def __next__(self): - cdef _Element current_node = self._node - if current_node is None: - raise StopIteration - self._storeNext(current_node) - return current_node - -cdef class ElementChildIterator(_ElementMatchIterator): - """ElementChildIterator(self, node, tag=None, reversed=False) - Iterates over the children of an element. - """ - def __cinit__(self, _Element node not None, tag=None, *, bint reversed=False): - cdef xmlNode* c_node - _assertValidNode(node) - self._initTagMatcher(tag) - if reversed: - c_node = _findChildBackwards(node._c_node, 0) - self._next_element = _previousElement - else: - c_node = _findChildForwards(node._c_node, 0) - self._next_element = _nextElement - self._matcher.cacheTags(node._doc) - while c_node is not NULL and not self._matcher.matches(c_node): - c_node = self._next_element(c_node) - # store Python ref to next node to make sure it's kept alive - self._node = _elementFactory(node._doc, c_node) if c_node is not NULL else None - -cdef class SiblingsIterator(_ElementMatchIterator): - """SiblingsIterator(self, node, tag=None, preceding=False) - Iterates over the siblings of an element. - - You can pass the boolean keyword ``preceding`` to specify the direction. - """ - def __cinit__(self, _Element node not None, tag=None, *, bint preceding=False): - _assertValidNode(node) - self._initTagMatcher(tag) - if preceding: - self._next_element = _previousElement - else: - self._next_element = _nextElement - self._storeNext(node) - -cdef class AncestorsIterator(_ElementMatchIterator): - """AncestorsIterator(self, node, tag=None) - Iterates over the ancestors of an element (from parent to parent). - """ - def __cinit__(self, _Element node not None, tag=None): - _assertValidNode(node) - self._initTagMatcher(tag) - self._next_element = _parentElement - self._storeNext(node) - -cdef class ElementDepthFirstIterator: - """ElementDepthFirstIterator(self, node, tag=None, inclusive=True) - Iterates over an element and its sub-elements in document order (depth - first pre-order). - - Note that this also includes comments, entities and processing - instructions. To filter them out, check if the ``tag`` property - of the returned element is a string (i.e. not None and not a - factory function), or pass the ``Element`` factory for the ``tag`` - argument to receive only Elements. - - If the optional ``tag`` argument is not None, the iterator returns only - the elements that match the respective name and namespace. - - The optional boolean argument 'inclusive' defaults to True and can be set - to False to exclude the start element itself. - - Note that the behaviour of this iterator is completely undefined if the - tree it traverses is modified during iteration. - """ - # we keep Python references here to control GC - # keep the next Element after the one we return, and the (s)top node - cdef _Element _next_node - cdef _Element _top_node - cdef _MultiTagMatcher _matcher - def __cinit__(self, _Element node not None, tag=None, *, bint inclusive=True): - _assertValidNode(node) - self._top_node = node - self._next_node = node - self._matcher = _MultiTagMatcher.__new__(_MultiTagMatcher, tag) - self._matcher.cacheTags(node._doc) - if not inclusive or not self._matcher.matches(node._c_node): - # find start node (this cannot raise StopIteration, self._next_node != None) - next(self) - - def __iter__(self): - return self - - def __next__(self): - cdef xmlNode* c_node - cdef _Element current_node = self._next_node - if current_node is None: - raise StopIteration - c_node = current_node._c_node - self._matcher.cacheTags(current_node._doc) - if not self._matcher._tag_count: - # no tag name was found in the dict => not in document either - # try to match by node type - c_node = self._nextNodeAnyTag(c_node) - else: - c_node = self._nextNodeMatchTag(c_node) - if c_node is NULL: - self._next_node = None - else: - self._next_node = _elementFactory(current_node._doc, c_node) - return current_node - - @cython.final - cdef xmlNode* _nextNodeAnyTag(self, xmlNode* c_node) noexcept: - cdef int node_types = self._matcher._node_types - if not node_types: - return NULL - tree.BEGIN_FOR_EACH_ELEMENT_FROM(self._top_node._c_node, c_node, 0) - if node_types & (1 << c_node.type): - return c_node - tree.END_FOR_EACH_ELEMENT_FROM(c_node) - return NULL - - @cython.final - cdef xmlNode* _nextNodeMatchTag(self, xmlNode* c_node) noexcept: - tree.BEGIN_FOR_EACH_ELEMENT_FROM(self._top_node._c_node, c_node, 0) - if self._matcher.matches(c_node): - return c_node - tree.END_FOR_EACH_ELEMENT_FROM(c_node) - return NULL - - -cdef class ElementTextIterator: - """ElementTextIterator(self, element, tag=None, with_tail=True) - Iterates over the text content of a subtree. - - You can pass the ``tag`` keyword argument to restrict text content to a - specific tag name. - - You can set the ``with_tail`` keyword argument to ``False`` to skip over - tail text (e.g. if you know that it's only whitespace from pretty-printing). - """ - cdef object _events - cdef _Element _start_element - def __cinit__(self, _Element element not None, tag=None, *, bint with_tail=True): - _assertValidNode(element) - if with_tail: - events = ("start", "comment", "pi", "end") - else: - events = ("start",) - self._start_element = element - self._events = iterwalk(element, events=events, tag=tag) - - def __iter__(self): - return self - - def __next__(self): - cdef _Element element - result = None - while result is None: - event, element = next(self._events) # raises StopIteration - if event == "start": - result = element.text - elif element is not self._start_element: - result = element.tail - return result - - -cdef xmlNode* _createElement(xmlDoc* c_doc, object name_utf) except NULL: - cdef xmlNode* c_node - c_node = tree.xmlNewDocNode(c_doc, NULL, _xcstr(name_utf), NULL) - return c_node - -cdef xmlNode* _createComment(xmlDoc* c_doc, const_xmlChar* text) noexcept: - cdef xmlNode* c_node - c_node = tree.xmlNewDocComment(c_doc, text) - return c_node - -cdef xmlNode* _createPI(xmlDoc* c_doc, const_xmlChar* target, const_xmlChar* text) noexcept: - cdef xmlNode* c_node - c_node = tree.xmlNewDocPI(c_doc, target, text) - return c_node - -cdef xmlNode* _createEntity(xmlDoc* c_doc, const_xmlChar* name) noexcept: - cdef xmlNode* c_node - c_node = tree.xmlNewReference(c_doc, name) - return c_node - -# module-level API for ElementTree - -from abc import ABC - -class Element(ABC): - """Element(_tag, attrib=None, nsmap=None, **_extra) - - Element factory, as a class. - - An instance of this class is an object implementing the - Element interface. - - >>> element = Element("test") - >>> type(element) - - >>> isinstance(element, Element) - True - >>> issubclass(_Element, Element) - True - - Also look at the `_Element.makeelement()` and - `_BaseParser.makeelement()` methods, which provide a faster way to - create an Element within a specific document or parser context. - """ - def __new__(cls, _tag, attrib=None, nsmap=None, **_extra): - return _makeElement(_tag, NULL, None, None, None, None, - attrib, nsmap, _extra) - -# Register _Element as a virtual subclass of Element -Element.register(_Element) - - -def Comment(text=None): - """Comment(text=None) - - Comment element factory. This factory function creates a special element that will - be serialized as an XML comment. - """ - cdef _Document doc - cdef xmlNode* c_node - cdef xmlDoc* c_doc - - if text is None: - text = b'' - else: - text = _utf8(text) - if b'--' in text or text.endswith(b'-'): - raise ValueError("Comment may not contain '--' or end with '-'") - - c_doc = _newXMLDoc() - doc = _documentFactory(c_doc, None) - c_node = _createComment(c_doc, _xcstr(text)) - tree.xmlAddChild(c_doc, c_node) - return _elementFactory(doc, c_node) - - -def ProcessingInstruction(target, text=None): - """ProcessingInstruction(target, text=None) - - ProcessingInstruction element factory. This factory function creates a - special element that will be serialized as an XML processing instruction. - """ - cdef _Document doc - cdef xmlNode* c_node - cdef xmlDoc* c_doc - - target = _utf8(target) - _tagValidOrRaise(target) - if target.lower() == b'xml': - raise ValueError, f"Invalid PI name '{target}'" - - if text is None: - text = b'' - else: - text = _utf8(text) - if b'?>' in text: - raise ValueError, "PI text must not contain '?>'" - - c_doc = _newXMLDoc() - doc = _documentFactory(c_doc, None) - c_node = _createPI(c_doc, _xcstr(target), _xcstr(text)) - tree.xmlAddChild(c_doc, c_node) - return _elementFactory(doc, c_node) - -PI = ProcessingInstruction - - -cdef class CDATA: - """CDATA(data) - - CDATA factory. This factory creates an opaque data object that - can be used to set Element text. The usual way to use it is:: - - >>> el = Element('content') - >>> el.text = CDATA('a string') - - >>> print(el.text) - a string - >>> print(tostring(el, encoding="unicode")) - - """ - cdef bytes _utf8_data - def __cinit__(self, data): - self._utf8_data = _utf8(data) - - -def Entity(name): - """Entity(name) - - Entity factory. This factory function creates a special element - that will be serialized as an XML entity reference or character - reference. Note, however, that entities will not be automatically - declared in the document. A document that uses entity references - requires a DTD to define the entities. - """ - cdef _Document doc - cdef xmlNode* c_node - cdef xmlDoc* c_doc - name_utf = _utf8(name) - c_name = _xcstr(name_utf) - if c_name[0] == c'#': - if not _characterReferenceIsValid(c_name + 1): - raise ValueError, f"Invalid character reference: '{name}'" - elif not _xmlNameIsValid(c_name): - raise ValueError, f"Invalid entity reference: '{name}'" - c_doc = _newXMLDoc() - doc = _documentFactory(c_doc, None) - c_node = _createEntity(c_doc, c_name) - tree.xmlAddChild(c_doc, c_node) - return _elementFactory(doc, c_node) - - -def SubElement(_Element _parent not None, _tag, - attrib=None, nsmap=None, **_extra): - """SubElement(_parent, _tag, attrib=None, nsmap=None, **_extra) - - Subelement factory. This function creates an element instance, and - appends it to an existing element. - """ - return _makeSubElement(_parent, _tag, None, None, attrib, nsmap, _extra) - -from typing import Generic, TypeVar - -T = TypeVar("T") - -class ElementTree(ABC, Generic[T]): - def __new__(cls, _Element element=None, *, file=None, _BaseParser parser=None): - """ElementTree(element=None, file=None, parser=None) - - ElementTree wrapper class. - """ - cdef xmlNode* c_next - cdef xmlNode* c_node - cdef xmlNode* c_node_copy - cdef xmlDoc* c_doc - cdef _ElementTree etree - cdef _Document doc - - if element is not None: - doc = element._doc - elif file is not None: - try: - doc = _parseDocument(file, parser, None) - except _TargetParserResult as result_container: - return result_container.result - else: - c_doc = _newXMLDoc() - doc = _documentFactory(c_doc, parser) - - return _elementTreeFactory(doc, element) - -# Register _ElementTree as a virtual subclass of ElementTree -ElementTree.register(_ElementTree) - -# Remove "ABC" and typing helpers from module dict -del ABC, Generic, TypeVar, T - -def HTML(text, _BaseParser parser=None, *, base_url=None): - """HTML(text, parser=None, base_url=None) - - Parses an HTML document from a string constant. Returns the root - node (or the result returned by a parser target). This function - can be used to embed "HTML literals" in Python code. - - To override the parser with a different ``HTMLParser`` you can pass it to - the ``parser`` keyword argument. - - The ``base_url`` keyword argument allows to set the original base URL of - the document to support relative Paths when looking up external entities - (DTD, XInclude, ...). - """ - cdef _Document doc - if parser is None: - parser = __GLOBAL_PARSER_CONTEXT.getDefaultParser() - if not isinstance(parser, HTMLParser): - parser = __DEFAULT_HTML_PARSER - try: - doc = _parseMemoryDocument(text, base_url, parser) - return doc.getroot() - except _TargetParserResult as result_container: - return result_container.result - - -def XML(text, _BaseParser parser=None, *, base_url=None): - """XML(text, parser=None, base_url=None) - - Parses an XML document or fragment from a string constant. - Returns the root node (or the result returned by a parser target). - This function can be used to embed "XML literals" in Python code, - like in - - >>> root = XML("") - >>> print(root.tag) - root - - To override the parser with a different ``XMLParser`` you can pass it to - the ``parser`` keyword argument. - - The ``base_url`` keyword argument allows to set the original base URL of - the document to support relative Paths when looking up external entities - (DTD, XInclude, ...). - """ - cdef _Document doc - if parser is None: - parser = __GLOBAL_PARSER_CONTEXT.getDefaultParser() - if not isinstance(parser, XMLParser): - parser = __DEFAULT_XML_PARSER - try: - doc = _parseMemoryDocument(text, base_url, parser) - return doc.getroot() - except _TargetParserResult as result_container: - return result_container.result - - -def fromstring(text, _BaseParser parser=None, *, base_url=None): - """fromstring(text, parser=None, base_url=None) - - Parses an XML document or fragment from a string. Returns the - root node (or the result returned by a parser target). - - To override the default parser with a different parser you can pass it to - the ``parser`` keyword argument. - - The ``base_url`` keyword argument allows to set the original base URL of - the document to support relative Paths when looking up external entities - (DTD, XInclude, ...). - """ - cdef _Document doc - try: - doc = _parseMemoryDocument(text, base_url, parser) - return doc.getroot() - except _TargetParserResult as result_container: - return result_container.result - - -def fromstringlist(strings, _BaseParser parser=None): - """fromstringlist(strings, parser=None) - - Parses an XML document from a sequence of strings. Returns the - root node (or the result returned by a parser target). - - To override the default parser with a different parser you can pass it to - the ``parser`` keyword argument. - """ - cdef _Document doc - if isinstance(strings, (bytes, unicode)): - raise ValueError("passing a single string into fromstringlist() is not" - " efficient, use fromstring() instead") - if parser is None: - parser = __GLOBAL_PARSER_CONTEXT.getDefaultParser() - feed = parser.feed - for data in strings: - feed(data) - return parser.close() - - -def iselement(element): - """iselement(element) - - Checks if an object appears to be a valid element object. - """ - return isinstance(element, _Element) and (<_Element>element)._c_node is not NULL - - -def indent(tree, space=" ", *, Py_ssize_t level=0): - """indent(tree, space=" ", level=0) - - Indent an XML document by inserting newlines and indentation space - after elements. - - *tree* is the ElementTree or Element to modify. The (root) element - itself will not be changed, but the tail text of all elements in its - subtree will be adapted. - - *space* is the whitespace to insert for each indentation level, two - space characters by default. - - *level* is the initial indentation level. Setting this to a higher - value than 0 can be used for indenting subtrees that are more deeply - nested inside of a document. - """ - root = _rootNodeOrRaise(tree) - if level < 0: - raise ValueError(f"Initial indentation level must be >= 0, got {level}") - if _hasChild(root._c_node): - space = _utf8(space) - indent = b"\n" + level * space - _indent_children(root._c_node, 1, space, [indent, indent + space]) - - -cdef int _indent_children(xmlNode* c_node, Py_ssize_t level, bytes one_space, list indentations) except -1: - # Reuse indentation strings for speed. - if len(indentations) <= level: - indentations.append(indentations[-1] + one_space) - - # Start a new indentation level for the first child. - child_indentation = indentations[level] - if not _hasNonWhitespaceText(c_node): - _setNodeText(c_node, child_indentation) - - # Recursively indent all children. - cdef xmlNode* c_child = _findChildForwards(c_node, 0) - while c_child is not NULL: - if _hasChild(c_child): - _indent_children(c_child, level+1, one_space, indentations) - c_next_child = _nextElement(c_child) - if not _hasNonWhitespaceTail(c_child): - if c_next_child is NULL: - # Dedent after the last child. - child_indentation = indentations[level-1] - _setTailText(c_child, child_indentation) - c_child = c_next_child - return 0 - - -def dump(_Element elem not None, *, bint pretty_print=True, bint with_tail=True): - """dump(elem, pretty_print=True, with_tail=True) - - Writes an element tree or element structure to sys.stdout. This function - should be used for debugging only. - """ - xml = tostring(elem, pretty_print=pretty_print, with_tail=with_tail, encoding='unicode') - if not pretty_print: - xml += '\n' - sys.stdout.write(xml) - - -def tostring(element_or_tree, *, encoding=None, method="xml", - xml_declaration=None, bint pretty_print=False, bint with_tail=True, - standalone=None, doctype=None, - # method='c14n' - bint exclusive=False, inclusive_ns_prefixes=None, - # method='c14n2' - bint with_comments=True, bint strip_text=False, - ): - """tostring(element_or_tree, encoding=None, method="xml", - xml_declaration=None, pretty_print=False, with_tail=True, - standalone=None, doctype=None, - exclusive=False, inclusive_ns_prefixes=None, - with_comments=True, strip_text=False, - ) - - Serialize an element to an encoded string representation of its XML - tree. - - Defaults to ASCII encoding without XML declaration. This - behaviour can be configured with the keyword arguments 'encoding' - (string) and 'xml_declaration' (bool). Note that changing the - encoding to a non UTF-8 compatible encoding will enable a - declaration by default. - - You can also serialise to a Unicode string without declaration by - passing the name ``'unicode'`` as encoding (or the ``str`` function - in Py3 or ``unicode`` in Py2). This changes the return value from - a byte string to an unencoded unicode string. - - The keyword argument 'pretty_print' (bool) enables formatted XML. - - The keyword argument 'method' selects the output method: 'xml', - 'html', plain 'text' (text content without tags), 'c14n' or 'c14n2'. - Default is 'xml'. - - With ``method="c14n"`` (C14N version 1), the options ``exclusive``, - ``with_comments`` and ``inclusive_ns_prefixes`` request exclusive - C14N, include comments, and list the inclusive prefixes respectively. - - With ``method="c14n2"`` (C14N version 2), the ``with_comments`` and - ``strip_text`` options control the output of comments and text space - according to C14N 2.0. - - Passing a boolean value to the ``standalone`` option will output - an XML declaration with the corresponding ``standalone`` flag. - - The ``doctype`` option allows passing in a plain string that will - be serialised before the XML tree. Note that passing in non - well-formed content here will make the XML output non well-formed. - Also, an existing doctype in the document tree will not be removed - when serialising an ElementTree instance. - - You can prevent the tail text of the element from being serialised - by passing the boolean ``with_tail`` option. This has no impact - on the tail text of children, which will always be serialised. - """ - cdef bint write_declaration - cdef int is_standalone - # C14N serialisation - if method in ('c14n', 'c14n2'): - if encoding is not None: - raise ValueError("Cannot specify encoding with C14N") - if xml_declaration: - raise ValueError("Cannot enable XML declaration in C14N") - if method == 'c14n': - return _tostringC14N(element_or_tree, exclusive, with_comments, inclusive_ns_prefixes) - else: - out = BytesIO() - target = C14NWriterTarget( - utf8_writer(out).write, - with_comments=with_comments, strip_text=strip_text) - _tree_to_target(element_or_tree, target) - return out.getvalue() - if not with_comments: - raise ValueError("Can only discard comments in C14N serialisation") - if strip_text: - raise ValueError("Can only strip text in C14N 2.0 serialisation") - if encoding is unicode or (encoding is not None and encoding.lower() == 'unicode'): - if xml_declaration: - raise ValueError, \ - "Serialisation to unicode must not request an XML declaration" - write_declaration = 0 - encoding = unicode - elif xml_declaration is None: - # by default, write an XML declaration only for non-standard encodings - write_declaration = encoding is not None and encoding.upper() not in \ - ('ASCII', 'UTF-8', 'UTF8', 'US-ASCII') - else: - write_declaration = xml_declaration - if encoding is None: - encoding = 'ASCII' - if standalone is None: - is_standalone = -1 - elif standalone: - write_declaration = 1 - is_standalone = 1 - else: - write_declaration = 1 - is_standalone = 0 - - if isinstance(element_or_tree, _Element): - return _tostring(<_Element>element_or_tree, encoding, doctype, method, - write_declaration, 0, pretty_print, with_tail, - is_standalone) - elif isinstance(element_or_tree, _ElementTree): - return _tostring((<_ElementTree>element_or_tree)._context_node, - encoding, doctype, method, write_declaration, 1, - pretty_print, with_tail, is_standalone) - else: - raise TypeError, f"Type '{python._fqtypename(element_or_tree).decode('utf8')}' cannot be serialized." - - - -def tostringlist(element_or_tree, *args, **kwargs): - """tostringlist(element_or_tree, *args, **kwargs) - - Serialize an element to an encoded string representation of its XML - tree, stored in a list of partial strings. - - This is purely for ElementTree 1.3 compatibility. The result is a - single string wrapped in a list. - """ - return [tostring(element_or_tree, *args, **kwargs)] - - -def tounicode(element_or_tree, *, method="xml", bint pretty_print=False, - bint with_tail=True, doctype=None): - """tounicode(element_or_tree, method="xml", pretty_print=False, - with_tail=True, doctype=None) - - Serialize an element to the Python unicode representation of its XML - tree. - - :deprecated: use ``tostring(el, encoding='unicode')`` instead. - - Note that the result does not carry an XML encoding declaration and is - therefore not necessarily suited for serialization to byte streams without - further treatment. - - The boolean keyword argument 'pretty_print' enables formatted XML. - - The keyword argument 'method' selects the output method: 'xml', - 'html' or plain 'text'. - - You can prevent the tail text of the element from being serialised - by passing the boolean ``with_tail`` option. This has no impact - on the tail text of children, which will always be serialised. - """ - if isinstance(element_or_tree, _Element): - return _tostring(<_Element>element_or_tree, unicode, doctype, method, - 0, 0, pretty_print, with_tail, -1) - elif isinstance(element_or_tree, _ElementTree): - return _tostring((<_ElementTree>element_or_tree)._context_node, - unicode, doctype, method, 0, 1, pretty_print, - with_tail, -1) - else: - raise TypeError, f"Type '{type(element_or_tree)}' cannot be serialized." - - -def parse(source, _BaseParser parser=None, *, base_url=None): - """parse(source, parser=None, base_url=None) - - Return an ElementTree object loaded with source elements. If no parser - is provided as second argument, the default parser is used. - - The ``source`` can be any of the following: - - - a file name/path - - a file object - - a file-like object - - a URL using the HTTP or FTP protocol - - To parse from a string, use the ``fromstring()`` function instead. - - Note that it is generally faster to parse from a file path or URL - than from an open file object or file-like object. Transparent - decompression from gzip compressed sources is supported (unless - explicitly disabled in libxml2). - - The ``base_url`` keyword allows setting a URL for the document - when parsing from a file-like object. This is needed when looking - up external entities (DTD, XInclude, ...) with relative paths. - """ - cdef _Document doc - try: - doc = _parseDocument(source, parser, base_url) - return _elementTreeFactory(doc, None) - except _TargetParserResult as result_container: - return result_container.result - - -def adopt_external_document(capsule, _BaseParser parser=None): - """adopt_external_document(capsule, parser=None) - - Unpack a libxml2 document pointer from a PyCapsule and wrap it in an - lxml ElementTree object. - - This allows external libraries to build XML/HTML trees using libxml2 - and then pass them efficiently into lxml for further processing. - - If a ``parser`` is provided, it will be used for configuring the - lxml document. No parsing will be done. - - The capsule must have the name ``"libxml2:xmlDoc"`` and its pointer - value must reference a correct libxml2 document of type ``xmlDoc*``. - The creator of the capsule must take care to correctly clean up the - document using an appropriate capsule destructor. By default, the - libxml2 document will be copied to let lxml safely own the memory - of the internal tree that it uses. - - If the capsule context is non-NULL, it must point to a C string that - can be compared using ``strcmp()``. If the context string equals - ``"destructor:xmlFreeDoc"``, the libxml2 document will not be copied - but the capsule invalidated instead by clearing its destructor and - name. That way, lxml takes ownership of the libxml2 document in memory - without creating a copy first, and the capsule destructor will not be - called. The document will then eventually be cleaned up by lxml using - the libxml2 API function ``xmlFreeDoc()`` once it is no longer used. - - If no copy is made, later modifications of the tree outside of lxml - should not be attempted after transferring the ownership. - """ - cdef xmlDoc* c_doc - cdef bint is_owned = False - c_doc = python.lxml_unpack_xmldoc_capsule(capsule, &is_owned) - doc = _adoptForeignDoc(c_doc, parser, is_owned) - return _elementTreeFactory(doc, None) - - -################################################################################ -# Include submodules - -include "readonlytree.pxi" # Read-only implementation of Element proxies -include "classlookup.pxi" # Element class lookup mechanisms -include "nsclasses.pxi" # Namespace implementation and registry -include "docloader.pxi" # Support for custom document loaders -include "parser.pxi" # XML and HTML parsers -include "saxparser.pxi" # SAX-like Parser interface and tree builder -include "parsertarget.pxi" # ET Parser target -include "serializer.pxi" # XML output functions -include "iterparse.pxi" # incremental XML parsing -include "xmlid.pxi" # XMLID and IDDict -include "xinclude.pxi" # XInclude -include "cleanup.pxi" # Cleanup and recursive element removal functions - - -################################################################################ -# Include submodules for XPath and XSLT - -include "extensions.pxi" # XPath/XSLT extension functions -include "xpath.pxi" # XPath evaluation -include "xslt.pxi" # XSL transformations -include "xsltext.pxi" # XSL extension elements - - -################################################################################ -# Validation - -cdef class DocumentInvalid(LxmlError): - """Validation error. - - Raised by all document validators when their ``assertValid(tree)`` - method fails. - """ - - -cdef class _Validator: - "Base class for XML validators." - cdef _ErrorLog _error_log - def __cinit__(self): - self._error_log = _ErrorLog() - - def validate(self, etree): - """validate(self, etree) - - Validate the document using this schema. - - Returns true if document is valid, false if not. - """ - return self(etree) - - def assertValid(self, etree): - """assertValid(self, etree) - - Raises `DocumentInvalid` if the document does not comply with the schema. - """ - if not self(etree): - raise DocumentInvalid(self._error_log._buildExceptionMessage( - "Document does not comply with schema"), - self._error_log) - - def assert_(self, etree): - """assert_(self, etree) - - Raises `AssertionError` if the document does not comply with the schema. - """ - if not self(etree): - raise AssertionError, self._error_log._buildExceptionMessage( - "Document does not comply with schema") - - cpdef _append_log_message(self, int domain, int type, int level, int line, - message, filename): - self._error_log._receiveGeneric(domain, type, level, line, message, - filename) - - cpdef _clear_error_log(self): - self._error_log.clear() - - @property - def error_log(self): - """The log of validation errors and warnings.""" - assert self._error_log is not None, "XPath evaluator not initialised" - return self._error_log.copy() - -include "dtd.pxi" # DTD -include "relaxng.pxi" # RelaxNG -include "xmlschema.pxi" # XMLSchema -include "schematron.pxi" # Schematron (requires libxml2 2.6.21+) - -################################################################################ -# Public C API - -include "public-api.pxi" - -################################################################################ -# Other stuff - -include "debug.pxi" diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/etree_api.h b/presentation/.venv/lib/python3.12/site-packages/lxml/etree_api.h deleted file mode 100644 index 702fef2..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/etree_api.h +++ /dev/null @@ -1,214 +0,0 @@ -/* Generated by Cython 3.2.4 */ - -#ifndef __PYX_HAVE_API__lxml__etree -#define __PYX_HAVE_API__lxml__etree -#ifdef __MINGW64__ -#define MS_WIN64 -#endif -#include "Python.h" -#include "etree.h" - -static struct LxmlElement *(*__pyx_api_f_4lxml_5etree_deepcopyNodeToDocument)(struct LxmlDocument *, xmlNode *) = 0; -#define deepcopyNodeToDocument __pyx_api_f_4lxml_5etree_deepcopyNodeToDocument -static struct LxmlElementTree *(*__pyx_api_f_4lxml_5etree_elementTreeFactory)(struct LxmlElement *) = 0; -#define elementTreeFactory __pyx_api_f_4lxml_5etree_elementTreeFactory -static struct LxmlElementTree *(*__pyx_api_f_4lxml_5etree_newElementTree)(struct LxmlElement *, PyObject *) = 0; -#define newElementTree __pyx_api_f_4lxml_5etree_newElementTree -static struct LxmlElementTree *(*__pyx_api_f_4lxml_5etree_adoptExternalDocument)(xmlDoc *, PyObject *, int) = 0; -#define adoptExternalDocument __pyx_api_f_4lxml_5etree_adoptExternalDocument -static struct LxmlElement *(*__pyx_api_f_4lxml_5etree_elementFactory)(struct LxmlDocument *, xmlNode *) = 0; -#define elementFactory __pyx_api_f_4lxml_5etree_elementFactory -static struct LxmlElement *(*__pyx_api_f_4lxml_5etree_makeElement)(PyObject *, struct LxmlDocument *, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *) = 0; -#define makeElement __pyx_api_f_4lxml_5etree_makeElement -static struct LxmlElement *(*__pyx_api_f_4lxml_5etree_makeSubElement)(struct LxmlElement *, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *) = 0; -#define makeSubElement __pyx_api_f_4lxml_5etree_makeSubElement -static void (*__pyx_api_f_4lxml_5etree_setElementClassLookupFunction)(_element_class_lookup_function, PyObject *) = 0; -#define setElementClassLookupFunction __pyx_api_f_4lxml_5etree_setElementClassLookupFunction -static PyObject *(*__pyx_api_f_4lxml_5etree_lookupDefaultElementClass)(PyObject *, PyObject *, xmlNode *) = 0; -#define lookupDefaultElementClass __pyx_api_f_4lxml_5etree_lookupDefaultElementClass -static PyObject *(*__pyx_api_f_4lxml_5etree_lookupNamespaceElementClass)(PyObject *, PyObject *, xmlNode *) = 0; -#define lookupNamespaceElementClass __pyx_api_f_4lxml_5etree_lookupNamespaceElementClass -static PyObject *(*__pyx_api_f_4lxml_5etree_callLookupFallback)(struct LxmlFallbackElementClassLookup *, struct LxmlDocument *, xmlNode *) = 0; -#define callLookupFallback __pyx_api_f_4lxml_5etree_callLookupFallback -static int (*__pyx_api_f_4lxml_5etree_tagMatches)(xmlNode *, const xmlChar *, const xmlChar *) = 0; -#define tagMatches __pyx_api_f_4lxml_5etree_tagMatches -static struct LxmlDocument *(*__pyx_api_f_4lxml_5etree_documentOrRaise)(PyObject *) = 0; -#define documentOrRaise __pyx_api_f_4lxml_5etree_documentOrRaise -static struct LxmlElement *(*__pyx_api_f_4lxml_5etree_rootNodeOrRaise)(PyObject *) = 0; -#define rootNodeOrRaise __pyx_api_f_4lxml_5etree_rootNodeOrRaise -static int (*__pyx_api_f_4lxml_5etree_hasText)(xmlNode *) = 0; -#define hasText __pyx_api_f_4lxml_5etree_hasText -static int (*__pyx_api_f_4lxml_5etree_hasTail)(xmlNode *) = 0; -#define hasTail __pyx_api_f_4lxml_5etree_hasTail -static PyObject *(*__pyx_api_f_4lxml_5etree_textOf)(xmlNode *) = 0; -#define textOf __pyx_api_f_4lxml_5etree_textOf -static PyObject *(*__pyx_api_f_4lxml_5etree_tailOf)(xmlNode *) = 0; -#define tailOf __pyx_api_f_4lxml_5etree_tailOf -static int (*__pyx_api_f_4lxml_5etree_setNodeText)(xmlNode *, PyObject *) = 0; -#define setNodeText __pyx_api_f_4lxml_5etree_setNodeText -static int (*__pyx_api_f_4lxml_5etree_setTailText)(xmlNode *, PyObject *) = 0; -#define setTailText __pyx_api_f_4lxml_5etree_setTailText -static PyObject *(*__pyx_api_f_4lxml_5etree_attributeValue)(xmlNode *, xmlAttr *) = 0; -#define attributeValue __pyx_api_f_4lxml_5etree_attributeValue -static PyObject *(*__pyx_api_f_4lxml_5etree_attributeValueFromNsName)(xmlNode *, const xmlChar *, const xmlChar *) = 0; -#define attributeValueFromNsName __pyx_api_f_4lxml_5etree_attributeValueFromNsName -static PyObject *(*__pyx_api_f_4lxml_5etree_getAttributeValue)(struct LxmlElement *, PyObject *, PyObject *) = 0; -#define getAttributeValue __pyx_api_f_4lxml_5etree_getAttributeValue -static PyObject *(*__pyx_api_f_4lxml_5etree_iterattributes)(struct LxmlElement *, int) = 0; -#define iterattributes __pyx_api_f_4lxml_5etree_iterattributes -static PyObject *(*__pyx_api_f_4lxml_5etree_collectAttributes)(xmlNode *, int) = 0; -#define collectAttributes __pyx_api_f_4lxml_5etree_collectAttributes -static int (*__pyx_api_f_4lxml_5etree_setAttributeValue)(struct LxmlElement *, PyObject *, PyObject *) = 0; -#define setAttributeValue __pyx_api_f_4lxml_5etree_setAttributeValue -static int (*__pyx_api_f_4lxml_5etree_delAttribute)(struct LxmlElement *, PyObject *) = 0; -#define delAttribute __pyx_api_f_4lxml_5etree_delAttribute -static int (*__pyx_api_f_4lxml_5etree_delAttributeFromNsName)(xmlNode *, const xmlChar *, const xmlChar *) = 0; -#define delAttributeFromNsName __pyx_api_f_4lxml_5etree_delAttributeFromNsName -static int (*__pyx_api_f_4lxml_5etree_hasChild)(xmlNode *) = 0; -#define hasChild __pyx_api_f_4lxml_5etree_hasChild -static xmlNode *(*__pyx_api_f_4lxml_5etree_findChild)(xmlNode *, Py_ssize_t) = 0; -#define findChild __pyx_api_f_4lxml_5etree_findChild -static xmlNode *(*__pyx_api_f_4lxml_5etree_findChildForwards)(xmlNode *, Py_ssize_t) = 0; -#define findChildForwards __pyx_api_f_4lxml_5etree_findChildForwards -static xmlNode *(*__pyx_api_f_4lxml_5etree_findChildBackwards)(xmlNode *, Py_ssize_t) = 0; -#define findChildBackwards __pyx_api_f_4lxml_5etree_findChildBackwards -static xmlNode *(*__pyx_api_f_4lxml_5etree_nextElement)(xmlNode *) = 0; -#define nextElement __pyx_api_f_4lxml_5etree_nextElement -static xmlNode *(*__pyx_api_f_4lxml_5etree_previousElement)(xmlNode *) = 0; -#define previousElement __pyx_api_f_4lxml_5etree_previousElement -static void (*__pyx_api_f_4lxml_5etree_appendChild)(struct LxmlElement *, struct LxmlElement *) = 0; -#define appendChild __pyx_api_f_4lxml_5etree_appendChild -static int (*__pyx_api_f_4lxml_5etree_appendChildToElement)(struct LxmlElement *, struct LxmlElement *) = 0; -#define appendChildToElement __pyx_api_f_4lxml_5etree_appendChildToElement -static PyObject *(*__pyx_api_f_4lxml_5etree_pyunicode)(const xmlChar *) = 0; -#define pyunicode __pyx_api_f_4lxml_5etree_pyunicode -static PyObject *(*__pyx_api_f_4lxml_5etree_utf8)(PyObject *) = 0; -#define utf8 __pyx_api_f_4lxml_5etree_utf8 -static PyObject *(*__pyx_api_f_4lxml_5etree_getNsTag)(PyObject *) = 0; -#define getNsTag __pyx_api_f_4lxml_5etree_getNsTag -static PyObject *(*__pyx_api_f_4lxml_5etree_getNsTagWithEmptyNs)(PyObject *) = 0; -#define getNsTagWithEmptyNs __pyx_api_f_4lxml_5etree_getNsTagWithEmptyNs -static PyObject *(*__pyx_api_f_4lxml_5etree_namespacedName)(xmlNode *) = 0; -#define namespacedName __pyx_api_f_4lxml_5etree_namespacedName -static PyObject *(*__pyx_api_f_4lxml_5etree_namespacedNameFromNsName)(const xmlChar *, const xmlChar *) = 0; -#define namespacedNameFromNsName __pyx_api_f_4lxml_5etree_namespacedNameFromNsName -static void (*__pyx_api_f_4lxml_5etree_iteratorStoreNext)(struct LxmlElementIterator *, struct LxmlElement *) = 0; -#define iteratorStoreNext __pyx_api_f_4lxml_5etree_iteratorStoreNext -static void (*__pyx_api_f_4lxml_5etree_initTagMatch)(struct LxmlElementTagMatcher *, PyObject *) = 0; -#define initTagMatch __pyx_api_f_4lxml_5etree_initTagMatch -static xmlNs *(*__pyx_api_f_4lxml_5etree_findOrBuildNodeNsPrefix)(struct LxmlDocument *, xmlNode *, const xmlChar *, const xmlChar *) = 0; -#define findOrBuildNodeNsPrefix __pyx_api_f_4lxml_5etree_findOrBuildNodeNsPrefix -static int __Pyx_ImportFunction_3_2_4(PyObject *module, const char *funcname, void (**f)(void), const char *sig); - -#ifndef __PYX_HAVE_RT_ImportFromPxd_3_2_4 -#define __PYX_HAVE_RT_ImportFromPxd_3_2_4 -static int __Pyx_ImportFromPxd_3_2_4(PyObject *module, const char *name, void **p, const char *sig, const char *what) { - PyObject *d = 0; - PyObject *cobj = 0; - d = PyObject_GetAttrString(module, "__pyx_capi__"); - if (!d) - goto bad; -#if (defined(Py_LIMITED_API) && Py_LIMITED_API >= 0x030d0000) || (!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030d0000) - PyDict_GetItemStringRef(d, name, &cobj); -#else - cobj = PyDict_GetItemString(d, name); - Py_XINCREF(cobj); -#endif - if (!cobj) { - PyErr_Format(PyExc_ImportError, - "%.200s does not export expected C %.8s %.200s", - PyModule_GetName(module), what, name); - goto bad; - } - if (!PyCapsule_IsValid(cobj, sig)) { - PyErr_Format(PyExc_TypeError, - "C %.8s %.200s.%.200s has wrong signature (expected %.500s, got %.500s)", - what, PyModule_GetName(module), name, sig, PyCapsule_GetName(cobj)); - goto bad; - } - *p = PyCapsule_GetPointer(cobj, sig); - if (!(*p)) - goto bad; - Py_DECREF(d); - Py_DECREF(cobj); - return 0; -bad: - Py_XDECREF(d); - Py_XDECREF(cobj); - return -1; -} -#endif - -#ifndef __PYX_HAVE_RT_ImportFunction_3_2_4 -#define __PYX_HAVE_RT_ImportFunction_3_2_4 -static int __Pyx_ImportFunction_3_2_4(PyObject *module, const char *funcname, void (**f)(void), const char *sig) { - union { - void (*fp)(void); - void *p; - } tmp; - int result = __Pyx_ImportFromPxd_3_2_4(module, funcname, &tmp.p, sig, "function"); - if (result == 0) { - *f = tmp.fp; - } - return result; -} -#endif - - -static int import_lxml__etree(void) { - PyObject *module = 0; - module = PyImport_ImportModule("lxml.etree"); - if (!module) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "deepcopyNodeToDocument", (void (**)(void))&__pyx_api_f_4lxml_5etree_deepcopyNodeToDocument, "struct LxmlElement *(struct LxmlDocument *, xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "elementTreeFactory", (void (**)(void))&__pyx_api_f_4lxml_5etree_elementTreeFactory, "struct LxmlElementTree *(struct LxmlElement *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "newElementTree", (void (**)(void))&__pyx_api_f_4lxml_5etree_newElementTree, "struct LxmlElementTree *(struct LxmlElement *, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "adoptExternalDocument", (void (**)(void))&__pyx_api_f_4lxml_5etree_adoptExternalDocument, "struct LxmlElementTree *(xmlDoc *, PyObject *, int)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "elementFactory", (void (**)(void))&__pyx_api_f_4lxml_5etree_elementFactory, "struct LxmlElement *(struct LxmlDocument *, xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "makeElement", (void (**)(void))&__pyx_api_f_4lxml_5etree_makeElement, "struct LxmlElement *(PyObject *, struct LxmlDocument *, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "makeSubElement", (void (**)(void))&__pyx_api_f_4lxml_5etree_makeSubElement, "struct LxmlElement *(struct LxmlElement *, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "setElementClassLookupFunction", (void (**)(void))&__pyx_api_f_4lxml_5etree_setElementClassLookupFunction, "void (_element_class_lookup_function, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "lookupDefaultElementClass", (void (**)(void))&__pyx_api_f_4lxml_5etree_lookupDefaultElementClass, "PyObject *(PyObject *, PyObject *, xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "lookupNamespaceElementClass", (void (**)(void))&__pyx_api_f_4lxml_5etree_lookupNamespaceElementClass, "PyObject *(PyObject *, PyObject *, xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "callLookupFallback", (void (**)(void))&__pyx_api_f_4lxml_5etree_callLookupFallback, "PyObject *(struct LxmlFallbackElementClassLookup *, struct LxmlDocument *, xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "tagMatches", (void (**)(void))&__pyx_api_f_4lxml_5etree_tagMatches, "int (xmlNode *, const xmlChar *, const xmlChar *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "documentOrRaise", (void (**)(void))&__pyx_api_f_4lxml_5etree_documentOrRaise, "struct LxmlDocument *(PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "rootNodeOrRaise", (void (**)(void))&__pyx_api_f_4lxml_5etree_rootNodeOrRaise, "struct LxmlElement *(PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "hasText", (void (**)(void))&__pyx_api_f_4lxml_5etree_hasText, "int (xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "hasTail", (void (**)(void))&__pyx_api_f_4lxml_5etree_hasTail, "int (xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "textOf", (void (**)(void))&__pyx_api_f_4lxml_5etree_textOf, "PyObject *(xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "tailOf", (void (**)(void))&__pyx_api_f_4lxml_5etree_tailOf, "PyObject *(xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "setNodeText", (void (**)(void))&__pyx_api_f_4lxml_5etree_setNodeText, "int (xmlNode *, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "setTailText", (void (**)(void))&__pyx_api_f_4lxml_5etree_setTailText, "int (xmlNode *, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "attributeValue", (void (**)(void))&__pyx_api_f_4lxml_5etree_attributeValue, "PyObject *(xmlNode *, xmlAttr *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "attributeValueFromNsName", (void (**)(void))&__pyx_api_f_4lxml_5etree_attributeValueFromNsName, "PyObject *(xmlNode *, const xmlChar *, const xmlChar *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "getAttributeValue", (void (**)(void))&__pyx_api_f_4lxml_5etree_getAttributeValue, "PyObject *(struct LxmlElement *, PyObject *, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "iterattributes", (void (**)(void))&__pyx_api_f_4lxml_5etree_iterattributes, "PyObject *(struct LxmlElement *, int)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "collectAttributes", (void (**)(void))&__pyx_api_f_4lxml_5etree_collectAttributes, "PyObject *(xmlNode *, int)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "setAttributeValue", (void (**)(void))&__pyx_api_f_4lxml_5etree_setAttributeValue, "int (struct LxmlElement *, PyObject *, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "delAttribute", (void (**)(void))&__pyx_api_f_4lxml_5etree_delAttribute, "int (struct LxmlElement *, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "delAttributeFromNsName", (void (**)(void))&__pyx_api_f_4lxml_5etree_delAttributeFromNsName, "int (xmlNode *, const xmlChar *, const xmlChar *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "hasChild", (void (**)(void))&__pyx_api_f_4lxml_5etree_hasChild, "int (xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "findChild", (void (**)(void))&__pyx_api_f_4lxml_5etree_findChild, "xmlNode *(xmlNode *, Py_ssize_t)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "findChildForwards", (void (**)(void))&__pyx_api_f_4lxml_5etree_findChildForwards, "xmlNode *(xmlNode *, Py_ssize_t)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "findChildBackwards", (void (**)(void))&__pyx_api_f_4lxml_5etree_findChildBackwards, "xmlNode *(xmlNode *, Py_ssize_t)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "nextElement", (void (**)(void))&__pyx_api_f_4lxml_5etree_nextElement, "xmlNode *(xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "previousElement", (void (**)(void))&__pyx_api_f_4lxml_5etree_previousElement, "xmlNode *(xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "appendChild", (void (**)(void))&__pyx_api_f_4lxml_5etree_appendChild, "void (struct LxmlElement *, struct LxmlElement *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "appendChildToElement", (void (**)(void))&__pyx_api_f_4lxml_5etree_appendChildToElement, "int (struct LxmlElement *, struct LxmlElement *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "pyunicode", (void (**)(void))&__pyx_api_f_4lxml_5etree_pyunicode, "PyObject *(const xmlChar *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "utf8", (void (**)(void))&__pyx_api_f_4lxml_5etree_utf8, "PyObject *(PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "getNsTag", (void (**)(void))&__pyx_api_f_4lxml_5etree_getNsTag, "PyObject *(PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "getNsTagWithEmptyNs", (void (**)(void))&__pyx_api_f_4lxml_5etree_getNsTagWithEmptyNs, "PyObject *(PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "namespacedName", (void (**)(void))&__pyx_api_f_4lxml_5etree_namespacedName, "PyObject *(xmlNode *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "namespacedNameFromNsName", (void (**)(void))&__pyx_api_f_4lxml_5etree_namespacedNameFromNsName, "PyObject *(const xmlChar *, const xmlChar *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "iteratorStoreNext", (void (**)(void))&__pyx_api_f_4lxml_5etree_iteratorStoreNext, "void (struct LxmlElementIterator *, struct LxmlElement *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "initTagMatch", (void (**)(void))&__pyx_api_f_4lxml_5etree_initTagMatch, "void (struct LxmlElementTagMatcher *, PyObject *)") < 0) goto bad; - if (__Pyx_ImportFunction_3_2_4(module, "findOrBuildNodeNsPrefix", (void (**)(void))&__pyx_api_f_4lxml_5etree_findOrBuildNodeNsPrefix, "xmlNs *(struct LxmlDocument *, xmlNode *, const xmlChar *, const xmlChar *)") < 0) goto bad; - Py_DECREF(module); module = 0; - return 0; - bad: - Py_XDECREF(module); - return -1; -} - -#endif /* !__PYX_HAVE_API__lxml__etree */ diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/extensions.pxi b/presentation/.venv/lib/python3.12/site-packages/lxml/extensions.pxi deleted file mode 100644 index 5945080..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/extensions.pxi +++ /dev/null @@ -1,838 +0,0 @@ -# support for extension functions in XPath and XSLT - -cdef class XPathError(LxmlError): - """Base class of all XPath errors. - """ - -cdef class XPathEvalError(XPathError): - """Error during XPath evaluation. - """ - -cdef class XPathFunctionError(XPathEvalError): - """Internal error looking up an XPath extension function. - """ - -cdef class XPathResultError(XPathEvalError): - """Error handling an XPath result. - """ - - -# forward declarations - -ctypedef int (*_register_function)(void* ctxt, name_utf, ns_uri_utf) -cdef class _ExsltRegExp - -################################################################################ -# Base class for XSLT and XPath evaluation contexts: functions, namespaces, ... - -@cython.internal -cdef class _BaseContext: - cdef xpath.xmlXPathContext* _xpathCtxt - cdef _Document _doc - cdef dict _extensions - cdef list _namespaces - cdef list _global_namespaces - cdef dict _utf_refs - cdef dict _function_cache - cdef dict _eval_context_dict - cdef bint _build_smart_strings - # for exception handling and temporary reference keeping: - cdef _TempStore _temp_refs - cdef set _temp_documents - cdef _ExceptionContext _exc - cdef _ErrorLog _error_log - - def __init__(self, namespaces, extensions, error_log, enable_regexp, - build_smart_strings): - cdef _ExsltRegExp _regexp - cdef dict new_extensions - cdef list ns - self._utf_refs = {} - self._global_namespaces = [] - self._function_cache = {} - self._eval_context_dict = None - self._error_log = error_log - - if extensions is not None: - # convert extensions to UTF-8 - if isinstance(extensions, dict): - extensions = (extensions,) - # format: [ {(ns, name):function} ] -> {(ns_utf, name_utf):function} - new_extensions = {} - for extension in extensions: - for (ns_uri, name), function in extension.items(): - if name is None: - raise ValueError, "extensions must have non empty names" - ns_utf = self._to_utf(ns_uri) - name_utf = self._to_utf(name) - new_extensions[(ns_utf, name_utf)] = function - extensions = new_extensions or None - - if namespaces is not None: - if isinstance(namespaces, dict): - namespaces = namespaces.items() - if namespaces: - ns = [] - for prefix, ns_uri in namespaces: - if prefix is None or not prefix: - raise TypeError, \ - "empty namespace prefix is not supported in XPath" - if ns_uri is None or not ns_uri: - raise TypeError, \ - "setting default namespace is not supported in XPath" - prefix_utf = self._to_utf(prefix) - ns_uri_utf = self._to_utf(ns_uri) - ns.append( (prefix_utf, ns_uri_utf) ) - namespaces = ns - else: - namespaces = None - - self._doc = None - self._exc = _ExceptionContext() - self._extensions = extensions - self._namespaces = namespaces - self._temp_refs = _TempStore() - self._temp_documents = set() - self._build_smart_strings = build_smart_strings - - if enable_regexp: - _regexp = _ExsltRegExp() - _regexp._register_in_context(self) - - cdef _BaseContext _copy(self): - cdef _BaseContext context - if self._namespaces is not None: - namespaces = self._namespaces[:] - else: - namespaces = None - context = self.__class__(namespaces, None, self._error_log, False, - self._build_smart_strings) - if self._extensions is not None: - context._extensions = self._extensions.copy() - return context - - cdef bytes _to_utf(self, s): - "Convert to UTF-8 and keep a reference to the encoded string" - cdef python.PyObject* dict_result - if s is None: - return None - dict_result = python.PyDict_GetItem(self._utf_refs, s) - if dict_result is not NULL: - return dict_result - utf = _utf8(s) - self._utf_refs[s] = utf - if python.IS_PYPY: - # use C level refs, PyPy refs are not enough! - python.Py_INCREF(utf) - return utf - - cdef void _set_xpath_context(self, xpath.xmlXPathContext* xpathCtxt) noexcept: - self._xpathCtxt = xpathCtxt - xpathCtxt.userData = self - # Need a cast here because older libxml2 releases do not use 'const' in the functype. - xpathCtxt.error = _receiveXPathError - - @cython.final - cdef _register_context(self, _Document doc): - self._doc = doc - self._exc.clear() - - @cython.final - cdef _cleanup_context(self): - #xpath.xmlXPathRegisteredNsCleanup(self._xpathCtxt) - #self.unregisterGlobalNamespaces() - if python.IS_PYPY: - # clean up double refs in PyPy (see "_to_utf()" method) - for ref in self._utf_refs.itervalues(): - python.Py_DECREF(ref) - self._utf_refs.clear() - self._eval_context_dict = None - self._doc = None - - @cython.final - cdef _release_context(self): - if self._xpathCtxt is not NULL: - self._xpathCtxt.userData = NULL - self._xpathCtxt = NULL - - # namespaces (internal UTF-8 methods with leading '_') - - cdef addNamespace(self, prefix, ns_uri): - cdef list namespaces - if prefix is None: - raise TypeError, "empty prefix is not supported in XPath" - prefix_utf = self._to_utf(prefix) - ns_uri_utf = self._to_utf(ns_uri) - new_item = (prefix_utf, ns_uri_utf) - if self._namespaces is None: - self._namespaces = [new_item] - else: - namespaces = [] - for item in self._namespaces: - if item[0] == prefix_utf: - item = new_item - new_item = None - namespaces.append(item) - if new_item is not None: - namespaces.append(new_item) - self._namespaces = namespaces - if self._xpathCtxt is not NULL: - xpath.xmlXPathRegisterNs( - self._xpathCtxt, _xcstr(prefix_utf), _xcstr(ns_uri_utf)) - - cdef registerNamespace(self, prefix, ns_uri): - if prefix is None: - raise TypeError, "empty prefix is not supported in XPath" - prefix_utf = self._to_utf(prefix) - ns_uri_utf = self._to_utf(ns_uri) - self._global_namespaces.append(prefix_utf) - xpath.xmlXPathRegisterNs(self._xpathCtxt, - _xcstr(prefix_utf), _xcstr(ns_uri_utf)) - - cdef registerLocalNamespaces(self): - if self._namespaces is None: - return - for prefix_utf, ns_uri_utf in self._namespaces: - xpath.xmlXPathRegisterNs( - self._xpathCtxt, _xcstr(prefix_utf), _xcstr(ns_uri_utf)) - - cdef registerGlobalNamespaces(self): - cdef list ns_prefixes = _find_all_extension_prefixes() - if python.PyList_GET_SIZE(ns_prefixes) > 0: - for prefix_utf, ns_uri_utf in ns_prefixes: - self._global_namespaces.append(prefix_utf) - xpath.xmlXPathRegisterNs( - self._xpathCtxt, _xcstr(prefix_utf), _xcstr(ns_uri_utf)) - - cdef unregisterGlobalNamespaces(self): - if python.PyList_GET_SIZE(self._global_namespaces) > 0: - for prefix_utf in self._global_namespaces: - xpath.xmlXPathRegisterNs(self._xpathCtxt, - _xcstr(prefix_utf), NULL) - del self._global_namespaces[:] - - cdef void _unregisterNamespace(self, prefix_utf) noexcept: - xpath.xmlXPathRegisterNs(self._xpathCtxt, - _xcstr(prefix_utf), NULL) - - # extension functions - - cdef int _addLocalExtensionFunction(self, ns_utf, name_utf, function) except -1: - if self._extensions is None: - self._extensions = {} - self._extensions[(ns_utf, name_utf)] = function - return 0 - - cdef registerGlobalFunctions(self, void* ctxt, - _register_function reg_func): - cdef python.PyObject* dict_result - cdef dict d - for ns_utf, ns_functions in __FUNCTION_NAMESPACE_REGISTRIES.iteritems(): - dict_result = python.PyDict_GetItem( - self._function_cache, ns_utf) - if dict_result is not NULL: - d = dict_result - else: - d = {} - self._function_cache[ns_utf] = d - for name_utf, function in ns_functions.iteritems(): - d[name_utf] = function - reg_func(ctxt, name_utf, ns_utf) - - cdef registerLocalFunctions(self, void* ctxt, - _register_function reg_func): - cdef python.PyObject* dict_result - cdef dict d - if self._extensions is None: - return # done - last_ns = None - d = None - for (ns_utf, name_utf), function in self._extensions.iteritems(): - if ns_utf is not last_ns or d is None: - last_ns = ns_utf - dict_result = python.PyDict_GetItem( - self._function_cache, ns_utf) - if dict_result is not NULL: - d = dict_result - else: - d = {} - self._function_cache[ns_utf] = d - d[name_utf] = function - reg_func(ctxt, name_utf, ns_utf) - - cdef unregisterAllFunctions(self, void* ctxt, - _register_function unreg_func): - for ns_utf, functions in self._function_cache.iteritems(): - for name_utf in functions: - unreg_func(ctxt, name_utf, ns_utf) - - cdef unregisterGlobalFunctions(self, void* ctxt, - _register_function unreg_func): - for ns_utf, functions in self._function_cache.items(): - for name_utf in functions: - if self._extensions is None or \ - (ns_utf, name_utf) not in self._extensions: - unreg_func(ctxt, name_utf, ns_utf) - - @cython.final - cdef _find_cached_function(self, const_xmlChar* c_ns_uri, const_xmlChar* c_name): - """Lookup an extension function in the cache and return it. - - Parameters: c_ns_uri may be NULL, c_name must not be NULL - """ - cdef python.PyObject* c_dict - cdef python.PyObject* dict_result - c_dict = python.PyDict_GetItem( - self._function_cache, None if c_ns_uri is NULL else c_ns_uri) - if c_dict is not NULL: - dict_result = python.PyDict_GetItem( - c_dict, c_name) - if dict_result is not NULL: - return dict_result - return None - - # Python access to the XPath context for extension functions - - @property - def context_node(self): - cdef xmlNode* c_node - if self._xpathCtxt is NULL: - raise XPathError, \ - "XPath context is only usable during the evaluation" - c_node = self._xpathCtxt.node - if c_node is NULL: - raise XPathError, "no context node" - if c_node.doc != self._xpathCtxt.doc: - raise XPathError, \ - "document-external context nodes are not supported" - if self._doc is None: - raise XPathError, "document context is missing" - return _elementFactory(self._doc, c_node) - - @property - def eval_context(self): - if self._eval_context_dict is None: - self._eval_context_dict = {} - return self._eval_context_dict - - # Python reference keeping during XPath function evaluation - - @cython.final - cdef _release_temp_refs(self): - "Free temporarily referenced objects from this context." - self._temp_refs.clear() - self._temp_documents.clear() - - @cython.final - cdef _hold(self, obj): - """A way to temporarily hold references to nodes in the evaluator. - - This is needed because otherwise nodes created in XPath extension - functions would be reference counted too soon, during the XPath - evaluation. This is most important in the case of exceptions. - """ - cdef _Element element - if isinstance(obj, _Element): - self._temp_refs.add(obj) - self._temp_documents.add((<_Element>obj)._doc) - return - elif _isString(obj) or not python.PySequence_Check(obj): - return - for o in obj: - if isinstance(o, _Element): - #print "Holding element:", element._c_node - self._temp_refs.add(o) - #print "Holding document:", element._doc._c_doc - self._temp_documents.add((<_Element>o)._doc) - - @cython.final - cdef _Document _findDocumentForNode(self, xmlNode* c_node): - """If an XPath expression returns an element from a different - document than the current context document, we call this to - see if it was possibly created by an extension and is a known - document instance. - """ - cdef _Document doc - for doc in self._temp_documents: - if doc is not None and doc._c_doc is c_node.doc: - return doc - return None - - -# libxml2 keeps these error messages in a static array in its code -# and doesn't give us access to them ... - -cdef tuple LIBXML2_XPATH_ERROR_MESSAGES = ( - b"Ok", - b"Number encoding", - b"Unfinished literal", - b"Start of literal", - b"Expected $ for variable reference", - b"Undefined variable", - b"Invalid predicate", - b"Invalid expression", - b"Missing closing curly brace", - b"Unregistered function", - b"Invalid operand", - b"Invalid type", - b"Invalid number of arguments", - b"Invalid context size", - b"Invalid context position", - b"Memory allocation error", - b"Syntax error", - b"Resource error", - b"Sub resource error", - b"Undefined namespace prefix", - b"Encoding error", - b"Char out of XML range", - b"Invalid or incomplete context", - b"Stack usage error", - b"Forbidden variable\n", - b"?? Unknown error ??\n", -) - -cdef void _forwardXPathError(void* c_ctxt, const xmlerror.xmlError* c_error) noexcept with gil: - cdef xmlerror.xmlError error - cdef int xpath_code - if c_error.message is not NULL: - error.message = c_error.message - else: - xpath_code = c_error.code - xmlerror.XML_XPATH_EXPRESSION_OK - if 0 <= xpath_code < len(LIBXML2_XPATH_ERROR_MESSAGES): - error.message = _cstr(LIBXML2_XPATH_ERROR_MESSAGES[xpath_code]) - else: - error.message = b"unknown error" - error.domain = c_error.domain - error.code = c_error.code - error.level = c_error.level - error.line = c_error.line - error.int2 = c_error.int1 # column - error.file = c_error.file - error.node = NULL - - (<_BaseContext>c_ctxt)._error_log._receive(&error) - -cdef void _receiveXPathError(void* c_context, const xmlerror.xmlError* error) noexcept nogil: - if not __DEBUG: - return - if c_context is NULL: - _forwardError(NULL, error) - else: - _forwardXPathError(c_context, error) - - -def Extension(module, function_mapping=None, *, ns=None): - """Extension(module, function_mapping=None, ns=None) - - Build a dictionary of extension functions from the functions - defined in a module or the methods of an object. - - As second argument, you can pass an additional mapping of - attribute names to XPath function names, or a list of function - names that should be taken. - - The ``ns`` keyword argument accepts a namespace URI for the XPath - functions. - """ - cdef dict functions = {} - if isinstance(function_mapping, dict): - for function_name, xpath_name in function_mapping.items(): - functions[(ns, xpath_name)] = getattr(module, function_name) - else: - if function_mapping is None: - function_mapping = [ name for name in dir(module) - if not name.startswith('_') ] - for function_name in function_mapping: - functions[(ns, function_name)] = getattr(module, function_name) - return functions - -################################################################################ -# EXSLT regexp implementation - -@cython.final -@cython.internal -cdef class _ExsltRegExp: - cdef dict _compile_map - def __cinit__(self): - self._compile_map = {} - - cdef _make_string(self, value): - if _isString(value): - return value - elif isinstance(value, list): - # node set: take recursive text concatenation of first element - if python.PyList_GET_SIZE(value) == 0: - return '' - firstnode = value[0] - if _isString(firstnode): - return firstnode - elif isinstance(firstnode, _Element): - c_text = tree.xmlNodeGetContent((<_Element>firstnode)._c_node) - if c_text is NULL: - raise MemoryError() - try: - return funicode(c_text) - finally: - tree.xmlFree(c_text) - else: - return unicode(firstnode) - else: - return unicode(value) - - cdef _compile(self, rexp, ignore_case): - cdef python.PyObject* c_result - rexp = self._make_string(rexp) - key = (rexp, ignore_case) - c_result = python.PyDict_GetItem(self._compile_map, key) - if c_result is not NULL: - return c_result - py_flags = re.UNICODE - if ignore_case: - py_flags = py_flags | re.IGNORECASE - rexp_compiled = re.compile(rexp, py_flags) - self._compile_map[key] = rexp_compiled - return rexp_compiled - - def test(self, ctxt, s, rexp, flags=''): - flags = self._make_string(flags) - s = self._make_string(s) - rexpc = self._compile(rexp, 'i' in flags) - if rexpc.search(s) is None: - return False - else: - return True - - def match(self, ctxt, s, rexp, flags=''): - cdef list result_list - flags = self._make_string(flags) - s = self._make_string(s) - rexpc = self._compile(rexp, 'i' in flags) - if 'g' in flags: - results = rexpc.findall(s) - if not results: - return () - else: - result = rexpc.search(s) - if not result: - return () - results = [ result.group() ] - results.extend( result.groups('') ) - result_list = [] - root = Element('matches') - for s_match in results: - if python.PyTuple_CheckExact(s_match): - s_match = ''.join(s_match) - elem = SubElement(root, 'match') - elem.text = s_match - result_list.append(elem) - return result_list - - def replace(self, ctxt, s, rexp, flags, replacement): - replacement = self._make_string(replacement) - flags = self._make_string(flags) - s = self._make_string(s) - rexpc = self._compile(rexp, 'i' in flags) - count: object = 0 if 'g' in flags else 1 - return rexpc.sub(replacement, s, count) - - cdef _register_in_context(self, _BaseContext context): - ns = b"http://exslt.org/regular-expressions" - context._addLocalExtensionFunction(ns, b"test", self.test) - context._addLocalExtensionFunction(ns, b"match", self.match) - context._addLocalExtensionFunction(ns, b"replace", self.replace) - - -################################################################################ -# helper functions - -cdef xpath.xmlXPathObject* _wrapXPathObject(object obj, _Document doc, - _BaseContext context) except NULL: - cdef xpath.xmlNodeSet* resultSet - cdef _Element fake_node = None - cdef xmlNode* c_node - - if isinstance(obj, unicode): - obj = _utf8(obj) - if isinstance(obj, bytes): - # libxml2 copies the string value - return xpath.xmlXPathNewCString(_cstr(obj)) - if isinstance(obj, bool): - return xpath.xmlXPathNewBoolean(obj) - if python.PyNumber_Check(obj): - return xpath.xmlXPathNewFloat(obj) - if obj is None: - resultSet = xpath.xmlXPathNodeSetCreate(NULL) - elif isinstance(obj, _Element): - resultSet = xpath.xmlXPathNodeSetCreate((<_Element>obj)._c_node) - elif python.PySequence_Check(obj): - resultSet = xpath.xmlXPathNodeSetCreate(NULL) - try: - for value in obj: - if isinstance(value, _Element): - if context is not None: - context._hold(value) - xpath.xmlXPathNodeSetAdd(resultSet, (<_Element>value)._c_node) - else: - if context is None or doc is None: - raise XPathResultError, \ - f"Non-Element values not supported at this point - got {value!r}" - # support strings by appending text nodes to an Element - if isinstance(value, unicode): - value = _utf8(value) - if isinstance(value, bytes): - if fake_node is None: - fake_node = _makeElement("text-root", NULL, doc, None, - None, None, None, None, None) - context._hold(fake_node) - else: - # append a comment node to keep the text nodes separate - c_node = tree.xmlNewDocComment(doc._c_doc, "") - if c_node is NULL: - raise MemoryError() - tree.xmlAddChild(fake_node._c_node, c_node) - context._hold(value) - c_node = tree.xmlNewDocText(doc._c_doc, _xcstr(value)) - if c_node is NULL: - raise MemoryError() - tree.xmlAddChild(fake_node._c_node, c_node) - xpath.xmlXPathNodeSetAdd(resultSet, c_node) - else: - raise XPathResultError, \ - f"This is not a supported node-set result: {value!r}" - except: - xpath.xmlXPathFreeNodeSet(resultSet) - raise - else: - raise XPathResultError, f"Unknown return type: {python._fqtypename(obj).decode('utf8')}" - return xpath.xmlXPathWrapNodeSet(resultSet) - -cdef object _unwrapXPathObject(xpath.xmlXPathObject* xpathObj, - _Document doc, _BaseContext context): - if xpathObj.type == xpath.XPATH_UNDEFINED: - raise XPathResultError, "Undefined xpath result" - elif xpathObj.type == xpath.XPATH_NODESET: - return _createNodeSetResult(xpathObj, doc, context) - elif xpathObj.type == xpath.XPATH_BOOLEAN: - return xpathObj.boolval - elif xpathObj.type == xpath.XPATH_NUMBER: - return xpathObj.floatval - elif xpathObj.type == xpath.XPATH_STRING: - stringval = funicode(xpathObj.stringval) - if context._build_smart_strings: - stringval = _elementStringResultFactory( - stringval, None, None, False) - return stringval - elif xpathObj.type == xpath.XPATH_POINT: - raise NotImplementedError, "XPATH_POINT" - elif xpathObj.type == xpath.XPATH_RANGE: - raise NotImplementedError, "XPATH_RANGE" - elif xpathObj.type == xpath.XPATH_LOCATIONSET: - raise NotImplementedError, "XPATH_LOCATIONSET" - elif xpathObj.type == xpath.XPATH_USERS: - raise NotImplementedError, "XPATH_USERS" - elif xpathObj.type == xpath.XPATH_XSLT_TREE: - return _createNodeSetResult(xpathObj, doc, context) - else: - raise XPathResultError, f"Unknown xpath result {xpathObj.type}" - -cdef object _createNodeSetResult(xpath.xmlXPathObject* xpathObj, _Document doc, - _BaseContext context): - cdef xmlNode* c_node - cdef int i - cdef list result - result = [] - if xpathObj.nodesetval is NULL: - return result - for i in range(xpathObj.nodesetval.nodeNr): - c_node = xpathObj.nodesetval.nodeTab[i] - _unpackNodeSetEntry(result, c_node, doc, context, - xpathObj.type == xpath.XPATH_XSLT_TREE) - return result - -cdef _unpackNodeSetEntry(list results, xmlNode* c_node, _Document doc, - _BaseContext context, bint is_fragment): - cdef xmlNode* c_child - if _isElement(c_node): - if c_node.doc != doc._c_doc and c_node.doc._private is NULL: - # XXX: works, but maybe not always the right thing to do? - # XPath: only runs when extensions create or copy trees - # -> we store Python refs to these, so that is OK - # XSLT: can it leak when merging trees from multiple sources? - c_node = tree.xmlDocCopyNode(c_node, doc._c_doc, 1) - if not c_node: - raise MemoryError() - # FIXME: call _instantiateElementFromXPath() instead? - results.append( - _fakeDocElementFactory(doc, c_node)) - elif c_node.type == tree.XML_TEXT_NODE or \ - c_node.type == tree.XML_CDATA_SECTION_NODE or \ - c_node.type == tree.XML_ATTRIBUTE_NODE: - results.append( - _buildElementStringResult(doc, c_node, context)) - elif c_node.type == tree.XML_NAMESPACE_DECL: - results.append( (funicodeOrNone((c_node).prefix), - funicodeOrNone((c_node).href)) ) - elif c_node.type == tree.XML_DOCUMENT_NODE or \ - c_node.type == tree.XML_HTML_DOCUMENT_NODE: - # ignored for everything but result tree fragments - if is_fragment: - c_child = c_node.children - while c_child is not NULL: - _unpackNodeSetEntry(results, c_child, doc, context, 0) - c_child = c_child.next - elif c_node.type == tree.XML_XINCLUDE_START or \ - c_node.type == tree.XML_XINCLUDE_END: - pass - else: - raise NotImplementedError, \ - f"Not yet implemented result node type: {c_node.type}" - -cdef void _freeXPathObject(xpath.xmlXPathObject* xpathObj) noexcept: - """Free the XPath object, but *never* free the *content* of node sets. - Python dealloc will do that for us. - """ - if xpathObj.nodesetval is not NULL: - xpath.xmlXPathFreeNodeSet(xpathObj.nodesetval) - xpathObj.nodesetval = NULL - xpath.xmlXPathFreeObject(xpathObj) - -cdef _Element _instantiateElementFromXPath(xmlNode* c_node, _Document doc, - _BaseContext context): - # NOTE: this may copy the element - only call this when it can't leak - if c_node.doc != doc._c_doc and c_node.doc._private is NULL: - # not from the context document and not from a fake document - # either => may still be from a known document, e.g. one - # created by an extension function - node_doc = context._findDocumentForNode(c_node) - if node_doc is None: - # not from a known document at all! => can only make a - # safety copy here - c_node = tree.xmlDocCopyNode(c_node, doc._c_doc, 1) - if not c_node: - raise MemoryError() - else: - doc = node_doc - return _fakeDocElementFactory(doc, c_node) - -################################################################################ -# special str/unicode subclasses - -@cython.final -cdef class _ElementUnicodeResult(unicode): - cdef _Element _parent - cdef readonly object attrname - cdef readonly bint is_tail - - def getparent(self): - return self._parent - - @property - def is_text(self): - return self._parent is not None and not (self.is_tail or self.attrname is not None) - - @property - def is_attribute(self): - return self.attrname is not None - -cdef object _elementStringResultFactory(string_value, _Element parent, - attrname, bint is_tail): - result = _ElementUnicodeResult(string_value) - result._parent = parent - result.is_tail = is_tail - result.attrname = attrname - return result - -cdef object _buildElementStringResult(_Document doc, xmlNode* c_node, - _BaseContext context): - cdef _Element parent = None - cdef object attrname = None - cdef xmlNode* c_element - cdef bint is_tail - - if c_node.type == tree.XML_ATTRIBUTE_NODE: - attrname = _namespacedName(c_node) - is_tail = 0 - s = tree.xmlNodeGetContent(c_node) - if s is NULL: - raise MemoryError() - try: - value = funicode(s) - finally: - tree.xmlFree(s) - c_element = NULL - else: - #assert c_node.type == tree.XML_TEXT_NODE or c_node.type == tree.XML_CDATA_SECTION_NODE, "invalid node type" - # may be tail text or normal text - value = funicode(c_node.content) - c_element = _previousElement(c_node) - is_tail = c_element is not NULL - - if not context._build_smart_strings: - return value - - if c_element is NULL: - # non-tail text or attribute text - c_element = c_node.parent - while c_element is not NULL and not _isElement(c_element): - c_element = c_element.parent - - if c_element is not NULL: - parent = _instantiateElementFromXPath(c_element, doc, context) - - return _elementStringResultFactory( - value, parent, attrname, is_tail) - -################################################################################ -# callbacks for XPath/XSLT extension functions - -cdef void _extension_function_call(_BaseContext context, function, - xpath.xmlXPathParserContext* ctxt, int nargs) noexcept: - cdef _Document doc - cdef xpath.xmlXPathObject* obj - cdef list args - cdef int i - doc = context._doc - try: - args = [] - for i in range(nargs): - obj = xpath.valuePop(ctxt) - try: - o = _unwrapXPathObject(obj, doc, context) - finally: - _freeXPathObject(obj) - args.append(o) - args.reverse() - - res = function(context, *args) - # wrap result for XPath consumption - obj = _wrapXPathObject(res, doc, context) - # prevent Python from deallocating elements handed to libxml2 - context._hold(res) - xpath.valuePush(ctxt, obj) - except: - xpath.xmlXPathErr(ctxt, xpath.XPATH_EXPR_ERROR) - context._exc._store_raised() - finally: - return # swallow any further exceptions - -# lookup the function by name and call it - -cdef void _xpath_function_call(xpath.xmlXPathParserContext* ctxt, - int nargs) noexcept with gil: - cdef _BaseContext context - cdef xpath.xmlXPathContext* rctxt = ctxt.context - context = <_BaseContext> rctxt.userData - try: - function = context._find_cached_function(rctxt.functionURI, rctxt.function) - if function is not None: - _extension_function_call(context, function, ctxt, nargs) - else: - xpath.xmlXPathErr(ctxt, xpath.XPATH_UNKNOWN_FUNC_ERROR) - context._exc._store_exception(XPathFunctionError( - f"XPath function '{_namespacedNameFromNsName(rctxt.functionURI, rctxt.function)}' not found")) - except: - # may not be the right error, but we need to tell libxml2 *something* - xpath.xmlXPathErr(ctxt, xpath.XPATH_UNKNOWN_FUNC_ERROR) - context._exc._store_raised() - finally: - return # swallow any further exceptions diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/html/ElementSoup.py b/presentation/.venv/lib/python3.12/site-packages/lxml/html/ElementSoup.py deleted file mode 100644 index c35365d..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/html/ElementSoup.py +++ /dev/null @@ -1,10 +0,0 @@ -__doc__ = """Legacy interface to the BeautifulSoup HTML parser. -""" - -__all__ = ["parse", "convert_tree"] - -from .soupparser import convert_tree, parse as _parse - -def parse(file, beautifulsoup=None, makeelement=None): - root = _parse(file, beautifulsoup=beautifulsoup, makeelement=makeelement) - return root.getroot() diff --git a/presentation/.venv/lib/python3.12/site-packages/lxml/html/__init__.py b/presentation/.venv/lib/python3.12/site-packages/lxml/html/__init__.py deleted file mode 100644 index 2cee9f4..0000000 --- a/presentation/.venv/lib/python3.12/site-packages/lxml/html/__init__.py +++ /dev/null @@ -1,1927 +0,0 @@ -# Copyright (c) 2004 Ian Bicking. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in -# the documentation and/or other materials provided with the -# distribution. -# -# 3. Neither the name of Ian Bicking nor the names of its contributors may -# be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL IAN BICKING OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""The ``lxml.html`` tool set for HTML handling. -""" - - -__all__ = [ - 'document_fromstring', 'fragment_fromstring', 'fragments_fromstring', 'fromstring', - 'tostring', 'Element', 'defs', 'open_in_browser', 'submit_form', - 'find_rel_links', 'find_class', 'make_links_absolute', - 'resolve_base_href', 'iterlinks', 'rewrite_links', 'parse'] - - -import copy -import re - -from collections.abc import MutableMapping, MutableSet -from functools import partial -from urllib.parse import urljoin - -from .. import etree -from . import defs -from ._setmixin import SetMixin - - -def __fix_docstring(s): - # TODO: remove and clean up doctests - if not s: - return s - sub = re.compile(r"^(\s*)u'", re.M).sub - return sub(r"\1'", s) - - -XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml" - -_rel_links_xpath = etree.XPath("descendant-or-self::a[@rel]|descendant-or-self::x:a[@rel]", - namespaces={'x':XHTML_NAMESPACE}) -_options_xpath = etree.XPath("descendant-or-self::option|descendant-or-self::x:option", - namespaces={'x':XHTML_NAMESPACE}) -_forms_xpath = etree.XPath("descendant-or-self::form|descendant-or-self::x:form", - namespaces={'x':XHTML_NAMESPACE}) -#_class_xpath = etree.XPath(r"descendant-or-self::*[regexp:match(@class, concat('\b', $class_name, '\b'))]", {'regexp': 'http://exslt.org/regular-expressions'}) -_class_xpath = etree.XPath("descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), concat(' ', $class_name, ' '))]") -_id_xpath = etree.XPath("descendant-or-self::*[@id=$id]") -_collect_string_content = etree.XPath("string()", smart_strings=False) -_iter_css_urls = re.compile(r'url\(('+'["][^"]*["]|'+"['][^']*[']|"+r'[^)]*)\)', re.I).finditer -_iter_css_imports = re.compile(r'@import "(.*?)"').finditer -_label_xpath = etree.XPath("//label[@for=$id]|//x:label[@for=$id]", - namespaces={'x':XHTML_NAMESPACE}) -_archive_re = re.compile(r'[^ ]+') -_parse_meta_refresh_url = re.compile( - r'[^;=]*;\s*(?:url\s*=\s*)?(?P.*)$', re.I).search - - -def _unquote_match(s, pos): - if s[:1] == '"' and s[-1:] == '"' or s[:1] == "'" and s[-1:] == "'": - return s[1:-1], pos+1 - else: - return s,pos - - -def _transform_result(typ, result): - """Convert the result back into the input type. - """ - if issubclass(typ, bytes): - return tostring(result, encoding='utf-8') - elif issubclass(typ, str): - return tostring(result, encoding='unicode') - else: - return result - - -def _nons(tag): - if isinstance(tag, str): - if tag[0] == '{' and tag[1:len(XHTML_NAMESPACE)+1] == XHTML_NAMESPACE: - return tag.split('}')[-1] - return tag - - -class Classes(MutableSet): - """Provides access to an element's class attribute as a set-like collection. - Usage:: - - >>> el = fromstring('') - >>> classes = el.classes # or: classes = Classes(el.attrib) - >>> classes |= ['block', 'paragraph'] - >>> el.get('class') - 'hidden large block paragraph' - >>> classes.toggle('hidden') - False - >>> el.get('class') - 'large block paragraph' - >>> classes -= ('some', 'classes', 'block') - >>> el.get('class') - 'large paragraph' - """ - def __init__(self, attributes): - self._attributes = attributes - self._get_class_value = partial(attributes.get, 'class', '') - - def add(self, value): - """ - Add a class. - - This has no effect if the class is already present. - """ - if not value or re.search(r'\s', value): - raise ValueError("Invalid class name: %r" % value) - classes = self._get_class_value().split() - if value in classes: - return - classes.append(value) - self._attributes['class'] = ' '.join(classes) - - def discard(self, value): - """ - Remove a class if it is currently present. - - If the class is not present, do nothing. - """ - if not value or re.search(r'\s', value): - raise ValueError("Invalid class name: %r" % value) - classes = [name for name in self._get_class_value().split() - if name != value] - if classes: - self._attributes['class'] = ' '.join(classes) - elif 'class' in self._attributes: - del self._attributes['class'] - - def remove(self, value): - """ - Remove a class; it must currently be present. - - If the class is not present, raise a KeyError. - """ - if not value or re.search(r'\s', value): - raise ValueError("Invalid class name: %r" % value) - super().remove(value) - - def __contains__(self, name): - classes = self._get_class_value() - return name in classes and name in classes.split() - - def __iter__(self): - return iter(self._get_class_value().split()) - - def __len__(self): - return len(self._get_class_value().split()) - - # non-standard methods - - def update(self, values): - """ - Add all names from 'values'. - """ - classes = self._get_class_value().split() - extended = False - for value in values: - if value not in classes: - classes.append(value) - extended = True - if extended: - self._attributes['class'] = ' '.join(classes) - - def toggle(self, value): - """ - Add a class name if it isn't there yet, or remove it if it exists. - - Returns true if the class was added (and is now enabled) and - false if it was removed (and is now disabled). - """ - if not value or re.search(r'\s', value): - raise ValueError("Invalid class name: %r" % value) - classes = self._get_class_value().split() - try: - classes.remove(value) - enabled = False - except ValueError: - classes.append(value) - enabled = True - if classes: - self._attributes['class'] = ' '.join(classes) - else: - del self._attributes['class'] - return enabled - - -class HtmlMixin: - - def set(self, key, value=None): - """set(self, key, value=None) - - Sets an element attribute. If no value is provided, or if the value is None, - creates a 'boolean' attribute without value, e.g. "
" - for ``form.set('novalidate')``. - """ - super().set(key, value) - - @property - def classes(self): - """ - A set-like wrapper around the 'class' attribute. - """ - return Classes(self.attrib) - - @classes.setter - def classes(self, classes): - assert isinstance(classes, Classes) # only allow "el.classes |= ..." etc. - value = classes._get_class_value() - if value: - self.set('class', value) - elif self.get('class') is not None: - del self.attrib['class'] - - @property - def base_url(self): - """ - Returns the base URL, given when the page was parsed. - - Use with ``urlparse.urljoin(el.base_url, href)`` to get - absolute URLs. - """ - return self.getroottree().docinfo.URL - - @property - def forms(self): - """ - Return a list of all the forms - """ - return _forms_xpath(self) - - @property - def body(self): - """ - Return the element. Can be called from a child element - to get the document's head. - """ - for element in self.getroottree().iter("body", f"{{{XHTML_NAMESPACE}}}body"): - return element - return None - - @property - def head(self): - """ - Returns the element. Can be called from a child - element to get the document's head. - """ - for element in self.getroottree().iter("head", f"{{{XHTML_NAMESPACE}}}head"): - return element - return None - - @property - def label(self): - """ - Get or set any